mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-19 00:56:16 +00:00
460 lines
11 KiB
Go
460 lines
11 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-08DF1F0BA325"
|
|
|
|
info := &models.ServiceDeviceInfo{
|
|
DeviceID: deviceID,
|
|
Name: "Test Speaker",
|
|
IPAddress: "192.168.1.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.168.1.1",
|
|
AccountID: account,
|
|
}
|
|
info2 := &models.ServiceDeviceInfo{
|
|
DeviceID: "",
|
|
Name: "Speaker 2",
|
|
IPAddress: "192.168.1.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,
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|