# Test Suite Plan — Pick'n'Play

Purpose: full coverage of backend + critical UI flows using a mock (in-memory) SQLite DB; validates roles, phases, and data rules described in SPEC.md/API.md.

## Stakeholders & Permissions by Phase

| Role | Suggest phase | Vote phase | Results phase | Anytime |
| --- | --- | --- | --- | --- |
| Unauthenticated visitor | No API access; only static assets | — | — | Health check only |
| Player (non-admin) | Create/see own suggestions (≤5), edit all fields, delete own; can advance to Vote; title locks after leaving phase | View all suggestions, vote 0–10, finalize/unfinalize, use joker once to add a game; cannot go backward | Read leaderboard only when resultsOpen=true; no writes | Login/logout, read /state and /me |
| Admin (isAdmin=true) | Same as player; may edit/delete any suggestion | All player actions; may grant jokers, link/unlink games, delete players | Open/close results; sees leaderboard like player | Toggle results, reset/factory-reset DB, fetch vote status, move self backward |

## Phase/Permission Chart (for tests)
```mermaid
stateDiagram-v2
    [*] --> Suggest
    Suggest --> Vote: player POST /api/me/phase/next
    Vote --> Results: admin opens results OR player next when resultsOpen=true
    Results --> Vote: admin closes results
    Admin: Admin-only actions
    Admin --> Vote: grant joker/link/unlink/delete players/reset/factory-reset
    Admin --> Results: toggle results
```

## Test Strategy (mock DB)
- Use xUnit + WebApplicationFactory; replace DbContext with in-memory SQLite (shared connection) seeded per test.
- Mock IHttpClientFactory with TestHttpMessageHandler to control image URL reachability; avoid real network.
- Helper builders: `TestServerClient` to act as specific user (cookie auth), `SeedData` for players/suggestions/votes, `PhaseHelper` to set phases/resultsOpen directly in DB.
- Run migrations once per test suite against in-memory DB to mirror schema.

## Test Cases (ordered by feature)

### 1) Authentication & Identity
- Register success (player, admin key path) issues cookie, trims fields, stores normalized username, hashes password.
- Register rejects missing/long username, missing password, missing display name, duplicate username, bad admin key, >24 chars username, >16 display name.
- Login success updates LastLoginAt and sets DisplayName if null; rejects wrong password/username; enforces length limits.
- Logout clears cookie.
- EnsurePlayerExistsMiddleware: signed cookie for deleted player returns 401 and clears auth.
- Cookie contains admin claim; non-admin cookie cannot access admin routes (401/403 via filter).

### 2) State & Phase Alignment (/api/state, /api/me)
- /api/state returns player-specific phase, votesFinal, hasJoker, counts; unauthorized returns 401.
- GetPhase auto-upgrades legacy Reveal -> Vote and realigns when resultsOpen toggles (to Results and back to Vote clearing votesFinal).
- /me/phase/next: moves Suggest->Vote, Vote->Results only when resultsOpen true; clears votesFinal; rejects when results locked.
- /me/phase/prev: admin only; moves back one step, clears votesFinal, rejects for player.
- Display name is immutable after registration; attempts to change via /api/me/name return 404.

### 3) Suggestions
- GET /mine returns only caller’s suggestions ordered by CreatedAt.
- POST /: success with valid data; enforces ≤5 per player; trims optional fields; requires display name; rejects bad image URL/ext, unreachable image (mocked), invalid game/youtube URLs, invalid player counts, missing name/too long.
- Joker path: when phase=Vote and HasJoker=true allows creation, consumes joker, resets VotesFinal for all players.
- Phase gating: non-admin cannot create/update/delete outside Suggest (except joker create); admin bypasses phase checks for update/delete.
- PUT /{id}: player can edit own in Suggest; name locked outside Suggest; admin can edit any time; validation mirrors create.
- DELETE /{id}: player deletes own in Suggest; admin any time; also breaks child links and deletes related votes.
- GET /all: accessible from Vote+, orders by CreatedAt, includes link metadata, enforces phase mismatch before Vote.

### 4) Votes
- GET /mine: only in Vote, returns player votes; unauthorized/phase mismatch handled.
- POST /: creates or updates vote; rejects score outside 0–10; rejects when VotesFinal=true; enforces display name requirement and phase gating.
- Linked votes: when suggestions are linked, a single post updates all linked IDs; invalid suggestionId returns 400; linking root detection works for nested links.
- Finalize: POST /finalize toggles VotesFinal flag; allowed only in Vote.

### 5) Results
- GET /api/results: requires auth, resultsOpen=true, phase=Results; returns ordered leaderboard with totals/count/avg, caller’s vote, link metadata, and handles empty vote lists (Average=0).
- Phase mismatch and locked results return 400; unauthorized 401.

### 6) Admin Operations
- POST /admin/results toggles resultsOpen and aligns all player phases (to Results or back to Vote clearing votesFinal); updates UpdatedAt.
- GET /admin/vote-status returns list ordered by display/username with suggestion counts, finalized flag, joker flag; ready/waiting derived correctly.
- POST /admin/joker grants joker only when target in Vote; resets VotesFinal for target.
- DELETE /admin/players/{id}: removes player, cascades suggestions, breaks links to their suggestions, deletes related votes, wrapped in transaction.
- POST /admin/link-suggestions: only in Vote; errors on same ids/already linked/not found; re-parents groups correctly; deletes votes for affected group and unfinalizes affected players.
- POST /admin/unlink-suggestions: only in Vote; clears parents for group, deletes votes in group, unfinalizes affected players; no-op safe when missing.
- POST /admin/reset: wipes suggestions/votes, resets phases to Suggest, clears votesFinal/hasJoker, closes results, updates timestamp.
- POST /admin/factory-reset: wipes all players/suggestions/votes/state; reseeds AppState with defaults; transactional.

### 7) Infrastructure/Helpers
- PasswordHasher: hash+verify roundtrip, rejects empty password, constant-time compare (FixedTimeEquals usage).
- EndpointHelpers.IsValidImageUrl/IsValidHttpUrl: accepts empty, http/https; rejects others/invalid ext.
- IsReachableImageAsync: with mocked Http responses covers head success, get fallback, redirect rejection, size guard, invalid host (private/loopback) detection.
- BuildLinkRoots/LinkedIdsFor/FindRootId: cover disjoint groups, chains, cycles guard (visited set), non-existent ids.
- UpdateIndexMetaBase (Program.cs): rewrites app-base meta when BasePath set; no change when matching/marker missing; safe exceptions swallowed.
- Global exception handler returns 500 with JSON body and logs error.
- /health returns {status:"ok"}.

## Execution Notes
- Use named test data builders for players/suggestions to keep cases small and isolated.
- Reset in-memory DB per test to avoid cross-contamination; assert timestamps using time providers or approximate windows.
- Cover success + failure for every endpoint status path to reach 100% line/branch coverage.
