high priority medium complexity testing pending testing specialist Tier 3

Acceptance Criteria

SessionResumeManager registers exactly one WidgetsBindingObserver on construction and removes it on dispose()
Calling didChangeAppLifecycleState(AppLifecycleState.resumed) twice within the debounce window results in exactly one routing action
Calling didChangeAppLifecycleState(AppLifecycleState.resumed) after the debounce window elapses results in a second routing action
When session token is expired, onResume routes to the credential login route and no biometric prompt is shown
When session is valid and BiometricStatus.available, onResume routes to BiometricPromptOverlay route
When session is valid and BiometricStatus.notEnrolled or hardwareUnavailable, onResume routes to BiometricUnavailableBanner route
Revocation flow invokes SecureStorage.deleteAll() and CredentialStore.clear() before any navigation call (atomicity: both calls succeed or neither navigates)
If SecureStorage.deleteAll() throws, revocation flow does not navigate and surfaces an error event on the error stream
dispose() called after registration does not throw and subsequent lifecycle events are silently ignored

Technical Requirements

frameworks
flutter_test
mocktail or mockito
fake_async
apis
WidgetsBindingObserver (mocked binding)
SecureStorage (mocked)
CredentialStore (mocked)
AppRouter (mocked)
data models
SessionState
BiometricStatus
AuthCredentials
performance requirements
All unit tests complete in under 3 seconds
Debounce timer tests use fake_async to avoid real wall-clock waits
security requirements
Verify deleteAll() is called before navigate() in revocation to prevent credential leakage on partial failure
Ensure no credentials are written to test logs or stdout during teardown scenarios

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

SessionResumeManager should accept its dependencies (SecureStorage, CredentialStore, AppRouter, BiometricStatusChecker, clock/ticker for debounce) through constructor injection — not via service locator — to make unit testing straightforward. The debounce timer should use a Ticker or a clock abstraction rather than calling DateTime.now() directly, so fake_async can control time. For atomicity in the revocation flow, wrap deleteAll() and clear() in a try/catch that rolls back or surfaces an error before calling navigate(). Avoid catching broad Exception types — be explicit about storage-layer exceptions.

Testing Requirements

Pure Dart unit tests using flutter_test (no widget pump needed). Use fake_async package to control time for debounce assertions — never use real Future.delayed or sleep in tests. Mock WidgetsBinding using a FakeWidgetsBinding or pass the observer management methods as injectable callbacks to avoid tight coupling with the Flutter engine. Verify call order on mocks (e.g., deleteAll before navigate) using mocktail's verifyInOrder.

Each test case should be isolated with setUp/tearDown creating fresh manager instances. Target 100% line and branch coverage for SessionResumeManager.

Component
Session Resume Manager
service medium
Epic Risks (3)
medium impact high prob technical

Flutter's AppLifecycleState.resumed event fires in scenarios beyond simple user-initiated app-switching: it also fires when the native biometric dialog itself dismisses and returns control to Flutter, when system alerts (low battery, notifications) temporarily cover the app, and when the app is foregrounded by a deep link. Without careful debouncing and state tracking, SessionResumeManager can trigger multiple overlapping biometric prompts or prompt immediately after a just-completed authentication, creating a confusing loop.

Mitigation & Contingency

Mitigation: Implement a state machine inside SessionResumeManager with explicit states (idle, prompting, authenticated, awaiting-fallback) and guard all prompt triggers with a state check. Add a minimum inter-prompt interval of 3 seconds. Write widget tests that simulate rapid lifecycle event sequences and verify only one prompt is shown.

Contingency: If the state machine approach proves difficult to test or maintain, replace it with a simple boolean isPromptActive flag with a debounce timer, accepting slightly less precise semantics in exchange for simpler reasoning about concurrent lifecycle events.

high impact medium prob technical

The BiometricPromptOverlay appears on top of the existing app content when the app resumes. If focus is not correctly transferred to the overlay and returned to the underlying screen after authentication, screen reader users (VoiceOver/TalkBack) will either be unable to interact with the prompt or will lose their navigation position in the app after authentication completes, violating WCAG 2.2 focus management requirements and creating a broken experience for Blindeforbundet users.

Mitigation & Contingency

Mitigation: Use Flutter's FocusScope and FocusTrap utilities to capture focus within the overlay on presentation and restore the previous FocusNode after dismissal. Add a live region announcement (using the accessibility live region announcer component) when the overlay appears. Include dedicated VoiceOver and TalkBack test cases in the acceptance criteria.

Contingency: If FocusTrap behaviour proves unreliable across Flutter versions, implement the overlay as a full Navigator push to a modal route rather than an overlay widget, which gives Flutter's built-in modal semantics and focus management automatic WCAG-compliant behaviour.

low impact high prob technical

The BiometricUnavailableBanner needs to deep-link to biometric enrollment settings on both iOS and Android. iOS uses a single URL scheme (app-settings:) that opens the app's settings page. Android has no universal URL for biometric settings — the correct Intent action (Settings.ACTION_BIOMETRIC_ENROLL) was introduced in API 30, with different fallback actions required for API 23-29. Using the wrong action on older Android devices either crashes or navigates to an unrelated settings screen.

Mitigation & Contingency

Mitigation: Build an Android API version check into LocalAuthIntegration that selects the correct Intent action based on the runtime SDK version. Test against Android API 23, 28, and 30+ in the CI matrix. For iOS, validate that the app-settings: URL scheme is correctly declared.

Contingency: If the Android settings Intent fragmentation cannot be resolved reliably for all target API levels, fall back to navigating to the top-level Settings screen (Settings.ACTION_SETTINGS) with an overlay instruction telling the user to navigate to 'Security > Biometrics' manually, ensuring the user always has a path to resolve the issue even if the deep link is imprecise.