Testprojekt hinzugefügt & umfassende Server-Tests
Neues Testprojekt SlipItIn.Server.Tests erstellt und in die Solution eingebunden. Unit- und Integrationstests für AuthController, DbSeeder, GameHub und GameService implementiert. TestDbHelper für InMemory-DbContexts und Testdaten hinzugefügt. Tests prüfen Erfolgs- und Fehlerfälle inkl. Validierungen, Fehlerbehandlung und DB-Zustände.
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
<Solution>
|
<Solution>
|
||||||
<Project Path="SlipItIN.AppHost/SlipItIn.AppHost.csproj" />
|
<Project Path="SlipItIN.AppHost/SlipItIn.AppHost.csproj" />
|
||||||
|
<Project Path="SlipItIn.Server.Tests/SlipItIn.Server.Tests.csproj" />
|
||||||
<Project Path="SlipItIn.Server/SlipItIn.Server.csproj" />
|
<Project Path="SlipItIn.Server/SlipItIn.Server.csproj" />
|
||||||
<Project Path="SlipItIN.ServiceDefaults/SlipItIn.ServiceDefaults.csproj" />
|
<Project Path="SlipItIN.ServiceDefaults/SlipItIn.ServiceDefaults.csproj" />
|
||||||
<Project Path="SlipItIn.Shared/SlipItIn.Shared.csproj" />
|
<Project Path="SlipItIn.Shared/SlipItIn.Shared.csproj" />
|
||||||
|
|||||||
203
SlipItIn.Server.Tests/AuthControllerTests.cs
Normal file
203
SlipItIn.Server.Tests/AuthControllerTests.cs
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using SlipItIn.Server.Controllers;
|
||||||
|
using SlipItIn.Server.Data;
|
||||||
|
using SlipItIn.Shared.DTOs;
|
||||||
|
using SlipItIn.Shared.Models;
|
||||||
|
using System.IdentityModel.Tokens.Jwt;
|
||||||
|
using System.Security.Claims;
|
||||||
|
|
||||||
|
namespace SlipItIn.Server.Tests;
|
||||||
|
|
||||||
|
public class AuthControllerTests
|
||||||
|
{
|
||||||
|
private readonly IDbContextFactory<SlipItInDbContext> _factory = TestDbHelper.CreateFactory();
|
||||||
|
private readonly AuthController _sut;
|
||||||
|
|
||||||
|
public AuthControllerTests()
|
||||||
|
{
|
||||||
|
var config = new ConfigurationBuilder()
|
||||||
|
.AddInMemoryCollection(new Dictionary<string, string?>
|
||||||
|
{
|
||||||
|
["Jwt:Key"] = "TestKeyThatIsAtLeast32CharactersLong!!",
|
||||||
|
["Jwt:Issuer"] = "TestIssuer",
|
||||||
|
["Jwt:Audience"] = "TestAudience",
|
||||||
|
["Jwt:ExpirationMinutes"] = "60"
|
||||||
|
})
|
||||||
|
.Build();
|
||||||
|
_sut = new AuthController(_factory, config);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Register ----------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Register_ReturnsOk_WithTokenAndUserData()
|
||||||
|
{
|
||||||
|
var result = await _sut.Register(new RegisterRequestDto
|
||||||
|
{
|
||||||
|
Username = " Alice ",
|
||||||
|
Email = " ALICE@Test.Local ",
|
||||||
|
Password = "Secret123!"
|
||||||
|
});
|
||||||
|
|
||||||
|
var ok = Assert.IsType<OkObjectResult>(result.Result);
|
||||||
|
var dto = Assert.IsType<AuthResponseDto>(ok.Value);
|
||||||
|
Assert.Equal("Alice", dto.Username); // getrimmt
|
||||||
|
Assert.Equal("alice@test.local", dto.Email); // getrimmt + lowercase
|
||||||
|
Assert.True(dto.UserId > 0);
|
||||||
|
Assert.False(string.IsNullOrWhiteSpace(dto.Token));
|
||||||
|
Assert.True(dto.ExpiresAtUtc > DateTime.UtcNow);
|
||||||
|
|
||||||
|
// Token enthält die erwarteten Claims und ist valide lesbar
|
||||||
|
var token = new JwtSecurityTokenHandler().ReadJwtToken(dto.Token);
|
||||||
|
Assert.Equal("TestIssuer", token.Issuer);
|
||||||
|
Assert.Contains("TestAudience", token.Audiences);
|
||||||
|
Assert.Equal(dto.UserId.ToString(), token.Claims.First(c => c.Type == ClaimTypes.NameIdentifier).Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("", "a@b.c", "pw")]
|
||||||
|
[InlineData("user", "", "pw")]
|
||||||
|
[InlineData("user", "a@b.c", "")]
|
||||||
|
[InlineData(" ", "a@b.c", "pw")]
|
||||||
|
public async Task Register_ReturnsBadRequest_WhenFieldsMissing(string username, string email, string password)
|
||||||
|
{
|
||||||
|
var result = await _sut.Register(new RegisterRequestDto { Username = username, Email = email, Password = password });
|
||||||
|
|
||||||
|
Assert.IsType<BadRequestObjectResult>(result.Result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Register_ReturnsConflict_WhenEmailExists_CaseInsensitive()
|
||||||
|
{
|
||||||
|
using var db = _factory.CreateDbContext();
|
||||||
|
TestDbHelper.SeedUser(db, "existing", "alice@test.local");
|
||||||
|
|
||||||
|
var result = await _sut.Register(new RegisterRequestDto
|
||||||
|
{
|
||||||
|
Username = "newuser",
|
||||||
|
Email = "ALICE@TEST.LOCAL",
|
||||||
|
Password = "Secret123!"
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.IsType<ConflictObjectResult>(result.Result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Register_ReturnsConflict_WhenUsernameExists_CaseInsensitive()
|
||||||
|
{
|
||||||
|
using var db = _factory.CreateDbContext();
|
||||||
|
TestDbHelper.SeedUser(db, "alice", "other@test.local");
|
||||||
|
|
||||||
|
var result = await _sut.Register(new RegisterRequestDto
|
||||||
|
{
|
||||||
|
Username = "ALICE",
|
||||||
|
Email = "new@test.local",
|
||||||
|
Password = "Secret123!"
|
||||||
|
});
|
||||||
|
|
||||||
|
Assert.IsType<ConflictObjectResult>(result.Result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Login ----------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Login_ReturnsOk_WithValidCredentials()
|
||||||
|
{
|
||||||
|
using var db = _factory.CreateDbContext();
|
||||||
|
db.Users.Add(new User
|
||||||
|
{
|
||||||
|
Username = "bob",
|
||||||
|
Email = "bob@test.local",
|
||||||
|
PasswordHash = BCrypt.Net.BCrypt.HashPassword("Secret123!")
|
||||||
|
});
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
var result = await _sut.Login(new LoginRequestDto { Email = " BOB@test.local ", Password = "Secret123!" });
|
||||||
|
|
||||||
|
var ok = Assert.IsType<OkObjectResult>(result.Result);
|
||||||
|
var dto = Assert.IsType<AuthResponseDto>(ok.Value);
|
||||||
|
Assert.Equal("bob", dto.Username);
|
||||||
|
Assert.False(string.IsNullOrWhiteSpace(dto.Token));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData("", "pw")]
|
||||||
|
[InlineData("a@b.c", "")]
|
||||||
|
public async Task Login_ReturnsBadRequest_WhenFieldsMissing(string email, string password)
|
||||||
|
{
|
||||||
|
var result = await _sut.Login(new LoginRequestDto { Email = email, Password = password });
|
||||||
|
|
||||||
|
Assert.IsType<BadRequestObjectResult>(result.Result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Login_ReturnsUnauthorized_WhenUserNotFound()
|
||||||
|
{
|
||||||
|
var result = await _sut.Login(new LoginRequestDto { Email = "ghost@test.local", Password = "Secret123!" });
|
||||||
|
|
||||||
|
Assert.IsType<UnauthorizedObjectResult>(result.Result);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task Login_ReturnsUnauthorized_WhenPasswordWrong()
|
||||||
|
{
|
||||||
|
using var db = _factory.CreateDbContext();
|
||||||
|
db.Users.Add(new User
|
||||||
|
{
|
||||||
|
Username = "bob",
|
||||||
|
Email = "bob@test.local",
|
||||||
|
PasswordHash = BCrypt.Net.BCrypt.HashPassword("CorrectPassword")
|
||||||
|
});
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
var result = await _sut.Login(new LoginRequestDto { Email = "bob@test.local", Password = "WrongPassword" });
|
||||||
|
|
||||||
|
Assert.IsType<UnauthorizedObjectResult>(result.Result);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- Me ----------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Me_ReturnsUserData_FromClaims()
|
||||||
|
{
|
||||||
|
var identity = new ClaimsIdentity([
|
||||||
|
new Claim(ClaimTypes.NameIdentifier, "42"),
|
||||||
|
new Claim(ClaimTypes.Name, "alice"),
|
||||||
|
new Claim(ClaimTypes.Email, "alice@test.local")
|
||||||
|
], "TestAuth");
|
||||||
|
_sut.ControllerContext = new ControllerContext
|
||||||
|
{
|
||||||
|
HttpContext = new Microsoft.AspNetCore.Http.DefaultHttpContext
|
||||||
|
{
|
||||||
|
User = new ClaimsPrincipal(identity)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = _sut.Me();
|
||||||
|
|
||||||
|
var ok = Assert.IsType<OkObjectResult>(result.Result);
|
||||||
|
var value = ok.Value!;
|
||||||
|
Assert.Equal(42, (int)value.GetType().GetProperty("UserId")!.GetValue(value)!);
|
||||||
|
Assert.Equal("alice", (string)value.GetType().GetProperty("Username")!.GetValue(value)!);
|
||||||
|
Assert.Equal("alice@test.local", (string)value.GetType().GetProperty("Email")!.GetValue(value)!);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Me_ReturnsUnauthorized_WhenNameIdentifierClaimMissing()
|
||||||
|
{
|
||||||
|
var identity = new ClaimsIdentity([new Claim(ClaimTypes.Name, "alice")], "TestAuth");
|
||||||
|
_sut.ControllerContext = new ControllerContext
|
||||||
|
{
|
||||||
|
HttpContext = new Microsoft.AspNetCore.Http.DefaultHttpContext
|
||||||
|
{
|
||||||
|
User = new ClaimsPrincipal(identity)
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var result = _sut.Me();
|
||||||
|
|
||||||
|
Assert.IsType<UnauthorizedResult>(result.Result);
|
||||||
|
}
|
||||||
|
}
|
||||||
61
SlipItIn.Server.Tests/DbSeederTests.cs
Normal file
61
SlipItIn.Server.Tests/DbSeederTests.cs
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
using SlipItIn.Server.Data;
|
||||||
|
|
||||||
|
namespace SlipItIn.Server.Tests;
|
||||||
|
|
||||||
|
public class DbSeederTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task SeedAsync_SeedsUsersAndPhrases_WhenDatabaseIsEmpty()
|
||||||
|
{
|
||||||
|
var factory = TestDbHelper.CreateFactory();
|
||||||
|
using var db = factory.CreateDbContext();
|
||||||
|
|
||||||
|
await DbSeeder.SeedAsync(db);
|
||||||
|
|
||||||
|
Assert.Equal(2, db.Users.Count());
|
||||||
|
Assert.True(db.Phrases.Count() >= 5);
|
||||||
|
Assert.All(db.Phrases, p => Assert.True(p.IsActive));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SeedAsync_DoesNothing_WhenUsersAlreadyExist()
|
||||||
|
{
|
||||||
|
var factory = TestDbHelper.CreateFactory();
|
||||||
|
using var db = factory.CreateDbContext();
|
||||||
|
TestDbHelper.SeedUser(db);
|
||||||
|
|
||||||
|
await DbSeeder.SeedAsync(db);
|
||||||
|
|
||||||
|
Assert.Equal(1, db.Users.Count());
|
||||||
|
Assert.Empty(db.Phrases);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SeedAsync_DoesNothing_WhenPhrasesAlreadyExist()
|
||||||
|
{
|
||||||
|
var factory = TestDbHelper.CreateFactory();
|
||||||
|
using var db = factory.CreateDbContext();
|
||||||
|
TestDbHelper.SeedPhrases(db, 1);
|
||||||
|
|
||||||
|
await DbSeeder.SeedAsync(db);
|
||||||
|
|
||||||
|
Assert.Equal(1, db.Users.Count()); // nur der Creator-User des Phrase-Seedings
|
||||||
|
Assert.Equal(1, db.Phrases.Count());
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SeedAsync_IsIdempotent_WhenCalledTwice()
|
||||||
|
{
|
||||||
|
var factory = TestDbHelper.CreateFactory();
|
||||||
|
using var db = factory.CreateDbContext();
|
||||||
|
|
||||||
|
await DbSeeder.SeedAsync(db);
|
||||||
|
var userCount = db.Users.Count();
|
||||||
|
var phraseCount = db.Phrases.Count();
|
||||||
|
|
||||||
|
await DbSeeder.SeedAsync(db);
|
||||||
|
|
||||||
|
Assert.Equal(userCount, db.Users.Count());
|
||||||
|
Assert.Equal(phraseCount, db.Phrases.Count());
|
||||||
|
}
|
||||||
|
}
|
||||||
606
SlipItIn.Server.Tests/GameHubTests.cs
Normal file
606
SlipItIn.Server.Tests/GameHubTests.cs
Normal file
@@ -0,0 +1,606 @@
|
|||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using Moq;
|
||||||
|
using SlipItIn.Server.Data;
|
||||||
|
using SlipItIn.Server.Hubs;
|
||||||
|
using SlipItIn.Server.Services;
|
||||||
|
using SlipItIn.Shared.DTOs;
|
||||||
|
using SlipItIn.Shared.Models;
|
||||||
|
using System.Security.Claims;
|
||||||
|
|
||||||
|
namespace SlipItIn.Server.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Tests für den GameHub mit gemocktem IGameService und gemockten SignalR-Abhängigkeiten.
|
||||||
|
/// Echte DB (InMemory) wird nur für die Validierungs-Methoden des Hubs verwendet.
|
||||||
|
/// </summary>
|
||||||
|
public class GameHubTests
|
||||||
|
{
|
||||||
|
private readonly IDbContextFactory<SlipItInDbContext> _factory = TestDbHelper.CreateFactory();
|
||||||
|
private readonly Mock<IGameService> _gameService = new();
|
||||||
|
private readonly Mock<IGroupManager> _groups = new();
|
||||||
|
private readonly Mock<IHubCallerClients> _clients = new();
|
||||||
|
private readonly Mock<ISingleClientProxy> _callerProxy = new();
|
||||||
|
private readonly Mock<IClientProxy> _groupProxy = new();
|
||||||
|
private readonly Mock<ISingleClientProxy> _clientProxy = new();
|
||||||
|
private readonly GameHub _hub;
|
||||||
|
|
||||||
|
private const string ConnectionId = "test-connection-id";
|
||||||
|
private int _userId = 1;
|
||||||
|
|
||||||
|
public GameHubTests()
|
||||||
|
{
|
||||||
|
_clients.Setup(c => c.Caller).Returns(_callerProxy.Object);
|
||||||
|
_clients.Setup(c => c.Group(It.IsAny<string>())).Returns(_groupProxy.Object);
|
||||||
|
_clients.Setup(c => c.Client(It.IsAny<string>())).Returns(_clientProxy.Object);
|
||||||
|
|
||||||
|
_hub = new GameHub(_gameService.Object, _factory, NullLogger<GameHub>.Instance);
|
||||||
|
SetupContext();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetupContext()
|
||||||
|
{
|
||||||
|
var identity = new ClaimsIdentity([new Claim(ClaimTypes.NameIdentifier, _userId.ToString())], "TestAuth");
|
||||||
|
var context = new Mock<HubCallerContext>();
|
||||||
|
context.Setup(c => c.ConnectionId).Returns(ConnectionId);
|
||||||
|
context.Setup(c => c.User).Returns(new ClaimsPrincipal(identity));
|
||||||
|
_hub.Context = context.Object;
|
||||||
|
_hub.Clients = _clients.Object;
|
||||||
|
_hub.Groups = _groups.Object;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void WithoutAuth()
|
||||||
|
{
|
||||||
|
var context = new Mock<HubCallerContext>();
|
||||||
|
context.Setup(c => c.ConnectionId).Returns(ConnectionId);
|
||||||
|
context.Setup(c => c.User).Returns((ClaimsPrincipal?)null);
|
||||||
|
_hub.Context = context.Object;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? ErrorMessageOf(Mock<ISingleClientProxy> proxy)
|
||||||
|
{
|
||||||
|
// Die Aufrufe kommen als SendCoreAsync(string, object?[]) mit dem Payload in args[1] rein.
|
||||||
|
var invocation = proxy.Invocations.FirstOrDefault(i => i.Arguments.Count >= 2 && i.Arguments[0] is string m && m == "Error");
|
||||||
|
if (invocation == null) return null;
|
||||||
|
var payload = invocation.Arguments[1];
|
||||||
|
// Payload kann ein anonymes Objekt sein oder ein object?[] mit einem anonymen Objekt drin.
|
||||||
|
if (payload is object?[] arr && arr.Length > 0) payload = arr[0];
|
||||||
|
return payload?.GetType().GetProperty("Message")?.GetValue(payload) as string;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Game SeedGameWithPlayers(params (User user, string? connId)[] players)
|
||||||
|
{
|
||||||
|
using var db = _factory.CreateDbContext();
|
||||||
|
var game = new Game { LobbyCode = "ABC123", HostId = players[0].user.Id, Status = GameStatus.Lobby };
|
||||||
|
db.Games.Add(game);
|
||||||
|
foreach (var (user, connId) in players)
|
||||||
|
{
|
||||||
|
db.Players.Add(new Player { UserId = user.Id, Game = game, ConnectionId = connId });
|
||||||
|
}
|
||||||
|
db.SaveChanges();
|
||||||
|
return game;
|
||||||
|
}
|
||||||
|
|
||||||
|
private (User user, Player player) SeedUserWithPlayer(Game game)
|
||||||
|
{
|
||||||
|
using var db = _factory.CreateDbContext();
|
||||||
|
var user = TestDbHelper.SeedUser(db, Guid.NewGuid().ToString("N")[..8], Guid.NewGuid().ToString("N")[..8] + "@t.local");
|
||||||
|
var player = new Player { UserId = user.Id, GameId = game.Id };
|
||||||
|
db.Players.Add(player);
|
||||||
|
db.SaveChanges();
|
||||||
|
return (user, player);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- CreateLobby ----------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateLobby_AddsCallerToGroupAndSendsLobbyCreated()
|
||||||
|
{
|
||||||
|
using var db = _factory.CreateDbContext();
|
||||||
|
var user = TestDbHelper.SeedUser(db);
|
||||||
|
_userId = user.Id;
|
||||||
|
SetupContext();
|
||||||
|
|
||||||
|
var game = new Game { Id = 7, LobbyCode = "XYZ789", HostId = user.Id };
|
||||||
|
_gameService.Setup(s => s.CreateGameAsync(user.Id, ConnectionId)).ReturnsAsync(game);
|
||||||
|
|
||||||
|
await _hub.CreateLobby();
|
||||||
|
|
||||||
|
_groups.Verify(g => g.AddToGroupAsync(ConnectionId, "XYZ789", default), Times.Once);
|
||||||
|
_callerProxy.Verify(p => p.SendCoreAsync("LobbyCreated", It.Is<object?[]>(a => a.Length == 1), default), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateLobby_SendsUnauthorized_WhenNotAuthenticated()
|
||||||
|
{
|
||||||
|
WithoutAuth();
|
||||||
|
|
||||||
|
await _hub.CreateLobby();
|
||||||
|
|
||||||
|
Assert.Equal("Unauthorized", ErrorMessageOf(_callerProxy));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateLobby_SendsGenericError_WhenServiceThrows()
|
||||||
|
{
|
||||||
|
_gameService.Setup(s => s.CreateGameAsync(It.IsAny<int>(), It.IsAny<string?>()))
|
||||||
|
.ThrowsAsync(new InvalidOperationException("Host not found"));
|
||||||
|
|
||||||
|
await _hub.CreateLobby();
|
||||||
|
|
||||||
|
Assert.Equal("An error occurred while creating the lobby.", ErrorMessageOf(_callerProxy));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- JoinLobby ----------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task JoinLobby_AddsToGroupAndBroadcastsPlayerJoined()
|
||||||
|
{
|
||||||
|
using var db = _factory.CreateDbContext();
|
||||||
|
var user = TestDbHelper.SeedUser(db);
|
||||||
|
_userId = user.Id;
|
||||||
|
SetupContext();
|
||||||
|
|
||||||
|
var game = new Game { Id = 3, LobbyCode = "JOIN42", HostId = 99 };
|
||||||
|
_gameService.Setup(s => s.JoinGameAsync("JOIN42", user.Id, ConnectionId)).ReturnsAsync(game);
|
||||||
|
_gameService.Setup(s => s.GetGameStateAsync(game.Id)).ReturnsAsync(new GameStateDto { GameId = 3 });
|
||||||
|
|
||||||
|
await _hub.JoinLobby("JOIN42");
|
||||||
|
|
||||||
|
_groups.Verify(g => g.AddToGroupAsync(ConnectionId, "JOIN42", default), Times.Once);
|
||||||
|
_groupProxy.Verify(p => p.SendCoreAsync("PlayerJoined", It.IsAny<object?[]>(), default), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task JoinLobby_SendsUnauthorized_WhenNotAuthenticated()
|
||||||
|
{
|
||||||
|
WithoutAuth();
|
||||||
|
|
||||||
|
await _hub.JoinLobby("ANY");
|
||||||
|
|
||||||
|
Assert.Equal("Unauthorized", ErrorMessageOf(_callerProxy));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task JoinLobby_SendsServiceMessage_WhenGameFullOrStarted()
|
||||||
|
{
|
||||||
|
_gameService.Setup(s => s.JoinGameAsync(It.IsAny<string>(), It.IsAny<int>(), It.IsAny<string?>()))
|
||||||
|
.ThrowsAsync(new InvalidOperationException("Game is full"));
|
||||||
|
|
||||||
|
await _hub.JoinLobby("FULL1");
|
||||||
|
|
||||||
|
Assert.Equal("Game is full", ErrorMessageOf(_callerProxy));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task JoinLobby_SendsGenericError_WhenUnexpectedException()
|
||||||
|
{
|
||||||
|
_gameService.Setup(s => s.JoinGameAsync(It.IsAny<string>(), It.IsAny<int>(), It.IsAny<string?>()))
|
||||||
|
.ThrowsAsync(new Exception("boom"));
|
||||||
|
|
||||||
|
await _hub.JoinLobby("ANY");
|
||||||
|
|
||||||
|
Assert.Equal("An error occurred while joining the lobby.", ErrorMessageOf(_callerProxy));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- PlayerReady ----------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PlayerReady_BroadcastsGameState_WhenValid()
|
||||||
|
{
|
||||||
|
using var db = _factory.CreateDbContext();
|
||||||
|
var user = TestDbHelper.SeedUser(db);
|
||||||
|
_userId = user.Id;
|
||||||
|
SetupContext();
|
||||||
|
var game = SeedGameWithPlayers((user, ConnectionId));
|
||||||
|
var player = db.Players.Single(p => p.GameId == game.Id);
|
||||||
|
|
||||||
|
_gameService.Setup(s => s.SetPlayerReadyAsync(game.Id, player.Id))
|
||||||
|
.ReturnsAsync(new Game { Id = game.Id, LobbyCode = game.LobbyCode });
|
||||||
|
_gameService.Setup(s => s.GetGameStateAsync(game.Id)).ReturnsAsync(new GameStateDto { GameId = game.Id });
|
||||||
|
|
||||||
|
await _hub.PlayerReady(game.Id, player.Id);
|
||||||
|
|
||||||
|
_groupProxy.Verify(p => p.SendCoreAsync("GameStateUpdated", It.IsAny<object?[]>(), default), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PlayerReady_SendsError_WhenPlayerBelongsToOtherUser()
|
||||||
|
{
|
||||||
|
using var db = _factory.CreateDbContext();
|
||||||
|
var user = TestDbHelper.SeedUser(db, "me", "me@t.local");
|
||||||
|
var other = TestDbHelper.SeedUser(db, "other", "other@t.local");
|
||||||
|
_userId = user.Id;
|
||||||
|
SetupContext();
|
||||||
|
var game = SeedGameWithPlayers((other, null));
|
||||||
|
var otherPlayer = db.Players.Single(p => p.GameId == game.Id);
|
||||||
|
|
||||||
|
await _hub.PlayerReady(game.Id, otherPlayer.Id);
|
||||||
|
|
||||||
|
Assert.Equal("Player does not belong to authenticated user", ErrorMessageOf(_callerProxy));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PlayerReady_SendsGenericError_WhenServiceThrows()
|
||||||
|
{
|
||||||
|
using var db = _factory.CreateDbContext();
|
||||||
|
var user = TestDbHelper.SeedUser(db);
|
||||||
|
_userId = user.Id;
|
||||||
|
SetupContext();
|
||||||
|
var game = SeedGameWithPlayers((user, ConnectionId));
|
||||||
|
var player = db.Players.Single(p => p.GameId == game.Id);
|
||||||
|
|
||||||
|
_gameService.Setup(s => s.SetPlayerReadyAsync(It.IsAny<int>(), It.IsAny<int>()))
|
||||||
|
.ThrowsAsync(new InvalidOperationException("Player not found"));
|
||||||
|
|
||||||
|
await _hub.PlayerReady(game.Id, player.Id);
|
||||||
|
|
||||||
|
Assert.Equal("An error occurred while setting player status.", ErrorMessageOf(_callerProxy));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- StartGame ----------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task StartGame_BroadcastsGameStarted_AndSendsHandsToEachPlayer()
|
||||||
|
{
|
||||||
|
using var db = _factory.CreateDbContext();
|
||||||
|
var host = TestDbHelper.SeedUser(db, "host", "host@t.local");
|
||||||
|
_userId = host.Id;
|
||||||
|
SetupContext();
|
||||||
|
var game = SeedGameWithPlayers((host, "conn-host"));
|
||||||
|
var (_, otherPlayer) = SeedUserWithPlayer(game);
|
||||||
|
using (var db2 = _factory.CreateDbContext())
|
||||||
|
{
|
||||||
|
db2.Players.Single(p => p.Id == otherPlayer.Id).ConnectionId = "conn-other";
|
||||||
|
db2.SaveChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
_gameService.Setup(s => s.StartGameAsync(game.Id)).ReturnsAsync(new Game { Id = game.Id, LobbyCode = game.LobbyCode });
|
||||||
|
_gameService.Setup(s => s.DealCardsAsync(game.Id)).ReturnsAsync([
|
||||||
|
new PlayerCard { PlayerId = game.Players.First().Id },
|
||||||
|
new PlayerCard { PlayerId = otherPlayer.Id }
|
||||||
|
]);
|
||||||
|
_gameService.Setup(s => s.GetPlayerHandAsync(It.IsAny<int>())).ReturnsAsync((int pid) => new PlayerHandDto { PlayerId = pid });
|
||||||
|
|
||||||
|
await _hub.StartGame(game.Id);
|
||||||
|
|
||||||
|
_groupProxy.Verify(p => p.SendCoreAsync("GameStarted", It.IsAny<object?[]>(), default), Times.Once);
|
||||||
|
// Beide Spieler erhalten ihre Hand (über Clients.Client)
|
||||||
|
_clients.Verify(c => c.Client("conn-host"), Times.Once);
|
||||||
|
_clients.Verify(c => c.Client("conn-other"), Times.Once);
|
||||||
|
_clientProxy.Verify(p => p.SendCoreAsync("PlayerHandUpdated", It.IsAny<object?[]>(), default), Times.Exactly(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task StartGame_SendsHostOnlyError_WhenCallerIsNotHost()
|
||||||
|
{
|
||||||
|
using var db = _factory.CreateDbContext();
|
||||||
|
var host = TestDbHelper.SeedUser(db, "host", "host@t.local");
|
||||||
|
var notHost = TestDbHelper.SeedUser(db, "not", "not@t.local");
|
||||||
|
_userId = notHost.Id;
|
||||||
|
SetupContext();
|
||||||
|
var game = SeedGameWithPlayers((host, null), (notHost, null));
|
||||||
|
|
||||||
|
await _hub.StartGame(game.Id);
|
||||||
|
|
||||||
|
Assert.Equal("Only the host can start the game.", ErrorMessageOf(_callerProxy));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task StartGame_SendsGenericError_WhenServiceThrows()
|
||||||
|
{
|
||||||
|
using var db = _factory.CreateDbContext();
|
||||||
|
var host = TestDbHelper.SeedUser(db);
|
||||||
|
_userId = host.Id;
|
||||||
|
SetupContext();
|
||||||
|
var game = SeedGameWithPlayers((host, null));
|
||||||
|
|
||||||
|
_gameService.Setup(s => s.StartGameAsync(It.IsAny<int>())).ThrowsAsync(new Exception("boom"));
|
||||||
|
|
||||||
|
await _hub.StartGame(game.Id);
|
||||||
|
|
||||||
|
Assert.Equal("An error occurred while starting the game.", ErrorMessageOf(_callerProxy));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- SubmitSlip ----------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SubmitSlip_BroadcastsSlipSubmitted_WhenValid()
|
||||||
|
{
|
||||||
|
using var db = _factory.CreateDbContext();
|
||||||
|
var user = TestDbHelper.SeedUser(db);
|
||||||
|
_userId = user.Id;
|
||||||
|
SetupContext();
|
||||||
|
var game = SeedGameWithPlayers((user, ConnectionId));
|
||||||
|
var player = db.Players.Single(p => p.GameId == game.Id);
|
||||||
|
|
||||||
|
_gameService.Setup(s => s.SubmitSlipAsync(game.Id, player.Id, 55)).ReturnsAsync(true);
|
||||||
|
_gameService.Setup(s => s.GetGameAsync(game.Id)).ReturnsAsync(new Game { Id = game.Id, LobbyCode = game.LobbyCode });
|
||||||
|
|
||||||
|
await _hub.SubmitSlip(game.Id, player.Id, 55);
|
||||||
|
|
||||||
|
_groupProxy.Verify(p => p.SendCoreAsync("SlipSubmitted", It.IsAny<object?[]>(), default), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SubmitSlip_SendsError_WhenPlayerNotOwnedByUser()
|
||||||
|
{
|
||||||
|
using var db = _factory.CreateDbContext();
|
||||||
|
var user = TestDbHelper.SeedUser(db, "me", "me@t.local");
|
||||||
|
var other = TestDbHelper.SeedUser(db, "other", "other@t.local");
|
||||||
|
_userId = user.Id;
|
||||||
|
SetupContext();
|
||||||
|
var game = SeedGameWithPlayers((other, null));
|
||||||
|
var otherPlayer = db.Players.Single(p => p.GameId == game.Id);
|
||||||
|
|
||||||
|
await _hub.SubmitSlip(game.Id, otherPlayer.Id, 1);
|
||||||
|
|
||||||
|
Assert.Equal("Player does not belong to authenticated user", ErrorMessageOf(_callerProxy));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SubmitSlip_SendsGenericError_WhenServiceThrows()
|
||||||
|
{
|
||||||
|
using var db = _factory.CreateDbContext();
|
||||||
|
var user = TestDbHelper.SeedUser(db);
|
||||||
|
_userId = user.Id;
|
||||||
|
SetupContext();
|
||||||
|
var game = SeedGameWithPlayers((user, ConnectionId));
|
||||||
|
var player = db.Players.Single(p => p.GameId == game.Id);
|
||||||
|
|
||||||
|
_gameService.Setup(s => s.SubmitSlipAsync(It.IsAny<int>(), It.IsAny<int>(), It.IsAny<int>()))
|
||||||
|
.ThrowsAsync(new InvalidOperationException("Card not found"));
|
||||||
|
|
||||||
|
await _hub.SubmitSlip(game.Id, player.Id, 999);
|
||||||
|
|
||||||
|
Assert.Equal("An error occurred while submitting slip.", ErrorMessageOf(_callerProxy));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- ChallengeSlip ----------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ChallengeSlip_BroadcastsAndNotifiesTargetPlayer()
|
||||||
|
{
|
||||||
|
using var db = _factory.CreateDbContext();
|
||||||
|
var challenger = TestDbHelper.SeedUser(db, "ch", "ch@t.local");
|
||||||
|
_userId = challenger.Id;
|
||||||
|
SetupContext();
|
||||||
|
var game = SeedGameWithPlayers((challenger, ConnectionId));
|
||||||
|
var (_, targetPlayer) = SeedUserWithPlayer(game);
|
||||||
|
using (var db2 = _factory.CreateDbContext())
|
||||||
|
{
|
||||||
|
db2.Players.Single(p => p.Id == targetPlayer.Id).ConnectionId = "conn-target";
|
||||||
|
db2.SaveChanges();
|
||||||
|
}
|
||||||
|
var challengerPlayer = db.Players.Single(p => p.UserId == challenger.Id);
|
||||||
|
|
||||||
|
_gameService.Setup(s => s.CreateChallengeAsync(game.Id, challengerPlayer.Id, 42))
|
||||||
|
.ReturnsAsync(new SlipChallenge
|
||||||
|
{
|
||||||
|
Id = 1,
|
||||||
|
ChallengingPlayerId = challengerPlayer.Id,
|
||||||
|
TargetPlayerId = targetPlayer.Id,
|
||||||
|
TargetCardId = 42,
|
||||||
|
Status = ChallengeStatus.Pending
|
||||||
|
});
|
||||||
|
_gameService.Setup(s => s.GetGameAsync(game.Id)).ReturnsAsync(new Game { Id = game.Id, LobbyCode = game.LobbyCode });
|
||||||
|
|
||||||
|
await _hub.ChallengeSlip(game.Id, challengerPlayer.Id, 42);
|
||||||
|
|
||||||
|
_groupProxy.Verify(p => p.SendCoreAsync("SlipChallenged", It.IsAny<object?[]>(), default), Times.Once);
|
||||||
|
_clients.Verify(c => c.Client("conn-target"), Times.Once);
|
||||||
|
_clientProxy.Verify(p => p.SendCoreAsync("ChallengeReceived", It.IsAny<object?[]>(), default), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ChallengeSlip_SendsError_WhenPlayerNotOwnedByUser()
|
||||||
|
{
|
||||||
|
using var db = _factory.CreateDbContext();
|
||||||
|
var user = TestDbHelper.SeedUser(db, "me", "me@t.local");
|
||||||
|
var other = TestDbHelper.SeedUser(db, "other", "other@t.local");
|
||||||
|
_userId = user.Id;
|
||||||
|
SetupContext();
|
||||||
|
var game = SeedGameWithPlayers((other, null));
|
||||||
|
var otherPlayer = db.Players.Single(p => p.GameId == game.Id);
|
||||||
|
|
||||||
|
await _hub.ChallengeSlip(game.Id, otherPlayer.Id, 1);
|
||||||
|
|
||||||
|
Assert.Equal("Player does not belong to authenticated user", ErrorMessageOf(_callerProxy));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ChallengeSlip_SendsGenericError_WhenServiceThrows()
|
||||||
|
{
|
||||||
|
using var db = _factory.CreateDbContext();
|
||||||
|
var user = TestDbHelper.SeedUser(db);
|
||||||
|
_userId = user.Id;
|
||||||
|
SetupContext();
|
||||||
|
var game = SeedGameWithPlayers((user, ConnectionId));
|
||||||
|
var player = db.Players.Single(p => p.GameId == game.Id);
|
||||||
|
|
||||||
|
_gameService.Setup(s => s.CreateChallengeAsync(It.IsAny<int>(), It.IsAny<int>(), It.IsAny<int>()))
|
||||||
|
.ThrowsAsync(new InvalidOperationException("No active round found"));
|
||||||
|
|
||||||
|
await _hub.ChallengeSlip(game.Id, player.Id, 1);
|
||||||
|
|
||||||
|
Assert.Equal("An error occurred while challenging slip.", ErrorMessageOf(_callerProxy));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- ResolveChallenge ----------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ResolveChallenge_BroadcastsAndUpdatesHands_WhenTargetResolves()
|
||||||
|
{
|
||||||
|
using var db = _factory.CreateDbContext();
|
||||||
|
var target = TestDbHelper.SeedUser(db, "target", "target@t.local");
|
||||||
|
_userId = target.Id;
|
||||||
|
SetupContext();
|
||||||
|
var game = SeedGameWithPlayers((target, "conn-target"));
|
||||||
|
var targetPlayer = db.Players.Single(p => p.UserId == target.Id);
|
||||||
|
var (_, challengingPlayer) = SeedUserWithPlayer(game);
|
||||||
|
using (var db2 = _factory.CreateDbContext())
|
||||||
|
{
|
||||||
|
db2.Players.Single(p => p.Id == challengingPlayer.Id).ConnectionId = "conn-challenger";
|
||||||
|
var round = new GameRound { GameId = game.Id, RoundNumber = 1, Status = RoundStatus.Active };
|
||||||
|
db2.GameRounds.Add(round);
|
||||||
|
var phrase = TestDbHelper.SeedPhrases(db2, 1).Single();
|
||||||
|
var card = new PlayerCard { PlayerId = targetPlayer.Id, PhraseId = phrase.Id, GameRoundId = round.Id };
|
||||||
|
db2.PlayerCards.Add(card);
|
||||||
|
db2.SlipChallenges.Add(new SlipChallenge
|
||||||
|
{
|
||||||
|
Id = 77,
|
||||||
|
GameRoundId = round.Id,
|
||||||
|
ChallengingPlayerId = challengingPlayer.Id,
|
||||||
|
TargetPlayerId = targetPlayer.Id,
|
||||||
|
TargetCardId = card.Id,
|
||||||
|
Status = ChallengeStatus.Pending
|
||||||
|
});
|
||||||
|
db2.SaveChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
_gameService.Setup(s => s.ResolveChallengeAsync(77, true))
|
||||||
|
.ReturnsAsync(new SlipChallenge
|
||||||
|
{
|
||||||
|
Id = 77,
|
||||||
|
GameRound = new GameRound { GameId = game.Id },
|
||||||
|
ChallengingPlayerId = challengingPlayer.Id,
|
||||||
|
TargetPlayerId = targetPlayer.Id,
|
||||||
|
Status = ChallengeStatus.Approved
|
||||||
|
});
|
||||||
|
_gameService.Setup(s => s.GetGameAsync(game.Id)).ReturnsAsync(new Game { Id = game.Id, LobbyCode = game.LobbyCode });
|
||||||
|
_gameService.Setup(s => s.GetPlayerHandAsync(It.IsAny<int>())).ReturnsAsync((int pid) => new PlayerHandDto { PlayerId = pid });
|
||||||
|
|
||||||
|
await _hub.ResolveChallenge(77, true);
|
||||||
|
|
||||||
|
_groupProxy.Verify(p => p.SendCoreAsync("ChallengeResolved", It.IsAny<object?[]>(), default), Times.Once);
|
||||||
|
_clientProxy.Verify(p => p.SendCoreAsync("PlayerHandUpdated", It.IsAny<object?[]>(), default), Times.Exactly(2));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ResolveChallenge_SendsError_WhenCallerIsNotTarget()
|
||||||
|
{
|
||||||
|
using var db = _factory.CreateDbContext();
|
||||||
|
var target = TestDbHelper.SeedUser(db, "target", "target@t.local");
|
||||||
|
var notTarget = TestDbHelper.SeedUser(db, "not", "not@t.local");
|
||||||
|
_userId = notTarget.Id;
|
||||||
|
SetupContext();
|
||||||
|
var game = SeedGameWithPlayers((target, null));
|
||||||
|
var targetPlayer = db.Players.Single(p => p.UserId == target.Id);
|
||||||
|
using (var db2 = _factory.CreateDbContext())
|
||||||
|
{
|
||||||
|
db2.SlipChallenges.Add(new SlipChallenge
|
||||||
|
{
|
||||||
|
Id = 78,
|
||||||
|
GameRoundId = 1,
|
||||||
|
ChallengingPlayerId = 1,
|
||||||
|
TargetPlayerId = targetPlayer.Id,
|
||||||
|
TargetCardId = 1,
|
||||||
|
Status = ChallengeStatus.Pending
|
||||||
|
});
|
||||||
|
db2.SaveChanges();
|
||||||
|
}
|
||||||
|
|
||||||
|
await _hub.ResolveChallenge(78, true);
|
||||||
|
|
||||||
|
Assert.Equal("Only the accused player can resolve this challenge", ErrorMessageOf(_callerProxy));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ResolveChallenge_SendsMessage_WhenChallengeNotFound()
|
||||||
|
{
|
||||||
|
using var db = _factory.CreateDbContext();
|
||||||
|
var user = TestDbHelper.SeedUser(db);
|
||||||
|
_userId = user.Id;
|
||||||
|
SetupContext();
|
||||||
|
|
||||||
|
await _hub.ResolveChallenge(999, true);
|
||||||
|
|
||||||
|
Assert.Equal("Challenge not found", ErrorMessageOf(_callerProxy));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- RequestGameState ----------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RequestGameState_SendsStateToCaller_WhenParticipant()
|
||||||
|
{
|
||||||
|
using var db = _factory.CreateDbContext();
|
||||||
|
var user = TestDbHelper.SeedUser(db);
|
||||||
|
_userId = user.Id;
|
||||||
|
SetupContext();
|
||||||
|
SeedGameWithPlayers((user, ConnectionId));
|
||||||
|
var gameId = db.Games.Single().Id;
|
||||||
|
|
||||||
|
_gameService.Setup(s => s.GetGameStateAsync(gameId)).ReturnsAsync(new GameStateDto { GameId = gameId });
|
||||||
|
|
||||||
|
await _hub.RequestGameState(gameId);
|
||||||
|
|
||||||
|
_callerProxy.Verify(p => p.SendCoreAsync("GameStateUpdated", It.IsAny<object?[]>(), default), Times.Once);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RequestGameState_SendsError_WhenNotParticipant()
|
||||||
|
{
|
||||||
|
using var db = _factory.CreateDbContext();
|
||||||
|
var user = TestDbHelper.SeedUser(db);
|
||||||
|
var other = TestDbHelper.SeedUser(db, "other", "other@t.local");
|
||||||
|
_userId = user.Id;
|
||||||
|
SetupContext();
|
||||||
|
SeedGameWithPlayers((other, null));
|
||||||
|
var gameId = db.Games.Single().Id;
|
||||||
|
|
||||||
|
await _hub.RequestGameState(gameId);
|
||||||
|
|
||||||
|
Assert.Equal("User is not part of this game", ErrorMessageOf(_callerProxy));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RequestGameState_SendsGenericError_WhenServiceThrows()
|
||||||
|
{
|
||||||
|
using var db = _factory.CreateDbContext();
|
||||||
|
var user = TestDbHelper.SeedUser(db);
|
||||||
|
_userId = user.Id;
|
||||||
|
SetupContext();
|
||||||
|
SeedGameWithPlayers((user, ConnectionId));
|
||||||
|
var gameId = db.Games.Single().Id;
|
||||||
|
|
||||||
|
_gameService.Setup(s => s.GetGameStateAsync(It.IsAny<int>())).ThrowsAsync(new Exception("boom"));
|
||||||
|
|
||||||
|
await _hub.RequestGameState(gameId);
|
||||||
|
|
||||||
|
Assert.Equal("An error occurred while requesting game state.", ErrorMessageOf(_callerProxy));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- OnConnectedAsync ----------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task OnConnectedAsync_StoresConnectionId_WhenUserInActiveGame()
|
||||||
|
{
|
||||||
|
using var db = _factory.CreateDbContext();
|
||||||
|
var user = TestDbHelper.SeedUser(db);
|
||||||
|
_userId = user.Id;
|
||||||
|
SetupContext();
|
||||||
|
SeedGameWithPlayers((user, null));
|
||||||
|
|
||||||
|
await _hub.OnConnectedAsync();
|
||||||
|
|
||||||
|
using var verify = _factory.CreateDbContext();
|
||||||
|
Assert.Equal(ConnectionId, verify.Players.Single(p => p.UserId == user.Id).ConnectionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task OnConnectedAsync_DoesNotThrow_WhenNotAuthenticated()
|
||||||
|
{
|
||||||
|
WithoutAuth();
|
||||||
|
|
||||||
|
await _hub.OnConnectedAsync(); // darf nicht werfen
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- OnDisconnectedAsync ----------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task OnDisconnectedAsync_Completes()
|
||||||
|
{
|
||||||
|
await _hub.OnDisconnectedAsync(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
504
SlipItIn.Server.Tests/GameServiceTests.cs
Normal file
504
SlipItIn.Server.Tests/GameServiceTests.cs
Normal file
@@ -0,0 +1,504 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using SlipItIn.Server.Data;
|
||||||
|
using SlipItIn.Server.Services;
|
||||||
|
using SlipItIn.Shared.Models;
|
||||||
|
|
||||||
|
namespace SlipItIn.Server.Tests;
|
||||||
|
|
||||||
|
public class GameServiceTests
|
||||||
|
{
|
||||||
|
private readonly IDbContextFactory<SlipItInDbContext> _factory = TestDbHelper.CreateFactory();
|
||||||
|
private readonly GameService _sut;
|
||||||
|
|
||||||
|
public GameServiceTests()
|
||||||
|
{
|
||||||
|
_sut = new GameService(_factory, NullLogger<GameService>.Instance);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- CreateGameAsync ----------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateGameAsync_CreatesGameWithHostAsFirstPlayer()
|
||||||
|
{
|
||||||
|
using var db = _factory.CreateDbContext();
|
||||||
|
var host = TestDbHelper.SeedUser(db);
|
||||||
|
|
||||||
|
var game = await _sut.CreateGameAsync(host.Id, "conn-1");
|
||||||
|
|
||||||
|
Assert.Equal(host.Id, game.HostId);
|
||||||
|
Assert.Equal(GameStatus.Lobby, game.Status);
|
||||||
|
Assert.Equal(6, game.LobbyCode.Length);
|
||||||
|
Assert.Single(game.Players);
|
||||||
|
Assert.Equal(host.Id, game.Players.First().UserId);
|
||||||
|
Assert.Equal("conn-1", game.Players.First().ConnectionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateGameAsync_Throws_WhenHostNotFound()
|
||||||
|
{
|
||||||
|
await Assert.ThrowsAsync<InvalidOperationException>(() => _sut.CreateGameAsync(999));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- JoinGameAsync ----------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task JoinGameAsync_AddsPlayerToGame()
|
||||||
|
{
|
||||||
|
using var db = _factory.CreateDbContext();
|
||||||
|
var host = TestDbHelper.SeedUser(db, "host", "host@t.local");
|
||||||
|
var joiner = TestDbHelper.SeedUser(db, "joiner", "joiner@t.local");
|
||||||
|
var (game, _) = TestDbHelper.SeedGame(db, host);
|
||||||
|
|
||||||
|
var result = await _sut.JoinGameAsync(game.LobbyCode, joiner.Id, "conn-2");
|
||||||
|
|
||||||
|
Assert.Equal(game.Id, result.Id);
|
||||||
|
Assert.Equal(2, db.Players.Count(p => p.GameId == game.Id));
|
||||||
|
Assert.Equal("conn-2", db.Players.Single(p => p.UserId == joiner.Id).ConnectionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task JoinGameAsync_Throws_WhenGameNotFound()
|
||||||
|
{
|
||||||
|
using var db = _factory.CreateDbContext();
|
||||||
|
var user = TestDbHelper.SeedUser(db);
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<InvalidOperationException>(() => _sut.JoinGameAsync("NOCODE", user.Id));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task JoinGameAsync_Throws_WhenGameAlreadyStarted()
|
||||||
|
{
|
||||||
|
using var db = _factory.CreateDbContext();
|
||||||
|
var host = TestDbHelper.SeedUser(db, "host", "host@t.local");
|
||||||
|
var joiner = TestDbHelper.SeedUser(db, "joiner", "joiner@t.local");
|
||||||
|
var (game, _) = TestDbHelper.SeedGame(db, host, GameStatus.InProgress);
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<InvalidOperationException>(() => _sut.JoinGameAsync(game.LobbyCode, joiner.Id));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task JoinGameAsync_Throws_WhenGameIsFull()
|
||||||
|
{
|
||||||
|
using var db = _factory.CreateDbContext();
|
||||||
|
var host = TestDbHelper.SeedUser(db, "host", "host@t.local");
|
||||||
|
var (game, _) = TestDbHelper.SeedGame(db, host);
|
||||||
|
game.MaxPlayers = 1;
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
var joiner = TestDbHelper.SeedUser(db, "joiner", "joiner@t.local");
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<InvalidOperationException>(() => _sut.JoinGameAsync(game.LobbyCode, joiner.Id));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task JoinGameAsync_Throws_WhenUserNotFound()
|
||||||
|
{
|
||||||
|
using var db = _factory.CreateDbContext();
|
||||||
|
var host = TestDbHelper.SeedUser(db);
|
||||||
|
var (game, _) = TestDbHelper.SeedGame(db, host);
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<InvalidOperationException>(() => _sut.JoinGameAsync(game.LobbyCode, 999));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task JoinGameAsync_Throws_WhenUserAlreadyInGame()
|
||||||
|
{
|
||||||
|
using var db = _factory.CreateDbContext();
|
||||||
|
var host = TestDbHelper.SeedUser(db);
|
||||||
|
var (game, _) = TestDbHelper.SeedGame(db, host);
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<InvalidOperationException>(() => _sut.JoinGameAsync(game.LobbyCode, host.Id));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- StartGameAsync ----------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task StartGameAsync_SetsStatusAndCreatesFirstRound()
|
||||||
|
{
|
||||||
|
using var db = _factory.CreateDbContext();
|
||||||
|
var host = TestDbHelper.SeedUser(db);
|
||||||
|
var (game, _) = TestDbHelper.SeedGame(db, host);
|
||||||
|
|
||||||
|
var result = await _sut.StartGameAsync(game.Id);
|
||||||
|
|
||||||
|
Assert.Equal(GameStatus.InProgress, result.Status);
|
||||||
|
Assert.NotNull(result.StartedAt);
|
||||||
|
var round = db.GameRounds.Single(r => r.GameId == game.Id);
|
||||||
|
Assert.Equal(1, round.RoundNumber);
|
||||||
|
Assert.Equal(RoundStatus.Waiting, round.Status);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task StartGameAsync_Throws_WhenGameNotFound()
|
||||||
|
{
|
||||||
|
await Assert.ThrowsAsync<InvalidOperationException>(() => _sut.StartGameAsync(999));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- SetPlayerReadyAsync ----------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SetPlayerReadyAsync_MarksPlayerReady()
|
||||||
|
{
|
||||||
|
using var db = _factory.CreateDbContext();
|
||||||
|
var host = TestDbHelper.SeedUser(db);
|
||||||
|
var (game, hostPlayer) = TestDbHelper.SeedGame(db, host);
|
||||||
|
|
||||||
|
var result = await _sut.SetPlayerReadyAsync(game.Id, hostPlayer.Id);
|
||||||
|
|
||||||
|
Assert.Equal(game.Id, result.Id);
|
||||||
|
using var verify = _factory.CreateDbContext();
|
||||||
|
Assert.True(verify.Players.Single(p => p.Id == hostPlayer.Id).IsReady);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SetPlayerReadyAsync_Throws_WhenPlayerNotFound()
|
||||||
|
{
|
||||||
|
using var db = _factory.CreateDbContext();
|
||||||
|
var host = TestDbHelper.SeedUser(db);
|
||||||
|
var (game, _) = TestDbHelper.SeedGame(db, host);
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<InvalidOperationException>(() => _sut.SetPlayerReadyAsync(game.Id, 999));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- DealCardsAsync ----------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DealCardsAsync_DealsFiveCardsPerPlayerAndActivatesRound()
|
||||||
|
{
|
||||||
|
using var db = _factory.CreateDbContext();
|
||||||
|
var host = TestDbHelper.SeedUser(db, "host", "host@t.local");
|
||||||
|
var other = TestDbHelper.SeedUser(db, "other", "other@t.local");
|
||||||
|
var (game, _) = TestDbHelper.SeedGame(db, host);
|
||||||
|
db.Players.Add(new Player { UserId = other.Id, GameId = game.Id });
|
||||||
|
var round = new GameRound { GameId = game.Id, RoundNumber = 1, Status = RoundStatus.Waiting };
|
||||||
|
db.GameRounds.Add(round);
|
||||||
|
TestDbHelper.SeedPhrases(db, 10);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
var cards = await _sut.DealCardsAsync(game.Id);
|
||||||
|
|
||||||
|
Assert.Equal(10, cards.Count); // 2 Spieler x 5 Karten
|
||||||
|
Assert.Equal(5, cards.Count(c => c.PlayerId == game.Players.First().Id));
|
||||||
|
Assert.All(cards, c => Assert.False(c.IsUsed));
|
||||||
|
using var verify = _factory.CreateDbContext();
|
||||||
|
Assert.Equal(RoundStatus.Active, verify.GameRounds.Single(r => r.Id == round.Id).Status);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DealCardsAsync_Throws_WhenNoWaitingRound()
|
||||||
|
{
|
||||||
|
using var db = _factory.CreateDbContext();
|
||||||
|
var host = TestDbHelper.SeedUser(db);
|
||||||
|
var (game, _) = TestDbHelper.SeedGame(db, host);
|
||||||
|
TestDbHelper.SeedPhrases(db, 10);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<InvalidOperationException>(() => _sut.DealCardsAsync(game.Id));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task DealCardsAsync_Throws_WhenNotEnoughPhrases()
|
||||||
|
{
|
||||||
|
using var db = _factory.CreateDbContext();
|
||||||
|
var host = TestDbHelper.SeedUser(db);
|
||||||
|
var (game, _) = TestDbHelper.SeedGame(db, host);
|
||||||
|
db.GameRounds.Add(new GameRound { GameId = game.Id, RoundNumber = 1, Status = RoundStatus.Waiting });
|
||||||
|
TestDbHelper.SeedPhrases(db, 4);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<InvalidOperationException>(() => _sut.DealCardsAsync(game.Id));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- SubmitSlipAsync ----------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SubmitSlipAsync_MarksCardAsUsed()
|
||||||
|
{
|
||||||
|
using var db = _factory.CreateDbContext();
|
||||||
|
var host = TestDbHelper.SeedUser(db);
|
||||||
|
var (game, hostPlayer) = TestDbHelper.SeedGame(db, host);
|
||||||
|
var round = new GameRound { GameId = game.Id, RoundNumber = 1, Status = RoundStatus.Active };
|
||||||
|
db.GameRounds.Add(round);
|
||||||
|
var phrase = TestDbHelper.SeedPhrases(db, 1).Single();
|
||||||
|
var card = new PlayerCard { PlayerId = hostPlayer.Id, PhraseId = phrase.Id, GameRoundId = round.Id };
|
||||||
|
db.PlayerCards.Add(card);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
var result = await _sut.SubmitSlipAsync(game.Id, hostPlayer.Id, card.Id);
|
||||||
|
|
||||||
|
Assert.True(result);
|
||||||
|
using var verify = _factory.CreateDbContext();
|
||||||
|
Assert.True(verify.PlayerCards.Single(c => c.Id == card.Id).IsUsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SubmitSlipAsync_Throws_WhenCardNotFound()
|
||||||
|
{
|
||||||
|
await Assert.ThrowsAsync<InvalidOperationException>(() => _sut.SubmitSlipAsync(1, 1, 999));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SubmitSlipAsync_Throws_WhenCardBelongsToOtherPlayer()
|
||||||
|
{
|
||||||
|
using var db = _factory.CreateDbContext();
|
||||||
|
var host = TestDbHelper.SeedUser(db, "host", "host@t.local");
|
||||||
|
var other = TestDbHelper.SeedUser(db, "other", "other@t.local");
|
||||||
|
var (game, hostPlayer) = TestDbHelper.SeedGame(db, host);
|
||||||
|
var otherPlayer = new Player { UserId = other.Id, GameId = game.Id };
|
||||||
|
db.Players.Add(otherPlayer);
|
||||||
|
var round = new GameRound { GameId = game.Id, RoundNumber = 1, Status = RoundStatus.Active };
|
||||||
|
db.GameRounds.Add(round);
|
||||||
|
var phrase = TestDbHelper.SeedPhrases(db, 1).Single();
|
||||||
|
var card = new PlayerCard { PlayerId = hostPlayer.Id, PhraseId = phrase.Id, GameRoundId = round.Id };
|
||||||
|
db.PlayerCards.Add(card);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
// otherPlayer versucht, die Karte des Hosts zu verwenden
|
||||||
|
await Assert.ThrowsAsync<UnauthorizedAccessException>(() => _sut.SubmitSlipAsync(game.Id, otherPlayer.Id, card.Id));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- CreateChallengeAsync ----------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateChallengeAsync_CreatesPendingChallenge()
|
||||||
|
{
|
||||||
|
using var db = _factory.CreateDbContext();
|
||||||
|
var host = TestDbHelper.SeedUser(db, "host", "host@t.local");
|
||||||
|
var other = TestDbHelper.SeedUser(db, "other", "other@t.local");
|
||||||
|
var (game, hostPlayer) = TestDbHelper.SeedGame(db, host);
|
||||||
|
var otherPlayer = new Player { UserId = other.Id, GameId = game.Id };
|
||||||
|
db.Players.Add(otherPlayer);
|
||||||
|
var round = new GameRound { GameId = game.Id, RoundNumber = 1, Status = RoundStatus.Active };
|
||||||
|
db.GameRounds.Add(round);
|
||||||
|
var phrase = TestDbHelper.SeedPhrases(db, 1).Single();
|
||||||
|
var card = new PlayerCard { PlayerId = otherPlayer.Id, PhraseId = phrase.Id, GameRoundId = round.Id };
|
||||||
|
db.PlayerCards.Add(card);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
var challenge = await _sut.CreateChallengeAsync(game.Id, hostPlayer.Id, card.Id);
|
||||||
|
|
||||||
|
Assert.Equal(round.Id, challenge.GameRoundId);
|
||||||
|
Assert.Equal(hostPlayer.Id, challenge.ChallengingPlayerId);
|
||||||
|
Assert.Equal(otherPlayer.Id, challenge.TargetPlayerId);
|
||||||
|
Assert.Equal(card.Id, challenge.TargetCardId);
|
||||||
|
Assert.Equal(ChallengeStatus.Pending, challenge.Status);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateChallengeAsync_Throws_WhenTargetCardNotFound()
|
||||||
|
{
|
||||||
|
await Assert.ThrowsAsync<InvalidOperationException>(() => _sut.CreateChallengeAsync(1, 1, 999));
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task CreateChallengeAsync_Throws_WhenNoActiveRound()
|
||||||
|
{
|
||||||
|
using var db = _factory.CreateDbContext();
|
||||||
|
var host = TestDbHelper.SeedUser(db);
|
||||||
|
var (game, hostPlayer) = TestDbHelper.SeedGame(db, host);
|
||||||
|
var phrase = TestDbHelper.SeedPhrases(db, 1).Single();
|
||||||
|
var round = new GameRound { GameId = game.Id, RoundNumber = 1, Status = RoundStatus.Waiting };
|
||||||
|
db.GameRounds.Add(round);
|
||||||
|
var card = new PlayerCard { PlayerId = hostPlayer.Id, PhraseId = phrase.Id, GameRoundId = round.Id };
|
||||||
|
db.PlayerCards.Add(card);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
await Assert.ThrowsAsync<InvalidOperationException>(() => _sut.CreateChallengeAsync(game.Id, hostPlayer.Id, card.Id));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- ResolveChallengeAsync ----------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ResolveChallengeAsync_Approved_KeepsCardWithTargetPlayer()
|
||||||
|
{
|
||||||
|
using var db = _factory.CreateDbContext();
|
||||||
|
var host = TestDbHelper.SeedUser(db, "host", "host@t.local");
|
||||||
|
var other = TestDbHelper.SeedUser(db, "other", "other@t.local");
|
||||||
|
var (game, hostPlayer) = TestDbHelper.SeedGame(db, host);
|
||||||
|
var otherPlayer = new Player { UserId = other.Id, GameId = game.Id };
|
||||||
|
db.Players.Add(otherPlayer);
|
||||||
|
var round = new GameRound { GameId = game.Id, RoundNumber = 1, Status = RoundStatus.Active };
|
||||||
|
db.GameRounds.Add(round);
|
||||||
|
var phrase = TestDbHelper.SeedPhrases(db, 1).Single();
|
||||||
|
var card = new PlayerCard { PlayerId = otherPlayer.Id, PhraseId = phrase.Id, GameRoundId = round.Id };
|
||||||
|
db.PlayerCards.Add(card);
|
||||||
|
var challenge = new SlipChallenge
|
||||||
|
{
|
||||||
|
GameRoundId = round.Id,
|
||||||
|
ChallengingPlayerId = hostPlayer.Id,
|
||||||
|
TargetPlayerId = otherPlayer.Id,
|
||||||
|
TargetCardId = card.Id,
|
||||||
|
Status = ChallengeStatus.Pending
|
||||||
|
};
|
||||||
|
db.SlipChallenges.Add(challenge);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
var result = await _sut.ResolveChallengeAsync(challenge.Id, approved: true);
|
||||||
|
|
||||||
|
Assert.Equal(ChallengeStatus.Approved, result.Status);
|
||||||
|
Assert.NotNull(result.ResolvedAt);
|
||||||
|
// Karte bleibt beim Beschuldigten
|
||||||
|
using var verify = _factory.CreateDbContext();
|
||||||
|
Assert.Equal(otherPlayer.Id, verify.PlayerCards.Single(c => c.Id == card.Id).PlayerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ResolveChallengeAsync_Rejected_TransfersCardToChallenger()
|
||||||
|
{
|
||||||
|
using var db = _factory.CreateDbContext();
|
||||||
|
var host = TestDbHelper.SeedUser(db, "host", "host@t.local");
|
||||||
|
var other = TestDbHelper.SeedUser(db, "other", "other@t.local");
|
||||||
|
var (game, hostPlayer) = TestDbHelper.SeedGame(db, host);
|
||||||
|
var otherPlayer = new Player { UserId = other.Id, GameId = game.Id };
|
||||||
|
db.Players.Add(otherPlayer);
|
||||||
|
var round = new GameRound { GameId = game.Id, RoundNumber = 1, Status = RoundStatus.Active };
|
||||||
|
db.GameRounds.Add(round);
|
||||||
|
var phrase = TestDbHelper.SeedPhrases(db, 1).Single();
|
||||||
|
var card = new PlayerCard { PlayerId = otherPlayer.Id, PhraseId = phrase.Id, GameRoundId = round.Id };
|
||||||
|
db.PlayerCards.Add(card);
|
||||||
|
var challenge = new SlipChallenge
|
||||||
|
{
|
||||||
|
GameRoundId = round.Id,
|
||||||
|
ChallengingPlayerId = hostPlayer.Id,
|
||||||
|
TargetPlayerId = otherPlayer.Id,
|
||||||
|
TargetCardId = card.Id,
|
||||||
|
Status = ChallengeStatus.Pending
|
||||||
|
};
|
||||||
|
db.SlipChallenges.Add(challenge);
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
var result = await _sut.ResolveChallengeAsync(challenge.Id, approved: false);
|
||||||
|
|
||||||
|
Assert.Equal(ChallengeStatus.Rejected, result.Status);
|
||||||
|
Assert.NotNull(result.ResolvedAt);
|
||||||
|
// Karte geht an den Beschuldiger
|
||||||
|
using var verify = _factory.CreateDbContext();
|
||||||
|
Assert.Equal(hostPlayer.Id, verify.PlayerCards.Single(c => c.Id == card.Id).PlayerId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task ResolveChallengeAsync_Throws_WhenChallengeNotFound()
|
||||||
|
{
|
||||||
|
await Assert.ThrowsAsync<InvalidOperationException>(() => _sut.ResolveChallengeAsync(999, true));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- GetGameStateAsync ----------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetGameStateAsync_ReturnsStateWithoutRound_WhenNoRoundsExist()
|
||||||
|
{
|
||||||
|
using var db = _factory.CreateDbContext();
|
||||||
|
var host = TestDbHelper.SeedUser(db);
|
||||||
|
var (game, _) = TestDbHelper.SeedGame(db, host);
|
||||||
|
|
||||||
|
var state = await _sut.GetGameStateAsync(game.Id);
|
||||||
|
|
||||||
|
Assert.Equal(game.Id, state.GameId);
|
||||||
|
Assert.Equal(game.LobbyCode, state.LobbyCode);
|
||||||
|
Assert.Equal("Lobby", state.Status);
|
||||||
|
Assert.Single(state.Players);
|
||||||
|
Assert.Equal(host.Username, state.Players[0].Username);
|
||||||
|
Assert.Equal(0, state.CurrentRound);
|
||||||
|
Assert.Equal(45, state.RoundTimeRemaining); // Default RoundDurationSeconds
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetGameStateAsync_ComputesRemainingTime_WhenRoundActive()
|
||||||
|
{
|
||||||
|
using var db = _factory.CreateDbContext();
|
||||||
|
var host = TestDbHelper.SeedUser(db);
|
||||||
|
var (game, _) = TestDbHelper.SeedGame(db, host);
|
||||||
|
game.RoundDurationSeconds = 45;
|
||||||
|
db.GameRounds.Add(new GameRound
|
||||||
|
{
|
||||||
|
GameId = game.Id,
|
||||||
|
RoundNumber = 2,
|
||||||
|
Status = RoundStatus.Active,
|
||||||
|
StartedAt = DateTime.UtcNow.AddSeconds(-20)
|
||||||
|
});
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
var state = await _sut.GetGameStateAsync(game.Id);
|
||||||
|
|
||||||
|
Assert.Equal(2, state.CurrentRound);
|
||||||
|
Assert.InRange(state.RoundTimeRemaining, 24, 25);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetGameStateAsync_ClampsRemainingTimeToZero_WhenRoundExpired()
|
||||||
|
{
|
||||||
|
using var db = _factory.CreateDbContext();
|
||||||
|
var host = TestDbHelper.SeedUser(db);
|
||||||
|
var (game, _) = TestDbHelper.SeedGame(db, host);
|
||||||
|
db.GameRounds.Add(new GameRound
|
||||||
|
{
|
||||||
|
GameId = game.Id,
|
||||||
|
RoundNumber = 1,
|
||||||
|
Status = RoundStatus.Active,
|
||||||
|
StartedAt = DateTime.UtcNow.AddSeconds(-120)
|
||||||
|
});
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
var state = await _sut.GetGameStateAsync(game.Id);
|
||||||
|
|
||||||
|
Assert.Equal(0, state.RoundTimeRemaining);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetGameStateAsync_Throws_WhenGameNotFound()
|
||||||
|
{
|
||||||
|
await Assert.ThrowsAsync<InvalidOperationException>(() => _sut.GetGameStateAsync(999));
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- GetPlayerHandAsync ----------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetPlayerHandAsync_ReturnsCardsWithPhraseText()
|
||||||
|
{
|
||||||
|
using var db = _factory.CreateDbContext();
|
||||||
|
var host = TestDbHelper.SeedUser(db);
|
||||||
|
var (game, hostPlayer) = TestDbHelper.SeedGame(db, host);
|
||||||
|
var round = new GameRound { GameId = game.Id, RoundNumber = 1 };
|
||||||
|
db.GameRounds.Add(round);
|
||||||
|
var phrases = TestDbHelper.SeedPhrases(db, 2);
|
||||||
|
db.PlayerCards.Add(new PlayerCard { PlayerId = hostPlayer.Id, PhraseId = phrases[0].Id, GameRoundId = round.Id });
|
||||||
|
db.PlayerCards.Add(new PlayerCard { PlayerId = hostPlayer.Id, PhraseId = phrases[1].Id, GameRoundId = round.Id, IsUsed = true });
|
||||||
|
await db.SaveChangesAsync();
|
||||||
|
|
||||||
|
var hand = await _sut.GetPlayerHandAsync(hostPlayer.Id);
|
||||||
|
|
||||||
|
Assert.Equal(hostPlayer.Id, hand.PlayerId);
|
||||||
|
Assert.Equal(2, hand.Cards.Count);
|
||||||
|
Assert.Contains(hand.Cards, c => c.Text == "Phrase 1" && !c.IsUsed);
|
||||||
|
Assert.Contains(hand.Cards, c => c.Text == "Phrase 2" && c.IsUsed);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetPlayerHandAsync_ReturnsEmptyHand_WhenNoCards()
|
||||||
|
{
|
||||||
|
var hand = await _sut.GetPlayerHandAsync(999);
|
||||||
|
|
||||||
|
Assert.Equal(999, hand.PlayerId);
|
||||||
|
Assert.Empty(hand.Cards);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------- GetGameAsync ----------
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetGameAsync_ReturnsGame()
|
||||||
|
{
|
||||||
|
using var db = _factory.CreateDbContext();
|
||||||
|
var host = TestDbHelper.SeedUser(db);
|
||||||
|
var (game, _) = TestDbHelper.SeedGame(db, host);
|
||||||
|
|
||||||
|
var result = await _sut.GetGameAsync(game.Id);
|
||||||
|
|
||||||
|
Assert.Equal(game.Id, result.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task GetGameAsync_Throws_WhenGameNotFound()
|
||||||
|
{
|
||||||
|
await Assert.ThrowsAsync<InvalidOperationException>(() => _sut.GetGameAsync(999));
|
||||||
|
}
|
||||||
|
}
|
||||||
27
SlipItIn.Server.Tests/SlipItIn.Server.Tests.csproj
Normal file
27
SlipItIn.Server.Tests/SlipItIn.Server.Tests.csproj
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFramework>net10.0</TargetFramework>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<IsPackable>false</IsPackable>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||||
|
<PackageReference Include="Microsoft.EntityFrameworkCore.InMemory" Version="10.0.10" />
|
||||||
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||||
|
<PackageReference Include="Moq" Version="4.20.72" />
|
||||||
|
<PackageReference Include="xunit" Version="2.9.3" />
|
||||||
|
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\SlipItIn.Server\SlipItIn.Server.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Using Include="Xunit" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
56
SlipItIn.Server.Tests/TestDbHelper.cs
Normal file
56
SlipItIn.Server.Tests/TestDbHelper.cs
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using SlipItIn.Server.Data;
|
||||||
|
using SlipItIn.Shared.Models;
|
||||||
|
|
||||||
|
namespace SlipItIn.Server.Tests;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Hilfsmethoden zum Erstellen isolierter InMemory-DbContexts und Testdaten.
|
||||||
|
/// </summary>
|
||||||
|
public static class TestDbHelper
|
||||||
|
{
|
||||||
|
public static IDbContextFactory<SlipItInDbContext> CreateFactory()
|
||||||
|
{
|
||||||
|
var options = new DbContextOptionsBuilder<SlipItInDbContext>()
|
||||||
|
.UseInMemoryDatabase(Guid.NewGuid().ToString())
|
||||||
|
.Options;
|
||||||
|
return new TestDbContextFactory(options);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static User SeedUser(SlipItInDbContext db, string username = "user", string email = "user@test.local", bool isActive = true)
|
||||||
|
{
|
||||||
|
var user = new User { Username = username, Email = email, PasswordHash = "hash", IsActive = isActive };
|
||||||
|
db.Users.Add(user);
|
||||||
|
db.SaveChanges();
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static List<Phrase> SeedPhrases(SlipItInDbContext db, int count, bool isActive = true)
|
||||||
|
{
|
||||||
|
var creator = SeedUser(db, "creator-" + Guid.NewGuid().ToString("N")[..8], Guid.NewGuid().ToString("N")[..8] + "@t.local");
|
||||||
|
var phrases = Enumerable.Range(1, count)
|
||||||
|
.Select(i => new Phrase { Text = $"Phrase {i}", CreatorId = creator.Id, IsActive = isActive })
|
||||||
|
.ToList();
|
||||||
|
db.Phrases.AddRange(phrases);
|
||||||
|
db.SaveChanges();
|
||||||
|
return phrases;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Erstellt ein Spiel inkl. Host-Spieler über den echten GameService-Pfad (direkt in DB).</summary>
|
||||||
|
public static (Game game, Player hostPlayer) SeedGame(SlipItInDbContext db, User host, GameStatus status = GameStatus.Lobby)
|
||||||
|
{
|
||||||
|
var game = new Game { LobbyCode = Guid.NewGuid().ToString("N")[..6].ToUpper(), HostId = host.Id, Status = status };
|
||||||
|
var hostPlayer = new Player { UserId = host.Id, Game = game };
|
||||||
|
game.Players.Add(hostPlayer);
|
||||||
|
db.Games.Add(game);
|
||||||
|
db.SaveChanges();
|
||||||
|
return (game, hostPlayer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestDbContextFactory(DbContextOptions<SlipItInDbContext> options) : IDbContextFactory<SlipItInDbContext>
|
||||||
|
{
|
||||||
|
public SlipItInDbContext CreateDbContext() => new(options);
|
||||||
|
public Task<SlipItInDbContext> CreateDbContextAsync(CancellationToken cancellationToken = default)
|
||||||
|
=> Task.FromResult(new SlipItInDbContext(options));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
<Solution>
|
<Solution>
|
||||||
<Project Path="SlipItIN.AppHost/SlipItIn.AppHost.csproj" />
|
<Project Path="SlipItIN.AppHost/SlipItIn.AppHost.csproj" />
|
||||||
|
<Project Path="SlipItIn.Server.Tests/SlipItIn.Server.Tests.csproj" />
|
||||||
<Project Path="SlipItIn.Server/SlipItIn.Server.csproj" />
|
<Project Path="SlipItIn.Server/SlipItIn.Server.csproj" />
|
||||||
<Project Path="SlipItIN.ServiceDefaults/SlipItIn.ServiceDefaults.csproj" />
|
<Project Path="SlipItIN.ServiceDefaults/SlipItIn.ServiceDefaults.csproj" />
|
||||||
<Project Path="SlipItIn.Shared/SlipItIn.Shared.csproj" />
|
<Project Path="SlipItIn.Shared/SlipItIn.Shared.csproj" />
|
||||||
|
|||||||
Reference in New Issue
Block a user