Slip- und Beschuldigungslogik überarbeitet
All checks were successful
Build / build (push) Successful in 2m19s

Slips werden jetzt 30 Sekunden vorgehalten und anschließend automatisch abgeschlossen; direkte Spieler-Beschuldigungen, Win-Status, SignalR-Events, MAUI-UI, EF-Mapping/Migrationen sowie Tests wurden an die neue Spiellogik angepasst.
This commit is contained in:
Tim Krampitz
2026-10-04 19:14:16 +02:00
parent f54b177441
commit 010e844976
18 changed files with 1272 additions and 166 deletions

View File

@@ -244,7 +244,7 @@ public class GameServiceTests
// ---------- SubmitSlipAsync ----------
[Fact]
public async Task SubmitSlipAsync_MarksCardAsUsed()
public async Task SubmitSlipAsync_HoldsCardFor30Seconds_ThenCompletes()
{
using var db = _factory.CreateDbContext();
var host = TestDbHelper.SeedUser(db);
@@ -260,7 +260,15 @@ public class GameServiceTests
Assert.True(result);
using var verify = _factory.CreateDbContext();
Assert.True(verify.PlayerCards.Single(c => c.Id == card.Id).IsUsed);
var verifyCard = verify.PlayerCards.Single(c => c.Id == card.Id);
Assert.NotNull(verifyCard.SlippedAt);
Assert.False(verifyCard.IsUsed); // Für 30 Sekunden vorgehalten!
// CompleteSlipAsync vollendet nach Ablauf der Zeit
var completed = await _sut.CompleteSlipAsync(game.Id, card.Id, force: true);
Assert.True(completed);
using var verify2 = _factory.CreateDbContext();
Assert.True(verify2.PlayerCards.Single(c => c.Id == card.Id).IsUsed);
}
[Fact]
@@ -307,11 +315,11 @@ public class GameServiceTests
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 };
var card = new PlayerCard { PlayerId = otherPlayer.Id, PhraseId = phrase.Id, GameRoundId = round.Id, IsUsed = false, SlippedAt = DateTime.UtcNow };
db.PlayerCards.Add(card);
await db.SaveChangesAsync();
var challenge = await _sut.CreateChallengeAsync(game.Id, hostPlayer.Id, card.Id);
var challenge = await _sut.CreateChallengeAsync(game.Id, hostPlayer.Id, otherPlayer.Id);
Assert.Equal(round.Id, challenge.GameRoundId);
Assert.Equal(hostPlayer.Id, challenge.ChallengingPlayerId);
@@ -321,9 +329,37 @@ public class GameServiceTests
}
[Fact]
public async Task CreateChallengeAsync_Throws_WhenTargetCardNotFound()
public async Task CreateChallengeAsync_ChallengesPlayerDirectly()
{
await Assert.ThrowsAsync<InvalidOperationException>(() => _sut.CreateChallengeAsync(1, 1, 999));
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);
await db.SaveChangesAsync();
var challenge = await _sut.CreateChallengeAsync(game.Id, hostPlayer.Id, otherPlayer.Id);
Assert.Equal(round.Id, challenge.GameRoundId);
Assert.Equal(hostPlayer.Id, challenge.ChallengingPlayerId);
Assert.Equal(otherPlayer.Id, challenge.TargetPlayerId);
Assert.Equal(ChallengeStatus.Pending, challenge.Status);
}
[Fact]
public async Task CreateChallengeAsync_Throws_WhenTargetPlayerNotFound()
{
using var db = _factory.CreateDbContext();
var host = TestDbHelper.SeedUser(db, "host", "host@t.local");
var (game, hostPlayer) = TestDbHelper.SeedGame(db, host);
var round = new GameRound { GameId = game.Id, RoundNumber = 1, Status = RoundStatus.Active };
db.GameRounds.Add(round);
await db.SaveChangesAsync();
await Assert.ThrowsAsync<InvalidOperationException>(() => _sut.CreateChallengeAsync(game.Id, hostPlayer.Id, 999));
}
[Fact]
@@ -331,7 +367,10 @@ public class GameServiceTests
{
using var db = _factory.CreateDbContext();
var host = TestDbHelper.SeedUser(db);
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 phrase = TestDbHelper.SeedPhrases(db, 1).Single();
var round = new GameRound { GameId = game.Id, RoundNumber = 1, Status = RoundStatus.Waiting };
db.GameRounds.Add(round);
@@ -339,7 +378,7 @@ public class GameServiceTests
db.PlayerCards.Add(card);
await db.SaveChangesAsync();
await Assert.ThrowsAsync<InvalidOperationException>(() => _sut.CreateChallengeAsync(game.Id, hostPlayer.Id, card.Id));
await Assert.ThrowsAsync<InvalidOperationException>(() => _sut.CreateChallengeAsync(game.Id, hostPlayer.Id, otherPlayer.Id));
}
[Fact]
@@ -361,7 +400,7 @@ public class GameServiceTests
// outsider ist nicht in diesem Spiel und versucht eine Challenge anzulegen
await Assert.ThrowsAsync<UnauthorizedAccessException>(
() => _sut.CreateChallengeAsync(game.Id, hostPlayer.Id + 1000, card.Id));
() => _sut.CreateChallengeAsync(game.Id, hostPlayer.Id + 1000, otherPlayer.Id));
}
[Fact]
@@ -382,15 +421,15 @@ public class GameServiceTests
db.PlayerCards.Add(cardB);
await db.SaveChangesAsync();
// hostPlayerA gehört zu gameA, versucht aber eine Karte aus gameB zu challengen
// hostPlayerA gehört zu gameA, versucht aber einen Spieler aus gameB zu challengen
await Assert.ThrowsAsync<UnauthorizedAccessException>(
() => _sut.CreateChallengeAsync(gameB.Id, hostPlayerA.Id, cardB.Id));
() => _sut.CreateChallengeAsync(gameB.Id, hostPlayerA.Id, otherPlayerB.Id));
}
// ---------- ResolveChallengeAsync ----------
[Fact]
public async Task ResolveChallengeAsync_Approved_TransfersCardToChallenger()
public async Task ResolveChallengeAsync_Approved_DiscardsCardAndDrawsTwoNewCards()
{
using var db = _factory.CreateDbContext();
var host = TestDbHelper.SeedUser(db, "host", "host@t.local");
@@ -400,8 +439,8 @@ public class GameServiceTests
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 };
var phrases = TestDbHelper.SeedPhrases(db, 5);
var card = new PlayerCard { PlayerId = otherPlayer.Id, PhraseId = phrases[0].Id, GameRoundId = round.Id, IsUsed = false, SlippedAt = DateTime.UtcNow };
db.PlayerCards.Add(card);
var challenge = new SlipChallenge
{
@@ -418,13 +457,19 @@ public class GameServiceTests
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);
// Vorgehaltene Karte zählt nicht und wird abgelegt (IsUsed = true)
Assert.True(verify.PlayerCards.Single(c => c.Id == card.Id).IsUsed);
// Beschuldigter erhält 2 neue Karten (insgesamt 3 Karten für otherPlayer)
Assert.Equal(3, verify.PlayerCards.Count(c => c.PlayerId == otherPlayer.Id));
// 2 davon aktiv
Assert.Equal(2, verify.PlayerCards.Count(c => c.PlayerId == otherPlayer.Id && !c.IsUsed));
// Challenger erhält +1 Score
Assert.Equal(1, verify.Players.Single(p => p.Id == hostPlayer.Id).Score);
}
[Fact]
public async Task ResolveChallengeAsync_Rejected_KeepsCardWithTargetPlayer()
public async Task ResolveChallengeAsync_Rejected_WithdrawsChallengeWithoutPenalty()
{
using var db = _factory.CreateDbContext();
var host = TestDbHelper.SeedUser(db, "host", "host@t.local");
@@ -434,8 +479,8 @@ public class GameServiceTests
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 };
var phrases = TestDbHelper.SeedPhrases(db, 3);
var card = new PlayerCard { PlayerId = otherPlayer.Id, PhraseId = phrases[0].Id, GameRoundId = round.Id, IsUsed = false, SlippedAt = DateTime.UtcNow };
db.PlayerCards.Add(card);
var challenge = new SlipChallenge
{
@@ -452,9 +497,11 @@ public class GameServiceTests
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);
// Keine Karten abgelegt oder neu gezogen
Assert.Single(verify.PlayerCards.Where(c => c.PlayerId == otherPlayer.Id));
Assert.False(verify.PlayerCards.Single(c => c.Id == card.Id).IsUsed);
Assert.Equal(0, verify.Players.Single(p => p.Id == hostPlayer.Id).Score);
}
[Fact]
@@ -463,6 +510,33 @@ public class GameServiceTests
await Assert.ThrowsAsync<InvalidOperationException>(() => _sut.ResolveChallengeAsync(999, true));
}
// ---------- Win Condition (Point 3) ----------
[Fact]
public async Task CompleteSlipAsync_PlayerWins_WhenNoCardsRemain()
{
using var db = _factory.CreateDbContext();
var host = TestDbHelper.SeedUser(db);
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, SlippedAt = DateTime.UtcNow.AddSeconds(-31) };
db.PlayerCards.Add(card);
await db.SaveChangesAsync();
var completed = await _sut.CompleteSlipAsync(game.Id, card.Id);
Assert.True(completed);
using var verify = _factory.CreateDbContext();
var verifyGame = verify.Games.Single(g => g.Id == game.Id);
Assert.Equal(GameStatus.Completed, verifyGame.Status);
Assert.Equal(hostPlayer.Id, verifyGame.WinnerPlayerId);
}
// ---------- GetGameStateAsync ----------
[Fact]