package client import ( "encoding/xml" "io" "net/http" "net/http/httptest" "strings" "testing" "github.com/gesellix/bose-soundtouch/pkg/models" ) func TestClient_GetGroup_Configured(t *testing.T) { responseXML := ` Living Room Pair 9070658C9D4A 9070658C9D4A LEFT 192.0.2.131 F45EAB3115DA RIGHT 192.0.2.134 192.0.2.131 GROUP_OK ` server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/getGroup" { t.Errorf("path = %q, want /getGroup", r.URL.Path) } if r.Method != http.MethodGet { t.Errorf("method = %s, want GET", r.Method) } w.Header().Set("Content-Type", "application/xml") _, _ = w.Write([]byte(responseXML)) })) defer server.Close() g, err := createTestClient(server.URL).GetGroup() if err != nil { t.Fatalf("GetGroup: %v", err) } if g.ID != "1234567" { t.Errorf("ID = %q, want 1234567", g.ID) } if g.Name != "Living Room Pair" { t.Errorf("Name = %q, want Living Room Pair", g.Name) } if g.MasterDeviceID != "9070658C9D4A" { t.Errorf("MasterDeviceID = %q", g.MasterDeviceID) } if g.Status != "GROUP_OK" { t.Errorf("Status = %q, want GROUP_OK", g.Status) } if len(g.Roles.Roles) != 2 { t.Fatalf("roles = %d, want 2", len(g.Roles.Roles)) } if g.Roles.Roles[0].Role != "LEFT" || g.Roles.Roles[1].Role != "RIGHT" { t.Errorf("role order LEFT/RIGHT not preserved: %+v", g.Roles.Roles) } if g.IsEmpty() { t.Errorf("IsEmpty = true for populated group") } } func TestClient_GetGroup_Empty(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/xml") _, _ = w.Write([]byte(``)) })) defer server.Close() g, err := createTestClient(server.URL).GetGroup() if err != nil { t.Fatalf("GetGroup: %v", err) } if !g.IsEmpty() { t.Errorf("IsEmpty = false for , got %+v", g) } } func TestClient_AddGroup(t *testing.T) { var capturedBody string server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/addGroup" { t.Errorf("path = %q, want /addGroup", r.URL.Path) } if r.Method != http.MethodPost { t.Errorf("method = %s, want POST", r.Method) } body, _ := io.ReadAll(r.Body) capturedBody = string(body) // Echo the request back with an assigned ID and GROUP_OK status — // matches real device behaviour. var got models.Group if err := xml.Unmarshal(body, &got); err != nil { t.Fatalf("decode request body: %v", err) } got.ID = "9999999" got.Status = "GROUP_OK" got.SenderIPAddress = "192.0.2.131" w.Header().Set("Content-Type", "application/xml") enc, _ := xml.Marshal(&got) _, _ = w.Write(enc) })) defer server.Close() req := &models.Group{ Name: "Living Room", MasterDeviceID: "9070658C9D4A", Roles: models.GroupRoles{ Roles: []models.GroupRole{ {DeviceID: "9070658C9D4A", Role: "LEFT", IPAddress: "192.0.2.131"}, {DeviceID: "F45EAB3115DA", Role: "RIGHT", IPAddress: "192.0.2.134"}, }, }, } resp, err := createTestClient(server.URL).AddGroup(req) if err != nil { t.Fatalf("AddGroup: %v", err) } if resp.ID != "9999999" { t.Errorf("response ID = %q, want 9999999", resp.ID) } if resp.Status != "GROUP_OK" { t.Errorf("response Status = %q, want GROUP_OK", resp.Status) } // Wire-shape sanity: the request body must carry both roles and the // master ID (the device validates these on the wire). for _, want := range []string{"LEFT", "RIGHT", "9070658C9D4A"} { if !strings.Contains(capturedBody, want) { t.Errorf("request body missing %q\nbody:\n%s", want, capturedBody) } } } func TestClient_UpdateGroup_RenameRoundtrip(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/updateGroup" { t.Errorf("path = %q, want /updateGroup", r.URL.Path) } body, _ := io.ReadAll(r.Body) var got models.Group if err := xml.Unmarshal(body, &got); err != nil { t.Fatalf("decode: %v", err) } got.Status = "GROUP_OK" w.Header().Set("Content-Type", "application/xml") enc, _ := xml.Marshal(&got) _, _ = w.Write(enc) })) defer server.Close() req := &models.Group{ ID: "1234567", Name: "Kitchen Pair", MasterDeviceID: "AAAA", Roles: models.GroupRoles{ Roles: []models.GroupRole{ {DeviceID: "AAAA", Role: "LEFT"}, {DeviceID: "BBBB", Role: "RIGHT"}, }, }, } resp, err := createTestClient(server.URL).UpdateGroup(req) if err != nil { t.Fatalf("UpdateGroup: %v", err) } if resp.Name != "Kitchen Pair" { t.Errorf("Name = %q, want Kitchen Pair", resp.Name) } if resp.ID != "1234567" { t.Errorf("ID = %q, want 1234567", resp.ID) } } func TestClient_RemoveGroup(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/removeGroup" { t.Errorf("path = %q, want /removeGroup", r.URL.Path) } // The wiki specifies GET (not DELETE) for /removeGroup. We honour // that, surprising as it is for a state-mutating endpoint. if r.Method != http.MethodGet { t.Errorf("method = %s, want GET", r.Method) } w.Header().Set("Content-Type", "application/xml") _, _ = w.Write([]byte(``)) })) defer server.Close() if err := createTestClient(server.URL).RemoveGroup(); err != nil { t.Fatalf("RemoveGroup: %v", err) } }