high priority medium complexity testing pending testing specialist Tier 5

Acceptance Criteria

Draft write test: calling saveDraft with a valid selection set writes it to local storage; subsequent readDraft returns the identical selection set
Draft read on empty storage returns null or an empty selection, not an exception
Draft cleared test: calling persistFinalSelection clears the local draft so readDraft returns null/empty afterward
Supabase upsert payload test: mock Supabase client captures the argument passed to upsert; test asserts the payload contains the correct user ID, organisation ID, selected expense types array, and timestamp
Mutual exclusion test: calling persistFinalSelection with an invalid combination throws MutualExclusionViolationException and the mock Supabase client records zero upsert calls
Partial failure test: if Supabase upsert throws a network exception, the local draft is NOT cleared (rollback behaviour)
All tests use a mock Supabase client (Mockito or manual fake) — no real network calls
All tests use an in-memory fake for local storage (not SharedPreferences in test mode) — no file system dependencies
Test file is organised with group() blocks matching repository method names
All tests are deterministic and pass in isolation and in suite

Technical Requirements

frameworks
Flutter
flutter_test
Dart
Mockito or manual fakes
apis
Supabase upsert (mocked)
data models
ExpenseType
ExpenseTypeSelection
MutualExclusionViolationException
performance requirements
Full test suite for this repository must complete in under 1 second
security requirements
No real user IDs or Supabase keys in test fixtures

Execution Context

Execution Tier
Tier 5

Tier 5 - 253 tasks

Can start after Tier 4 completes

Implementation Notes

The repository must be designed for testability: inject the Supabase client and local storage adapter via constructor parameters (not singletons). If the current implementation uses static access to Supabase.instance, refactor the constructor to accept an abstract SupabaseClientInterface before writing tests. For the rollback test, use a try/catch in the test body: configure the mock to throw on upsert, call persistFinalSelection, catch the exception, then call readDraft and assert it still contains the pre-submission draft. For payload structure assertions, define an expected map literal and use expect(capturedPayload, equals(expectedPayload)) — this makes failures readable.

Organise tests as: group('saveDraft'), group('readDraft'), group('persistFinalSelection / happy path'), group('persistFinalSelection / mutual exclusion'), group('persistFinalSelection / network failure').

Testing Requirements

Unit tests using flutter_test. Create a FakeLocalStorage class implementing the storage interface with an in-memory map — do not use Hive or SharedPreferences in unit tests. Create a MockSupabaseClient using Mockito's @GenerateMocks or a manual fake that records all upsert calls and can be configured to throw on demand. Use verify(mockClient.upsert(captureAny)) to assert payload structure.

Run with: flutter test test/unit/expense_type_repository_test.dart. Aim for 100% branch coverage on the repository class.

Component
Expense Type Repository
data low
Epic Risks (2)
high impact medium prob dependency

The per-km reimbursement rate and transit zone amounts must be read from org-specific configuration stored in Supabase. If the rate configuration table or RLS policies are not yet deployed when this epic runs, the calculation service cannot be completed and integration tests will fail.

Mitigation & Contingency

Mitigation: Define a RateConfigRepository interface and inject a stub implementation with default HLF rates from day one; write the real Supabase adapter in parallel and swap via dependency injection before merge.

Contingency: If org rate config is delayed beyond this epic's window, ship with the default-rate stub and log a prominent warning; calculate with defaults and surface a 'rates not confirmed' notice in the UI preview.

medium impact low prob technical

If the peer mentor opens an expense claim on two devices simultaneously, the local draft and the Supabase record may diverge. The repository's last-write-wins strategy could silently overwrite a valid selection with a stale one.

Mitigation & Contingency

Mitigation: Add an updated_at timestamp to the draft record and reject saves where the server timestamp is newer than the local copy; surface a conflict resolution prompt rather than silently overwriting.

Contingency: If conflict resolution UI is out of scope, fall back to server-authoritative reads on app foreground resume and discard local draft, notifying the user that their draft was refreshed from the server.