Skip to content

test: raise API coverage to 99.88% of lines and 100% of branches - #138

Merged
bgard68 merged 3 commits into
mainfrom
claude/new-session-muubr7
Aug 25, 2026
Merged

test: raise API coverage to 99.88% of lines and 100% of branches#138
bgard68 merged 3 commits into
mainfrom
claude/new-session-muubr7

Conversation

@bgard68

@bgard68 bgard68 commented Aug 25, 2026

Copy link
Copy Markdown
Owner

Raises API coverage from 86.51% of lines / 64.69% of branches to 99.88% / 100%, and the suite from 97 tests to 289. All green.

Tooling

  • coverlet.collector on both test projects, configured by coverlet.runsettings at the repository root: only the four TodoApp.* assemblies, auto-properties skipped.
  • Emits coverlet JSON as well as Cobertura, because the two suites have to be merged. Cobertura records only a per-line branch percentage, so a branch covered by the unit suite and one covered by the integration suite both read "1 of 2" and a naive merge under-reports. The JSON keeps each branch as its own record with a hit count, so the merge is exact. This is written up in docs/development/testing.md §3.4.

Tests

  • Auth — the rejection paths (unknown, expired and replayed refresh tokens, deactivated accounts, logout against a token the caller does not own), the already-linked and orphaned Google sign-in branches, and the concurrent-insert race that has to surface as 409 rather than 500.
  • Domain — every entity guard, including that Revoke keeps the first reason and that Activate does not rotate the security stamp.
  • Todos and categories — ownership isolation, filters, search, ordering, and the concurrency conflict where the row is deleted between the read and the write.
  • Infrastructure — the Pwned Passwords range lookup against a stub handler (k-anonymity, match, threshold, malformed lines, every fail-open path), the current-user claims reader, the hasher's malformed-hash paths, and provider selection in the composition root.
  • WebApi — each arm of the exception-to-problem mapping, the refresh cookie and its CSRF check, the JWT signing-key guard, and the app hosted as Production and with no appsettings at all.

Source changes

Two, both to make untested behaviour testable. No behaviour change either time.

  • GoogleTokenValidator — the payload mapping is split into FromPayload so it can be tested without Google's live signing keys.
  • Program.cs — the startup database initialisation and its background retry loop move to a new DatabaseStartup, which returns the retry task rather than discarding it internally. Program.cs still discards it, so startup does not wait; the retry delay and attempt count are now explicit at the call site. That is 31 lines of recovery logic that previously had no test at all.

What is not covered

Two lines, both needing a live cloud service:

Location Why
GoogleTokenValidator.ValidateAsync — the mapping after a successful validation Verifying a real Google ID token requires Google's live signing keys. The mapping itself is tested via FromPayload; the configuration guard and the malformed-token path are tested too.
Program.cs — the closing brace of the Key Vault block Only reached when AddAzureKeyVault succeeds against a real vault. The opt-in branch is covered both ways, including that a malformed KeyVault:Uri stops startup.

Verification

dotnet test TodoApp.sln on .NET 10 — 212 unit + 77 integration, 0 failures, in Debug and Release (this repo builds with TreatWarningsAsErrors).

🤖 Generated with Claude Code

https://claude.ai/code/session_019h7znwAftvs36vD4YwKMRf


Generated by Claude Code

Adds coverage tooling and the tests to close the gaps it found. Baseline was
86.51% of lines and 64.69% of branches across 97 tests; it is now 99.88% and
100% across 289.

Tooling
- coverlet.collector on both test projects, configured by coverlet.runsettings
  at the repository root: Cobertura and coverlet JSON, only the four TodoApp.*
  assemblies, auto-properties skipped.
- JSON as well as Cobertura because the two suites have to be merged. Cobertura
  records only a per-line branch percentage, so a branch covered by the unit
  suite and one covered by the integration suite both read "1 of 2" and a naive
  merge under-reports. The JSON keeps each branch as its own record with a hit
  count, so the merge is exact.

Tests
- Auth: the rejection paths (unknown, expired and replayed refresh tokens,
  deactivated accounts, logout against a token the caller does not own), the
  already-linked and orphaned Google sign-in branches, and the concurrent-insert
  race that has to surface as 409 rather than 500.
- Domain: every entity guard, including that Revoke keeps the first reason and
  that Activate does not rotate the security stamp.
- Todos and categories: ownership isolation, filters, search, ordering, and the
  concurrency conflict where the row is deleted between the read and the write.
- Infrastructure: the Pwned Passwords range lookup against a stub handler
  (k-anonymity, match, threshold, malformed lines, and every fail-open path),
  the current-user claims reader, the hasher's malformed-hash paths, and the
  provider selection in the composition root.
- WebApi: each arm of the exception-to-problem mapping, the refresh cookie and
  its CSRF check, the JWT signing-key guard, and the app hosted as Production
  and with no appsettings at all.

Production changes, both to make untested behaviour testable
- GoogleTokenValidator: the payload mapping is split into FromPayload so it can
  be tested without Google's live signing keys. No behaviour change.
- Program.cs: the startup database initialisation and its background retry loop
  move to DatabaseStartup, which returns the retry task instead of discarding it
  internally. Program.cs still discards it, so startup does not wait; the retry
  delay and attempt count are now explicit at the call site. This is 31 lines of
  recovery logic that previously had no test at all.

Two lines remain uncovered, both needing a live cloud service: the mapping after
a successful Google token validation, and the closing brace of the Key Vault
block, which is only reached when AddAzureKeyVault succeeds against a real vault.
Both are documented in docs/development/testing.md §3.4.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019h7znwAftvs36vD4YwKMRf
Comment thread src/TodoApp.WebApi/DatabaseStartup.cs Dismissed
Comment thread src/TodoApp.WebApi/DatabaseStartup.cs Dismissed
claude and others added 2 commits August 25, 2026 05:35
Two failures the CI runner exposed and this machine could not.

The breach check is on in appsettings.json and off only in
appsettings.Development.json. The rest of the suite runs as Development and so
inherited "off"; the new Production and bare-configuration hosts did not, and
started calling the real Pwned Passwords service. "Password1" is in the corpus,
so registration returned 400 on a runner with connectivity and passed on this
sandbox, where the request fails open. Turning it off in the base factory makes
the guarantee explicit for every environment a factory might host, and a new
test asserts it rather than leaving it to be rediscovered.

The bare-configuration factory also deleted its temporary content root in
Dispose, which runs on both the sync and async teardown paths — the second
delete threw out of test-class cleanup. It now checks the directory is still
there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019h7znwAftvs36vD4YwKMRf
@bgard68
bgard68 merged commit 5e8a574 into main Aug 25, 2026
11 checks passed
@bgard68
bgard68 deleted the claude/new-session-muubr7 branch August 25, 2026 05:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants