diff --git a/pkg/service/handlers/handlers_bmx_tunein.go b/pkg/service/handlers/handlers_bmx_tunein.go index 4fd0e5c1..bc084d70 100644 --- a/pkg/service/handlers/handlers_bmx_tunein.go +++ b/pkg/service/handlers/handlers_bmx_tunein.go @@ -9,11 +9,11 @@ import ( "encoding/json" "log" "net/http" - "strconv" "strings" "github.com/gesellix/bose-soundtouch/pkg/service/bmx" "github.com/gesellix/bose-soundtouch/pkg/service/datastore" + "github.com/gesellix/bose-soundtouch/pkg/service/stations" "github.com/go-chi/chi/v5" ) @@ -236,7 +236,7 @@ func (s *Server) HandleTuneInNavigate(w http.ResponseWriter, r *http.Request) { wildcard := chi.URLParam(r, "*") - resp, err := parseTuneInNavigatePath(wildcard) + resp, err := stations.Navigate(stations.ProviderTuneIn, wildcard) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError) return @@ -249,57 +249,6 @@ func (s *Server) HandleTuneInNavigate(w http.ResponseWriter, r *http.Request) { } } -func parseTuneInNavigatePath(wildcard string) (interface{}, error) { - if wildcard == "" { - return bmx.TuneInNavigate("", nil) - } - - firstSlash := strings.Index(wildcard, "/") - if firstSlash == -1 { - return bmx.TuneInNavigate(wildcard, nil) - } - - prefix := wildcard[:firstSlash] - rest := wildcard[firstSlash+1:] - - switch prefix { - case "sub": - secondSlash := strings.Index(rest, "/") - if secondSlash == -1 { - return bmx.TuneInNavigate(rest, nil) - } - - n, err := strconv.Atoi(rest[:secondSlash]) - if err != nil { - return bmx.TuneInNavigate(wildcard, nil) - } - - return bmx.TuneInNavigate(rest[secondSlash+1:], &n) - - case "profiles": - // Hrefs are generated as a single path segment: - // /v1/navigate/profiles/{encodedURI} (see tuneInSearchProfile in - // pkg/service/bmx/tunein.go). Requiring a profiles/{type}/{id}/{encodedURI} - // shape here caused every profile link to fall through to - // bmx.TuneInNavigate with the literal "profiles/..." prefix still - // attached, which is not valid base64 and produced a 500 ("illegal - // base64 data..."). Take the last path segment as the encoded URI so - // both the current single-segment hrefs and any legacy - // multi-segment ones decode correctly. - parts := strings.Split(rest, "/") - - encodedURI := parts[len(parts)-1] - if encodedURI == "" { - return bmx.TuneInNavigate(wildcard, nil) - } - - return bmx.TuneInNavigateProfile(encodedURI) - - default: - return bmx.TuneInNavigate(wildcard, nil) - } -} - // HandleTuneInSearch returns live TuneIn search results for the given query. func (s *Server) HandleTuneInSearch(w http.ResponseWriter, r *http.Request) { // Authorization gate temporarily disabled (was: 401 if header missing). diff --git a/pkg/service/stations/stations_test.go b/pkg/service/stations/stations_test.go index 8d6c2606..87be95bd 100644 --- a/pkg/service/stations/stations_test.go +++ b/pkg/service/stations/stations_test.go @@ -1,6 +1,8 @@ package stations import ( + "encoding/base64" + "strings" "testing" ) @@ -176,3 +178,50 @@ func TestNavigate_UnknownProvider(t *testing.T) { t.Error("expected error for unknown provider") } } + +// TestNavigateTuneIn_ProfilesPathDispatch covers navigateTuneIn's "profiles" +// case, the only TuneIn dispatch this package previously had zero coverage +// for (this is also the sole implementation now: handlers_bmx_tunein.go's +// HandleTuneInNavigate delegates here instead of carrying its own, +// previously byte-for-byte identical, copy of this parsing logic). +// +// Each case encodes the same fake, deliberately-unreachable host so a +// "URL host not in allowed list: " error proves +// the encoded URI was extracted and decoded correctly, independent of any +// real network access. +func TestNavigateTuneIn_ProfilesPathDispatch(t *testing.T) { + const fakeURL = "https://not-a-real-tunein-host.invalid/profiles/p123" + + encoded := base64.RawURLEncoding.EncodeToString([]byte(fakeURL)) + + cases := []struct { + name string + wildcard string + wantErrHas string + }{ + { + name: "single-segment href (current shape)", + wildcard: "profiles/" + encoded, + wantErrHas: fakeURL, + }, + { + name: "legacy multi-segment href extracts the last segment", + wildcard: "profiles/type/id/" + encoded, + wantErrHas: fakeURL, + }, + { + name: "empty encoded URI falls back instead of erroring on empty input", + wildcard: "profiles/", + wantErrHas: "illegal base64 data", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + _, err := navigateTuneIn(tc.wildcard) + if err == nil || !strings.Contains(err.Error(), tc.wantErrHas) { + t.Fatalf("navigateTuneIn(%q) error = %v, want containing %q", tc.wildcard, err, tc.wantErrHas) + } + }) + } +}