Introduce typed API responses and align workflow outputs

This commit is contained in:
2026-02-07 01:19:51 +01:00
parent 35d842d6ee
commit 79dc8f899f
7 changed files with 99 additions and 77 deletions

56
Contracts/Responses.cs Normal file
View File

@@ -0,0 +1,56 @@
using GameList.Domain;
namespace GameList.Contracts;
public record SuggestionCreatedResponse(int Id);
public record SuggestionUpdatedResponse(
int Id,
string Name,
string? Genre,
string? Description,
string? ScreenshotUrl,
string? YoutubeUrl,
string? GameUrl,
int? MinPlayers,
int? MaxPlayers
);
public record VoteUpsertResponse(IReadOnlyList<int> SuggestionIds, int Score);
public record VoteFinalizeResponse(bool VotesFinal);
public record AdminResultsStateResponse(bool ResultsOpen, DateTimeOffset UpdatedAt);
public record AdminGrantJokerResponse(Guid Id, bool HasJoker);
public record AdminDeletePlayerResponse(Guid DeletedPlayerId);
public record AdminLinkSuggestionsResponse(int RootId, IReadOnlyList<int> LinkedSuggestionIds, int UnfinalizedPlayers);
public record AdminUnlinkSuggestionsResponse(IReadOnlyList<int> UnlinkedSuggestionIds, int UnfinalizedPlayers);
public record AdminResetStateResponse(Phase Phase, bool ResultsOpen, DateTimeOffset UpdatedAt);
public record VoteStatusResponse(IReadOnlyList<VoteStatusDto> Voters, bool Ready, IReadOnlyList<string> Waiting);
public record ResultItemDto(
int Id,
string Name,
string? Author,
int? MinPlayers,
int? MaxPlayers,
int Total,
int Count,
double Average,
IReadOnlyList<int> Votes,
int? MyVote,
string? ScreenshotUrl,
string? YoutubeUrl,
string? GameUrl,
string? Description,
string? Genre,
int? ParentSuggestionId,
IReadOnlyList<int> LinkedIds,
IReadOnlyList<string> LinkedTitles
);

View File

