mirror of
https://github.com/navidrome/navidrome.git
synced 2026-10-31 15:37:51 +01:00
136 lines
4.9 KiB
Go
136 lines
4.9 KiB
Go
|
|
package apiv1
|
||
|
|
|
||
|
|
import (
|
||
|
|
"bytes"
|
||
|
|
"encoding/json"
|
||
|
|
"net/http"
|
||
|
|
"net/http/httptest"
|
||
|
|
|
||
|
|
"github.com/navidrome/navidrome/api"
|
||
|
|
"github.com/navidrome/navidrome/conf"
|
||
|
|
"github.com/navidrome/navidrome/conf/configtest"
|
||
|
|
"github.com/navidrome/navidrome/consts"
|
||
|
|
"github.com/navidrome/navidrome/tests"
|
||
|
|
. "github.com/onsi/ginkgo/v2"
|
||
|
|
. "github.com/onsi/gomega"
|
||
|
|
"gopkg.in/yaml.v3"
|
||
|
|
)
|
||
|
|
|
||
|
|
var _ = Describe("OpenAPI document routes", func() {
|
||
|
|
var router *Router
|
||
|
|
|
||
|
|
BeforeEach(func() {
|
||
|
|
router = New(&tests.MockDataStore{})
|
||
|
|
})
|
||
|
|
|
||
|
|
get := func(path string, headers map[string]string) *httptest.ResponseRecorder {
|
||
|
|
req := httptest.NewRequest(http.MethodGet, path, nil)
|
||
|
|
for k, v := range headers {
|
||
|
|
req.Header.Set(k, v)
|
||
|
|
}
|
||
|
|
return serve(router, req)
|
||
|
|
}
|
||
|
|
|
||
|
|
DescribeTable("serves the embedded bundle",
|
||
|
|
func(path, contentType string, body []byte) {
|
||
|
|
w := get(path, nil)
|
||
|
|
Expect(w.Code).To(Equal(http.StatusOK))
|
||
|
|
Expect(w.Header().Get("Content-Type")).To(Equal(contentType))
|
||
|
|
Expect(w.Header().Get("ETag")).ToNot(BeEmpty())
|
||
|
|
Expect(w.Header().Get("Cache-Control")).To(Equal("no-cache"))
|
||
|
|
Expect(w.Body.Bytes()).To(Equal(body))
|
||
|
|
},
|
||
|
|
Entry("JSON", "/api/v1/openapi.json", "application/json", api.SpecJSON()),
|
||
|
|
Entry("YAML", "/api/v1/openapi.yaml", "application/yaml", api.SpecYAML()),
|
||
|
|
)
|
||
|
|
|
||
|
|
DescribeTable("revalidates with If-None-Match",
|
||
|
|
func(ifNoneMatch func(etag string) string, expected int) {
|
||
|
|
etag := get("/api/v1/openapi.json", nil).Header().Get("ETag")
|
||
|
|
w := get("/api/v1/openapi.json", map[string]string{"If-None-Match": ifNoneMatch(etag)})
|
||
|
|
Expect(w.Code).To(Equal(expected))
|
||
|
|
if expected == http.StatusNotModified {
|
||
|
|
Expect(w.Body.Len()).To(BeZero())
|
||
|
|
Expect(w.Header().Get("ETag")).To(Equal(etag))
|
||
|
|
} else {
|
||
|
|
Expect(w.Body.Bytes()).To(Equal(api.SpecJSON()))
|
||
|
|
}
|
||
|
|
},
|
||
|
|
Entry("exact ETag", func(etag string) string { return etag }, http.StatusNotModified),
|
||
|
|
Entry("ETag in a list", func(etag string) string { return `"other", ` + etag }, http.StatusNotModified),
|
||
|
|
Entry("weak ETag", func(etag string) string { return "W/" + etag }, http.StatusNotModified),
|
||
|
|
Entry("wildcard", func(string) string { return "*" }, http.StatusNotModified),
|
||
|
|
Entry("stale ETag", func(string) string { return `"stale"` }, http.StatusOK),
|
||
|
|
)
|
||
|
|
|
||
|
|
It("ignores Range and returns the full document", func() {
|
||
|
|
w := get("/api/v1/openapi.json", map[string]string{"Range": "bytes=0-9"})
|
||
|
|
Expect(w.Code).To(Equal(http.StatusOK))
|
||
|
|
Expect(w.Header().Get("Accept-Ranges")).To(BeEmpty())
|
||
|
|
Expect(w.Body.Bytes()).To(Equal(api.SpecJSON()))
|
||
|
|
})
|
||
|
|
|
||
|
|
It("uses different ETags for JSON and YAML", func() {
|
||
|
|
j := get("/api/v1/openapi.json", nil)
|
||
|
|
y := get("/api/v1/openapi.yaml", nil)
|
||
|
|
Expect(j.Header().Get("ETag")).ToNot(Equal(y.Header().Get("ETag")))
|
||
|
|
})
|
||
|
|
|
||
|
|
Describe("with a base path", func() {
|
||
|
|
decode := func(format string, body []byte) map[string]any {
|
||
|
|
var doc map[string]any
|
||
|
|
if format == "json" {
|
||
|
|
ExpectWithOffset(1, json.Unmarshal(body, &doc)).To(Succeed())
|
||
|
|
} else {
|
||
|
|
ExpectWithOffset(1, yaml.Unmarshal(body, &doc)).To(Succeed())
|
||
|
|
}
|
||
|
|
return doc
|
||
|
|
}
|
||
|
|
serverURL := func(doc map[string]any) any {
|
||
|
|
return doc["servers"].([]any)[0].(map[string]any)["url"]
|
||
|
|
}
|
||
|
|
var plainETag string
|
||
|
|
|
||
|
|
BeforeEach(func() {
|
||
|
|
plainETag = get("/api/v1/openapi.json", nil).Header().Get("ETag")
|
||
|
|
DeferCleanup(configtest.SetupConfig())
|
||
|
|
conf.Server.BasePath = "/music"
|
||
|
|
router = New(&tests.MockDataStore{})
|
||
|
|
})
|
||
|
|
|
||
|
|
DescribeTable("advertises the server under the base path and changes nothing else",
|
||
|
|
func(path, format string, bundle []byte) {
|
||
|
|
w := get(path, nil)
|
||
|
|
Expect(w.Code).To(Equal(http.StatusOK))
|
||
|
|
served, original := decode(format, w.Body.Bytes()), decode(format, bundle)
|
||
|
|
Expect(serverURL(served)).To(Equal("/music/api/v1"))
|
||
|
|
delete(served, "servers")
|
||
|
|
delete(original, "servers")
|
||
|
|
Expect(served).To(Equal(original))
|
||
|
|
},
|
||
|
|
Entry("JSON", "/api/v1/openapi.json", "json", api.SpecJSON()),
|
||
|
|
Entry("YAML", "/api/v1/openapi.yaml", "yaml", api.SpecYAML()),
|
||
|
|
)
|
||
|
|
|
||
|
|
It("uses its own ETag, and still revalidates", func() {
|
||
|
|
etag := get("/api/v1/openapi.json", nil).Header().Get("ETag")
|
||
|
|
Expect(etag).ToNot(Equal(plainETag))
|
||
|
|
Expect(get("/api/v1/openapi.json", map[string]string{"If-None-Match": etag}).Code).To(Equal(http.StatusNotModified))
|
||
|
|
})
|
||
|
|
|
||
|
|
It("escapes base paths that need quoting", func() {
|
||
|
|
conf.Server.BasePath = "/my music: \"live\""
|
||
|
|
router = New(&tests.MockDataStore{})
|
||
|
|
Expect(serverURL(decode("json", get("/api/v1/openapi.json", nil).Body.Bytes()))).To(Equal("/my music: \"live\"/api/v1"))
|
||
|
|
Expect(serverURL(decode("yaml", get("/api/v1/openapi.yaml", nil).Body.Bytes()))).To(Equal("/my music: \"live\"/api/v1"))
|
||
|
|
})
|
||
|
|
})
|
||
|
|
|
||
|
|
DescribeTable("the bundle advertises the API path exactly once, which the base-path rewrite relies on",
|
||
|
|
func(bundle []byte) {
|
||
|
|
Expect(bytes.Count(bundle, []byte(consts.URLPathAPIv1))).To(Equal(1))
|
||
|
|
},
|
||
|
|
Entry("JSON", api.SpecJSON()),
|
||
|
|
Entry("YAML", api.SpecYAML()),
|
||
|
|
)
|
||
|
|
})
|