Expand test coverage to match specs
This commit is contained in:
@@ -3,6 +3,7 @@ using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using GameList.Domain;
|
||||
using GameList.Tests.Support;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace GameList.Tests;
|
||||
|
||||
@@ -156,4 +157,171 @@ public class AdminTests
|
||||
Assert.Single(db.AppState);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Admin_results_closing_moves_back_to_vote_and_clears_finalize()
|
||||
{
|
||||
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;
|
||||
await db.SaveChangesAsync();
|
||||
});
|
||||
|
||||
var beforeState = await factory.WithDbContextAsync(async db => await db.AppState.AsNoTracking().FirstAsync());
|
||||
await Task.Delay(5);
|
||||
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().FirstAsync();
|
||||
Assert.False(state.ResultsOpen);
|
||||
Assert.True(state.UpdatedAt > beforeState.UpdatedAt);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Vote_status_lists_waiting_players()
|
||||
{
|
||||
using var factory = new TestWebApplicationFactory();
|
||||
var admin = factory.CreateClientWithCookies();
|
||||
await admin.RegisterAsync("admin", admin: true);
|
||||
await admin.PostAsJsonAsync("/api/me/phase/next", new { });
|
||||
|
||||
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.PostAsJsonAsync("/api/me/phase/next", new { });
|
||||
await p1.PostAsJsonAsync("/api/votes", new { SuggestionId = s, Score = 5 });
|
||||
await p1.PostAsJsonAsync("/api/votes/finalize", new { Final = true });
|
||||
|
||||
var status = await admin.GetFromJsonAsync<JsonElement>("/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()
|
||||
{
|
||||
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.PostAsJsonAsync("/api/me/phase/next", new { });
|
||||
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()
|
||||
{
|
||||
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.PostAsJsonAsync("/api/me/phase/next", new { });
|
||||
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 Unlink_not_found_returns_empty_payload()
|
||||
{
|
||||
using var factory = new TestWebApplicationFactory();
|
||||
var admin = factory.CreateClientWithCookies();
|
||||
await admin.RegisterAsync("admin", admin: true);
|
||||
await admin.PostAsJsonAsync("/api/me/phase/next", new { });
|
||||
|
||||
var resp = await admin.PostAsJsonAsync("/api/admin/unlink-suggestions", new { suggestionId = 9999 });
|
||||
resp.EnsureSuccessStatusCode();
|
||||
var json = await resp.Content.ReadFromJsonAsync<JsonElement>();
|
||||
Assert.Equal(0, json.GetProperty("unlinkedSuggestionIds").GetArrayLength());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Reset_clears_flags_and_factory_reset_seeds_defaults()
|
||||
{
|
||||
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().FirstAsync();
|
||||
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().FirstAsync();
|
||||
Assert.False(state.ResultsOpen);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,12 +1,95 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using GameList.Data;
|
||||
using GameList.Infrastructure;
|
||||
using GameList.Tests.Support;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace GameList.Tests;
|
||||
|
||||
public class AuthTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task Register_trims_limits_and_sets_cookie_and_normalized_username()
|
||||
{
|
||||
using var factory = new TestWebApplicationFactory();
|
||||
var client = factory.CreateClientWithCookies();
|
||||
|
||||
var response = await client.PostAsJsonAsync("/api/auth/register", new
|
||||
{
|
||||
Username = " MixedCaseUser ",
|
||||
Password = "Pass123!",
|
||||
DisplayName = " Display Name ",
|
||||
AdminKey = (string?)null
|
||||
});
|
||||
|
||||
response.EnsureSuccessStatusCode();
|
||||
Assert.True(response.Headers.TryGetValues("Set-Cookie", out var cookies) &&
|
||||
cookies.Any(c => c.Contains(PlayerIdentityExtensions.PlayerCookieName)));
|
||||
|
||||
await factory.WithDbContextAsync(async db =>
|
||||
{
|
||||
var player = await db.Players.AsNoTracking().SingleAsync(p => p.Username == "MixedCaseUser");
|
||||
Assert.Equal("mixedcaseuser", player.NormalizedUsername);
|
||||
Assert.True(player.DisplayName!.Length <= 16);
|
||||
Assert.NotEqual(Array.Empty<byte>(), player.PasswordHash);
|
||||
Assert.NotEqual(Array.Empty<byte>(), player.PasswordSalt);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Register_rejects_overlength_username_or_display_name()
|
||||
{
|
||||
using var factory = new TestWebApplicationFactory();
|
||||
var client = factory.CreateClientWithCookies();
|
||||
|
||||
var tooLongUser = new string('u', 25);
|
||||
var userResp = await client.PostAsJsonAsync("/api/auth/register", new
|
||||
{
|
||||
Username = tooLongUser,
|
||||
Password = "Pass123!",
|
||||
DisplayName = "short"
|
||||
});
|
||||
Assert.Equal(HttpStatusCode.BadRequest, userResp.StatusCode);
|
||||
|
||||
var longDisplay = new string('d', 17);
|
||||
var displayResp = await client.PostAsJsonAsync("/api/auth/register", new
|
||||
{
|
||||
Username = "okuser",
|
||||
Password = "Pass123!",
|
||||
DisplayName = longDisplay
|
||||
});
|
||||
|
||||
Assert.Equal(HttpStatusCode.BadRequest, displayResp.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Login_sets_last_login_and_fills_missing_display_name()
|
||||
{
|
||||
using var factory = new TestWebApplicationFactory();
|
||||
var client = factory.CreateClientWithCookies();
|
||||
await client.RegisterAsync("loginfill");
|
||||
|
||||
await factory.WithDbContextAsync(async db =>
|
||||
{
|
||||
var player = await db.Players.FirstAsync();
|
||||
player.DisplayName = null;
|
||||
player.LastLoginAt = DateTimeOffset.UnixEpoch;
|
||||
await db.SaveChangesAsync();
|
||||
});
|
||||
|
||||
var login = await client.LoginAsync("loginfill", "Pass123!");
|
||||
login.EnsureSuccessStatusCode();
|
||||
|
||||
await factory.WithDbContextAsync(async db =>
|
||||
{
|
||||
var player = await db.Players.AsNoTracking().SingleAsync();
|
||||
Assert.NotEqual(DateTimeOffset.UnixEpoch, player.LastLoginAt);
|
||||
Assert.Equal("loginfill", player.DisplayName);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Register_with_admin_key_sets_admin_flag()
|
||||
{
|
||||
@@ -60,6 +143,28 @@ public class AuthTests
|
||||
Assert.Equal(HttpStatusCode.BadRequest, badKey.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Non_admin_cannot_access_admin_routes()
|
||||
{
|
||||
using var factory = new TestWebApplicationFactory();
|
||||
var player = factory.CreateClientWithCookies();
|
||||
await player.RegisterAsync("regular");
|
||||
|
||||
var resp = await player.GetAsync("/api/admin/vote-status");
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, resp.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Admin_can_access_admin_routes()
|
||||
{
|
||||
using var factory = new TestWebApplicationFactory();
|
||||
var admin = factory.CreateClientWithCookies();
|
||||
await admin.RegisterAsync("adminuser", admin: true);
|
||||
|
||||
var resp = await admin.GetAsync("/api/admin/vote-status");
|
||||
resp.EnsureSuccessStatusCode();
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Logout_clears_cookie()
|
||||
{
|
||||
|
||||
@@ -1,13 +1,18 @@
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Reflection;
|
||||
using GameList.Infrastructure;
|
||||
using GameList.Endpoints;
|
||||
using GameList.Tests.Support;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Hosting;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Microsoft.Extensions.FileProviders;
|
||||
using System.Linq;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
using System.Text.Json;
|
||||
using System.Net.Http.Json;
|
||||
|
||||
namespace GameList.Tests;
|
||||
|
||||
@@ -62,6 +67,74 @@ public class HelperTests
|
||||
Assert.False(await EndpointHelpers.IsReachableImageAsync("http://127.0.0.1/img.png", new StubHttpClientFactory(new StubHttpMessageHandler())));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IsReachableImageAsync_handles_head_success_redirect_and_size_guard()
|
||||
{
|
||||
var handler = new StubHttpMessageHandler();
|
||||
handler.SetResponder(req =>
|
||||
{
|
||||
if (req.Method == HttpMethod.Head)
|
||||
{
|
||||
var resp = new HttpResponseMessage(HttpStatusCode.OK);
|
||||
resp.Content = new ByteArrayContent(Array.Empty<byte>());
|
||||
resp.Content.Headers.ContentType = new MediaTypeHeaderValue("image/png");
|
||||
resp.Content.Headers.ContentLength = 100;
|
||||
return resp;
|
||||
}
|
||||
return new HttpResponseMessage(HttpStatusCode.OK)
|
||||
{
|
||||
Content = new ByteArrayContent(Array.Empty<byte>())
|
||||
{
|
||||
Headers =
|
||||
{
|
||||
ContentType = new MediaTypeHeaderValue("image/png"),
|
||||
ContentLength = 100
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
var ok = await EndpointHelpers.IsReachableImageAsync("http://example.com/img.png", new StubHttpClientFactory(handler), handler);
|
||||
Assert.True(ok);
|
||||
|
||||
handler.SetResponder(_ =>
|
||||
{
|
||||
var resp = new HttpResponseMessage(HttpStatusCode.Redirect);
|
||||
resp.Headers.Location = new Uri("http://example.com/other");
|
||||
return resp;
|
||||
});
|
||||
var redirect = await EndpointHelpers.IsReachableImageAsync("http://example.com/img.png", new StubHttpClientFactory(handler), handler);
|
||||
Assert.False(redirect);
|
||||
|
||||
handler.SetResponder(_ =>
|
||||
{
|
||||
var resp = new HttpResponseMessage(HttpStatusCode.OK);
|
||||
resp.Content = new ByteArrayContent(new byte[10]);
|
||||
resp.Content.Headers.ContentType = new MediaTypeHeaderValue("image/png");
|
||||
resp.Content.Headers.ContentLength = 6 * 1024 * 1024; // over 5 MB
|
||||
return resp;
|
||||
});
|
||||
var tooLarge = await EndpointHelpers.IsReachableImageAsync("http://example.com/img.png", new StubHttpClientFactory(handler), handler);
|
||||
Assert.False(tooLarge);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task IsReachableImageAsync_rejects_non_image_content()
|
||||
{
|
||||
var handler = new StubHttpMessageHandler();
|
||||
handler.SetResponder(_ =>
|
||||
{
|
||||
var resp = new HttpResponseMessage(HttpStatusCode.OK);
|
||||
resp.Content = new ByteArrayContent(System.Text.Encoding.UTF8.GetBytes("not image"));
|
||||
resp.Content.Headers.ContentType = new MediaTypeHeaderValue("text/plain");
|
||||
resp.Content.Headers.ContentLength = 9;
|
||||
return resp;
|
||||
});
|
||||
|
||||
var result = await EndpointHelpers.IsReachableImageAsync("http://example.com/img.png", new StubHttpClientFactory(handler), handler);
|
||||
Assert.False(result);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Link_root_helpers_handle_groups()
|
||||
{
|
||||
@@ -98,6 +171,29 @@ public class HelperTests
|
||||
Assert.Equal(3, root); // cycle breaks on revisit
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Global_exception_handler_returns_json_error()
|
||||
{
|
||||
using var factory = new TestWebApplicationFactory().WithWebHostBuilder(builder =>
|
||||
{
|
||||
builder.Configure(app =>
|
||||
{
|
||||
app.UseGlobalExceptionLogging();
|
||||
app.UseRouting();
|
||||
app.UseEndpoints(endpoints =>
|
||||
{
|
||||
endpoints.MapGet("/boom", _ => throw new InvalidOperationException("boom"));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
var client = factory.CreateClient();
|
||||
var resp = await client.GetAsync("/boom");
|
||||
Assert.Equal(HttpStatusCode.InternalServerError, resp.StatusCode);
|
||||
var json = await resp.Content.ReadFromJsonAsync<JsonElement>();
|
||||
Assert.Equal("Unexpected server error", json.GetProperty("error").GetString());
|
||||
}
|
||||
|
||||
private class FakeEnv : IWebHostEnvironment
|
||||
{
|
||||
public string ApplicationName { get; set; } = "";
|
||||
|
||||
@@ -43,4 +43,50 @@ public class ResultsTests
|
||||
var resp = await client.GetAsync("/api/results");
|
||||
Assert.Equal(System.Net.HttpStatusCode.BadRequest, resp.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Results_require_results_phase_and_auth()
|
||||
{
|
||||
using var factory = new TestWebApplicationFactory();
|
||||
var admin = factory.CreateClientWithCookies();
|
||||
await admin.RegisterAsync("admin", admin: true);
|
||||
var player = factory.CreateClientWithCookies();
|
||||
await player.RegisterAsync("player");
|
||||
await admin.PostAsJsonAsync("/api/admin/results", new { resultsOpen = true });
|
||||
|
||||
var anon = factory.CreateClient();
|
||||
var unauthorized = await anon.GetAsync("/api/results");
|
||||
Assert.Equal(System.Net.HttpStatusCode.Unauthorized, unauthorized.StatusCode);
|
||||
|
||||
var ok = await player.GetAsync("/api/results");
|
||||
Assert.Equal(System.Net.HttpStatusCode.OK, ok.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Results_payload_contains_fields_and_ordering()
|
||||
{
|
||||
using var factory = new TestWebApplicationFactory();
|
||||
var admin = factory.CreateClientWithCookies();
|
||||
await admin.RegisterAsync("admin", admin: true);
|
||||
|
||||
var player = factory.CreateClientWithCookies();
|
||||
await player.RegisterAsync("player");
|
||||
var s1 = await player.CreateSuggestionAsync("High");
|
||||
var s2 = await player.CreateSuggestionAsync("NoVotes");
|
||||
|
||||
await player.PostAsJsonAsync("/api/me/phase/next", new { });
|
||||
await player.PostAsJsonAsync("/api/votes", new { SuggestionId = s1, Score = 9 });
|
||||
await player.PostAsJsonAsync("/api/votes/finalize", new { Final = true });
|
||||
|
||||
await admin.PostAsJsonAsync("/api/admin/results", new { resultsOpen = true });
|
||||
await player.PostAsJsonAsync("/api/me/phase/next", new { });
|
||||
|
||||
var results = await player.GetFromJsonAsync<List<JsonElement>>("/api/results");
|
||||
Assert.NotNull(results);
|
||||
Assert.Equal(2, results!.Count);
|
||||
Assert.Equal("High", results[0].GetProperty("name").GetString());
|
||||
Assert.Equal(9, (int)results[0].GetProperty("average").GetDouble());
|
||||
Assert.Equal(1, results[0].GetProperty("count").GetInt32());
|
||||
Assert.Equal(0, results[1].GetProperty("average").GetDouble());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,6 +11,142 @@ namespace GameList.Tests;
|
||||
|
||||
public class StateTests
|
||||
{
|
||||
[Fact]
|
||||
public async Task State_endpoint_returns_expected_payload_for_authenticated_user()
|
||||
{
|
||||
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(Phase.Suggest.ToString(), 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 GetPhase_upgrades_reveal_and_resets_when_results_close()
|
||||
{
|
||||
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 = new byte[] { 1 },
|
||||
PasswordSalt = new byte[] { 1 },
|
||||
DisplayName = "Legacy",
|
||||
CurrentPhase = Phase.Reveal,
|
||||
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 GameList.Endpoints.EndpointHelpers.GetPhase(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 GameList.Endpoints.EndpointHelpers.GetPhase(db, playerId);
|
||||
var player = await db.Players.FindAsync(playerId);
|
||||
Assert.Equal(Phase.Vote, phase);
|
||||
Assert.False(player!.VotesFinal);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Phase_next_advances_and_clears_votesfinal()
|
||||
{
|
||||
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(Phase.Results.ToString(), me.GetProperty("currentPhase").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Phase_prev_moves_back_and_clears_votesfinal()
|
||||
{
|
||||
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(Phase.Suggest.ToString(), me.GetProperty("currentPhase").GetString());
|
||||
Assert.False(me.GetProperty("votesFinal").GetBoolean());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Name_endpoint_rejects_over_16_chars()
|
||||
{
|
||||
using var factory = new TestWebApplicationFactory();
|
||||
var client = factory.CreateClientWithCookies();
|
||||
await client.RegisterAsync("namelimit");
|
||||
|
||||
var resp = await client.PostAsJsonAsync("/api/me/name", new { name = new string('a', 17) });
|
||||
Assert.Equal(HttpStatusCode.BadRequest, resp.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Cannot_advance_to_results_when_locked()
|
||||
{
|
||||
|
||||
@@ -87,12 +87,16 @@ public class SuggestionTests
|
||||
using var factory = new TestWebApplicationFactory();
|
||||
var player = factory.CreateClientWithCookies();
|
||||
await player.RegisterAsync("joker");
|
||||
var other = factory.CreateClientWithCookies();
|
||||
await other.RegisterAsync("other");
|
||||
|
||||
await factory.WithDbContextAsync(async db =>
|
||||
{
|
||||
var p = await db.Players.FirstAsync();
|
||||
p.HasJoker = true;
|
||||
p.CurrentPhase = Domain.Phase.Vote;
|
||||
var o = await db.Players.SingleAsync(x => x.Username == "other");
|
||||
o.VotesFinal = true;
|
||||
await db.SaveChangesAsync();
|
||||
});
|
||||
|
||||
@@ -114,6 +118,8 @@ public class SuggestionTests
|
||||
var p = await db.Players.FirstAsync();
|
||||
Assert.False(p.HasJoker);
|
||||
Assert.False(p.VotesFinal);
|
||||
var o = await db.Players.SingleAsync(x => x.Username == "other");
|
||||
Assert.False(o.VotesFinal);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -219,4 +225,173 @@ public class SuggestionTests
|
||||
var mine = await client.GetFromJsonAsync<List<JsonElement>>("/api/suggestions/mine");
|
||||
Assert.Equal("Second", mine![0].GetProperty("name").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Create_requires_suggest_phase_and_display_name()
|
||||
{
|
||||
using var factory = new TestWebApplicationFactory();
|
||||
var client = factory.CreateClientWithCookies();
|
||||
await client.RegisterAsync("phasegate");
|
||||
|
||||
await factory.WithDbContextAsync(async db =>
|
||||
{
|
||||
var p = await db.Players.FirstAsync();
|
||||
p.CurrentPhase = Domain.Phase.Vote;
|
||||
p.DisplayName = null;
|
||||
await db.SaveChangesAsync();
|
||||
});
|
||||
|
||||
var badPhase = await client.PostAsJsonAsync("/api/suggestions", new { Name = "Nope", Genre = (string?)null, Description = (string?)null, ScreenshotUrl = (string?)null, YoutubeUrl = (string?)null, GameUrl = (string?)null, MinPlayers = (int?)null, MaxPlayers = (int?)null });
|
||||
Assert.Equal(HttpStatusCode.BadRequest, badPhase.StatusCode);
|
||||
|
||||
await factory.WithDbContextAsync(async db =>
|
||||
{
|
||||
var p = await db.Players.FirstAsync();
|
||||
p.CurrentPhase = Domain.Phase.Suggest;
|
||||
await db.SaveChangesAsync();
|
||||
});
|
||||
|
||||
var noDisplay = await client.PostAsJsonAsync("/api/suggestions", new { Name = "NoDisplay", Genre = (string?)null, Description = (string?)null, ScreenshotUrl = (string?)null, YoutubeUrl = (string?)null, GameUrl = (string?)null, MinPlayers = (int?)null, MaxPlayers = (int?)null });
|
||||
Assert.Equal(HttpStatusCode.BadRequest, noDisplay.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Rejects_invalid_urls_name_length_and_player_counts()
|
||||
{
|
||||
using var factory = new TestWebApplicationFactory();
|
||||
var client = factory.CreateClientWithCookies();
|
||||
await client.RegisterAsync("validate2");
|
||||
|
||||
var badGame = await client.PostAsJsonAsync("/api/suggestions", new { Name = "Bad", Genre = (string?)null, Description = (string?)null, ScreenshotUrl = (string?)null, YoutubeUrl = (string?)null, GameUrl = "ftp://bad", MinPlayers = (int?)null, MaxPlayers = (int?)null });
|
||||
Assert.Equal(HttpStatusCode.BadRequest, badGame.StatusCode);
|
||||
|
||||
var badYoutube = await client.PostAsJsonAsync("/api/suggestions", new { Name = "BadYt", Genre = (string?)null, Description = (string?)null, ScreenshotUrl = (string?)null, YoutubeUrl = "file://bad", GameUrl = (string?)null, MinPlayers = (int?)null, MaxPlayers = (int?)null });
|
||||
Assert.Equal(HttpStatusCode.BadRequest, badYoutube.StatusCode);
|
||||
|
||||
var longName = await client.PostAsJsonAsync("/api/suggestions", new { Name = new string('x', 101), Genre = (string?)null, Description = (string?)null, ScreenshotUrl = (string?)null, YoutubeUrl = (string?)null, GameUrl = (string?)null, MinPlayers = (int?)null, MaxPlayers = (int?)null });
|
||||
Assert.Equal(HttpStatusCode.BadRequest, longName.StatusCode);
|
||||
|
||||
var minOnly = await client.PostAsJsonAsync("/api/suggestions", new { Name = "MinOnly", Genre = (string?)null, Description = (string?)null, ScreenshotUrl = (string?)null, YoutubeUrl = (string?)null, GameUrl = (string?)null, MinPlayers = 2, MaxPlayers = (int?)null });
|
||||
Assert.Equal(HttpStatusCode.BadRequest, minOnly.StatusCode);
|
||||
|
||||
var maxTooHigh = await client.PostAsJsonAsync("/api/suggestions", new { Name = "MaxHigh", Genre = (string?)null, Description = (string?)null, ScreenshotUrl = (string?)null, YoutubeUrl = (string?)null, GameUrl = (string?)null, MinPlayers = 2, MaxPlayers = 40 });
|
||||
Assert.Equal(HttpStatusCode.BadRequest, maxTooHigh.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Trims_and_truncates_optional_fields()
|
||||
{
|
||||
using var factory = new TestWebApplicationFactory();
|
||||
var client = factory.CreateClientWithCookies();
|
||||
await client.RegisterAsync("trim");
|
||||
|
||||
var longGenre = new string('g', 60);
|
||||
var longDesc = new string('d', 600);
|
||||
var resp = await client.PostAsJsonAsync("/api/suggestions", new
|
||||
{
|
||||
Name = "Trim",
|
||||
Genre = $" {longGenre} ",
|
||||
Description = $" {longDesc} ",
|
||||
ScreenshotUrl = "http://example.com/img.png",
|
||||
YoutubeUrl = "http://example.com/y",
|
||||
GameUrl = "http://example.com/g",
|
||||
MinPlayers = 1,
|
||||
MaxPlayers = 4
|
||||
});
|
||||
resp.EnsureSuccessStatusCode();
|
||||
|
||||
await factory.WithDbContextAsync(async db =>
|
||||
{
|
||||
var s = await db.Suggestions.AsNoTracking().FirstAsync();
|
||||
Assert.Equal(50, s.Genre!.Length);
|
||||
Assert.Equal(500, s.Description!.Length);
|
||||
Assert.Equal("http://example.com/img.png", s.ScreenshotUrl);
|
||||
});
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Mine_excludes_other_players()
|
||||
{
|
||||
using var factory = new TestWebApplicationFactory();
|
||||
var a = factory.CreateClientWithCookies();
|
||||
await a.RegisterAsync("alice");
|
||||
var b = factory.CreateClientWithCookies();
|
||||
await b.RegisterAsync("bob");
|
||||
|
||||
await a.CreateSuggestionAsync("AliceGame");
|
||||
await b.CreateSuggestionAsync("BobGame");
|
||||
|
||||
var mine = await a.GetFromJsonAsync<List<JsonElement>>("/api/suggestions/mine");
|
||||
Assert.Single(mine!);
|
||||
Assert.Equal("AliceGame", mine[0].GetProperty("name").GetString());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task All_returns_link_metadata_and_ordering()
|
||||
{
|
||||
using var factory = new TestWebApplicationFactory();
|
||||
var client = factory.CreateClientWithCookies();
|
||||
await client.RegisterAsync("owner");
|
||||
|
||||
var id1 = await client.CreateSuggestionAsync("Alpha");
|
||||
await Task.Delay(10);
|
||||
var id2 = await client.CreateSuggestionAsync("Beta");
|
||||
await factory.WithDbContextAsync(async db =>
|
||||
{
|
||||
var beta = await db.Suggestions.FindAsync(id2);
|
||||
beta!.ParentSuggestionId = id1;
|
||||
await db.SaveChangesAsync();
|
||||
});
|
||||
|
||||
await client.PostAsJsonAsync("/api/me/phase/next", new { }); // to Vote
|
||||
|
||||
var all = await client.GetFromJsonAsync<List<JsonElement>>("/api/suggestions/all");
|
||||
Assert.Equal(2, all!.Count);
|
||||
var first = all[0];
|
||||
Assert.Equal("Alpha", first.GetProperty("name").GetString());
|
||||
var second = all[1];
|
||||
var linkedIds = second.GetProperty("linkedIds").EnumerateArray().Select(x => x.GetInt32()).ToList();
|
||||
Assert.Contains(id1, linkedIds);
|
||||
var linkedTitles = second.GetProperty("linkedTitles").EnumerateArray().Select(x => x.GetString()).ToList();
|
||||
Assert.Contains("Alpha", linkedTitles);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Delete_respects_phase_and_clears_links_and_votes()
|
||||
{
|
||||
using var factory = new TestWebApplicationFactory();
|
||||
var owner = factory.CreateClientWithCookies();
|
||||
await owner.RegisterAsync("deleter");
|
||||
var other = factory.CreateClientWithCookies();
|
||||
await other.RegisterAsync("voter");
|
||||
|
||||
var id = await owner.CreateSuggestionAsync("DeleteMe");
|
||||
var child = await owner.CreateSuggestionAsync("Child");
|
||||
await factory.WithDbContextAsync(async db =>
|
||||
{
|
||||
var c = await db.Suggestions.FindAsync(child);
|
||||
c!.ParentSuggestionId = id;
|
||||
await db.SaveChangesAsync();
|
||||
});
|
||||
|
||||
await owner.PostAsJsonAsync("/api/me/phase/next", new { }); // Vote
|
||||
await other.PostAsJsonAsync("/api/me/phase/next", new { });
|
||||
await other.PostAsJsonAsync("/api/votes", new { SuggestionId = id, Score = 5 });
|
||||
|
||||
var blocked = await owner.DeleteAsync($"/api/suggestions/{id}");
|
||||
Assert.Equal(HttpStatusCode.BadRequest, blocked.StatusCode);
|
||||
|
||||
var admin = factory.CreateClientWithCookies();
|
||||
await admin.RegisterAsync("admin", admin: true);
|
||||
var delete = await admin.DeleteAsync($"/api/suggestions/{id}");
|
||||
delete.EnsureSuccessStatusCode();
|
||||
|
||||
await factory.WithDbContextAsync(async db =>
|
||||
{
|
||||
Assert.False(await db.Suggestions.AnyAsync(s => s.Id == id));
|
||||
var childEntity = await db.Suggestions.FindAsync(child);
|
||||
Assert.Null(childEntity!.ParentSuggestionId);
|
||||
Assert.False(db.Votes.Any(v => v.SuggestionId == id));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json;
|
||||
using GameList.Tests.Support;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
@@ -42,6 +43,19 @@ public class VoteTests
|
||||
Assert.Equal(HttpStatusCode.BadRequest, resp.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Negative_score_rejected()
|
||||
{
|
||||
using var factory = new TestWebApplicationFactory();
|
||||
var client = factory.CreateClientWithCookies();
|
||||
await client.RegisterAsync("negative");
|
||||
var id = await client.CreateSuggestionAsync("RangeGame2");
|
||||
await client.PostAsJsonAsync("/api/me/phase/next", new { });
|
||||
|
||||
var resp = await client.PostAsJsonAsync("/api/votes", new { SuggestionId = id, Score = -1 });
|
||||
Assert.Equal(HttpStatusCode.BadRequest, resp.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Invalid_suggestion_id_rejected()
|
||||
{
|
||||
@@ -85,6 +99,25 @@ public class VoteTests
|
||||
Assert.Equal(HttpStatusCode.BadRequest, resp.StatusCode);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Finalize_toggle_allows_unfinalize()
|
||||
{
|
||||
using var factory = new TestWebApplicationFactory();
|
||||
var client = factory.CreateClientWithCookies();
|
||||
await client.RegisterAsync("toggle");
|
||||
var id = await client.CreateSuggestionAsync("Toggle");
|
||||
await client.PostAsJsonAsync("/api/me/phase/next", new { });
|
||||
await client.PostAsJsonAsync("/api/votes", new { SuggestionId = id, Score = 5 });
|
||||
|
||||
var finalize = await client.PostAsJsonAsync("/api/votes/finalize", new { Final = true });
|
||||
finalize.EnsureSuccessStatusCode();
|
||||
var unfinalize = await client.PostAsJsonAsync("/api/votes/finalize", new { Final = false });
|
||||
unfinalize.EnsureSuccessStatusCode();
|
||||
|
||||
var me = await client.GetFromJsonAsync<JsonElement>("/api/me");
|
||||
Assert.False(me.GetProperty("votesFinal").GetBoolean());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Linked_votes_apply_to_all_linked_suggestions()
|
||||
{
|
||||
@@ -114,5 +147,48 @@ public class VoteTests
|
||||
Assert.All(mine, v => Assert.Equal(9, v.Score));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Linked_votes_apply_across_chain()
|
||||
{
|
||||
using var factory = new TestWebApplicationFactory();
|
||||
var admin = factory.CreateClientWithCookies();
|
||||
await admin.RegisterAsync("admin", admin: true);
|
||||
await admin.PostAsJsonAsync("/api/me/phase/next", new { });
|
||||
|
||||
var player = factory.CreateClientWithCookies();
|
||||
await player.RegisterAsync("chain");
|
||||
|
||||
var a = await player.CreateSuggestionAsync("A");
|
||||
var b = await player.CreateSuggestionAsync("B");
|
||||
var c = await player.CreateSuggestionAsync("C");
|
||||
|
||||
await player.PostAsJsonAsync("/api/me/phase/next", new { });
|
||||
|
||||
await admin.PostAsJsonAsync("/api/admin/link-suggestions", new { SourceSuggestionId = a, TargetSuggestionId = b });
|
||||
await admin.PostAsJsonAsync("/api/admin/link-suggestions", new { SourceSuggestionId = b, TargetSuggestionId = c });
|
||||
|
||||
var vote = await player.PostAsJsonAsync("/api/votes", new { SuggestionId = c, Score = 6 });
|
||||
vote.EnsureSuccessStatusCode();
|
||||
|
||||
var mine = await player.GetFromJsonAsync<List<VoteRecord>>("/api/votes/mine");
|
||||
Assert.NotNull(mine);
|
||||
Assert.Equal(3, mine!.Count);
|
||||
Assert.All(mine, v => Assert.Equal(6, v.Score));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Votes_mine_requires_vote_phase_and_auth()
|
||||
{
|
||||
using var factory = new TestWebApplicationFactory();
|
||||
var anon = factory.CreateClient();
|
||||
var unauth = await anon.GetAsync("/api/votes/mine");
|
||||
Assert.Equal(HttpStatusCode.Unauthorized, unauth.StatusCode);
|
||||
|
||||
var client = factory.CreateClientWithCookies();
|
||||
await client.RegisterAsync("phaseguard");
|
||||
var resp = await client.GetAsync("/api/votes/mine");
|
||||
Assert.Equal(HttpStatusCode.BadRequest, resp.StatusCode);
|
||||
}
|
||||
|
||||
private record VoteRecord(int SuggestionId, int Score);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user