Unit and integration tests for all three core services
epic-travel-expense-registration-core-services-task-011 — Write comprehensive test coverage for ExpenseValidationService (mutual exclusion combinations, receipt threshold edge cases, distance boundary values), ExpenseSubmissionService (happy path auto-approval, receipt upload failure recovery, offline draft queue flush), and ExpenseAttestationService (queue fetch pagination, approve/reject with and without comment, Realtime subscription merge logic). Use flutter_test with mocked Supabase and repository fakes.
Acceptance Criteria
Technical Requirements
Execution Context
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
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'.
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.
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.
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.