Files
Bose-SoundTouch/pkg/service/setup/preflight_crosscheck_test.go
T
Tobias GesellchenandClaude Opus 4.7 ae21552878 fix(setup,web): parse the protobuf-text getpdo reply real devices send
The live SoundTouch firmware (FW 27.0.6.46330.5043500, ST 20) replies
to `getpdo CurrentSystemConfiguration` with a Protobuf-text-like
nested-block format, not the key=value format my parser was written
against:

    margeServerUrl {
      text: "https://streaming.bose.com"
    }
    statsServerUrl {
      text: "https://events.api.bosecm.com"
    }
    ...
    ->OK
    ->

Effect of the bug: the four "Current on Device" cells in the telnet
URL Targets table stayed empty after a summary load, and the
crossCheckPreflights helper silently produced no warnings even when
SSH-XML and telnet-getpdo would have disagreed. Both behaviours were
reported from a real-device summary fetched against the running
service.

Both parsers (Go setup.parseGetpdoConfig and JS
parseTelnetVerifiedConfig) now accept the protobuf-text shape and keep
the legacy key=value path as a tolerance fallback. An isIdentifier
guard prevents protobuf "text: …" lines from being misread as flat
fields and keeps prompt characters (->, ->OK) out of the result map.

A new TestParseGetpdoConfig_ProtobufTextRealDevice test pins the
parser to the verbatim live response so this regression cannot recur
silently.

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

187 lines
5.6 KiB
Go