@@ -27,11 +27,7 @@ internal sealed class AdminWorkflowService(AppDbContext db)
await db.SaveChangesAsync(); await db.SaveChangesAsync();
await tx.CommitAsync(); await tx.CommitAsync();
var currentState = await db.AppState.AsNoTracking().FirstAsync(); var currentState = await db.AppState.AsNoTracking().FirstAsync();
return Results.Ok(new return Results.Ok(new AdminResultsStateResponse(currentState.ResultsOpen, currentState.UpdatedAt));
{
currentState.ResultsOpen,
currentState.UpdatedAt
});
} }
public async Task<IResult> GetVoteStatusAsync() public async Task<IResult> GetVoteStatusAsync()
@@ -45,12 +41,7 @@ internal sealed class AdminWorkflowService(AppDbContext db)
var waiting = voters.Where(v => !v.Finalized).Select(v => v.Name).ToList(); var waiting = voters.Where(v => !v.Finalized).Select(v => v.Name).ToList();
var ready = waiting.Count == 0; var ready = waiting.Count == 0;
return Results.Ok(new return Results.Ok(new VoteStatusResponse(voters, ready, waiting));
{
voters,
ready,
waiting
});
} }
public async Task<IResult> GrantJokerAsync(GrantJokerRequest request) public async Task<IResult> GrantJokerAsync(GrantJokerRequest request)
@@ -67,11 +58,7 @@ internal sealed class AdminWorkflowService(AppDbContext db)
player.VotesFinal = false; player.VotesFinal = false;
await db.SaveChangesAsync(); await db.SaveChangesAsync();
return Results.Ok(new return Results.Ok(new AdminGrantJokerResponse(player.Id, player.HasJoker));
{
player.Id,
player.HasJoker
});
} }
public async Task<IResult> DeletePlayerAsync(Guid playerId) public async Task<IResult> DeletePlayerAsync(Guid playerId)
@@ -98,7 +85,7 @@ internal sealed class AdminWorkflowService(AppDbContext db)
await db.SaveChangesAsync(); await db.SaveChangesAsync();
await tx.CommitAsync(); await tx.CommitAsync();
return Results.Ok(new { DeletedPlayerId = playerId }); return Results.Ok(new AdminDeletePlayerResponse(playerId));
} }
public async Task<IResult> LinkSuggestionsAsync(Player adminPlayer, LinkSuggestionsRequest request) public async Task<IResult> LinkSuggestionsAsync(Player adminPlayer, LinkSuggestionsRequest request)
@@ -153,12 +140,7 @@ internal sealed class AdminWorkflowService(AppDbContext db)
await tx.CommitAsync(); await tx.CommitAsync();
return Results.Ok(new return Results.Ok(new AdminLinkSuggestionsResponse(targetRoot, affectedIds, await db.Players.CountAsync()));
{
RootId = targetRoot,
LinkedSuggestionIds = affectedIds,
UnfinalizedPlayers = await db.Players.CountAsync()
});
} }
public async Task<IResult> UnlinkSuggestionsAsync(Player adminPlayer, UnlinkSuggestionsRequest request) public async Task<IResult> UnlinkSuggestionsAsync(Player adminPlayer, UnlinkSuggestionsRequest request)
@@ -170,19 +152,11 @@ internal sealed class AdminWorkflowService(AppDbContext db)
var suggestions = await db.Suggestions.ToListAsync(); var suggestions = await db.Suggestions.ToListAsync();
var target = suggestions.FirstOrDefault(s => s.Id == request.SuggestionId); var target = suggestions.FirstOrDefault(s => s.Id == request.SuggestionId);
if (target is null) if (target is null)
return Results.Ok(new return Results.Ok(new AdminUnlinkSuggestionsResponse(Array.Empty<int>(), 0));
{
UnlinkedSuggestionIds = Array.Empty<int>(),
UnfinalizedPlayers = 0
});
var rootIndex = EndpointHelpers.BuildLinkRoots(suggestions.Select(s => (s.Id, s.ParentSuggestionId))); var rootIndex = EndpointHelpers.BuildLinkRoots(suggestions.Select(s => (s.Id, s.ParentSuggestionId)));
if (!rootIndex.TryGetValue(target.Id, out var rootId)) if (!rootIndex.TryGetValue(target.Id, out var rootId))
return Results.Ok(new return Results.Ok(new AdminUnlinkSuggestionsResponse(Array.Empty<int>(), 0));
{
UnlinkedSuggestionIds = Array.Empty<int>(),
UnfinalizedPlayers = 0
});
var groupIds = rootIndex.Where(kv => kv.Value == rootId).Select(kv => kv.Key).ToList(); var groupIds = rootIndex.Where(kv => kv.Value == rootId).Select(kv => kv.Key).ToList();
@@ -201,11 +175,7 @@ internal sealed class AdminWorkflowService(AppDbContext db)
await tx.CommitAsync(); await tx.CommitAsync();
return Results.Ok(new return Results.Ok(new AdminUnlinkSuggestionsResponse(groupIds, await db.Players.CountAsync()));
{
UnlinkedSuggestionIds = groupIds,
UnfinalizedPlayers = await db.Players.CountAsync()
});
} }
public async Task<IResult> ResetAsync() public async Task<IResult> ResetAsync()
@@ -222,12 +192,7 @@ internal sealed class AdminWorkflowService(AppDbContext db)
await db.SaveChangesAsync(); await db.SaveChangesAsync();
await tx.CommitAsync(); await tx.CommitAsync();
return Results.Ok(new return Results.Ok(new AdminResetStateResponse(Phase.Suggest, state.ResultsOpen, state.UpdatedAt));
{
Phase = Phase.Suggest,
state.ResultsOpen,
state.UpdatedAt
});
} }
public async Task<IResult> FactoryResetAsync() public async Task<IResult> FactoryResetAsync()
@@ -245,11 +210,6 @@ internal sealed class AdminWorkflowService(AppDbContext db)
await tx.CommitAsync(); await tx.CommitAsync();
return Results.Ok(new return Results.Ok(new AdminResetStateResponse(Phase.Suggest, fresh.ResultsOpen, fresh.UpdatedAt));
{
Phase = Phase.Suggest,
fresh.ResultsOpen,
fresh.UpdatedAt
});
} }
} }

View File

@@ -1,3 +1,4 @@
using GameList.Contracts;
using GameList.Data; using GameList.Data;
using GameList.Domain; using GameList.Domain;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
@@ -54,8 +55,12 @@ internal sealed class ResultsWorkflowService(AppDbContext db)
.Where(id => id != r.Id) .Where(id => id != r.Id)
.ToList(); .ToList();
return new var linkedTitles = linkedIds
{ .Where(nameLookup.ContainsKey)
.Select(id => nameLookup[id])
.ToList();
return new ResultItemDto(
r.Id, r.Id,
r.Name, r.Name,
r.Author, r.Author,
@@ -72,12 +77,9 @@ internal sealed class ResultsWorkflowService(AppDbContext db)
r.Description, r.Description,
r.Genre, r.Genre,
r.ParentSuggestionId, r.ParentSuggestionId,
LinkedIds = linkedIds, linkedIds,
LinkedTitles = linkedIds linkedTitles
.Where(nameLookup.ContainsKey) );
.Select(id => nameLookup[id])
.ToList()
};
}); });
return Results.Ok(shaped); return Results.Ok(shaped);

View File

@@ -80,7 +80,7 @@ internal sealed class SuggestionWorkflowService(AppDbContext db, IHttpClientFact
await db.SaveChangesAsync(); await db.SaveChangesAsync();
await tx.CommitAsync(); await tx.CommitAsync();
return Results.Created($"/api/suggestions/{suggestion.Id}", new { suggestion.Id }); return Results.Created($"/api/suggestions/{suggestion.Id}", new SuggestionCreatedResponse(suggestion.Id));
} }
public async Task<IResult> DeleteAsync(Player player, bool isAdmin, int suggestionId) public async Task<IResult> DeleteAsync(Player player, bool isAdmin, int suggestionId)
@@ -150,8 +150,7 @@ internal sealed class SuggestionWorkflowService(AppDbContext db, IHttpClientFact
await db.SaveChangesAsync(); await db.SaveChangesAsync();
return Results.Ok(new return Results.Ok(new SuggestionUpdatedResponse(
{
suggestion.Id, suggestion.Id,
suggestion.Name, suggestion.Name,
suggestion.Genre, suggestion.Genre,
@@ -161,7 +160,7 @@ internal sealed class SuggestionWorkflowService(AppDbContext db, IHttpClientFact
suggestion.GameUrl, suggestion.GameUrl,
suggestion.MinPlayers, suggestion.MinPlayers,
suggestion.MaxPlayers suggestion.MaxPlayers
}); ));
} }
public async Task<IResult> GetAllAsync(Player player) public async Task<IResult> GetAllAsync(Player player)

View File

@@ -80,11 +80,7 @@ internal sealed class VoteWorkflowService(AppDbContext db)
} }
await db.SaveChangesAsync(); await db.SaveChangesAsync();
return Results.Ok(new return Results.Ok(new VoteUpsertResponse(linkedIds, request.Score));
{
SuggestionIds = linkedIds,
request.Score
});
} }
public async Task<IResult> SetFinalizeAsync(Player player, VoteFinalizeRequest request) public async Task<IResult> SetFinalizeAsync(Player player, VoteFinalizeRequest request)
@@ -95,6 +91,6 @@ internal sealed class VoteWorkflowService(AppDbContext db)
player.VotesFinal = request.Final; player.VotesFinal = request.Final;
await db.SaveChangesAsync(); await db.SaveChangesAsync();
return Results.Ok(new { player.VotesFinal }); return Results.Ok(new VoteFinalizeResponse(player.VotesFinal));
} }
} }

View File

