using System.Net; using System.Net.Http.Json; using System.Text.Json; using GameList.Domain; using GameList.Tests.Support; using Microsoft.EntityFrameworkCore; using GameList.Data; using Microsoft.Extensions.DependencyInjection; namespace GameList.Tests; public class StateTests { [Fact] public async Task State_endpoint_returns_expected_payload_for_authenticated_user() { await using var factory = new TestWebApplicationFactory(); var client = factory.CreateClientWithCookies(); await client.RegisterAsync("payload"); await factory.WithDbContextAsync(async db => { var player = await db.Players.SingleAsync(); player.HasJoker = true; await db.SaveChangesAsync(); }); await client.CreateSuggestionAsync("One"); var state = await client.GetFromJsonAsync("/api/state"); Assert.Equal(nameof(Phase.Suggest), state.GetProperty("currentPhase").GetString()); Assert.False(state.GetProperty("votesFinal").GetBoolean()); Assert.True(state.GetProperty("hasJoker").GetBoolean()); Assert.True(state.GetProperty("players").GetInt32() >= 1); Assert.True(state.GetProperty("suggestions").GetInt32() >= 1); Assert.True(state.GetProperty("votes").GetInt32() >= 0); } [Fact] public async Task GetCurrentPhase_reads_effective_phase_without_mutating_player() { await using var factory = new TestWebApplicationFactory(); Guid playerId = Guid.Empty; await factory.WithDbContextAsync(async db => { var player = new Player { Id = Guid.NewGuid(), Username = "legacy", NormalizedUsername = "legacy", PasswordHash = [1], PasswordSalt = [1], DisplayName = "Legacy", CurrentPhase = (Phase)1, VotesFinal = true }; playerId = player.Id; db.Players.Add(player); var state = await db.AppState.SingleAsync(); state.ResultsOpen = true; await db.SaveChangesAsync(); }); using (var scope = factory.Services.CreateScope()) { var db = scope.ServiceProvider.GetRequiredService(); var phase = await Endpoints.EndpointHelpers.GetCurrentPhaseAsync(db, playerId); Assert.Equal(Phase.Results, phase); } await factory.WithDbContextAsync(async db => { var state = await db.AppState.SingleAsync(); state.ResultsOpen = false; await db.SaveChangesAsync(); }); using (var scope = factory.Services.CreateScope()) { var db = scope.ServiceProvider.GetRequiredService(); var phase = await Endpoints.EndpointHelpers.GetCurrentPhaseAsync(db, playerId); var player = await db.Players.FindAsync(playerId); Assert.Equal(Phase.Vote, phase); Assert.Equal((Phase)1, player!.CurrentPhase); Assert.True(player.VotesFinal); } } [Fact] public async Task Phase_next_advances_and_clears_votesfinal() { await using var factory = new TestWebApplicationFactory(); var client = factory.CreateClientWithCookies(); await client.RegisterAsync("advance"); await client.CreateSuggestionAsync("Advance game"); await factory.WithDbContextAsync(async db => { var player = await db.Players.SingleAsync(); player.VotesFinal = true; await db.SaveChangesAsync(); }); var toVote = await client.PostAsJsonAsync("/api/me/phase/next", new { }); toVote.EnsureSuccessStatusCode(); var toResultsLocked = await client.PostAsJsonAsync("/api/me/phase/next", new { }); Assert.Equal(HttpStatusCode.BadRequest, toResultsLocked.StatusCode); // unlock results and advance var admin = factory.CreateClientWithCookies(); await admin.RegisterAsync("admin", admin: true); await admin.PostAsJsonAsync("/api/admin/results", new { resultsOpen = true }); var toResults = await client.PostAsJsonAsync("/api/me/phase/next", new { }); toResults.EnsureSuccessStatusCode(); var me = await client.GetFromJsonAsync("/api/me"); Assert.False(me.GetProperty("votesFinal").GetBoolean()); Assert.Equal(nameof(Phase.Results), me.GetProperty("currentPhase").GetString()); } [Fact] public async Task Phase_prev_moves_back_and_clears_votesfinal() { await using var factory = new TestWebApplicationFactory(); var admin = factory.CreateClientWithCookies(); await admin.RegisterAsync("admin", admin: true); await admin.CreateSuggestionAsync("Admin game"); await admin.PostAsJsonAsync("/api/me/phase/next", new { }); // Vote await factory.WithDbContextAsync(async db => { var player = await db.Players.SingleAsync(); player.VotesFinal = true; await db.SaveChangesAsync(); }); var backToSuggest = await admin.PostAsJsonAsync("/api/me/phase/prev", new { }); backToSuggest.EnsureSuccessStatusCode(); var me = await admin.GetFromJsonAsync("/api/me"); Assert.Equal(nameof(Phase.Suggest), me.GetProperty("currentPhase").GetString()); Assert.False(me.GetProperty("votesFinal").GetBoolean()); } [Fact] public async Task Cannot_advance_to_results_when_locked() { await using var factory = new TestWebApplicationFactory(); var client = factory.CreateClientWithCookies(); await client.RegisterAsync("player"); await client.CreateSuggestionAsync("Player game"); var toVote = await client.PostAsync("/api/me/phase/next", JsonContent.Create(new { })); toVote.EnsureSuccessStatusCode(); var toResults = await client.PostAsync("/api/me/phase/next", JsonContent.Create(new { })); Assert.Equal(HttpStatusCode.BadRequest, toResults.StatusCode); } [Fact] public async Task Phase_next_from_suggest_requires_at_least_one_suggestion() { await using var factory = new TestWebApplicationFactory(); var client = factory.CreateClientWithCookies(); await client.RegisterAsync("nosuggest"); var response = await client.PostAsJsonAsync("/api/me/phase/next", new { }); Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); var me = await client.GetFromJsonAsync("/api/me"); Assert.Equal(nameof(Phase.Suggest), me.GetProperty("currentPhase").GetString()); } [Fact] public async Task Admin_opening_results_moves_players_to_results_phase() { await using var factory = new TestWebApplicationFactory(); var admin = factory.CreateClientWithCookies(); await admin.RegisterAsync("admin", admin: true); var player = factory.CreateClientWithCookies(); await player.RegisterAsync("user"); var toggle = await admin.PostAsJsonAsync("/api/admin/results", new { resultsOpen = true }); toggle.EnsureSuccessStatusCode(); var state = await player.GetFromJsonAsync("/api/state"); Assert.Equal(nameof(Phase.Results), state.GetProperty("currentPhase").GetString()); Assert.True(state.GetProperty("resultsOpen").GetBoolean()); } [Fact] public async Task Display_name_cannot_be_changed_after_registration() { await using var factory = new TestWebApplicationFactory(); var client = factory.CreateClientWithCookies(); var username = "fixedname"; await client.RegisterAsync(username); var originalDisplay = $"{username}-name"; var attempt = await client.PostAsJsonAsync("/api/me/name", new { name = "New Name" }); Assert.Equal(HttpStatusCode.NotFound, attempt.StatusCode); var me = await client.GetFromJsonAsync("/api/me"); Assert.Equal(originalDisplay, me.GetProperty("displayName").GetString()); } [Fact] public async Task Phase_prev_admin_only() { await using var factory = new TestWebApplicationFactory(); var player = factory.CreateClientWithCookies(); await player.RegisterAsync("phase"); var notAdmin = await player.PostAsJsonAsync("/api/me/phase/prev", new { }); Assert.Equal(HttpStatusCode.BadRequest, notAdmin.StatusCode); var admin = factory.CreateClientWithCookies(); await admin.RegisterAsync("admin", admin: true); await admin.CreateSuggestionAsync("Admin phase game"); await admin.PostAsJsonAsync("/api/me/phase/next", new { }); // to Vote var back = await admin.PostAsJsonAsync("/api/me/phase/prev", new { }); back.EnsureSuccessStatusCode(); var me = await admin.GetFromJsonAsync("/api/me"); Assert.Equal(nameof(Phase.Suggest), me.GetProperty("currentPhase").GetString()); } [Fact] public async Task Phase_prev_with_granted_joker_moves_player_back_once_and_consumes_it() { await using var factory = new TestWebApplicationFactory(); var admin = factory.CreateClientWithCookies(); await admin.RegisterAsync("admin", admin: true); var player = factory.CreateClientWithCookies(); await player.RegisterAsync("jokerback"); await player.AdvanceToVoteAsync("Joker back seed"); var grant = await admin.PostAsJsonAsync("/api/admin/joker", new { playerId = await player.GetProfileIdAsync() }); grant.EnsureSuccessStatusCode(); var back = await player.PostAsJsonAsync("/api/me/phase/prev", new { }); back.EnsureSuccessStatusCode(); var meAfterBack = await player.GetFromJsonAsync("/api/me"); Assert.Equal(nameof(Phase.Suggest), meAfterBack.GetProperty("currentPhase").GetString()); Assert.False(meAfterBack.GetProperty("hasJoker").GetBoolean()); await player.PostAsJsonAsync("/api/me/phase/next", new { }); var denied = await player.PostAsJsonAsync("/api/me/phase/prev", new { }); Assert.Equal(HttpStatusCode.BadRequest, denied.StatusCode); } [Fact] public async Task State_endpoint_requires_auth_and_counts() { await using var factory = new TestWebApplicationFactory(); var anon = factory.CreateClient(); var unauthorized = await anon.GetAsync("/api/state"); Assert.Equal(HttpStatusCode.Unauthorized, unauthorized.StatusCode); var unauthorizedJson = await unauthorized.Content.ReadFromJsonAsync(); Assert.Equal("Unauthorized", unauthorizedJson.GetProperty("title").GetString()); Assert.Equal("Unauthorized", unauthorizedJson.GetProperty("detail").GetString()); Assert.Equal("Unauthorized", unauthorizedJson.GetProperty("error").GetString()); var client = factory.CreateClientWithCookies(); await client.RegisterAsync("counting"); await client.CreateSuggestionAsync("One"); var state = await client.GetFromJsonAsync("/api/state"); Assert.True(state.TryGetProperty("Players", out var players) || state.TryGetProperty("players", out players)); Assert.True(players.GetInt32() >= 1); Assert.True(state.TryGetProperty("Suggestions", out var suggestions) || state.TryGetProperty("suggestions", out suggestions)); Assert.True(suggestions.GetInt32() >= 1); } [Fact] public async Task State_endpoint_with_stale_cookie_returns_unauthorized_and_clears_cookie() { await using var factory = new TestWebApplicationFactory(); var client = factory.CreateClientWithCookies(); await client.RegisterAsync("stale"); await factory.WithDbContextAsync(async db => { var player = await db.Players.SingleAsync(); db.Players.Remove(player); await db.SaveChangesAsync(); }); var resp = await client.GetAsync("/api/state"); Assert.Equal(HttpStatusCode.Unauthorized, resp.StatusCode); Assert.True(resp.Headers.TryGetValues("Set-Cookie", out var cookies)); Assert.Contains(cookies, c => c.Contains("player=", StringComparison.OrdinalIgnoreCase)); } [Fact] public async Task Health_endpoint_ok() { await using var factory = new TestWebApplicationFactory(); var resp = await factory.CreateClient().GetFromJsonAsync("/health"); Assert.Equal("ok", resp.GetProperty("status").GetString()); } [Fact] public async Task State_and_me_reads_do_not_persist_phase_reconciliation() { await using var factory = new TestWebApplicationFactory(); var client = factory.CreateClientWithCookies(); await client.RegisterAsync("reader"); await factory.WithDbContextAsync(async db => { var player = await db.Players.SingleAsync(); player.CurrentPhase = Phase.Results; player.VotesFinal = true; var state = await db.AppState.SingleAsync(); state.ResultsOpen = false; await db.SaveChangesAsync(); }); var stateResponse = await client.GetFromJsonAsync("/api/state"); var meResponse = await client.GetFromJsonAsync("/api/me"); Assert.Equal(nameof(Phase.Vote), stateResponse.GetProperty("currentPhase").GetString()); Assert.Equal(nameof(Phase.Vote), meResponse.GetProperty("currentPhase").GetString()); await factory.WithDbContextAsync(async db => { var player = await db.Players.AsNoTracking().SingleAsync(); Assert.Equal(Phase.Results, player.CurrentPhase); Assert.True(player.VotesFinal); }); } [Fact] public async Task GetPhase_aligns_to_results_when_open() { await using var factory = new TestWebApplicationFactory(); await factory.WithDbContextAsync(async db => { var player = new Player { Id = Guid.NewGuid(), Username = "phase", NormalizedUsername = "phase", PasswordHash = [1], PasswordSalt = [1], DisplayName = "phase", CurrentPhase = Phase.Vote }; db.Players.Add(player); var state = await db.AppState.SingleAsync(); state.ResultsOpen = true; await db.SaveChangesAsync(); }); using var scope = factory.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); var playerId = await db.Players.Select(p => p.Id).SingleAsync(); var phase = await Endpoints.EndpointHelpers.GetCurrentPhaseAsync(db, playerId); Assert.Equal(Phase.Results, phase); } }