using System.Net; using System.Net.Http.Json; using System.Text.Json; using GameList.Domain; using GameList.Tests.Support; using Microsoft.EntityFrameworkCore; namespace GameList.Tests; public class AdminTests { [Fact] public async Task Admin_vote_status_marks_ready_when_all_finalized() { await using var factory = new TestWebApplicationFactory(); var admin = factory.CreateClientWithCookies(); await admin.RegisterAsync("admin", admin: true); await admin.AdvanceToVoteAsync("Admin seed"); // move to Vote var p1 = factory.CreateClientWithCookies(); await p1.RegisterAsync("alice"); var p2 = factory.CreateClientWithCookies(); await p2.RegisterAsync("bob"); await p2.AdvanceToVoteAsync("Bob seed"); var s1 = await p1.CreateSuggestionAsync("A"); await p1.PostAsJsonAsync("/api/me/phase/next", new { }); await p1.PostAsJsonAsync("/api/votes", new { SuggestionId = s1, Score = 5 }); await p1.PostAsJsonAsync("/api/votes/finalize", new { Final = true }); await p2.PostAsJsonAsync("/api/votes", new { SuggestionId = s1, Score = 7 }); await p2.PostAsJsonAsync("/api/votes/finalize", new { Final = true }); await admin.PostAsJsonAsync("/api/votes/finalize", new { Final = true }); var status = await admin.GetFromJsonAsync("/api/admin/vote-status"); Assert.True(status.GetProperty("ready").GetBoolean()); Assert.Equal(0, status.GetProperty("waiting").GetArrayLength()); } [Fact] public async Task Grant_joker_only_in_vote_phase() { await using var factory = new TestWebApplicationFactory(); var admin = factory.CreateClientWithCookies(); await admin.RegisterAsync("admin", admin: true); var p = factory.CreateClientWithCookies(); await p.RegisterAsync("player"); var give = await admin.PostAsJsonAsync("/api/admin/joker", new { playerId = (await p.GetProfileIdAsync()) }); Assert.Equal(HttpStatusCode.BadRequest, give.StatusCode); } [Fact] public async Task Delete_player_cascades_suggestions_and_votes() { await using var factory = new TestWebApplicationFactory(); var admin = factory.CreateClientWithCookies(); await admin.RegisterAsync("admin", admin: true); var player = factory.CreateClientWithCookies(); await player.RegisterAsync("deleteme"); var suggestionId = await player.CreateSuggestionAsync("DeleteGame"); await player.PostAsJsonAsync("/api/me/phase/next", new { }); await player.PostAsJsonAsync("/api/votes", new { SuggestionId = suggestionId, Score = 8 }); var resp = await admin.DeleteAsync($"/api/admin/players/{await player.GetProfileIdAsync()}"); resp.EnsureSuccessStatusCode(); await factory.WithDbContextAsync(db => { try { Assert.Single(db.Players); // admin remains Assert.Empty(db.Suggestions); Assert.Empty(db.Votes); return Task.CompletedTask; } catch (Exception exception) { return Task.FromException(exception); } }); } [Fact] public async Task Link_suggestions_errors_on_same_id_and_already_linked() { await using var factory = new TestWebApplicationFactory(); var admin = factory.CreateClientWithCookies(); await admin.RegisterAsync("admin", admin: true); var player = factory.CreateClientWithCookies(); await player.RegisterAsync("linker"); var a = await player.CreateSuggestionAsync("Game A"); var b = await player.CreateSuggestionAsync("Game B"); await player.PostAsJsonAsync("/api/me/phase/next", new { }); await admin.AdvanceToVoteAsync("Admin link seed"); var same = await admin.PostAsJsonAsync("/api/admin/link-suggestions", new { SourceSuggestionId = a, TargetSuggestionId = a }); Assert.Equal(HttpStatusCode.BadRequest, same.StatusCode); var first = await admin.PostAsJsonAsync("/api/admin/link-suggestions", new { SourceSuggestionId = a, TargetSuggestionId = b }); first.EnsureSuccessStatusCode(); var already = await admin.PostAsJsonAsync("/api/admin/link-suggestions", new { SourceSuggestionId = a, TargetSuggestionId = b }); Assert.Equal(HttpStatusCode.BadRequest, already.StatusCode); } [Fact] public async Task Unlink_suggestions_clears_group_votes() { await using var factory = new TestWebApplicationFactory(); var admin = factory.CreateClientWithCookies(); await admin.RegisterAsync("admin", admin: true); var player = factory.CreateClientWithCookies(); await player.RegisterAsync("unlinker"); var a = await player.CreateSuggestionAsync("Game A"); var b = await player.CreateSuggestionAsync("Game B"); await player.PostAsJsonAsync("/api/me/phase/next", new { }); await admin.AdvanceToVoteAsync("Admin unlink seed"); await admin.PostAsJsonAsync("/api/admin/link-suggestions", new { SourceSuggestionId = a, TargetSuggestionId = b }); await player.PostAsJsonAsync("/api/votes", new { SuggestionId = a, Score = 6 }); var resp = await admin.PostAsJsonAsync("/api/admin/unlink-suggestions", new { suggestionId = a }); resp.EnsureSuccessStatusCode(); await factory.WithDbContextAsync(db => { try { Assert.Empty(db.Votes); Assert.All(db.Suggestions, s => Assert.Null(s.ParentSuggestionId)); return Task.CompletedTask; } catch (Exception exception) { return Task.FromException(exception); } }); } [Fact] public async Task Reset_and_factory_reset_clear_state() { await using var factory = new TestWebApplicationFactory(); var admin = factory.CreateClientWithCookies(); await admin.RegisterAsync("admin", admin: true); var player = factory.CreateClientWithCookies(); await player.RegisterAsync("player"); await player.CreateSuggestionAsync("Keep"); var reset = await admin.PostAsJsonAsync("/api/admin/reset", new { }); reset.EnsureSuccessStatusCode(); await factory.WithDbContextAsync(db => { try { Assert.Empty(db.Suggestions); Assert.Empty(db.Votes); Assert.All(db.Players, p => Assert.Equal(Phase.Suggest, p.CurrentPhase)); return Task.CompletedTask; } catch (Exception exception) { return Task.FromException(exception); } }); var factoryReset = await admin.PostAsJsonAsync("/api/admin/factory-reset", new { }); factoryReset.EnsureSuccessStatusCode(); await factory.WithDbContextAsync(db => { try { Assert.Empty(db.Players); Assert.Single(db.AppState); return Task.CompletedTask; } catch (Exception exception) { return Task.FromException(exception); } }); } [Fact] public async Task Admin_results_closing_moves_back_to_vote_and_clears_finalize() { await using var factory = new TestWebApplicationFactory(); var admin = factory.CreateClientWithCookies(); await admin.RegisterAsync("admin", admin: true); var player = factory.CreateClientWithCookies(); await player.RegisterAsync("player"); var open = await admin.PostAsJsonAsync("/api/admin/results", new { resultsOpen = true }); open.EnsureSuccessStatusCode(); await factory.WithDbContextAsync(async db => { var p = await db.Players.FirstAsync(x => !x.IsAdmin); p.VotesFinal = true; var state = await db.AppState.SingleAsync(); state.UpdatedAt = DateTimeOffset.UnixEpoch; await db.SaveChangesAsync(); }); var close = await admin.PostAsJsonAsync("/api/admin/results", new { resultsOpen = false }); close.EnsureSuccessStatusCode(); await factory.WithDbContextAsync(async db => { var p = await db.Players.FirstAsync(x => !x.IsAdmin); Assert.Equal(Phase.Vote, p.CurrentPhase); Assert.False(p.VotesFinal); var state = await db.AppState.AsNoTracking().SingleAsync(); Assert.False(state.ResultsOpen); Assert.True(state.UpdatedAt > DateTimeOffset.UnixEpoch); }); } [Fact] public async Task Vote_status_lists_waiting_players() { await using var factory = new TestWebApplicationFactory(); var admin = factory.CreateClientWithCookies(); await admin.RegisterAsync("admin", admin: true); await admin.AdvanceToVoteAsync("Admin vote status seed"); var p1 = factory.CreateClientWithCookies(); await p1.RegisterAsync("alice"); var p2 = factory.CreateClientWithCookies(); await p2.RegisterAsync("bob"); var s = await p1.CreateSuggestionAsync("Game"); await p1.PostAsJsonAsync("/api/me/phase/next", new { }); await p2.AdvanceToVoteAsync("Bob vote seed"); await p1.PostAsJsonAsync("/api/votes", new { SuggestionId = s, Score = 5 }); await p1.PostAsJsonAsync("/api/votes/finalize", new { Final = true }); var status = await admin.GetFromJsonAsync("/api/admin/vote-status"); Assert.False(status.GetProperty("ready").GetBoolean()); var waiting = status.GetProperty("waiting").EnumerateArray().Select(e => e.GetString()).ToList(); Assert.Contains("bob-name", waiting); } [Fact] public async Task Grant_joker_in_vote_sets_flag_and_unfinalizes() { await using var factory = new TestWebApplicationFactory(); var admin = factory.CreateClientWithCookies(); await admin.RegisterAsync("admin", admin: true); var p = factory.CreateClientWithCookies(); await p.RegisterAsync("player"); await p.AdvanceToVoteAsync("Player joker seed"); await p.PostAsJsonAsync("/api/votes/finalize", new { Final = true }); var give = await admin.PostAsJsonAsync("/api/admin/joker", new { playerId = (await p.GetProfileIdAsync()) }); give.EnsureSuccessStatusCode(); await factory.WithDbContextAsync(async db => { var player = await db.Players.SingleAsync(x => x.Username == "player"); Assert.True(player.HasJoker); Assert.False(player.VotesFinal); }); } [Fact] public async Task Link_requires_vote_phase_and_reparents_votes_reset() { await using var factory = new TestWebApplicationFactory(); var admin = factory.CreateClientWithCookies(); await admin.RegisterAsync("admin", admin: true); var player = factory.CreateClientWithCookies(); await player.RegisterAsync("linker"); var a = await player.CreateSuggestionAsync("A"); var b = await player.CreateSuggestionAsync("B"); var beforeVotePhase = await admin.PostAsJsonAsync("/api/admin/link-suggestions", new { SourceSuggestionId = a, TargetSuggestionId = b }); Assert.Equal(HttpStatusCode.BadRequest, beforeVotePhase.StatusCode); await admin.AdvanceToVoteAsync("Admin link-phase seed"); await player.PostAsJsonAsync("/api/me/phase/next", new { }); await player.PostAsJsonAsync("/api/votes", new { SuggestionId = a, Score = 3 }); await player.PostAsJsonAsync("/api/votes/finalize", new { Final = true }); var link = await admin.PostAsJsonAsync("/api/admin/link-suggestions", new { SourceSuggestionId = a, TargetSuggestionId = b }); link.EnsureSuccessStatusCode(); await factory.WithDbContextAsync(async db => { var votes = await db.Votes.ToListAsync(); Assert.Empty(votes); var p = await db.Players.SingleAsync(x => x.Username == "linker"); Assert.False(p.VotesFinal); }); } [Fact] public async Task Link_unfinalizes_all_players() { await using var factory = new TestWebApplicationFactory(); var admin = factory.CreateClientWithCookies(); await admin.RegisterAsync("admin", admin: true); var p1 = factory.CreateClientWithCookies(); await p1.RegisterAsync("p1"); var p2 = factory.CreateClientWithCookies(); await p2.RegisterAsync("p2"); var a = await p1.CreateSuggestionAsync("A"); var b = await p1.CreateSuggestionAsync("B"); await admin.AdvanceToVoteAsync("Admin unfinalize seed"); await p1.PostAsJsonAsync("/api/me/phase/next", new { }); await p2.AdvanceToVoteAsync("P2 unfinalize seed"); await p1.PostAsJsonAsync("/api/votes/finalize", new { Final = true }); await p2.PostAsJsonAsync("/api/votes/finalize", new { Final = true }); var link = await admin.PostAsJsonAsync("/api/admin/link-suggestions", new { SourceSuggestionId = a, TargetSuggestionId = b }); link.EnsureSuccessStatusCode(); await factory.WithDbContextAsync(async db => { var players = await db.Players.Where(p => !p.IsAdmin).ToListAsync(); Assert.All(players, p => Assert.False(p.VotesFinal)); }); } [Fact] public async Task Unlink_not_found_returns_empty_payload() { await using var factory = new TestWebApplicationFactory(); var admin = factory.CreateClientWithCookies(); await admin.RegisterAsync("admin", admin: true); await admin.AdvanceToVoteAsync("Admin unlink not-found seed"); var resp = await admin.PostAsJsonAsync("/api/admin/unlink-suggestions", new { suggestionId = 9999 }); resp.EnsureSuccessStatusCode(); var json = await resp.Content.ReadFromJsonAsync(); Assert.Equal(0, json.GetProperty("unlinkedSuggestionIds").GetArrayLength()); } [Fact] public async Task Reset_clears_flags_and_factory_reset_seeds_defaults() { await using var factory = new TestWebApplicationFactory(); var admin = factory.CreateClientWithCookies(); await admin.RegisterAsync("admin", admin: true); var p = factory.CreateClientWithCookies(); await p.RegisterAsync("flags"); await factory.WithDbContextAsync(async db => { var player = await db.Players.SingleAsync(x => x.Username == "flags"); player.HasJoker = true; player.VotesFinal = true; await db.SaveChangesAsync(); }); var reset = await admin.PostAsJsonAsync("/api/admin/reset", new { }); reset.EnsureSuccessStatusCode(); await factory.WithDbContextAsync(async db => { var player = await db.Players.SingleAsync(x => x.Username == "flags"); Assert.False(player.HasJoker); Assert.False(player.VotesFinal); var state = await db.AppState.AsNoTracking().SingleAsync(); Assert.False(state.ResultsOpen); }); var factoryReset = await admin.PostAsJsonAsync("/api/admin/factory-reset", new { }); factoryReset.EnsureSuccessStatusCode(); await factory.WithDbContextAsync(async db => { var state = await db.AppState.AsNoTracking().SingleAsync(); Assert.False(state.ResultsOpen); }); } }