@@ -10,6 +10,7 @@ Progress update (as of February 7, 2026):
- Completed: build/test guardrails added (`.github/workflows/ci.yml`) and root ownership/setup docs added (`README.md:1`). - Completed: build/test guardrails added (`.github/workflows/ci.yml`) and root ownership/setup docs added (`README.md:1`).
- Completed: backend validators centralized for suggestions and auth (`Endpoints/SuggestionValidator.cs:7`, `Endpoints/AuthValidator.cs:11`). - Completed: backend validators centralized for suggestions and auth (`Endpoints/SuggestionValidator.cs:7`, `Endpoints/AuthValidator.cs:11`).
- Completed: request safety hardened for redirects and forwarded headers (`Program.cs:44`, `Program.cs:108`, `Endpoints/EndpointHelpers.cs:105`, `GameList.Tests/HelperTests.cs:121`, `GameList.Tests/HelperTests.cs:219`). - Completed: request safety hardened for redirects and forwarded headers (`Program.cs:44`, `Program.cs:108`, `Endpoints/EndpointHelpers.cs:105`, `GameList.Tests/HelperTests.cs:121`, `GameList.Tests/HelperTests.cs:219`).
- In progress: API/response contract standardization started with typed response DTOs and client parsing compatibility (`Contracts/Responses.cs:5`, `Contracts/Responses.cs:35`, `wwwroot/js/api.js:25`).
Top 5 maintainability risks (priority order): Top 5 maintainability risks (priority order):
@@ -19,7 +20,7 @@ Top 5 maintainability risks (priority order):
- Impact: hard-to-debug regressions and fragile refactors in UI workflows. - Impact: hard-to-debug regressions and fragile refactors in UI workflows.
2. Rule duplication still present between backend/frontend validations (High) 2. Rule duplication still present between backend/frontend validations (High)
- Suggestion validation is centralized on the backend (`Endpoints/SuggestionWorkflowService.cs:39`, `Endpoints/SuggestionWorkflowService.cs:109`, `Endpoints/SuggestionValidator.cs:7`) but frontend still duplicates parts (`wwwroot/js/ui.js:648`, `wwwroot/js/ui.js:1019`). - Suggestion validation is centralized on the backend (`Endpoints/SuggestionWorkflowService.cs:39`, `Endpoints/SuggestionWorkflowService.cs:115`, `Endpoints/SuggestionValidator.cs:7`) but frontend still duplicates parts (`wwwroot/js/ui.js:648`, `wwwroot/js/ui.js:1019`).
- Auth validation is centralized on the backend (`Endpoints/AuthEndpoints.cs:18`, `Endpoints/AuthEndpoints.cs:65`, `Endpoints/AuthValidator.cs:11`) while frontend length checks remain duplicated (`wwwroot/app.js:92`, `wwwroot/app.js:121`). - Auth validation is centralized on the backend (`Endpoints/AuthEndpoints.cs:18`, `Endpoints/AuthEndpoints.cs:65`, `Endpoints/AuthValidator.cs:11`) while frontend length checks remain duplicated (`wwwroot/app.js:92`, `wwwroot/app.js:121`).
- Impact: inconsistent behavior and repeated fixes across server/client. - Impact: inconsistent behavior and repeated fixes across server/client.
@@ -29,8 +30,8 @@ Top 5 maintainability risks (priority order):
- Hidden module coupling through globals: `wwwroot/js/data.js:131`-`wwwroot/js/data.js:134`, plus `window` callbacks consumed in `wwwroot/js/ui.js:473`, `wwwroot/js/ui.js:696`, `wwwroot/js/ui.js:1009`. - Hidden module coupling through globals: `wwwroot/js/data.js:131`-`wwwroot/js/data.js:134`, plus `window` callbacks consumed in `wwwroot/js/ui.js:473`, `wwwroot/js/ui.js:696`, `wwwroot/js/ui.js:1009`.
- Impact: every UI change risks regressions outside its feature area. - Impact: every UI change risks regressions outside its feature area.
4. Endpoint contract consistency and response shaping are still uneven (High) 4. Endpoint contract consistency and error shaping are still uneven (High)
- Service-layer extraction is now in place for suggestions, votes, admin, and results (`Endpoints/SuggestionWorkflowService.cs:8`, `Endpoints/VoteWorkflowService.cs:8`, `Endpoints/AdminWorkflowService.cs:8`, `Endpoints/ResultsWorkflowService.cs:7`), but response shapes are still mostly anonymous objects and ad-hoc error payloads. - Service-layer extraction is now in place for suggestions, votes, admin, and results (`Endpoints/SuggestionWorkflowService.cs:8`, `Endpoints/VoteWorkflowService.cs:8`, `Endpoints/AdminWorkflowService.cs:8`, `Endpoints/ResultsWorkflowService.cs:8`), and typed response DTO adoption has started (`Contracts/Responses.cs:5`, `Contracts/Responses.cs:35`), but many endpoints still emit ad-hoc error payloads.
- Impact: API evolution and client compatibility changes are still high-friction. - Impact: API evolution and client compatibility changes are still high-friction.
5. Static-analysis and frontend lint guardrails remain incomplete (Medium) 5. Static-analysis and frontend lint guardrails remain incomplete (Medium)
@@ -77,7 +78,7 @@ Worst coupling points:
[P0][Done] Make phase reads side-effect free and move reconciliation to explicit writes [P0][Done] Make phase reads side-effect free and move reconciliation to explicit writes
- Problem: Severity `Critical`, Category `Architecture`. Read endpoints/filters previously relied on mutating phase reads. Impact: unsafe refactors and non-deterministic behavior. - Problem: Severity `Critical`, Category `Architecture`. Read endpoints/filters previously relied on mutating phase reads. Impact: unsafe refactors and non-deterministic behavior.
- Evidence: `Endpoints/EndpointHelpers.cs:37`, `Endpoints/EndpointHelpers.cs:61`, `Endpoints/StateEndpoints.cs:20`, `Infrastructure/PhaseRequirementFilter.cs:17`, `Endpoints/ResultsWorkflowService.cs:9`, `GameList.Tests/StateTests.cs:236`, `GameList.Tests/FiltersTests.cs:55`. - Evidence: `Endpoints/EndpointHelpers.cs:37`, `Endpoints/EndpointHelpers.cs:61`, `Endpoints/StateEndpoints.cs:20`, `Infrastructure/PhaseRequirementFilter.cs:17`, `Endpoints/ResultsWorkflowService.cs:10`, `GameList.Tests/StateTests.cs:236`, `GameList.Tests/FiltersTests.cs:55`.
- Recommendation: Split into `GetCurrentPhaseAsync` (pure read) and explicit `ReconcilePhaseAsync` (write command). Run reconciliation only on intentional transition points (admin toggle, phase change commands, migration job), not on GET paths. - Recommendation: Split into `GetCurrentPhaseAsync` (pure read) and explicit `ReconcilePhaseAsync` (write command). Run reconciliation only on intentional transition points (admin toggle, phase change commands, migration job), not on GET paths.
- Acceptance criteria (testable): GET `/api/state` and GET `/api/me` never call `SaveChangesAsync`; integration tests verify no phase mutations occur during read-only requests; filters perform one phase check path without side effects. - Acceptance criteria (testable): GET `/api/state` and GET `/api/me` never call `SaveChangesAsync`; integration tests verify no phase mutations occur during read-only requests; filters perform one phase check path without side effects.
- Effort / Risk: `M / Med`. - Effort / Risk: `M / Med`.
@@ -101,7 +102,7 @@ Worst coupling points:
[P0][Partial] Centralize validation rules to stop backend/frontend drift [P0][Partial] Centralize validation rules to stop backend/frontend drift
- Problem: Severity `High`, Category `Complexity/Duplication`. Validation rules are duplicated in multiple backend endpoints and frontend forms. Impact: inconsistent behavior and repeated fixes. - Problem: Severity `High`, Category `Complexity/Duplication`. Validation rules are duplicated in multiple backend endpoints and frontend forms. Impact: inconsistent behavior and repeated fixes.
- Evidence: backend centralized in `Endpoints/SuggestionWorkflowService.cs:39`, `Endpoints/SuggestionWorkflowService.cs:109`, `Endpoints/SuggestionValidator.cs:7`, `Endpoints/AuthEndpoints.cs:18`, `Endpoints/AuthEndpoints.cs:65`, `Endpoints/AuthValidator.cs:11`; frontend duplicates remain in `wwwroot/js/ui.js:648`, `wwwroot/js/ui.js:1019`, `wwwroot/app.js:92`. - Evidence: backend centralized in `Endpoints/SuggestionWorkflowService.cs:39`, `Endpoints/SuggestionWorkflowService.cs:115`, `Endpoints/SuggestionValidator.cs:7`, `Endpoints/AuthEndpoints.cs:18`, `Endpoints/AuthEndpoints.cs:65`, `Endpoints/AuthValidator.cs:11`; frontend duplicates remain in `wwwroot/js/ui.js:648`, `wwwroot/js/ui.js:1019`, `wwwroot/app.js:92`.
- Recommendation: Extract backend validators (e.g., `SuggestionValidator`, `AuthValidator`) and reuse in create/update paths; simplify frontend to UX-only prechecks and rely on server responses for source-of-truth. - Recommendation: Extract backend validators (e.g., `SuggestionValidator`, `AuthValidator`) and reuse in create/update paths; simplify frontend to UX-only prechecks and rely on server responses for source-of-truth.
- Acceptance criteria (testable): create/update share one backend validator path; tests cover validator once and both endpoints; frontend no longer re-implements server-only security rules. - Acceptance criteria (testable): create/update share one backend validator path; tests cover validator once and both endpoints; frontend no longer re-implements server-only security rules.
- Effort / Risk: `M / Med`. - Effort / Risk: `M / Med`.
@@ -117,7 +118,7 @@ Worst coupling points:
[P1][Done] Extract service-layer workflows from endpoint lambdas [P1][Done] Extract service-layer workflows from endpoint lambdas
- Problem: Severity `High`, Category `Architecture`. Endpoint files contain business orchestration, persistence, and policy logic inline; large lambdas are hard to reason about and reuse. - Problem: Severity `High`, Category `Architecture`. Endpoint files contain business orchestration, persistence, and policy logic inline; large lambdas are hard to reason about and reuse.
- Evidence: extraction completed for suggestions, votes, admin, and results (`Endpoints/SuggestionWorkflowService.cs:8`, `Endpoints/VoteWorkflowService.cs:8`, `Endpoints/AdminWorkflowService.cs:8`, `Endpoints/ResultsWorkflowService.cs:7`, `Program.cs:37`, `Program.cs:38`, `Program.cs:39`, `Program.cs:40`). - Evidence: extraction completed for suggestions, votes, admin, and results (`Endpoints/SuggestionWorkflowService.cs:8`, `Endpoints/VoteWorkflowService.cs:8`, `Endpoints/AdminWorkflowService.cs:8`, `Endpoints/ResultsWorkflowService.cs:8`, `Program.cs:37`, `Program.cs:38`, `Program.cs:39`, `Program.cs:40`).
- Recommendation: Introduce focused application services (`SuggestionService`, `VoteService`, `AdminWorkflowService`) and keep endpoints as transport adapters. - Recommendation: Introduce focused application services (`SuggestionService`, `VoteService`, `AdminWorkflowService`) and keep endpoints as transport adapters.
- Acceptance criteria (testable): endpoint handlers reduced to routing + DTO mapping + service calls; domain rule tests target service methods directly; endpoint tests remain green. - Acceptance criteria (testable): endpoint handlers reduced to routing + DTO mapping + service calls; domain rule tests target service methods directly; endpoint tests remain green.
- Effort / Risk: `L / Med`. - Effort / Risk: `L / Med`.
@@ -149,7 +150,7 @@ Worst coupling points:
[P1] Make write workflows transaction-consistent and explicit [P1] Make write workflows transaction-consistent and explicit
- Problem: Severity `Medium`, Category `Correctness/Architecture`. Several multi-step state changes rely on multiple DB commands without explicit transaction grouping. - Problem: Severity `Medium`, Category `Correctness/Architecture`. Several multi-step state changes rely on multiple DB commands without explicit transaction grouping.
- Evidence: `Endpoints/SuggestionWorkflowService.cs:71`, `Endpoints/SuggestionWorkflowService.cs:75`, `Endpoints/AdminWorkflowService.cs:74`, `Endpoints/AdminWorkflowService.cs:208`, `Endpoints/AdminWorkflowService.cs:227`. - Evidence: `Endpoints/SuggestionWorkflowService.cs:72`, `Endpoints/SuggestionWorkflowService.cs:77`, `Endpoints/AdminWorkflowService.cs:17`, `Endpoints/AdminWorkflowService.cs:181`, `Endpoints/AdminWorkflowService.cs:198`.
- Recommendation: Wrap multi-entity updates in explicit transactions where consistency matters, or refactor into idempotent command handlers with compensating behavior. - Recommendation: Wrap multi-entity updates in explicit transactions where consistency matters, or refactor into idempotent command handlers with compensating behavior.
- Acceptance criteria (testable): fault-injection tests prove no partial state after exceptions; transaction boundaries documented per workflow. - Acceptance criteria (testable): fault-injection tests prove no partial state after exceptions; transaction boundaries documented per workflow.
- Effort / Risk: `M / Med`. - Effort / Risk: `M / Med`.
@@ -157,12 +158,20 @@ Worst coupling points:
[P1] Strengthen test quality for flaky/time-sensitive cases and security edges [P1] Strengthen test quality for flaky/time-sensitive cases and security edges
- Problem: Severity `Medium`, Category `Testing`. Some tests depend on sleeps and do not cover realistic redirect behavior or overlapping refresh flows. - Problem: Severity `Medium`, Category `Testing`. Some tests depend on sleeps and do not cover realistic redirect behavior or overlapping refresh flows.
- Evidence: `GameList.Tests/SuggestionTests.cs:379`, `GameList.Tests/SuggestionTests.cs:575`, `GameList.Tests/HelperTests.cs:121`. - Evidence: redirect and forwarded-header cases are covered (`GameList.Tests/HelperTests.cs:121`, `GameList.Tests/HelperTests.cs:219`), and delay-based ordering checks were removed from `GameList.Tests/SuggestionTests.cs` and `GameList.Tests/AdminTests.cs`.
- Recommendation: replace `Task.Delay` ordering checks with deterministic seeded timestamps where feasible; add explicit redirect-follow tests and concurrency-path tests. - Recommendation: replace `Task.Delay` ordering checks with deterministic seeded timestamps where feasible; add explicit redirect-follow tests and concurrency-path tests.
- Acceptance criteria (testable): no timing sleeps in endpoint tests for ordering; new tests cover redirect-chain rejection and race-sensitive refresh logic. - Acceptance criteria (testable): no timing sleeps in endpoint tests for ordering; new tests cover redirect-chain rejection and race-sensitive refresh logic.
- Effort / Risk: `M / Low`. - Effort / Risk: `M / Low`.
- Dependencies (if any): P0 redirect-hardening task. - Dependencies (if any): P0 redirect-hardening task.
[P1][In Progress] Standardize API response contracts and error envelopes
- Problem: Severity `Medium`, Category `API/Contracts`. Success payloads are being typed, but error payloads are still inconsistent (`{ error }` objects, plain status codes, and mixed shapes).
- Evidence: typed response DTOs added in `Contracts/Responses.cs:5` and used in service workflows (`Endpoints/SuggestionWorkflowService.cs:83`, `Endpoints/VoteWorkflowService.cs:83`, `Endpoints/AdminWorkflowService.cs:44`, `Endpoints/ResultsWorkflowService.cs:63`); frontend parser now supports `error`, `detail`, and `title` in `wwwroot/js/api.js:25`.
- Recommendation: migrate endpoint error responses to `ProblemDetails` consistently while keeping backward-compatible `error` fields during transition; continue replacing anonymous success payloads with DTOs.
- Acceptance criteria (testable): all API endpoints return typed success DTOs; all non-2xx responses include `ProblemDetails` fields and remain consumable by current frontend.
- Effort / Risk: `M / Med`.
- Dependencies (if any): none.
[P2] Externalize i18n/FAQ content from executable JS modules [P2] Externalize i18n/FAQ content from executable JS modules
- Problem: Severity `Low`, Category `Complexity/Documentation`. Translation and FAQ payloads are embedded in code, making review and localization hard. - Problem: Severity `Low`, Category `Complexity/Documentation`. Translation and FAQ payloads are embedded in code, making review and localization hard.
- Evidence: `wwwroot/js/i18n.js:1`-`wwwroot/js/i18n.js:799`. - Evidence: `wwwroot/js/i18n.js:1`-`wwwroot/js/i18n.js:799`.

View File

@@ -22,7 +22,7 @@ async function request(path, { method = "GET", body } = {}) {
let msg = `${res.status}`; let msg = `${res.status}`;
try { try {
const data = await res.json(); const data = await res.json();
msg = data.error || JSON.stringify(data); msg = data.error || data.detail || data.title || JSON.stringify(data);
} catch { /* ignore */ } } catch { /* ignore */ }
const err = new Error(msg); const err = new Error(msg);
err.status = res.status; err.status = res.status;