🏡 index : quine-global/quintodrome.git

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()),
	)
})