mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-19 00:56:16 +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>
241 lines
7.7 KiB
Go
241 lines
7.7 KiB
Go
package setup
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// telnetSummaryEnv builds a Manager whose:
|
|
// - SSH client is the supplied mockSSH (or a no-op if nil).
|
|
// - Telnet client is the supplied fakeTelnet.
|
|
// - Live :8090/info call hits an httptest server returning a minimal XML.
|
|
//
|
|
// The deviceIP returned is the httptest server's listener addr ("host:port"),
|
|
// so the live-info call works; the SSH and telnet clients ignore the addr
|
|
// and return whatever the fakes are scripted to return.
|
|
func telnetSummaryEnv(t *testing.T, ssh *mockSSH, ft *fakeTelnet) (*Manager, string, func()) {
|
|
t.Helper()
|
|
return telnetSummaryEnvWithInfo(t, ssh, ft, `<info deviceID="123"><name>Test</name></info>`)
|
|
}
|
|
|
|
// telnetSummaryEnvWithInfo is telnetSummaryEnv with a caller-supplied
|
|
// :8090/info XML body, so individual tests can exercise device-info
|
|
// fields that affect summary state (e.g. margeAccountUUID for IsPaired).
|
|
func telnetSummaryEnvWithInfo(t *testing.T, ssh *mockSSH, ft *fakeTelnet, infoXML string) (*Manager, string, func()) {
|
|
t.Helper()
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "application/xml")
|
|
_, _ = fmt.Fprint(w, infoXML)
|
|
}))
|
|
|
|
m := NewManager("http://example:8000", nil, nil)
|
|
m.NewSSH = func(string) SSHClient {
|
|
if ssh != nil {
|
|
return ssh
|
|
}
|
|
return &mockSSH{runFunc: func(string) (string, error) { return "", errors.New("ssh disabled in test") }}
|
|
}
|
|
m.NewTelnet = func(string) TelnetClient { return ft }
|
|
|
|
return m, server.Listener.Addr().String(), server.Close
|
|
}
|
|
|
|
func TestGetMigrationSummary_TelnetSucceedsSSHFails(t *testing.T) {
|
|
target := "http://example:8000"
|
|
ft := &fakeTelnet{
|
|
banner: "BoseShell\n-> ",
|
|
responses: map[string]string{
|
|
"getpdo CurrentSystemConfiguration": "margeServerUrl=" + target + "\n",
|
|
},
|
|
}
|
|
|
|
m, host, cleanup := telnetSummaryEnv(t, nil, ft)
|
|
defer cleanup()
|
|
|
|
summary, err := m.GetMigrationSummary(host, "", "", nil)
|
|
if err != nil {
|
|
t.Fatalf("GetMigrationSummary: %v", err)
|
|
}
|
|
|
|
if summary.SSHSuccess {
|
|
t.Errorf("SSHSuccess = true, want false")
|
|
}
|
|
|
|
if !summary.TelnetReachable {
|
|
t.Errorf("TelnetReachable = false, want true")
|
|
}
|
|
|
|
if !strings.Contains(summary.TelnetBanner, "BoseShell") {
|
|
t.Errorf("TelnetBanner = %q, want it to contain BoseShell", summary.TelnetBanner)
|
|
}
|
|
|
|
if !strings.Contains(summary.TelnetVerifiedConfig, target) {
|
|
t.Errorf("TelnetVerifiedConfig = %q, want it to contain %q", summary.TelnetVerifiedConfig, target)
|
|
}
|
|
}
|
|
|
|
func TestGetMigrationSummary_TelnetFailsSSHFails(t *testing.T) {
|
|
ft := &fakeTelnet{dialErr: errors.New("connection refused")}
|
|
|
|
m, host, cleanup := telnetSummaryEnv(t, nil, ft)
|
|
defer cleanup()
|
|
|
|
summary, err := m.GetMigrationSummary(host, "", "", nil)
|
|
if err != nil {
|
|
t.Fatalf("GetMigrationSummary: %v", err)
|
|
}
|
|
|
|
if summary.SSHSuccess {
|
|
t.Errorf("SSHSuccess = true, want false")
|
|
}
|
|
|
|
if summary.TelnetReachable {
|
|
t.Errorf("TelnetReachable = true, want false")
|
|
}
|
|
|
|
if !strings.Contains(summary.TelnetProbeError, "connection refused") {
|
|
t.Errorf("TelnetProbeError = %q, want connection refused", summary.TelnetProbeError)
|
|
}
|
|
}
|
|
|
|
func TestGetMigrationSummary_IsPairedFromLiveInfo(t *testing.T) {
|
|
ft := &fakeTelnet{dialErr: errors.New("not the focus of this test")}
|
|
|
|
t.Run("with margeAccountUUID", func(t *testing.T) {
|
|
m, host, cleanup := telnetSummaryEnvWithInfo(t, nil, ft,
|
|
`<info deviceID="123"><name>Test</name><margeAccountUUID>1000001</margeAccountUUID></info>`,
|
|
)
|
|
defer cleanup()
|
|
|
|
summary, err := m.GetMigrationSummary(host, "", "", nil)
|
|
if err != nil {
|
|
t.Fatalf("GetMigrationSummary: %v", err)
|
|
}
|
|
|
|
if !summary.IsPaired {
|
|
t.Errorf("IsPaired = false, want true (margeAccountUUID present in :8090/info)")
|
|
}
|
|
|
|
if summary.AccountID != "1000001" {
|
|
t.Errorf("AccountID = %q, want 1000001 (live info should populate)", summary.AccountID)
|
|
}
|
|
})
|
|
|
|
t.Run("without margeAccountUUID", func(t *testing.T) {
|
|
m, host, cleanup := telnetSummaryEnvWithInfo(t, nil, ft,
|
|
`<info deviceID="123"><name>Test</name><margeAccountUUID></margeAccountUUID></info>`,
|
|
)
|
|
defer cleanup()
|
|
|
|
summary, err := m.GetMigrationSummary(host, "", "", nil)
|
|
if err != nil {
|
|
t.Fatalf("GetMigrationSummary: %v", err)
|
|
}
|
|
|
|
if summary.IsPaired {
|
|
t.Errorf("IsPaired = true, want false (factory-reset device with empty margeAccountUUID)")
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestGetMigrationSummary_TelnetSucceedsSSHSucceeds(t *testing.T) {
|
|
target := "http://example:8000"
|
|
ft := &fakeTelnet{
|
|
responses: map[string]string{
|
|
"getpdo CurrentSystemConfiguration": "margeServerUrl=" + target + "\n",
|
|
},
|
|
}
|
|
|
|
// SSH mock returns enough for SSHSuccess to be true (cat /opt/Bose/etc/...).
|
|
ssh := &mockSSH{
|
|
runFunc: func(cmd string) (string, error) {
|
|
switch {
|
|
case strings.HasPrefix(cmd, "cat "+SoundTouchSdkPrivateCfgPath):
|
|
return `<?xml version="1.0"?><SoundTouchSdkPrivateCfg><margeServerUrl>` + target + `</margeServerUrl></SoundTouchSdkPrivateCfg>`, nil
|
|
case strings.HasPrefix(cmd, "[ -f"):
|
|
return "", errors.New("not found")
|
|
default:
|
|
return "", nil
|
|
}
|
|
},
|
|
}
|
|
|
|
m, host, cleanup := telnetSummaryEnv(t, ssh, ft)
|
|
defer cleanup()
|
|
|
|
summary, err := m.GetMigrationSummary(host, "", "", nil)
|
|
if err != nil {
|
|
t.Fatalf("GetMigrationSummary: %v", err)
|
|
}
|
|
|
|
if !summary.SSHSuccess {
|
|
t.Errorf("SSHSuccess = false, want true")
|
|
}
|
|
|
|
if !summary.TelnetReachable {
|
|
t.Errorf("TelnetReachable = false, want true")
|
|
}
|
|
|
|
if !strings.Contains(summary.TelnetVerifiedConfig, target) {
|
|
t.Errorf("TelnetVerifiedConfig = %q, want %q", summary.TelnetVerifiedConfig, target)
|
|
}
|
|
}
|
|
|
|
// TestGetMigrationSummary_TelnetOnlyMigrationDetected pins the ordering
|
|
// bug fixed in PR #294 / issue #293.
|
|
//
|
|
// Before the fix, GetMigrationSummary called checkIsMigratedFromProbe
|
|
// before draining the telnet goroutine's result, so
|
|
// summary.TelnetVerifiedConfig was empty when isTelnetMigrated read it
|
|
// — and the telnet axis was always reported false. For speakers
|
|
// migrated *only* via telnet (envswitch flip; no SSH XML rewrite,
|
|
// no DNS hook, no CA install), this misclassification meant
|
|
// summary.IsMigrated was false despite the speaker actually pointing
|
|
// at AfterTouch. The CLI's `setup verify` exited non-zero, and the
|
|
// web UI rendered "Not Migrated".
|
|
//
|
|
// The fix moves m.checkIsMigratedFromProbe(summary, probe) to run
|
|
// *after* the <-telnetCh drain, so TelnetVerifiedConfig is populated
|
|
// when isTelnetMigrated inspects it.
|
|
//
|
|
// The scenario here matches foob61451's 2026-05-16 #293 reproducer:
|
|
// SSH unavailable / disabled (every axis false), telnet getpdo reports
|
|
// the AfterTouch host, no other migration path applied.
|
|
func TestGetMigrationSummary_TelnetOnlyMigrationDetected(t *testing.T) {
|
|
target := "http://example:8000"
|
|
ft := &fakeTelnet{
|
|
banner: "BoseShell\n-> ",
|
|
responses: map[string]string{
|
|
"getpdo CurrentSystemConfiguration": "margeServerUrl=" + target + "\n",
|
|
},
|
|
}
|
|
|
|
m, host, cleanup := telnetSummaryEnv(t, nil, ft)
|
|
defer cleanup()
|
|
|
|
summary, err := m.GetMigrationSummary(host, "", "", nil)
|
|
if err != nil {
|
|
t.Fatalf("GetMigrationSummary: %v", err)
|
|
}
|
|
|
|
// Pre-condition for the test to be meaningful: the telnet probe
|
|
// must have populated TelnetVerifiedConfig. Without this, the
|
|
// downstream assertions could pass trivially.
|
|
if !strings.Contains(summary.TelnetVerifiedConfig, target) {
|
|
t.Fatalf("setup: TelnetVerifiedConfig = %q, want it to contain %q", summary.TelnetVerifiedConfig, target)
|
|
}
|
|
|
|
if !summary.TelnetMigrated {
|
|
t.Errorf("TelnetMigrated = false, want true — telnet getpdo reports %q which matches Manager.ServerURL host. Likely regression of PR #294 ordering fix in GetMigrationSummary.", target)
|
|
}
|
|
|
|
if !summary.IsMigrated {
|
|
t.Errorf("IsMigrated = false, want true — telnet axis should carry IsMigrated when SSH-driven axes are false. Likely regression of PR #294 ordering fix.")
|
|
}
|
|
}
|