Files

98 lines
3.1 KiB
Go
Raw Permalink Normal View History

package contentid
import (
"strings"
"testing"
)
// TestPackUnpackRoundTrip is the load-bearing property for the reversible
// jellycompat codec: every provider-anchored and local content_id must survive
// Pack -> Unpack unchanged, and the packed form must fit the 15-byte budget a
// compat UUID leaves after its 1-byte kind prefix. This is what lets the compat
// layer decode an item id with no lookup table, so it stays correct across
// server restarts.
func TestPackUnpackRoundTrip(t *testing.T) {
ids := []string{
"movie-tmdb-228064",
"movie-imdb-tt2413338",
"movie-imdb-tt0944947", // leading zero must be preserved
"movie-tvdb-12345",
"series-tvdb-296762",
"series-tmdb-1399",
"series-imdb-tt0944947",
"season-tvdb-296762-1",
"season-tvdb-296762-0", // specials
"episode-tvdb-296762-1-5",
"episode-imdb-tt0944947-2-10",
"episode-tmdb-1399-10-100",
// 14-byte (28 hex) local hash — the post-change ForLocal width.
"local-0123456789abcdef0123456789ab",
ForLocal("/media/movies/Home Video.mkv"),
}
for _, id := range ids {
data, ok := Pack(id)
if !ok {
t.Fatalf("Pack(%q) ok=false, want true", id)
}
if len(data) > 15 {
t.Fatalf("Pack(%q) = %d bytes, exceeds 15-byte compat budget", id, len(data))
}
got, ok := Unpack(data)
if !ok || got != id {
t.Fatalf("round trip %q -> %x -> (%q,%v)", id, data, got, ok)
}
}
}
// TestPackRejectsNonContentID: Pack handles only the structured and local
// namespaces. Legacy numeric Sonyflake ids and anything malformed return
// ok=false so the caller falls back to its numeric/opaque path.
func TestPackRejectsNonContentID(t *testing.T) {
for _, id := range []string{
"",
"1234567890123456", // legacy Sonyflake
"garbage",
"movie-bogus-x", // invalid provider id
"episode-tvdb-296762", // truncated (missing season/episode)
"movie-",
"local-", // empty hash
"local-xyz", // non-hex hash
} {
if data, ok := Pack(id); ok {
t.Fatalf("Pack(%q) = (%x, true), want ok=false", id, data)
}
}
}
// TestUnpackRejectsGarbage: Unpack must fail closed on bytes that were not
// produced by Pack rather than returning a plausible-looking id.
func TestUnpackRejectsGarbage(t *testing.T) {
for _, b := range [][]byte{
nil,
{},
{0x00},
{0xff, 0xff, 0xff},
[]byte(strings.Repeat("\x01", 15)),
} {
if id, ok := Unpack(b); ok {
t.Fatalf("Unpack(%x) = (%q, true), want ok=false", b, id)
}
}
}
// TestUnpackLocalRequiresExactLength: unlike the padded structured forms, the
// local form fills the payload exactly, so its body must be exactly
// localHashLen. Trailing or missing bytes are non-canonical and must fail closed.
func TestUnpackLocalRequiresExactLength(t *testing.T) {
exact := append([]byte{tagLocal}, make([]byte, localHashLen)...)
if _, ok := Unpack(exact); !ok {
t.Fatalf("Unpack(exact-length local body) = false, want true")
}
for _, n := range []int{localHashLen - 1, localHashLen + 1} {
b := append([]byte{tagLocal}, make([]byte, n)...)
if id, ok := Unpack(b); ok {
t.Fatalf("Unpack(local body len %d) = (%q, true), want fail-closed", n, id)
}
}
}