high priority medium complexity testing pending testing specialist Tier 3

Acceptance Criteria

All tests run with flutter test — no physical device or emulator required
LocalAuthentication is mocked (mockito @GenerateMocks or manual fake implementing a mockable interface)
isAvailable: returns false when isDeviceSupported() returns false (regardless of canCheckBiometrics)
isAvailable: returns false when canCheckBiometrics returns false even if isDeviceSupported is true
isAvailable: returns true when both isDeviceSupported and canCheckBiometrics return true
getAvailableBiometrics: maps local_auth BiometricType.face to domain BiometricType.face
getAvailableBiometrics: maps local_auth BiometricType.fingerprint to domain BiometricType.fingerprint
getAvailableBiometrics: maps local_auth BiometricType.iris to domain BiometricType.iris (if supported)
getAvailableBiometrics: an unrecognised BiometricType is omitted from the result list — no crash
authenticate: returns Result.success when LocalAuthentication.authenticate returns true
authenticate: returns Result.failure(LocalAuthFailure.notAvailable) on the corresponding PlatformException
authenticate: returns Result.failure(LocalAuthFailure.notEnrolled) on the corresponding PlatformException
authenticate: returns Result.failure(LocalAuthFailure.lockedOut) on the corresponding PlatformException
authenticate: returns Result.failure(LocalAuthFailure.permanentlyLockedOut) on the corresponding PlatformException
authenticate: returns Result.failure(LocalAuthFailure.cancelled) when authentication returns false without exception
authenticate: returns Result.failure(LocalAuthFailure.unknown) for an unrecognised exception
All tests isolated with setUp() resetting mocks

Technical Requirements

frameworks
Flutter
flutter_test
mockito (or manual fake)
apis
LocalAuthentication.isDeviceSupported()
LocalAuthentication.canCheckBiometrics
LocalAuthentication.getAvailableBiometrics()
LocalAuthentication.authenticate()
data models
ILocalAuthIntegration
LocalAuthIntegrationImpl
BiometricType (domain)
LocalAuthFailure (sealed class)
Result<void, LocalAuthFailure>
performance requirements
Each test completes under 100ms
Full suite under 10 seconds
security requirements
Tests must verify that stickyAuth: true is passed to authenticate() — use argument captor
Tests must verify that useErrorDialogs: true is passed — ensures OS-level error UI is shown, not silently swallowed

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

Because LocalAuthentication is a concrete class from a plugin (not an interface), mocking it directly with mockito requires @GenerateMocks([LocalAuthentication]) and running build_runner. An alternative that avoids codegen is to wrap LocalAuthentication behind a thin ILocalAuthPlugin interface with a real adapter — then the test can use a hand-written fake implementing ILocalAuthPlugin. This approach is recommended if the project does not already use build_runner for mockito. For the BiometricType mapping test, pass a list containing all known local_auth BiometricType values and assert the domain list matches exactly.

For the cancelled test: local_auth returns false (not an exception) when the user dismisses the dialog — the implementation should treat a false return as LocalAuthFailure.cancelled. Ensure this case is covered explicitly as it is easy to miss.

Testing Requirements

Pure unit tests with flutter_test. Group by method: isAvailable (3 cases), getAvailableBiometrics (4 cases), authenticate (7 cases). For PlatformException-based failures, construct PlatformException(code: 'NotEnrolled') etc. to simulate local_auth error codes — verify the LocalAuthFailure mapper produces the exact expected variant.

Use expect(result.isFailure, isTrue) and expect(result.failure, isA()) style assertions. Ensure exhaustive coverage of every LocalAuthFailure variant via authenticate tests. Minimum 14 test cases.

Component
Local Auth Integration
infrastructure low
Epic Risks (3)
high impact medium prob technical

iOS Keychain access requires correct entitlement configuration and provisioning profile setup. Misconfigured entitlements cause silent failures in CI/CD and on physical devices, where the plugin appears to work in the simulator but fails at runtime. This can delay foundation delivery and block all downstream epics.

Mitigation & Contingency

Mitigation: Add a dedicated integration test running on a physical iOS device early in the epic. Document required entitlements and provisioning steps in a developer runbook. Validate Keychain access in the CI pipeline using an iOS simulator with correct entitlements enabled.

Contingency: If Keychain entitlements cannot be resolved quickly, temporarily use in-memory storage behind the SecureSessionStorage interface to unblock downstream epics, then resolve the Keychain issue in a hotfix before release.

medium impact medium prob dependency

The Flutter local_auth plugin has a history of breaking API changes between major versions, and its Android implementation depends on BiometricPrompt which behaves differently across Android API levels (23-34). An incompatible plugin version or unexpected Android API behaviour can cause authentication failures on a significant portion of the target device fleet.

Mitigation & Contingency

Mitigation: Pin local_auth to a specific stable version in pubspec.yaml. Test against Android API levels 23, 28, and 33 in the CI matrix. Review the plugin changelog and migration guide before adopting any version bump.

Contingency: If the pinned version proves incompatible with target devices, evaluate flutter_local_auth_android as a replacement or fork the plugin adapter to isolate the breaking surface.

high impact low prob security

If users upgrade from a version of the app that stored session data in non-encrypted storage (SharedPreferences), a migration path is required. Failing to migrate silently leaves old tokens in plain storage, creating a security gap and potentially causing confusing authentication state on first launch of the new version.

Mitigation & Contingency

Mitigation: Audit existing storage usage across the codebase before writing SecureSessionStorage. If legacy plain storage keys exist, implement a one-time migration routine that reads from SharedPreferences, writes to Keychain/Keystore, and deletes the plain-text entry.

Contingency: If migration is discovered late, ship the migration as a mandatory patch release before the biometric feature is enabled for users, and add a startup check that blocks biometric opt-in until migration is confirmed complete.