Files
Bose-SoundTouch/pkg/service/setup/factory_reset_test.go
T
Tobias GesellchenandClaude Opus 4.7 29a462da2b feat(setup): add CLI setup command group for end-to-end speaker provisioning
Add `soundtouch-cli setup` subcommand group covering the full reset →
re-provision → pair lifecycle as a scriptable alternative to the web UI:

  inspect, verify, plan, factory-reset, wait-ap, wifi-push, wait-online,
  ssh-check, install-ca, migrate, reboot, pair (bare | full state machine)

Supporting library code lives in pkg/service/setup: factory_reset.go,
wifi_provision.go, inspect.go, init_plan.go, setup_session.go.

Confirmed against ST10 firmware 27.0.6 that bare setMargeAccount over
WebSocket — no SETUP_START/SETUP_ENTER/SETUP_LEAVE bracket — is
sufficient to pair a factory-reset speaker; the firmware materializes
SystemConfigurationDB.xml and Sources.xml itself and the pairing
survives reboot. Result and field-by-field SystemConfigurationDB
comparison documented in docs/analysis/SETUP-WEBSOCKET-EXPERIMENT.md.
Captures the device's pre-reset DELETE-to-marge plus its LAN peer
notification flow in docs/analysis/FACTORY-RESET-PROTOCOL.md.

Perf: batch GetMigrationSummary's SSH probes into one Run() call via
ssh_probe.go / ssh_probe_apply.go — was ~8 sequential dials at
500-1000 ms each on FW 27 crypto, now one round-trip. Same data shape,
same MigrationSummary fields populated.

Fixes /clockTime and /clockDisplay wire formats — firmware 27 rejects
the legacy flat XML ("Error parsing request"). ClockTimeRequest now
uses utcTime attribute; ClockDisplayRequest emits the nested
<clockConfig> envelope with timezoneInfo/timeFormat/brightnessLevel.

Removes cmd/example-init-speaker (superseded by setup pair).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 18:43:36 +02:00

77 lines
2.1 KiB
Go

package setup
import (
"errors"
"strings"
"testing"
)
func TestFactoryReset_HappyPath(t *testing.T) {
f := &fakeTelnet{
banner: "BoseDebug>",
responses: map[string]string{"sys factorydefault": "Rebooting...\n"},
}
m := newFakeTelnetManager(f)
logs, err := m.FactoryReset("192.0.2.10")
if err != nil {
t.Fatalf("FactoryReset: %v", err)
}
if len(f.commands) != 1 || f.commands[0] != "sys factorydefault" {
t.Errorf("commands = %v, want [sys factorydefault]", f.commands)
}
if !strings.Contains(logs, "Rebooting") {
t.Errorf("logs missing reboot output: %s", logs)
}
}
func TestFactoryReset_DisconnectIsAcceptedAsSuccess(t *testing.T) {
// Some firmwares drop the socket as soon as the reset starts, before
// they finish writing a response. That's not a failure.
f := &fakeTelnet{
fail: map[string]error{"sys factorydefault": errors.New("read EOF")},
}
m := newFakeTelnetManager(f)
logs, err := m.FactoryReset("192.0.2.10")
if err != nil {
t.Fatalf("disconnect during reset should be treated as success, got: %v", err)
}
if !strings.Contains(logs, "device disconnected") {
t.Errorf("logs should mention the expected disconnect, got: %s", logs)
}
}
func TestFactoryReset_RejectsFirmwareWithoutCommand(t *testing.T) {
// Default fakeTelnet response is "Command not found\n" for unmapped commands.
f := &fakeTelnet{}
m := newFakeTelnetManager(f)
_, err := m.FactoryReset("192.0.2.10")
if err == nil || !strings.Contains(err.Error(), "firmware does not expose") {
t.Errorf("err = %v, want firmware-rejection error", err)
}
}
func TestFactoryReset_NoTelnetClient(t *testing.T) {
m := &Manager{} // NewTelnet nil
_, err := m.FactoryReset("192.0.2.10")
if err == nil || !strings.Contains(err.Error(), "NewTelnet") {
t.Errorf("err = %v, want NewTelnet-nil error", err)
}
}
func TestFactoryReset_DialFailurePropagates(t *testing.T) {
f := &fakeTelnet{dialErr: errors.New("connection refused")}
m := newFakeTelnetManager(f)
_, err := m.FactoryReset("192.0.2.10")
if err == nil || !strings.Contains(err.Error(), "connection refused") {
t.Errorf("err = %v, want dial error", err)
}
}