Files
SlipItIn/SlipItIn.Server.Tests/GameHubTests.cs
Tim Krampitz 510cc0e520 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.
2026-08-22 19:31:45 +02:00

607 lines
23 KiB
C#

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);
}
}