T3 — SignInService Passkey/WebAuthn Unit Tests (closing the highest-risk zero-coverage service)
SignInService.swift (1244 lines) is on the CLAUDE.md high-risk list — it owns Apple Sign In, Google, Email/OTP, and passkey/WebAuthn — yet had zero direct tests. The system passkey UI flow runs through ASAuthorizationController and needs a real device, so it stays out of scope (like AuthManagerTests documents for biometrics). What IS testable is the deterministic surface that gates the whole flow: config invariants, the persisted SessionTokenType raw-value contract, and the UserSession Codable round-trip. 10 deterministic, device-free tests; 10/10 pass on simulator. The first run earned its keep — 4 not-configured-guard tests failed, surfacing that the test bundle ships a real relying-party ID, which forced the durable rewrite to environment-independent invariants.
What do T1 / T2 / T3 mean?▾
T3 — SignInService Passkey/WebAuthn Unit Tests
Status: shipped 2026-06-10. Closes the highest-risk zero-coverage service in the test-coverage inventory (RICE 48.0).
The risk
SignInService.swift (1244 lines) is on the CLAUDE.md high-risk list — it owns Apple Sign In, Google, Email/OTP, and passkey/WebAuthn — yet had zero direct tests. A refactor could silently break the passkey config gating, the not-configured fallback, or the persisted token-type contract, and nothing would catch it.
What is — and isn't — unit-testable
The passkey registration/assertion flow runs through ASAuthorizationController (Apple's system WebAuthn UI), which needs a real device + user interaction — not unit-testable. Exactly like AuthManagerTests documents for real-device biometrics, those happy paths are out of scope here. What IS testable is the deterministic surface that gates the whole flow:
- Config invariants — discovered by running the tests: the test bundle inherits the app Info.plist, so
isPasskeyConfiguredis actually true in the test environment and the not-configured guard is unreachable. The robust, environment-independent surface is the logical invariantcanShowPasskeyLogin ⇒ (isPasskeyConfigured ∧ hasRegisteredPasskey)andcanShowPasskeyLogin == falsewithout a registered passkey. - Persisted contracts —
SessionTokenTyperaw values (passkeySignature,supabaseJWT, …) are stored in sessions; a rename would silently invalidate every stored passkey session on upgrade. Pinned via raw-value + Codable round-trip tests. UserSessionCodable round-trip for a passkey session (withcredentialID) — survives encode/decode intact.
What shipped
FitTrackerTests/SignInServicePasskeyTests.swift — 10 tests (@MainActor, @testable import FitTracker, mirroring AuthManagerTests style), registered in FitTracker.xcodeproj/project.pbxproj (4 entries: PBXBuildFile + PBXFileReference + group + Sources phase). All deterministic, no UI, no network — they run as ordinary unit tests on the existing test target.
Verification
10/10 pass on a clean run (iPhone 17 Pro simulator). The suite is fully deterministic + device-free. The ASAuthorizationController registration/assertion happy path remains a device-only gap, documented in the test header — the honest boundary, matching the project's UI-test-thinness policy.
Why this is the right scope
It would have been easy — and wrong — to mock ASAuthorizationController into a fake "passkey succeeds" path; that tests the mock, not the service. Pinning the config gating + the not-configured fallback + the persisted token contract is the part that (a) is deterministic and (b) actually protects users: if passkey silently became "configured" when it isn't, or a stored token type drifted, real sessions break. That's the surface a unit test should own.
Where to read more
- Source case study:
docs/case-studies/t3-signinservice-passkey-tests-case-study.md - Spec:
docs/master-plan/test-coverage-master-plan-2026-05-13.md§4 T3 - Template:
FitTrackerTests/AuthManagerTests.swift(device-free scope precedent) - Sibling reframes (same week):
ai-golden-set-evals,t5-mock-protocol-drift
- •Scope is deterministic + device-free only: the actual passkey registration/assertion happy path runs through
ASAuthorizationController(Apple's system WebAuthn UI), needs a real device + user interaction, and stays an untested gap — these 10 tests pin config invariants, theSessionTokenTyperaw-value contract, and Codable round-trips, not the live ceremony. - •The first run surfaced that the test bundle inherits the app Info.plist (so
isPasskeyConfiguredis true in-test and the not-configured guard is unreachable), forcing a rewrite to environment-independent logical invariants — i.e. some originally-intended guard paths cannot be exercised in the unit environment at all.
- Tests assert against the ASAuthorizationController UI flow (would be non-deterministic / device-only).
- Tests pin implementation detail rather than the behavioral/persistence contract.