mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-24 03:26:15 +00:00
Mirrors the .md/.txt sweep across all tracked _test.go, testdata XML,
and .http integration files. Test files are self-contained (producer
+ assertion in the same file), so the matched-pair swap stays green
under `go test ./...`.
Mapping applied:
192.168.178.[0-9]+ → 192.0.2.[same]
192.168.1.[0-9]+ → 192.0.2.[same]
Sound Machinechen → Living Room SoundTouch
A Sound Machine → Kitchen SoundTouch
A81B6A536A98 + case/separator variants → AABBCCDDEEFF (etc.)
A81B6A849D99 → AABBCCDDEE01
A81B6A849D88 → AABBCCDDEE03
A81B6A536A09 → AABBCCDDEE04
884AEAEEBD27 → AABBCCDDEE02
3230304 → 1000001
9569497 → 1000002
Two semantic fixes alongside the bulk swap:
- pkg/service/zeroconf/zeroconf_test.go: the "private 192" and
"strips query" cases pin acceptance of RFC-1918 192.168/16. They
must use a real 192.168 value; doc-range IPs would (correctly) be
rejected by validateZcBaseURL. Switched to 192.168.10.10 — generic
enough not to match any home LAN default, real enough for the
validator. Added a comment explaining why this single test still
carries a 192.168 literal.
- pkg/service/setup/setup_test.go: TestTestDNSRedirection mocks the
device's `od -An -tu1` byte output, which is space-separated
octets ("192 168 1 100"). My sed only matched the dot-separated
form, so the mock was returning the old IP while the test
assertions had moved to the doc range. Updated to " 192 0 2 100".
go build ./... clean. go test ./... clean (only TestDocsConsistency
remains failing, which is a pre-existing/untracked-file issue).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
160 lines
5.3 KiB
Go
160 lines
5.3 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/go-chi/chi/v5"
|
|
)
|
|
|
|
func TestPrintRoutes(t *testing.T) {
|
|
// Initialize a minimal server to get the router
|
|
server := handlers.NewServer(nil, nil, "http://localhost:8000", true, true, true)
|
|
r := setupRouter(server, nil)
|
|
|
|
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)
|
|
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)
|
|
}
|
|
}
|