The lock's ViewModels and biometric path have no tests #65
Labels
No Label
P0
P1
P2
release-blocker
No Milestone
No project
No Assignees
1 Participants
Notifications
Due Date
No due date set.
Dependencies
No dependencies set.
Reference: null/Privacy-Period-Tracker#65
Loading…
Reference in New Issue
No description provided.
Delete Branch "%!s(<nil>)"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What is true now.
core/securityis well covered (AppLockRepositoryTest,PinVerifierTest,LockoutPolicyTest, instrumentedKeystoreVerifierTest), andapphasAppLockControllerTestandLockCopyTest. Everything between them is untested:AppLockViewModel(no test file at all — the LockState derivation, the five UnlockResult branches,relock()state reset,deliverPendingNotificationAction),LockSettingsViewModel(none — which is why the change-PIN bypass went unnoticed),BiometricUnlock(none — the availability matrix, every error branch, the persistence side-effect),AppLockGate(none — the ON_STOP / isChangingConfigurations / authInProgress guard matrix), andLockEraseViewModel's mandated ordering. There is noapp/src/androidTestdirectory and no compose-ui-test dependency (app/build.gradle.kts:108-113). Settings and setup copy is unguarded —LockCopyTestcovers only the lock screen'sLockCopy.What it costs.
docs/qa/ClaudeReport.mdrecords the standing judgment that every defect of consequence in this project was found by a person using the app and none by a test. The lock is the feature where that is most expensive.What to do.
AppLockViewModelTestandLockSettingsViewModelTest(Robolectric, real repositories over temp DataStores, using the small public(store, macs)seam onAppLockRepository); a pureBiometricDecisionTestover every prompt error code once the decision logic is extracted from the callbacks;BiometricAvailabilityTestusing Robolectric'sShadowBiometricManager;LockEraseViewModelTestfor the erase ordering; and aLockSettingsCopyTestextending theLockCopyTestpattern to the settings and setup strings.Traps.
AppLockRepository's three-argument constructor,Clocksandcore/security'sFakeMacProviderare allinternaland unreachable fromapp/src/test; the one-argument constructor needs the Android Keystore, which does not exist on the host JVM. A small public two-argument seam is the minimum honest change. Robolectric'sShadowBiometricManagerhas a quirk — a non-STRONG authenticator type is returned as the status code — comment it in the test so nobody 'fixes' it.Verify:
./gradlew :app:testDebugUnitTestruns the named ViewModel, decision and copy tests, andscripts/prove-guard.shreddens exactly one test for each of two mutations (the migration's BIOMETRIC guard; the biometric no-enrolment branch).Closed by the same work as #63, which is where the tests had to come from — each rule needed its harness before it could be proved.
AppLockViewModel's lock-state derivation is now an extracted lockStateOf(method, unlocked) asserted directly in LockSettingsViewModelTest's gate test, which collects the flow throughout a PIN being set and asserts Locked never appears — the race that used to throw a user to the lock screen seconds after choosing a PIN.
LockSettingsViewModel: 11 tests covering every transition (off→PIN, off→either, PIN→off with a wrong PIN refused, PIN→either, PIN→fingerprint-only, abandoning, choosing the method already in force), including that a failed scan writes nothing and that consent alone commits nothing.
LockMethodMigration: 9 tests including the never-PIN-less invariant across all four legacy shapes, idempotency, not overwriting a chosen method, and an unreadable preferences file still leaving a PIN path.
The biometric path is covered where it is decidable off-device: the outcome handling is a pure branch on the ViewModel (onBiometricOutcome), tested in both directions, and the persistence side-effect is gone entirely — no biometric result writes anything in fingerprint-only mode, which was the bypass risk. The prompt itself (BiometricPrompt construction, availability codes) remains a device row in SECURITY_CHECKLIST.md, now with four new entries, because Robolectric's ShadowBiometricManager cannot exercise a real sensor.
Copy is guarded by LockCopyTest (3) and the new LockSettingsCopyTest (5). Back behaviour by LockSettingsBackTest (4) and ForgotPinBackTest (1). AppLockControllerTest (6) unchanged and still green.
Verify: ./gradlew :app:testDebugUnitTest :core:security:testDebugUnitTest — 115 and 44 tests, all green.