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>
320 lines
11 KiB
Go
320 lines
11 KiB
Go
package discovery
|
|
|
|
import (
|
|
"fmt"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gesellix/bose-soundtouch/pkg/models"
|
|
)
|
|
|
|
func TestUPnP_EnrichDeviceInfo_RealDeviceXML(t *testing.T) {
|
|
// This tests the exact UPnP XML format provided by the user
|
|
realDeviceXML := `<?xml version="1.0" encoding="utf-8"?>
|
|
<root xmlns="urn:schemas-upnp-org:device-1-0">
|
|
<specVersion>
|
|
<major>1</major>
|
|
<minor>0</minor>
|
|
</specVersion>
|
|
<device>
|
|
<deviceType>urn:schemas-upnp-org:device:MediaRenderer:1</deviceType>
|
|
<friendlyName>Living Room SoundTouch</friendlyName>
|
|
<qq:X_QPlay_SoftwareCapability xmlns:qq="http://www.tencent.com">QPlay:2</qq:X_QPlay_SoftwareCapability>
|
|
<manufacturer>Bose Corporation</manufacturer>
|
|
<manufacturerURL>http://www.bose.com</manufacturerURL>
|
|
<modelName>SoundTouch 10</modelName>
|
|
<modelNumber></modelNumber>
|
|
<modelDescription>Bose SoundTouch Wireless Streaming Audio Device</modelDescription>
|
|
<modelURL>http://www.bose.com</modelURL>
|
|
<serialNumber>AABBCCDDEEFF</serialNumber>
|
|
<UDN>uuid:BO5EBO5E-F00D-F00D-FEED-AABBCCDDEEFF</UDN>
|
|
<serviceList>
|
|
<service>
|
|
<serviceType>urn:schemas-upnp-org:service:AVTransport:1</serviceType>
|
|
<serviceId>urn:upnp-org:serviceId:AVTransport</serviceId>
|
|
<SCPDURL>/Xml/AVTransport3.xml</SCPDURL>
|
|
<controlURL>/AVTransport/Control</controlURL>
|
|
<eventSubURL>/AVTransport/Event</eventSubURL>
|
|
</service>
|
|
<service>
|
|
<serviceType>urn:schemas-upnp-org:service:ConnectionManager:1</serviceType>
|
|
<serviceId>urn:upnp-org:serviceId:ConnectionManager</serviceId>
|
|
<SCPDURL>/Xml/ConnectionManager3.xml</SCPDURL>
|
|
<controlURL>/ConnectionManager/Control</controlURL>
|
|
<eventSubURL>/ConnectionManager/Event</eventSubURL>
|
|
</service>
|
|
<service>
|
|
<serviceType>urn:schemas-upnp-org:service:RenderingControl:1</serviceType>
|
|
<serviceId>urn:upnp-org:serviceId:RenderingControl</serviceId>
|
|
<SCPDURL>/Xml/RenderingControl3.xml</SCPDURL>
|
|
<controlURL>/RenderingControl/Control</controlURL>
|
|
<eventSubURL>/RenderingControl/Event</eventSubURL>
|
|
</service>
|
|
<service>
|
|
<serviceType>urn:schemas-tencent-com:service:QPlay:2</serviceType>
|
|
<serviceId>urn:tencent-com:serviceId:QPlay</serviceId>
|
|
<controlURL>/QPlay/Control</controlURL>
|
|
<eventSubURL>/QPlay/Event</eventSubURL>
|
|
<SCPDURL>/Xml/QPlay.xml</SCPDURL>
|
|
</service>
|
|
</serviceList>
|
|
</device>
|
|
</root>`
|
|
|
|
// Create a test server that serves the UPnP XML
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "text/xml; charset=utf-8")
|
|
fmt.Fprint(w, realDeviceXML)
|
|
}))
|
|
defer server.Close()
|
|
|
|
// Create a discovered device to enrich
|
|
device := &models.DiscoveredDevice{
|
|
Host: "192.0.2.100",
|
|
Port: 8091,
|
|
Name: "Initial Device Name",
|
|
}
|
|
|
|
// Create discovery service and enrich the device
|
|
service := NewService(5 * time.Second)
|
|
err := service.EnrichDeviceInfo(device, server.URL)
|
|
|
|
if err != nil {
|
|
t.Fatalf("enrichDeviceInfo failed: %v", err)
|
|
}
|
|
|
|
// Verify that the MAC address was extracted correctly from serialNumber
|
|
expectedMAC := "AABBCCDDEEFF"
|
|
if device.UPnPSerial != expectedMAC {
|
|
t.Errorf("Expected UPnPSerial '%s', got '%s'", expectedMAC, device.UPnPSerial)
|
|
}
|
|
|
|
// Verify other enriched fields
|
|
expectedName := "Living Room SoundTouch"
|
|
if device.Name != expectedName {
|
|
t.Errorf("Expected Name '%s', got '%s'", expectedName, device.Name)
|
|
}
|
|
|
|
expectedModel := "SoundTouch 10"
|
|
if device.ModelID != expectedModel {
|
|
t.Errorf("Expected ModelID '%s', got '%s'", expectedModel, device.ModelID)
|
|
}
|
|
|
|
t.Logf("✓ Successfully extracted MAC address '%s' from UPnP serialNumber field", device.UPnPSerial)
|
|
t.Logf("✓ Device name: '%s'", device.Name)
|
|
t.Logf("✓ Device model: '%s'", device.ModelID)
|
|
}
|
|
|
|
func TestUPnP_MACAddressDiscovery_Integration(t *testing.T) {
|
|
// Test various MAC address formats that might appear in serialNumber
|
|
testCases := []struct {
|
|
name string
|
|
serialNumberInXML string
|
|
expectedUPnPSerial string
|
|
description string
|
|
}{
|
|
{
|
|
name: "StandardMAC",
|
|
serialNumberInXML: "AABBCCDDEEFF",
|
|
expectedUPnPSerial: "AABBCCDDEEFF",
|
|
description: "Standard MAC address format without separators",
|
|
},
|
|
{
|
|
name: "MACWithColons",
|
|
serialNumberInXML: "AA:BB:CC:DD:EE:FF",
|
|
expectedUPnPSerial: "AA:BB:CC:DD:EE:FF",
|
|
description: "MAC address with colon separators",
|
|
},
|
|
{
|
|
name: "MACWithDashes",
|
|
serialNumberInXML: "AA-BB-CC-DD-EE-FF",
|
|
expectedUPnPSerial: "AA-BB-CC-DD-EE-FF",
|
|
description: "MAC address with dash separators",
|
|
},
|
|
{
|
|
name: "LowercaseMAC",
|
|
serialNumberInXML: "aabbccddeeff",
|
|
expectedUPnPSerial: "aabbccddeeff",
|
|
description: "Lowercase MAC address",
|
|
},
|
|
{
|
|
name: "MixedCaseMAC",
|
|
serialNumberInXML: "aaBBccddEEff",
|
|
expectedUPnPSerial: "aaBBccddEEff",
|
|
description: "Mixed case MAC address",
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
// Create UPnP XML with the specific serialNumber format
|
|
xmlTemplate := `<?xml version="1.0" encoding="utf-8"?>
|
|
<root xmlns="urn:schemas-upnp-org:device-1-0">
|
|
<device>
|
|
<deviceType>urn:schemas-upnp-org:device:MediaRenderer:1</deviceType>
|
|
<friendlyName>Test Device</friendlyName>
|
|
<manufacturer>Bose Corporation</manufacturer>
|
|
<modelName>SoundTouch 10</modelName>
|
|
<serialNumber>%s</serialNumber>
|
|
<UDN>uuid:BO5EBO5E-F00D-F00D-FEED-TEST</UDN>
|
|
</device>
|
|
</root>`
|
|
|
|
deviceXML := fmt.Sprintf(xmlTemplate, tc.serialNumberInXML)
|
|
|
|
// Create test server
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "text/xml")
|
|
fmt.Fprint(w, deviceXML)
|
|
}))
|
|
defer server.Close()
|
|
|
|
// Create and enrich device
|
|
device := &models.DiscoveredDevice{
|
|
Host: "192.0.2.100",
|
|
Port: 8090,
|
|
}
|
|
|
|
service := NewService(5 * time.Second)
|
|
err := service.EnrichDeviceInfo(device, server.URL)
|
|
|
|
if err != nil {
|
|
t.Errorf("%s: enrichDeviceInfo failed: %v", tc.description, err)
|
|
return
|
|
}
|
|
|
|
if device.UPnPSerial != tc.expectedUPnPSerial {
|
|
t.Errorf("%s: Expected UPnPSerial '%s', got '%s'",
|
|
tc.description, tc.expectedUPnPSerial, device.UPnPSerial)
|
|
} else {
|
|
t.Logf("✓ %s: Successfully extracted '%s'", tc.description, device.UPnPSerial)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestUPnP_URLPattern_Realistic(t *testing.T) {
|
|
// Test the exact URL pattern mentioned:
|
|
// http://192.0.2.100:8091/XD/BO5EBO5E-F00D-F00D-FEED-AABBCCDDEEFF.xml
|
|
|
|
realDeviceXML := `<?xml version="1.0" encoding="utf-8"?>
|
|
<root xmlns="urn:schemas-upnp-org:device-1-0">
|
|
<device>
|
|
<deviceType>urn:schemas-upnp-org:device:MediaRenderer:1</deviceType>
|
|
<friendlyName>Living Room SoundTouch</friendlyName>
|
|
<manufacturer>Bose Corporation</manufacturer>
|
|
<modelName>SoundTouch 10</modelName>
|
|
<serialNumber>AABBCCDDEEFF</serialNumber>
|
|
<UDN>uuid:BO5EBO5E-F00D-F00D-FEED-AABBCCDDEEFF</UDN>
|
|
</device>
|
|
</root>`
|
|
|
|
// Create server that responds to the specific path
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.URL.Path == "/XD/BO5EBO5E-F00D-F00D-FEED-AABBCCDDEEFF.xml" {
|
|
w.Header().Set("Content-Type", "text/xml")
|
|
fmt.Fprint(w, realDeviceXML)
|
|
} else {
|
|
w.WriteHeader(http.StatusNotFound)
|
|
}
|
|
}))
|
|
defer server.Close()
|
|
|
|
// Test enrichment using the realistic URL path
|
|
device := &models.DiscoveredDevice{
|
|
Host: "192.0.2.100",
|
|
Port: 8091,
|
|
Name: "Initial Name",
|
|
}
|
|
|
|
service := NewService(5 * time.Second)
|
|
locationURL := server.URL + "/XD/BO5EBO5E-F00D-F00D-FEED-AABBCCDDEEFF.xml"
|
|
err := service.EnrichDeviceInfo(device, locationURL)
|
|
|
|
if err != nil {
|
|
t.Fatalf("enrichDeviceInfo failed for realistic URL: %v", err)
|
|
}
|
|
|
|
// Verify MAC address extraction
|
|
expectedMAC := "AABBCCDDEEFF"
|
|
if device.UPnPSerial != expectedMAC {
|
|
t.Errorf("Expected MAC '%s', got '%s'", expectedMAC, device.UPnPSerial)
|
|
}
|
|
|
|
// Note: The MAC address in the URL and in the XML serialNumber should match
|
|
if device.UPnPSerial == expectedMAC {
|
|
t.Logf("✓ MAC address '%s' extracted from UPnP XML matches expected value", device.UPnPSerial)
|
|
t.Logf("✓ This MAC can now be used for datastore mapping")
|
|
t.Logf("✓ Request URL pattern: GET /streaming/account/{account}/device/%s/presets", device.UPnPSerial)
|
|
}
|
|
}
|
|
|
|
func TestUPnP_ErrorHandling(t *testing.T) {
|
|
service := NewService(5 * time.Second)
|
|
device := &models.DiscoveredDevice{
|
|
Host: "192.0.2.100",
|
|
Name: "Test Device",
|
|
}
|
|
|
|
t.Run("InvalidXML", func(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "text/xml")
|
|
fmt.Fprint(w, "invalid xml content")
|
|
}))
|
|
defer server.Close()
|
|
|
|
err := service.EnrichDeviceInfo(device, server.URL)
|
|
if err == nil {
|
|
t.Error("Expected error for invalid XML, got nil")
|
|
} else {
|
|
t.Logf("✓ Correctly handled invalid XML: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("HTTPError", func(t *testing.T) {
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
}))
|
|
defer server.Close()
|
|
|
|
err := service.EnrichDeviceInfo(device, server.URL)
|
|
if err == nil {
|
|
t.Error("Expected error for HTTP 500, got nil")
|
|
} else {
|
|
t.Logf("✓ Correctly handled HTTP error: %v", err)
|
|
}
|
|
})
|
|
|
|
t.Run("MissingSerialNumber", func(t *testing.T) {
|
|
xmlWithoutSerial := `<?xml version="1.0" encoding="utf-8"?>
|
|
<root xmlns="urn:schemas-upnp-org:device-1-0">
|
|
<device>
|
|
<friendlyName>Test Device</friendlyName>
|
|
<modelName>Test Model</modelName>
|
|
</device>
|
|
</root>`
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Content-Type", "text/xml")
|
|
fmt.Fprint(w, xmlWithoutSerial)
|
|
}))
|
|
defer server.Close()
|
|
|
|
deviceCopy := *device // Make a copy to avoid modifying the original
|
|
err := service.EnrichDeviceInfo(&deviceCopy, server.URL)
|
|
|
|
// Should not error, but UPnPSerial should be empty
|
|
if err != nil {
|
|
t.Errorf("Unexpected error for missing serialNumber: %v", err)
|
|
}
|
|
|
|
if deviceCopy.UPnPSerial != "" {
|
|
t.Errorf("Expected empty UPnPSerial, got '%s'", deviceCopy.UPnPSerial)
|
|
} else {
|
|
t.Logf("✓ Correctly handled missing serialNumber (empty UPnPSerial)")
|
|
}
|
|
})
|
|
}
|