high priority low complexity testing pending testing specialist Tier 4

Acceptance Criteria

Test: uploadReportFile constructs storage path as `{orgId}/{reportId}/{filename}` (or equivalent org-scoped convention) — verified via captured argument to mock upload call
Test: upload method emits at least one progress event with `bytesTransferred > 0` before completing
Test: getSignedUrl is called with the correct bucket name, file path, and expiry duration matching the configured value (e.g., 3600 seconds)
Test: getSignedUrl response is returned as a non-empty String URL
Test: downloadReportFile internally calls getSignedUrl and does not perform a separate direct download without authorization
Test: deleteReportFile invokes storage `remove()` with the exact same org-scoped path used during upload
Test: StorageException from Supabase storage is caught and re-thrown as a typed domain StorageException with the original message preserved
Test: upload with a null or empty file throws an ArgumentError before any network call is made
All tests are pure unit tests — no real Supabase storage calls
Mockito mocks are generated and committed under `test/unit/clients/`

Technical Requirements

frameworks
flutter_test
mockito (^5.x)
build_runner
apis
Supabase Storage API (SupabaseStorageClient, StorageFileApi)
ReportFileStorageClient public interface (uploadReportFile, getSignedUrl, downloadReportFile, deleteReportFile)
data models
ReportHistoryRecord (for reportId used in path construction)
StorageUploadProgress (stream event type)
performance requirements
All unit tests complete in under 3 seconds total
security requirements
Storage paths must always include organization_id as the first path segment — test failure if org prefix is missing
Signed URL expiry must be validated — excessively long expiry (>24h) should either be rejected or flagged in a test

Execution Context

Execution Tier
Tier 4

Tier 4 - 323 tasks

Can start after Tier 3 completes

Implementation Notes

Supabase Flutter's storage client (`supabase.storage.from(bucket)`) returns a `StorageFileApi`. The upload method signature and progress streaming vary between supabase_flutter versions — check `pubspec.lock` for the exact version before writing mocks. In newer versions, progress is reported via an `onUploadProgress` callback rather than a Stream. If so, capture the callback in the mock and call it with a synthetic `StorageUploadProgress` object to verify the client handles it.

For path construction, define the expected convention as a constant or helper in the client class itself (e.g., `_buildPath(orgId, reportId, filename)`), which makes it independently testable with a simple unit test that doesn't need mocks. Wrap all storage API calls in try/catch converting `StorageException` to a domain exception — keep the error wrapping in a single private method `_handleStorageError()` to avoid repetition across methods.

Testing Requirements

Pure unit tests using Mockito mocks for `SupabaseStorageClient` and `StorageFileApi`. For the progress stream test, use a `StreamController` and emit synthetic progress events from the mock, then assert the stream received them via `expectLater(stream, emitsInOrder([...]))`. For path construction tests, use `verify(mockStorageApi.uploadBinary(captureAny, ...))` and assert the captured path matches the expected pattern. For error wrapping tests, configure the mock to throw `StorageException` and verify the client catches it and throws the domain-level equivalent.

Test file: `report_file_storage_client_test.dart` under `test/unit/clients/`. Group tests by method name for clarity.

Component
Report File Storage Client
data low
Epic Risks (3)
high impact medium prob security

Incorrectly authored RLS policies could silently allow cross-organization data reads, exposing sensitive report history of one organization to coordinators of another in a multi-tenant environment.

Mitigation & Contingency

Mitigation: Write integration tests that explicitly authenticate as a user from organization A and assert zero rows are returned for organization B's history records. Use Supabase's built-in RLS testing utilities and review policies with a second developer.

Contingency: If a cross-tenant leak is discovered post-deployment, immediately revoke all active sessions for affected organizations, audit query logs for unauthorized access, and patch the RLS policy in a hotfix migration before re-enabling access.

medium impact medium prob technical

The 5-year retention policy for report files may conflict with Supabase Storage's lack of native lifecycle rules, requiring a custom pg_cron job that could fail silently and either delete files prematurely or never clean up.

Mitigation & Contingency

Mitigation: Implement the retention cleanup as a documented pg_cron job with explicit logging to a separate audit_jobs table. Add a Supabase Edge Function health check that verifies the cron job ran within the last 25 hours.

Contingency: If the cron job fails, files accumulate in storage (non-critical for compliance — over-retention is safer than under-retention). Alert the ops team via monitoring and manually trigger the cleanup function once the cron issue is resolved.

medium impact low prob integration

Signed URL generation depends on the requesting user's Supabase session being valid at the time of the call. If sessions expire during a long screen interaction, URL generation will fail with an authorization error and confuse the coordinator.

Mitigation & Contingency

Mitigation: Wrap signed URL generation in the service layer with a session-refresh check before calling Supabase Storage. Generate URLs on demand (tap-to-download) rather than pre-generating them for all list items on screen load.

Contingency: If a URL generation fails due to session expiry, surface a clear error message prompting the coordinator to re-authenticate, then automatically retry URL generation after session refresh completes.