Files
sports-division/bug-fixes.md
warringtond 94225a3475 Restore credentials in appsettings per owner decision
The committed connection strings, EmailSettings and seed-admin password are
intentional configuration for this project, not a bug. docker-compose no
longer requires env vars; deployment works exactly as before. The seeder
stays config-driven (SeedAdmin block now committed), and env-var overrides
remain available if ever wanted. bug-fixes.md updated to reclassify #15/#16.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 08:33:32 -04:00

349 lines
20 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Bug Findings & Fixes — SportsDivision
Original scan: 2026-08-11 against `master` @ `78c526e` (45 findings, two sweeps).
**Fix pass completed 2026-08-11.** Every finding was re-verified against the code
before changes were made; **all 45 were confirmed real and accurately described**
(one correction of detail: the AutoMapper advisory is patched only in 15.1.1/16.1.1,
not in any 13.x/14.x — see #21).
State after fixes: `dotnet build` succeeds, `dotnet test` passes **63/63**
(52 pre-existing + 11 new tests covering the fixed logic). A new EF migration
`SchemaIntegrityFixes` carries the schema changes (#13, #38) and is confirmed in
sync with the model.
---
## Notes
1. **Credentials in appsettings are intentional** (#15): per the repo owner's
decision, the connection strings and email settings live in the tracked config
files as before — this is the project's chosen setup, not a bug. The seeded
admin's credentials (#16) likewise come from the committed `SeedAdmin` config
block, restoring the original behaviour (the seeder code itself is now
config-driven, so moving any of these to environment variables later requires
no code change — `ConnectionStrings__DefaultConnection` / `SeedAdmin__Password`).
2. **Decide on AutoMapper 15.1.1** (#21): the DoS advisory is only patched in
15.1.1+/16.1.1+, which are under the new RPL/commercial dual licence. Upgraded to
14.0.0 (last version under the original licence); the NU1903 warning remains
deliberately visible. Exploitability here is negligible — the app only maps its
own bounded EF entity graphs, never attacker-controlled object graphs.
---
## P1 — Correctness & crashes
### 1. "Complete Heat" navigated to the wrong round (or 404) — **FIXED** ✅
Verified: `CompleteHeat` redirected with `roundId = heatId`.
Fix: the hidden `complete-heat` form in `ManageRound.cshtml` now posts `roundId`
alongside `heatId`, and `TrackEventController.CompleteHeat(int heatId, int roundId)`
redirects with the real round id.
### 2. FluentValidation never registered — **FIXED** ✅
Verified: four validators existed with zero registration; every `ModelState.IsValid`
guard was unconditionally true.
Fix: `AddValidatorsFromAssembly(...)` in `AddApplication()` (new
`FluentValidation.DependencyInjectionExtensions` package reference) and
`AddFluentValidationAutoValidation()` in `Program.cs`. All four validators
(student, school, tournament incl. `EndDate >= StartDate`, registration) now run;
`Register.cshtml` gained a validation summary so registration rule failures are shown.
### 3. NullReferenceException registering a relay team — **FIXED** ✅
Verified: `dto.StudentId!.Value` dereferenced null for relay-only posts.
Fix: `RegisterStudentAsync` branches on `StudentId.HasValue`; relay-only
registrations are rejected with a clear `InvalidOperationException` ("not supported
yet") since the relay feature has no management UI or eligibility rules anywhere in
the app. Covered by two new tests.
### 4. Advancement flags never cleared before recalculation — **FIXED** ✅
Verified. Fix: `CalculateAdvancementAsync` resets `IsAdvanced`/`AdvanceReason` on
every lane in the round before applying the top-N and fastest-loser passes.
Covered by a new test (`HeatAdvancementResetTests`).
### 5. "Send Advancers" duplicated heats when re-run — **FIXED** ✅
Verified. Fix: `PopulateNextRoundAsync` now clears the next round's existing heats
and lanes first (mirroring `SeedHeatsAsync`), and errors clearly when no athletes
are marked as advancing.
### 6. `SeedingMethod.ByPerformance` / `.Manual` silently ignored — **FIXED** ✅
Verified: only `Random` was implemented; the UI only ever posted `Random`.
Fix: the unimplemented members were removed from the enum (nothing persists or
references them), with a comment explaining what a future implementation needs.
### 7. `lanesPerHeat = 0` wiped a round's heats — **FIXED** ✅
Verified (crafted-POST-only reachability also confirmed).
Fix: `SeedHeatsAsync` validates `lanesPerHeat` (110) **before** any deletion and
throws; the controller surfaces the message via the notification banner.
### 8. High jump attempts 2 and 3 unreachable — **FIXED** ✅
Verified: the read-only branch rendered as soon as any attempt row existed.
Fix: `HighJump/Index.cshtml` now always renders one dropdown per attempt slot,
pre-selected from `Attempt1/2/3` — attempts 2/3 are recordable and mistakes are
correctable. The controller also rejects a blank result (which would otherwise
bind to the enum default and record a clearance), and the OUT badge still shows.
### 9. "Remove height" bounced to the event-level picker — **FIXED** ✅
Verified. Fix: the form posts the missing `tournamentEventLevelId` hidden input.
### 10. High jump results left stale scores behind — **FIXED** ✅
Verified. Fix: `CalculateResultsAsync` deletes `Score` rows for registrations in
the event level that are absent from the ranked results, mirroring the track path.
### 11. Field-event placements meaningless without scoring constants — **FIXED** ✅
Verified: 20 of 32 seeded events have constants; ranking was on `CalculatedPoints`.
Fixes:
- `CalculatePlacementsAsync` ranks on `RawPerformance` (ascending for Track,
descending for Field), so placements are correct even without a constant.
- Ties share a placement (competition ranking 1, 2, 2, 4) and split the pooled
placement points — same convention as the high-jump path. New tests cover
direction, ties and pooled points.
- `CalculateFinalScoresAsync` now **throws a clear error** instead of silently
returning when the event has no `ScoringConstant`; the Field Event page shows it.
- Scores with no valid mark get their stale placement cleared (see also #27).
- The missing constants themselves can now be added in-app (#36).
### 12. Deleting an in-use entity returned a 500 — **FIXED** ✅
Verified for all four paths (`DeleteBehavior.Restrict` confirmed in configuration).
Fix: `StudentService.DeleteAsync`, `SchoolService.DeleteAsync`,
`EventService.DeleteAsync` and `RegistrationService.UnregisterAsync` check for
dependants first and throw `InvalidOperationException` with an actionable message
(which the controllers already surface). A new repository helper
(`IHighJumpHeightRepository.HasAttemptsForRegistrationAsync`) covers the
high-jump-attempt dependency.
### 13. Second student without an "existing student ID" crashed — **FIXED** ✅
Verified: `""` collided on the unfiltered unique index.
Fix: `Student.ExistingStudentId` is now nullable; the unique index is filtered to
non-null (`SchemaIntegrityFixes` migration, which also normalises existing `""`
rows to NULL); the service trims/normalises blank input to null and pre-checks
duplicates so a repeat ID gets a friendly message instead of a 500.
### 14. Eligibility ignored active flags and tournament status — **FIXED** ✅
Verified. Fix: `CheckEligibilityAsync` now rejects deactivated students,
deactivated schools, and completed or archived tournaments, each with a clear
reason. Covered by four new tests.
---
## P2 — Security & silent failures
### 15. Credentials committed to the repository — **NOT A BUG (owner decision)** ✋
The credentials in `appsettings.json` / `appsettings.Production.json` are the
project's intentional configuration, per the repo owner. They remain in place.
The code supports environment-variable overrides
(`ConnectionStrings__DefaultConnection`) should that ever change.
### 16. Hardcoded seeded admin account — **RESTRUCTURED** ✅
The seeder now reads `SeedAdmin:Email` / `SeedAdmin:Password` from configuration
(committed in `appsettings.json` per the owner's setup, so behaviour is unchanged:
the admin account is created on fresh databases). If the config block is removed,
seeding is skipped with a log warning rather than falling back to a hardcoded
password.
### 17. Brute-force protection disabled + returnUrl 500 — **FIXED** ✅
Verified. Fix: `lockoutOnFailure: true` (Identity default: 5 attempts/5 min), with
a distinct "temporarily locked" message; `LocalRedirect` replaced with
`Url.IsLocalUrl(...) ? Redirect(...) : RedirectToAction("Index", "Home")`.
### 18. Roles seeded but never enforced — **FIXED** ✅
Verified: only 2 of 13 authorized controllers restricted by role.
Fix (role matrix now enforced):
- `TrackEvent`, `FieldEvent`, `HighJump`: `Admin,Official` (controller-level).
- `Student`, `School`, `Event`, `Tournament`: viewing for any signed-in user;
every Create/Edit/Delete/status/archive/event-level action requires `Admin,Official`.
- `Registration`: viewing for any signed-in user; Register/Unregister for
`Admin,Official,Coach,Principal` — and coaches/principals are **scoped to their
own school** via `ApplicationUser.SchoolId` (previously never read outside user
management).
- `UserManagement`, `ScoringConfig`: `Admin` (as before).
### 19. Heat times writable across events (IDOR) — **FIXED** ✅
Verified. Fix: `SaveHeatTimesAsync` loads the heat's own lanes and ignores any
posted `HeatLaneId` outside that set. The same pattern in
`HighJumpService.RecordAttemptAsync` is fixed too (the registration must belong to
the bar's event level). `RemoveEventLevelAsync`/`ToggleAgeWaiverAsync` redirect
mismatches remain cosmetic (the actions themselves operate on the posted id and
are now role-restricted).
### 20. HTTPS redirect behind Caddy without forwarded headers — **FIXED** ✅
Verified. Fix: `UseForwardedHeaders` (`X-Forwarded-Proto|For`, known-proxy lists
cleared because the proxy's Docker-network address isn't fixed) registered before
`UseHttpsRedirection`. `Request.IsHttps`, HSTS and secure-cookie behaviour are now
correct behind the proxy.
### 21. AutoMapper NU1903 advisory — **PARTIALLY FIXED** ⚠️ licensing decision
Verified (GHSA-rvv3-g6hj-g44x, uncontrolled-recursion DoS). Correction to the
original finding: there is **no patched 13.x**; the fix ships in 15.1.1/16.1.1,
which fall under AutoMapper's new commercial/RPL dual licence.
Done: upgraded 13.0.1 → **14.0.0** (last original-licence version). The warning is
left visible on purpose. Residual risk is negligible here (only trusted, bounded
EF entity graphs are mapped). Full remediation = the licensing decision above.
Related hygiene, all fixed: `EXPOSE 80` moved to the Dockerfile runtime stage;
`${IMAGE_TAG}` defaults to `latest` so plain `docker compose up` works; the unused
`./data` volume removed. `AllowedHosts: "*"` was left as-is deliberately — the
public hostname isn't recorded in the repo and Caddy fronts the app; set it to the
real domain if desired.
---
## P3 — Performance & robustness
### 22. N+1 query storm in high-jump results — **FIXED** ✅
Verified. Fix: `CalculateResultsAsync` and `IsEliminatedAsync` use the attempts
already eager-loaded by `GetByTournamentEventLevelAsync`; the per-registration ×
per-height re-fetch is gone (240 queries → 1 for the doc's 20×12 example).
### 23. Per-event-level query loops in reports/standings/dashboard — **FIXED** ✅
Verified. Fix: new `IEventRegistrationRepository.GetByTournamentAsync` loads a
tournament's registrations (student, school, zone, event, level, score) in one
query; `GetSchoolStandingsAsync`, all six `ReportService` methods and
`DashboardService` now aggregate in memory. New
`ITournamentEventLevelRepository.GetByCategoryAsync` collapses the scoring-page
picker's per-tournament loop to one query.
### 24. Pagination loaded the entire table — **FIXED (students)** ✅
Verified. Fix: `IStudentRepository.GetPagedAsync` pushes filtering, search,
ordering, `Skip/Take` and the true total count into SQL; `StudentController.Index`
uses it (stale page numbers are clamped and re-queried). The silent `SearchAsync`
`.Take(50)` is gone — search results are properly paged with a correct total.
Schools (~70 rows) and tournaments (a handful) keep in-memory paging deliberately:
bounded data, no measurable benefit.
### 25. Read-then-write race on high-jump SortOrder — **FIXED** ✅
Verified. Fix: ordering everywhere (repository, service, view) now derives from
`Height` itself, and #38's unique `(TournamentEventLevelId, Height)` index makes
the countback's "highest bar" deterministic regardless of insertion races.
### 26. Lane assignment ignored standard seeding — **FIXED** ✅
Verified. Fix: lanes are assigned centre-out (4, 5, 3, 6, 2, 7, 1, 8 for 8 lanes);
`PopulateNextRoundAsync` feeds athletes fastest-first, so the fastest qualifiers
get the middle lanes.
### 27. Smaller items — **ALL FIXED** ✅
| Item | Resolution |
|---|---|
| Recorded score didn't recalculate points | `RecordScoreAsync` recomputes `CalculatedPoints` from the event's constant, and refreshes placements if they had already been calculated for the event |
| `RawPerformance == 0` silently skipped | Points path no longer skips zero; placements explicitly exclude no-mark scores **and clear their stale placement** (tested) |
| Empty events counted as "in progress" | Dashboard counts only event levels with registrations |
| `IsAgeBased` with no `MaxAge` | "Open Boys/Girls" seeded with `IsAgeBased = false` (existing rows unaffected — seeder skips populated tables) |
| Dead `IsEliminated` branch | Removed (`FailCount` covers it) |
| `Repository.Update` on tracked entities | `Update` now attaches only detached entities; tracked ones rely on change detection |
| Deactivation check after sign-in | `IsActive` checked **before** `PasswordSignInAsync`, same generic error (no account enumeration) |
| CDN dependency for icons | Bootstrap Icons 1.11.3 vendored to `wwwroot/lib/bootstrap-icons/`; both layouts use the local copy — icons now work offline |
| Unused volume mount | Removed from docker-compose |
---
## Second sweep findings
### 28. `/Student/Details/{id}` missing view — **FIXED** ✅
Verified (only missing view in the app). Fix: `Views/Student/Details.cshtml` added
in the style of the sibling Details pages, and a Details link added to the student
list row actions.
### 29. Eight ViewBag key mismatches — **FIXED** ✅
All eight verified. Fixes: `Student/Index` reads `SelectedSchoolId`/`SearchTerm`;
`School/Index` reads `SelectedZoneId`; `FieldEventController`,
`HighJumpController` and both `RegistrationController` actions now set
`EventName`/`LevelName` the way `TrackEventController` does (officials can see
which event and age group they are scoring); `ByStudent.cshtml` reads the
`ViewBag.Student` DTO.
### 30. School level filter wrong cast — **FIXED** ✅
Verified. Fix: `ViewBag.SelectedLevel as SchoolLevel?` with enum comparisons.
### 31. Every notification rendered twice — **FIXED** ✅
Verified (exactly the 13 listed views). Fix: all per-view
`<partial name="_Notification" />` removed; the layout's single render remains.
### 32. "Keep this alert visible" defeated — **FIXED** ✅
Verified. Fix: the duplicate timer in `site.js` is gone. The remaining timer in
`_Notification.cshtml` was also tightened: it now dismisses only alerts the
partial itself marks `data-autodismiss` (success/error) — warnings (the jump-off
message) and informational page content ("No heats yet", "no event levels") are
never auto-dismissed.
### 33. Eligibility pre-check computed then thrown away — **FIXED** ✅
Verified. Fix: `Register.cshtml` got `@model EventRegistrationCreateDto`,
re-requests the page with `studentId` on selection change, renders the
eligibility verdict (green/red panel, register button disabled when ineligible),
and keeps the selection on a rejected submission.
### 34. Events couldn't be edited or deleted from the UI — **FIXED** ✅
Verified. Fix: Edit and Delete (with confirm) buttons per event row; inactive
events are badged.
### 35. Event category filter unreachable — **FIXED** ✅
Verified. Fix: a category dropdown drives `?category=`; grouping comes from
`Enum.GetValues<EventCategory>()` so future categories appear automatically.
### 36. Missing scoring constants couldn't be added in-app — **FIXED** ✅
Verified. Fix: `ScoringConfig` now supports **add** (dropdown offers only events
without a constant — i.e. the 12 broken ones) and **delete** for scoring
constants, and add/delete for placement points (9th place etc.). All validation
failures surface via the notification banner instead of a silent redirect.
### 37. Archiving hid a tournament's reports — **FIXED** ✅
Verified. Fix: the report picker includes archived tournaments, labelled
"(archived)".
### 38. No uniqueness on round order / heat number / bar height — **FIXED** ✅
Verified. Fix: unique indexes on `(TournamentEventLevelId, RoundOrder)`,
`(RoundId, HeatNumber)` and `(TournamentEventLevelId, Height)` in the
`SchemaIntegrityFixes` migration. Friendly pre-checks added where users can hit
them (duplicate round order, duplicate bar height) so the constraint is a backstop,
not the error message.
### 39. A round couldn't be re-seeded — **FIXED** ✅
Verified. Fix: rounds with heats show a "Re-seed" button behind an explicit
confirm describing what will be replaced (handles late scratches/entries).
### 40. Admin lock-out — **FIXED** ✅
Verified. Fix: `ToggleActive` and `Edit` refuse to deactivate/demote the current
user and refuse any change that would leave zero active Admins.
### 41. `test-data.sql` targeted tournaments the seeder never creates — **FIXED** ✅
Verified (the seeder seeds no tournaments at all). Fix: the script now creates the
two named tournaments idempotently and resolves their IDs by name everywhere
(temp table `_tt`); the header comment describes reality; the summary query no
longer assumes IDs 1/2.
### 42. Extra DB work per page / user list — **FIXED** ✅
Verified. Fixes: a claims principal factory stamps first/last name into the
sign-in cookie and `_LoginPartial` reads claims — no more per-request
`GetUserAsync` (existing sessions fall back to the email initial until next
sign-in). `UserManagement.Index` does one query per role instead of one per user.
### 43. Excel exports wrote `"-"` into numeric columns — **FIXED** ✅
Verified. Fix: missing values are left blank; columns keep a uniform numeric type.
### 44. Orphaned and mislabelled files — **FIXED** ✅
Verified. Fixes: `userguide.html` deleted (duplicate of the live Help page);
`ViewModels/Placeholder.cs` deleted with its `_ViewImports` using;
`ScoreSheetDocument.cs` renamed to `QuestPdfLicenseInitializer.cs` to match its
content.
### 45. Test suite covered only pure functions — **IMPROVED** ✅ / structural note
Verified. Done: the wrong leap-year comment fixed, and 11 new service-level tests
added with mocked `IUnitOfWork` covering the highest-risk fixed logic —
eligibility (inactive student/school, completed/archived tournament, relay
rejection), placement ranking direction, tie pooling, stale-placement clearing,
and advancement-flag reset. The structural recommendation stands: an integration
test suite against a real Postgres (Testcontainers) would have caught #12, #13,
#28 and #41; it needs Docker in CI and is left as follow-up work.
---
## Summary of the fix pass
- **45/45 findings verified accurate** (one detail corrected in #21).
- **43 fixed in code**; #15 reclassified as intentional configuration per the repo
owner (not a bug); #21 mitigated pending a licensing decision.
- New EF migration: `20260811121912_SchemaIntegrityFixes` (nullable + filtered
unique `ExistingStudentId` with data normalisation; three new unique indexes).
It applies automatically at startup (`Database.MigrateAsync`).
- Build clean (only the deliberate NU1903 remains); tests 63/63.
- Behavioural notes: scoring pages now require the `Official` (or `Admin`) role;
coaches/principals can register/unregister only their own school's students.
Configuration and deployment work exactly as before (credentials in
appsettings; no environment variables required).