package main import ( "errors" "os" "path/filepath" "strings" "testing" "time" "mxl-player/internal/playback" ) func TestDecodePlaylistFile(t *testing.T) { input := `{ "loop": true, "on_failure": "next", "entries": [ { "name": "sync", "video": {"domain": "/video", "uuid": "video-1"}, "audio": {"domain": "/audio", "uuid": "audio-1"}, "sync": true, "duration": "10s" }, { "name": "video", "video": {"domain": "/other-video", "uuid": "video-2"}, "duration": "250ms" }, { "name": "audio", "audio": {"domain": "/other-audio", "uuid": "audio-3"}, "duration": "1m" }, { "name": "manual", "video": {"domain": "/video", "uuid": "video-4"} } ] }` got, err := decodePlaylistFile(strings.NewReader(input)) if err != nil { t.Fatalf("decodePlaylistFile() error = %v", err) } want := playback.Playlist{ Loop: true, OnFailure: playback.PlaylistFailureNext, Entries: []playback.PlaylistEntry{ { Name: "sync", Video: playback.PlaylistFeed{Domain: "/video", UUID: "video-1"}, Audio: playback.PlaylistFeed{Domain: "/audio", UUID: "audio-1"}, SyncRequested: true, Duration: 10 * time.Second, }, { Name: "video", Video: playback.PlaylistFeed{Domain: "/other-video", UUID: "video-2"}, Duration: 250 * time.Millisecond, }, { Name: "audio", Audio: playback.PlaylistFeed{Domain: "/other-audio", UUID: "audio-3"}, Duration: time.Minute, }, { Name: "manual", Video: playback.PlaylistFeed{Domain: "/video", UUID: "video-4"}, }, }, } if len(got.Entries) != len(want.Entries) || got.Loop != want.Loop || got.OnFailure != want.OnFailure { t.Fatalf("decodePlaylistFile() = %#v, want %#v", got, want) } for index := range want.Entries { if got.Entries[index] != want.Entries[index] { t.Fatalf("entry %d = %#v, want %#v", index, got.Entries[index], want.Entries[index]) } } } func TestDecodePlaylistFileAllowsEmptyPlaylist(t *testing.T) { got, err := decodePlaylistFile(strings.NewReader(`{"entries": []}`)) if err != nil { t.Fatalf("decodePlaylistFile() error = %v", err) } if len(got.Entries) != 0 || got.Loop { t.Fatalf("decodePlaylistFile() = %#v, want empty non-looping playlist", got) } } func TestDecodePlaylistFileRejectsInvalidInput(t *testing.T) { tests := []struct { name string input string wantErr error wantText string }{ {name: "empty input", input: ``, wantText: "decode JSON"}, {name: "malformed JSON", input: `{"entries": [`, wantText: "decode JSON"}, {name: "unknown field", input: `{"unknown": true}`, wantText: "unknown field"}, {name: "multiple roots", input: `{"entries": []} {"entries": []}`, wantText: "multiple root values"}, {name: "invalid failure policy", input: `{"on_failure":"skip","entries":[]}`, wantErr: playback.ErrPlaylistFailurePolicy}, { name: "invalid duration", input: `{"entries":[{"video":{"domain":"/video","uuid":"video"},"duration":"later"}]}`, wantText: `playlist entry 0 duration "later"`, }, { name: "negative duration", input: `{"entries":[{"video":{"domain":"/video","uuid":"video"},"duration":"-1s"}]}`, wantErr: playback.ErrPlaylistDurationNegative, }, { name: "UUID without domain", input: `{"entries":[{"video":{"uuid":"video"}}]}`, wantErr: playback.ErrFeedDomainRequired, }, { name: "domain without UUID", input: `{"entries":[{"audio":{"domain":"/audio"}}]}`, wantErr: playback.ErrPlaylistFeedUUIDRequired, }, { name: "empty entry", input: `{"entries":[{}]}`, wantErr: playback.ErrPlaylistEntryEmpty, }, { name: "sync with one feed", input: `{"entries":[{"video":{"domain":"/video","uuid":"video"},"sync":true}]}`, wantErr: playback.ErrPlaylistSyncFeedsRequired, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { _, err := decodePlaylistFile(strings.NewReader(test.input)) if err == nil { t.Fatal("decodePlaylistFile() error = nil") } if test.wantErr != nil && !errors.Is(err, test.wantErr) { t.Fatalf("decodePlaylistFile() error = %v, want %v", err, test.wantErr) } if test.wantText != "" && !strings.Contains(err.Error(), test.wantText) { t.Fatalf("decodePlaylistFile() error = %q, want text %q", err, test.wantText) } }) } } func TestLoadPlaylistFile(t *testing.T) { directory := t.TempDir() path := filepath.Join(directory, "playlist.json") input := []byte(`{"loop":true,"entries":[{"audio":{"domain":"/audio","uuid":"audio"}}]}`) if err := os.WriteFile(path, input, 0o600); err != nil { t.Fatalf("WriteFile() error = %v", err) } got, err := loadPlaylistFile(path) if err != nil { t.Fatalf("loadPlaylistFile() error = %v", err) } if !got.Loop || len(got.Entries) != 1 || got.Entries[0].Audio.UUID != "audio" { t.Fatalf("loadPlaylistFile() = %#v", got) } } func TestLoadPlaylistFileIncludesPathInErrors(t *testing.T) { path := filepath.Join(t.TempDir(), "missing.json") _, err := loadPlaylistFile(path) if err == nil || !strings.Contains(err.Error(), path) { t.Fatalf("loadPlaylistFile() error = %v, want path %q", err, path) } }