Files
Bose-SoundTouch/pkg/service/datastore/mac_first_resolution_test.go
T
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

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