package serverimport ("context""encoding/json""fmt""net/http""net/http/httptest""os""regexp""strconv""strings""time""github.com/navidrome/navidrome/conf""github.com/navidrome/navidrome/conf/configtest""github.com/navidrome/navidrome/conf/mime""github.com/navidrome/navidrome/consts""github.com/navidrome/navidrome/model""github.com/navidrome/navidrome/tests". "github.com/onsi/ginkgo/v2". "github.com/onsi/gomega")var _ = Describe("serveIndex", func() {var ds model.DataStoremockUser := &mockedUserRepo{}fs := os.DirFS("tests/fixtures")BeforeEach(func() {ds = &tests.MockDataStore{MockedUser: mockUser}DeferCleanup(configtest.SetupConfig())})It("adds app_config to index.html", func() {r := httptest.NewRequest("GET", "/index.html", nil)w := httptest.NewRecorder()serveIndex(ds, fs, nil)(w, r)Expect(w.Code).To(Equal(200))config := extractAppConfig(w.Body.String())Expect(config).To(BeAssignableToTypeOf(map[string]any{}))})It("sets firstTime = true when User table is empty", func() {mockUser.empty = truer := httptest.NewRequest("GET", "/index.html", nil)w := httptest.NewRecorder()serveIndex(ds, fs, nil)(w, r)config := extractAppConfig(w.Body.String())Expect(config).To(HaveKeyWithValue("firstTime", true))})It("sets firstTime = false when User table is not empty", func() {mockUser.empty = falser := httptest.NewRequest("GET", "/index.html", nil)w := httptest.NewRecorder()serveIndex(ds, fs, nil)(w, r)config := extractAppConfig(w.Body.String())Expect(config).To(HaveKeyWithValue("firstTime", false))})DescribeTable("sets configuration values",func(configSetter func(), configKey string, expectedValue any) {configSetter()r := httptest.NewRequest("GET", "/index.html", nil)w := httptest.NewRecorder()serveIndex(ds, fs, nil)(w, r)config := extractAppConfig(w.Body.String())Expect(config).To(HaveKeyWithValue(configKey, expectedValue))},Entry("baseURL", func() { conf.Server.BasePath = "base_url_test" }, "baseURL", "base_url_test"),Entry("welcomeMessage", func() { conf.Server.UIWelcomeMessage = "Hello" }, "welcomeMessage", "Hello"),Entry("maxSidebarPlaylists", func() { conf.Server.MaxSidebarPlaylists = 42 }, "maxSidebarPlaylists", float64(42)),Entry("enableTranscodingConfig", func() { conf.Server.EnableTranscodingConfig = true }, "enableTranscodingConfig", true),Entry("enableDownloads", func() { conf.Server.EnableDownloads = true }, "enableDownloads", true),Entry("enableFavourites", func() { conf.Server.EnableFavourites = true }, "enableFavourites", true),Entry("enableStarRating", func() { conf.Server.EnableStarRating = true }, "enableStarRating", true),Entry("defaultTheme", func() { conf.Server.DefaultTheme = "Light" }, "defaultTheme", "Light"),Entry("defaultLanguage", func() { conf.Server.DefaultLanguage = "pt" }, "defaultLanguage", "pt"),Entry("defaultUIVolume", func() { conf.Server.DefaultUIVolume = 45 }, "defaultUIVolume", float64(45)),Entry("uiSearchDebounceMs", func() { conf.Server.UISearchDebounceMs = 500 }, "uiSearchDebounceMs", float64(500)),Entry("uiCoverArtSize", func() { conf.Server.UICoverArtSize = 300 }, "uiCoverArtSize", float64(300)),Entry("enableCoverAnimation", func() { conf.Server.EnableCoverAnimation = true }, "enableCoverAnimation", true),Entry("pidAlbum", func() { conf.Server.PID.Album = "folder" }, "pidAlbum", "folder"),Entry("pidTrack", func() { conf.Server.PID.Track = "title" }, "pidTrack", "title"),Entry("enableNowPlaying", func() { conf.Server.EnableNowPlaying = true }, "enableNowPlaying", true),Entry("gaTrackingId", func() { conf.Server.GATrackingID = "UA-12345" }, "gaTrackingId", "UA-12345"),Entry("defaultDownloadableShare", func() { conf.Server.DefaultDownloadableShare = true }, "defaultDownloadableShare", true),Entry("devSidebarPlaylists", func() { conf.Server.DevSidebarPlaylists = true }, "devSidebarPlaylists", true),Entry("lastFMEnabled", func() { conf.Server.LastFM.Enabled = true }, "lastFMEnabled", true),Entry("devShowArtistPage", func() { conf.Server.DevShowArtistPage = true }, "devShowArtistPage", true),Entry("devUIShowConfig", func() { conf.Server.DevUIShowConfig = true }, "devUIShowConfig", true),Entry("listenBrainzEnabled", func() { conf.Server.ListenBrainz.Enabled = true }, "listenBrainzEnabled", true),Entry("enableReplayGain", func() { conf.Server.EnableReplayGain = true }, "enableReplayGain", true),Entry("enableQuickConnect", func() { conf.Server.Jellyfin.Enabled = true; conf.Server.Jellyfin.QuickConnect = true }, "enableQuickConnect", true),Entry("enableQuickConnect without the Jellyfin API", func() { conf.Server.Jellyfin.Enabled = false; conf.Server.Jellyfin.QuickConnect = true }, "enableQuickConnect", false),Entry("enableExternalServices", func() { conf.Server.EnableExternalServices = true }, "enableExternalServices", true),Entry("devActivityPanel", func() { conf.Server.DevActivityPanel = true }, "devActivityPanel", true),Entry("shareURL", func() { conf.Server.ShareURL = "https://share.example.com" }, "shareURL", "https://share.example.com"),Entry("enableInspect", func() { conf.Server.Inspect.Enabled = true }, "enableInspect", true),Entry("defaultDownsamplingFormat", func() { conf.Server.DefaultDownsamplingFormat = "mp3" }, "defaultDownsamplingFormat", "mp3"),Entry("enableUserEditing", func() { conf.Server.EnableUserEditing = false }, "enableUserEditing", false),Entry("enableSharing", func() { conf.Server.EnableSharing = true }, "enableSharing", true),Entry("devNewEventStream", func() { conf.Server.DevNewEventStream = true }, "devNewEventStream", true),Entry("extAuthLogoutURL", func() { conf.Server.ExtAuth.LogoutURL = "https://auth.example.com/logout" }, "extAuthLogoutURL", "https://auth.example.com/logout"),Entry("playbackReportIntervalMs", func() { conf.Server.UIPlaybackReportInterval = 30 * time.Second }, "playbackReportIntervalMs", float64(30000)),)It("sanitizes entity-encoded welcomeMessage as html", func() {conf.Server.UIWelcomeMessage = `<img src=x onerror=alert(1)><b>Hello</b>`r := httptest.NewRequest("GET", "/index.html", nil)w := httptest.NewRecorder()serveIndex(ds, fs, nil)(w, r)config := extractAppConfig(w.Body.String())Expect(config).To(HaveKey("welcomeMessage"))Expect(config["welcomeMessage"]).To(Equal(`<img src="x"><b>Hello</b>`))})DescribeTable("sets other UI configuration values",func(configKey string, expectedValueFunc func() any) {r := httptest.NewRequest("GET", "/index.html", nil)w := httptest.NewRecorder()serveIndex(ds, fs, nil)(w, r)config := extractAppConfig(w.Body.String())Expect(config).To(HaveKeyWithValue(configKey, expectedValueFunc()))},Entry("version", "version", func() any { return consts.Version }),Entry("variousArtistsId", "variousArtistsId", func() any { return consts.VariousArtistsID }),Entry("losslessFormats", "losslessFormats", func() any {return strings.ToUpper(strings.Join(mime.LosslessFormats, ","))}),Entry("separator", "separator", func() any { return string(os.PathSeparator) }),)Describe("loginBackgroundURL", func() {Context("empty BaseURL", func() {BeforeEach(func() {conf.Server.BasePath = "/"})When("it is the default URL", func() {It("points to the default URL", func() {conf.Server.UILoginBackgroundURL = consts.DefaultUILoginBackgroundURLr := httptest.NewRequest("GET", "/index.html", nil)w := httptest.NewRecorder()serveIndex(ds, fs, nil)(w, r)config := extractAppConfig(w.Body.String())Expect(config).To(HaveKeyWithValue("loginBackgroundURL", consts.DefaultUILoginBackgroundURL))})})When("it is the default offline URL", func() {It("points to the offline URL", func() {conf.Server.UILoginBackgroundURL = consts.DefaultUILoginBackgroundURLOffliner := httptest.NewRequest("GET", "/index.html", nil)w := httptest.NewRecorder()serveIndex(ds, fs, nil)(w, r)config := extractAppConfig(w.Body.String())Expect(config).To(HaveKeyWithValue("loginBackgroundURL", consts.DefaultUILoginBackgroundURLOffline))})})When("it is a custom URL", func() {It("points to the offline URL", func() {conf.Server.UILoginBackgroundURL = "https://example.com/images/1.jpg"r := httptest.NewRequest("GET", "/index.html", nil)w := httptest.NewRecorder()serveIndex(ds, fs, nil)(w, r)config := extractAppConfig(w.Body.String())Expect(config).To(HaveKeyWithValue("loginBackgroundURL", "https://example.com/images/1.jpg"))})})})Context("with a BaseURL", func() {BeforeEach(func() {conf.Server.BasePath = "/music"})When("it is the default URL", func() {It("points to the default URL with BaseURL prefix", func() {conf.Server.UILoginBackgroundURL = consts.DefaultUILoginBackgroundURLr := httptest.NewRequest("GET", "/index.html", nil)w := httptest.NewRecorder()serveIndex(ds, fs, nil)(w, r)config := extractAppConfig(w.Body.String())Expect(config).To(HaveKeyWithValue("loginBackgroundURL", "/music"+consts.DefaultUILoginBackgroundURL))})})When("it is the default offline URL", func() {It("points to the offline URL", func() {conf.Server.UILoginBackgroundURL = consts.DefaultUILoginBackgroundURLOffliner := httptest.NewRequest("GET", "/index.html", nil)w := httptest.NewRecorder()serveIndex(ds, fs, nil)(w, r)config := extractAppConfig(w.Body.String())Expect(config).To(HaveKeyWithValue("loginBackgroundURL", consts.DefaultUILoginBackgroundURLOffline))})})When("it is a custom URL", func() {It("points to the offline URL", func() {conf.Server.UILoginBackgroundURL = "https://example.com/images/1.jpg"r := httptest.NewRequest("GET", "/index.html", nil)w := httptest.NewRecorder()serveIndex(ds, fs, nil)(w, r)config := extractAppConfig(w.Body.String())Expect(config).To(HaveKeyWithValue("loginBackgroundURL", "https://example.com/images/1.jpg"))})})})})})var _ = Describe("addShareData", func() {var (r *http.Requestdata map[string]anyshareInfo *model.Share)BeforeEach(func() {data = make(map[string]any)r = httptest.NewRequest("GET", "/", nil)})Context("when shareInfo is nil or has an empty ID", func() {It("should not modify data", func() {addShareData(r, data, nil)Expect(data).To(BeEmpty())shareInfo = &model.Share{}addShareData(r, data, shareInfo)Expect(data).To(BeEmpty())})})Context("when shareInfo is not nil and has a non-empty ID", func() {BeforeEach(func() {shareInfo = &model.Share{ID: "testID",Description: "Test description",Downloadable: true,Tracks: []model.MediaFile{{ID: "track1",Title: "Track 1",Artist: "Artist 1",Album: "Album 1",Duration: 100,UpdatedAt: time.Date(2023, time.Month(3), 27, 0, 0, 0, 0, time.UTC),},{ID: "track2",Title: "Track 2",Artist: "Artist 2",Album: "Album 2",Duration: 200,UpdatedAt: time.Date(2023, time.Month(3), 26, 0, 0, 0, 0, time.UTC),},},Contents: "Test contents",URL: "https://example.com/share/testID",ImageURL: "https://example.com/share/testID/image",}})It("should populate data with shareInfo data", func() {addShareData(r, data, shareInfo)Expect(data["ShareDescription"]).To(Equal(shareInfo.Description))Expect(data["ShareURL"]).To(Equal(shareInfo.URL))Expect(data["ShareImageURL"]).To(Equal(shareInfo.ImageURL))var shareData shareDataerr := json.Unmarshal([]byte(data["ShareInfo"].(string)), &shareData)Expect(err).NotTo(HaveOccurred())Expect(shareData.ID).To(Equal(shareInfo.ID))Expect(shareData.Description).To(Equal(shareInfo.Description))Expect(shareData.Downloadable).To(Equal(shareInfo.Downloadable))Expect(shareData.Tracks).To(HaveLen(len(shareInfo.Tracks)))for i, track := range shareData.Tracks {Expect(track.ID).To(Equal(shareInfo.Tracks[i].ID))Expect(track.Title).To(Equal(shareInfo.Tracks[i].Title))Expect(track.Artist).To(Equal(shareInfo.Tracks[i].Artist))Expect(track.Album).To(Equal(shareInfo.Tracks[i].Album))Expect(track.Duration).To(Equal(shareInfo.Tracks[i].Duration))Expect(track.UpdatedAt).To(Equal(shareInfo.Tracks[i].UpdatedAt))}})Context("when shareInfo has an empty description", func() {BeforeEach(func() {shareInfo.Description = ""})It("should use shareInfo.Contents as ShareDescription", func() {addShareData(r, data, shareInfo)Expect(data["ShareDescription"]).To(Equal(shareInfo.Contents))})})})})var appConfigRegex = regexp.MustCompile(`(?m)window.__APP_CONFIG__=(.*);</script>`)func extractAppConfig(body string) map[string]any {config := make(map[string]any)match := appConfigRegex.FindStringSubmatch(body)if match == nil {return config}str, err := strconv.Unquote(match[1])if err != nil {panic(fmt.Sprintf("%s: %s", match[1], err))}if err := json.Unmarshal([]byte(str), &config); err != nil {panic(err)}return config}type mockedUserRepo struct {model.UserRepositoryempty bool}func (u *mockedUserRepo) CountAll(context.Context, ...model.QueryOptions) (int64, error) {if u.empty {return 0, nil}return 1, nil}