high priority medium complexity testing pending testing specialist Tier 3

Acceptance Criteria

Test case: resolver constructs correct Supabase query filtering by authenticated user's ID and returning only active organization memberships
Test case: on the second call to resolve(), the cached result is returned and the Supabase mock is NOT called again (verified via mock interaction count)
Test case: after sign-out event, the cache is invalidated and the next call to resolve() triggers a fresh Supabase query
Test case: when Supabase returns exactly 1 organization, the resolver returns a SingleOrgResult (or equivalent) and the auto-proceed path is taken
Test case: when Supabase returns 2–5 organizations, the resolver returns a MultiOrgResult with the full list and the switcher path is taken
Test case: when Supabase returns an empty list (no active memberships), the resolver returns a NoMembershipsResult with a typed error
Test case: when Supabase query throws a network exception, the resolver propagates a typed NetworkError (does not crash or return null)
Test case: cache entry has a TTL — after TTL expiry, the next call triggers a fresh Supabase query (if TTL is implemented)
All mocks verified using mocktail's `verify` to assert exact call counts
Test file has 100% branch coverage on MultiOrgMembershipResolver logic

Technical Requirements

frameworks
Flutter
flutter_test
mocktail (Supabase client mock)
apis
Supabase PostgREST query builder (mocked: .from().select().eq().execute())
Supabase Auth onAuthStateChange stream (mocked for sign-out event)
data models
assignment (membership records, org_id, status fields)
contact (user identity for query filter)
performance requirements
Cache hit must return synchronously (no async gap)
All unit tests complete in under 10 seconds total
security requirements
Test fixtures use clearly fake UUIDs — no real user data
Verify that RLS filtering is applied via the authenticated Supabase client (not service role key)

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

Use a simple in-memory map as the cache implementation (Map) keyed by user_id with an optional DateTime expiry. For cache invalidation on sign-out, subscribe to `supabase.auth.onAuthStateChange` in the constructor and call `_cache.clear()` on SIGNED_OUT events — but be sure to cancel the subscription in a dispose() method to prevent memory leaks. Model the resolver output as a sealed class with three subtypes: `SingleOrgMembership`, `MultiOrgMembership`, `NoActiveMembership` — this makes the switcher/auto-proceed branching exhaustive and compiler-enforced. When constructing the Supabase query, select only the columns needed (org_id, org_name, is_active) rather than `*` to minimize payload size.

For the NHF 5-chapter case, ensure the resolver sorts organizations consistently (alphabetically by name) so the switcher list order is deterministic.

Testing Requirements

Pure unit tests using `test()` (not `testWidgets`). Use mocktail to create a `MockSupabaseClient` and a `MockSupabaseQueryBuilder`. Stub the query chain to return controlled JSON payloads. Use `verify()` and `verifyNever()` to assert exact Supabase call counts for cache hit/miss scenarios.

For the sign-out invalidation test, emit a signed-out event on the mocked auth state stream and then call resolve() again. Group tests by behavior: query construction, cache behavior, result routing, error handling. Aim for 100% branch coverage — use `lcov` or Flutter's built-in coverage tool to verify. Add a coverage assertion in CI if the project supports it.

Component
Multi-Organization Membership Resolver
service medium
Epic Risks (4)
high impact medium prob technical

TenantContextService must invalidate all downstream Riverpod providers when the org context changes (org switch scenario). If any provider caches org-specific data without subscribing to the tenant context, it will serve stale data from the previous org after a switch — which is both a UX failure and a potential GDPR violation.

Mitigation & Contingency

Mitigation: Define a single TenantContextProvider at the root of the Riverpod provider graph that all org-scoped providers depend on via ref.watch(). When TenantContextService.seedContext() runs, it invalidates TenantContextProvider which cascades invalidation to all dependents. Document this pattern in an architectural decision record so all developers follow it.

Contingency: Implement a post-switch integrity check that re-fetches a sample of each major data entity type and confirms the returned org_id matches the newly selected context; surface a reload prompt if any mismatch is detected.

medium impact medium prob security

MultiOrgMembershipResolver must query role assignments across potentially multiple tenant schemas. The anon or authenticated Supabase RLS policy may not permit cross-schema queries, making it impossible to return the full list of orgs a user belongs to in a single call.

Mitigation & Contingency

Mitigation: Design the membership query to use a dedicated Supabase edge function or a shared public schema view that aggregates role assignments across tenant schemas with a service-role key, returning only the org IDs the calling user is permitted to see. This keeps the client read-only.

Contingency: If cross-schema queries cannot be made safely, fall back to a per-org sequential membership check using the list of known org IDs and coalesce results client-side with appropriate timeout handling.

medium impact low prob technical

go_router redirect guards behave differently on web vs. mobile for deep links and browser back-button navigation. If the app is later deployed as a Progressive Web App (PWA) for admin use, the OrgRouteGuard may loop or fail to apply correctly on browser navigation events.

Mitigation & Contingency

Mitigation: Implement the guard as a GoRouter.redirect callback (not a ShellRoute redirect) following go_router best practices for platform-agnostic guards. Write widget tests that simulate navigation with and without auth/org context on both mobile and web target platforms in CI.

Contingency: If web-specific guard behaviour differs unacceptably, introduce a platform check in the guard and apply separate redirect logic branches for web vs. mobile until a unified solution is found.

medium impact medium prob scope

In Phase 2 the OrgSelectionService will need to coordinate the handoff to BankID/Vipps authentication after the org is selected, storing the returned personnummer against the correct tenant's member record. If the service is designed too narrowly for Phase 1 email/password flow, retrofitting Phase 2 will require invasive changes to an already-tested component.

Mitigation & Contingency

Mitigation: Design OrgSelectionService with an AuthHandoffStrategy interface from the start (Phase 1 implementation: email/password, Phase 2: BankID/Vipps). The strategy pattern makes the Phase 2 swap an additive change rather than a rewrite. Stub the interface in Phase 1 with a TODO comment referencing the Phase 2 epic.

Contingency: If Phase 2 requirements diverge significantly from Phase 1 assumptions, create a dedicated Phase2OrgSelectionService subclass that extends the base and overrides the auth handoff step, preserving Phase 1 behaviour unchanged.