mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-18 16:46:17 +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>
438 lines
12 KiB
Go
438 lines
12 KiB
Go
package models
|
|
|
|
import (
|
|
"encoding/xml"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestName_UnmarshalXML(t *testing.T) {
|
|
xmlData := `<?xml version="1.0" encoding="UTF-8" ?><name>Living Room SoundTouch</name>`
|
|
|
|
var name Name
|
|
|
|
err := xml.Unmarshal([]byte(xmlData), &name)
|
|
if err != nil {
|
|
t.Fatalf("Failed to unmarshal XML: %v", err)
|
|
}
|
|
|
|
if name.Value != "Living Room SoundTouch" {
|
|
t.Errorf("Expected name 'Living Room SoundTouch', got '%s'", name.Value)
|
|
}
|
|
|
|
if name.GetName() != "Living Room SoundTouch" {
|
|
t.Errorf("Expected GetName() 'Living Room SoundTouch', got '%s'", name.GetName())
|
|
}
|
|
|
|
if name.String() != "Living Room SoundTouch" {
|
|
t.Errorf("Expected String() 'Living Room SoundTouch', got '%s'", name.String())
|
|
}
|
|
|
|
if name.IsEmpty() {
|
|
t.Error("Expected IsEmpty() to return false for non-empty name")
|
|
}
|
|
}
|
|
|
|
func TestName_EmptyName(t *testing.T) {
|
|
xmlData := `<?xml version="1.0" encoding="UTF-8" ?><name></name>`
|
|
|
|
var name Name
|
|
|
|
err := xml.Unmarshal([]byte(xmlData), &name)
|
|
if err != nil {
|
|
t.Fatalf("Failed to unmarshal XML: %v", err)
|
|
}
|
|
|
|
if !name.IsEmpty() {
|
|
t.Error("Expected IsEmpty() to return true for empty name")
|
|
}
|
|
|
|
if name.GetName() != "" {
|
|
t.Errorf("Expected GetName() to return empty string, got '%s'", name.GetName())
|
|
}
|
|
}
|
|
|
|
func TestCapabilities_UnmarshalXML(t *testing.T) {
|
|
xmlData := `<?xml version="1.0" encoding="UTF-8" ?>
|
|
<capabilities deviceID="AABBCCDDEEFF">
|
|
<networkConfig>
|
|
<dualMode>true</dualMode>
|
|
<wsapiproxy>true</wsapiproxy>
|
|
<allInterfacesSupported />
|
|
<wlanInterfaces />
|
|
<security />
|
|
</networkConfig>
|
|
<dspCapabilities>
|
|
<dspMonoStereo available="false" />
|
|
</dspCapabilities>
|
|
<lightswitch>false</lightswitch>
|
|
<clockDisplay>false</clockDisplay>
|
|
<capability name="systemtimeout" url="/systemtimeout" info="" />
|
|
<capability name="rebroadcastlatencymode" url="/rebroadcastlatencymode" info="" />
|
|
<lrStereoCapable>true</lrStereoCapable>
|
|
<bcoresetCapable>false</bcoresetCapable>
|
|
<disablePowerSaving>true</disablePowerSaving>
|
|
</capabilities>`
|
|
|
|
var capabilities Capabilities
|
|
|
|
err := xml.Unmarshal([]byte(xmlData), &capabilities)
|
|
if err != nil {
|
|
t.Fatalf("Failed to unmarshal XML: %v", err)
|
|
}
|
|
|
|
// Test basic fields
|
|
if capabilities.DeviceID != "AABBCCDDEEFF" {
|
|
t.Errorf("Expected DeviceID 'AABBCCDDEEFF', got '%s'", capabilities.DeviceID)
|
|
}
|
|
|
|
// Test boolean capabilities
|
|
if capabilities.HasLightswitch() {
|
|
t.Error("Expected HasLightswitch() to return false")
|
|
}
|
|
|
|
if capabilities.HasClockDisplay() {
|
|
t.Error("Expected HasClockDisplay() to return false")
|
|
}
|
|
|
|
if !capabilities.HasLRStereoCapability() {
|
|
t.Error("Expected HasLRStereoCapability() to return true")
|
|
}
|
|
|
|
if capabilities.HasBCOResetCapability() {
|
|
t.Error("Expected HasBCOResetCapability() to return false")
|
|
}
|
|
|
|
if !capabilities.HasPowerSavingDisabled() {
|
|
t.Error("Expected HasPowerSavingDisabled() to return true")
|
|
}
|
|
|
|
// Test network capabilities
|
|
if !capabilities.HasDualModeNetwork() {
|
|
t.Error("Expected HasDualModeNetwork() to return true")
|
|
}
|
|
|
|
if !capabilities.HasWSAPIProxy() {
|
|
t.Error("Expected HasWSAPIProxy() to return true")
|
|
}
|
|
|
|
// Test DSP capabilities
|
|
if capabilities.HasDSPMonoStereo() {
|
|
t.Error("Expected HasDSPMonoStereo() to return false")
|
|
}
|
|
|
|
// Test capability by name
|
|
if !capabilities.HasCapability("systemtimeout") {
|
|
t.Error("Expected to have systemtimeout capability")
|
|
}
|
|
|
|
if capabilities.HasCapability("nonexistent") {
|
|
t.Error("Expected to not have nonexistent capability")
|
|
}
|
|
|
|
// Test capability names
|
|
capNames := capabilities.GetCapabilityNames()
|
|
if len(capNames) != 2 {
|
|
t.Errorf("Expected 2 capability names, got %d", len(capNames))
|
|
}
|
|
|
|
// Test summaries
|
|
networkCaps := capabilities.GetNetworkCapabilities()
|
|
|
|
expectedNetworkCaps := []string{"Dual Mode", "WSAPI Proxy"}
|
|
if len(networkCaps) != len(expectedNetworkCaps) {
|
|
t.Errorf("Expected %d network capabilities, got %d", len(expectedNetworkCaps), len(networkCaps))
|
|
}
|
|
|
|
audioCaps := capabilities.GetAudioCapabilities()
|
|
|
|
expectedAudioCaps := []string{"L/R Stereo"}
|
|
if len(audioCaps) != len(expectedAudioCaps) {
|
|
t.Errorf("Expected %d audio capabilities, got %d", len(expectedAudioCaps), len(audioCaps))
|
|
}
|
|
|
|
systemCaps := capabilities.GetSystemCapabilities()
|
|
|
|
expectedSystemCaps := []string{"Power Saving Disabled"}
|
|
if len(systemCaps) != len(expectedSystemCaps) {
|
|
t.Errorf("Expected %d system capabilities, got %d", len(expectedSystemCaps), len(systemCaps))
|
|
}
|
|
}
|
|
|
|
func TestCapabilities_HostedWifiConfig(t *testing.T) {
|
|
xmlData := `<?xml version="1.0" encoding="UTF-8" ?>
|
|
<capabilities deviceID="1234567890AB">
|
|
<networkConfig>
|
|
<hostedWifiConfigWebPage hostedBy="BCO" generation="1" port="80">true</hostedWifiConfigWebPage>
|
|
<wsapiproxy>false</wsapiproxy>
|
|
<allInterfacesSupported />
|
|
<wlanInterfaces />
|
|
<security />
|
|
</networkConfig>
|
|
<lightswitch>true</lightswitch>
|
|
<clockDisplay>true</clockDisplay>
|
|
</capabilities>`
|
|
|
|
var capabilities Capabilities
|
|
|
|
err := xml.Unmarshal([]byte(xmlData), &capabilities)
|
|
if err != nil {
|
|
t.Fatalf("Failed to unmarshal XML: %v", err)
|
|
}
|
|
|
|
if !capabilities.HasHostedWifiConfig() {
|
|
t.Error("Expected HasHostedWifiConfig() to return true")
|
|
}
|
|
|
|
if capabilities.GetHostedWifiPort() != "80" {
|
|
t.Errorf("Expected hosted wifi port '80', got '%s'", capabilities.GetHostedWifiPort())
|
|
}
|
|
|
|
if capabilities.GetHostedWifiHostedBy() != "BCO" {
|
|
t.Errorf("Expected hosted wifi hosted by 'BCO', got '%s'", capabilities.GetHostedWifiHostedBy())
|
|
}
|
|
|
|
if !capabilities.HasLightswitch() {
|
|
t.Error("Expected HasLightswitch() to return true")
|
|
}
|
|
|
|
if !capabilities.HasClockDisplay() {
|
|
t.Error("Expected HasClockDisplay() to return true")
|
|
}
|
|
}
|
|
|
|
func TestPresets_UnmarshalXML(t *testing.T) {
|
|
xmlData := `<?xml version="1.0" encoding="UTF-8" ?>
|
|
<presets>
|
|
<preset id="1" createdOn="1719128436" updatedOn="1728740382">
|
|
<ContentItem source="SPOTIFY" type="tracklisturl" location="/playback/container/abc123" sourceAccount="user@example.com" isPresetable="true">
|
|
<itemName>My Playlist</itemName>
|
|
<containerArt>https://i.scdn.co/image/ab67616d00001e025ff75c5d082fc50a3a74ad7b</containerArt>
|
|
</ContentItem>
|
|
</preset>
|
|
<preset id="2" createdOn="1703353552" updatedOn="1743615710">
|
|
<ContentItem source="SPOTIFY" type="tracklisturl" location="/playback/container/def456" sourceAccount="user@example.com" isPresetable="true">
|
|
<itemName>Chill Music Collection</itemName>
|
|
<containerArt>https://i.scdn.co/image/ab67616d0000b273e07c8adc6fb49168dc8b7a2f</containerArt>
|
|
</ContentItem>
|
|
</preset>
|
|
<preset id="3">
|
|
<ContentItem source="TUNEIN" type="stationurl" location="http://stream.example.com" isPresetable="true">
|
|
<itemName>Radio Station</itemName>
|
|
</ContentItem>
|
|
</preset>
|
|
</presets>`
|
|
|
|
var presets Presets
|
|
|
|
err := xml.Unmarshal([]byte(xmlData), &presets)
|
|
if err != nil {
|
|
t.Fatalf("Failed to unmarshal XML: %v", err)
|
|
}
|
|
|
|
// Test basic structure
|
|
if presets.GetPresetCount() != 3 {
|
|
t.Errorf("Expected 3 presets, got %d", presets.GetPresetCount())
|
|
}
|
|
|
|
// Test first preset
|
|
preset1 := presets.GetPresetByID(1)
|
|
if preset1 == nil {
|
|
t.Fatal("Expected to find preset with ID 1")
|
|
}
|
|
|
|
if preset1.GetDisplayName() != "My Playlist" {
|
|
t.Errorf("Expected display name 'My Playlist', got '%s'", preset1.GetDisplayName())
|
|
}
|
|
|
|
if !preset1.IsSpotifyPreset() {
|
|
t.Error("Expected preset 1 to be a Spotify preset")
|
|
}
|
|
|
|
if preset1.GetSource() != "SPOTIFY" {
|
|
t.Errorf("Expected source 'SPOTIFY', got '%s'", preset1.GetSource())
|
|
}
|
|
|
|
if preset1.GetSourceAccount() != "user@example.com" {
|
|
t.Errorf("Expected source account 'user@example.com', got '%s'", preset1.GetSourceAccount())
|
|
}
|
|
|
|
if !preset1.HasTimestamps() {
|
|
t.Error("Expected preset 1 to have timestamps")
|
|
}
|
|
|
|
// Test timestamps
|
|
expectedCreated := time.Unix(1719128436, 0)
|
|
if !preset1.GetCreatedTime().Equal(expectedCreated) {
|
|
t.Errorf("Expected created time %v, got %v", expectedCreated, preset1.GetCreatedTime())
|
|
}
|
|
|
|
expectedUpdated := time.Unix(1728740382, 0)
|
|
if !preset1.GetUpdatedTime().Equal(expectedUpdated) {
|
|
t.Errorf("Expected updated time %v, got %v", expectedUpdated, preset1.GetUpdatedTime())
|
|
}
|
|
|
|
// Test third preset (TuneIn radio)
|
|
preset3 := presets.GetPresetByID(3)
|
|
if preset3 == nil {
|
|
t.Fatal("Expected to find preset with ID 3")
|
|
}
|
|
|
|
if preset3.IsSpotifyPreset() {
|
|
t.Error("Expected preset 3 to not be a Spotify preset")
|
|
}
|
|
|
|
if preset3.GetSource() != "TUNEIN" {
|
|
t.Errorf("Expected source 'TUNEIN', got '%s'", preset3.GetSource())
|
|
}
|
|
|
|
if preset3.HasTimestamps() {
|
|
t.Error("Expected preset 3 to not have timestamps")
|
|
}
|
|
|
|
// Test filtering methods
|
|
spotifyPresets := presets.GetSpotifyPresets()
|
|
if len(spotifyPresets) != 2 {
|
|
t.Errorf("Expected 2 Spotify presets, got %d", len(spotifyPresets))
|
|
}
|
|
|
|
tuneinPresets := presets.GetPresetsBySource("TUNEIN")
|
|
if len(tuneinPresets) != 1 {
|
|
t.Errorf("Expected 1 TuneIn preset, got %d", len(tuneinPresets))
|
|
}
|
|
|
|
// Test empty slots
|
|
emptySlots := presets.GetEmptyPresetSlots()
|
|
|
|
expectedEmpty := []int{4, 5, 6}
|
|
if len(emptySlots) != len(expectedEmpty) {
|
|
t.Errorf("Expected %d empty slots, got %d", len(expectedEmpty), len(emptySlots))
|
|
}
|
|
|
|
// Test used slots
|
|
usedSlots := presets.GetUsedPresetSlots()
|
|
|
|
expectedUsed := []int{1, 2, 3}
|
|
if len(usedSlots) != len(expectedUsed) {
|
|
t.Errorf("Expected %d used slots, got %d", len(expectedUsed), len(usedSlots))
|
|
}
|
|
|
|
// Test summary
|
|
summary := presets.GetPresetsSummary()
|
|
if summary["total"] != 3 {
|
|
t.Errorf("Expected total 3, got %d", summary["total"])
|
|
}
|
|
|
|
if summary["used"] != 3 {
|
|
t.Errorf("Expected used 3, got %d", summary["used"])
|
|
}
|
|
|
|
if summary["spotify"] != 2 {
|
|
t.Errorf("Expected spotify 2, got %d", summary["spotify"])
|
|
}
|
|
|
|
if summary["SPOTIFY"] != 2 {
|
|
t.Errorf("Expected SPOTIFY 2, got %d", summary["SPOTIFY"])
|
|
}
|
|
|
|
if summary["TUNEIN"] != 1 {
|
|
t.Errorf("Expected TUNEIN 1, got %d", summary["TUNEIN"])
|
|
}
|
|
|
|
// Test most recent preset
|
|
mostRecent := presets.GetMostRecentPreset()
|
|
if mostRecent == nil {
|
|
t.Fatal("Expected to find most recent preset")
|
|
}
|
|
|
|
if mostRecent.ID != 2 {
|
|
t.Errorf("Expected most recent preset ID 2, got %d", mostRecent.ID)
|
|
}
|
|
|
|
// Test oldest preset
|
|
oldest := presets.GetOldestPreset()
|
|
if oldest == nil {
|
|
t.Fatal("Expected to find oldest preset")
|
|
}
|
|
|
|
if oldest.ID != 2 {
|
|
t.Errorf("Expected oldest preset ID 2, got %d", oldest.ID)
|
|
}
|
|
}
|
|
|
|
func TestPresets_EmptyPresets(t *testing.T) {
|
|
xmlData := `<?xml version="1.0" encoding="UTF-8" ?><presets></presets>`
|
|
|
|
var presets Presets
|
|
|
|
err := xml.Unmarshal([]byte(xmlData), &presets)
|
|
if err != nil {
|
|
t.Fatalf("Failed to unmarshal XML: %v", err)
|
|
}
|
|
|
|
if presets.HasPresets() {
|
|
t.Error("Expected HasPresets() to return false for empty presets")
|
|
}
|
|
|
|
if presets.GetPresetCount() != 0 {
|
|
t.Errorf("Expected preset count 0, got %d", presets.GetPresetCount())
|
|
}
|
|
|
|
emptySlots := presets.GetEmptyPresetSlots()
|
|
|
|
expectedSlots := []int{1, 2, 3, 4, 5, 6}
|
|
if len(emptySlots) != len(expectedSlots) {
|
|
t.Errorf("Expected %d empty slots, got %d", len(expectedSlots), len(emptySlots))
|
|
}
|
|
|
|
if presets.GetMostRecentPreset() != nil {
|
|
t.Error("Expected GetMostRecentPreset() to return nil for empty presets")
|
|
}
|
|
|
|
if presets.GetOldestPreset() != nil {
|
|
t.Error("Expected GetOldestPreset() to return nil for empty presets")
|
|
}
|
|
}
|
|
|
|
func TestPreset_EdgeCases(t *testing.T) {
|
|
// Test preset without ContentItem
|
|
emptyPreset := Preset{ID: 1}
|
|
|
|
if !emptyPreset.IsEmpty() {
|
|
t.Error("Expected IsEmpty() to return true for preset without ContentItem")
|
|
}
|
|
|
|
if emptyPreset.GetDisplayName() != "Preset 1" {
|
|
t.Errorf("Expected display name 'Preset 1', got '%s'", emptyPreset.GetDisplayName())
|
|
}
|
|
|
|
if emptyPreset.GetSource() != "" {
|
|
t.Errorf("Expected empty source, got '%s'", emptyPreset.GetSource())
|
|
}
|
|
|
|
if emptyPreset.GetArtworkURL() != "" {
|
|
t.Errorf("Expected empty artwork URL, got '%s'", emptyPreset.GetArtworkURL())
|
|
}
|
|
|
|
if emptyPreset.IsSpotifyPreset() {
|
|
t.Error("Expected IsSpotifyPreset() to return false for empty preset")
|
|
}
|
|
|
|
// Test preset with ContentItem but no artwork
|
|
presetNoArt := Preset{
|
|
ID: 2,
|
|
ContentItem: &ContentItem{
|
|
Source: "TUNEIN",
|
|
ItemName: "Test Station",
|
|
},
|
|
}
|
|
|
|
if presetNoArt.GetArtworkURL() != "" {
|
|
t.Errorf("Expected empty artwork URL, got '%s'", presetNoArt.GetArtworkURL())
|
|
}
|
|
|
|
if presetNoArt.GetDisplayName() != "Test Station" {
|
|
t.Errorf("Expected display name 'Test Station', got '%s'", presetNoArt.GetDisplayName())
|
|
}
|
|
}
|