veza/veza-backend-api/internal/handlers/embed_handler_test.go

109 lines
2.3 KiB
Go
Raw Normal View History

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 13:49:54 +00:00
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)
}
})
}
}