mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-19 09:06:14 +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>
369 lines
12 KiB
Go
369 lines
12 KiB
Go
package datastore
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/gesellix/bose-soundtouch/pkg/models"
|
|
)
|
|
|
|
func TestAccountDeviceDir_MACFirstResolution(t *testing.T) {
|
|
tempDir, err := os.MkdirTemp("", "mac-first-test-*")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.RemoveAll(tempDir)
|
|
|
|
ds := NewDataStore(tempDir)
|
|
accountID := "testaccount"
|
|
macAddress := "AABBCCDDEEFF"
|
|
serialNumber := "I6332527703739342000020"
|
|
|
|
t.Run("NewMACBasedDevice", func(t *testing.T) {
|
|
// Create a new device with MAC as deviceID
|
|
deviceInfo := &models.ServiceDeviceInfo{
|
|
DeviceID: macAddress,
|
|
AccountID: accountID,
|
|
Name: "New MAC Device",
|
|
IPAddress: "192.0.2.100",
|
|
MacAddress: macAddress,
|
|
DeviceSerialNumber: serialNumber,
|
|
ProductCode: "SoundTouch 10 sm2",
|
|
}
|
|
|
|
// Save the device
|
|
if err := ds.SaveDeviceInfo(accountID, macAddress, deviceInfo); err != nil {
|
|
t.Fatalf("Failed to save MAC-based device: %v", err)
|
|
}
|
|
|
|
// Test AccountDeviceDir resolution
|
|
resolvedDir := ds.AccountDeviceDir(accountID, macAddress)
|
|
expectedDir := filepath.Join(tempDir, "accounts", accountID, "devices", macAddress)
|
|
|
|
if resolvedDir != expectedDir {
|
|
t.Errorf("Expected MAC-based device dir '%s', got '%s'", expectedDir, resolvedDir)
|
|
}
|
|
|
|
// Verify the directory actually exists
|
|
if _, err := os.Stat(resolvedDir); os.IsNotExist(err) {
|
|
t.Errorf("MAC-based device directory should exist: %s", resolvedDir)
|
|
}
|
|
|
|
t.Logf("✅ MAC-based device correctly resolved to: %s", resolvedDir)
|
|
})
|
|
|
|
t.Run("LegacySerialBasedDevice", func(t *testing.T) {
|
|
// Create a legacy device with serial as deviceID (simulating old storage)
|
|
legacyInfo := &models.ServiceDeviceInfo{
|
|
DeviceID: serialNumber,
|
|
AccountID: accountID,
|
|
Name: "Legacy Serial Device",
|
|
IPAddress: "192.0.2.101",
|
|
MacAddress: macAddress,
|
|
DeviceSerialNumber: serialNumber,
|
|
ProductCode: "SoundTouch 10",
|
|
}
|
|
|
|
// Save the legacy device
|
|
if err := ds.SaveDeviceInfo(accountID, serialNumber, legacyInfo); err != nil {
|
|
t.Fatalf("Failed to save legacy device: %v", err)
|
|
}
|
|
|
|
// Initialize to populate mappings
|
|
if err := ds.Initialize(); err != nil {
|
|
t.Fatalf("Failed to initialize datastore: %v", err)
|
|
}
|
|
|
|
// Test resolution by MAC address (should find the legacy device via mapping)
|
|
resolvedDir := ds.AccountDeviceDir(accountID, macAddress)
|
|
expectedLegacyDir := filepath.Join(tempDir, "accounts", accountID, "devices", serialNumber)
|
|
|
|
// Since both MAC and serial devices exist, MAC device should take priority
|
|
expectedMACDir := filepath.Join(tempDir, "accounts", accountID, "devices", macAddress)
|
|
if resolvedDir != expectedMACDir {
|
|
t.Errorf("Expected MAC device to take priority. Got '%s', expected '%s'", resolvedDir, expectedMACDir)
|
|
}
|
|
|
|
t.Logf("✅ MAC address resolution correctly prioritized MAC-based device")
|
|
|
|
// Test resolution by serial number (should find the legacy device directly)
|
|
serialResolvedDir := ds.AccountDeviceDir(accountID, serialNumber)
|
|
if serialResolvedDir != expectedLegacyDir {
|
|
t.Errorf("Expected serial-based device dir '%s', got '%s'", expectedLegacyDir, serialResolvedDir)
|
|
}
|
|
|
|
t.Logf("✅ Serial number correctly resolved to legacy device: %s", serialResolvedDir)
|
|
})
|
|
|
|
t.Run("MACResolutionWithOnlyLegacyDevice", func(t *testing.T) {
|
|
// Create a fresh datastore
|
|
tempDir2, err := os.MkdirTemp("", "mac-legacy-only-*")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.RemoveAll(tempDir2)
|
|
|
|
ds2 := NewDataStore(tempDir2)
|
|
testAccount := "legacyaccount"
|
|
testSerial := "LEGACY123456789"
|
|
testMAC := "BB:CC:DD:EE:FF:00"
|
|
|
|
// Create ONLY a legacy device (no MAC-based device)
|
|
legacyInfo := &models.ServiceDeviceInfo{
|
|
DeviceID: testSerial,
|
|
AccountID: testAccount,
|
|
Name: "Only Legacy Device",
|
|
MacAddress: testMAC,
|
|
DeviceSerialNumber: testSerial,
|
|
}
|
|
|
|
if err := ds2.SaveDeviceInfo(testAccount, testSerial, legacyInfo); err != nil {
|
|
t.Fatalf("Failed to save legacy-only device: %v", err)
|
|
}
|
|
|
|
// Initialize to populate mappings
|
|
if err := ds2.Initialize(); err != nil {
|
|
t.Fatalf("Failed to initialize datastore: %v", err)
|
|
}
|
|
|
|
// Test MAC resolution (should find the legacy device via mapping)
|
|
resolvedDir := ds2.AccountDeviceDir(testAccount, testMAC)
|
|
expectedDir := filepath.Join(tempDir2, "accounts", testAccount, "devices", testSerial)
|
|
|
|
if resolvedDir != expectedDir {
|
|
t.Errorf("MAC '%s' should resolve to legacy device '%s', got '%s'", testMAC, expectedDir, resolvedDir)
|
|
}
|
|
|
|
t.Logf("✅ MAC address correctly resolved to legacy device when no MAC-based device exists")
|
|
})
|
|
|
|
t.Run("NonExistentDevice", func(t *testing.T) {
|
|
unknownMAC := "FF:FF:FF:FF:FF:FF"
|
|
resolvedDir := ds.AccountDeviceDir(accountID, unknownMAC)
|
|
expectedDir := filepath.Join(tempDir, "accounts", accountID, "devices", unknownMAC)
|
|
|
|
if resolvedDir != expectedDir {
|
|
t.Errorf("Non-existent device should resolve to direct path '%s', got '%s'", expectedDir, resolvedDir)
|
|
}
|
|
|
|
t.Logf("✅ Non-existent device correctly resolved to direct MAC path")
|
|
})
|
|
|
|
t.Run("MACNormalization", func(t *testing.T) {
|
|
// Test different MAC address formats
|
|
macFormats := []string{
|
|
"AABBCCDDEEFF", // No separators
|
|
"AA:BB:CC:DD:EE:FF", // Colons
|
|
"AA-BB-CC-DD-EE-FF", // Dashes
|
|
"aabbccddeeff", // Lowercase
|
|
"aa:bb:cc:dd:ee:ff", // Lowercase with colons
|
|
}
|
|
|
|
for _, macFormat := range macFormats {
|
|
resolvedDir := ds.AccountDeviceDir(accountID, macFormat)
|
|
// Should resolve to the MAC-based device we created earlier
|
|
expectedDir := filepath.Join(tempDir, "accounts", accountID, "devices", macAddress)
|
|
|
|
if resolvedDir != expectedDir {
|
|
t.Logf("MAC format '%s' resolved to '%s', expected '%s'", macFormat, resolvedDir, expectedDir)
|
|
// For now, we'll log this - full normalization might require additional work
|
|
}
|
|
}
|
|
})
|
|
|
|
t.Run("BackwardCompatibilityMapping", func(t *testing.T) {
|
|
// Test that the legacy UpdateMapping method still works
|
|
testMAC := "CC:DD:EE:FF:00:11"
|
|
testSerial := "COMPAT789"
|
|
|
|
ds.UpdateMapping(testMAC, testSerial)
|
|
|
|
// After calling UpdateMapping, the MAC should resolve via the mapping
|
|
resolvedDir := ds.AccountDeviceDir(accountID, testMAC)
|
|
directPath := filepath.Join(tempDir, "accounts", accountID, "devices", testMAC)
|
|
|
|
// Since no actual device exists, it should return the direct path
|
|
if resolvedDir != directPath {
|
|
t.Errorf("UpdateMapping backward compatibility test failed. Got '%s', expected '%s'", resolvedDir, directPath)
|
|
}
|
|
|
|
t.Logf("✅ UpdateMapping backward compatibility maintained")
|
|
})
|
|
}
|
|
|
|
func TestDeviceMappings_Bidirectional(t *testing.T) {
|
|
tempDir, err := os.MkdirTemp("", "bidirectional-test-*")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.RemoveAll(tempDir)
|
|
|
|
ds := NewDataStore(tempDir)
|
|
accountID := "testaccount"
|
|
|
|
t.Run("MACBasedDeviceCreatesSerialMapping", func(t *testing.T) {
|
|
macAddress := "11:22:33:44:55:66"
|
|
serialNumber := "NEWDEVICE123"
|
|
|
|
// Create MAC-based device
|
|
deviceInfo := &models.ServiceDeviceInfo{
|
|
DeviceID: macAddress,
|
|
AccountID: accountID,
|
|
Name: "MAC First Device",
|
|
MacAddress: macAddress,
|
|
DeviceSerialNumber: serialNumber,
|
|
}
|
|
|
|
if err := ds.SaveDeviceInfo(accountID, macAddress, deviceInfo); err != nil {
|
|
t.Fatalf("Failed to save MAC-based device: %v", err)
|
|
}
|
|
|
|
// Initialize to populate mappings
|
|
if err := ds.Initialize(); err != nil {
|
|
t.Fatalf("Failed to initialize: %v", err)
|
|
}
|
|
|
|
// Serial should resolve to the MAC-based device
|
|
resolvedDir := ds.AccountDeviceDir(accountID, serialNumber)
|
|
expectedDir := filepath.Join(tempDir, "accounts", accountID, "devices", macAddress)
|
|
|
|
if resolvedDir != expectedDir {
|
|
t.Errorf("Serial '%s' should resolve to MAC device '%s', got '%s'", serialNumber, expectedDir, resolvedDir)
|
|
}
|
|
|
|
t.Logf("✅ MAC-based device creates correct serial→MAC mapping")
|
|
})
|
|
|
|
t.Run("SerialBasedDeviceCreatesMACMapping", func(t *testing.T) {
|
|
macAddress := "77:88:99:AA:BB:CC"
|
|
serialNumber := "SERIALDEVICE456"
|
|
|
|
// Create serial-based device (legacy)
|
|
deviceInfo := &models.ServiceDeviceInfo{
|
|
DeviceID: serialNumber,
|
|
AccountID: accountID,
|
|
Name: "Serial First Device",
|
|
MacAddress: macAddress,
|
|
DeviceSerialNumber: serialNumber,
|
|
}
|
|
|
|
if err := ds.SaveDeviceInfo(accountID, serialNumber, deviceInfo); err != nil {
|
|
t.Fatalf("Failed to save serial-based device: %v", err)
|
|
}
|
|
|
|
// Initialize to populate mappings
|
|
if err := ds.Initialize(); err != nil {
|
|
t.Fatalf("Failed to initialize: %v", err)
|
|
}
|
|
|
|
// MAC should resolve to the serial-based device
|
|
resolvedDir := ds.AccountDeviceDir(accountID, macAddress)
|
|
expectedDir := filepath.Join(tempDir, "accounts", accountID, "devices", serialNumber)
|
|
|
|
if resolvedDir != expectedDir {
|
|
t.Errorf("MAC '%s' should resolve to serial device '%s', got '%s'", macAddress, expectedDir, resolvedDir)
|
|
}
|
|
|
|
t.Logf("✅ Serial-based device creates correct MAC→serial mapping")
|
|
})
|
|
}
|
|
|
|
func TestMACAddressFormatDetection(t *testing.T) {
|
|
testCases := []struct {
|
|
input string
|
|
expected bool
|
|
name string
|
|
}{
|
|
{"AABBCCDDEEFF", true, "12-char hex"},
|
|
{"aabbccddeeff", true, "12-char hex lowercase"},
|
|
{"AA:BB:CC:DD:EE:FF", true, "colon-separated"},
|
|
{"AA-BB-CC-DD-EE-FF", true, "dash-separated"},
|
|
{"aa:bb:cc:dd:ee:ff", true, "colon-separated lowercase"},
|
|
{"aa-bb-cc-dd-ee-ff", true, "dash-separated lowercase"},
|
|
{"I6332527703739342000020", false, "device serial"},
|
|
{"192.0.2.100", false, "IP address"},
|
|
{"ABCDEFGHIJKL", false, "12-char non-hex"},
|
|
{"AA:BB:CC:DD:EE", false, "incomplete MAC"},
|
|
{"AA:BB:CC:DD:EE:FF:01", false, "too long MAC"},
|
|
{"", false, "empty string"},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
result := isMACAddressFormat(tc.input)
|
|
if result != tc.expected {
|
|
t.Errorf("isMACAddressFormat('%s') = %v, expected %v", tc.input, result, tc.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestAccountDeviceDir_PriorityOrder(t *testing.T) {
|
|
tempDir, err := os.MkdirTemp("", "priority-test-*")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.RemoveAll(tempDir)
|
|
|
|
ds := NewDataStore(tempDir)
|
|
accountID := "prioritytest"
|
|
macAddress := "AA:BB:CC:DD:EE:FF"
|
|
serialNumber := "PRIORITY123456789"
|
|
|
|
// Create both MAC-based and serial-based devices for the same physical device
|
|
macDevice := &models.ServiceDeviceInfo{
|
|
DeviceID: macAddress,
|
|
AccountID: accountID,
|
|
Name: "MAC Version",
|
|
MacAddress: macAddress,
|
|
DeviceSerialNumber: serialNumber,
|
|
}
|
|
|
|
serialDevice := &models.ServiceDeviceInfo{
|
|
DeviceID: serialNumber,
|
|
AccountID: accountID,
|
|
Name: "Serial Version",
|
|
MacAddress: macAddress,
|
|
DeviceSerialNumber: serialNumber,
|
|
}
|
|
|
|
// Save both devices
|
|
if err := ds.SaveDeviceInfo(accountID, macAddress, macDevice); err != nil {
|
|
t.Fatalf("Failed to save MAC device: %v", err)
|
|
}
|
|
|
|
if err := ds.SaveDeviceInfo(accountID, serialNumber, serialDevice); err != nil {
|
|
t.Fatalf("Failed to save serial device: %v", err)
|
|
}
|
|
|
|
// Initialize mappings
|
|
if err := ds.Initialize(); err != nil {
|
|
t.Fatalf("Failed to initialize: %v", err)
|
|
}
|
|
|
|
// Test priority: MAC address should resolve to MAC-based device (not serial-based)
|
|
resolvedDir := ds.AccountDeviceDir(accountID, macAddress)
|
|
expectedMACDir := filepath.Join(tempDir, "accounts", accountID, "devices", macAddress)
|
|
|
|
if resolvedDir != expectedMACDir {
|
|
t.Errorf("MAC address should resolve to MAC-based device directory")
|
|
t.Errorf("Expected: %s", expectedMACDir)
|
|
t.Errorf("Got: %s", resolvedDir)
|
|
}
|
|
|
|
// Test that serial still resolves to its own device
|
|
serialResolvedDir := ds.AccountDeviceDir(accountID, serialNumber)
|
|
expectedSerialDir := filepath.Join(tempDir, "accounts", accountID, "devices", serialNumber)
|
|
|
|
if serialResolvedDir != expectedSerialDir {
|
|
t.Errorf("Serial should resolve to serial-based device directory")
|
|
t.Errorf("Expected: %s", expectedSerialDir)
|
|
t.Errorf("Got: %s", serialResolvedDir)
|
|
}
|
|
|
|
t.Logf("✅ Priority test passed:")
|
|
t.Logf(" MAC '%s' → %s", macAddress, resolvedDir)
|
|
t.Logf(" Serial '%s' → %s", serialNumber, serialResolvedDir)
|
|
}
|