Write unit and integration tests for TenantSessionStore dual persistence
epic-organization-selection-and-onboarding-data-layer-task-010 — Write comprehensive tests for TenantSessionStore covering: unit tests for SecureStorage serialization/deserialization, null handling when no session exists, and rollback behavior on RPC failure. Write integration tests verifying that after persistSelection() the Supabase custom claim is present in the active JWT and that restoreSelection() on simulated app restart returns the correct TenantSessionData and re-applies the claim. Verify clearSelection() removes both the secure storage entry and the session claim. Use flutter_test with mocktail for unit tests.
Acceptance Criteria
Technical Requirements
Execution Context
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.
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.
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.