mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-19 00:56:16 +00:00
feat: improve Bose SoundTouch parity, Spotify integration, and data
reliability
- Update XML marshaling for ServicePreset and ServiceRecent to match
Bose parity requirements.
- Add support for adding music sources via
`/streaming/account/{account}/source`.
- Implement HandleBoseAccountToken for Spotify OAuth code exchange and
token persistence.
- Implement atomic file writes in the datastore to prevent data
corruption.
- Add startup logic to initialize default sources for existing devices.
- Expand test coverage with new parity regression and Spotify
integration tests.
---------
Co-authored-by: Junie <junie@jetbrains.com>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
111 lines
3.3 KiB
Go
111 lines
3.3 KiB
Go
package datastore
|
|
|
|
import (
|
|
"encoding/xml"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/gesellix/bose-soundtouch/pkg/models"
|
|
)
|
|
|
|
func TestSaveRecents_Format(t *testing.T) {
|
|
tempDir, err := os.MkdirTemp("", "datastore_recents_test")
|
|
if err != nil {
|
|
t.Fatalf("Failed to create temp dir: %v", err)
|
|
}
|
|
defer os.RemoveAll(tempDir)
|
|
|
|
ds := NewDataStore(tempDir)
|
|
|
|
account := "test-account"
|
|
device := "test-device"
|
|
|
|
recents := []models.ServiceRecent{
|
|
{
|
|
ServiceContentItem: models.ServiceContentItem{
|
|
ID: "2567119953",
|
|
Name: "The National",
|
|
Source: "SPOTIFY",
|
|
ContentItemType: "tracklisturl",
|
|
Location: "/playback/container/c3BvdGlmeTp1c2VyOnRlc3QtdXNlcjpjb2xsZWN0aW9uOmFydGlzdDoyY0NVdEdLOXNEVTJFb0VsbmswR05C",
|
|
SourceAccount: "test-user",
|
|
IsPresetable: "true",
|
|
},
|
|
DeviceID: "001122334455",
|
|
UtcTime: "1771666755",
|
|
},
|
|
}
|
|
|
|
if err := ds.SaveRecents(account, device, recents); err != nil {
|
|
t.Fatalf("SaveRecents failed: %v", err)
|
|
}
|
|
|
|
path := filepath.Join(ds.AccountDeviceDir(account, device), "Recents.xml")
|
|
content, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("Failed to read Recents.xml: %v", err)
|
|
}
|
|
|
|
expectedXML := `<?xml version="1.0" encoding="UTF-8"?>
|
|
<recents>
|
|
<recent id="2567119953" deviceID="001122334455" utcTime="1771666755">
|
|
<contentItem source="SPOTIFY" type="tracklisturl" location="/playback/container/c3BvdGlmeTp1c2VyOnRlc3QtdXNlcjpjb2xsZWN0aW9uOmFydGlzdDoyY0NVdEdLOXNEVTJFb0VsbmswR05C" sourceAccount="test-user" isPresetable="true">
|
|
<itemName>The National</itemName>
|
|
</contentItem>
|
|
</recent>
|
|
</recents>`
|
|
|
|
// Normalize whitespace for comparison by unmarshaling both
|
|
var expected, actual struct {
|
|
XMLName xml.Name `xml:"recents"`
|
|
Recents []struct {
|
|
ID string `xml:"id,attr"`
|
|
DeviceID string `xml:"deviceID,attr"`
|
|
UtcTime string `xml:"utcTime,attr"`
|
|
ContentItem struct {
|
|
Source string `xml:"source,attr"`
|
|
Type string `xml:"type,attr"`
|
|
Location string `xml:"location,attr"`
|
|
SourceAccount string `xml:"sourceAccount,attr"`
|
|
IsPresetable string `xml:"isPresetable,attr"`
|
|
ItemName string `xml:"itemName"`
|
|
} `xml:"contentItem"`
|
|
} `xml:"recent"`
|
|
}
|
|
|
|
if err := xml.Unmarshal([]byte(expectedXML), &expected); err != nil {
|
|
t.Fatalf("Failed to unmarshal expected XML: %v", err)
|
|
}
|
|
if err := xml.Unmarshal(content, &actual); err != nil {
|
|
t.Fatalf("Failed to unmarshal actual XML: %v", err)
|
|
}
|
|
|
|
if len(actual.Recents) != 1 {
|
|
t.Fatalf("Expected 1 recent, got %d", len(actual.Recents))
|
|
}
|
|
|
|
r := actual.Recents[0]
|
|
if r.ID != "2567119953" || r.DeviceID != "001122334455" || r.UtcTime != "1771666755" {
|
|
t.Errorf("Attributes mismatch: %+v", r)
|
|
}
|
|
if r.ContentItem.ItemName != "The National" || r.ContentItem.Source != "SPOTIFY" {
|
|
t.Errorf("ContentItem mismatch: %+v", r)
|
|
}
|
|
|
|
// Now test Round-trip (GetRecents)
|
|
loadedRecents, err := ds.GetRecents(account, device)
|
|
if err != nil {
|
|
t.Fatalf("GetRecents failed: %v", err)
|
|
}
|
|
|
|
if len(loadedRecents) != 1 {
|
|
t.Fatalf("Expected 1 loaded recent, got %d", len(loadedRecents))
|
|
}
|
|
|
|
lr := loadedRecents[0]
|
|
if lr.ID != "2567119953" || lr.Name != "The National" || lr.Source != "SPOTIFY" || lr.SourceAccount != "test-user" {
|
|
t.Errorf("Loaded recent mismatch: %+v", lr)
|
|
}
|
|
}
|