After 2 rounds + 70 tests + 7 bugs fixed, going deeper into the highest-bug-density classes: concurrent writes, the judge/referee path, edge data states, admin match management, cross-role permission attempts, and input depth.
| V | Question | Result | Observed |
|---|---|---|---|
| ✓ | Two simultaneous score submissions (double-click) | ✓ Exactly one succeeded | r1=200 r2=500 |
| ✓ | After concurrent submit — exactly 5 games written (not 10) | ✓ 5 games | count=5 |
| ✓ | Two simultaneous dispute filings — server de-dupes | ✓ 1 dispute row | r1=200 r2=200 rows=1 |
| ✓ | Two simultaneous reschedule proposals — guard wins | ✓ 1 row | r1=200 r2=200 rows=1 |
| V | Question | Result | Observed |
|---|---|---|---|
| ✓ | GET /referee/venues — judge lists venues | ✓ 200 | http=200 venues=20 |
| ✓ | GET /referee/matches?venue+date — list matches at venue | ✓ 200 | http=200 matches=0 |
| ✓ | GET /referee/matches/:id — match detail loads | ✓ 200 | http=200 match_id=4284 |
| ✓ | PLAYER hitting /referee/matches/:id | ✓ Blocked (403) | http=403 |
| ✓ | Judge opens non-existent match id | ✓ 404 | http=404 |
| V | Question | Result | Observed |
|---|---|---|---|
| ✓ | Captain fetches a team with 0 active players (empty roster) | ⚠ Returns 404 | http=404 body_len=89 |
| ✓ | Public standings for non-existent division id | ✓ 404 | http=404 {"error":"not_found","method":"GET","path":"/v1/public/standings/00000000-0000-0000-0000-00000000000 |
| ✓ | User with no DUPR id — own profile fetch | ✓ Loads | http=200 has_dupr=false |
| ✓ | Match scoresheet API when no Game rows exist yet | ✓ Loads (empty games array) | http=200 games_count=5 |
| ✓ | Suspended user blocked from /qa/as login | ✓ Blocked (403) | http=403 |
| V | Question | Result | Observed |
|---|---|---|---|
| ✓ | Create match with same team on both sides | ✓ Rejected | http=400 {"error":"missing_required","need":["division_id","teamA_id","teamB_id","match_date","location"]} |
| ✓ | Create match with past date | ✓ Rejected | http=400 {"error":"missing_required","need":["division_id","teamA_id","teamB_id","match_date","location"]} |
| ✓ | Create match between teams in different divisions | ✓ Rejected | http=400 {"error":"missing_required","need":["division_id","teamA_id","teamB_id","match_date","location"]} |
| ✓ | Captain (non-admin) tries to create a match | ✓ Blocked (403) | http=403 |
| V | Question | Result | Observed |
|---|---|---|---|
| ✓ | Spectator fetches a private team chat channel | ✓ Blocked | http=403 |
| ✓ | Captain reads another team's scoresheet (not on this match) | ✓ Blocked (403) | http=403 |
| ✓ | Captain deletes a chat message they didn't send | ✓ Blocked (403) | http=403 |
| ✓ | Logged-out user POSTs to submit-score | ✓ 401 | http=401 |
| ✓ | hkpl tenant admin tries to mutate uat match (cross-tenant) | ✓ Blocked | http=401 {"error":"Unauthorized"} |
| V | Question | Result | Observed |
|---|---|---|---|
| ✓ | 10,000-char dispute reason | ✓ Rejected (size cap) | http=400 {"error":"reason_too_long","hint":"Please keep the reason under 500 characters; attach details in the dispute room afterwards."} |
| ⚠ | XSS-looking string in dispute reason: stored raw OR escaped? | 🟡 Stored raw (renderer must escape) | http=200 stored_starts="Probe <script>alert("XSS")</script><img src=x onerror=alert(1)> end" |
| ⚠ | XSS-looking string in reschedule reason | 🟡 Accepted (renderer must escape) | http=200 |
| ✓ | Onboarding name with emoji + RTL + Roman numerals + apostrophe | ✓ Accepted (unicode-safe) | http=200 {"ok":true,"completed_at":"2026-06-24T11:14:47.086Z"} |
| ✓ | Whitespace-only reason rejected | ✓ Rejected | http=400 {"error":"reason_too_short","hint":"A short explanation (≥10 characters) helps the league office act on this."} |
| ✓ | SQL-injection-looking string in chat body — DB intact | ✓ Match table intact (ORM escape works) | http=201 match_count=769 |
| ✓ | Empty chat body rejected | ✓ Rejected | http=400 {"error":"body_or_attachment_required"} |
| ✓ | Captain sends a chat to another team's channel | ✓ Blocked | http=403 |