package hevcpull import ( "errors" "net/url" "testing" "time" ) func TestSignAndVerifyURL(t *testing.T) { now := time.Date(2026, 7, 25, 12, 0, 0, 0, time.UTC) signed, err := SignURL( "https://app.example.com/api/app/vid/transcode/m3u8/laosiji/m3m/demo.m3u8", "test-pull-secret-strong-32-bytes!!", now.Add(480*time.Hour), ) if err != nil { t.Fatalf("SignURL failed: %v", err) } parsed, err := url.Parse(signed) if err != nil { t.Fatalf("parse signed URL: %v", err) } if err = VerifyURL(parsed, "test-pull-secret-strong-32-bytes!!", now); err != nil { t.Fatalf("VerifyURL failed: %v", err) } parsed.Path += ".tampered" if err = VerifyURL(parsed, "test-pull-secret-strong-32-bytes!!", now); !errors.Is(err, ErrInvalidSignature) { t.Fatalf("tampered path returned %v", err) } } func TestVerifyURLRejectsExpiredOrExtraQuery(t *testing.T) { now := time.Date(2026, 7, 25, 12, 0, 0, 0, time.UTC) signed, err := SignURL( "https://app.example.com/api/app/vid/transcode/m3u8/source.m3u8", "test-pull-secret-strong-32-bytes!!", now.Add(time.Minute), ) if err != nil { t.Fatalf("SignURL failed: %v", err) } parsed, _ := url.Parse(signed) if err = VerifyURL(parsed, "test-pull-secret-strong-32-bytes!!", now.Add(time.Minute)); !errors.Is(err, ErrExpired) { t.Fatalf("expired URL returned %v", err) } parsed, _ = url.Parse(signed) query := parsed.Query() query.Set("c", "unbound-cdn") parsed.RawQuery = query.Encode() if err = VerifyURL(parsed, "test-pull-secret-strong-32-bytes!!", now); !errors.Is(err, ErrInvalidSignature) { t.Fatalf("extra query returned %v", err) } } func TestSignURLRejectsWeakSecret(t *testing.T) { _, err := SignURL("https://app.example.com/source.m3u8", "short", time.Now().Add(time.Hour)) if !errors.Is(err, ErrInvalidSecret) { t.Fatalf("weak secret returned %v", err) } } func TestRedactSignedPullURL(t *testing.T) { const signature = "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef" raw := "https://app.example/source.m3u8?hevc_exp=1&hevc_sig=" + signature redacted := RedactURL(raw) if redacted == raw || RedactText(redacted) != redacted { t.Fatalf("URL was not redacted: %s", redacted) } if got := RedactText("file_url=" + url.QueryEscape(raw)); got == "file_url="+url.QueryEscape(raw) { t.Fatalf("escaped nested URL was not redacted: %s", got) } } func TestNormalizeSource(t *testing.T) { tests := []struct { source string want string }{ {source: " /laosiji/m3m/demo.m3u8 ", want: "laosiji/m3m/demo.m3u8"}, {source: "sp/movie/index.m3u8", want: "sp/movie/index.m3u8"}, {source: "sp/movie/../index.m3u8"}, {source: `sp\movie\index.m3u8`}, {source: `sp/movie%5Cindex.m3u8`}, {source: `sp/movie%0Aindex.m3u8`}, {source: "https://cdn.example.com/index.m3u8"}, {source: "sp/movie/index.m3u8?token=x"}, {source: "sp/movie/index.mp4"}, {source: "sp/movie/index.M3U8"}, } for _, tt := range tests { got, err := NormalizeSource(tt.source) if tt.want == "" { if err == nil { t.Errorf("NormalizeSource(%q) = %q, want error", tt.source, got) } continue } if err != nil || got != tt.want { t.Errorf("NormalizeSource(%q) = %q, %v; want %q", tt.source, got, err, tt.want) } } } func TestResolveChildSource(t *testing.T) { tests := []struct { name string parent string child string want string }{ { name: "relative child", parent: "sp/movie/master.m3u8", child: "720/index.m3u8", want: "sp/movie/720/index.m3u8", }, { name: "parent traversal", parent: "sp/movie/master.m3u8", child: "../audio/index.m3u8", want: "sp/audio/index.m3u8", }, { name: "root child remains on default SP origin", parent: "sp/movie/master.m3u8", child: "/shared/index.m3u8", want: "shared/index.m3u8", }, { name: "explicit PMS root namespace is preserved", parent: "pms/movie/master.m3u8", child: "/pms/shared/index.m3u8", want: "pms/shared/index.m3u8", }, { name: "explicit laosiji root namespace is preserved", parent: "laosiji/m3m/movie/master.m3u8", child: "/laosiji/shared/index.m3u8", want: "laosiji/shared/index.m3u8", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got, err := ResolveChildSource(tt.parent, tt.child) if err != nil { t.Fatalf("ResolveChildSource error: %v", err) } if got != tt.want { t.Fatalf("ResolveChildSource = %q, want %q", got, tt.want) } }) } for _, tt := range []struct { parent string child string }{ {parent: "sp/movie/master.m3u8", child: "https://cdn.example.com/index.m3u8"}, {parent: "sp/movie/master.m3u8", child: "index.m3u8?token=secret"}, {parent: "sp/movie/master.m3u8", child: "index.ts"}, {parent: "sp/movie/master.m3u8", child: `..\index.m3u8`}, {parent: "sp/movie/master.m3u8", child: "../../outside/index.m3u8"}, {parent: "sp/movie/master.m3u8", child: "../../../sp/outside/index.m3u8"}, {parent: "sp/movie/master.m3u8", child: "/pms/outside/index.m3u8"}, {parent: "pms/movie/master.m3u8", child: "/shared/index.m3u8"}, {parent: "pms/movie/master.m3u8", child: "/sp/outside/index.m3u8"}, {parent: "laosiji/movie/master.m3u8", child: "/shared/index.m3u8"}, } { if _, err := ResolveChildSource(tt.parent, tt.child); err == nil { t.Fatalf("unsafe child accepted: parent=%q child=%q", tt.parent, tt.child) } } }