package subsonic import ( "io" "net/http" "testing" "git.noroutes.net/NoRoutes/vibetunes/internal/database" ) func TestGetPodcastEpisode(t *testing.T) { s, db := newTestServer(t) u := createTestUser(t, s, "alice", "password123", false) perms := u.Permissions perms.Podcast = true if err := database.SetUserPermissions(db, u.ID, perms); err != nil { t.Fatal(err) } channel, err := database.CreatePodcastChannel(db, "http://example.com/feed.xml") if err != nil { t.Fatal(err) } ep, err := database.UpsertPodcastEpisode(db, channel.ID, "guid1", "Episode One", "desc", "", "http://example.com/a.mp3", 100) if err != nil { t.Fatal(err) } srv := newSubsonicTestServer(t, s) q := baseParams("alice", "password123") q.Set("id", ep.ID) q.Set("f", "json") resp := getPath(t, srv, "/rest/getPodcastEpisode.view", q) j := decodeJSON(t, resp) got, ok := j["podcastEpisode"].(map[string]any) if !ok || got["title"] != "Episode One" { t.Fatalf("getPodcastEpisode = %v", j) } if got["channelId"] != channel.ID { t.Fatalf("channelId = %v, want %v", got["channelId"], channel.ID) } } func TestGetPodcastEpisodeNotFound(t *testing.T) { s, db := newTestServer(t) u := createTestUser(t, s, "alice", "password123", false) perms := u.Permissions perms.Podcast = true if err := database.SetUserPermissions(db, u.ID, perms); err != nil { t.Fatal(err) } srv := newSubsonicTestServer(t, s) q := baseParams("alice", "password123") q.Set("id", "pe_doesnotexist") resp := getPath(t, srv, "/rest/getPodcastEpisode.view", q) x := decodeXML(t, resp) if x.Status != "failed" || x.Error == nil || x.Error.Code != ErrNotFound { t.Fatalf("expected error 70, got %+v", x) } } func TestGetPodcastEpisodeRequiresPermission(t *testing.T) { s, db := newTestServer(t) u := createTestUser(t, s, "alice", "password123", false) perms := u.Permissions perms.Podcast = false if err := database.SetUserPermissions(db, u.ID, perms); err != nil { t.Fatal(err) } srv := newSubsonicTestServer(t, s) q := baseParams("alice", "password123") q.Set("id", "pe_whatever") resp := getPath(t, srv, "/rest/getPodcastEpisode.view", q) x := decodeXML(t, resp) if x.Status != "failed" || x.Error == nil || x.Error.Code != ErrUnauthorized { t.Fatalf("expected error 50, got %+v", x) } } func TestTopSongsByArtistID(t *testing.T) { s, db := newTestServer(t) createTestUser(t, s, "alice", "password123", false) seedLibrary(t, s) artists, err := database.ListArtists(db) if err != nil || len(artists) == 0 { t.Fatalf("ListArtists: %v %v", artists, err) } srv := newSubsonicTestServer(t, s) q := baseParams("alice", "password123") q.Set("id", artists[0].ID) q.Set("f", "json") resp := getPath(t, srv, "/rest/getTopSongs.view", q) j := decodeJSON(t, resp) topSongs, ok := j["topSongs"].(map[string]any) if !ok { t.Fatalf("topSongs missing: %v", j) } songs, ok := topSongs["song"].([]any) if !ok || len(songs) == 0 { t.Fatalf("expected songs from getTopSongs by artist id, got %v", topSongs) } } func TestTopSongsByArtistIDUnknown(t *testing.T) { s, srv := newTestRecorderServer(t) createTestUser(t, s, "alice", "password123", false) q := baseParams("alice", "password123") q.Set("id", "ar_doesnotexist") resp := getPath(t, srv.URL, "/rest/getTopSongs.view", q) x := decodeXML(t, resp) if x.Status != "ok" { t.Fatalf("unknown artist id should return ok with empty list, got %+v", x) } } func TestStreamTranscodeOffsetSeeksReal(t *testing.T) { s, srv := newTestRecorderServer(t) createTestUser(t, s, "alice", "password123", false) seedLibrary(t, s) song := firstSong(t, s) q := baseParams("alice", "password123") q.Set("id", song.ID) // The seeded fixture songs are ~1s long; ask to start after 1s so a // naive "decode from zero" implementation would still succeed but a // real seek should produce a very short (near-silent) stream. q.Set("timeOffset", "1") resp := getPath(t, srv.URL, "/rest/stream.view", q) defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("status = %d", resp.StatusCode) } // A transcode was actually triggered (not a direct byte stream) — // confirmed by the response being a compressed audio Content-Type // rather than a plain file passthrough with Accept-Ranges. if resp.Header.Get("Accept-Ranges") == "bytes" { t.Error("expected a transcoded (non-range-servable) response when timeOffset is set") } body, _ := io.ReadAll(resp.Body) if len(body) == 0 { t.Fatal("transcoded+offset stream returned no bytes") } }