Phase 2: share system + analytics feed (backend)
CI / policy (push) Successful in 4s
CI / backend (push) Failing after 44s
CI / mobile (push) Successful in 49s
CI / profile (push) Successful in 2m19s

- share_links model/queries; opaque 12-char base62 tokens (PRD §17.1)
- POST /v1/cards/{id}/share — create share link (owner-scoped, modality-validated)
- GET /v1/s/{token} — record modality-attributed event (qr→qr_scan, nfc→nfc_tap)
  and 302-redirect to the profile; unknown/expired → 404
- GET /v1/cards/{id}/share-links — list a card's links
- analytics_events now carry the share_token correlation
- GET /v1/cards/{id}/analytics/feed — chronological event feed (§6.7.2)

6 new integration tests; 73 backend tests total. fmt + clippy clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
Omar Sobh
2026-06-04 11:19:08 -05:00
co-authored by Claude Opus 4.8
parent c30d3afeec
commit e66813ef58
17 changed files with 527 additions and 10 deletions
@@ -207,6 +207,22 @@ impl TestApp {
(status, content_type, bytes)
}
/// GET a path without following redirects; returns (status, Location header).
pub async fn get_redirect(&self, path: &str) -> (StatusCode, Option<String>) {
let req = Request::builder()
.method("GET")
.uri(path)
.body(Body::empty())
.unwrap();
let resp = self.router.clone().oneshot(req).await.unwrap();
let location = resp
.headers()
.get("location")
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string());
(resp.status(), location)
}
/// Register a fresh user and return its access token.
pub async fn register_and_token(&self) -> String {
let (email, handle) = unique_identity();