refactor(tunein): route HandleTuneInNavigate through stations.Navigate

handlers_bmx_tunein.go's parseTuneInNavigatePath and stations.go's
navigateTuneIn were byte-for-byte identical (confirmed both already
independently implemented the same "profiles" parsing before PR #677;
that fix was applied consistently to both copies rather than
introducing new duplication). Removed the duplicate: HandleTuneInNavigate
now delegates to stations.Navigate(stations.ProviderTuneIn, wildcard),
the same path stations' own callers already use.

Added TestNavigateTuneIn_ProfilesPathDispatch, covering the single-
segment (current), legacy multi-segment, and empty-URI shapes -- this
package (and the handler package's now-removed copy) had zero test
coverage for this logic before.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Tobias Gesellchen
2026-09-05 14:24:10 +02:00
co-authored by Claude Sonnet 5
parent c56fc6189c
commit 7b2c10507f
2 changed files with 51 additions and 53 deletions
+2 -53
View File
@@ -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).
+49
View File
@@ -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: <the encoded URL, verbatim>" 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)
}
})
}
}