high priority high complexity testing pending testing specialist Tier 7

Acceptance Criteria

All BiometricAuthService public methods have corresponding unit tests with 100% branch coverage
BiometricAuthBloc state machine transitions are fully tested: Initial → Authenticating → Authenticated, Initial → Authenticating → Failed, Initial → Authenticating → Cancelled, Initial → Authenticating → LockedOut, Authenticated → StepUpRequired → StepUpAuthenticated
Device capability detection tests cover iOS (Face ID, Touch ID, none) and Android (fingerprint, face, none) scenarios using mocked LocalAuthIntegration
Offline mode tests verify that deferred network behavior queues the session token refresh and resolves correctly when connectivity is restored
Step-up auth gating tests verify that protected operations are blocked when step-up has not been completed and allowed after successful step-up
Fallback flow tests confirm graceful degradation to PIN/password when biometrics are unavailable or locked out
Lockout scenario tests verify correct lockout state emission and recovery flow after lockout clears
All tests use Mockito-style mocks for LocalAuthIntegration and SupabaseSessionManager — no real network or device calls
Test file is co-located with source in test/ mirroring lib/ structure and named *_test.dart
All 6 BiometricAuthOutcome variants (success, failure, cancel, lockout, fallback, notAvailable) are covered by at least one positive and one negative test case
No test depends on wall-clock time; use fake timers or injectable clocks for any timeout logic
CI pipeline executes all tests with zero flakiness across 3 consecutive runs

Technical Requirements

frameworks
flutter_test (unit test runner)
bloc_test (BlocTestCase, emitsInOrder)
mocktail or mockito (mock generation)
fake_async (fake timer control)
apis
flutter_local_auth (mocked — LocalAuthentication interface)
Supabase Auth SDK (mocked — SupabaseSessionManager interface)
performance requirements
Full test suite executes in under 30 seconds on CI
No individual test exceeds 2 seconds execution time
Zero memory leaks — all StreamControllers and BLoC instances closed in tearDown
security requirements
No real credentials, tokens, or personnummer appear in test fixtures
Mock responses must not expose real Supabase project URLs or API keys
Test data uses clearly fake UUIDs (e.g., 00000000-0000-0000-0000-000000000001)

Execution Context

Execution Tier
Tier 7

Tier 7 - 84 tasks

Can start after Tier 6 completes

Implementation Notes

Use mocktail over mockito to avoid code generation overhead in a test-only task. Structure tests in three files: biometric_auth_service_test.dart (service logic), biometric_auth_bloc_test.dart (state machine), and biometric_auth_integration_mock_test.dart (composed flows). For Bloc tests, prefer blocTest() from package:bloc_test over manual stream assertions — it handles async event dispatch and teardown cleanly. For offline deferred behavior, inject a connectivity notifier mock that exposes a StreamController; trigger connectivity restoration mid-test to validate queue drain.

For lockout, verify that the Bloc emits BiometricLockedOut state and that a subsequent authenticate() event while locked out immediately re-emits the lockout state without calling LocalAuthentication. Document each test group with a brief comment explaining the scenario being modelled. Ensure test fixtures are defined as constants at the top of each file to avoid magic strings.

Testing Requirements

This task IS the testing task. Specifically: (1) Unit tests using flutter_test — no widget or integration tests required here. (2) Use bloc_test's blocTest() helper to assert exact state sequences emitted by BiometricAuthBloc. (3) Use fake_async to control Timer behavior for lockout recovery and offline retry logic without real delays.

(4) Each test class must call setUp() to initialize fresh mocks and tearDown() to dispose streams/blocs. (5) Test naming convention: 'given_[context]_when_[action]_then_[expected]'. (6) Generate coverage report with flutter test --coverage; coverage/lcov.info must show ≥95% line coverage on BiometricAuthService and BiometricAuthBloc.

Component
Biometric Authentication Service
service medium
Epic Risks (3)
high impact medium prob technical

Multiple concurrent callers (e.g., SessionResumeManager and a background sync service) could simultaneously detect a near-expired token and each invoke SupabaseSessionManager.refreshSession(), causing duplicate refresh API calls and potentially a token invalidation race condition on the Supabase Auth server. This can result in one caller receiving a valid refreshed token while another receives a 401, causing intermittent authentication failures.

Mitigation & Contingency

Mitigation: Implement a single-flight pattern inside SupabaseSessionManager so that concurrent refresh calls coalesce into one in-flight request. Use a Dart Completer or AsyncMemoizer to ensure all waiters receive the same refreshed token. Write a concurrent integration test to validate the single-flight behaviour.

Contingency: If the single-flight pattern introduces deadlocks or timeout complexity, fall back to a mutex-based lock with a 10-second timeout, logging a warning if the lock is held longer than expected, and triggering a full re-login if the refresh ultimately fails.

high impact low prob security

Supabase Row-Level Security policies evaluate the JWT claims (user_id, role, org_id) on every query. If the refreshed token contains stale or changed claims — for example if a coordinator's role was updated server-side — RLS may silently block data access even though the session appears valid from the client's perspective, causing confusing empty screens rather than an authentication error.

Mitigation & Contingency

Mitigation: After every token refresh, decode the new JWT and compare key claims (role, org_id) with the cached values. If claims have changed, emit a session-claims-changed event that triggers a role re-resolution and navigation reset. Document this behaviour in the SupabaseSessionManager API contract.

Contingency: If claims drift is detected in production and causes data visibility issues, provide a force-refresh mechanism in the UI (pull-to-refresh on home screen) that clears cached role state and re-fetches from Supabase, accompanied by a user-visible toast indicating the session was refreshed.

medium impact medium prob security

Allowing session resumption from cached local token when offline introduces a window where a revoked or invalidated session can still grant app access. For example, if a coordinator deactivates a peer mentor's account while the mentor is offline, the mentor continues to have access until connectivity is restored and the token is validated server-side.

Mitigation & Contingency

Mitigation: Set a maximum offline grace period (e.g., 24 hours) stored alongside the token in SecureSessionStorage. If the grace period is exceeded, force a full credential re-login regardless of connectivity status. Scope offline access to read-only operations only, requiring connectivity for any write that reaches Supabase.

Contingency: If the offline grace period logic is found to be insufficient for compliance, implement remote session invalidation via a lightweight push notification that clears SecureSessionStorage even when the app is backgrounded, using FCM with a data-only message.