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.
236 lines
6.8 KiB
Go
236 lines
6.8 KiB
Go
package datastore
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/gesellix/bose-soundtouch/pkg/models"
|
|
)
|
|
|
|
func TestMacAddressSerialization(t *testing.T) {
|
|
tempDir, err := os.MkdirTemp("", "mac-serialization-test-*")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.RemoveAll(tempDir)
|
|
|
|
ds := NewDataStore(tempDir)
|
|
account := "3230304"
|
|
device := "I6332527703739342000020"
|
|
macAddress := "A81B6A536A98"
|
|
|
|
// Create device info with MAC address
|
|
info := &models.ServiceDeviceInfo{
|
|
DeviceID: device,
|
|
Name: "Test SoundTouch",
|
|
ProductCode: "SoundTouch 10",
|
|
IPAddress: "192.168.1.100",
|
|
MacAddress: macAddress,
|
|
DeviceSerialNumber: device,
|
|
ProductSerialNumber: "PROD123456",
|
|
FirmwareVersion: "4.8.1.23456",
|
|
DiscoveryMethod: "UPnP",
|
|
}
|
|
|
|
// Save device info
|
|
err = ds.SaveDeviceInfo(account, device, info)
|
|
if err != nil {
|
|
t.Fatalf("SaveDeviceInfo failed: %v", err)
|
|
}
|
|
|
|
// Verify the XML file was created
|
|
deviceInfoPath := filepath.Join(ds.AccountDeviceDir(account, device), "DeviceInfo.xml")
|
|
if _, err := os.Stat(deviceInfoPath); err != nil {
|
|
t.Fatalf("DeviceInfo.xml not created: %v", err)
|
|
}
|
|
|
|
// Read back the device info
|
|
loadedInfo, err := ds.GetDeviceInfo(account, device)
|
|
if err != nil {
|
|
t.Fatalf("GetDeviceInfo failed: %v", err)
|
|
}
|
|
|
|
// Verify MAC address is preserved
|
|
if loadedInfo.MacAddress != macAddress {
|
|
t.Errorf("MAC address not preserved. Expected: '%s', Got: '%s'", macAddress, loadedInfo.MacAddress)
|
|
}
|
|
|
|
// Verify other fields are also correct
|
|
if loadedInfo.DeviceID != device {
|
|
t.Errorf("DeviceID mismatch. Expected: %s, Got: %s", device, loadedInfo.DeviceID)
|
|
}
|
|
|
|
if loadedInfo.IPAddress != "192.168.1.100" {
|
|
t.Errorf("IPAddress mismatch. Expected: 192.168.1.100, Got: %s", loadedInfo.IPAddress)
|
|
}
|
|
|
|
// Initialize datastore to populate MAC mappings
|
|
err = ds.Initialize()
|
|
if err != nil {
|
|
t.Fatalf("Initialize failed: %v", err)
|
|
}
|
|
|
|
// Test that MAC address mapping works
|
|
resolvedPath := ds.AccountDeviceDir(account, macAddress)
|
|
expectedPath := ds.AccountDeviceDir(account, device)
|
|
|
|
if resolvedPath != expectedPath {
|
|
t.Errorf("MAC address mapping failed. MAC '%s' resolved to '%s', expected '%s'",
|
|
macAddress, resolvedPath, expectedPath)
|
|
}
|
|
|
|
// Test that Sources.xml path resolves correctly via MAC address
|
|
// (We don't need to actually read the file, just verify the path resolution works)
|
|
macPath := ds.AccountDeviceDir(account, macAddress)
|
|
devicePath := ds.AccountDeviceDir(account, device)
|
|
|
|
if macPath != devicePath {
|
|
t.Errorf("MAC address path resolution failed. MAC path: %s, Device path: %s", macPath, devicePath)
|
|
}
|
|
|
|
t.Logf("✅ MAC address serialization working correctly")
|
|
t.Logf(" - MAC address '%s' saved to DeviceInfo.xml", macAddress)
|
|
t.Logf(" - MAC address '%s' loaded from DeviceInfo.xml", loadedInfo.MacAddress)
|
|
t.Logf(" - MAC mapping: '%s' -> '%s'", macAddress, device)
|
|
t.Logf(" - Sources.xml accessible via MAC address")
|
|
}
|
|
|
|
func TestMacAddressSerializationEdgeCases(t *testing.T) {
|
|
tempDir, err := os.MkdirTemp("", "mac-edge-cases-test-*")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.RemoveAll(tempDir)
|
|
|
|
ds := NewDataStore(tempDir)
|
|
account := "testaccount"
|
|
device := "testdevice"
|
|
|
|
testCases := []struct {
|
|
name string
|
|
macAddress string
|
|
expected string
|
|
}{
|
|
{"uppercase", "A81B6A536A98", "A81B6A536A98"},
|
|
{"lowercase", "a81b6a536a98", "a81b6a536a98"},
|
|
{"with_colons", "A8:1B:6A:53:6A:98", "A8:1B:6A:53:6A:98"},
|
|
{"with_dashes", "A8-1B-6A-53-6A-98", "A8-1B-6A-53-6A-98"},
|
|
{"empty", "", ""},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
deviceID := device + "_" + tc.name
|
|
|
|
info := &models.ServiceDeviceInfo{
|
|
DeviceID: deviceID,
|
|
Name: "Test Device " + tc.name,
|
|
ProductCode: "SoundTouch 10",
|
|
IPAddress: "192.168.1.100",
|
|
MacAddress: tc.macAddress,
|
|
DeviceSerialNumber: deviceID,
|
|
}
|
|
|
|
// Save and load
|
|
err := ds.SaveDeviceInfo(account, deviceID, info)
|
|
if err != nil {
|
|
t.Fatalf("SaveDeviceInfo failed for %s: %v", tc.name, err)
|
|
}
|
|
|
|
loadedInfo, err := ds.GetDeviceInfo(account, deviceID)
|
|
if err != nil {
|
|
t.Fatalf("GetDeviceInfo failed for %s: %v", tc.name, err)
|
|
}
|
|
|
|
if loadedInfo.MacAddress != tc.expected {
|
|
t.Errorf("MAC address mismatch for %s. Expected: '%s', Got: '%s'",
|
|
tc.name, tc.expected, loadedInfo.MacAddress)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestExistingDeviceInfoUpdate(t *testing.T) {
|
|
tempDir, err := os.MkdirTemp("", "device-update-test-*")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.RemoveAll(tempDir)
|
|
|
|
ds := NewDataStore(tempDir)
|
|
account := "3230304"
|
|
device := "I6332527703739342000020"
|
|
|
|
// First save without MAC address (simulating old DeviceInfo.xml)
|
|
infoWithoutMAC := &models.ServiceDeviceInfo{
|
|
DeviceID: device,
|
|
Name: "Test SoundTouch",
|
|
ProductCode: "SoundTouch 10",
|
|
IPAddress: "192.168.1.100",
|
|
MacAddress: "", // No MAC address initially
|
|
DeviceSerialNumber: device,
|
|
}
|
|
|
|
err = ds.SaveDeviceInfo(account, device, infoWithoutMAC)
|
|
if err != nil {
|
|
t.Fatalf("Initial SaveDeviceInfo failed: %v", err)
|
|
}
|
|
|
|
// Verify no MAC address initially
|
|
loadedInfo1, err := ds.GetDeviceInfo(account, device)
|
|
if err != nil {
|
|
t.Fatalf("Initial GetDeviceInfo failed: %v", err)
|
|
}
|
|
|
|
if loadedInfo1.MacAddress != "" {
|
|
t.Errorf("Expected empty MAC address, got '%s'", loadedInfo1.MacAddress)
|
|
}
|
|
|
|
// Now update with MAC address (simulating discovery update)
|
|
macAddress := "A81B6A536A98"
|
|
infoWithMAC := &models.ServiceDeviceInfo{
|
|
DeviceID: device,
|
|
Name: "Test SoundTouch",
|
|
ProductCode: "SoundTouch 10",
|
|
IPAddress: "192.168.1.100",
|
|
MacAddress: macAddress,
|
|
DeviceSerialNumber: device,
|
|
}
|
|
|
|
err = ds.SaveDeviceInfo(account, device, infoWithMAC)
|
|
if err != nil {
|
|
t.Fatalf("Update SaveDeviceInfo failed: %v", err)
|
|
}
|
|
|
|
// Verify MAC address is now present
|
|
loadedInfo2, err := ds.GetDeviceInfo(account, device)
|
|
if err != nil {
|
|
t.Fatalf("Updated GetDeviceInfo failed: %v", err)
|
|
}
|
|
|
|
if loadedInfo2.MacAddress != macAddress {
|
|
t.Errorf("MAC address not updated. Expected: '%s', Got: '%s'", macAddress, loadedInfo2.MacAddress)
|
|
}
|
|
|
|
// Initialize to test mapping
|
|
err = ds.Initialize()
|
|
if err != nil {
|
|
t.Fatalf("Initialize failed: %v", err)
|
|
}
|
|
|
|
// Test that MAC mapping now works
|
|
resolvedPath := ds.AccountDeviceDir(account, macAddress)
|
|
expectedPath := ds.AccountDeviceDir(account, device)
|
|
|
|
if resolvedPath != expectedPath {
|
|
t.Errorf("MAC mapping failed after update. MAC '%s' resolved to '%s', expected '%s'",
|
|
macAddress, resolvedPath, expectedPath)
|
|
}
|
|
|
|
t.Logf("✅ DeviceInfo.xml update with MAC address working correctly")
|
|
t.Logf(" - Initial: no MAC address")
|
|
t.Logf(" - Updated: MAC address '%s' added", macAddress)
|
|
t.Logf(" - Mapping: '%s' -> '%s'", macAddress, device)
|
|
}
|