318 lines
12 KiB
C#
318 lines
12 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.FirstAsync();
|
|
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.FirstAsync();
|
|
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.FirstAsync();
|
|
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 factory.WithDbContextAsync(async db =>
|
|
{
|
|
var player = await db.Players.FirstAsync();
|
|
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.PostAsJsonAsync("/api/me/phase/next", new { }); // Vote
|
|
await factory.WithDbContextAsync(async db =>
|
|
{
|
|
var player = await db.Players.FirstAsync();
|
|
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");
|
|
|
|
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 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.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 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.FirstAsync();
|
|
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.FirstAsync();
|
|
player.CurrentPhase = Phase.Results;
|
|
player.VotesFinal = true;
|
|
var state = await db.AppState.FirstAsync();
|
|
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().FirstAsync();
|
|
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.FirstAsync();
|
|
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).FirstAsync();
|
|
var phase = await Endpoints.EndpointHelpers.GetCurrentPhaseAsync(db, playerId);
|
|
|
|
Assert.Equal(Phase.Results, phase);
|
|
}
|
|
}
|
|
|