package setup
import (
"strings"
"testing"
)
func TestParseGetpdoConfig_StandardLines(t *testing.T) {
in := "margeServerUrl=http://example:8000\nbmxRegistryUrl=http://example:8000/bmx/registry/v1/services\n"
got := parseGetpdoConfig(in)
if got["margeServerUrl"] != "http://example:8000" {
t.Errorf("margeServerUrl = %q, want http://example:8000", got["margeServerUrl"])
}
if got["bmxRegistryUrl"] != "http://example:8000/bmx/registry/v1/services" {
t.Errorf("bmxRegistryUrl = %q", got["bmxRegistryUrl"])
}
}
func TestParseGetpdoConfig_TolerantToNoise(t *testing.T) {
in := "BoseShell\n-> getpdo CurrentSystemConfiguration\nmargeServerUrl=http://example:8000\nrandom line without equals\n statsServerUrl = http://example:8000 \n-> "
got := parseGetpdoConfig(in)
if got["margeServerUrl"] != "http://example:8000" {
t.Errorf("margeServerUrl = %q, want http://example:8000", got["margeServerUrl"])
}
if got["statsServerUrl"] != "http://example:8000" {
t.Errorf("statsServerUrl = %q, want trimmed http://example:8000", got["statsServerUrl"])
}
if _, exists := got["random line without equals"]; exists {
t.Errorf("non-key=value line should not be parsed")
}
}
// TestParseGetpdoConfig_ProtobufTextRealDevice pins the parser to the
// live response captured from a SoundTouch 20 (FW 27.0.6.46330.5043500)
// against http://mac.fritz.box:8000/setup/summary. This is the format
// the parser actually has to handle in production — the prior
// key=value-only implementation returned an empty map for this input,
// which surfaced as empty "Current on Device" cells in the migration
// UI.
func TestParseGetpdoConfig_ProtobufTextRealDevice(t *testing.T) {
in := `margeServerUrl {
text: "https://streaming.bose.com"
}
statsServerUrl {
text: "https://events.api.bosecm.com"
}
swUpdateUrl {
text: "https://worldwide.bose.com/updates/soundtouch"
}
isZeroconfEnabled {
text: true
}
usePandoraProductionServer {
text: true
}
saveMargeCustomerReport {
text: false
}
bmxRegistryUrl {
text: "https://content.api.bose.io/bmx/registry/v1/services"
}
->OK
->`
got := parseGetpdoConfig(in)
want := map[string]string{
"margeServerUrl": "https://streaming.bose.com",
"statsServerUrl": "https://events.api.bosecm.com",
"swUpdateUrl": "https://worldwide.bose.com/updates/soundtouch",
"bmxRegistryUrl": "https://content.api.bose.io/bmx/registry/v1/services",
"isZeroconfEnabled": "true",
"usePandoraProductionServer": "true",
"saveMargeCustomerReport": "false",
}
for k, v := range want {
if got[k] != v {
t.Errorf("%s = %q, want %q", k, got[k], v)
}
}
}
func TestCrossCheckPreflights_AgreementProducesNoWarnings(t *testing.T) {
m := &Manager{ServerURL: "http://example:8000"}
summary := &MigrationSummary{
ParsedCurrentConfig: &PrivateCfg{
MargeServerUrl: "http://example:8000",
StatsServerUrl: "http://example:8000",
SwUpdateUrl: "http://example:8000/updates/soundtouch",
BmxRegistryUrl: "http://example:8000/bmx/registry/v1/services",
},
TelnetVerifiedConfig: "margeServerUrl=http://example:8000\n" +
"statsServerUrl=http://example:8000\n" +
"swUpdateUrl=http://example:8000/updates/soundtouch\n" +
"bmxRegistryUrl=http://example:8000/bmx/registry/v1/services\n",
}
m.crossCheckPreflights(summary)
if len(summary.Warnings) != 0 {
t.Errorf("Warnings = %v, want none when both transports agree", summary.Warnings)
}
}
func TestCrossCheckPreflights_MismatchProducesWarning(t *testing.T) {
m := &Manager{ServerURL: "http://example:8000"}
// SSH-XML still shows the original cloud URL (envswitch wrote the
// runtime layer but the on-device file hasn't been re-rendered).
summary := &MigrationSummary{
ParsedCurrentConfig: &PrivateCfg{
MargeServerUrl: "https://streaming.bose.com",
},
TelnetVerifiedConfig: "margeServerUrl=http://example:8000\n",
}
m.crossCheckPreflights(summary)
if len(summary.Warnings) != 1 {
t.Fatalf("Warnings = %v, want exactly one warning", summary.Warnings)
}
w := summary.Warnings[0]
if !strings.Contains(w, "margeServerUrl") {
t.Errorf("warning %q should name the field", w)
}
if !strings.Contains(w, "streaming.bose.com") || !strings.Contains(w, "example:8000") {
t.Errorf("warning %q should quote both values", w)
}
}
func TestCrossCheckPreflights_NoWarningWhenTelnetMissesField(t *testing.T) {
m := &Manager{ServerURL: "http://example:8000"}
summary := &MigrationSummary{
ParsedCurrentConfig: &PrivateCfg{
MargeServerUrl: "http://example:8000",
StatsServerUrl: "http://example:8000",
},
// getpdo only echoes margeServerUrl — statsServerUrl is silently
// absent on this firmware. Absence is not a disagreement.
TelnetVerifiedConfig: "margeServerUrl=http://example:8000\n",
}
m.crossCheckPreflights(summary)
if len(summary.Warnings) != 0 {
t.Errorf("Warnings = %v, want none when a field is missing from one transport", summary.Warnings)
}
}
func TestCrossCheckPreflights_OnlyOneTransportPresent(t *testing.T) {
m := &Manager{ServerURL: "http://example:8000"}
t.Run("telnet only", func(t *testing.T) {
summary := &MigrationSummary{
TelnetVerifiedConfig: "margeServerUrl=http://example:8000\n",
}
m.crossCheckPreflights(summary)
if len(summary.Warnings) != 0 {
t.Errorf("Warnings = %v, want none when SSH didn't read the XML", summary.Warnings)
}
})
t.Run("ssh only", func(t *testing.T) {
summary := &MigrationSummary{
ParsedCurrentConfig: &PrivateCfg{MargeServerUrl: "http://example:8000"},
}
m.crossCheckPreflights(summary)
if len(summary.Warnings) != 0 {
t.Errorf("Warnings = %v, want none when telnet didn't respond", summary.Warnings)
}
})
}