diff --git a/GameList.Tests/AdminTests.cs b/GameList.Tests/AdminTests.cs new file mode 100644 index 0000000..f4d67a9 --- /dev/null +++ b/GameList.Tests/AdminTests.cs @@ -0,0 +1,159 @@ +using System.Net; +using System.Net.Http.Json; +using System.Text.Json; +using GameList.Domain; +using GameList.Tests.Support; + +namespace GameList.Tests; + +public class AdminTests +{ + [Fact] + public async Task Admin_vote_status_marks_ready_when_all_finalized() + { + using var factory = new TestWebApplicationFactory(); + var admin = factory.CreateClientWithCookies(); + await admin.RegisterAsync("admin", admin: true); + await admin.PostAsJsonAsync("/api/me/phase/next", new { }); // move to Vote + + var p1 = factory.CreateClientWithCookies(); + await p1.RegisterAsync("alice"); + var p2 = factory.CreateClientWithCookies(); + await p2.RegisterAsync("bob"); + await p2.PostAsJsonAsync("/api/me/phase/next", new { }); + + 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() + { + 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() + { + 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(async db => + { + Assert.Single(db.Players); // admin remains + Assert.Empty(db.Suggestions); + Assert.Empty(db.Votes); + }); + } + + [Fact] + public async Task Link_suggestions_errors_on_same_id_and_already_linked() + { + 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.PostAsJsonAsync("/api/me/phase/next", new { }); + + 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() + { + 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.PostAsJsonAsync("/api/me/phase/next", new { }); + 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(async db => + { + Assert.Empty(db.Votes); + Assert.All(db.Suggestions, s => Assert.Null(s.ParentSuggestionId)); + }); + } + + [Fact] + public async Task Reset_and_factory_reset_clear_state() + { + 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(async db => + { + Assert.Empty(db.Suggestions); + Assert.Empty(db.Votes); + Assert.All(db.Players, p => Assert.Equal(Phase.Suggest, p.CurrentPhase)); + }); + + var factoryReset = await admin.PostAsJsonAsync("/api/admin/factory-reset", new { }); + factoryReset.EnsureSuccessStatusCode(); + + await factory.WithDbContextAsync(async db => + { + Assert.Empty(db.Players); + Assert.Single(db.AppState); + }); + } +} diff --git a/GameList.Tests/AuthTests.cs b/GameList.Tests/AuthTests.cs index 5314df7..816e3c2 100644 --- a/GameList.Tests/AuthTests.cs +++ b/GameList.Tests/AuthTests.cs @@ -1,5 +1,4 @@ using System.Net; -using System.Net; using System.Net.Http.Json; using System.Text.Json; using GameList.Tests.Support; @@ -47,4 +46,29 @@ public class AuthTests Assert.Equal(HttpStatusCode.Unauthorized, login.StatusCode); } + + [Fact] + public async Task Register_validates_required_fields() + { + using var factory = new TestWebApplicationFactory(); + var client = factory.CreateClientWithCookies(); + + var missing = await client.PostAsJsonAsync("/api/auth/register", new { Username = "", Password = "", DisplayName = "" }); + Assert.Equal(HttpStatusCode.BadRequest, missing.StatusCode); + + var badKey = await client.PostAsJsonAsync("/api/auth/register", new { Username = "u", Password = "p", DisplayName = "d", AdminKey = "wrong" }); + Assert.Equal(HttpStatusCode.BadRequest, badKey.StatusCode); + } + + [Fact] + public async Task Logout_clears_cookie() + { + using var factory = new TestWebApplicationFactory(); + var client = factory.CreateClientWithCookies(); + await client.RegisterAsync("logoutme"); + + var resp = await client.PostAsync("/api/auth/logout", null); + resp.EnsureSuccessStatusCode(); + Assert.True(resp.Headers.TryGetValues("Set-Cookie", out var cookies) && cookies.Any(c => c.Contains("player"))); + } } diff --git a/GameList.Tests/HelperTests.cs b/GameList.Tests/HelperTests.cs new file mode 100644 index 0000000..16f3740 --- /dev/null +++ b/GameList.Tests/HelperTests.cs @@ -0,0 +1,81 @@ +using System.Net; +using System.Net.Http.Headers; +using System.Reflection; +using GameList.Infrastructure; +using GameList.Endpoints; +using GameList.Tests.Support; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; +using System.Linq; + +namespace GameList.Tests; + +public class HelperTests +{ + [Fact] + public void PasswordHasher_roundtrip_and_empty_guard() + { + var (hash, salt) = PasswordHasher.HashPassword("secret"); + Assert.True(PasswordHasher.Verify("secret", hash, salt)); + Assert.False(PasswordHasher.Verify("other", hash, salt)); + Assert.Throws(() => PasswordHasher.HashPassword("")); + } + + [Fact] + public void UpdateIndexMetaBase_rewrites_content_value() + { + var webRoot = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(webRoot); + var index = Path.Combine(webRoot, "index.html"); + File.WriteAllText(index, ""); + + var env = new FakeEnv { WebRootPath = webRoot }; + var method = typeof(Program).GetMethods(BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public) + .First(m => m.Name.Contains("UpdateIndexMetaBase")); + method.Invoke(null, new object?[] { env, "/pick" }); + + var text = File.ReadAllText(index); + Assert.Contains("content=\"/pick\"", text); + } + + [Fact] + public async Task IsReachableImageAsync_rejects_redirect_and_accepts_image() + { + Assert.True(await EndpointHelpers.IsReachableImageAsync(null, new StubHttpClientFactory(new StubHttpMessageHandler()))); + // Private host should be rejected before network call + Assert.False(await EndpointHelpers.IsReachableImageAsync("http://127.0.0.1/img.png", new StubHttpClientFactory(new StubHttpMessageHandler()))); + } + + [Fact] + public void Link_root_helpers_handle_groups() + { + var roots = EndpointHelpers.BuildLinkRoots(new[] { (1, (int?)null), (2, 1), (3, (int?)null) }); + Assert.Equal(1, roots[1]); + Assert.Equal(1, roots[2]); + Assert.Equal(3, roots[3]); + + var linked = EndpointHelpers.LinkedIdsFor(2, roots); + Assert.Contains(1, linked); + Assert.Contains(2, linked); + } + + [Fact] + public void Url_validation_rules() + { + Assert.True(EndpointHelpers.IsValidImageUrl("https://x.com/img.png")); + Assert.False(EndpointHelpers.IsValidImageUrl("ftp://x/img.png")); + Assert.True(EndpointHelpers.IsValidHttpUrl("http://x")); + Assert.False(EndpointHelpers.IsValidHttpUrl("file://x")); + } + + private class FakeEnv : IWebHostEnvironment + { + public string ApplicationName { get; set; } = ""; + public IFileProvider WebRootFileProvider { get; set; } = null!; + public string WebRootPath { get; set; } = ""; + public string EnvironmentName { get; set; } = ""; + public string ContentRootPath { get; set; } = ""; + public IFileProvider ContentRootFileProvider { get; set; } = null!; + } +} diff --git a/GameList.Tests/MiddlewareTests.cs b/GameList.Tests/MiddlewareTests.cs new file mode 100644 index 0000000..8b69c79 --- /dev/null +++ b/GameList.Tests/MiddlewareTests.cs @@ -0,0 +1,28 @@ +using System.Net; +using GameList.Tests.Support; + +namespace GameList.Tests; + +public class MiddlewareTests +{ + [Fact] + public async Task Deleted_player_cookie_is_signed_out() + { + using var factory = new TestWebApplicationFactory(); + var client = factory.CreateClientWithCookies(); + await client.RegisterAsync("ghost"); + + var playerId = await client.GetProfileIdAsync(); + + await factory.WithDbContextAsync(async db => + { + var player = await db.Players.FindAsync(playerId); + db.Players.Remove(player!); + await db.SaveChangesAsync(); + }); + + var resp = await client.GetAsync("/api/state"); + Assert.Equal(HttpStatusCode.Unauthorized, resp.StatusCode); + Assert.Contains(resp.Headers, h => h.Key.Equals("Set-Cookie", StringComparison.OrdinalIgnoreCase)); + } +} diff --git a/GameList.Tests/ResultsTests.cs b/GameList.Tests/ResultsTests.cs index c18b60e..9b8e262 100644 --- a/GameList.Tests/ResultsTests.cs +++ b/GameList.Tests/ResultsTests.cs @@ -32,4 +32,15 @@ public class ResultsTests Assert.Equal("ResultGame", first.GetProperty("name").GetString()); Assert.Equal(8, (int)first.GetProperty("average").GetDouble()); } + + [Fact] + public async Task Results_locked_returns_error() + { + using var factory = new TestWebApplicationFactory(); + var client = factory.CreateClientWithCookies(); + await client.RegisterAsync("user"); + await client.PostAsJsonAsync("/api/me/phase/next", new { }); + var resp = await client.GetAsync("/api/results"); + Assert.Equal(System.Net.HttpStatusCode.BadRequest, resp.StatusCode); + } } diff --git a/GameList.Tests/StateTests.cs b/GameList.Tests/StateTests.cs index 03efc6d..7afa02b 100644 --- a/GameList.Tests/StateTests.cs +++ b/GameList.Tests/StateTests.cs @@ -1,5 +1,4 @@ using System.Net; -using System.Net; using System.Net.Http.Json; using System.Text.Json; using GameList.Domain; @@ -42,4 +41,65 @@ public class StateTests Assert.Equal(Phase.Results.ToString(), state.GetProperty("currentPhase").GetString()); Assert.True(state.GetProperty("resultsOpen").GetBoolean()); } + + [Fact] + public async Task Name_endpoint_trims_and_rejects_blank() + { + using var factory = new TestWebApplicationFactory(); + var client = factory.CreateClientWithCookies(); + await client.RegisterAsync("nametest"); + + var bad = await client.PostAsJsonAsync("/api/me/name", new { name = " " }); + Assert.Equal(HttpStatusCode.BadRequest, bad.StatusCode); + + var ok = await client.PostAsJsonAsync("/api/me/name", new { name = " Alice " }); + ok.EnsureSuccessStatusCode(); + var me = await client.GetFromJsonAsync("/api/me"); + Assert.Equal("Alice", me.GetProperty("displayName").GetString()); + } + + [Fact] + public async Task Phase_prev_admin_only() + { + 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("/api/me"); + Assert.Equal(Phase.Suggest.ToString(), me.GetProperty("currentPhase").GetString()); + } + + [Fact] + public async Task State_endpoint_requires_auth_and_counts() + { + using var factory = new TestWebApplicationFactory(); + var anon = factory.CreateClient(); + var unauthorized = await anon.GetAsync("/api/state"); + Assert.NotEqual(HttpStatusCode.OK, unauthorized.StatusCode); + + 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 Health_endpoint_ok() + { + using var factory = new TestWebApplicationFactory(); + var resp = await factory.CreateClient().GetFromJsonAsync("/health"); + Assert.Equal("ok", resp.GetProperty("status").GetString()); + } } diff --git a/GameList.Tests/SuggestionTests.cs b/GameList.Tests/SuggestionTests.cs index 1a26a4f..f1b593c 100644 --- a/GameList.Tests/SuggestionTests.cs +++ b/GameList.Tests/SuggestionTests.cs @@ -1,7 +1,9 @@ using System.Net; -using System.Net; using System.Net.Http.Json; +using System.Text.Json; using GameList.Tests.Support; +using GameList.Domain; +using Microsoft.EntityFrameworkCore; namespace GameList.Tests; @@ -45,6 +47,129 @@ public class SuggestionTests Assert.Equal(HttpStatusCode.BadRequest, sixth.StatusCode); } + [Fact] + public async Task Rejects_invalid_image_extension_and_player_counts() + { + using var factory = new TestWebApplicationFactory(); + var client = factory.CreateClientWithCookies(); + await client.RegisterAsync("validate"); + + var badExt = await client.PostAsJsonAsync("/api/suggestions", new + { + Name = "BadImg", + Genre = (string?)null, + Description = (string?)null, + ScreenshotUrl = "http://example.com/file.txt", + YoutubeUrl = (string?)null, + GameUrl = (string?)null, + MinPlayers = (int?)null, + MaxPlayers = (int?)null + }); + Assert.Equal(HttpStatusCode.BadRequest, badExt.StatusCode); + + var badPlayers = await client.PostAsJsonAsync("/api/suggestions", new + { + Name = "BadPlayers", + Genre = (string?)null, + Description = (string?)null, + ScreenshotUrl = (string?)null, + YoutubeUrl = (string?)null, + GameUrl = (string?)null, + MinPlayers = 4, + MaxPlayers = 2 + }); + Assert.Equal(HttpStatusCode.BadRequest, badPlayers.StatusCode); + } + + [Fact] + public async Task Joker_allows_single_extra_suggestion_and_unfinalizes_votes() + { + using var factory = new TestWebApplicationFactory(); + var player = factory.CreateClientWithCookies(); + await player.RegisterAsync("joker"); + + await factory.WithDbContextAsync(async db => + { + var p = await db.Players.FirstAsync(); + p.HasJoker = true; + p.CurrentPhase = Domain.Phase.Vote; + await db.SaveChangesAsync(); + }); + + var suggestion = await player.PostAsJsonAsync("/api/suggestions", new + { + Name = "JokerGame", + Genre = (string?)null, + Description = (string?)null, + ScreenshotUrl = (string?)null, + YoutubeUrl = (string?)null, + GameUrl = (string?)null, + MinPlayers = (int?)null, + MaxPlayers = (int?)null + }); + suggestion.EnsureSuccessStatusCode(); + + await factory.WithDbContextAsync(async db => + { + var p = await db.Players.FirstAsync(); + Assert.False(p.HasJoker); + Assert.False(p.VotesFinal); + }); + } + + [Fact] + public async Task Admin_can_update_during_vote_phase() + { + using var factory = new TestWebApplicationFactory(); + var admin = factory.CreateClientWithCookies(); + await admin.RegisterAsync("admin", admin: true); + + var player = factory.CreateClientWithCookies(); + await player.RegisterAsync("author"); + var id = await player.CreateSuggestionAsync("OldName"); + await player.PostAsJsonAsync("/api/me/phase/next", new { }); + + var update = await admin.PutAsJsonAsync($"/api/suggestions/{id}", new + { + Name = "NewName", + Genre = (string?)null, + Description = (string?)null, + ScreenshotUrl = (string?)null, + YoutubeUrl = (string?)null, + GameUrl = (string?)null, + MinPlayers = (int?)null, + MaxPlayers = (int?)null + }); + update.EnsureSuccessStatusCode(); + } + + [Fact] + public async Task Phase_gate_blocks_player_update_in_vote_phase() + { + using var factory = new TestWebApplicationFactory(); + var player = factory.CreateClientWithCookies(); + await player.RegisterAsync("phase"); + var id = await player.CreateSuggestionAsync("Lock"); + await player.PostAsJsonAsync("/api/me/phase/next", new { }); + + var update = await player.PutAsJsonAsync($"/api/suggestions/{id}", new + { + Name = "Blocked", + Genre = "NewGenre", + Description = (string?)null, + ScreenshotUrl = (string?)null, + YoutubeUrl = (string?)null, + GameUrl = (string?)null, + MinPlayers = (int?)null, + MaxPlayers = (int?)null + }); + + update.EnsureSuccessStatusCode(); + + var loaded = await factory.WithDbContextAsync(async db => await db.Suggestions.FindAsync(id)); + Assert.Equal("Lock", loaded!.Name); // title locked + } + [Fact] public async Task Unreachable_screenshot_url_is_rejected() { @@ -68,4 +193,30 @@ public class SuggestionTests Assert.Equal(HttpStatusCode.BadRequest, response.StatusCode); } + + [Fact] + public async Task Get_all_requires_vote_phase() + { + using var factory = new TestWebApplicationFactory(); + var client = factory.CreateClientWithCookies(); + await client.RegisterAsync("viewer"); + + var resp = await client.GetAsync("/api/suggestions/all"); + Assert.Equal(HttpStatusCode.BadRequest, resp.StatusCode); + } + + [Fact] + public async Task Mine_returns_ordered_list() + { + using var factory = new TestWebApplicationFactory(); + var client = factory.CreateClientWithCookies(); + await client.RegisterAsync("mine"); + + await client.PostAsJsonAsync("/api/suggestions", new { Name = "Second", Genre = (string?)null, Description = (string?)null, ScreenshotUrl = (string?)null, YoutubeUrl = (string?)null, GameUrl = (string?)null, MinPlayers = (int?)null, MaxPlayers = (int?)null }); + await Task.Delay(10); + await client.PostAsJsonAsync("/api/suggestions", new { Name = "Third", Genre = (string?)null, Description = (string?)null, ScreenshotUrl = (string?)null, YoutubeUrl = (string?)null, GameUrl = (string?)null, MinPlayers = (int?)null, MaxPlayers = (int?)null }); + + var mine = await client.GetFromJsonAsync>("/api/suggestions/mine"); + Assert.Equal("Second", mine![0].GetProperty("name").GetString()); + } } diff --git a/GameList.Tests/Support/TestClientExtensions.cs b/GameList.Tests/Support/TestClientExtensions.cs index d4f1532..bfcb3a4 100644 --- a/GameList.Tests/Support/TestClientExtensions.cs +++ b/GameList.Tests/Support/TestClientExtensions.cs @@ -43,4 +43,11 @@ internal static class TestClientExtensions var json = await response.Content.ReadFromJsonAsync(); return json.GetProperty("id").GetInt32(); } + + public static async Task GetProfileIdAsync(this HttpClient client) + { + var me = await client.GetFromJsonAsync("/api/me"); + return Guid.Parse(me.GetProperty("id").GetString()!); + } + } diff --git a/GameList.Tests/Support/TestWebApplicationFactory.cs b/GameList.Tests/Support/TestWebApplicationFactory.cs index 63ea5c6..34869c6 100644 --- a/GameList.Tests/Support/TestWebApplicationFactory.cs +++ b/GameList.Tests/Support/TestWebApplicationFactory.cs @@ -79,6 +79,13 @@ internal class TestWebApplicationFactory : WebApplicationFactory return action(db); } + public Task WithDbContextAsync(Func> action) + { + using var scope = Services.CreateScope(); + var db = scope.ServiceProvider.GetRequiredService(); + return action(db); + } + public HttpClient CreateClientWithCookies() { return CreateClient(new WebApplicationFactoryClientOptions diff --git a/GameList.Tests/VoteTests.cs b/GameList.Tests/VoteTests.cs index 10a7b32..87994ab 100644 --- a/GameList.Tests/VoteTests.cs +++ b/GameList.Tests/VoteTests.cs @@ -1,7 +1,7 @@ using System.Net; -using System.Net; using System.Net.Http.Json; using GameList.Tests.Support; +using Microsoft.EntityFrameworkCore; namespace GameList.Tests; @@ -29,6 +29,62 @@ public class VoteTests Assert.Equal(HttpStatusCode.BadRequest, change.StatusCode); } + [Fact] + public async Task Score_out_of_range_rejected() + { + using var factory = new TestWebApplicationFactory(); + var client = factory.CreateClientWithCookies(); + await client.RegisterAsync("score"); + var id = await client.CreateSuggestionAsync("RangeGame"); + await client.PostAsJsonAsync("/api/me/phase/next", new { }); + + var resp = await client.PostAsJsonAsync("/api/votes", new { SuggestionId = id, Score = 11 }); + Assert.Equal(HttpStatusCode.BadRequest, resp.StatusCode); + } + + [Fact] + public async Task Invalid_suggestion_id_rejected() + { + using var factory = new TestWebApplicationFactory(); + var client = factory.CreateClientWithCookies(); + await client.RegisterAsync("invalid"); + await client.PostAsJsonAsync("/api/me/phase/next", new { }); + + var resp = await client.PostAsJsonAsync("/api/votes", new { SuggestionId = 9999, Score = 5 }); + Assert.Equal(HttpStatusCode.BadRequest, resp.StatusCode); + } + + [Fact] + public async Task Votes_require_display_name() + { + using var factory = new TestWebApplicationFactory(); + var client = factory.CreateClientWithCookies(); + await client.RegisterAsync("anon"); + var id = await client.CreateSuggestionAsync("NeedName"); + + await factory.WithDbContextAsync(async db => + { + var p = await db.Players.FirstAsync(); + p.DisplayName = null; + await db.SaveChangesAsync(); + }); + + await client.PostAsJsonAsync("/api/me/phase/next", new { }); + var resp = await client.PostAsJsonAsync("/api/votes", new { SuggestionId = id, Score = 5 }); + Assert.Equal(HttpStatusCode.BadRequest, resp.StatusCode); + } + + [Fact] + public async Task Finalize_only_in_vote_phase() + { + using var factory = new TestWebApplicationFactory(); + var client = factory.CreateClientWithCookies(); + await client.RegisterAsync("phase"); + + var resp = await client.PostAsJsonAsync("/api/votes/finalize", new { Final = true }); + Assert.Equal(HttpStatusCode.BadRequest, resp.StatusCode); + } + [Fact] public async Task Linked_votes_apply_to_all_linked_suggestions() { diff --git a/Properties/AssemblyInfo.cs b/Properties/AssemblyInfo.cs new file mode 100644 index 0000000..40f2378 --- /dev/null +++ b/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("GameList.Tests")]