high priority high complexity testing pending testing specialist Tier 8

Acceptance Criteria

All three services have a dedicated test file: expense_validation_service_test.dart, expense_submission_service_test.dart, expense_attestation_service_test.dart
ExpenseValidationService: at least 12 test cases covering all mutual-exclusion pairs (km + bus ticket, km + taxi, parking + taxi, etc.), receipt threshold boundary values (99 NOK = no receipt, 100 NOK = receipt required, 101 NOK = receipt required), and distance boundaries (0 km, 49 km, 50 km, 51 km for auto-approval threshold)
ExpenseSubmissionService: happy path auto-approval test (distance < 50 km, no receipt required, claim status becomes 'approved'), receipt upload failure recovery test (Supabase Storage throws → claim saved as draft with receiptPending=true, not lost), offline draft queue flush test (3 drafts queued offline, connection restored → all 3 submitted in order)
ExpenseAttestationService: queue fetch pagination test (page 1 returns 20 items, page 2 returns next 20), approve with optional comment (comment=null accepted), reject with empty comment throws ValidationException before any network call, reject with valid comment succeeds and emits AlreadyDecidedException on re-attempt
Realtime subscription tests: INSERT event merges at correct sort position, UPDATE to 'approved' removes claim, channel.unsubscribe() called on provider dispose
All tests use fakes/mocks — no real Supabase credentials or network calls in any test
Test suite executes in under 30 seconds with flutter test
Zero tests marked skip or todo — all tests must pass green
Test coverage for the three service files reaches at least 85% line coverage (measured via flutter test --coverage)

Technical Requirements

frameworks
Flutter
flutter_test
Dart
Riverpod
data models
activity
claim_event
assignment
performance requirements
Full test suite completes in under 30 seconds in CI
No real timers or delays — use fake async (FakeAsync from flutter_test) for timeout and retry scenarios
security requirements
No real API keys, Supabase URLs, or credentials in any test file
Test fixtures must not contain real PII — use clearly synthetic names like 'Test User' and placeholder UUIDs

Execution Context

Execution Tier
Tier 8

Tier 8 - 48 tasks

Can start after Tier 7 completes

Implementation Notes

Structure fakes in a test/fakes/ directory shared across all three test files to avoid duplication. FakeExpenseRepository should implement the same abstract interface as the real ExpenseRepository and allow tests to configure: throw on next call, return a specific list, simulate offline. For Riverpod notifier tests, use ProviderContainer with overrides to inject fakes: ProviderContainer(overrides: [expenseRepositoryProvider.overrideWithValue(fakeRepo)]). For the Realtime subscription tests, create a FakeRealtimeChannel that exposes a StreamController the test can push events into, then assert on the resulting notifier state.

For coverage measurement, add a Makefile or README note: flutter test --coverage && genhtml coverage/lcov.info -o coverage/html. Do not write tests for private helper methods — only test public service interfaces.

Testing Requirements

This task IS the testing task. All tests written with flutter_test. Use manual fake classes (FakeExpenseRepository, FakeNotificationDispatcher, FakeRealtimeChannel) rather than mockito generated mocks to avoid code-gen overhead. Use ProviderContainer from Riverpod for testing notifiers in isolation.

Use FakeAsync for all time-dependent scenarios (offline queue flush delay, Realtime reconnect). Ensure every test has a clear arrange-act-assert structure with descriptive test names in the format 'given X, when Y, then Z'.

Component
Expense Validation Service
service high
Epic Risks (3)
high impact medium prob scope

Mutual exclusion rules are stored in the expense type catalogue's exclusive_groups field. If the catalogue schema or group definitions differ between HLF and Blindeforbundet, the validation service must handle multiple group configurations without hardcoding organisation-specific logic.

Mitigation & Contingency

Mitigation: Design the validation service to be purely data-driven: it reads exclusive_groups from the cached catalogue and enforces whichever groups are defined, with no hardcoded organisation names. Write parameterised unit tests covering at least 4 different catalogue configurations to verify generality.

Contingency: If an organisation requires non-standard exclusion semantics (e.g. partial exclusion within a group), introduce an exclusion_type field to the catalogue schema and extend the service, treating it as a catalogue configuration change rather than a code fork.

medium impact high prob technical

The attestation service subscribes to Supabase Realtime for live queue updates. On mobile, Realtime WebSocket connections can be dropped during network transitions, causing the coordinator queue to become stale without the user being aware.

Mitigation & Contingency

Mitigation: Implement connection lifecycle management: reconnect on network-change events, show a 'reconnecting' indicator when the subscription is broken, and perform a full queue refresh on reconnect rather than relying solely on delta events.

Contingency: Add a manual pull-to-refresh gesture on the attestation queue screen as a guaranteed fallback. If Realtime proves unreliable in production, switch to periodic polling (30-second interval) as a degraded but functional mode.

medium impact medium prob integration

If a peer mentor submits a draft while offline and then submits the same claim again after connectivity is restored (thinking the first attempt failed), duplicate claims may be persisted in Supabase.

Mitigation & Contingency

Mitigation: Assign a client-generated idempotency key (UUID) to each draft at creation time. The submission service sends this key as an upsert key to Supabase, preventing duplicate inserts. The draft is marked 'submitted' locally after first successful upload.

Contingency: Implement a server-side duplicate detection trigger on the expense_claims table checking (activity_id, claimant_id, created_date) within a 24-hour window and returning the existing record ID rather than inserting a duplicate.