diff --git a/.docsignore b/.docsignore
new file mode 100644
index 0000000..d049825
--- /dev/null
+++ b/.docsignore
@@ -0,0 +1,5 @@
+# Files intentionally not linked in docs/SUMMARY.md.
+# Paths are relative to the docs/ directory.
+# Lines starting with # and blank lines are ignored.
+
+analysis/bose-soundtouch-community-tools.md
diff --git a/cmd/soundtouch-service/main.go b/cmd/soundtouch-service/main.go
index cb5ec60..b596e71 100644
--- a/cmd/soundtouch-service/main.go
+++ b/cmd/soundtouch-service/main.go
@@ -804,6 +804,10 @@ func setupRouter(server *handlers.Server) *chi.Mux {
r.Get("/group/member", server.HandleMargeDeviceGroupMember)
})
+ r.Post("/group", server.HandleMargeAddGroup)
+ r.Post("/group/{groupId}", server.HandleMargeModifyGroup)
+ r.Delete("/group/{groupId}", server.HandleMargeDeleteGroup)
+
r.Delete("/device/{device}", server.HandleMargeRemoveDevice)
})
@@ -846,6 +850,10 @@ func setupRouter(server *handlers.Server) *chi.Mux {
r.Get("/devices/{device}/group/", server.HandleMargeDeviceGroup)
r.Get("/devices/{device}/group/server", server.HandleMargeDeviceGroupServer)
r.Get("/devices/{device}/group/member", server.HandleMargeDeviceGroupMember)
+
+ r.Post("/group", server.HandleMargeAddGroup)
+ r.Post("/group/{groupId}", server.HandleMargeModifyGroup)
+ r.Delete("/group/{groupId}", server.HandleMargeDeleteGroup)
r.Get("/devices/{device}/presets", server.HandleMargePresets)
r.Get("/devices/{device}/recents", server.HandleMargeRecents)
diff --git a/cmd/soundtouch-service/testdata/router_routes.txt b/cmd/soundtouch-service/testdata/router_routes.txt
index 9396f8b..ea2622e 100644
--- a/cmd/soundtouch-service/testdata/router_routes.txt
+++ b/cmd/soundtouch-service/testdata/router_routes.txt
@@ -1,5 +1,6 @@
CONNECT /oauth/* handlers.(*Server).HandleBoseProxy-fm
DELETE /accounts/{account}/devices/{device} handlers.(*Server).HandleMargeRemoveDevice-fm
+DELETE /accounts/{account}/group/{groupId} handlers.(*Server).HandleMargeDeleteGroup-fm
DELETE /oauth/* handlers.(*Server).HandleBoseProxy-fm
DELETE /setup/devices/{deviceId} handlers.(*Server).HandleRemoveDevice-fm
DELETE /setup/dns-discoveries handlers.(*Server).HandleClearDNSDiscoveries-fm
@@ -7,6 +8,7 @@ DELETE /setup/interactions/sessions handlers.(
DELETE /setup/interactions/sessions/{session} handlers.(*Server).HandleDeleteSession-fm
DELETE /setup/parity-mismatches handlers.(*Server).HandleClearParityMismatches-fm
DELETE /streaming/account/{account}/device/{device}/preset/{presetNumber} handlers.(*Server).HandleMargeRemovePreset-fm
+DELETE /streaming/account/{account}/group/{groupId} handlers.(*Server).HandleMargeDeleteGroup-fm
GET / handlers.(*Server).HandleRoot-fm
GET /accounts/{account}/devices handlers.(*Server).HandleMargeAccountDevices-fm
GET /accounts/{account}/devices/{device}/group handlers.(*Server).HandleMargeDeviceGroup-fm
@@ -84,6 +86,8 @@ PATCH /oauth/* handlers.(
POST /accounts/{account}/devices handlers.(*Server).HandleMargeAddDevice-fm
POST /accounts/{account}/devices/{device}/presets/{presetNumber} handlers.(*Server).HandleMargeUpdatePreset-fm
POST /accounts/{account}/devices/{device}/recents handlers.(*Server).HandleMargeAddRecent-fm
+POST /accounts/{account}/group handlers.(*Server).HandleMargeAddGroup-fm
+POST /accounts/{account}/group/{groupId} handlers.(*Server).HandleMargeModifyGroup-fm
POST /bmx/orion/v1/playback/station/{data} handlers.(*Server).HandleOrionPlayback-fm
POST /bmx/tunein/v1/report handlers.(*Server).HandleTuneInReport-fm
POST /bmx/tunein/v1/token handlers.(*Server).HandleTuneInToken-fm
@@ -120,6 +124,8 @@ POST /streaming/account/{account}/device/ handlers.(
POST /streaming/account/{account}/device/{device} handlers.(*Server).HandleMargeAddDevice-fm
POST /streaming/account/{account}/device/{device}/presets/{presetNumber} handlers.(*Server).HandleMargeUpdatePreset-fm
POST /streaming/account/{account}/device/{device}/recent handlers.(*Server).HandleMargeAddRecent-fm
+POST /streaming/account/{account}/group handlers.(*Server).HandleMargeAddGroup-fm
+POST /streaming/account/{account}/group/{groupId} handlers.(*Server).HandleMargeModifyGroup-fm
POST /streaming/account/{account}/source handlers.(*Server).HandleMargeAddSource-fm
POST /streaming/device_setting/account/{account}/device/{device}/device_settings handlers.(*Server).HandleMargeUpdateDeviceSettings-fm
POST /streaming/music/musicprovider/{providerID}/is_eligible handlers.(*Server).HandleMusicProviderIsEligible-fm
diff --git a/pkg/models/group.go b/pkg/models/group.go
new file mode 100644
index 0000000..a4aa763
--- /dev/null
+++ b/pkg/models/group.go
@@ -0,0 +1,25 @@
+package models
+
+import "encoding/xml"
+
+// Group represents a stereo pair of two ST10 SoundTouch speakers.
+type Group struct {
+ XMLName xml.Name `xml:"group"`
+ ID string `xml:"id,attr,omitempty"`
+ Name string `xml:"name"`
+ MasterDeviceID string `xml:"masterDeviceId"`
+ Roles GroupRoles `xml:"roles"`
+ SenderIPAddress string `xml:"senderIPAddress,omitempty"`
+}
+
+// GroupRoles contains the role assignments for devices in a group.
+type GroupRoles struct {
+ Roles []GroupRole `xml:"groupRole"`
+}
+
+// GroupRole describes the role (LEFT or RIGHT) of a single device in a group.
+type GroupRole struct {
+ DeviceID string `xml:"deviceId"`
+ Role string `xml:"role"`
+ IPAddress string `xml:"ipAddress,omitempty"`
+}
diff --git a/pkg/service/datastore/datastore.go b/pkg/service/datastore/datastore.go
index 0bd9774..0f9c4e9 100644
--- a/pkg/service/datastore/datastore.go
+++ b/pkg/service/datastore/datastore.go
@@ -10,6 +10,7 @@ import (
"encoding/xml"
"fmt"
"io"
+ "math/rand"
"os"
"path/filepath"
"sort"
@@ -1906,3 +1907,128 @@ func (ds *DataStore) ClearDNSDiscoveries() error {
return os.Remove(path)
}
+
+// groupFilePath returns the on-disk path for a group file.
+func (ds *DataStore) groupFilePath(account, groupID string) string {
+ return filepath.Join(ds.AccountDevicesDir(account), "Group_"+groupID+".xml")
+}
+
+// generateGroupID returns a unique 7-digit group ID that has no existing file.
+func (ds *DataStore) generateGroupID(account string) string {
+ for {
+ id := fmt.Sprintf("%07d", rand.Int63n(10_000_000)) //nolint:gosec
+ if !exists(ds.groupFilePath(account, id)) {
+ return id
+ }
+ }
+}
+
+// GetGroupForDevice returns the group containing the given device, or nil if ungrouped.
+func (ds *DataStore) GetGroupForDevice(account, deviceID string) (*models.Group, error) {
+ ds.fileMutex.RLock()
+ defer ds.fileMutex.RUnlock()
+
+ dir := ds.AccountDevicesDir(account)
+
+ entries, err := os.ReadDir(dir)
+ if err != nil {
+ if os.IsNotExist(err) {
+ return nil, nil
+ }
+
+ return nil, err
+ }
+
+ for _, e := range entries {
+ if e.IsDir() || !strings.HasPrefix(e.Name(), "Group_") || !strings.HasSuffix(e.Name(), ".xml") {
+ continue
+ }
+
+ data, readErr := os.ReadFile(filepath.Join(dir, e.Name()))
+ if readErr != nil {
+ continue
+ }
+
+ var g models.Group
+ if unmarshalErr := xml.Unmarshal(data, &g); unmarshalErr != nil {
+ continue
+ }
+
+ for _, role := range g.Roles.Roles {
+ if role.DeviceID == deviceID {
+ return &g, nil
+ }
+ }
+ }
+
+ return nil, nil
+}
+
+// AddGroup saves a new group to disk and returns its generated ID.
+func (ds *DataStore) AddGroup(account string, group *models.Group) (string, error) {
+ ds.fileMutex.Lock()
+ defer ds.fileMutex.Unlock()
+
+ dir := ds.AccountDevicesDir(account)
+ if err := os.MkdirAll(dir, 0755); err != nil {
+ return "", err
+ }
+
+ id := ds.generateGroupID(account)
+ group.ID = id
+
+ data, err := xml.MarshalIndent(group, "", " ")
+ if err != nil {
+ return "", err
+ }
+
+ return id, ds.atomicWriteFile(ds.groupFilePath(account, id), append([]byte(xml.Header), data...))
+}
+
+// ModifyGroup updates the name of an existing group and returns the updated group.
+func (ds *DataStore) ModifyGroup(account, groupID, newName string) (*models.Group, error) {
+ ds.fileMutex.Lock()
+ defer ds.fileMutex.Unlock()
+
+ path := ds.groupFilePath(account, groupID)
+
+ data, err := os.ReadFile(path)
+ if err != nil {
+ if os.IsNotExist(err) {
+ return nil, fmt.Errorf("group %s not found", groupID)
+ }
+
+ return nil, err
+ }
+
+ var g models.Group
+ if err := xml.Unmarshal(data, &g); err != nil {
+ return nil, err
+ }
+
+ g.Name = newName
+
+ updated, err := xml.MarshalIndent(&g, "", " ")
+ if err != nil {
+ return nil, err
+ }
+
+ if err := ds.atomicWriteFile(path, append([]byte(xml.Header), updated...)); err != nil {
+ return nil, err
+ }
+
+ return &g, nil
+}
+
+// DeleteGroup removes a group from disk.
+func (ds *DataStore) DeleteGroup(account, groupID string) error {
+ ds.fileMutex.Lock()
+ defer ds.fileMutex.Unlock()
+
+ err := os.Remove(ds.groupFilePath(account, groupID))
+ if os.IsNotExist(err) {
+ return fmt.Errorf("group %s not found", groupID)
+ }
+
+ return err
+}
diff --git a/pkg/service/handlers/docs_consistency_test.go b/pkg/service/handlers/docs_consistency_test.go
index e45428d..0afd7d7 100644
--- a/pkg/service/handlers/docs_consistency_test.go
+++ b/pkg/service/handlers/docs_consistency_test.go
@@ -1,6 +1,7 @@
package handlers
import (
+ "bufio"
"io/fs"
"os"
"path/filepath"
@@ -22,6 +23,8 @@ func TestDocsConsistency(t *testing.T) {
summaryText := string(summaryContent)
+ docsIgnore := readDocsIgnore(t, filepath.Join(projectRoot, ".docsignore"))
+
// List of directories to check
dirsToCheck := []string{".", "guides", "reference", "analysis"}
@@ -48,6 +51,13 @@ func TestDocsConsistency(t *testing.T) {
return nil
}
+ // Skip files listed in .docsignore at the project root
+ for _, skip := range docsIgnore {
+ if strings.HasSuffix(path, filepath.FromSlash(skip)) {
+ return nil
+ }
+ }
+
// Get relative path from docs/
relPath, err := filepath.Rel(docsDir, path)
if err != nil {
@@ -69,3 +79,31 @@ func TestDocsConsistency(t *testing.T) {
}
}
}
+
+// readDocsIgnore reads a .docsignore file and returns the non-empty, non-comment lines.
+// If the file does not exist it returns nil without failing the test.
+func readDocsIgnore(t *testing.T, path string) []string {
+ t.Helper()
+ f, err := os.Open(path)
+ if os.IsNotExist(err) {
+ return nil
+ }
+ if err != nil {
+ t.Fatalf("Failed to read %s: %v", path, err)
+ }
+ defer func() { _ = f.Close() }()
+
+ var patterns []string
+ scanner := bufio.NewScanner(f)
+ for scanner.Scan() {
+ line := strings.TrimSpace(scanner.Text())
+ if line == "" || strings.HasPrefix(line, "#") {
+ continue
+ }
+ patterns = append(patterns, line)
+ }
+ if err := scanner.Err(); err != nil {
+ t.Fatalf("Error reading %s: %v", path, err)
+ }
+ return patterns
+}
diff --git a/pkg/service/handlers/handlers_marge.go b/pkg/service/handlers/handlers_marge.go
index 8be7d5f..12e3587 100644
--- a/pkg/service/handlers/handlers_marge.go
+++ b/pkg/service/handlers/handlers_marge.go
@@ -679,26 +679,142 @@ func (s *Server) HandleMargeStreamingToken(w http.ResponseWriter, _ *http.Reques
_, _ = w.Write(data)
}
-// HandleMargeDeviceGroup returns grouping information for a device (empty group by default).
-func (s *Server) HandleMargeDeviceGroup(w http.ResponseWriter, _ *http.Request) {
- // Native firmware expects vnd.bose.streaming content type
+// HandleMargeDeviceGroup returns grouping information for a device.
+func (s *Server) HandleMargeDeviceGroup(w http.ResponseWriter, r *http.Request) {
+ account := chi.URLParam(r, "account")
+ device := chi.URLParam(r, "device")
+
w.Header().Set("Content-Type", "application/vnd.bose.streaming-v1.2+xml")
+
+ group, err := s.ds.GetGroupForDevice(account, device)
+ if err != nil || group == nil {
+ w.WriteHeader(http.StatusOK)
+ _, _ = w.Write([]byte(constants.XMLHeader + ``))
+
+ return
+ }
+
+ data, err := xml.Marshal(group)
+ if err != nil {
+ w.WriteHeader(http.StatusOK)
+ _, _ = w.Write([]byte(constants.XMLHeader + ``))
+
+ return
+ }
+
w.WriteHeader(http.StatusOK)
- _, _ = w.Write([]byte(constants.XMLHeader + ``))
+ _, _ = w.Write([]byte(constants.XMLHeader))
+ _, _ = w.Write(data)
}
// HandleMargeDeviceGroupServer returns grouping server information (404 by default if not a server).
func (s *Server) HandleMargeDeviceGroupServer(w http.ResponseWriter, r *http.Request) {
- // Not in a group as server
http.NotFound(w, r)
}
// HandleMargeDeviceGroupMember returns grouping member information (404 by default if not a member).
func (s *Server) HandleMargeDeviceGroupMember(w http.ResponseWriter, r *http.Request) {
- // Not in a group as member
http.NotFound(w, r)
}
+// HandleMargeAddGroup creates a new stereo group for an account.
+func (s *Server) HandleMargeAddGroup(w http.ResponseWriter, r *http.Request) {
+ account := chi.URLParam(r, "account")
+ if !validatePathID(account) {
+ http.Error(w, "Invalid account ID", http.StatusBadRequest)
+ return
+ }
+
+ body, err := io.ReadAll(r.Body)
+ if err != nil {
+ http.Error(w, "Failed to read body", http.StatusInternalServerError)
+ return
+ }
+
+ var group models.Group
+ if err := xml.Unmarshal(body, &group); err != nil {
+ http.Error(w, "Invalid XML", http.StatusBadRequest)
+ return
+ }
+
+ id, err := s.ds.AddGroup(account, &group)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ data, err := xml.Marshal(&group)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/vnd.bose.streaming-v1.2+xml")
+ w.Header().Set("Location", s.serverURL+"/account/"+account+"/group/"+id)
+ w.WriteHeader(http.StatusCreated)
+ _, _ = w.Write([]byte(constants.XMLHeader))
+ _, _ = w.Write(data)
+}
+
+// HandleMargeModifyGroup updates the name of an existing stereo group.
+func (s *Server) HandleMargeModifyGroup(w http.ResponseWriter, r *http.Request) {
+ account := chi.URLParam(r, "account")
+ groupID := chi.URLParam(r, "groupId")
+
+ if !validatePathID(account) || !validatePathID(groupID) {
+ http.Error(w, "Invalid ID", http.StatusBadRequest)
+ return
+ }
+
+ body, err := io.ReadAll(r.Body)
+ if err != nil {
+ http.Error(w, "Failed to read body", http.StatusInternalServerError)
+ return
+ }
+
+ var req models.Group
+ if err := xml.Unmarshal(body, &req); err != nil {
+ http.Error(w, "Invalid XML", http.StatusBadRequest)
+ return
+ }
+
+ updated, err := s.ds.ModifyGroup(account, groupID, req.Name)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusNotFound)
+ return
+ }
+
+ data, err := xml.Marshal(updated)
+ if err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/vnd.bose.streaming-v1.2+xml")
+ _, _ = w.Write([]byte(constants.XMLHeader))
+ _, _ = w.Write(data)
+}
+
+// HandleMargeDeleteGroup removes a stereo group.
+func (s *Server) HandleMargeDeleteGroup(w http.ResponseWriter, r *http.Request) {
+ account := chi.URLParam(r, "account")
+ groupID := chi.URLParam(r, "groupId")
+
+ if !validatePathID(account) || !validatePathID(groupID) {
+ http.Error(w, "Invalid ID", http.StatusBadRequest)
+ return
+ }
+
+ if err := s.ds.DeleteGroup(account, groupID); err != nil {
+ http.Error(w, err.Error(), http.StatusNotFound)
+ return
+ }
+
+ w.Header().Set("Content-Type", "application/vnd.bose.streaming-v1.2+xml")
+ w.WriteHeader(http.StatusOK)
+ _, _ = w.Write([]byte(constants.XMLHeader + `Group deleted successfully`))
+}
+
// HandleMusicProviderIsEligible returns the music provider eligibility.
func (s *Server) HandleMusicProviderIsEligible(w http.ResponseWriter, _ *http.Request) {
// For now, we return false as seen in the interaction sample.
diff --git a/pkg/service/handlers/handlers_marge_test.go b/pkg/service/handlers/handlers_marge_test.go
index 826f130..3ed6b20 100644
--- a/pkg/service/handlers/handlers_marge_test.go
+++ b/pkg/service/handlers/handlers_marge_test.go
@@ -1563,3 +1563,177 @@ func TestMargeAdvancedFeatures(t *testing.T) {
}
})
}
+
+func TestMargeGroupCRUD(t *testing.T) {
+ tempDir, err := os.MkdirTemp("", "st-group-test-*")
+ if err != nil {
+ t.Fatalf("Failed to create temp dir: %v", err)
+ }
+
+ defer func() { _ = os.RemoveAll(tempDir) }()
+
+ ds := datastore.NewDataStore(tempDir)
+ r, _ := setupRouter("http://localhost:8001", ds)
+ ts := httptest.NewServer(r)
+ defer ts.Close()
+
+ account := "ACC001"
+ device1 := "AABBCCDDEEFF"
+ device2 := "112233445566"
+
+ groupXML := `
+
+ Living Room Stereo
+ ` + device1 + `
+
+ ` + device1 + `LEFT192.168.1.10
+ ` + device2 + `RIGHT192.168.1.11
+
+ 192.168.1.10
+`
+
+ var groupID string
+
+ t.Run("GET device group returns empty group before creation", func(t *testing.T) {
+ res, err := http.Get(ts.URL + "/marge/streaming/account/" + account + "/device/" + device1 + "/group")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer func() { _ = res.Body.Close() }()
+
+ if res.StatusCode != http.StatusOK {
+ t.Errorf("Expected 200, got %d", res.StatusCode)
+ }
+ body, _ := io.ReadAll(res.Body)
+ if !strings.Contains(string(body), " element, got: %s", body)
+ }
+ })
+
+ t.Run("POST group creates a new group and returns 201 with ID", func(t *testing.T) {
+ res, err := http.Post(
+ ts.URL+"/marge/streaming/account/"+account+"/group",
+ "application/xml",
+ bytes.NewBufferString(groupXML),
+ )
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer func() { _ = res.Body.Close() }()
+
+ if res.StatusCode != http.StatusCreated {
+ body, _ := io.ReadAll(res.Body)
+ t.Fatalf("Expected 201 Created, got %d: %s", res.StatusCode, body)
+ }
+
+ body, _ := io.ReadAll(res.Body)
+ if !strings.Contains(string(body), ` with id attr: %s", body)
+ }
+
+ // Parse out the group ID from the response XML
+ type groupResp struct {
+ ID string `xml:"id,attr"`
+ }
+ var gr groupResp
+ if err := xml.Unmarshal(body, &gr); err != nil {
+ t.Fatalf("Failed to unmarshal group response: %v", err)
+ }
+ if gr.ID == "" {
+ t.Fatalf("Response group has no ID: %s", body)
+ }
+ groupID = gr.ID
+ })
+
+ t.Run("GET device group returns the group after creation", func(t *testing.T) {
+ res, err := http.Get(ts.URL + "/marge/streaming/account/" + account + "/device/" + device1 + "/group")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer func() { _ = res.Body.Close() }()
+
+ if res.StatusCode != http.StatusOK {
+ t.Errorf("Expected 200, got %d", res.StatusCode)
+ }
+ body, _ := io.ReadAll(res.Body)
+ if !strings.Contains(string(body), "Living Room Stereo") {
+ t.Errorf("Expected group name in response: %s", body)
+ }
+ })
+
+ t.Run("POST group/{groupId} renames the group", func(t *testing.T) {
+ if groupID == "" {
+ t.Skip("No group ID from prior subtest")
+ }
+ modXML := `Bedroom Stereo` + device1 + ``
+ req, _ := http.NewRequest(http.MethodPost,
+ ts.URL+"/marge/streaming/account/"+account+"/group/"+groupID,
+ bytes.NewBufferString(modXML),
+ )
+ req.Header.Set("Content-Type", "application/xml")
+ res, err := http.DefaultClient.Do(req)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer func() { _ = res.Body.Close() }()
+
+ if res.StatusCode != http.StatusOK {
+ body, _ := io.ReadAll(res.Body)
+ t.Fatalf("Expected 200, got %d: %s", res.StatusCode, body)
+ }
+ body, _ := io.ReadAll(res.Body)
+ if !strings.Contains(string(body), "Bedroom Stereo") {
+ t.Errorf("Expected updated name in response: %s", body)
+ }
+ })
+
+ t.Run("DELETE group/{groupId} removes the group", func(t *testing.T) {
+ if groupID == "" {
+ t.Skip("No group ID from prior subtest")
+ }
+ req, _ := http.NewRequest(http.MethodDelete,
+ ts.URL+"/marge/streaming/account/"+account+"/group/"+groupID,
+ nil,
+ )
+ res, err := http.DefaultClient.Do(req)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer func() { _ = res.Body.Close() }()
+
+ if res.StatusCode != http.StatusOK {
+ body, _ := io.ReadAll(res.Body)
+ t.Fatalf("Expected 200, got %d: %s", res.StatusCode, body)
+ }
+ })
+
+ t.Run("DELETE group/{groupId} returns 404 for missing group", func(t *testing.T) {
+ req, _ := http.NewRequest(http.MethodDelete,
+ ts.URL+"/marge/streaming/account/"+account+"/group/9999999",
+ nil,
+ )
+ res, err := http.DefaultClient.Do(req)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer func() { _ = res.Body.Close() }()
+
+ if res.StatusCode != http.StatusNotFound {
+ t.Errorf("Expected 404, got %d", res.StatusCode)
+ }
+ })
+
+ t.Run("GET device group is empty after deletion", func(t *testing.T) {
+ res, err := http.Get(ts.URL + "/marge/streaming/account/" + account + "/device/" + device1 + "/group")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer func() { _ = res.Body.Close() }()
+
+ body, _ := io.ReadAll(res.Body)
+ // Should be back to empty
+ if strings.Contains(string(body), "Bedroom Stereo") {
+ t.Errorf("Group should be gone after deletion: %s", body)
+ }
+ })
+}
diff --git a/pkg/service/handlers/main_test.go b/pkg/service/handlers/main_test.go
index 0740d8e..4d870b4 100644
--- a/pkg/service/handlers/main_test.go
+++ b/pkg/service/handlers/main_test.go
@@ -58,6 +58,9 @@ func setupRouter(targetURL string, ds *datastore.DataStore) (*chi.Mux, *Server)
r.Get("/account/{account}/device/{device}/group/", server.HandleMargeDeviceGroup)
r.Get("/account/{account}/device/{device}/group/server", server.HandleMargeDeviceGroupServer)
r.Get("/account/{account}/device/{device}/group/member", server.HandleMargeDeviceGroupMember)
+ r.Post("/account/{account}/group", server.HandleMargeAddGroup)
+ r.Post("/account/{account}/group/{groupId}", server.HandleMargeModifyGroup)
+ r.Delete("/account/{account}/group/{groupId}", server.HandleMargeDeleteGroup)
r.Post("/device_setting/account/{account}/device/{device}/device_settings", server.HandleMargeUpdateDeviceSettings)
r.Get("/account/{account}/emailaddress", server.HandleMargeGetEmailAddress)
r.Get("/account/{account}/full", server.HandleMargeAccountFull)
@@ -87,6 +90,9 @@ func setupRouter(targetURL string, ds *datastore.DataStore) (*chi.Mux, *Server)
r.Get("/{account}/devices/{device}/group/", server.HandleMargeDeviceGroup)
r.Get("/{account}/devices/{device}/group/server", server.HandleMargeDeviceGroupServer)
r.Get("/{account}/devices/{device}/group/member", server.HandleMargeDeviceGroupMember)
+ r.Post("/{account}/group", server.HandleMargeAddGroup)
+ r.Post("/{account}/group/{groupId}", server.HandleMargeModifyGroup)
+ r.Delete("/{account}/group/{groupId}", server.HandleMargeDeleteGroup)
}
// Setup Marge for tests