Add comprehensive backend test suite and helper access
This commit is contained in:
159
GameList.Tests/AdminTests.cs
Normal file
159
GameList.Tests/AdminTests.cs
Normal file
@@ -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<JsonElement>("/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);
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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")));
|
||||
}
|
||||
}
|
||||
|
||||
81
GameList.Tests/HelperTests.cs
Normal file
81
GameList.Tests/HelperTests.cs
Normal file
@@ -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<ArgumentException>(() => 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, "<meta name=\"app-base\" content=\"\">");
|
||||
|
||||
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!;
|
||||
}
|
||||
}
|
||||
28
GameList.Tests/MiddlewareTests.cs
Normal file
28
GameList.Tests/MiddlewareTests.cs
Normal file
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<JsonElement>("/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<JsonElement>("/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<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 Health_endpoint_ok()
|
||||
{
|
||||
using var factory = new TestWebApplicationFactory();
|
||||
var resp = await factory.CreateClient().GetFromJsonAsync<JsonElement>("/health");
|
||||
Assert.Equal("ok", resp.GetProperty("status").GetString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<List<JsonElement>>("/api/suggestions/mine");
|
||||
Assert.Equal("Second", mine![0].GetProperty("name").GetString());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,4 +43,11 @@ internal static class TestClientExtensions
|
||||
var json = await response.Content.ReadFromJsonAsync<JsonElement>();
|
||||
return json.GetProperty("id").GetInt32();
|
||||
}
|
||||
|
||||
public static async Task<Guid> GetProfileIdAsync(this HttpClient client)
|
||||
{
|
||||
var me = await client.GetFromJsonAsync<JsonElement>("/api/me");
|
||||
return Guid.Parse(me.GetProperty("id").GetString()!);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -79,6 +79,13 @@ internal class TestWebApplicationFactory : WebApplicationFactory<Program>
|
||||
return action(db);
|
||||
}
|
||||
|
||||
public Task<T> WithDbContextAsync<T>(Func<AppDbContext, Task<T>> action)
|
||||
{
|
||||
using var scope = Services.CreateScope();
|
||||
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
|
||||
return action(db);
|
||||
}
|
||||
|
||||
public HttpClient CreateClientWithCookies()
|
||||
{
|
||||
return CreateClient(new WebApplicationFactoryClientOptions
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
|
||||
3
Properties/AssemblyInfo.cs
Normal file
3
Properties/AssemblyInfo.cs
Normal file
@@ -0,0 +1,3 @@
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("GameList.Tests")]
|
||||
Reference in New Issue
Block a user