mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-19 09:06:14 +00:00
Mirrors the .md/.txt sweep across all tracked _test.go, testdata XML,
and .http integration files. Test files are self-contained (producer
+ assertion in the same file), so the matched-pair swap stays green
under `go test ./...`.
Mapping applied:
192.168.178.[0-9]+ → 192.0.2.[same]
192.168.1.[0-9]+ → 192.0.2.[same]
Sound Machinechen → Living Room SoundTouch
A Sound Machine → Kitchen SoundTouch
A81B6A536A98 + case/separator variants → AABBCCDDEEFF (etc.)
A81B6A849D99 → AABBCCDDEE01
A81B6A849D88 → AABBCCDDEE03
A81B6A536A09 → AABBCCDDEE04
884AEAEEBD27 → AABBCCDDEE02
3230304 → 1000001
9569497 → 1000002
Two semantic fixes alongside the bulk swap:
- pkg/service/zeroconf/zeroconf_test.go: the "private 192" and
"strips query" cases pin acceptance of RFC-1918 192.168/16. They
must use a real 192.168 value; doc-range IPs would (correctly) be
rejected by validateZcBaseURL. Switched to 192.168.10.10 — generic
enough not to match any home LAN default, real enough for the
validator. Added a comment explaining why this single test still
carries a 192.168 literal.
- pkg/service/setup/setup_test.go: TestTestDNSRedirection mocks the
device's `od -An -tu1` byte output, which is space-separated
octets ("192 168 1 100"). My sed only matched the dot-separated
form, so the mock was returning the old IP while the test
assertions had moved to the doc range. Updated to " 192 0 2 100".
go build ./... clean. go test ./... clean (only TestDocsConsistency
remains failing, which is a pre-existing/untracked-file issue).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
235 lines
5.8 KiB
Go
235 lines
5.8 KiB
Go
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 := `<?xml version="1.0" encoding="UTF-8" ?>
|
|
<group id="1234567">
|
|
<name>Living Room Pair</name>
|
|
<masterDeviceId>9070658C9D4A</masterDeviceId>
|
|
<roles>
|
|
<groupRole>
|
|
<deviceId>9070658C9D4A</deviceId>
|
|
<role>LEFT</role>
|
|
<ipAddress>192.0.2.131</ipAddress>
|
|
</groupRole>
|
|
<groupRole>
|
|
<deviceId>F45EAB3115DA</deviceId>
|
|
<role>RIGHT</role>
|
|
<ipAddress>192.0.2.134</ipAddress>
|
|
</groupRole>
|
|
</roles>
|
|
<senderIPAddress>192.0.2.131</senderIPAddress>
|
|
<status>GROUP_OK</status>
|
|
</group>`
|
|
|
|
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(`<group />`))
|
|
}))
|
|
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 <group/>, 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{"<role>LEFT</role>", "<role>RIGHT</role>", "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(`<group />`))
|
|
}))
|
|
defer server.Close()
|
|
|
|
if err := createTestClient(server.URL).RemoveGroup(); err != nil {
|
|
t.Fatalf("RemoveGroup: %v", err)
|
|
}
|
|
}
|