high priority medium complexity testing pending testing specialist Tier 3

Acceptance Criteria

Test case: first call for an attachment ID triggers exactly one Supabase createSignedUrl call and stores the result in the cache
Test case: second call for the same attachment ID within TTL returns the cached URL without calling Supabase
Test case: call at exactly TTL boundary (now == expiresAt) is treated as expired and triggers a refresh
Test case: call at TTL - 1 second is treated as valid and returns cached URL without refresh
Test case: invalidateCacheEntry(attachmentId) removes only the specified key; subsequent call triggers a new Supabase fetch
Test case: when Supabase createSignedUrl throws, the service returns/throws url_generation_failed error code
Clock abstraction is injected via constructor — tests do not use DateTime.now() directly
All Supabase calls are mocked; no real network calls are made
All tests pass in under 200 ms each
Test file uses descriptive group() blocks: 'cache hit', 'cache miss', 'TTL expiry', 'invalidation', 'error handling'

Technical Requirements

frameworks
Flutter
flutter_test
mocktail or mockito
apis
Supabase Storage createSignedUrl
data models
SignedUrlCacheEntry
AttachmentSignedUrl
performance requirements
Cache lookup must be O(1) — use a Map keyed by attachment ID
No async gaps in cache hit path — must be synchronous map lookup
security requirements
Signed URLs must not be persisted to disk in tests
Mocked URLs must not be real Supabase URLs to avoid accidental logging

Execution Context

Execution Tier
Tier 3

Tier 3 - 413 tasks

Can start after Tier 2 completes

Implementation Notes

Define a Clock abstraction (e.g. abstract class Clock { DateTime now(); }) and inject it into AttachmentSignedUrlService. In tests, implement a FakeClock that exposes a mutable 'now' field. Store cache entries as a Map where SignedUrlCacheEntry holds the URL string and the DateTime expiresAt.

On every cache read, compare clock.now() >= entry.expiresAt to determine expiry. For the Supabase mock, use mocktail's when().thenAnswer() to return a Future with a fake URL string. For the error test, use when().thenThrow(). Keep the service stateless except for the internal cache map to simplify testing.

Testing Requirements

Unit tests only with flutter_test. Inject a FakeClock (or equivalent DateTime provider) into AttachmentSignedUrlService constructor to control time in tests. Mock the Supabase storage client with mocktail. Test TTL boundary at t = expiresAt (expired) and t = expiresAt - 1s (valid).

Test cache invalidation by calling invalidateCacheEntry then verifying a subsequent fetch triggers a new Supabase call. Test error propagation when Supabase throws. Aim for 100% branch coverage of the cache and TTL logic.

Component
Signed URL Service
service low
Epic Risks (3)
medium impact medium prob technical

The storage upload succeeds but the subsequent metadata insert fails. The rollback delete call to Supabase Storage could itself fail (network error, transient timeout), leaving an orphaned object in the bucket with no database record pointing to it — a cost and compliance risk that also breaks delete-on-cascade logic.

Mitigation & Contingency

Mitigation: Wrap the rollback delete in a retry loop (3 attempts, exponential back-off). Log orphaned-object incidents to a dedicated structured log stream for periodic audit. Consider a scheduled Supabase Edge Function that reconciles storage objects against database records and flags orphans.

Contingency: If orphaned objects accumulate, run the reconciliation edge function manually to identify and purge them. Add a monitoring alert for metadata insert failures after successful uploads so the issue is caught within minutes.

medium impact medium prob scope

If the signed URL TTL is set too short, users browsing the attachment preview modal on slow connections will receive expired URLs before the content loads, causing a broken experience. If set too long, a URL shared outside the app (e.g., pasted into a chat) remains valid beyond the intended access window.

Mitigation & Contingency

Mitigation: Default TTL to 60 minutes, configurable via a named constant. The in-memory cache TTL should be set to TTL minus 5 minutes to ensure cached URLs are refreshed before they expire. Document the trade-off in code comments.

Contingency: If users report broken previews, shorten the cache TTL hotfix. If a URL leak is reported, rotate the Supabase storage signing secret to invalidate all outstanding signed URLs immediately.

medium impact medium prob technical

The multi-attachment user story requires parallel uploads with individual progress indicators. Managing concurrent BLoC events for 3–5 simultaneous uploads risks state collisions, progress indicator mixups, or partial rollbacks that are difficult to reason about.

Mitigation & Contingency

Mitigation: Design the BLoC to maintain a per-attachment upload state map keyed by a client-generated UUID. Each upload runs as an isolated Future with its own result emitted as a typed event. Write integration tests for 3-concurrent-upload scenarios.

Contingency: If state collisions occur in production, fall back to sequential upload processing (one at a time) gated behind a feature flag until the concurrent model is stabilised.