package publicurl_test
import (
"context"
"net/url"
"testing"
"github.com/go-chi/jwtauth/v5"
"github.com/navidrome/navidrome/conf"
"github.com/navidrome/navidrome/conf/configtest"
"github.com/navidrome/navidrome/core/auth"
"github.com/navidrome/navidrome/core/publicurl"
"github.com/navidrome/navidrome/log"
"github.com/navidrome/navidrome/model"
"github.com/navidrome/navidrome/model/request"
"github.com/navidrome/navidrome/tests"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestPublicURL(t *testing.T) {
tests.Init(t, false)
log.SetLevel(log.LevelFatal)
RegisterFailHandler(Fail)
RunSpecs(t, "Public URL Suite")
}
var _ = Describe("Public URL Utilities", func() {
BeforeEach(func() {
DeferCleanup(configtest.SetupConfig())
})
Describe("PublicURL", func() {
When("ShareURL is set", func() {
BeforeEach(func() {
conf.Server.ShareURL = "https://share.example.com"
})
It("uses ShareURL as the base", func() {
result := publicurl.PublicURL(context.Background(), "/path/to/resource", nil)
Expect(result).To(Equal("https://share.example.com/path/to/resource"))
})
It("includes query parameters", func() {
params := url.Values{"size": []string{"300"}, "format": []string{"png"}}
result := publicurl.PublicURL(context.Background(), "/image/123", params)
Expect(result).To(ContainSubstring("https://share.example.com/image/123"))
Expect(result).To(ContainSubstring("size=300"))
Expect(result).To(ContainSubstring("format=png"))
})
})
When("ShareURL includes a path", func() {
BeforeEach(func() {
conf.Server.ShareURL = "https://example.com/navi"
})
It("prepends the ShareURL path to the resource", func() {
result := publicurl.PublicURL(context.Background(), "/share/img/hash", nil)
Expect(result).To(Equal("https://example.com/navi/share/img/hash"))
})
It("prepends the ShareURL path and includes query parameters", func() {
params := url.Values{"size": []string{"600"}}
result := publicurl.PublicURL(context.Background(), "/share/img/hash", params)
Expect(result).To(Equal("https://example.com/navi/share/img/hash?size=600"))
})
It("handles trailing slash in ShareURL path", func() {
conf.Server.ShareURL = "https://example.com/navi/"
result := publicurl.PublicURL(context.Background(), "/share/img/hash", nil)
Expect(result).To(Equal("https://example.com/navi/share/img/hash"))
})
})
When("ShareURL is not set", func() {
BeforeEach(func() {
conf.Server.ShareURL = ""
})
It("falls back to AbsoluteURL with request", func() {
ctx := request.WithServerAddress(context.Background(), "https", "myserver.com")
result := publicurl.PublicURL(ctx, "/path/to/resource", nil)
Expect(result).To(Equal("https://myserver.com/path/to/resource"))
})
It("falls back to localhost on the configured port without request", func() {
conf.Server.Port = 4533
result := publicurl.PublicURL(context.Background(), "/path/to/resource", nil)
Expect(result).To(Equal("http://localhost:4533/path/to/resource"))
})
})
})
Describe("AbsoluteURL", func() {
When("BaseHost is set", func() {
BeforeEach(func() {
conf.Server.BaseHost = "configured.example.com"
conf.Server.BaseScheme = "https"
conf.Server.BasePath = ""
})
It("uses BaseHost and BaseScheme", func() {
result := publicurl.AbsoluteURL(context.Background(), "/path/to/resource", nil)
Expect(result).To(Equal("https://configured.example.com/path/to/resource"))
})
It("defaults to http scheme if BaseScheme is empty", func() {
conf.Server.BaseScheme = ""
result := publicurl.AbsoluteURL(context.Background(), "/path/to/resource", nil)
Expect(result).To(Equal("http://configured.example.com/path/to/resource"))
})
})
When("BaseHost is not set", func() {
BeforeEach(func() {
conf.Server.BaseHost = ""
conf.Server.BasePath = ""
})
It("extracts host from request", func() {
ctx := request.WithServerAddress(context.Background(), "https", "request.example.com")
result := publicurl.AbsoluteURL(ctx, "/path/to/resource", nil)
Expect(result).To(Equal("https://request.example.com/path/to/resource"))
})
It("falls back to localhost on the configured port without request", func() {
conf.Server.Port = 8080
result := publicurl.AbsoluteURL(context.Background(), "/path/to/resource", nil)
Expect(result).To(Equal("http://localhost:8080/path/to/resource"))
})
It("uses https in the fallback when TLS is configured", func() {
conf.Server.Port = 4533
conf.Server.TLSCert = "cert.pem"
conf.Server.TLSKey = "key.pem"
result := publicurl.AbsoluteURL(context.Background(), "/path/to/resource", nil)
Expect(result).To(Equal("https://localhost:4533/path/to/resource"))
})
It("stays on http when only the certificate is configured", func() {
conf.Server.Port = 4533
conf.Server.TLSCert = "cert.pem"
result := publicurl.AbsoluteURL(context.Background(), "/path/to/resource", nil)
Expect(result).To(Equal("http://localhost:4533/path/to/resource"))
})
})
When("BasePath is set", func() {
BeforeEach(func() {
conf.Server.BasePath = "/navidrome"
conf.Server.BaseHost = "example.com"
conf.Server.BaseScheme = "https"
})
It("prepends BasePath to the URL", func() {
result := publicurl.AbsoluteURL(context.Background(), "/path/to/resource", nil)
Expect(result).To(Equal("https://example.com/navidrome/path/to/resource"))
})
})
It("passes through absolute URLs unchanged", func() {
result := publicurl.AbsoluteURL(context.Background(), "https://other.example.com/path", nil)
Expect(result).To(Equal("https://other.example.com/path"))
})
It("includes query parameters", func() {
conf.Server.BaseHost = "example.com"
conf.Server.BaseScheme = "https"
params := url.Values{"key": []string{"value"}}
result := publicurl.AbsoluteURL(context.Background(), "/path", params)
Expect(result).To(Equal("https://example.com/path?key=value"))
})
})
Describe("ImageURL", func() {
BeforeEach(func() {
conf.Server.ShareURL = "https://share.example.com"
auth.PublicTokenAuth = jwtauth.New("HS256", []byte("test secret"), nil)
})
It("generates a URL with the artwork token", func() {
artID := model.NewArtworkID(model.KindAlbumArtwork, "album-123", nil)
result := publicurl.ImageURL(context.Background(), artID, 0)
Expect(result).To(HavePrefix("https://share.example.com/share/img/"))
})
It("includes size parameter when provided", func() {
artID := model.NewArtworkID(model.KindArtistArtwork, "artist-1", nil)
result := publicurl.ImageURL(context.Background(), artID, 300)
Expect(result).To(ContainSubstring("size=300"))
})
It("omits size parameter when zero", func() {
artID := model.NewArtworkID(model.KindMediaFileArtwork, "track-1", nil)
result := publicurl.ImageURL(context.Background(), artID, 0)
Expect(result).ToNot(ContainSubstring("size="))
})
})
Describe("ImageURL address precedence", func() {
var artID model.ArtworkID
BeforeEach(func() {
auth.PublicTokenAuth = jwtauth.New("HS256", []byte("test secret"), nil)
artID = model.NewArtworkID(model.KindMediaFileArtwork, "track-1", nil)
})
It("uses the address of the request that triggered the call", func() {
ctx := request.WithServerAddress(context.Background(), "https", "music.example.com")
result := publicurl.ImageURL(ctx, artID, 300)
Expect(result).To(HavePrefix("https://music.example.com/share/img/"))
Expect(result).To(ContainSubstring("size=300"))
})
It("prefers ShareURL over the address in the context", func() {
conf.Server.ShareURL = "https://share.example.com"
ctx := request.WithServerAddress(context.Background(), "https", "music.example.com")
result := publicurl.ImageURL(ctx, artID, 0)
Expect(result).To(HavePrefix("https://share.example.com/share/img/"))
})
It("falls back to localhost on the configured port when no address is available", func() {
conf.Server.Port = 4533
result := publicurl.ImageURL(context.Background(), artID, 0)
Expect(result).To(HavePrefix("http://localhost:4533/share/img/"))
})
})
})