mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-18 08:36:13 +00:00
The web player is intrinsically LAN-resident: it reaches speakers directly and only delegates cloud-only features (e.g. TTS) to a possibly-remote AfterTouch service via --service-url. That is exactly what a cloud-hosted soundtouch-service cannot do, so the standalone player binary stays useful and is not being deprecated. Rename it to state its purpose, with a transition window so existing downloads keep working. - cmd/soundtouch-web -> cmd/soundtouch-player; CLI name is now soundtouch-player. When the binary is invoked under its old name it prints a one-line rename notice (filepath.Base(os.Args[0])). - Build/release both names from the same source: Makefile (build-player + build-web alias, dev-player* targets), Dockerfile (soundtouch-player image + transitional soundtouch-web image), release.yml and ci.yml (player + web artifacts, checksums, Docker images; release notes announce the rename). The soundtouch-web binary, image, and install script remain a transitional alias to be dropped in a future release (which will break stale fetch scripts and nudge users to the release notes). - scripts/raspberry-pi/install-player.sh is canonical; install-web.sh keeps working but warns. - Sweep docs, code comments, user-facing strings, and assets (soundtouch-web-ui.png, soundtouch-web-tunein.png, soundtouch-web-roadmap.md) to soundtouch-player; README documents the rename and why the player remains separate from the embedded /app. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
163 lines
5.6 KiB
Go
163 lines
5.6 KiB
Go
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"reflect"
|
|
"runtime"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/gesellix/bose-soundtouch/pkg/service/datastore"
|
|
"github.com/gesellix/bose-soundtouch/pkg/service/handlers"
|
|
"github.com/gesellix/bose-soundtouch/pkg/service/soundtouchweb"
|
|
"github.com/go-chi/chi/v5"
|
|
)
|
|
|
|
func TestPrintRoutes(t *testing.T) {
|
|
// Initialize a minimal server to get the router. Pass a web app so the
|
|
// snapshot also captures the embedded soundtouch-player surface
|
|
// (/api/control + /app); discovery is nil since we only register routes.
|
|
server := handlers.NewServer(nil, nil, "http://localhost:8000", true, true, true)
|
|
r := setupRouter(server, nil, soundtouchweb.NewWebApp())
|
|
|
|
var routes []string
|
|
walkFunc := func(method string, route string, handler http.Handler, middlewares ...func(http.Handler) http.Handler) error {
|
|
route = strings.ReplaceAll(route, "/*/", "/")
|
|
handlerName := runtime.FuncForPC(reflect.ValueOf(handler).Pointer()).Name()
|
|
// Clean up the handler name (remove package path)
|
|
// For example, "github.com/gesellix/bose-soundtouch/cmd/soundtouch-service.setupRouter.func1"
|
|
// or "command-line-arguments.setupRouter.func1"
|
|
// or "main.setupRouter.func1"
|
|
parts := strings.Split(handlerName, "/")
|
|
if len(parts) > 0 {
|
|
handlerName = parts[len(parts)-1]
|
|
}
|
|
// Now we might have "soundtouch-service.setupRouter.func1"
|
|
// or "command-line-arguments.setupRouter.func1"
|
|
// or "main.setupRouter.func1"
|
|
// Let's remove the first part if it's a known varying package name
|
|
if idx := strings.Index(handlerName, "setupRouter"); idx != -1 {
|
|
handlerName = handlerName[idx:]
|
|
}
|
|
// In case it's not setupRouter but still has a package prefix
|
|
for {
|
|
dotIdx := strings.Index(handlerName, ".")
|
|
if dotIdx == -1 {
|
|
break
|
|
}
|
|
prefix := handlerName[:dotIdx]
|
|
if prefix == "main" || prefix == "command-line-arguments" || strings.Contains(prefix, "soundtouch-service") {
|
|
handlerName = handlerName[dotIdx+1:]
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
|
|
// Also remove any ".funcN" suffix if it's an anonymous function
|
|
if idx := strings.Index(handlerName, ".func"); idx != -1 {
|
|
handlerName = handlerName[:idx]
|
|
}
|
|
|
|
routes = append(routes, fmt.Sprintf("%-8s %-60s %s", method, route, handlerName))
|
|
return nil
|
|
}
|
|
|
|
if err := chi.Walk(r, walkFunc); err != nil {
|
|
t.Fatalf("Failed to walk routes: %v", err)
|
|
}
|
|
|
|
sort.Strings(routes)
|
|
|
|
output := strings.Join(routes, "\n") + "\n"
|
|
|
|
// Define snapshot path
|
|
snapshotPath := "testdata/router_routes.txt"
|
|
actualPath := "testdata/router_routes.actual.txt"
|
|
|
|
// Always write the current (actual) routes to a file
|
|
if err := os.WriteFile(actualPath, []byte(output), 0644); err != nil {
|
|
t.Fatalf("Failed to write actual routes: %v", err)
|
|
}
|
|
|
|
// Check if snapshot exists
|
|
if _, err := os.Stat(snapshotPath); os.IsNotExist(err) {
|
|
// Create testdata directory if it doesn't exist
|
|
if err := os.MkdirAll("testdata", 0755); err != nil {
|
|
t.Fatalf("Failed to create testdata directory: %v", err)
|
|
}
|
|
// Initial snapshot creation
|
|
if err := os.WriteFile(snapshotPath, []byte(output), 0644); err != nil {
|
|
t.Fatalf("Failed to write snapshot: %v", err)
|
|
}
|
|
t.Logf("Initial snapshot created at %s", snapshotPath)
|
|
return
|
|
}
|
|
|
|
// Read existing snapshot
|
|
existingOutput, err := os.ReadFile(snapshotPath)
|
|
if err != nil {
|
|
t.Fatalf("Failed to read snapshot: %v", err)
|
|
}
|
|
|
|
if string(existingOutput) != output {
|
|
t.Errorf("Router routes changed! Diff the snapshot at %s with %s", snapshotPath, actualPath)
|
|
}
|
|
}
|
|
|
|
// TestPUTRenameRoutesToLocalHandler reproduces the runtime routing
|
|
// behaviour the user saw on their deployed v0.80.0: a PUT to
|
|
// /streaming/account/{a}/device/{d} should land on
|
|
// HandleMargeUpdateDevice, not fall through to the [UNHANDLED]
|
|
// proxy. The handlers-package test (TestIssue285_*) uses a simplified
|
|
// router that doesn't have the overlapping `/device` and
|
|
// `/device/{device}` route groups, so it can't catch a chi radix-
|
|
// tree resolution that prefers the more-specific subrouter.
|
|
//
|
|
// This test exercises the actual production setupRouter so a
|
|
// regression in the route topology is caught against the same chi
|
|
// behaviour speakers will see.
|
|
func TestPUTRenameRoutesToLocalHandler(t *testing.T) {
|
|
tempDir, err := os.MkdirTemp("", "router-rename-")
|
|
if err != nil {
|
|
t.Fatalf("mkdir temp: %v", err)
|
|
}
|
|
defer os.RemoveAll(tempDir)
|
|
|
|
ds := datastore.NewDataStore(tempDir)
|
|
_ = ds.Initialize()
|
|
|
|
server := handlers.NewServer(ds, nil, "http://localhost:8000", false, false, false)
|
|
r := setupRouter(server, nil, nil)
|
|
ts := httptest.NewServer(r)
|
|
defer ts.Close()
|
|
|
|
body := `<?xml version="1.0" encoding="UTF-8" ?><device deviceid="AABBCCDDEEFF"><name>Living Room SoundTouch</name><macaddress>AABBCCDDEEFF</macaddress></device>`
|
|
|
|
req, err := http.NewRequest(http.MethodPut,
|
|
ts.URL+"/streaming/account/1111111/device/AABBCCDDEEFF",
|
|
strings.NewReader(body))
|
|
if err != nil {
|
|
t.Fatalf("build request: %v", err)
|
|
}
|
|
|
|
req.Header.Set("Content-Type", "application/xml")
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatalf("PUT: %v", err)
|
|
}
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
// 200 means our local HandleMargeUpdateDevice handled it.
|
|
// 401 / 502 / anything else means the request fell through to
|
|
// the [UNHANDLED] proxy and got the upstream response — which
|
|
// is exactly the failure mode #285 was supposed to fix.
|
|
if resp.StatusCode != http.StatusOK {
|
|
t.Fatalf("PUT status = %d, want 200 (local handler). Anything else means the request fell through to [UNHANDLED] proxy — chi is routing to a different subrouter than the PUT registration intended.", resp.StatusCode)
|
|
}
|
|
}
|