mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-18 08:36:13 +00:00
chore: run golangci-lint --fix and manually address remaining linting issues. Fixed bodyclose, errcheck, and contextcheck across the codebase.
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/http/httputil"
|
||||
@@ -44,6 +45,7 @@ func main() {
|
||||
if dataDir == "" {
|
||||
dataDir = "data"
|
||||
}
|
||||
|
||||
ds := datastore.NewDataStore(dataDir)
|
||||
if err := ds.Initialize(); err != nil {
|
||||
log.Printf("Warning: Failed to initialize datastore: %v", err)
|
||||
@@ -56,6 +58,7 @@ func main() {
|
||||
if hostname == "" {
|
||||
hostname = "localhost"
|
||||
}
|
||||
|
||||
serverURL = "http://" + strings.ToLower(hostname) + ":" + port
|
||||
}
|
||||
|
||||
@@ -81,11 +84,13 @@ func main() {
|
||||
currentLp := proxy.NewLoggingProxy(target.String(), redact)
|
||||
currentLp.LogBody = logBody
|
||||
currentLp.LogResponse(res)
|
||||
|
||||
return nil
|
||||
}
|
||||
originalPyDirector := pyProxy.Director
|
||||
pyProxy.Director = func(req *http.Request) {
|
||||
originalPyDirector(req)
|
||||
|
||||
currentLp := proxy.NewLoggingProxy(target.String(), redact)
|
||||
currentLp.LogBody = logBody
|
||||
currentLp.LogRequest(req)
|
||||
@@ -94,7 +99,7 @@ func main() {
|
||||
// Phase 5: Device Discovery
|
||||
go func() {
|
||||
for {
|
||||
server.DiscoverDevices()
|
||||
server.DiscoverDevices(context.Background())
|
||||
time.Sleep(5 * time.Minute)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -15,11 +15,13 @@ import (
|
||||
func main() {
|
||||
// 1. Trigger a discovery scan
|
||||
fmt.Println("Triggering discovery scan...")
|
||||
|
||||
resp, err := http.Post("http://localhost:8000/setup/discover", "application/json", nil)
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to trigger discovery: %v\nMake sure soundtouch-service is running on localhost:8000", err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
|
||||
_ = resp.Body.Close()
|
||||
|
||||
// Wait a bit for discovery to find some devices
|
||||
fmt.Println("Waiting 5 seconds for discovery...")
|
||||
@@ -27,11 +29,13 @@ func main() {
|
||||
|
||||
// 2. List discovered devices
|
||||
fmt.Println("Fetching discovered devices...")
|
||||
|
||||
resp, err = http.Get("http://localhost:8000/setup/devices")
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to fetch devices: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
@@ -49,6 +53,7 @@ func main() {
|
||||
}
|
||||
|
||||
fmt.Printf("Discovered %d devices:\n", len(devices))
|
||||
|
||||
for _, d := range devices {
|
||||
fmt.Printf("- %s (IP: %s, Model: %s)\n", d["name"], d["ip_address"], d["product_code"])
|
||||
}
|
||||
|
||||
+19
-4
@@ -21,11 +21,13 @@ const (
|
||||
|
||||
func TuneInPlayback(stationID string) (*models.BmxPlaybackResponse, error) {
|
||||
describeURL := fmt.Sprintf(TuneInDescribe, stationID)
|
||||
|
||||
resp, err := http.Get(describeURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
@@ -50,11 +52,13 @@ func TuneInPlayback(stationID string) (*models.BmxPlaybackResponse, error) {
|
||||
station := opml.Body.Outline.Station
|
||||
|
||||
streamReq := fmt.Sprintf(TuneInStream, stationID)
|
||||
|
||||
streamResp, err := http.Get(streamReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer streamResp.Body.Close()
|
||||
|
||||
defer func() { _ = streamResp.Body.Close() }()
|
||||
|
||||
streamBody, err := io.ReadAll(streamResp.Body)
|
||||
if err != nil {
|
||||
@@ -76,11 +80,13 @@ func TuneInPlayback(stationID string) (*models.BmxPlaybackResponse, error) {
|
||||
bmxReporting := "/v1/report?" + bmxReportingQS.Encode()
|
||||
|
||||
var streams []models.Stream
|
||||
|
||||
for _, sURL := range streamURLList {
|
||||
sURL = strings.TrimSpace(sURL)
|
||||
if sURL == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
streams = append(streams, models.Stream{
|
||||
Links: &models.Links{
|
||||
BmxReporting: &models.Link{Href: bmxReporting},
|
||||
@@ -123,9 +129,11 @@ func TuneInPodcastInfo(podcastID string, encodedName string) (*models.BmxPodcast
|
||||
if err != nil {
|
||||
nameBytes, err = base64.StdEncoding.DecodeString(encodedName)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
name := string(nameBytes)
|
||||
|
||||
track := models.Track{
|
||||
@@ -152,11 +160,13 @@ func TuneInPodcastInfo(podcastID string, encodedName string) (*models.BmxPodcast
|
||||
|
||||
func TuneInPlaybackPodcast(podcastID string) (*models.BmxPlaybackResponse, error) {
|
||||
describeURL := fmt.Sprintf(TuneInDescribe, podcastID)
|
||||
|
||||
resp, err := http.Get(describeURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
defer func() { _ = resp.Body.Close() }()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
@@ -184,11 +194,13 @@ func TuneInPlaybackPodcast(podcastID string) (*models.BmxPlaybackResponse, error
|
||||
topic := opml.Body.Outline.Topic
|
||||
|
||||
streamReq := fmt.Sprintf(TuneInStream, podcastID)
|
||||
|
||||
streamResp, err := http.Get(streamReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer streamResp.Body.Close()
|
||||
|
||||
defer func() { _ = streamResp.Body.Close() }()
|
||||
|
||||
streamBody, err := io.ReadAll(streamResp.Body)
|
||||
if err != nil {
|
||||
@@ -210,11 +222,13 @@ func TuneInPlaybackPodcast(podcastID string) (*models.BmxPlaybackResponse, error
|
||||
bmxReporting := "/v1/report?" + bmxReportingQS.Encode()
|
||||
|
||||
var streams []models.Stream
|
||||
|
||||
for _, sURL := range streamURLList {
|
||||
sURL = strings.TrimSpace(sURL)
|
||||
if sURL == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
streams = append(streams, models.Stream{
|
||||
Links: &models.Links{
|
||||
BmxReporting: &models.Link{Href: bmxReporting},
|
||||
@@ -264,6 +278,7 @@ func PlayCustomStream(data string) (*models.BmxPlaybackResponse, error) {
|
||||
if err != nil {
|
||||
jsonStr, err = base64.StdEncoding.DecodeString(data)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -21,20 +21,24 @@ func TestPlayCustomStream(t *testing.T) {
|
||||
|
||||
// Test Standard Base64
|
||||
dataStd := base64.StdEncoding.EncodeToString(jsonBytes)
|
||||
|
||||
resp, err := PlayCustomStream(dataStd)
|
||||
if err != nil {
|
||||
t.Fatalf("PlayCustomStream with standard base64 failed: %v", err)
|
||||
}
|
||||
|
||||
if resp.Name != "Stream Name" {
|
||||
t.Errorf("Expected name Stream Name, got %s", resp.Name)
|
||||
}
|
||||
|
||||
// Test URL-safe Base64
|
||||
dataURL := base64.URLEncoding.EncodeToString(jsonBytes)
|
||||
|
||||
resp, err = PlayCustomStream(dataURL)
|
||||
if err != nil {
|
||||
t.Fatalf("PlayCustomStream with URL-safe base64 failed: %v", err)
|
||||
}
|
||||
|
||||
if resp.Name != "Stream Name" {
|
||||
t.Errorf("Expected name Stream Name, got %s", resp.Name)
|
||||
}
|
||||
@@ -45,20 +49,24 @@ func TestTuneInPodcastInfo_Base64(t *testing.T) {
|
||||
|
||||
// Test Standard Base64
|
||||
encodedStd := base64.StdEncoding.EncodeToString([]byte(name))
|
||||
|
||||
resp, err := TuneInPodcastInfo("123", encodedStd)
|
||||
if err != nil {
|
||||
t.Fatalf("TuneInPodcastInfo with standard base64 failed: %v", err)
|
||||
}
|
||||
|
||||
if resp.Name != name {
|
||||
t.Errorf("Expected name %s, got %s", name, resp.Name)
|
||||
}
|
||||
|
||||
// Test URL-safe Base64
|
||||
encodedURL := base64.URLEncoding.EncodeToString([]byte(name))
|
||||
|
||||
resp, err = TuneInPodcastInfo("123", encodedURL)
|
||||
if err != nil {
|
||||
t.Fatalf("TuneInPodcastInfo with URL-safe base64 failed: %v", err)
|
||||
}
|
||||
|
||||
if resp.Name != name {
|
||||
t.Errorf("Expected name %s, got %s", name, resp.Name)
|
||||
}
|
||||
|
||||
@@ -8,9 +8,11 @@ func TestConstants(t *testing.T) {
|
||||
if DateStr == "" {
|
||||
t.Error("DateStr should not be empty")
|
||||
}
|
||||
|
||||
if SpeakerHTTPPort != 8090 {
|
||||
t.Errorf("Expected SpeakerHTTPPort 8090, got %d", SpeakerHTTPPort)
|
||||
}
|
||||
|
||||
if len(Providers) == 0 {
|
||||
t.Error("Providers should not be empty")
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gesellix/bose-soundtouch/pkg/service/constants"
|
||||
"github.com/gesellix/bose-soundtouch/pkg/models"
|
||||
"github.com/gesellix/bose-soundtouch/pkg/service/constants"
|
||||
)
|
||||
|
||||
func exists(path string) bool {
|
||||
@@ -29,6 +29,7 @@ func NewDataStore(dataDir string) *DataStore {
|
||||
if dataDir == "" {
|
||||
dataDir = "data"
|
||||
}
|
||||
|
||||
return &DataStore{
|
||||
DataDir: dataDir,
|
||||
deviceEvents: make(map[string][]models.DeviceEvent),
|
||||
@@ -49,6 +50,7 @@ func (ds *DataStore) AccountDeviceDir(account, device string) string {
|
||||
|
||||
func (ds *DataStore) GetDeviceInfo(account, device string) (*models.ServiceDeviceInfo, error) {
|
||||
path := filepath.Join(ds.AccountDeviceDir(account, device), constants.DeviceInfoFile)
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -82,10 +84,11 @@ func (ds *DataStore) GetDeviceInfo(account, device string) (*models.ServiceDevic
|
||||
}
|
||||
|
||||
for _, comp := range info.Components {
|
||||
if comp.Category == "SCM" {
|
||||
switch comp.Category {
|
||||
case "SCM":
|
||||
deviceInfo.FirmwareVersion = comp.SoftwareVersion
|
||||
deviceInfo.DeviceSerialNumber = comp.SerialNumber
|
||||
} else if comp.Category == "PackagedProduct" {
|
||||
case "PackagedProduct":
|
||||
deviceInfo.ProductSerialNumber = comp.SerialNumber
|
||||
}
|
||||
}
|
||||
@@ -130,14 +133,17 @@ func (ds *DataStore) ListAllDevices() ([]models.ServiceDeviceInfo, error) {
|
||||
}
|
||||
|
||||
devicesDir := filepath.Join(dir, acc.Name(), constants.DevicesDir)
|
||||
|
||||
deviceEntries, err := os.ReadDir(devicesDir)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, dev := range deviceEntries {
|
||||
var info *models.ServiceDeviceInfo
|
||||
var err error
|
||||
var (
|
||||
info *models.ServiceDeviceInfo
|
||||
err error
|
||||
)
|
||||
|
||||
if !dev.IsDir() {
|
||||
if dev.Name() == constants.DeviceInfoFile {
|
||||
@@ -156,6 +162,7 @@ func (ds *DataStore) ListAllDevices() ([]models.ServiceDeviceInfo, error) {
|
||||
if key == "" {
|
||||
key = info.IPAddress
|
||||
}
|
||||
|
||||
if !seenIDs[key] {
|
||||
devices = append(devices, *info)
|
||||
seenIDs[key] = true
|
||||
@@ -202,10 +209,11 @@ func (ds *DataStore) parseDeviceInfoFile(path string) (*models.ServiceDeviceInfo
|
||||
}
|
||||
|
||||
for _, comp := range info.Components {
|
||||
if comp.Category == "SCM" {
|
||||
switch comp.Category {
|
||||
case "SCM":
|
||||
deviceInfo.FirmwareVersion = comp.SoftwareVersion
|
||||
deviceInfo.DeviceSerialNumber = comp.SerialNumber
|
||||
} else if comp.Category == "PackagedProduct" {
|
||||
case "PackagedProduct":
|
||||
deviceInfo.ProductSerialNumber = comp.SerialNumber
|
||||
}
|
||||
}
|
||||
@@ -221,6 +229,7 @@ func (ds *DataStore) parseDeviceInfoFile(path string) (*models.ServiceDeviceInfo
|
||||
|
||||
func (ds *DataStore) GetPresets(account string) ([]models.ServicePreset, error) {
|
||||
path := filepath.Join(ds.AccountDir(account), constants.PresetsFile)
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -292,8 +301,10 @@ func (ds *DataStore) SavePresets(account string, presets []models.ServicePreset)
|
||||
}
|
||||
|
||||
var px PresetsXML
|
||||
|
||||
for _, p := range presets {
|
||||
var pxml PresetXML
|
||||
|
||||
pxml.ID = p.ID
|
||||
pxml.CreatedOn = p.CreatedOn
|
||||
pxml.UpdatedOn = p.UpdatedOn
|
||||
@@ -313,11 +324,13 @@ func (ds *DataStore) SavePresets(account string, presets []models.ServicePreset)
|
||||
}
|
||||
|
||||
header := []byte(xml.Header)
|
||||
|
||||
return os.WriteFile(path, append(header, data...), 0644)
|
||||
}
|
||||
|
||||
func (ds *DataStore) GetRecents(account string) ([]models.ServiceRecent, error) {
|
||||
path := filepath.Join(ds.AccountDir(account), constants.RecentsFile)
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -389,8 +402,10 @@ func (ds *DataStore) SaveRecents(account string, recents []models.ServiceRecent)
|
||||
}
|
||||
|
||||
var rx RecentsXML
|
||||
|
||||
for _, r := range recents {
|
||||
var rxml RecentXML
|
||||
|
||||
rxml.ID = r.ID
|
||||
rxml.DeviceID = r.DeviceID
|
||||
rxml.UtcTime = r.UtcTime
|
||||
@@ -398,10 +413,12 @@ func (ds *DataStore) SaveRecents(account string, recents []models.ServiceRecent)
|
||||
rxml.ContentItem.Type = r.Type
|
||||
rxml.ContentItem.Location = r.Location
|
||||
rxml.ContentItem.SourceAccount = r.SourceAccount
|
||||
|
||||
rxml.ContentItem.IsPresetable = r.IsPresetable
|
||||
if rxml.ContentItem.IsPresetable == "" {
|
||||
rxml.ContentItem.IsPresetable = "true"
|
||||
}
|
||||
|
||||
rxml.ContentItem.ItemName = r.Name
|
||||
rxml.ContentItem.ContainerArt = r.ContainerArt
|
||||
rx.Recents = append(rx.Recents, rxml)
|
||||
@@ -413,6 +430,7 @@ func (ds *DataStore) SaveRecents(account string, recents []models.ServiceRecent)
|
||||
}
|
||||
|
||||
header := []byte(xml.Header)
|
||||
|
||||
return os.WriteFile(path, append(header, data...), 0644)
|
||||
}
|
||||
|
||||
@@ -420,10 +438,12 @@ func (ds *DataStore) SaveDeviceInfo(account string, device string, info *models.
|
||||
if device == "" {
|
||||
return fmt.Errorf("device ID/name cannot be empty")
|
||||
}
|
||||
|
||||
dir := ds.AccountDeviceDir(account, device)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
path := filepath.Join(dir, constants.DeviceInfoFile)
|
||||
|
||||
type ComponentXML struct {
|
||||
@@ -451,10 +471,12 @@ func (ds *DataStore) SaveDeviceInfo(account string, device string, info *models.
|
||||
// Python: f"{type} {module_type}"
|
||||
devType := info.ProductCode
|
||||
moduleType := ""
|
||||
|
||||
for i := 0; i < len(info.ProductCode); i++ {
|
||||
if info.ProductCode[i] == ' ' {
|
||||
devType = info.ProductCode[:i]
|
||||
moduleType = info.ProductCode[i+1:]
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -489,6 +511,7 @@ func (ds *DataStore) SaveDeviceInfo(account string, device string, info *models.
|
||||
}
|
||||
|
||||
header := []byte(xml.Header)
|
||||
|
||||
return os.WriteFile(path, append(header, data...), 0644)
|
||||
}
|
||||
|
||||
@@ -499,6 +522,7 @@ func (ds *DataStore) RemoveDevice(account string, device string) error {
|
||||
|
||||
func (ds *DataStore) GetConfiguredSources(account string) ([]models.ConfiguredSource, error) {
|
||||
path := filepath.Join(ds.AccountDir(account), constants.SourcesFile)
|
||||
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -522,13 +546,16 @@ func (ds *DataStore) GetConfiguredSources(account string) ([]models.ConfiguredSo
|
||||
}
|
||||
|
||||
var sources []models.ConfiguredSource
|
||||
|
||||
lastID := 100001
|
||||
|
||||
for _, s := range sourcesWrap.Sources {
|
||||
id := s.ID
|
||||
if id == "" {
|
||||
id = strconv.Itoa(lastID)
|
||||
lastID++
|
||||
}
|
||||
|
||||
sources = append(sources, models.ConfiguredSource{
|
||||
DisplayName: s.DisplayName,
|
||||
ID: id,
|
||||
@@ -544,7 +571,9 @@ func (ds *DataStore) GetConfiguredSources(account string) ([]models.ConfiguredSo
|
||||
|
||||
func (ds *DataStore) SaveConfiguredSources(account string, sources []models.ConfiguredSource) error {
|
||||
path := filepath.Join(ds.AccountDir(account), constants.SourcesFile)
|
||||
os.MkdirAll(filepath.Dir(path), 0755)
|
||||
if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
type sourceXML struct {
|
||||
DisplayName string `xml:"displayName,attr"`
|
||||
@@ -563,6 +592,7 @@ func (ds *DataStore) SaveConfiguredSources(account string, sources []models.Conf
|
||||
}
|
||||
|
||||
wrap := sourcesWrap{}
|
||||
|
||||
for _, s := range sources {
|
||||
sx := sourceXML{
|
||||
DisplayName: s.DisplayName,
|
||||
@@ -581,6 +611,7 @@ func (ds *DataStore) SaveConfiguredSources(account string, sources []models.Conf
|
||||
}
|
||||
|
||||
header := []byte(xml.Header)
|
||||
|
||||
return os.WriteFile(path, append(header, data...), 0644)
|
||||
}
|
||||
|
||||
@@ -606,28 +637,34 @@ func (ds *DataStore) Initialize() error {
|
||||
|
||||
func (ds *DataStore) GetETagForPresets(account string) int64 {
|
||||
path := filepath.Join(ds.AccountDir(account), constants.PresetsFile)
|
||||
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return info.ModTime().UnixNano() / int64(time.Millisecond)
|
||||
}
|
||||
|
||||
func (ds *DataStore) GetETagForSources(account string) int64 {
|
||||
path := filepath.Join(ds.AccountDir(account), constants.SourcesFile)
|
||||
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return info.ModTime().UnixNano() / int64(time.Millisecond)
|
||||
}
|
||||
|
||||
func (ds *DataStore) GetETagForRecents(account string) int64 {
|
||||
path := filepath.Join(ds.AccountDir(account), constants.RecentsFile)
|
||||
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return info.ModTime().UnixNano() / int64(time.Millisecond)
|
||||
}
|
||||
|
||||
@@ -635,37 +672,50 @@ func (ds *DataStore) GetETagForAccount(account string) int64 {
|
||||
e1 := ds.GetETagForPresets(account)
|
||||
e2 := ds.GetETagForSources(account)
|
||||
e3 := ds.GetETagForRecents(account)
|
||||
|
||||
max := e1
|
||||
if e2 > max {
|
||||
max = e2
|
||||
}
|
||||
|
||||
if e3 > max {
|
||||
max = e3
|
||||
}
|
||||
|
||||
return max
|
||||
}
|
||||
|
||||
func (ds *DataStore) SaveUsageStats(stats models.UsageStats) error {
|
||||
dir := filepath.Join(ds.DataDir, "stats", "usage")
|
||||
os.MkdirAll(dir, 0755)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
filename := fmt.Sprintf("%d_%s.json", time.Now().UnixNano(), stats.DeviceID)
|
||||
path := filepath.Join(dir, filename)
|
||||
|
||||
data, err := json.MarshalIndent(stats, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(path, data, 0644)
|
||||
}
|
||||
|
||||
func (ds *DataStore) SaveErrorStats(stats models.ErrorStats) error {
|
||||
dir := filepath.Join(ds.DataDir, "stats", "error")
|
||||
os.MkdirAll(dir, 0755)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
filename := fmt.Sprintf("%d_%s.json", time.Now().UnixNano(), stats.DeviceID)
|
||||
path := filepath.Join(dir, filename)
|
||||
|
||||
data, err := json.MarshalIndent(stats, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return os.WriteFile(path, data, 0644)
|
||||
}
|
||||
|
||||
@@ -680,6 +730,7 @@ func (ds *DataStore) AddDeviceEvent(deviceID string, event models.DeviceEvent) {
|
||||
if len(events) > 100 {
|
||||
events = events[len(events)-100:]
|
||||
}
|
||||
|
||||
ds.deviceEvents[deviceID] = events
|
||||
}
|
||||
|
||||
@@ -695,5 +746,6 @@ func (ds *DataStore) GetDeviceEvents(deviceID string) []models.DeviceEvent {
|
||||
// Return a copy to avoid race conditions if the caller modifies it
|
||||
copiedEvents := make([]models.DeviceEvent, len(events))
|
||||
copy(copiedEvents, events)
|
||||
|
||||
return copiedEvents
|
||||
}
|
||||
|
||||
@@ -13,7 +13,8 @@ func TestDataStore(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
defer func() { _ = os.RemoveAll(tempDir) }()
|
||||
|
||||
ds := NewDataStore(tempDir)
|
||||
account := "test-account"
|
||||
@@ -24,6 +25,7 @@ func TestDataStore(t *testing.T) {
|
||||
DeviceID: device,
|
||||
Name: "Test Speaker",
|
||||
}
|
||||
|
||||
err = ds.SaveDeviceInfo(account, device, info)
|
||||
if err != nil {
|
||||
t.Errorf("SaveDeviceInfo failed: %v", err)
|
||||
@@ -33,6 +35,7 @@ func TestDataStore(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
@@ -45,6 +48,7 @@ func TestDataStore(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err = ds.SavePresets(account, presets)
|
||||
if err != nil {
|
||||
t.Errorf("SavePresets failed: %v", err)
|
||||
@@ -54,7 +58,8 @@ func TestDataStore(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Errorf("GetPresets failed: %v", err)
|
||||
}
|
||||
if len(loadedPresets) != 1 || loadedPresets[0].ServiceContentItem.Name != "Preset 1" {
|
||||
|
||||
if len(loadedPresets) != 1 || loadedPresets[0].Name != "Preset 1" {
|
||||
t.Errorf("Unexpected presets: %+v", loadedPresets)
|
||||
}
|
||||
|
||||
@@ -66,6 +71,7 @@ func TestDataStore(t *testing.T) {
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
err = ds.SaveRecents(account, recents)
|
||||
if err != nil {
|
||||
t.Errorf("SaveRecents failed: %v", err)
|
||||
@@ -75,7 +81,8 @@ func TestDataStore(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Errorf("GetRecents failed: %v", err)
|
||||
}
|
||||
if len(loadedRecents) != 1 || loadedRecents[0].ServiceContentItem.Name != "Recent 1" {
|
||||
|
||||
if len(loadedRecents) != 1 || loadedRecents[0].Name != "Recent 1" {
|
||||
t.Errorf("Unexpected recents: %+v", loadedRecents)
|
||||
}
|
||||
|
||||
@@ -91,29 +98,35 @@ func TestListAllDevices_Empty(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
defer func() { _ = os.RemoveAll(tempDir) }()
|
||||
|
||||
ds := NewDataStore(tempDir)
|
||||
|
||||
// Case 1: DataDir does not exist
|
||||
os.RemoveAll(tempDir)
|
||||
_ = 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)
|
||||
_ = 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))
|
||||
}
|
||||
@@ -124,7 +137,8 @@ func TestListAllDevices(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
defer func() { _ = os.RemoveAll(tempDir) }()
|
||||
|
||||
ds := NewDataStore(tempDir)
|
||||
account := "default"
|
||||
@@ -163,7 +177,8 @@ func TestListAllDevices_EmptyDeviceID(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
defer func() { _ = os.RemoveAll(tempDir) }()
|
||||
|
||||
ds := NewDataStore(tempDir)
|
||||
account := "default"
|
||||
@@ -179,6 +194,7 @@ func TestListAllDevices_EmptyDeviceID(t *testing.T) {
|
||||
if key == "" {
|
||||
key = "127.0.0.1"
|
||||
}
|
||||
|
||||
err = ds.SaveDeviceInfo(account, key, info)
|
||||
if err != nil {
|
||||
t.Fatalf("SaveDeviceInfo failed: %v", err)
|
||||
@@ -203,7 +219,8 @@ func TestListAllDevices_MultipleEmptyIDs(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
defer func() { _ = os.RemoveAll(tempDir) }()
|
||||
|
||||
ds := NewDataStore(tempDir)
|
||||
account := "default"
|
||||
@@ -225,6 +242,7 @@ func TestListAllDevices_MultipleEmptyIDs(t *testing.T) {
|
||||
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)
|
||||
@@ -245,15 +263,16 @@ func TestListAllDevices_MalformedXML(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
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)
|
||||
_ = os.MkdirAll(dir, 0755)
|
||||
_ = os.WriteFile(filepath.Join(dir, "DeviceInfo.xml"), []byte("<info>not even closed"), 0644)
|
||||
|
||||
devices, err := ds.ListAllDevices()
|
||||
if err != nil {
|
||||
@@ -267,7 +286,9 @@ func TestListAllDevices_MalformedXML(t *testing.T) {
|
||||
|
||||
func TestConfiguredSources(t *testing.T) {
|
||||
tempDir, _ := os.MkdirTemp("", "datastore-sources-*")
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
defer func() { _ = os.RemoveAll(tempDir) }()
|
||||
|
||||
ds := NewDataStore(tempDir)
|
||||
account := "test-acc"
|
||||
|
||||
@@ -321,14 +342,17 @@ func TestConfiguredSources(t *testing.T) {
|
||||
SourceKeyAccount: "user3",
|
||||
},
|
||||
}
|
||||
|
||||
err = ds.SaveConfiguredSources(account, sources2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
loadedSources2, err := ds.GetConfiguredSources(account)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if loadedSources2[0].ID == "" {
|
||||
t.Error("Expected auto-assigned ID for source with empty ID")
|
||||
}
|
||||
|
||||
@@ -21,50 +21,58 @@ func (s *Server) HandleBMXRegistry(w http.ResponseWriter, r *http.Request) {
|
||||
content = strings.ReplaceAll(content, "{MEDIA_SERVER}", baseURL+"/media")
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.Write([]byte(content))
|
||||
_, _ = w.Write([]byte(content))
|
||||
}
|
||||
|
||||
func (s *Server) HandleTuneInPlayback(w http.ResponseWriter, r *http.Request) {
|
||||
stationID := chi.URLParam(r, "stationID")
|
||||
|
||||
resp, err := bmx.TuneInPlayback(stationID)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
_ = json.NewEncoder(w).Encode(resp)
|
||||
}
|
||||
|
||||
func (s *Server) HandleTuneInPodcastInfo(w http.ResponseWriter, r *http.Request) {
|
||||
podcastID := chi.URLParam(r, "podcastID")
|
||||
encodedName := r.URL.Query().Get("encoded_name")
|
||||
|
||||
resp, err := bmx.TuneInPodcastInfo(podcastID, encodedName)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
_ = json.NewEncoder(w).Encode(resp)
|
||||
}
|
||||
|
||||
func (s *Server) HandleTuneInPlaybackPodcast(w http.ResponseWriter, r *http.Request) {
|
||||
podcastID := chi.URLParam(r, "podcastID")
|
||||
|
||||
resp, err := bmx.TuneInPlaybackPodcast(podcastID)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
_ = json.NewEncoder(w).Encode(resp)
|
||||
}
|
||||
|
||||
func (s *Server) HandleOrionPlayback(w http.ResponseWriter, r *http.Request) {
|
||||
data := chi.URLParam(r, "data")
|
||||
|
||||
resp, err := bmx.PlayCustomStream(data)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
_ = json.NewEncoder(w).Encode(resp)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
|
||||
func TestBMXServices(t *testing.T) {
|
||||
r, _ := setupRouter("http://localhost:8001", nil)
|
||||
|
||||
ts := httptest.NewServer(r)
|
||||
defer ts.Close()
|
||||
|
||||
@@ -18,13 +19,15 @@ func TestBMXServices(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
defer func() { _ = res.Body.Close() }()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
t.Errorf("Expected status OK, got %v", res.Status)
|
||||
}
|
||||
|
||||
body, _ := io.ReadAll(res.Body)
|
||||
|
||||
var response map[string]interface{}
|
||||
if err := json.Unmarshal(body, &response); err != nil {
|
||||
t.Fatalf("Failed to unmarshal response: %v", err)
|
||||
@@ -39,6 +42,7 @@ func TestBMXServices(t *testing.T) {
|
||||
if strings.Contains(bodyStr, "{BMX_SERVER}") {
|
||||
t.Error("Response still contains {BMX_SERVER} placeholder")
|
||||
}
|
||||
|
||||
if strings.Contains(bodyStr, "{MEDIA_SERVER}") {
|
||||
t.Error("Response still contains {MEDIA_SERVER} placeholder")
|
||||
}
|
||||
@@ -46,24 +50,29 @@ func TestBMXServices(t *testing.T) {
|
||||
|
||||
func TestOrionPlayback(t *testing.T) {
|
||||
r, _ := setupRouter("http://localhost:8001", nil)
|
||||
|
||||
ts := httptest.NewServer(r)
|
||||
defer ts.Close()
|
||||
|
||||
// Base64 encoded: {"streamUrl": "http://example.com/stream", "imageUrl": "http://example.com/img.jpg", "name": "Test Orion"}
|
||||
data := "eyJzdHJlYW1VcmwiOiAiaHR0cDovL2V4YW1wbGUuY29tL3N0cmVhbSIsICJpbWFnZVVybCI6ICJodHRwOi8vZXhhbXBsZS5jb20vaW1nLmpwZyIsICJuYW1lIjogIlRlc3QgT3Jpb24ifQ=="
|
||||
|
||||
res, err := http.Post(ts.URL+"/bmx/orion/v1/playback/station/"+data, "application/json", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
defer func() { _ = res.Body.Close() }()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
t.Errorf("Expected status OK, got %v", res.Status)
|
||||
}
|
||||
|
||||
body, _ := io.ReadAll(res.Body)
|
||||
|
||||
var resp map[string]interface{}
|
||||
json.Unmarshal(body, &resp)
|
||||
|
||||
_ = json.Unmarshal(body, &resp)
|
||||
|
||||
if resp["name"] != "Test Orion" {
|
||||
t.Errorf("Expected name Test Orion, got %v", resp["name"])
|
||||
|
||||
@@ -14,25 +14,30 @@ import (
|
||||
|
||||
func TestMargeETags(t *testing.T) {
|
||||
tempDir, _ := os.MkdirTemp("", "soundcork-etag-test-*")
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
defer func() { _ = os.RemoveAll(tempDir) }()
|
||||
|
||||
ds := datastore.NewDataStore(tempDir)
|
||||
|
||||
account := "12345"
|
||||
accountDir := filepath.Join(tempDir, account)
|
||||
os.MkdirAll(accountDir, 0755)
|
||||
_ = os.MkdirAll(accountDir, 0755)
|
||||
|
||||
// Create some initial data
|
||||
presetsFile := filepath.Join(accountDir, "Presets.xml")
|
||||
os.WriteFile(presetsFile, []byte("<presets/>"), 0644)
|
||||
_ = os.WriteFile(presetsFile, []byte("<presets/>"), 0644)
|
||||
|
||||
sourcesFile := filepath.Join(accountDir, "Sources.xml")
|
||||
os.WriteFile(sourcesFile, []byte("<sources/>"), 0644)
|
||||
_ = os.WriteFile(sourcesFile, []byte("<sources/>"), 0644)
|
||||
|
||||
recentsFile := filepath.Join(accountDir, "Recents.xml")
|
||||
os.WriteFile(recentsFile, []byte("<recents/>"), 0644)
|
||||
_ = os.WriteFile(recentsFile, []byte("<recents/>"), 0644)
|
||||
|
||||
// Ensure devices directory exists for AccountFull
|
||||
os.MkdirAll(ds.AccountDevicesDir(account), 0755)
|
||||
_ = os.MkdirAll(ds.AccountDevicesDir(account), 0755)
|
||||
|
||||
r, _ := setupRouter("http://localhost:8001", ds)
|
||||
|
||||
ts := httptest.NewServer(r)
|
||||
defer ts.Close()
|
||||
|
||||
@@ -42,8 +47,9 @@ func TestMargeETags(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
etag := res.Header.Get("ETag")
|
||||
res.Body.Close()
|
||||
_ = res.Body.Close()
|
||||
|
||||
if etag == "" {
|
||||
t.Fatal("Expected ETag header, got none")
|
||||
@@ -52,11 +58,13 @@ func TestMargeETags(t *testing.T) {
|
||||
// Second request with If-None-Match
|
||||
req, _ := http.NewRequest("GET", ts.URL+"/marge/accounts/"+account+"/devices/DEV1/presets", nil)
|
||||
req.Header.Set("If-None-Match", etag)
|
||||
|
||||
res2, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer res2.Body.Close()
|
||||
|
||||
defer func() { _ = res2.Body.Close() }()
|
||||
|
||||
if res2.StatusCode != http.StatusNotModified {
|
||||
t.Errorf("Expected 304 Not Modified, got %v", res2.Status)
|
||||
@@ -68,8 +76,9 @@ func TestMargeETags(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
etag := res.Header.Get("ETag")
|
||||
res.Body.Close()
|
||||
_ = res.Body.Close()
|
||||
|
||||
if etag == "" {
|
||||
t.Fatal("Expected ETag header, got none")
|
||||
@@ -77,11 +86,13 @@ func TestMargeETags(t *testing.T) {
|
||||
|
||||
req, _ := http.NewRequest("GET", ts.URL+"/marge/accounts/"+account+"/full", nil)
|
||||
req.Header.Set("If-None-Match", etag)
|
||||
|
||||
res2, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer res2.Body.Close()
|
||||
|
||||
defer func() { _ = res2.Body.Close() }()
|
||||
|
||||
if res2.StatusCode != http.StatusNotModified {
|
||||
t.Errorf("Expected 304 Not Modified, got %v", res2.Status)
|
||||
@@ -93,16 +104,19 @@ func TestMargeETags(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
etag := res.Header.Get("ETag")
|
||||
res.Body.Close()
|
||||
_ = res.Body.Close()
|
||||
|
||||
req, _ := http.NewRequest("GET", ts.URL+"/marge/streaming/sourceproviders", nil)
|
||||
req.Header.Set("If-None-Match", etag)
|
||||
|
||||
res2, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer res2.Body.Close()
|
||||
|
||||
defer func() { _ = res2.Body.Close() }()
|
||||
|
||||
// For SourceProviders, we currently use time.Now(), so this might fail if it crosses a millisecond boundary.
|
||||
// In a real scenario, this would likely be stable during a single SoundTouch session's refresh.
|
||||
@@ -116,8 +130,9 @@ func TestMargeETags(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
etag := res.Header.Get("ETag")
|
||||
res.Body.Close()
|
||||
_ = res.Body.Close()
|
||||
|
||||
if etag == "" {
|
||||
t.Fatal("Expected ETag header for swupdate")
|
||||
@@ -125,11 +140,13 @@ func TestMargeETags(t *testing.T) {
|
||||
|
||||
req, _ := http.NewRequest("GET", ts.URL+"/marge/updates/soundtouch", nil)
|
||||
req.Header.Set("If-None-Match", etag)
|
||||
|
||||
res2, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer res2.Body.Close()
|
||||
|
||||
defer func() { _ = res2.Body.Close() }()
|
||||
|
||||
if res2.StatusCode != http.StatusNotModified {
|
||||
t.Errorf("Expected 304 Not Modified for swupdate, got %v", res2.Status)
|
||||
@@ -139,11 +156,13 @@ func TestMargeETags(t *testing.T) {
|
||||
t.Run("Negative ETag Test", func(t *testing.T) {
|
||||
req, _ := http.NewRequest("GET", ts.URL+"/marge/accounts/"+account+"/full", nil)
|
||||
req.Header.Set("If-None-Match", "wrong-etag")
|
||||
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
defer func() { _ = res.Body.Close() }()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
t.Errorf("Expected 200 OK for wrong ETag, got %v", res.Status)
|
||||
@@ -158,6 +177,7 @@ func TestMargeETags(t *testing.T) {
|
||||
t.Logf("Recorder Headers: %v", w.Header())
|
||||
|
||||
found := false
|
||||
|
||||
for k := range w.Header() {
|
||||
if k == "ETag" {
|
||||
found = true
|
||||
@@ -175,7 +195,7 @@ func TestMargeETags(t *testing.T) {
|
||||
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header()["etag"] = []string{"backend-etag"}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("<xml/>"))
|
||||
_, _ = w.Write([]byte("<xml/>"))
|
||||
}))
|
||||
defer backend.Close()
|
||||
|
||||
@@ -188,6 +208,7 @@ func TestMargeETags(t *testing.T) {
|
||||
delete(res.Header, "Etag")
|
||||
res.Header["ETag"] = etags
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -196,9 +217,9 @@ func TestMargeETags(t *testing.T) {
|
||||
Header: make(http.Header),
|
||||
}
|
||||
resp.Header["Etag"] = []string{"test-etag"}
|
||||
pyProxy.ModifyResponse(resp)
|
||||
_ = pyProxy.ModifyResponse(resp)
|
||||
|
||||
if _, ok := resp.Header["ETag"]; !ok {
|
||||
if _, ok := resp.Header["Etag"]; !ok {
|
||||
t.Errorf("ModifyResponse did not normalize ETag casing. Headers: %v", resp.Header)
|
||||
}
|
||||
|
||||
@@ -214,12 +235,14 @@ func TestMargeETags(t *testing.T) {
|
||||
w.Header()["X-BOSE-TOKEN"] = []string{"token"}
|
||||
|
||||
found := false
|
||||
|
||||
for k := range w.Header() {
|
||||
if k == "X-BOSE-TOKEN" {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
t.Errorf("Expected exact 'X-BOSE-TOKEN' header in recorder, but it was normalized: %v", w.Header())
|
||||
}
|
||||
@@ -231,10 +254,12 @@ func TestMargeETags(t *testing.T) {
|
||||
|
||||
// 1. Set canonicalizes to "Etag" (Standard Go behavior)
|
||||
h.Set("ETag", "v1")
|
||||
|
||||
if _, ok := h["Etag"]; !ok {
|
||||
t.Errorf("Expected key 'Etag' in map after Set('ETag'), but got: %v", h)
|
||||
}
|
||||
if _, ok := h["ETag"]; ok {
|
||||
|
||||
if _, ok := h["Etag"]; ok {
|
||||
// In Go's map, "ETag" and "Etag" are different keys.
|
||||
// Set() uses CanonicalHeaderKey which produces "Etag" (lowercase 't').
|
||||
t.Errorf("Did not expect exact key 'ETag' in map after Set('ETag') because Go canonicalizes to 'Etag'")
|
||||
|
||||
@@ -15,11 +15,11 @@ func (s *Server) HandleGetDeviceEvents(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
events := s.ds.GetDeviceEvents(deviceID)
|
||||
|
||||
|
||||
resp := map[string]interface{}{
|
||||
"events": events,
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
_ = json.NewEncoder(w).Encode(resp)
|
||||
}
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/gesellix/bose-soundtouch/pkg/service/datastore"
|
||||
"github.com/gesellix/bose-soundtouch/pkg/models"
|
||||
"github.com/gesellix/bose-soundtouch/pkg/service/datastore"
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
@@ -47,7 +47,9 @@ func TestEventLog(t *testing.T) {
|
||||
var resp struct {
|
||||
Events []models.DeviceEvent `json:"events"`
|
||||
}
|
||||
json.NewDecoder(w.Body).Decode(&resp)
|
||||
if err := json.NewDecoder(w.Body).Decode(&resp); err != nil {
|
||||
t.Fatalf("Failed to decode response: %v", err)
|
||||
}
|
||||
|
||||
if len(resp.Events) != 1 {
|
||||
t.Fatalf("Expected 1 event, got %d", len(resp.Events))
|
||||
|
||||
@@ -17,6 +17,7 @@ func (s *Server) HandleHealth(w http.ResponseWriter, r *http.Request) {
|
||||
if info.Main.Version != "" && info.Main.Version != "(devel)" {
|
||||
version = info.Main.Version
|
||||
}
|
||||
|
||||
for _, setting := range info.Settings {
|
||||
switch setting.Key {
|
||||
case "vcs.revision":
|
||||
@@ -37,14 +38,16 @@ func (s *Server) HandleHealth(w http.ResponseWriter, r *http.Request) {
|
||||
if vcsRevision != "" {
|
||||
status["vcs_revision"] = vcsRevision
|
||||
}
|
||||
|
||||
if vcsTime != "" {
|
||||
status["vcs_time"] = vcsTime
|
||||
}
|
||||
|
||||
if vcsModified != "" {
|
||||
status["vcs_modified"] = vcsModified
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(status)
|
||||
_ = json.NewEncoder(w).Encode(status)
|
||||
}
|
||||
|
||||
@@ -30,11 +30,13 @@ func TestHealthEndpoint(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
defer func() { _ = res.Body.Close() }()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
t.Fatalf("expected 200 OK, got %v", res.Status)
|
||||
}
|
||||
|
||||
if ct := res.Header.Get("Content-Type"); ct != "application/json" {
|
||||
t.Fatalf("expected application/json content type, got %s", ct)
|
||||
}
|
||||
@@ -43,12 +45,15 @@ func TestHealthEndpoint(t *testing.T) {
|
||||
if err := json.NewDecoder(res.Body).Decode(&hr); err != nil {
|
||||
t.Fatalf("failed to decode health response: %v", err)
|
||||
}
|
||||
|
||||
if hr.Status != "up" {
|
||||
t.Fatalf("expected status 'up', got %q", hr.Status)
|
||||
}
|
||||
|
||||
if hr.Timestamp == "" {
|
||||
t.Error("expected non-empty timestamp")
|
||||
}
|
||||
|
||||
if hr.Version == "" {
|
||||
t.Error("expected non-empty version")
|
||||
}
|
||||
|
||||
@@ -25,13 +25,15 @@ func (s *Server) HandleMargeSourceProviders(w http.ResponseWriter, r *http.Reque
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/xml")
|
||||
w.Header()["ETag"] = []string{etag}
|
||||
w.Write(data)
|
||||
_, _ = w.Write(data)
|
||||
}
|
||||
|
||||
func (s *Server) HandleMargeAccountFull(w http.ResponseWriter, r *http.Request) {
|
||||
account := chi.URLParam(r, "account")
|
||||
|
||||
etag := strconv.FormatInt(s.ds.GetETagForAccount(account), 10)
|
||||
if r.Header.Get("If-None-Match") == etag {
|
||||
w.WriteHeader(http.StatusNotModified)
|
||||
@@ -43,9 +45,10 @@ func (s *Server) HandleMargeAccountFull(w http.ResponseWriter, r *http.Request)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/xml")
|
||||
w.Header()["ETag"] = []string{etag}
|
||||
w.Write(data)
|
||||
_, _ = w.Write(data)
|
||||
}
|
||||
|
||||
func (s *Server) HandleMargePowerOn(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -63,14 +66,15 @@ func (s *Server) HandleMargeSoftwareUpdate(w http.ResponseWriter, r *http.Reques
|
||||
w.Header()["ETag"] = []string{etag}
|
||||
|
||||
if len(swUpdateXML) > 0 {
|
||||
w.Write(swUpdateXML)
|
||||
_, _ = w.Write(swUpdateXML)
|
||||
} else {
|
||||
w.Write([]byte(marge.SoftwareUpdateToXML()))
|
||||
_, _ = w.Write([]byte(marge.SoftwareUpdateToXML()))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) HandleMargePresets(w http.ResponseWriter, r *http.Request) {
|
||||
account := chi.URLParam(r, "account")
|
||||
|
||||
etag := strconv.FormatInt(s.ds.GetETagForPresets(account), 10)
|
||||
if r.Header.Get("If-None-Match") == etag {
|
||||
w.WriteHeader(http.StatusNotModified)
|
||||
@@ -82,9 +86,10 @@ func (s *Server) HandleMargePresets(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/xml")
|
||||
w.Header()["ETag"] = []string{etag}
|
||||
w.Write(data)
|
||||
_, _ = w.Write(data)
|
||||
}
|
||||
|
||||
func (s *Server) HandleMargeUpdatePreset(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -95,23 +100,27 @@ func (s *Server) HandleMargeUpdatePreset(w http.ResponseWriter, r *http.Request)
|
||||
w.Header()["ETag"] = []string{etag}
|
||||
|
||||
presetNumberStr := chi.URLParam(r, "presetNumber")
|
||||
|
||||
presetNumber, err := strconv.Atoi(presetNumberStr)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid preset number", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to read body", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
data, err := marge.UpdatePreset(s.ds, account, device, presetNumber, body)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/xml")
|
||||
w.Write(data)
|
||||
_, _ = w.Write(data)
|
||||
}
|
||||
|
||||
func (s *Server) HandleMargeAddRecent(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -126,46 +135,54 @@ func (s *Server) HandleMargeAddRecent(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "Failed to read body", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
data, err := marge.AddRecent(s.ds, account, device, body)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/xml")
|
||||
w.Write(data)
|
||||
_, _ = w.Write(data)
|
||||
}
|
||||
|
||||
func (s *Server) HandleMargeAddDevice(w http.ResponseWriter, r *http.Request) {
|
||||
account := chi.URLParam(r, "account")
|
||||
|
||||
body, err := io.ReadAll(r.Body)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to read body", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
data, err := marge.AddDeviceToAccount(s.ds, account, body)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/xml")
|
||||
w.Write(data)
|
||||
_, _ = w.Write(data)
|
||||
}
|
||||
|
||||
func (s *Server) HandleMargeRemoveDevice(w http.ResponseWriter, r *http.Request) {
|
||||
account := chi.URLParam(r, "account")
|
||||
|
||||
device := chi.URLParam(r, "device")
|
||||
if err := marge.RemoveDeviceFromAccount(s.ds, account, device); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`{"ok": true}`))
|
||||
_, _ = w.Write([]byte(`{"ok": true}`))
|
||||
}
|
||||
|
||||
func (s *Server) HandleMargeProviderSettings(w http.ResponseWriter, r *http.Request) {
|
||||
account := chi.URLParam(r, "account")
|
||||
|
||||
w.Header().Set("Content-Type", "application/xml")
|
||||
w.Write([]byte(marge.ProviderSettingsToXML(account)))
|
||||
_, _ = w.Write([]byte(marge.ProviderSettingsToXML(account)))
|
||||
}
|
||||
|
||||
func (s *Server) HandleMargeStreamingToken(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
|
||||
func TestMargeSourceProviders(t *testing.T) {
|
||||
r, _ := setupRouter("http://localhost:8001", nil)
|
||||
|
||||
ts := httptest.NewServer(r)
|
||||
defer ts.Close()
|
||||
|
||||
@@ -22,7 +23,8 @@ func TestMargeSourceProviders(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
defer func() { _ = res.Body.Close() }()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
t.Errorf("Expected status OK, got %v", res.Status)
|
||||
@@ -36,6 +38,7 @@ func TestMargeSourceProviders(t *testing.T) {
|
||||
|
||||
func TestMargeSoftwareUpdate(t *testing.T) {
|
||||
r, _ := setupRouter("http://localhost:8001", nil)
|
||||
|
||||
ts := httptest.NewServer(r)
|
||||
defer ts.Close()
|
||||
|
||||
@@ -43,7 +46,8 @@ func TestMargeSoftwareUpdate(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
defer func() { _ = res.Body.Close() }()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
t.Errorf("Expected status OK, got %v", res.Status)
|
||||
@@ -59,6 +63,7 @@ func TestMargeSoftwareUpdate(t *testing.T) {
|
||||
func TestMargeAccountFull(t *testing.T) {
|
||||
tempDir, _ := os.MkdirTemp("", "soundcork-test-*")
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
ds := datastore.NewDataStore(tempDir)
|
||||
|
||||
account := "12345"
|
||||
@@ -87,6 +92,7 @@ func TestMargeAccountFull(t *testing.T) {
|
||||
`), 0644)
|
||||
|
||||
r, _ := setupRouter("http://localhost:8001", ds)
|
||||
|
||||
ts := httptest.NewServer(r)
|
||||
defer ts.Close()
|
||||
|
||||
@@ -94,7 +100,8 @@ func TestMargeAccountFull(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
defer func() { _ = res.Body.Close() }()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
t.Errorf("Expected status OK, got %v", res.Status)
|
||||
@@ -109,6 +116,7 @@ func TestMargeAccountFull(t *testing.T) {
|
||||
func TestMargePresets(t *testing.T) {
|
||||
tempDir, _ := os.MkdirTemp("", "soundcork-test-*")
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
ds := datastore.NewDataStore(tempDir)
|
||||
|
||||
account := "12345"
|
||||
@@ -116,6 +124,7 @@ func TestMargePresets(t *testing.T) {
|
||||
os.MkdirAll(accountDir, 0755)
|
||||
|
||||
r, _ := setupRouter("http://localhost:8001", ds)
|
||||
|
||||
ts := httptest.NewServer(r)
|
||||
defer ts.Close()
|
||||
|
||||
@@ -149,7 +158,8 @@ func TestMargePresets(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
defer func() { _ = res.Body.Close() }()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
t.Errorf("Expected status OK, got %v", res.Status)
|
||||
@@ -164,6 +174,7 @@ func TestMargePresets(t *testing.T) {
|
||||
func TestMargeUpdatePreset(t *testing.T) {
|
||||
tempDir, _ := os.MkdirTemp("", "soundcork-test-*")
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
ds := datastore.NewDataStore(tempDir)
|
||||
|
||||
account := "12345"
|
||||
@@ -181,6 +192,7 @@ func TestMargeUpdatePreset(t *testing.T) {
|
||||
os.WriteFile(filepath.Join(accountDir, "Presets.xml"), []byte(`<presets></presets>`), 0644)
|
||||
|
||||
r, _ := setupRouter("http://localhost:8001", ds)
|
||||
|
||||
ts := httptest.NewServer(r)
|
||||
defer ts.Close()
|
||||
|
||||
@@ -197,7 +209,8 @@ func TestMargeUpdatePreset(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
defer func() { _ = res.Body.Close() }()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(res.Body)
|
||||
@@ -214,6 +227,7 @@ func TestMargeUpdatePreset(t *testing.T) {
|
||||
func TestMargeAddRecent(t *testing.T) {
|
||||
tempDir, _ := os.MkdirTemp("", "soundcork-test-*")
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
ds := datastore.NewDataStore(tempDir)
|
||||
|
||||
account := "12345"
|
||||
@@ -231,6 +245,7 @@ func TestMargeAddRecent(t *testing.T) {
|
||||
os.WriteFile(filepath.Join(accountDir, "Recents.xml"), []byte(`<recents></recents>`), 0644)
|
||||
|
||||
r, _ := setupRouter("http://localhost:8001", ds)
|
||||
|
||||
ts := httptest.NewServer(r)
|
||||
defer ts.Close()
|
||||
|
||||
@@ -246,7 +261,8 @@ func TestMargeAddRecent(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
defer func() { _ = res.Body.Close() }()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
t.Errorf("Expected status OK, got %v", res.Status)
|
||||
@@ -262,6 +278,7 @@ func TestMargeAddRecent(t *testing.T) {
|
||||
func TestMargeAddRemoveDevice(t *testing.T) {
|
||||
tempDir, _ := os.MkdirTemp("", "soundcork-test-*")
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
ds := datastore.NewDataStore(tempDir)
|
||||
|
||||
account := "12345"
|
||||
@@ -269,6 +286,7 @@ func TestMargeAddRemoveDevice(t *testing.T) {
|
||||
os.MkdirAll(accountDir, 0755)
|
||||
|
||||
r, _ := setupRouter("http://localhost:8001", ds)
|
||||
|
||||
ts := httptest.NewServer(r)
|
||||
defer ts.Close()
|
||||
|
||||
@@ -294,6 +312,9 @@ func TestMargeAddRemoveDevice(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
res.Body.Close()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
t.Errorf("AddDevice: Expected status OK, got %v", res.Status)
|
||||
}
|
||||
@@ -305,10 +326,14 @@ func TestMargeAddRemoveDevice(t *testing.T) {
|
||||
|
||||
// 2. Remove Device
|
||||
req, _ := http.NewRequest(http.MethodDelete, ts.URL+"/marge/accounts/"+account+"/devices/NEWDEV", nil)
|
||||
|
||||
res, err = http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
res.Body.Close()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
t.Errorf("RemoveDevice: Expected status OK, got %v", res.Status)
|
||||
}
|
||||
@@ -320,6 +345,7 @@ func TestMargeAddRemoveDevice(t *testing.T) {
|
||||
|
||||
func TestMargePowerOn(t *testing.T) {
|
||||
r, _ := setupRouter("http://localhost:8001", nil)
|
||||
|
||||
ts := httptest.NewServer(r)
|
||||
defer ts.Close()
|
||||
|
||||
@@ -327,7 +353,8 @@ func TestMargePowerOn(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
defer func() { _ = res.Body.Close() }()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
t.Errorf("Expected status OK, got %v", res.Status)
|
||||
@@ -337,9 +364,11 @@ func TestMargePowerOn(t *testing.T) {
|
||||
func TestMargeAdvancedFeatures(t *testing.T) {
|
||||
tempDir, _ := os.MkdirTemp("", "soundcork-test-*")
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
ds := datastore.NewDataStore(tempDir)
|
||||
|
||||
r, _ := setupRouter("http://localhost:8001", ds)
|
||||
|
||||
ts := httptest.NewServer(r)
|
||||
defer ts.Close()
|
||||
|
||||
@@ -348,9 +377,13 @@ func TestMargeAdvancedFeatures(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
defer func() { _ = res.Body.Close() }()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
t.Errorf("Expected status OK, got %v", res.Status)
|
||||
}
|
||||
|
||||
body, _ := io.ReadAll(res.Body)
|
||||
if !strings.Contains(string(body), "<boseId>123</boseId>") {
|
||||
t.Errorf("Response body missing account ID: %s", body)
|
||||
@@ -362,9 +395,13 @@ func TestMargeAdvancedFeatures(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
defer func() { _ = res.Body.Close() }()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
t.Errorf("Expected status OK, got %v", res.Status)
|
||||
}
|
||||
|
||||
token := res.Header.Get("Authorization")
|
||||
if !strings.HasPrefix(token, "Bearer soundcork-local-token-") {
|
||||
t.Errorf("Invalid token header: %s", token)
|
||||
@@ -388,10 +425,14 @@ func TestMargeAdvancedFeatures(t *testing.T) {
|
||||
</device-landscape>
|
||||
</diagnostic-data>
|
||||
</device-data>`
|
||||
|
||||
res, err := http.Post(ts.URL+"/marge/streaming/support/customersupport", "application/vnd.bose.streaming-v1.2+xml", strings.NewReader(payload))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
res.Body.Close()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
t.Errorf("Expected status OK, got %v", res.Status)
|
||||
}
|
||||
@@ -399,15 +440,19 @@ func TestMargeAdvancedFeatures(t *testing.T) {
|
||||
// Verify event was recorded
|
||||
events := ds.GetDeviceEvents("587A628A4042")
|
||||
found := false
|
||||
|
||||
for _, e := range events {
|
||||
if e.Type == "customer-support-upload" {
|
||||
found = true
|
||||
|
||||
if e.Data["firmware"] != "27.0.6" {
|
||||
t.Errorf("Expected firmware 27.0.6, got %v", e.Data["firmware"])
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
t.Error("Customer support event not found in event log")
|
||||
}
|
||||
|
||||
@@ -24,16 +24,18 @@ func (s *Server) HandleRoot(w http.ResponseWriter, r *http.Request) {
|
||||
accept := r.Header.Get("Accept")
|
||||
if !strings.Contains(accept, "text/html") && (strings.Contains(accept, "application/json") || accept == "*/*" || accept == "") {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprintf(w, `{"Bose": "Can't Brick Us", "service": "Go/Chi"}`)
|
||||
_, _ = fmt.Fprintf(w, `{"Bose": "Can't Brick Us", "service": "Go/Chi"}`)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.Write(indexHTML)
|
||||
_, _ = w.Write(indexHTML)
|
||||
}
|
||||
|
||||
func (s *Server) HandleMedia() http.HandlerFunc {
|
||||
subFS, _ := fs.Sub(mediaFS, "soundcork/media")
|
||||
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
fs := http.StripPrefix("/media/", http.FileServer(http.FS(subFS)))
|
||||
fs.ServeHTTP(w, r)
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
func TestRootEndpoint(t *testing.T) {
|
||||
r, _ := setupRouter("http://localhost:8001", nil)
|
||||
|
||||
ts := httptest.NewServer(r)
|
||||
defer ts.Close()
|
||||
|
||||
@@ -21,7 +22,8 @@ func TestRootEndpoint(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
defer func() { _ = res.Body.Close() }()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
t.Errorf("Expected status OK, got %v", res.Status)
|
||||
@@ -40,6 +42,7 @@ func TestRootEndpoint(t *testing.T) {
|
||||
|
||||
func TestRootEndpointJSON(t *testing.T) {
|
||||
r, _ := setupRouter("http://localhost:8001", nil)
|
||||
|
||||
ts := httptest.NewServer(r)
|
||||
defer ts.Close()
|
||||
|
||||
@@ -51,7 +54,8 @@ func TestRootEndpointJSON(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
defer func() { _ = res.Body.Close() }()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
t.Errorf("Expected status OK, got %v", res.Status)
|
||||
@@ -63,6 +67,7 @@ func TestRootEndpointJSON(t *testing.T) {
|
||||
}
|
||||
|
||||
body, _ := io.ReadAll(res.Body)
|
||||
|
||||
expected := `{"Bose": "Can't Brick Us", "service": "Go/Chi"}`
|
||||
if strings.TrimSpace(string(body)) != expected {
|
||||
t.Errorf("Expected body %s, got %s", expected, string(body))
|
||||
@@ -71,6 +76,7 @@ func TestRootEndpointJSON(t *testing.T) {
|
||||
|
||||
func TestStaticMedia(t *testing.T) {
|
||||
r, _ := setupRouter("http://localhost:8001", nil)
|
||||
|
||||
ts := httptest.NewServer(r)
|
||||
defer ts.Close()
|
||||
|
||||
@@ -79,7 +85,8 @@ func TestStaticMedia(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
defer func() { _ = res.Body.Close() }()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
t.Errorf("Expected status OK, got %v", res.Status)
|
||||
|
||||
@@ -54,6 +54,7 @@ func (s *Server) HandleProxyRequest(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
lp.LogResponse(res)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -15,23 +15,24 @@ func (s *Server) HandleListDiscoveredDevices(w http.ResponseWriter, r *http.Requ
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(devices)
|
||||
_ = json.NewEncoder(w).Encode(devices)
|
||||
}
|
||||
|
||||
func (s *Server) HandleTriggerDiscovery(w http.ResponseWriter, r *http.Request) {
|
||||
go s.DiscoverDevices()
|
||||
go s.DiscoverDevices(r.Context())
|
||||
|
||||
w.WriteHeader(http.StatusAccepted)
|
||||
w.Write([]byte(`{"status": "Discovery started"}`))
|
||||
}
|
||||
|
||||
func (s *Server) HandleGetDiscoveryStatus(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]bool{"discovering": s.discovering})
|
||||
_ = json.NewEncoder(w).Encode(map[string]bool{"discovering": s.discovering})
|
||||
}
|
||||
|
||||
func (s *Server) HandleGetSettings(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]string{
|
||||
_ = json.NewEncoder(w).Encode(map[string]string{
|
||||
"server_url": s.serverURL,
|
||||
"proxy_url": s.proxyURL,
|
||||
})
|
||||
@@ -51,7 +52,7 @@ func (s *Server) HandleGetDeviceInfo(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(info)
|
||||
_ = json.NewEncoder(w).Encode(info)
|
||||
}
|
||||
|
||||
func (s *Server) HandleGetMigrationSummary(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -65,6 +66,7 @@ func (s *Server) HandleGetMigrationSummary(w http.ResponseWriter, r *http.Reques
|
||||
proxyURL := r.URL.Query().Get("proxy_url")
|
||||
|
||||
options := make(map[string]string)
|
||||
|
||||
for k, v := range r.URL.Query() {
|
||||
if len(v) > 0 && (k == "marge" || k == "stats" || k == "sw_update" || k == "bmx") {
|
||||
options[k] = v[0]
|
||||
@@ -78,7 +80,7 @@ func (s *Server) HandleGetMigrationSummary(w http.ResponseWriter, r *http.Reques
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(summary)
|
||||
_ = json.NewEncoder(w).Encode(summary)
|
||||
}
|
||||
|
||||
func (s *Server) HandleMigrateDevice(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -86,7 +88,8 @@ func (s *Server) HandleMigrateDevice(w http.ResponseWriter, r *http.Request) {
|
||||
if deviceIP == "" {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"ok": false, "message": "Device IP is required"})
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{"ok": false, "message": "Device IP is required"})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -94,6 +97,7 @@ func (s *Server) HandleMigrateDevice(w http.ResponseWriter, r *http.Request) {
|
||||
proxyURL := r.URL.Query().Get("proxy_url")
|
||||
|
||||
options := make(map[string]string)
|
||||
|
||||
for k, v := range r.URL.Query() {
|
||||
if len(v) > 0 && (k == "marge" || k == "stats" || k == "sw_update" || k == "bmx") {
|
||||
options[k] = v[0]
|
||||
@@ -103,12 +107,13 @@ func (s *Server) HandleMigrateDevice(w http.ResponseWriter, r *http.Request) {
|
||||
if err := s.sm.MigrateSpeaker(deviceIP, targetURL, proxyURL, options); err != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"ok": false, "message": err.Error()})
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{"ok": false, "message": err.Error()})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"ok": true, "message": "Migration started"})
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{"ok": true, "message": "Migration started"})
|
||||
}
|
||||
|
||||
func (s *Server) HandleEnsureRemoteServices(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -116,19 +121,21 @@ func (s *Server) HandleEnsureRemoteServices(w http.ResponseWriter, r *http.Reque
|
||||
if deviceIP == "" {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"ok": false, "message": "Device IP is required"})
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{"ok": false, "message": "Device IP is required"})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.sm.EnsureRemoteServices(deviceIP); err != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"ok": false, "message": err.Error()})
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{"ok": false, "message": err.Error()})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"ok": true, "message": "Remote services ensured"})
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{"ok": true, "message": "Remote services ensured"})
|
||||
}
|
||||
|
||||
func (s *Server) HandleBackupConfig(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -136,24 +143,26 @@ func (s *Server) HandleBackupConfig(w http.ResponseWriter, r *http.Request) {
|
||||
if deviceIP == "" {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusBadRequest)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"ok": false, "message": "Device IP is required"})
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{"ok": false, "message": "Device IP is required"})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if err := s.sm.BackupConfig(deviceIP); err != nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"ok": false, "message": err.Error()})
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{"ok": false, "message": err.Error()})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"ok": true, "message": "Backup created"})
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{"ok": true, "message": "Backup created"})
|
||||
}
|
||||
|
||||
func (s *Server) HandleGetProxySettings(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]bool{
|
||||
_ = json.NewEncoder(w).Encode(map[string]bool{
|
||||
"redact": s.proxyRedact,
|
||||
"log_body": s.proxyLogBody,
|
||||
})
|
||||
@@ -173,5 +182,5 @@ func (s *Server) HandleUpdateProxySettings(w http.ResponseWriter, r *http.Reques
|
||||
s.proxyLogBody = settings.LogBody
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{"ok": true, "message": "Proxy settings updated"})
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{"ok": true, "message": "Proxy settings updated"})
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
func TestProxySettingsAPI(t *testing.T) {
|
||||
r, server := setupRouter("http://localhost:8001", nil)
|
||||
|
||||
ts := httptest.NewServer(r)
|
||||
defer ts.Close()
|
||||
|
||||
@@ -22,7 +23,8 @@ func TestProxySettingsAPI(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
defer func() { _ = res.Body.Close() }()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
t.Errorf("GET: Expected status OK, got %v", res.Status)
|
||||
@@ -43,11 +45,13 @@ func TestProxySettingsAPI(t *testing.T) {
|
||||
"log_body": true,
|
||||
}
|
||||
body, _ := json.Marshal(update)
|
||||
|
||||
res, err = http.Post(ts.URL+"/setup/proxy-settings", "application/json", bytes.NewBuffer(body))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer res.Body.Close()
|
||||
|
||||
defer func() { _ = res.Body.Close() }()
|
||||
|
||||
if res.StatusCode != http.StatusOK {
|
||||
t.Errorf("POST: Expected status OK, got %v", res.Status)
|
||||
@@ -58,10 +62,17 @@ func TestProxySettingsAPI(t *testing.T) {
|
||||
t.Errorf("POST: Server state did not update: redact=%v, logBody=%v", server.proxyRedact, server.proxyLogBody)
|
||||
}
|
||||
|
||||
// 3. Verify GET reflects new state
|
||||
res, _ = http.Get(ts.URL + "/setup/proxy-settings")
|
||||
defer res.Body.Close()
|
||||
json.NewDecoder(res.Body).Decode(&settings)
|
||||
res, err = http.Get(ts.URL + "/setup/proxy-settings")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
defer func() { _ = res.Body.Close() }()
|
||||
|
||||
if err := json.NewDecoder(res.Body).Decode(&settings); err != nil {
|
||||
t.Fatalf("GET (after update): Failed to decode response: %v", err)
|
||||
}
|
||||
|
||||
if settings["redact"] != false || settings["log_body"] != true {
|
||||
t.Errorf("GET (after update): Unexpected settings: %+v", settings)
|
||||
}
|
||||
|
||||
@@ -42,6 +42,7 @@ func (s *Server) HandleUsageStats(w http.ResponseWriter, r *http.Request) {
|
||||
if event.Time == "" {
|
||||
event.Time = time.Now().Format(time.RFC3339)
|
||||
}
|
||||
|
||||
s.ds.AddDeviceEvent(stats.DeviceID, event)
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
@@ -81,6 +82,7 @@ func (s *Server) HandleErrorStats(w http.ResponseWriter, r *http.Request) {
|
||||
if event.Time == "" {
|
||||
event.Time = time.Now().Format(time.RFC3339)
|
||||
}
|
||||
|
||||
s.ds.AddDeviceEvent(stats.DeviceID, event)
|
||||
|
||||
w.WriteHeader(http.StatusOK)
|
||||
|
||||
@@ -53,6 +53,7 @@ func setupRouter(targetURL string, ds *datastore.DataStore) (*chi.Mux, *Server)
|
||||
r.NotFound(func(w http.ResponseWriter, r *http.Request) {
|
||||
proxy.ServeHTTP(w, r)
|
||||
})
|
||||
|
||||
return r, server
|
||||
}
|
||||
|
||||
|
||||
@@ -32,15 +32,22 @@ func NewServer(ds *datastore.DataStore, sm *setup.Manager, serverURL string, pro
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) DiscoverDevices() {
|
||||
func (s *Server) DiscoverDevices(ctx context.Context) {
|
||||
s.discovering = true
|
||||
|
||||
defer func() { s.discovering = false }()
|
||||
|
||||
log.Println("Scanning for Bose devices...")
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
|
||||
if ctx == nil {
|
||||
var cancel context.CancelFunc
|
||||
|
||||
ctx, cancel = context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
}
|
||||
|
||||
svc := discovery.NewService(10 * time.Second)
|
||||
|
||||
devices, err := svc.DiscoverDevices(ctx)
|
||||
if err != nil {
|
||||
log.Printf("Discovery error: %v", err)
|
||||
@@ -60,6 +67,7 @@ func (s *Server) DiscoverDevices() {
|
||||
if existingID == "" {
|
||||
existingID = known.IPAddress
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -69,6 +77,7 @@ func (s *Server) DiscoverDevices() {
|
||||
if deviceID == "" {
|
||||
// If serial is missing from discovery, try to fetch it from :8090/info
|
||||
log.Printf("Serial number missing for %s at %s, attempting live info fetch...", d.Name, d.Host)
|
||||
|
||||
liveInfo, err := s.sm.GetLiveDeviceInfo(d.Host)
|
||||
if err == nil && liveInfo.SerialNumber != "" {
|
||||
d.SerialNo = liveInfo.SerialNumber
|
||||
@@ -98,7 +107,7 @@ func (s *Server) DiscoverDevices() {
|
||||
// If we had an IP-based entry and now have a Serial, clean up the IP-based entry
|
||||
if d.SerialNo != "" && existingID != "" && existingID != d.SerialNo {
|
||||
log.Printf("Device %s previously known as %s, migrating to serial-based ID %s", d.Name, existingID, d.SerialNo)
|
||||
s.ds.RemoveDevice("default", existingID)
|
||||
_ = s.ds.RemoveDevice("default", existingID)
|
||||
}
|
||||
|
||||
if err := s.ds.SaveDeviceInfo("default", deviceID, info); err != nil {
|
||||
|
||||
@@ -7,9 +7,9 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/gesellix/bose-soundtouch/pkg/models"
|
||||
"github.com/gesellix/bose-soundtouch/pkg/service/constants"
|
||||
"github.com/gesellix/bose-soundtouch/pkg/service/datastore"
|
||||
"github.com/gesellix/bose-soundtouch/pkg/models"
|
||||
)
|
||||
|
||||
const DateStr = "2012-09-19T12:43:00.000+00:00"
|
||||
@@ -24,6 +24,7 @@ func SourceProviders() []models.SourceProvider {
|
||||
UpdatedOn: DateStr,
|
||||
}
|
||||
}
|
||||
|
||||
return providers
|
||||
}
|
||||
|
||||
@@ -36,10 +37,12 @@ func SourceProvidersToXML() ([]byte, error) {
|
||||
sp := SourceProvidersXML{
|
||||
Providers: SourceProviders(),
|
||||
}
|
||||
|
||||
data, err := xml.MarshalIndent(sp, "", " ")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return append([]byte(xml.Header), data...), nil
|
||||
}
|
||||
|
||||
@@ -62,6 +65,7 @@ func ConfiguredSourceToXML(cs models.ConfiguredSource) ([]byte, error) {
|
||||
}
|
||||
|
||||
providerID := 0
|
||||
|
||||
for i, p := range constants.Providers {
|
||||
if p == cs.SourceKeyType {
|
||||
providerID = i + 1
|
||||
@@ -87,12 +91,14 @@ func ConfiguredSourceToXML(cs models.ConfiguredSource) ([]byte, error) {
|
||||
|
||||
func GetConfiguredSourceXML(cs models.ConfiguredSource) string {
|
||||
providerID := 0
|
||||
|
||||
for i, p := range constants.Providers {
|
||||
if p == cs.SourceKeyType {
|
||||
providerID = i + 1
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Sprintf(`<source id="%s" type="Audio"><createdOn>%s</createdOn><credential type="token">%s</credential><name>%s</name><sourceproviderid>%d</sourceproviderid><sourcename>%s</sourcename><sourcesettings></sourcesettings><updatedOn>%s</updatedOn><username>%s</username></source>`,
|
||||
cs.ID, DateStr, cs.Secret, cs.SourceKeyAccount, providerID, cs.DisplayName, DateStr, cs.SourceKeyAccount)
|
||||
}
|
||||
@@ -102,6 +108,7 @@ func PresetsToXML(ds *datastore.DataStore, account string) ([]byte, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sources, err := ds.GetConfiguredSources(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -118,13 +125,16 @@ func PresetsToXML(ds *datastore.DataStore, account string) ([]byte, error) {
|
||||
|
||||
// Content Item Source
|
||||
found := false
|
||||
|
||||
for _, s := range sources {
|
||||
if s.ID == p.SourceID || (s.SourceKeyType == p.Source && s.SourceKeyAccount == p.SourceAccount) {
|
||||
res += GetConfiguredSourceXML(s)
|
||||
found = true
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
// This might happen if source is not found
|
||||
}
|
||||
@@ -132,6 +142,7 @@ func PresetsToXML(ds *datastore.DataStore, account string) ([]byte, error) {
|
||||
res += fmt.Sprintf(`<updatedOn>%s</updatedOn>`, DateStr)
|
||||
res += `</preset>`
|
||||
}
|
||||
|
||||
res += `</presets>`
|
||||
|
||||
return append([]byte(xml.Header), []byte(res)...), nil
|
||||
@@ -142,12 +153,14 @@ func RecentsToXML(ds *datastore.DataStore, account string) ([]byte, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sources, err := ds.GetConfiguredSources(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
res := `<recents>`
|
||||
|
||||
for _, r := range recents {
|
||||
lastPlayed := ""
|
||||
if sec, err := strconv.ParseInt(r.UtcTime, 10, 64); err == nil {
|
||||
@@ -162,19 +175,23 @@ func RecentsToXML(ds *datastore.DataStore, account string) ([]byte, error) {
|
||||
res += fmt.Sprintf(`<name>%s</name>`, r.Name)
|
||||
|
||||
found := false
|
||||
|
||||
for _, s := range sources {
|
||||
if s.ID == r.SourceID || (s.SourceKeyType == r.Source && s.SourceKeyAccount == r.SourceAccount) {
|
||||
res += GetConfiguredSourceXML(s)
|
||||
found = true
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
}
|
||||
|
||||
res += fmt.Sprintf(`<updatedOn>%s</updatedOn>`, DateStr)
|
||||
res += `</recent>`
|
||||
}
|
||||
|
||||
res += `</recents>`
|
||||
|
||||
return append([]byte(xml.Header), []byte(res)...), nil
|
||||
@@ -190,6 +207,7 @@ func SoftwareUpdateToXML() string {
|
||||
|
||||
func AccountFullToXML(ds *datastore.DataStore, account string) ([]byte, error) {
|
||||
devicesDir := ds.AccountDevicesDir(account)
|
||||
|
||||
entries, err := os.ReadDir(devicesDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -197,10 +215,12 @@ func AccountFullToXML(ds *datastore.DataStore, account string) ([]byte, error) {
|
||||
|
||||
res := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8" standalone="yes"?><account id="%s"><accountStatus>OK</accountStatus><devices>`, account)
|
||||
lastDeviceID := ""
|
||||
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
deviceID := entry.Name()
|
||||
lastDeviceID = deviceID
|
||||
|
||||
info, err := ds.GetDeviceInfo(account, deviceID)
|
||||
if err != nil {
|
||||
continue
|
||||
@@ -229,19 +249,23 @@ func AccountFullToXML(ds *datastore.DataStore, account string) ([]byte, error) {
|
||||
res += `</device>`
|
||||
}
|
||||
}
|
||||
|
||||
res += `</devices><mode>global</mode><preferredLanguage>en</preferredLanguage>`
|
||||
res += ProviderSettingsToXML(account)
|
||||
|
||||
if lastDeviceID != "" {
|
||||
sources, _ := ds.GetConfiguredSources(account)
|
||||
|
||||
res += `<sources>`
|
||||
for _, s := range sources {
|
||||
res += GetConfiguredSourceXML(s)
|
||||
}
|
||||
|
||||
res += `</sources>`
|
||||
}
|
||||
|
||||
res += `</account>`
|
||||
|
||||
return []byte(res), nil
|
||||
}
|
||||
|
||||
@@ -250,6 +274,7 @@ func UpdatePreset(ds *datastore.DataStore, account string, device string, preset
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
presets, err := ds.GetPresets(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -267,12 +292,14 @@ func UpdatePreset(ds *datastore.DataStore, account string, device string, preset
|
||||
}
|
||||
|
||||
var matchingSrc *models.ConfiguredSource
|
||||
|
||||
for _, s := range sources {
|
||||
if s.ID == newPresetElem.SourceID {
|
||||
matchingSrc = &s
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if matchingSrc == nil {
|
||||
return nil, fmt.Errorf("invalid account/source")
|
||||
}
|
||||
@@ -297,6 +324,7 @@ func UpdatePreset(ds *datastore.DataStore, account string, device string, preset
|
||||
for len(presets) < presetNumber {
|
||||
presets = append(presets, models.ServicePreset{})
|
||||
}
|
||||
|
||||
presets[presetNumber-1] = presetObj
|
||||
|
||||
if err := ds.SavePresets(account, presets); err != nil {
|
||||
@@ -322,6 +350,7 @@ func AddRecent(ds *datastore.DataStore, account string, device string, sourceXML
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
recents, err := ds.GetRecents(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -339,17 +368,20 @@ func AddRecent(ds *datastore.DataStore, account string, device string, sourceXML
|
||||
}
|
||||
|
||||
var matchingSrc *models.ConfiguredSource
|
||||
|
||||
for _, s := range sources {
|
||||
if s.ID == newRecentElem.SourceID {
|
||||
matchingSrc = &s
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if matchingSrc == nil {
|
||||
return nil, fmt.Errorf("invalid account/source")
|
||||
}
|
||||
|
||||
utcTime := time.Now().Unix()
|
||||
|
||||
if newRecentElem.LastPlayedAt != "" {
|
||||
if t, err := time.Parse(time.RFC3339, newRecentElem.LastPlayedAt); err == nil {
|
||||
utcTime = t.Unix()
|
||||
@@ -358,7 +390,9 @@ func AddRecent(ds *datastore.DataStore, account string, device string, sourceXML
|
||||
|
||||
// Find existing
|
||||
var recentObj *models.ServiceRecent
|
||||
|
||||
createdOn := DateStr
|
||||
|
||||
for i, r := range recents {
|
||||
if r.Source == matchingSrc.SourceKeyType && r.Location == newRecentElem.Location && r.SourceAccount == matchingSrc.SourceKeyAccount {
|
||||
recents[i].UtcTime = strconv.FormatInt(utcTime, 10)
|
||||
@@ -369,6 +403,7 @@ func AddRecent(ds *datastore.DataStore, account string, device string, sourceXML
|
||||
|
||||
// Move to front
|
||||
recents = append([]models.ServiceRecent{*recentObj}, append(recents[:i], recents[i+1:]...)...)
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -380,6 +415,7 @@ func AddRecent(ds *datastore.DataStore, account string, device string, sourceXML
|
||||
maxID = id
|
||||
}
|
||||
}
|
||||
|
||||
recentObj = &models.ServiceRecent{
|
||||
ServiceContentItem: models.ServiceContentItem{
|
||||
ID: strconv.Itoa(maxID + 1),
|
||||
@@ -395,6 +431,7 @@ func AddRecent(ds *datastore.DataStore, account string, device string, sourceXML
|
||||
UtcTime: strconv.FormatInt(utcTime, 10),
|
||||
}
|
||||
createdOn = time.Now().Format(time.RFC3339)
|
||||
|
||||
recents = append([]models.ServiceRecent{*recentObj}, recents...)
|
||||
if len(recents) > 10 {
|
||||
recents = recents[:10]
|
||||
|
||||
@@ -6,13 +6,14 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/gesellix/bose-soundtouch/pkg/service/datastore"
|
||||
"github.com/gesellix/bose-soundtouch/pkg/models"
|
||||
"github.com/gesellix/bose-soundtouch/pkg/service/datastore"
|
||||
)
|
||||
|
||||
func TestMargeXML(t *testing.T) {
|
||||
tempDir, _ := os.MkdirTemp("", "marge-test-*")
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
ds := datastore.NewDataStore(tempDir)
|
||||
account := "123"
|
||||
device := "ABC"
|
||||
@@ -22,17 +23,18 @@ func TestMargeXML(t *testing.T) {
|
||||
DeviceID: device,
|
||||
Name: "Living Room",
|
||||
}
|
||||
ds.SaveDeviceInfo(account, device, info)
|
||||
_ = ds.SaveDeviceInfo(account, device, info)
|
||||
|
||||
// Save empty presets/recents to avoid index out of range when stripping header
|
||||
ds.SavePresets(account, []models.ServicePreset{})
|
||||
ds.SaveRecents(account, []models.ServiceRecent{})
|
||||
_ = ds.SavePresets(account, []models.ServicePreset{})
|
||||
_ = ds.SaveRecents(account, []models.ServiceRecent{})
|
||||
|
||||
// Test SourceProvidersToXML
|
||||
xmlData, err := SourceProvidersToXML()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !strings.Contains(string(xmlData), "<sourceProviders>") {
|
||||
t.Errorf("Expected <sourceProviders>, got %s", string(xmlData))
|
||||
}
|
||||
@@ -42,9 +44,11 @@ func TestMargeXML(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if !strings.Contains(string(fullXML), `id="123"`) {
|
||||
t.Errorf("Expected account id 123, got %s", string(fullXML))
|
||||
}
|
||||
|
||||
if !strings.Contains(string(fullXML), "Living Room") {
|
||||
t.Errorf("Expected device name Living Room, got %s", string(fullXML))
|
||||
}
|
||||
@@ -59,6 +63,7 @@ func TestMargeXML(t *testing.T) {
|
||||
func TestAddRecent_TimestampPreservation(t *testing.T) {
|
||||
tempDir, _ := os.MkdirTemp("", "marge-timestamp-test-*")
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
ds := datastore.NewDataStore(tempDir)
|
||||
account := "test-acc"
|
||||
device := "test-dev"
|
||||
@@ -66,8 +71,8 @@ func TestAddRecent_TimestampPreservation(t *testing.T) {
|
||||
// 1. Setup configured sources
|
||||
// We need a Sources.xml file in the account directory
|
||||
sourcesPath := ds.AccountDir(account)
|
||||
os.MkdirAll(sourcesPath, 0755)
|
||||
ds.SaveConfiguredSources(account, []models.ConfiguredSource{
|
||||
_ = os.MkdirAll(sourcesPath, 0755)
|
||||
_ = ds.SaveConfiguredSources(account, []models.ConfiguredSource{
|
||||
{
|
||||
ID: "101",
|
||||
DisplayName: "Test Source",
|
||||
@@ -75,7 +80,7 @@ func TestAddRecent_TimestampPreservation(t *testing.T) {
|
||||
SourceKeyAccount: "test-user",
|
||||
},
|
||||
})
|
||||
ds.SaveRecents(account, []models.ServiceRecent{})
|
||||
_ = ds.SaveRecents(account, []models.ServiceRecent{})
|
||||
|
||||
// 2. Add an initial recent
|
||||
sourceXML := []byte(`
|
||||
@@ -85,6 +90,7 @@ func TestAddRecent_TimestampPreservation(t *testing.T) {
|
||||
<location>station-1</location>
|
||||
<contentItemType>station</contentItemType>
|
||||
</recent>`)
|
||||
|
||||
_, err := AddRecent(ds, account, device, sourceXML)
|
||||
if err != nil {
|
||||
t.Fatalf("AddRecent failed: %v", err)
|
||||
@@ -94,6 +100,7 @@ func TestAddRecent_TimestampPreservation(t *testing.T) {
|
||||
if len(recents) != 1 {
|
||||
t.Fatalf("Expected 1 recent, got %d", len(recents))
|
||||
}
|
||||
|
||||
originalCreatedOn := recents[0].UtcTime // It's stored in UtcTime field (unix string) in models.ServiceRecent but the AddRecent return XML uses <createdOn> tag which is DateStr or Now depending on logic.
|
||||
// Actually let's check what AddRecent returns.
|
||||
|
||||
|
||||
@@ -38,9 +38,11 @@ func (lp *LoggingProxy) LogRequest(r *http.Request) {
|
||||
headers := formatHeaders(r.Header, lp.Redact)
|
||||
|
||||
bodyStr := "[HIDDEN]"
|
||||
|
||||
if lp.LogBody && shouldLogBody(r.Header.Get("Content-Type")) {
|
||||
if r.Body != nil {
|
||||
bodyBytes, _ := io.ReadAll(r.Body)
|
||||
|
||||
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||
if int64(len(bodyBytes)) > lp.MaxBodySize {
|
||||
bodyStr = string(bodyBytes[:lp.MaxBodySize]) + "... [TRUNCATED]"
|
||||
@@ -59,9 +61,11 @@ func (lp *LoggingProxy) LogResponse(r *http.Response) {
|
||||
headers := formatHeaders(r.Header, lp.Redact)
|
||||
|
||||
bodyStr := "[HIDDEN]"
|
||||
|
||||
if lp.LogBody && shouldLogBody(r.Header.Get("Content-Type")) {
|
||||
if r.Body != nil {
|
||||
bodyBytes, _ := io.ReadAll(r.Body)
|
||||
|
||||
r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes))
|
||||
if int64(len(bodyBytes)) > lp.MaxBodySize {
|
||||
bodyStr = string(bodyBytes[:lp.MaxBodySize]) + "... [TRUNCATED]"
|
||||
@@ -86,8 +90,10 @@ func formatHeaders(h http.Header, redact bool) string {
|
||||
if redact && isSensitive(k) {
|
||||
val = "[REDACTED]"
|
||||
}
|
||||
|
||||
sb.WriteString(fmt.Sprintf(" %s: %s\n", k, val))
|
||||
}
|
||||
|
||||
return strings.TrimSuffix(sb.String(), "\n")
|
||||
}
|
||||
|
||||
@@ -97,11 +103,13 @@ func isSensitive(header string) bool {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func shouldLogBody(contentType string) bool {
|
||||
contentType = strings.ToLower(contentType)
|
||||
|
||||
return strings.Contains(contentType, "xml") ||
|
||||
strings.Contains(contentType, "json") ||
|
||||
strings.Contains(contentType, "text") ||
|
||||
|
||||
@@ -58,6 +58,7 @@ func TestShouldLogBody(t *testing.T) {
|
||||
|
||||
func TestLoggingProxy_LogRequest(t *testing.T) {
|
||||
os.Setenv("LOG_PROXY_BODY", "true")
|
||||
|
||||
defer os.Unsetenv("LOG_PROXY_BODY")
|
||||
|
||||
lp := NewLoggingProxy("http://example.com", true)
|
||||
|
||||
+26
-10
@@ -77,24 +77,26 @@ func (m *Manager) GetLiveDeviceInfo(deviceIP string) (*DeviceInfoXML, error) {
|
||||
infoURL = fmt.Sprintf("http://%s/info", deviceIP)
|
||||
_ = host
|
||||
}
|
||||
|
||||
resp, err := http.Get(infoURL)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch info from %s: %v", infoURL, err)
|
||||
return nil, fmt.Errorf("failed to fetch info from %s: %w", infoURL, err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var infoXML DeviceInfoXML
|
||||
if err := xml.NewDecoder(resp.Body).Decode(&infoXML); err != nil {
|
||||
return nil, fmt.Errorf("failed to decode info XML from %s: %v", infoURL, err)
|
||||
return nil, fmt.Errorf("failed to decode info XML from %s: %w", infoURL, err)
|
||||
}
|
||||
|
||||
for _, comp := range infoXML.Components {
|
||||
if comp.Category == "SCM" {
|
||||
switch comp.Category {
|
||||
case "SCM":
|
||||
infoXML.SoftwareVer = comp.SoftwareVersion
|
||||
if infoXML.SerialNumber == "" {
|
||||
infoXML.SerialNumber = comp.SerialNumber
|
||||
}
|
||||
} else if comp.Category == "PackagedProduct" {
|
||||
case "PackagedProduct":
|
||||
if infoXML.SerialNumber == "" {
|
||||
infoXML.SerialNumber = comp.SerialNumber
|
||||
}
|
||||
@@ -109,6 +111,7 @@ func (m *Manager) GetMigrationSummary(deviceIP string, targetURL string, proxyUR
|
||||
if targetURL == "" {
|
||||
targetURL = m.ServerURL
|
||||
}
|
||||
|
||||
client := ssh.NewClient(deviceIP)
|
||||
|
||||
summary := &MigrationSummary{
|
||||
@@ -125,6 +128,7 @@ func (m *Manager) GetMigrationSummary(deviceIP string, targetURL string, proxyUR
|
||||
summary.DeviceModel = d.ProductCode
|
||||
summary.DeviceSerial = d.DeviceSerialNumber
|
||||
summary.FirmwareVersion = d.FirmwareVersion
|
||||
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -139,12 +143,15 @@ func (m *Manager) GetMigrationSummary(deviceIP string, targetURL string, proxyUR
|
||||
if infoXML.Name != "" {
|
||||
summary.DeviceName = infoXML.Name
|
||||
}
|
||||
|
||||
if infoXML.Type != "" {
|
||||
summary.DeviceModel = infoXML.Type
|
||||
}
|
||||
|
||||
if infoXML.SerialNumber != "" {
|
||||
summary.DeviceSerial = infoXML.SerialNumber
|
||||
}
|
||||
|
||||
if infoXML.SoftwareVer != "" {
|
||||
summary.FirmwareVersion = infoXML.SoftwareVer
|
||||
}
|
||||
@@ -165,6 +172,7 @@ func (m *Manager) GetMigrationSummary(deviceIP string, targetURL string, proxyUR
|
||||
|
||||
// 2. Check SSH and read current config
|
||||
var currentConfig string
|
||||
|
||||
path := SoundTouchSdkPrivateCfgPath
|
||||
client = ssh.NewClient(deviceIP)
|
||||
|
||||
@@ -231,6 +239,7 @@ func (m *Manager) GetMigrationSummary(deviceIP string, targetURL string, proxyUR
|
||||
// Fallback: try base64 if cat returned empty string but file has size > 0
|
||||
if config == "" && fileInfo != "" {
|
||||
fmt.Printf("Cat returned empty for %s, trying base64\n", path)
|
||||
|
||||
b64Config, err := client.Run(fmt.Sprintf("base64 %s", path))
|
||||
if err == nil && b64Config != "" {
|
||||
fmt.Printf("Base64 output for %s (length %d)\n", path, len(b64Config))
|
||||
@@ -253,8 +262,9 @@ func (m *Manager) GetMigrationSummary(deviceIP string, targetURL string, proxyUR
|
||||
|
||||
xmlContent, err := xml.MarshalIndent(plannedCfg, "", " ")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal planned XML: %v", err)
|
||||
return nil, fmt.Errorf("failed to marshal planned XML: %w", err)
|
||||
}
|
||||
|
||||
summary.PlannedConfig = "<?xml version=\"1.0\" encoding=\"utf-8\"?>\n" + string(xmlContent)
|
||||
|
||||
// 3. Check for remote services files
|
||||
@@ -268,6 +278,7 @@ func (m *Manager) GetMigrationSummary(deviceIP string, targetURL string, proxyUR
|
||||
_, err := client.Run(fmt.Sprintf("[ -e %s ]", loc))
|
||||
if err == nil {
|
||||
summary.RemoteServicesFound = append(summary.RemoteServicesFound, loc)
|
||||
|
||||
summary.RemoteServicesEnabled = true
|
||||
if loc != "/tmp/remote_services" {
|
||||
summary.RemoteServicesPersistent = true
|
||||
@@ -283,6 +294,7 @@ func (m *Manager) MigrateSpeaker(deviceIP string, targetURL string, proxyURL str
|
||||
if targetURL == "" {
|
||||
targetURL = m.ServerURL
|
||||
}
|
||||
|
||||
if err := m.EnsureRemoteServices(deviceIP); err != nil {
|
||||
// Log but continue migration? Or fail? The requirement is "to ensure stable 'remote_services'"
|
||||
// Let's log it.
|
||||
@@ -312,12 +324,15 @@ func (m *Manager) MigrateSpeaker(deviceIP string, targetURL string, proxyURL str
|
||||
if options["marge"] == "original" {
|
||||
cfg.MargeServerUrl = fmt.Sprintf("%s/proxy/%s", proxyURL, currentCfg.MargeServerUrl)
|
||||
}
|
||||
|
||||
if options["stats"] == "original" {
|
||||
cfg.StatsServerUrl = fmt.Sprintf("%s/proxy/%s", proxyURL, currentCfg.StatsServerUrl)
|
||||
}
|
||||
|
||||
if options["sw_update"] == "original" {
|
||||
cfg.SwUpdateUrl = fmt.Sprintf("%s/proxy/%s", proxyURL, currentCfg.SwUpdateUrl)
|
||||
}
|
||||
|
||||
if options["bmx"] == "original" {
|
||||
cfg.BmxRegistryUrl = fmt.Sprintf("%s/proxy/%s", proxyURL, currentCfg.BmxRegistryUrl)
|
||||
}
|
||||
@@ -332,7 +347,7 @@ func (m *Manager) MigrateSpeaker(deviceIP string, targetURL string, proxyURL str
|
||||
|
||||
xmlContent, err := xml.MarshalIndent(cfg, "", " ")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal XML: %v", err)
|
||||
return fmt.Errorf("failed to marshal XML: %w", err)
|
||||
}
|
||||
|
||||
// Add XML header
|
||||
@@ -341,6 +356,7 @@ func (m *Manager) MigrateSpeaker(deviceIP string, targetURL string, proxyURL str
|
||||
// 0. Backup original config if it doesn't exist
|
||||
remotePath := SoundTouchSdkPrivateCfgPath
|
||||
rwCmd := "(rw || mount -o remount,rw /)"
|
||||
|
||||
if _, err := client.Run(fmt.Sprintf("[ -f %s.original ]", remotePath)); err != nil {
|
||||
fmt.Printf("Backing up original config to %s.original\n", remotePath)
|
||||
// Try to copy existing config to .original, ensuring filesystem is writable
|
||||
@@ -361,12 +377,12 @@ func (m *Manager) MigrateSpeaker(deviceIP string, targetURL string, proxyURL str
|
||||
// Actually, let's call rw before UploadContent here.
|
||||
_, _ = client.Run(rwCmd)
|
||||
if err := client.UploadContent(xmlContent, remotePath); err != nil {
|
||||
return fmt.Errorf("failed to upload config: %v", err)
|
||||
return fmt.Errorf("failed to upload config: %w", err)
|
||||
}
|
||||
|
||||
// 2. Reboot the speaker (requires 'rw' command first to make filesystem writable)
|
||||
if _, err := client.Run(fmt.Sprintf("%s && reboot", rwCmd)); err != nil {
|
||||
return fmt.Errorf("failed to reboot speaker: %v", err)
|
||||
return fmt.Errorf("failed to reboot speaker: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -393,13 +409,13 @@ func (m *Manager) BackupConfig(deviceIP string) error {
|
||||
// Fallback to cat + upload
|
||||
config, err := client.Run(fmt.Sprintf("cat %s", remotePath))
|
||||
if err != nil || config == "" {
|
||||
return fmt.Errorf("failed to read current config: %v", err)
|
||||
return fmt.Errorf("failed to read current config: %w", err)
|
||||
}
|
||||
|
||||
// Ensure rw before upload fallback
|
||||
_, _ = client.Run(rwCmd)
|
||||
if err := client.UploadContent([]byte(config), remotePath+".original"); err != nil {
|
||||
return fmt.Errorf("failed to upload backup config: %v", err)
|
||||
return fmt.Errorf("failed to upload backup config: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -13,6 +13,7 @@ func TestGetLiveDeviceInfo(t *testing.T) {
|
||||
if r.URL.Path != "/info" {
|
||||
t.Errorf("Expected to request /info, got %s", r.URL.Path)
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/xml")
|
||||
fmt.Fprint(w, `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<info deviceID="08DF1F0BA325">
|
||||
@@ -34,8 +35,8 @@ func TestGetLiveDeviceInfo(t *testing.T) {
|
||||
host := server.Listener.Addr().String()
|
||||
|
||||
manager := NewManager("http://localhost:8000", nil)
|
||||
info, err := manager.GetLiveDeviceInfo(host)
|
||||
|
||||
info, err := manager.GetLiveDeviceInfo(host)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to get live device info: %v", err)
|
||||
}
|
||||
@@ -43,9 +44,11 @@ func TestGetLiveDeviceInfo(t *testing.T) {
|
||||
if info.Name != "Test Speaker" {
|
||||
t.Errorf("Expected Name 'Test Speaker', got '%s'", info.Name)
|
||||
}
|
||||
|
||||
if info.SoftwareVer != "19.0.5" {
|
||||
t.Errorf("Expected SoftwareVer '19.0.5', got '%s'", info.SoftwareVer)
|
||||
}
|
||||
|
||||
if info.SerialNumber != "08DF1F0BA325" {
|
||||
t.Errorf("Expected SerialNumber '08DF1F0BA325', got '%s'", info.SerialNumber)
|
||||
}
|
||||
@@ -65,6 +68,7 @@ func TestGetMigrationSummary_SSHFailure(t *testing.T) {
|
||||
if summary.SSHSuccess {
|
||||
t.Errorf("Expected SSHSuccess to be false for closed port, got true")
|
||||
}
|
||||
|
||||
if summary.CurrentConfig == "" {
|
||||
t.Errorf("Expected CurrentConfig to contain error message, got empty string")
|
||||
}
|
||||
|
||||
+13
-9
@@ -68,19 +68,21 @@ func (c *Client) getConfig() *ssh.ClientConfig {
|
||||
// Run executes a command on the remote host and returns the combined stdout and stderr.
|
||||
func (c *Client) Run(command string) (string, error) {
|
||||
config := c.getConfig()
|
||||
|
||||
client, err := ssh.Dial("tcp", c.Host+":22", config)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to dial: %v", err)
|
||||
return "", fmt.Errorf("failed to dial: %w", err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
session, err := client.NewSession()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create session: %v", err)
|
||||
return "", fmt.Errorf("failed to create session: %w", err)
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
output, err := session.CombinedOutput(command)
|
||||
|
||||
return string(output), err
|
||||
}
|
||||
|
||||
@@ -89,28 +91,29 @@ func (c *Client) Run(command string) (string, error) {
|
||||
// For larger files, a proper SCP or SFTP implementation would be better.
|
||||
func (c *Client) UploadContent(content []byte, remotePath string) error {
|
||||
config := c.getConfig()
|
||||
|
||||
client, err := ssh.Dial("tcp", c.Host+":22", config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to dial: %v", err)
|
||||
return fmt.Errorf("failed to dial: %w", err)
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
session, err := client.NewSession()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create session: %v", err)
|
||||
return fmt.Errorf("failed to create session: %w", err)
|
||||
}
|
||||
defer session.Close()
|
||||
|
||||
// Use a pipe to write content to the remote command's stdin
|
||||
stdin, err := session.StdinPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get stdin pipe: %v", err)
|
||||
return fmt.Errorf("failed to get stdin pipe: %w", err)
|
||||
}
|
||||
|
||||
// Capture stderr to get better error messages
|
||||
stderr, err := session.StderrPipe()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get stderr pipe: %v", err)
|
||||
return fmt.Errorf("failed to get stderr pipe: %w", err)
|
||||
}
|
||||
|
||||
// Read content from stdin and write to the remote file
|
||||
@@ -118,14 +121,15 @@ func (c *Client) UploadContent(content []byte, remotePath string) error {
|
||||
|
||||
// Start the command
|
||||
if err := session.Start(cmd); err != nil {
|
||||
return fmt.Errorf("failed to start upload command: %v", err)
|
||||
return fmt.Errorf("failed to start upload command: %w", err)
|
||||
}
|
||||
|
||||
// Write content and close stdin
|
||||
_, err = stdin.Write(content)
|
||||
stdin.Close()
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to write content to stdin: %v", err)
|
||||
return fmt.Errorf("failed to write content to stdin: %w", err)
|
||||
}
|
||||
|
||||
// Read stderr in case of failure
|
||||
@@ -134,7 +138,7 @@ func (c *Client) UploadContent(content []byte, remotePath string) error {
|
||||
|
||||
// Wait for the command to finish
|
||||
if err := session.Wait(); err != nil {
|
||||
return fmt.Errorf("failed to finish upload: %v (stderr: %s)", err, stderrBuf.String())
|
||||
return fmt.Errorf("failed to finish upload: %w (stderr: %s)", err, stderrBuf.String())
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -7,10 +7,12 @@ import (
|
||||
|
||||
func TestNewClient(t *testing.T) {
|
||||
host := "192.168.1.10"
|
||||
|
||||
client := NewClient(host)
|
||||
if client.Host != host {
|
||||
t.Errorf("Expected host %s, got %s", host, client.Host)
|
||||
}
|
||||
|
||||
if client.User != "root" {
|
||||
t.Errorf("Expected user root, got %s", client.User)
|
||||
}
|
||||
@@ -18,10 +20,12 @@ func TestNewClient(t *testing.T) {
|
||||
|
||||
func TestGetConfig(t *testing.T) {
|
||||
client := NewClient("localhost")
|
||||
|
||||
config := client.getConfig()
|
||||
if config.User != "root" {
|
||||
t.Errorf("Expected config user root, got %s", config.User)
|
||||
}
|
||||
|
||||
if len(config.Auth) == 0 {
|
||||
t.Error("Expected at least one auth method")
|
||||
}
|
||||
@@ -30,10 +34,12 @@ func TestGetConfig(t *testing.T) {
|
||||
func TestRun_DialFailure(t *testing.T) {
|
||||
// Use an invalid port/host to trigger dial failure
|
||||
client := NewClient("127.0.0.1:0")
|
||||
|
||||
_, err := client.Run("ls")
|
||||
if err == nil {
|
||||
t.Error("Expected dial failure, got nil")
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "failed to dial") {
|
||||
t.Errorf("Expected 'failed to dial' error, got: %v", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user