mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-19 00:56:16 +00:00
Adds XMLMigrated, HostsMigrated, ResolvMigrated, TelnetMigrated, and IsPaired as explicit fields on the summary so the UI can render partial-state cells (URLs flipped via telnet but the on-disk XML hasn't caught up; DNS interception in place but no CA installed; etc.) and surface pairing as its own precondition. IsMigrated remains backward-compatible — it is now the OR of the four migration axes. checkIsMigrated stops short-circuiting and writes each axis verdict unconditionally so a "partial" state on any axis is always visible to the UI even when another axis already reports the device migrated. populateDeviceInfo now derives IsPaired from the live :8090/info margeAccountUUID (clobbering any stale datastore copy), so a factory-reset speaker is correctly flagged as unpaired. Tests cover the per-axis verdicts independently and the IsPaired derivation in both the populated and empty live-info cases. This is the data layer for the upcoming three-axis "state view" panel on the migration tab. No frontend or behavior changes here. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
187 lines
5.5 KiB
Go
187 lines
5.5 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()) {
|
|
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>3230304</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 != "3230304" {
|
|
t.Errorf("AccountID = %q, want 3230304 (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)
|
|
}
|
|
}
|