mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-09-07 15:07:17 +00:00
fix(stereo): verify wrapped missing-group cleanup
This commit is contained in:
committed by
Tobias Gesellchen
parent
a0d1fa7a04
commit
22586da982
@@ -1,6 +1,7 @@
|
||||
package stereopair
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -64,25 +65,67 @@ func DeleteMargeGroupGeneration(httpClient *http.Client, ref GenerationRef) erro
|
||||
return fmt.Errorf("close Marge generation cleanup response: %w", closeErr)
|
||||
}
|
||||
|
||||
group, verifyErr := getMargeDeviceGroup(httpClient, ref)
|
||||
if verifyErr != nil {
|
||||
return fmt.Errorf("verify Marge group generation deletion: %w", verifyErr)
|
||||
}
|
||||
|
||||
if group.IsEmpty() || group.ID != ref.GroupID {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("delete Marge group generation: generation %s is still active", ref.GroupID)
|
||||
return verifyMargeGroupGenerationDeleted(httpClient, ref)
|
||||
}
|
||||
|
||||
body, _ := io.ReadAll(io.LimitReader(response.Body, 1024))
|
||||
_ = response.Body.Close()
|
||||
body, readErr := io.ReadAll(io.LimitReader(response.Body, 1025))
|
||||
closeErr := response.Body.Close()
|
||||
if readErr != nil {
|
||||
return fmt.Errorf("read Marge generation cleanup response: %w", readErr)
|
||||
}
|
||||
if closeErr != nil {
|
||||
return fmt.Errorf("close Marge generation cleanup response: %w", closeErr)
|
||||
}
|
||||
if len(body) > 1024 {
|
||||
return fmt.Errorf("delete Marge group generation: HTTP %d response exceeds 1024 bytes", response.StatusCode)
|
||||
}
|
||||
if response.StatusCode == http.StatusInternalServerError && margeWrappedGroupNotFound(body, ref) {
|
||||
return verifyMargeGroupGenerationDeleted(httpClient, ref)
|
||||
}
|
||||
|
||||
return fmt.Errorf("delete Marge group generation: HTTP %d: %s",
|
||||
response.StatusCode, strings.TrimSpace(string(body)))
|
||||
}
|
||||
|
||||
func verifyMargeGroupGenerationDeleted(httpClient *http.Client, ref GenerationRef) error {
|
||||
group, err := getMargeDeviceGroup(httpClient, ref)
|
||||
if err != nil {
|
||||
return fmt.Errorf("verify Marge group generation deletion: %w", err)
|
||||
}
|
||||
|
||||
if group.IsEmpty() || group.ID != ref.GroupID {
|
||||
return nil
|
||||
}
|
||||
|
||||
return fmt.Errorf("delete Marge group generation: generation %s is still active", ref.GroupID)
|
||||
}
|
||||
|
||||
func margeWrappedGroupNotFound(body []byte, ref GenerationRef) bool {
|
||||
var response struct {
|
||||
XMLName xml.Name
|
||||
Message string `xml:",chardata"`
|
||||
}
|
||||
decoder := xml.NewDecoder(bytes.NewReader(body))
|
||||
if err := decoder.Decode(&response); err != nil || response.XMLName.Local != "error" {
|
||||
return false
|
||||
}
|
||||
for {
|
||||
token, err := decoder.Token()
|
||||
if err == io.EOF {
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if data, ok := token.(xml.CharData); !ok || strings.TrimSpace(string(data)) != "" {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
want := fmt.Sprintf("Unexpected error: 404: Group %s does not exist in account %s", ref.GroupID, ref.AccountID)
|
||||
return strings.TrimSpace(response.Message) == want
|
||||
}
|
||||
|
||||
// RenameMargeGroupGeneration updates and verifies the name of one persisted
|
||||
// generation at the backend configured by fresh speaker info. Topology, rather
|
||||
// than the old name, is the retry guard so a degraded rename can converge.
|
||||
|
||||
@@ -2,6 +2,8 @@ package stereopair
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
@@ -176,6 +178,138 @@ func TestDeleteMargeGroupGenerationAcceptsVerifiedAbsenceAfter404(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteMargeGroupGenerationAcceptsVerifiedAbsenceAfterWrapped404(t *testing.T) {
|
||||
deleteSeen := false
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodDelete {
|
||||
deleteSeen = true
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, _ = w.Write([]byte(`<error>Unexpected error: 404: Group PAIR1 does not exist in account ACCOUNT1</error>`))
|
||||
return
|
||||
}
|
||||
|
||||
if deleteSeen {
|
||||
_, _ = w.Write([]byte(`<group/>`))
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = w.Write([]byte(`<group id="PAIR1"><masterDeviceId>LEFT-ID</masterDeviceId><roles><groupRole><deviceId>LEFT-ID</deviceId><role>LEFT</role><ipAddress>192.0.2.10</ipAddress></groupRole><groupRole><deviceId>RIGHT-ID</deviceId><role>RIGHT</role><ipAddress>192.0.2.11</ipAddress></groupRole></roles></group>`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := DeleteMargeGroupGeneration(server.Client(), GenerationRef{
|
||||
MargeURL: server.URL, AccountID: "ACCOUNT1", GroupID: "PAIR1", DeviceID: "LEFT-ID",
|
||||
ExpectedGroup: margeTestGroup("PAIR1"),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("DeleteMargeGroupGeneration: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteMargeGroupGenerationRejectsWrapped404WhileGenerationRemains(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodDelete {
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, _ = w.Write([]byte(`<error>Unexpected error: 404: Group PAIR1 does not exist in account ACCOUNT1</error>`))
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = w.Write([]byte(`<group id="PAIR1"><masterDeviceId>LEFT-ID</masterDeviceId><roles><groupRole><deviceId>LEFT-ID</deviceId><role>LEFT</role><ipAddress>192.0.2.10</ipAddress></groupRole><groupRole><deviceId>RIGHT-ID</deviceId><role>RIGHT</role><ipAddress>192.0.2.11</ipAddress></groupRole></roles></group>`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := DeleteMargeGroupGeneration(server.Client(), GenerationRef{
|
||||
MargeURL: server.URL, AccountID: "ACCOUNT1", GroupID: "PAIR1", DeviceID: "LEFT-ID",
|
||||
ExpectedGroup: margeTestGroup("PAIR1"),
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), "still active") {
|
||||
t.Fatalf("error = %v, want failed postcondition", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMargeWrappedGroupNotFoundRequiresExactError(t *testing.T) {
|
||||
ref := GenerationRef{AccountID: "ACCOUNT1", GroupID: "PAIR1"}
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
body string
|
||||
want bool
|
||||
}{
|
||||
{name: "exact", body: `<error>Unexpected error: 404: Group PAIR1 does not exist in account ACCOUNT1</error>`, want: true},
|
||||
{name: "xml declaration and whitespace", body: xml.Header + ` <error> Unexpected error: 404: Group PAIR1 does not exist in account ACCOUNT1 </error> `, want: true},
|
||||
{name: "wrong group", body: `<error>Unexpected error: 404: Group OTHER does not exist in account ACCOUNT1</error>`},
|
||||
{name: "wrong account", body: `<error>Unexpected error: 404: Group PAIR1 does not exist in account OTHER</error>`},
|
||||
{name: "wrong inner status", body: `<error>Unexpected error: 500: Group PAIR1 does not exist in account ACCOUNT1</error>`},
|
||||
{name: "wrong root", body: `<status>Unexpected error: 404: Group PAIR1 does not exist in account ACCOUNT1</status>`},
|
||||
{name: "trailing element", body: `<error>Unexpected error: 404: Group PAIR1 does not exist in account ACCOUNT1</error><ignored/>`},
|
||||
{name: "malformed", body: `<error>Unexpected error: 404: Group PAIR1 does not exist in account ACCOUNT1`},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
if got := margeWrappedGroupNotFound([]byte(test.body), ref); got != test.want {
|
||||
t.Fatalf("margeWrappedGroupNotFound() = %t, want %t", got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type failingReadCloser struct{}
|
||||
|
||||
func (failingReadCloser) Read([]byte) (int, error) { return 0, errors.New("read failed") }
|
||||
func (failingReadCloser) Close() error { return nil }
|
||||
|
||||
type cleanupRoundTripper struct {
|
||||
responses []*http.Response
|
||||
}
|
||||
|
||||
func (r *cleanupRoundTripper) RoundTrip(*http.Request) (*http.Response, error) {
|
||||
response := r.responses[0]
|
||||
r.responses = r.responses[1:]
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func TestDeleteMargeGroupGenerationRejectsUnreadableWrappedResponse(t *testing.T) {
|
||||
initial := `<group id="PAIR1"><masterDeviceId>LEFT-ID</masterDeviceId><roles><groupRole><deviceId>LEFT-ID</deviceId><role>LEFT</role><ipAddress>192.0.2.10</ipAddress></groupRole><groupRole><deviceId>RIGHT-ID</deviceId><role>RIGHT</role><ipAddress>192.0.2.11</ipAddress></groupRole></roles></group>`
|
||||
httpClient := &http.Client{Transport: &cleanupRoundTripper{responses: []*http.Response{
|
||||
{StatusCode: http.StatusOK, Body: io.NopCloser(strings.NewReader(initial))},
|
||||
{StatusCode: http.StatusInternalServerError, Body: failingReadCloser{}},
|
||||
}}}
|
||||
|
||||
err := DeleteMargeGroupGeneration(httpClient, GenerationRef{
|
||||
MargeURL: "http://example.test", AccountID: "ACCOUNT1", GroupID: "PAIR1", DeviceID: "LEFT-ID",
|
||||
ExpectedGroup: margeTestGroup("PAIR1"),
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), "read failed") {
|
||||
t.Fatalf("error = %v, want read failure", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteMargeGroupGenerationRejectsOversizedWrappedResponse(t *testing.T) {
|
||||
deleteSeen := false
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method == http.MethodDelete {
|
||||
deleteSeen = true
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
_, _ = w.Write([]byte(`<error>Unexpected error: 404: Group PAIR1 does not exist in account ACCOUNT1</error>` + strings.Repeat(" ", 1024)))
|
||||
return
|
||||
}
|
||||
|
||||
if deleteSeen {
|
||||
_, _ = w.Write([]byte(`<group/>`))
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = w.Write([]byte(`<group id="PAIR1"><masterDeviceId>LEFT-ID</masterDeviceId><roles><groupRole><deviceId>LEFT-ID</deviceId><role>LEFT</role><ipAddress>192.0.2.10</ipAddress></groupRole><groupRole><deviceId>RIGHT-ID</deviceId><role>RIGHT</role><ipAddress>192.0.2.11</ipAddress></groupRole></roles></group>`))
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
err := DeleteMargeGroupGeneration(server.Client(), GenerationRef{
|
||||
MargeURL: server.URL, AccountID: "ACCOUNT1", GroupID: "PAIR1", DeviceID: "LEFT-ID",
|
||||
ExpectedGroup: margeTestGroup("PAIR1"),
|
||||
})
|
||||
if err == nil || !strings.Contains(err.Error(), "exceeds 1024 bytes") {
|
||||
t.Fatalf("error = %v, want oversized response rejection", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeleteMargeGroupGenerationRejectsUnrelatedGenerationBeforeDelete(t *testing.T) {
|
||||
deleteCalls := 0
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
Reference in New Issue
Block a user