veza/veza-backend-api/internal/handlers/embed_handler_test.go
senke 806bd77d09
Some checks failed
Veza CI / Rust (Stream Server) (push) Successful in 5m26s
Security Scan / Secret Scanning (gitleaks) (push) Failing after 56s
Veza CI / Backend (Go) (push) Failing after 8m39s
Veza CI / Frontend (Web) (push) Failing after 16m22s
Veza CI / Notify on failure (push) Successful in 11s
E2E Playwright / e2e (full) (push) Successful in 20m30s
feat(embed): /embed/track/:id widget + /oembed envelope + per-track OG tags (W3 Day 15)
End-to-end embed pipeline. Standalone HTML widget for iframes, oEmbed
JSON for unfurlers (Twitter/Discord/Slack), runtime per-track OG +
Twitter player card on the SPA. Share-token storage + handlers were
already in place from earlier — Day 15 only adds the embed surface.

Backend (root router, no /api/v1 prefix — matches what scrapers expect)
- internal/handlers/embed_handler.go : EmbedTrack renders inline HTML
  with OG tags + <audio controls>. DMCA-blocked tracks 451, private
  tracks 404 (don't leak existence). X-Frame-Options=ALLOWALL +
  CSP frame-ancestors=* so the page can be iframed by third parties.
  OEmbed handler accepts ?url=&format=json, validates the URL points
  at /tracks/:id, returns a type=rich envelope with an iframe HTML
  string. ?maxwidth clamped to [240, 1280].
- internal/api/routes_embed.go : registers the two endpoints.
- internal/handlers/embed_handler_test.go : pure-function coverage
  for extractTrackIDFromURL (8 cases incl. trailing slash, query
  string, hash fragment, subpath) + parseSafeInt (overflow + non-digit
  rejection).

Frontend
- apps/web/src/features/tracks/hooks/useTrackOpenGraph.ts : runtime
  injection of og:* + twitter:player + <link rel=alternate>
  (oEmbed discovery) into document.head. Limitation noted inline —
  pure HTML scrapers don't see these ; the embed widget itself
  carries server-rendered OG tags so unfurlers always work.
- TrackDetailPage : wires useTrackOpenGraph(track) on render.

E2E (tests/e2e/30-embed-and-share.spec.ts)
- 30. /embed/track/:id renders HTML with OG tags + audio src.
- 31. /oembed returns valid JSON envelope (rich type, iframe HTML).
- 32. /oembed rejects non-track URLs (400).
- 33. share-token roundtrip — creator mints, anonymous resolves via
  /api/v1/tracks/shared/:token (re-uses existing share handler ;
  Day 15 didn't add new share infra, just covers it under the embed
  acceptance gate).

Acceptance (Day 15) : embed widget Twitter card preview ✓ (OG tags
present), oEmbed JSON valid ✓, share token roundtrip ✓.

W3 verification gate : Redis Sentinel ✓ · MinIO distribué ✓ ·
CDN signed URLs ✓ · DMCA E2E ✓ · embed + share token ✓ · all 5
W3 days shipped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-28 15:49:54 +02:00

108 lines
2.3 KiB
Go

package handlers
import (
"testing"
"github.com/google/uuid"
)
// Pure-function tests for the embed handler helpers — no DB / HTTP
// fixtures required.
func TestExtractTrackIDFromURL(t *testing.T) {
id := uuid.New()
cases := []struct {
name string
url string
want uuid.UUID
ok bool
}{
{
name: "happy path — frontend URL",
url: "https://veza.fr/tracks/" + id.String(),
want: id,
ok: true,
},
{
name: "with trailing slash",
url: "https://veza.fr/tracks/" + id.String() + "/",
want: id,
ok: true,
},
{
name: "with query string",
url: "https://veza.fr/tracks/" + id.String() + "?utm_source=twitter",
want: id,
ok: true,
},
{
name: "with hash fragment",
url: "https://veza.fr/tracks/" + id.String() + "#segment-2",
want: id,
ok: true,
},
{
name: "subpath under /tracks",
url: "https://veza.fr/tracks/" + id.String() + "/comments",
want: id,
ok: true,
},
{
name: "empty",
url: "",
ok: false,
},
{
name: "no /tracks/ segment",
url: "https://veza.fr/playlists/abc",
ok: false,
},
{
name: "non-uuid",
url: "https://veza.fr/tracks/not-a-uuid",
ok: false,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
got, ok := extractTrackIDFromURL(tc.url)
if ok != tc.ok {
t.Fatalf("ok = %v, want %v", ok, tc.ok)
}
if ok && got != tc.want {
t.Fatalf("id = %s, want %s", got, tc.want)
}
})
}
}
func TestParseSafeInt(t *testing.T) {
cases := []struct {
in string
want int
wantOK bool
}{
{in: "0", want: 0, wantOK: true},
{in: "480", want: 480, wantOK: true},
{in: "1280", want: 1280, wantOK: true},
{in: "100000", want: 100000, wantOK: true},
// reject overflow attempts
{in: "999999", wantOK: false},
// reject non-digit
{in: "480px", wantOK: false},
{in: "-1", wantOK: false},
{in: "", wantOK: true}, // empty parses to 0 (loop never runs)
}
for _, tc := range cases {
t.Run(tc.in, func(t *testing.T) {
n, err := parseSafeInt(tc.in)
if (err == nil) != tc.wantOK {
t.Fatalf("parseSafeInt(%q) ok=%v, want %v (err=%v)", tc.in, err == nil, tc.wantOK, err)
}
if tc.wantOK && n != tc.want {
t.Fatalf("parseSafeInt(%q) = %d, want %d", tc.in, n, tc.want)
}
})
}
}