mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-21 01:56:15 +00:00
Follow-up to #591: UpdateCheckEnabled/UpdateCheckInterval are now persisted, live-reloaded Settings fields (mirroring the discovery enabled/interval pattern), editable from the admin Settings page without a restart. The env var/CLI flag remains the seed value for a fresh install with no settings.json yet. The background goroutine now always runs and polls the live settings every minute (updateCheckPollTick), instead of being started only if enabled at process launch, so flipping the toggle takes effect within a minute rather than requiring a restart.
639 lines
17 KiB
Go
639 lines
17 KiB
Go
package datastore
|
|
|
|
import (
|
|
"encoding/xml"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/gesellix/bose-soundtouch/pkg/models"
|
|
)
|
|
|
|
func TestDataStore(t *testing.T) {
|
|
tempDir, err := os.MkdirTemp("", "st-test-*")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
defer func() { _ = os.RemoveAll(tempDir) }()
|
|
|
|
ds := NewDataStore(tempDir)
|
|
account := "test-account"
|
|
device := "test-device"
|
|
|
|
// Test Save/Get DeviceInfo
|
|
info := &models.ServiceDeviceInfo{
|
|
DeviceID: device,
|
|
Name: "Test Speaker",
|
|
AccountID: account,
|
|
}
|
|
|
|
err = ds.SaveDeviceInfo(account, device, info)
|
|
if err != nil {
|
|
t.Errorf("SaveDeviceInfo failed: %v", err)
|
|
}
|
|
|
|
loadedInfo, err := ds.GetDeviceInfo(account, device)
|
|
if err != nil {
|
|
t.Errorf("GetDeviceInfo failed: %v", err)
|
|
}
|
|
|
|
if loadedInfo.Name != info.Name {
|
|
t.Errorf("Expected name %s, got %s", info.Name, loadedInfo.Name)
|
|
}
|
|
|
|
// Test Presets
|
|
presets := []models.ServicePreset{
|
|
{
|
|
ServiceContentItem: models.ServiceContentItem{
|
|
Name: "Preset 1",
|
|
},
|
|
},
|
|
}
|
|
|
|
err = ds.SavePresets(account, device, presets)
|
|
if err != nil {
|
|
t.Errorf("SavePresets failed: %v", err)
|
|
}
|
|
|
|
loadedPresets, err := ds.GetPresets(account, device)
|
|
if err != nil {
|
|
t.Errorf("GetPresets failed: %v", err)
|
|
}
|
|
|
|
if len(loadedPresets) != 1 || loadedPresets[0].Name != "Preset 1" {
|
|
t.Errorf("Unexpected presets: %+v", loadedPresets)
|
|
}
|
|
|
|
// Test Recents
|
|
recents := []models.ServiceRecent{
|
|
{
|
|
ServiceContentItem: models.ServiceContentItem{
|
|
Name: "Recent 1",
|
|
},
|
|
},
|
|
}
|
|
|
|
err = ds.SaveRecents(account, device, recents)
|
|
if err != nil {
|
|
t.Errorf("SaveRecents failed: %v", err)
|
|
}
|
|
|
|
loadedRecents, err := ds.GetRecents(account, device)
|
|
if err != nil {
|
|
t.Errorf("GetRecents failed: %v", err)
|
|
}
|
|
|
|
if len(loadedRecents) != 1 || loadedRecents[0].Name != "Recent 1" {
|
|
t.Errorf("Unexpected recents: %+v", loadedRecents)
|
|
}
|
|
|
|
// Test path helpers
|
|
expectedAccountDir := filepath.Join(tempDir, "accounts", account)
|
|
if ds.AccountDir(account) != expectedAccountDir {
|
|
t.Errorf("Expected account dir %s, got %s", expectedAccountDir, ds.AccountDir(account))
|
|
}
|
|
|
|
// Test GetAccountInfo with placeholder
|
|
accInfo, err := ds.GetAccountInfo("non-existent")
|
|
if err != nil {
|
|
t.Errorf("GetAccountInfo failed: %v", err)
|
|
}
|
|
if !accInfo.IsPlaceholder {
|
|
t.Errorf("Expected IsPlaceholder to be true for non-existent account")
|
|
}
|
|
|
|
// Test SaveAccountInfo and GetAccountInfo
|
|
accountID := "acc-123"
|
|
info2 := &models.ServiceAccountInfo{
|
|
AccountID: accountID,
|
|
PreferredLanguage: "en",
|
|
}
|
|
err = ds.SaveAccountInfo(accountID, info2)
|
|
if err != nil {
|
|
t.Errorf("SaveAccountInfo failed: %v", err)
|
|
}
|
|
|
|
loadedAccInfo, err := ds.GetAccountInfo(accountID)
|
|
if err != nil {
|
|
t.Errorf("GetAccountInfo failed: %v", err)
|
|
}
|
|
if loadedAccInfo.PreferredLanguage != "en" {
|
|
t.Errorf("Expected language en, got %s", loadedAccInfo.PreferredLanguage)
|
|
}
|
|
if loadedAccInfo.IsPlaceholder {
|
|
t.Errorf("Expected IsPlaceholder to be false for existing account")
|
|
}
|
|
}
|
|
|
|
func TestListAllDevices_Empty(t *testing.T) {
|
|
tempDir, err := os.MkdirTemp("", "st-empty-test-*")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
defer func() { _ = os.RemoveAll(tempDir) }()
|
|
|
|
ds := NewDataStore(tempDir)
|
|
|
|
// Case 1: DataDir does not exist
|
|
_ = os.RemoveAll(tempDir)
|
|
|
|
devices, err := ds.ListAllDevices()
|
|
if err != nil {
|
|
t.Errorf("ListAllDevices should not return error when DataDir does not exist, got %v", err)
|
|
}
|
|
|
|
if devices == nil || len(devices) != 0 {
|
|
t.Errorf("Expected empty slice when DataDir does not exist, got %+v", devices)
|
|
}
|
|
|
|
// Case 2: DataDir is empty
|
|
_ = os.MkdirAll(tempDir, 0755)
|
|
|
|
devices, err = ds.ListAllDevices()
|
|
if err != nil {
|
|
t.Errorf("ListAllDevices failed on empty dir: %v", err)
|
|
}
|
|
|
|
if devices == nil {
|
|
t.Errorf("Expected empty slice (not nil) when no devices exist")
|
|
}
|
|
|
|
if len(devices) != 0 {
|
|
t.Errorf("Expected 0 devices, got %d", len(devices))
|
|
}
|
|
}
|
|
|
|
func TestListAllDevices(t *testing.T) {
|
|
tempDir, err := os.MkdirTemp("", "st-list-test-*")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
defer func() { _ = os.RemoveAll(tempDir) }()
|
|
|
|
ds := NewDataStore(tempDir)
|
|
account := "default"
|
|
deviceID := "BO5EBO5E-F00D-F00D-FEED-AABBCCDDEE0A"
|
|
|
|
info := &models.ServiceDeviceInfo{
|
|
DeviceID: deviceID,
|
|
Name: "Test Speaker",
|
|
IPAddress: "192.0.2.100",
|
|
DeviceSerialNumber: deviceID,
|
|
ProductCode: "SoundTouch 10",
|
|
FirmwareVersion: "1.2.3",
|
|
AccountID: account,
|
|
}
|
|
|
|
err = ds.SaveDeviceInfo(account, deviceID, info)
|
|
if err != nil {
|
|
t.Fatalf("SaveDeviceInfo failed: %v", err)
|
|
}
|
|
|
|
devices, err := ds.ListAllDevices()
|
|
if err != nil {
|
|
t.Fatalf("ListAllDevices failed: %v", err)
|
|
}
|
|
|
|
if len(devices) != 1 {
|
|
t.Fatalf("Expected 1 device, got %d", len(devices))
|
|
}
|
|
|
|
if devices[0].DeviceID != deviceID {
|
|
t.Errorf("Expected DeviceID %s, got %s", deviceID, devices[0].DeviceID)
|
|
}
|
|
}
|
|
|
|
func TestListAllDevices_EmptyDeviceID(t *testing.T) {
|
|
tempDir, err := os.MkdirTemp("", "st-empty-id-test-*")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
defer func() { _ = os.RemoveAll(tempDir) }()
|
|
|
|
ds := NewDataStore(tempDir)
|
|
account := "default"
|
|
deviceID := ""
|
|
|
|
info := &models.ServiceDeviceInfo{
|
|
DeviceID: deviceID,
|
|
Name: "Empty ID Speaker",
|
|
AccountID: account,
|
|
}
|
|
|
|
// Use IP as fallback for device ID if it is empty
|
|
key := deviceID
|
|
if key == "" {
|
|
key = "127.0.0.1"
|
|
}
|
|
|
|
err = ds.SaveDeviceInfo(account, key, info)
|
|
if err != nil {
|
|
t.Fatalf("SaveDeviceInfo failed: %v", err)
|
|
}
|
|
|
|
devices, err := ds.ListAllDevices()
|
|
if err != nil {
|
|
t.Fatalf("ListAllDevices failed: %v", err)
|
|
}
|
|
|
|
if len(devices) != 1 {
|
|
t.Fatalf("Expected 1 device, got %d", len(devices))
|
|
}
|
|
|
|
if devices[0].Name != "Empty ID Speaker" {
|
|
t.Errorf("Expected Name 'Empty ID Speaker', got %s", devices[0].Name)
|
|
}
|
|
}
|
|
|
|
func TestListAllDevices_MultipleEmptyIDs(t *testing.T) {
|
|
tempDir, err := os.MkdirTemp("", "st-multi-empty-test-*")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
defer func() { _ = os.RemoveAll(tempDir) }()
|
|
|
|
ds := NewDataStore(tempDir)
|
|
account := "default"
|
|
|
|
// Save two devices with empty ID but different IPs
|
|
info1 := &models.ServiceDeviceInfo{
|
|
DeviceID: "",
|
|
Name: "Speaker 1",
|
|
IPAddress: "192.0.2.1",
|
|
AccountID: account,
|
|
}
|
|
info2 := &models.ServiceDeviceInfo{
|
|
DeviceID: "",
|
|
Name: "Speaker 2",
|
|
IPAddress: "192.0.2.2",
|
|
AccountID: account,
|
|
}
|
|
|
|
// We use the same logic as in main.go: use IP as fallback for directory name
|
|
err = ds.SaveDeviceInfo(account, info1.IPAddress, info1)
|
|
if err != nil {
|
|
t.Fatalf("SaveDeviceInfo 1 failed: %v", err)
|
|
}
|
|
|
|
err = ds.SaveDeviceInfo(account, info2.IPAddress, info2)
|
|
if err != nil {
|
|
t.Fatalf("SaveDeviceInfo 2 failed: %v", err)
|
|
}
|
|
|
|
devices, err := ds.ListAllDevices()
|
|
if err != nil {
|
|
t.Fatalf("ListAllDevices failed: %v", err)
|
|
}
|
|
|
|
if len(devices) != 2 {
|
|
t.Fatalf("Expected 2 devices, got %d", len(devices))
|
|
}
|
|
}
|
|
|
|
func TestListAllDevices_MalformedXML(t *testing.T) {
|
|
tempDir, err := os.MkdirTemp("", "st-malformed-test-*")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
defer func() { _ = os.RemoveAll(tempDir) }()
|
|
|
|
ds := NewDataStore(tempDir)
|
|
account := "default"
|
|
deviceID := "malformed-device"
|
|
|
|
dir := ds.AccountDeviceDir(account, deviceID)
|
|
_ = os.MkdirAll(dir, 0755)
|
|
_ = os.WriteFile(filepath.Join(dir, "DeviceInfo.xml"), []byte("<info>not even closed"), 0644)
|
|
|
|
devices, err := ds.ListAllDevices()
|
|
if err != nil {
|
|
t.Fatalf("ListAllDevices should not return error on malformed XML: %v", err)
|
|
}
|
|
|
|
if len(devices) != 0 {
|
|
t.Errorf("Expected 0 devices, got %d", len(devices))
|
|
}
|
|
}
|
|
|
|
func TestConfiguredSources(t *testing.T) {
|
|
tempDir, _ := os.MkdirTemp("", "datastore-sources-*")
|
|
|
|
defer func() { _ = os.RemoveAll(tempDir) }()
|
|
|
|
ds := NewDataStore(tempDir)
|
|
account := "test-acc"
|
|
|
|
sources := []models.ConfiguredSource{
|
|
{
|
|
DisplayName: "Source 1",
|
|
ID: "101",
|
|
Secret: "secret1",
|
|
SecretType: "type1",
|
|
SourceKey: struct {
|
|
Type string `xml:"type,attr"`
|
|
Account string `xml:"account,attr"`
|
|
}{Type: "TUNEIN", Account: "user1"},
|
|
SourceKeyType: "TUNEIN",
|
|
SourceKeyAccount: "user1",
|
|
},
|
|
{
|
|
DisplayName: "Source 2",
|
|
ID: "102",
|
|
Secret: "secret2",
|
|
SecretType: "type2",
|
|
SourceKey: struct {
|
|
Type string `xml:"type,attr"`
|
|
Account string `xml:"account,attr"`
|
|
}{Type: "PANDORA", Account: "user2"},
|
|
SourceKeyType: "PANDORA",
|
|
SourceKeyAccount: "user2",
|
|
},
|
|
}
|
|
|
|
err := ds.SaveConfiguredSources(account, "any", sources)
|
|
if err != nil {
|
|
t.Fatalf("SaveConfiguredSources failed: %v", err)
|
|
}
|
|
|
|
loadedSources, err := ds.GetConfiguredSources(account, "any")
|
|
if err != nil {
|
|
t.Fatalf("GetConfiguredSources failed: %v", err)
|
|
}
|
|
|
|
if len(loadedSources) != len(sources) {
|
|
t.Fatalf("Expected %d sources, got %d", len(sources), len(loadedSources))
|
|
}
|
|
|
|
for i := range sources {
|
|
ls := loadedSources[i]
|
|
expected := sources[i]
|
|
expected.Secret = ""
|
|
expected.SecretType = ""
|
|
expected.Type = ls.Type // Ignore Type mismatch in this test if it's auto-populated
|
|
|
|
// Clear secrets for comparison since they are not loaded by GetConfiguredSources
|
|
ls.Secret = ""
|
|
ls.SecretType = ""
|
|
|
|
if ls.DisplayName != expected.DisplayName || ls.ID != expected.ID || ls.Secret != expected.Secret ||
|
|
ls.SecretType != expected.SecretType || ls.SourceKeyType != expected.SourceKeyType ||
|
|
ls.SourceKeyAccount != expected.SourceKeyAccount || ls.Type != expected.Type {
|
|
// Clean XMLName for comparison
|
|
ls.XMLName = xml.Name{}
|
|
if ls.DisplayName != expected.DisplayName || ls.ID != expected.ID || ls.Secret != expected.Secret ||
|
|
ls.SecretType != expected.SecretType || ls.SourceKeyType != expected.SourceKeyType ||
|
|
ls.SourceKeyAccount != expected.SourceKeyAccount || ls.Type != expected.Type {
|
|
t.Errorf("Source %d mismatch. Expected %+v, got %+v", i, expected, ls)
|
|
}
|
|
}
|
|
}
|
|
|
|
// Test with missing ID (GetConfiguredSources should auto-assign)
|
|
sources2 := []models.ConfiguredSource{
|
|
{
|
|
DisplayName: "Source No ID",
|
|
SourceKeyType: "LOCAL",
|
|
SourceKeyAccount: "user3",
|
|
},
|
|
}
|
|
|
|
err = ds.SaveConfiguredSources(account, "any", sources2)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
loadedSources2, err := ds.GetConfiguredSources(account, "any")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if loadedSources2[0].ID == "" {
|
|
t.Error("Expected auto-assigned ID for source with empty ID")
|
|
}
|
|
}
|
|
|
|
func TestSettingsPersistence(t *testing.T) {
|
|
tempDir, err := os.MkdirTemp("", "settings-test-*")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.RemoveAll(tempDir)
|
|
|
|
ds := NewDataStore(tempDir)
|
|
|
|
settings := Settings{
|
|
ServerURL: "http://myserver:8000",
|
|
LogBodies: true,
|
|
DiscoveryInterval: "10m",
|
|
DiscoveryEnabled: true,
|
|
UpdateCheckInterval: "12h",
|
|
UpdateCheckEnabled: true,
|
|
}
|
|
|
|
err = ds.SaveSettings(settings)
|
|
if err != nil {
|
|
t.Fatalf("SaveSettings failed: %v", err)
|
|
}
|
|
|
|
loaded, err := ds.GetSettings()
|
|
if err != nil {
|
|
t.Fatalf("GetSettings failed: %v", err)
|
|
}
|
|
|
|
if loaded.ServerURL != settings.ServerURL {
|
|
t.Errorf("Expected ServerURL %s, got %s", settings.ServerURL, loaded.ServerURL)
|
|
}
|
|
if loaded.LogBodies != settings.LogBodies {
|
|
t.Errorf("Expected LogBodies %v, got %v", settings.LogBodies, loaded.LogBodies)
|
|
}
|
|
if loaded.DiscoveryInterval != settings.DiscoveryInterval {
|
|
t.Errorf("Expected DiscoveryInterval %s, got %s", settings.DiscoveryInterval, loaded.DiscoveryInterval)
|
|
}
|
|
if loaded.DiscoveryEnabled != settings.DiscoveryEnabled {
|
|
t.Errorf("Expected DiscoveryEnabled %v, got %v", settings.DiscoveryEnabled, loaded.DiscoveryEnabled)
|
|
}
|
|
if loaded.UpdateCheckInterval != settings.UpdateCheckInterval {
|
|
t.Errorf("Expected UpdateCheckInterval %s, got %s", settings.UpdateCheckInterval, loaded.UpdateCheckInterval)
|
|
}
|
|
if loaded.UpdateCheckEnabled != settings.UpdateCheckEnabled {
|
|
t.Errorf("Expected UpdateCheckEnabled %v, got %v", settings.UpdateCheckEnabled, loaded.UpdateCheckEnabled)
|
|
}
|
|
}
|
|
|
|
// TestUpdateCheckState_MissingFileReturnsZeroValue verifies a fresh install
|
|
// (or one where the update check has never run) gets a zero-value state,
|
|
// not an error — same shape as GetSettings on a missing settings.json.
|
|
func TestUpdateCheckState_MissingFileReturnsZeroValue(t *testing.T) {
|
|
tempDir, err := os.MkdirTemp("", "update-check-missing-test-*")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.RemoveAll(tempDir)
|
|
|
|
ds := NewDataStore(tempDir)
|
|
|
|
state, err := ds.GetUpdateCheckState()
|
|
if err != nil {
|
|
t.Fatalf("GetUpdateCheckState on a fresh install should not error, got: %v", err)
|
|
}
|
|
if state != (UpdateCheckState{}) {
|
|
t.Errorf("Expected zero-value state, got %+v", state)
|
|
}
|
|
}
|
|
|
|
// TestUpdateCheckState_Persistence is the roundtrip test, mirroring
|
|
// TestSettingsPersistence.
|
|
func TestUpdateCheckState_Persistence(t *testing.T) {
|
|
tempDir, err := os.MkdirTemp("", "update-check-persist-test-*")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.RemoveAll(tempDir)
|
|
|
|
ds := NewDataStore(tempDir)
|
|
|
|
state := UpdateCheckState{
|
|
LastCheckedAt: "2026-08-09T12:00:00Z",
|
|
LastSeenVersion: "v0.122.0",
|
|
LastReleaseURL: "https://github.com/gesellix/Bose-SoundTouch/releases/tag/v0.122.0",
|
|
}
|
|
|
|
if err := ds.SaveUpdateCheckState(state); err != nil {
|
|
t.Fatalf("SaveUpdateCheckState failed: %v", err)
|
|
}
|
|
|
|
loaded, err := ds.GetUpdateCheckState()
|
|
if err != nil {
|
|
t.Fatalf("GetUpdateCheckState failed: %v", err)
|
|
}
|
|
|
|
if loaded != state {
|
|
t.Errorf("Expected %+v, got %+v", state, loaded)
|
|
}
|
|
}
|
|
|
|
// TestRecordActivity_EmptyKindReturnsNilNotError verifies GetActivityRecords
|
|
// for a kind that was never recorded returns an empty, non-error result —
|
|
// the "nothing recorded yet" case, not a failure.
|
|
func TestRecordActivity_EmptyKindReturnsNilNotError(t *testing.T) {
|
|
tempDir, err := os.MkdirTemp("", "activity-empty-test-*")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.RemoveAll(tempDir)
|
|
|
|
ds := NewDataStore(tempDir)
|
|
|
|
records, err := ds.GetActivityRecords("notification_dismissed")
|
|
if err != nil {
|
|
t.Fatalf("GetActivityRecords on empty kind should not error, got: %v", err)
|
|
}
|
|
if len(records) != 0 {
|
|
t.Errorf("Expected no records, got %d", len(records))
|
|
}
|
|
}
|
|
|
|
// TestRecordActivity_SameIDRecursWithNewTimestamp is the regression test for
|
|
// the append-only shape agreed in the #419 design: dismissing the same
|
|
// announcement twice must produce two records, not overwrite one — this is
|
|
// a log, not a keyed map.
|
|
func TestRecordActivity_SameIDRecursWithNewTimestamp(t *testing.T) {
|
|
tempDir, err := os.MkdirTemp("", "activity-recur-test-*")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.RemoveAll(tempDir)
|
|
|
|
ds := NewDataStore(tempDir)
|
|
|
|
if err := ds.RecordActivity("notification_dismissed", "admin-gate-notice", nil); err != nil {
|
|
t.Fatalf("First RecordActivity failed: %v", err)
|
|
}
|
|
if err := ds.RecordActivity("notification_dismissed", "admin-gate-notice", nil); err != nil {
|
|
t.Fatalf("Second RecordActivity failed: %v", err)
|
|
}
|
|
|
|
records, err := ds.GetActivityRecords("notification_dismissed")
|
|
if err != nil {
|
|
t.Fatalf("GetActivityRecords failed: %v", err)
|
|
}
|
|
if len(records) != 2 {
|
|
t.Fatalf("Expected 2 records for the same recurring id, got %d: %+v", len(records), records)
|
|
}
|
|
for _, r := range records {
|
|
if r.ID != "admin-gate-notice" || r.Kind != "notification_dismissed" || r.Timestamp == "" {
|
|
t.Errorf("Unexpected record shape: %+v", r)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestRecordActivity_DetailRoundTrips verifies the optional detail payload
|
|
// survives a write/read round trip.
|
|
func TestRecordActivity_DetailRoundTrips(t *testing.T) {
|
|
tempDir, err := os.MkdirTemp("", "activity-detail-test-*")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer os.RemoveAll(tempDir)
|
|
|
|
ds := NewDataStore(tempDir)
|
|
|
|
if err := ds.RecordActivity("some_kind", "some-id", map[string]interface{}{"note": "hello"}); err != nil {
|
|
t.Fatalf("RecordActivity failed: %v", err)
|
|
}
|
|
|
|
records, err := ds.GetActivityRecords("some_kind")
|
|
if err != nil {
|
|
t.Fatalf("GetActivityRecords failed: %v", err)
|
|
}
|
|
if len(records) != 1 {
|
|
t.Fatalf("Expected 1 record, got %d", len(records))
|
|
}
|
|
if records[0].Detail["note"] != "hello" {
|
|
t.Errorf("Expected detail to round-trip, got: %+v", records[0].Detail)
|
|
}
|
|
}
|
|
|
|
func TestMoveDeviceMigratesData(t *testing.T) {
|
|
tempDir := t.TempDir()
|
|
ds := NewDataStore(tempDir)
|
|
|
|
oldAccount := "default"
|
|
newAccount := "8637922"
|
|
deviceID := "F4E11E930BEB"
|
|
|
|
// Seed the device under the old account with presets
|
|
presets := []models.ServicePreset{
|
|
{ID: "1", ServiceContentItem: models.ServiceContentItem{Name: "Radio Preset", ContentItemType: "stationurl"}},
|
|
}
|
|
if err := ds.SavePresets(oldAccount, deviceID, presets); err != nil {
|
|
t.Fatalf("SavePresets under %s: %v", oldAccount, err)
|
|
}
|
|
|
|
// Move the device to the new account
|
|
if err := ds.MoveDevice(oldAccount, newAccount, deviceID); err != nil {
|
|
t.Fatalf("MoveDevice %s → %s: %v", oldAccount, newAccount, err)
|
|
}
|
|
|
|
// Verify presets survived the move under the new account
|
|
moved, err := ds.GetPresets(newAccount, deviceID)
|
|
if err != nil {
|
|
t.Fatalf("GetPresets under %s after move: %v", newAccount, err)
|
|
}
|
|
if len(moved) != 1 || moved[0].Name != "Radio Preset" {
|
|
t.Errorf("presets not preserved: got %+v", moved)
|
|
}
|
|
|
|
// Verify the old account no longer has the device
|
|
oldPresets, err := ds.GetPresets(oldAccount, deviceID)
|
|
if err == nil && len(oldPresets) > 0 {
|
|
t.Errorf("old account %s still has presets after move: %+v", oldAccount, oldPresets)
|
|
}
|
|
}
|