mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-19 00:56:16 +00:00
- Replace serial number-based directory structure with deviceId from device /info
- Extract migration logic to handle transition from old to new directory structure
- Fix directory resolution bug that prevented proper migration to deviceId-based paths
- Ensure all device data (Presets.xml, Sources.xml, Recents.xml) preserved during transition
- Add configurable migration with --migration-enabled and --migration-dry-run flags
- Update DeviceInfo.xml to reflect authoritative deviceId from device's /info endpoint
- Directory structure now: /devices/{deviceId}/ instead of /devices/{serialNumber}/
This aligns the directory structure with the device's self-declared identity
and ensures data consistency with the device's /info endpoint.
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 := "A81B6A536A98"
|
|
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.168.1.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.168.1.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{
|
|
"A81B6A536A98", // No separators
|
|
"A8:1B:6A:53:6A:98", // Colons
|
|
"A8-1B-6A-53-6A-98", // Dashes
|
|
"a81b6a536a98", // Lowercase
|
|
"a8:1b:6a:53:6a:98", // 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
|
|
}{
|
|
{"A81B6A536A98", true, "12-char hex"},
|
|
{"a81b6a536a98", true, "12-char hex lowercase"},
|
|
{"A8:1B:6A:53:6A:98", true, "colon-separated"},
|
|
{"A8-1B-6A-53-6A-98", true, "dash-separated"},
|
|
{"a8:1b:6a:53:6a:98", true, "colon-separated lowercase"},
|
|
{"a8-1b-6a-53-6a-98", true, "dash-separated lowercase"},
|
|
{"I6332527703739342000020", false, "device serial"},
|
|
{"192.168.1.100", false, "IP address"},
|
|
{"ABCDEFGHIJKL", false, "12-char non-hex"},
|
|
{"A8:1B:6A:53:6A", false, "incomplete MAC"},
|
|
{"A8:1B:6A:53:6A:98: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 := "A8:1B:6A:53:6A:98"
|
|
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)
|
|
}
|