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 _factory = TestDbHelper.CreateFactory(); private readonly GameService _sut; public GameServiceTests() { _sut = new GameService(_factory, NullLogger.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(() => _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(() => _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(() => _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(() => _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(() => _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(() => _sut.JoinGameAsync(game.LobbyCode, host.Id)); } // ---------- StartGameAsync ---------- [Fact] public async Task StartGameAsync_SetsStatusAndCreatesFirstRound() { using var db = _factory.CreateDbContext(); var host = TestDbHelper.SeedUser(db); 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 }); await db.SaveChangesAsync(); 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(() => _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(() => _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(() => _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(() => _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, GameStatus.InProgress); 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() { using var db = _factory.CreateDbContext(); var host = TestDbHelper.SeedUser(db); var (game, _) = TestDbHelper.SeedGame(db, host, GameStatus.InProgress); await Assert.ThrowsAsync(() => _sut.SubmitSlipAsync(game.Id, 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, GameStatus.InProgress); 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(() => _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, IsUsed = true }; 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(() => _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(() => _sut.CreateChallengeAsync(game.Id, hostPlayer.Id, card.Id)); } [Fact] public async Task CreateChallengeAsync_Throws_WhenChallengerNotInGame() { using var db = _factory.CreateDbContext(); var host = TestDbHelper.SeedUser(db, "host", "host@t.local"); var other = TestDbHelper.SeedUser(db, "other", "other@t.local"); var outsider = TestDbHelper.SeedUser(db, "outsider", "outsider@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, IsUsed = true }; db.PlayerCards.Add(card); await db.SaveChangesAsync(); // outsider ist nicht in diesem Spiel und versucht eine Challenge anzulegen await Assert.ThrowsAsync( () => _sut.CreateChallengeAsync(game.Id, hostPlayer.Id + 1000, card.Id)); } [Fact] public async Task CreateChallengeAsync_Throws_WhenChallengerFromOtherGame() { using var db = _factory.CreateDbContext(); var hostA = TestDbHelper.SeedUser(db, "hostA", "hostA@t.local"); var hostB = TestDbHelper.SeedUser(db, "hostB", "hostB@t.local"); var other = TestDbHelper.SeedUser(db, "other", "other@t.local"); var (gameA, hostPlayerA) = TestDbHelper.SeedGame(db, hostA); var (gameB, _) = TestDbHelper.SeedGame(db, hostB); var otherPlayerB = new Player { UserId = other.Id, GameId = gameB.Id }; db.Players.Add(otherPlayerB); var roundB = new GameRound { GameId = gameB.Id, RoundNumber = 1, Status = RoundStatus.Active }; db.GameRounds.Add(roundB); var phrase = TestDbHelper.SeedPhrases(db, 1).Single(); var cardB = new PlayerCard { PlayerId = otherPlayerB.Id, PhraseId = phrase.Id, GameRoundId = roundB.Id, IsUsed = true }; db.PlayerCards.Add(cardB); await db.SaveChangesAsync(); // hostPlayerA gehört zu gameA, versucht aber eine Karte aus gameB zu challengen await Assert.ThrowsAsync( () => _sut.CreateChallengeAsync(gameB.Id, hostPlayerA.Id, cardB.Id)); } // ---------- ResolveChallengeAsync ---------- [Fact] public async Task ResolveChallengeAsync_Approved_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, IsUsed = true }; 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); // Berechtigte Beschuldigung: 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_Rejected_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, IsUsed = true }; 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); // Falsche Beschuldigung: 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_Throws_WhenChallengeNotFound() { await Assert.ThrowsAsync(() => _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(() => _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, Status = RoundStatus.Active }; 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(() => _sut.GetGameAsync(999)); } }