Files
Tobias GesellchenandClaude Opus 4.7 feadc478d5 test: sweep example data in test files to RFC-5737 + placeholders
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>
2026-05-17 22:05:13 +02:00

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)")
}
})
}