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
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>
108 lines
2.3 KiB
Go
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)
|
|
}
|
|
})
|
|
}
|
|
}
|