mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-28 22:27:10 +01:00
test(jellyfin): e2e for playlists (CRUD, expansion, cover) and item images
This commit is contained in:
parent
121dd71ea1
commit
6aa0d6a2aa
3 changed files with 213 additions and 0 deletions
|
|
@ -185,6 +185,19 @@ func postAs(u model.User, path, body string) *httptest.ResponseRecorder {
|
|||
func del(path string) *httptest.ResponseRecorder { return jReq(adminUser, "DELETE", path, "") }
|
||||
func delAs(u model.User, path string) *httptest.ResponseRecorder { return jReq(u, "DELETE", path, "") }
|
||||
|
||||
// upload performs an authenticated POST with a custom Content-Type and raw body (image upload).
|
||||
func upload(user model.User, path, contentType string, body []byte) *httptest.ResponseRecorder {
|
||||
w := httptest.NewRecorder()
|
||||
r := httptest.NewRequest("POST", path, bytes.NewReader(body))
|
||||
token, err := auth.CreateToken(&user)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
r.Header.Set("X-Emby-Token", token)
|
||||
r.Header.Set("X-Emby-Authorization", `MediaBrowser Client="e2e", Device="test", DeviceId="e2e-device", Version="1.0"`)
|
||||
r.Header.Set("Content-Type", contentType)
|
||||
router.ServeHTTP(w, r)
|
||||
return w
|
||||
}
|
||||
|
||||
// parseInto asserts a 200 and unmarshals the JSON body into target.
|
||||
func parseInto(w *httptest.ResponseRecorder, target any) {
|
||||
Expect(w.Code).To(Equal(http.StatusOK), "body: %s", w.Body.String())
|
||||
|
|
@ -198,6 +211,20 @@ func queryResult(w *httptest.ResponseRecorder) dto.QueryResult {
|
|||
return q
|
||||
}
|
||||
|
||||
// createPlaylist creates a playlist as admin (encodedIds are the Jellyfin-encoded item ids a
|
||||
// client would send) and returns its decoded Navidrome id.
|
||||
func createPlaylist(name string, encodedIds []string) string {
|
||||
if encodedIds == nil {
|
||||
encodedIds = []string{}
|
||||
}
|
||||
body, err := json.Marshal(map[string]any{"Name": name, "Ids": encodedIds})
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
var res map[string]string
|
||||
parseInto(post("/Playlists", string(body)), &res)
|
||||
Expect(res["Id"]).ToNot(BeEmpty())
|
||||
return dto.DecodeID(res["Id"])
|
||||
}
|
||||
|
||||
// --- Seeded-id lookup helpers (return Navidrome ids; wrap with enc() for URLs) ---
|
||||
|
||||
func enc(id string) string { return dto.EncodeID(id) }
|
||||
|
|
|
|||
50
server/jellyfin/e2e/images_test.go
Normal file
50
server/jellyfin/e2e/images_test.go
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
package e2e
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/navidrome/navidrome/model/request"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
// The image endpoint is public and resolves artwork under an elevated (admin) context. The suite
|
||||
// wires a spyArtwork that captures the resolved ArtworkID and the context, so these tests assert
|
||||
// resolution and elevation without needing real image processing.
|
||||
var _ = Describe("Item images", func() {
|
||||
BeforeEach(func() { setupTestDB() })
|
||||
|
||||
It("resolves an album's Primary image", func() {
|
||||
id := albumID("Abbey Road")
|
||||
w := get("/Items/" + enc(id) + "/Images/Primary")
|
||||
Expect(w.Code).To(Equal(http.StatusOK))
|
||||
Expect(w.Body.String()).To(Equal("IMG"))
|
||||
Expect(artworkSpy.lastID).To(ContainSubstring(id))
|
||||
})
|
||||
|
||||
It("resolves an artist's Primary image", func() {
|
||||
id := artistID("Miles Davis")
|
||||
Expect(get("/Items/" + enc(id) + "/Images/Primary").Code).To(Equal(http.StatusOK))
|
||||
Expect(artworkSpy.lastID).To(ContainSubstring(id))
|
||||
})
|
||||
|
||||
It("resolves a private playlist's cover under an elevated admin context", func() {
|
||||
// Regression: the public image route carries no user, so a private playlist would fail its
|
||||
// visibility filter without the elevated-context resolution.
|
||||
plID := createPlaylist("Private Mix", nil)
|
||||
w := get("/Items/" + enc(plID) + "/Images/Primary")
|
||||
Expect(w.Code).To(Equal(http.StatusOK))
|
||||
Expect(artworkSpy.lastID).To(ContainSubstring(plID))
|
||||
|
||||
u, ok := request.UserFrom(artworkSpy.lastCtx)
|
||||
Expect(ok).To(BeTrue())
|
||||
Expect(u.IsAdmin).To(BeTrue())
|
||||
})
|
||||
|
||||
It("serves images without authentication (public route)", func() {
|
||||
id := albumID("IV")
|
||||
w := rawReq("GET", "/Items/"+enc(id)+"/Images/Primary", "")
|
||||
Expect(w.Code).To(Equal(http.StatusOK))
|
||||
Expect(w.Body.String()).To(Equal("IMG"))
|
||||
})
|
||||
})
|
||||
136
server/jellyfin/e2e/playlists_test.go
Normal file
136
server/jellyfin/e2e/playlists_test.go
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
package e2e
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"github.com/navidrome/navidrome/server/jellyfin/dto"
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
)
|
||||
|
||||
var _ = Describe("Playlists", func() {
|
||||
BeforeEach(func() { setupTestDB() })
|
||||
|
||||
playlistItems := func(plID string) dto.QueryResult {
|
||||
return queryResult(get("/Playlists/" + enc(plID) + "/Items"))
|
||||
}
|
||||
|
||||
Describe("create", func() {
|
||||
It("creates an empty playlist", func() {
|
||||
plID := createPlaylist("Empty", nil)
|
||||
var info dto.PlaylistInfo
|
||||
parseInto(get("/Playlists/"+enc(plID)), &info)
|
||||
Expect(info.OpenAccess).To(BeFalse())
|
||||
Expect(info.Shares).To(BeEmpty())
|
||||
Expect(info.ItemIds).To(BeEmpty())
|
||||
})
|
||||
|
||||
It("creates a playlist from song ids", func() {
|
||||
plID := createPlaylist("Songs", []string{enc(songID("Come Together")), enc(songID("So What"))})
|
||||
Expect(playlistItems(plID).TotalRecordCount).To(Equal(2))
|
||||
})
|
||||
|
||||
It("expands an album id into its tracks", func() {
|
||||
plID := createPlaylist("From Album", []string{enc(albumID("Abbey Road"))})
|
||||
q := playlistItems(plID)
|
||||
Expect(q.TotalRecordCount).To(Equal(2))
|
||||
Expect(names(q.Items)).To(ConsistOf("Come Together", "Something"))
|
||||
})
|
||||
|
||||
It("expands an artist id into its tracks", func() {
|
||||
plID := createPlaylist("From Artist", []string{enc(artistID("The Beatles"))})
|
||||
Expect(playlistItems(plID).TotalRecordCount).To(Equal(3)) // Abbey Road (2) + Help! (1)
|
||||
})
|
||||
})
|
||||
|
||||
Describe("items", func() {
|
||||
It("tags each entry with a PlaylistItemId", func() {
|
||||
plID := createPlaylist("Tagged", []string{enc(songID("Help!"))})
|
||||
q := playlistItems(plID)
|
||||
Expect(q.Items).To(HaveLen(1))
|
||||
Expect(q.Items[0].Type).To(Equal("Audio"))
|
||||
Expect(q.Items[0].PlaylistItemId).ToNot(BeEmpty())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("add and remove", func() {
|
||||
It("adds a song by id", func() {
|
||||
plID := createPlaylist("Add", nil)
|
||||
Expect(post("/Playlists/"+enc(plID)+"/Items?ids="+enc(songID("So What")), "").Code).To(Equal(http.StatusNoContent))
|
||||
Expect(playlistItems(plID).TotalRecordCount).To(Equal(1))
|
||||
})
|
||||
|
||||
It("adds an album (expanding to its tracks)", func() {
|
||||
plID := createPlaylist("AddAlbum", []string{enc(songID("So What"))})
|
||||
post("/Playlists/"+enc(plID)+"/Items?ids="+enc(albumID("Abbey Road")), "")
|
||||
Expect(playlistItems(plID).TotalRecordCount).To(Equal(3)) // 1 + Abbey Road (2)
|
||||
})
|
||||
|
||||
It("removes an entry by its PlaylistItemId", func() {
|
||||
plID := createPlaylist("Remove", []string{enc(songID("Come Together")), enc(songID("Something"))})
|
||||
entryID := playlistItems(plID).Items[0].PlaylistItemId
|
||||
Expect(del("/Playlists/" + enc(plID) + "/Items?entryIds=" + entryID).Code).To(Equal(http.StatusNoContent))
|
||||
Expect(playlistItems(plID).TotalRecordCount).To(Equal(1))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("users", func() {
|
||||
It("reports the current user as an editor", func() {
|
||||
plID := createPlaylist("Perms", nil)
|
||||
var perms []dto.PlaylistUserPermissions
|
||||
parseInto(get("/Playlists/"+enc(plID)+"/Users"), &perms)
|
||||
Expect(perms).To(HaveLen(1))
|
||||
Expect(perms[0].UserId).To(Equal("admin-1"))
|
||||
Expect(perms[0].CanEdit).To(BeTrue())
|
||||
})
|
||||
})
|
||||
|
||||
Describe("listing", func() {
|
||||
It("lists a created playlist advertising a Primary image tag", func() {
|
||||
createPlaylist("Listed", nil)
|
||||
q := queryResult(get("/Items?IncludeItemTypes=Playlist&Recursive=true"))
|
||||
Expect(q.TotalRecordCount).To(Equal(1))
|
||||
Expect(q.Items[0].Name).To(Equal("Listed"))
|
||||
Expect(q.Items[0].ImageTags).To(HaveKey("Primary"))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("cover art", func() {
|
||||
jpeg := []byte{0xFF, 0xD8, 0xFF, 0xE0, 0x00, 0x10, 'J', 'F', 'I', 'F', 0x00}
|
||||
|
||||
It("uploads and removes a playlist cover", func() {
|
||||
plID := createPlaylist("Cover", nil)
|
||||
|
||||
Expect(upload(adminUser, "/Items/"+enc(plID)+"/Images/Primary", "image/jpeg", jpeg).Code).
|
||||
To(Equal(http.StatusNoContent))
|
||||
|
||||
pls, err := ds.Playlist(ctx).Get(plID)
|
||||
Expect(err).ToNot(HaveOccurred())
|
||||
Expect(pls.UploadedImage).ToNot(BeEmpty())
|
||||
_, statErr := os.Stat(pls.UploadedImagePath())
|
||||
Expect(statErr).ToNot(HaveOccurred(), "cover file should exist on disk")
|
||||
|
||||
Expect(del("/Items/" + enc(plID) + "/Images/Primary").Code).To(Equal(http.StatusNoContent))
|
||||
pls, _ = ds.Playlist(ctx).Get(plID)
|
||||
Expect(pls.UploadedImage).To(BeEmpty())
|
||||
})
|
||||
|
||||
It("rejects cover upload for a non-playlist item", func() {
|
||||
Expect(upload(adminUser, "/Items/"+enc(albumID("IV"))+"/Images/Primary", "image/jpeg", jpeg).Code).
|
||||
To(Equal(http.StatusNotImplemented))
|
||||
})
|
||||
})
|
||||
|
||||
Describe("delete", func() {
|
||||
It("deletes a playlist", func() {
|
||||
plID := createPlaylist("ToDelete", nil)
|
||||
Expect(del("/Items/" + enc(plID)).Code).To(Equal(http.StatusNoContent))
|
||||
Expect(queryResult(get("/Items?IncludeItemTypes=Playlist&Recursive=true")).TotalRecordCount).To(Equal(0))
|
||||
})
|
||||
|
||||
It("returns 404 when deleting a non-playlist item", func() {
|
||||
Expect(del("/Items/" + enc(albumID("IV"))).Code).To(Equal(http.StatusNotFound))
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue