high priority medium complexity testing pending testing specialist Tier 5

Acceptance Criteria

Unit test: persistSelection() serializes TenantSessionData to JSON and writes it to SecureStorage with the correct key
Unit test: restoreSelection() deserializes JSON from SecureStorage and returns a TenantSessionData with all fields intact
Unit test: restoreSelection() returns null (not throws) when SecureStorage contains no entry for the session key
Unit test: if the Supabase RPC call fails inside persistSelection(), the SecureStorage write is rolled back (atomic behavior)
Unit test: clearSelection() calls delete on SecureStorage and also invokes the RPC to remove the session claim
Integration test: after persistSelection(), decoding the active Supabase JWT reveals the app.current_org_id claim matching the selected organization
Integration test: simulating app restart (re-instantiating TenantSessionStore without clearing storage) calls restoreSelection() and the returned TenantSessionData matches the persisted value
Integration test: restoreSelection() re-applies the custom claim so RLS queries immediately scope to the restored tenant
Integration test: clearSelection() leaves SecureStorage empty and subsequent JWT contains no app.current_org_id claim
All tests pass in CI with mocked SecureStorage for unit tests and a test Supabase project for integration tests

Technical Requirements

frameworks
Flutter
flutter_test
mocktail
flutter_secure_storage
apis
Supabase RPC (set_current_org_id or equivalent)
Supabase Auth (JWT inspection)
data models
TenantSessionData
Organization
performance requirements
RPC round-trip for claim setting must complete within 2 seconds in integration tests
restoreSelection() must complete before the first authenticated route is rendered (blocking call on startup)
security requirements
SecureStorage key names must be non-guessable constants defined in a single location
JWT claim must only be set server-side via RPC — never by client-side JWT manipulation
Integration tests must not persist real org IDs beyond the test teardown; clearSelection() called in tearDown

Execution Context

Execution Tier
Tier 5

Tier 5 - 253 tasks

Can start after Tier 4 completes

Implementation Notes

The rollback test is the trickiest: TenantSessionStore must write to SecureStorage first, then call the Supabase RPC; if the RPC throws, it must call SecureStorage.delete before re-throwing. Test this by setting up the mock to: allow the write, throw on the RPC call, then assert delete was subsequently called. For JWT inspection in integration tests, parse the JWT manually (split on '.', base64-decode the payload) — no extra library needed in Dart. Use a `FakeSecureStorageAdapter` (implements the interface, backed by a plain Map) as a test double rather than mocktail for the SecureStorage, to avoid verbose stubbing of every key.

For the restart simulation, do NOT call `dispose()` on the first instance — instead instantiate a second `TenantSessionStore` with the same FakeSecureStorageAdapter instance to simulate reading persisted data.

Testing Requirements

Unit tests (flutter_test + mocktail): mock both SecureStorageAdapter and the Supabase client. Test the full state machine: no session → persistSelection → restoreSelection → clearSelection → no session. Verify rollback by making the mock RPC throw and asserting SecureStorage delete was called. Test null-safe deserialization by passing malformed JSON and asserting a safe fallback (null return, not crash).

Integration tests: authenticate against a test Supabase project, call persistSelection() with a known org ID, decode the JWT using a JWT library and assert the claim value, then simulate restart by creating a new TenantSessionStore instance and calling restoreSelection(). Run integration tests only when RUN_INTEGRATION_TESTS=true. Target 100% branch coverage on TenantSessionStore logic.

Component
Tenant Session Store
data medium
Epic Risks (2)
high impact low prob technical

TenantSessionStore must write to both SecureStorageAdapter and Supabase session synchronously. If the Supabase write succeeds but the local secure storage write fails (or vice versa), the system ends up in an inconsistent state: local app thinks org A is selected but Supabase queries are scoped to org B (or unscoped), causing silent data leakage or empty result sets.

Mitigation & Contingency

Mitigation: Implement the dual-write with compensating rollback: if the second write fails, undo the first write and surface a typed DualWriteFailureError to callers. Add a startup integrity check in restoreSession() that validates local storage and Supabase session agree on the current org_id.

Contingency: If integrity check fails on startup, clear both stores and redirect the user to the org selection screen rather than proceeding with potentially mismatched state.

medium impact low prob integration

An organization could be deactivated in Supabase between the time the org list is cached and the time the user taps to select it. If the repository serves stale cached data the org-selection-service will attempt to seed a context for an inactive org, potentially causing RLS scope issues or confusing error states.

Mitigation & Contingency

Mitigation: The OrganizationRepository.getOrganizationById() path used during selection validation always performs a fresh network fetch (bypassing cache) to confirm the org is still active before the TenantSessionStore writes anything.

Contingency: If a freshness check finds the org is inactive, surface a localized error message on the selection screen ('This organization is no longer available') and refresh the org list to show only currently active partners.