Files
GameList/GameList.Tests/StateTests.cs

341 lines
14 KiB
C#

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<JsonElement>("/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<AppDbContext>();
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<AppDbContext>();
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<JsonElement>("/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<JsonElement>("/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<JsonElement>("/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<JsonElement>("/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<JsonElement>("/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<JsonElement>("/api/me");
Assert.Equal(nameof(Phase.Suggest), me.GetProperty("currentPhase").GetString());
}
[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<JsonElement>();
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<JsonElement>("/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<JsonElement>("/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<JsonElement>("/api/state");
var meResponse = await client.GetFromJsonAsync<JsonElement>("/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<AppDbContext>();
var playerId = await db.Players.Select(p => p.Id).SingleAsync();
var phase = await Endpoints.EndpointHelpers.GetCurrentPhaseAsync(db, playerId);
Assert.Equal(Phase.Results, phase);
}
}