fix(models): surface DeviceError's name attribute, not just its message

ErrorsResponse.Error() only returned the <error> element's text body,
dropping the name attribute entirely. Some speaker error responses
have a Message that just restates Value as text (e.g. a bare "1047"
for SOURCE_ALREADY_REMOVED), so callers only ever saw the useless
numeric string. Found while live-debugging a Deezer account
add/remove cycle on real hardware, where the raw XML
(<error value="1047" name="SOURCE_ALREADY_REMOVED">1047</error>)
carried real information only in the name attribute.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Tobias Gesellchen
2026-08-09 19:47:49 +02:00
co-authored by Claude Sonnet 5
parent aae1673451
commit 27da179082
3 changed files with 96 additions and 3 deletions
+24 -1
View File
@@ -2,6 +2,8 @@ package models
import (
"encoding/xml"
"fmt"
"strconv"
"time"
)
@@ -80,7 +82,7 @@ type ErrorsResponse struct {
// Error implements the error interface for ErrorsResponse
func (e *ErrorsResponse) Error() string {
if len(e.Errors) > 0 {
return e.Errors[0].Message
return e.Errors[0].Error()
}
return "unknown API error"
@@ -93,6 +95,27 @@ type DeviceError struct {
Message string `xml:",chardata"`
}
// Error implements the error interface for DeviceError. Some speakers
// return a Message that just restates Value as text (e.g. a bare "1047"
// for an error the firmware has no localized string for) — Name is the
// only informative part in that case, so it's always included unless
// Message already carries it.
func (e DeviceError) Error() string {
if e.Name == "" {
if e.Message == "" {
return fmt.Sprintf("device error %d", e.Value)
}
return e.Message
}
if e.Message == "" || e.Message == e.Name || e.Message == strconv.Itoa(e.Value) {
return fmt.Sprintf("%s (%d)", e.Name, e.Value)
}
return fmt.Sprintf("%s: %s", e.Name, e.Message)
}
// DiscoveredDevice represents a device found through network discovery
type DiscoveredDevice struct {
Name string `json:"name"`
+69
View File
@@ -0,0 +1,69 @@
package models
import "testing"
func TestDeviceError_Error(t *testing.T) {
tests := []struct {
name string
err DeviceError
expected string
}{
{
name: "message repeats the numeric value (real speaker case)",
err: DeviceError{Value: 1047, Name: "SOURCE_ALREADY_REMOVED", Message: "1047"},
expected: "SOURCE_ALREADY_REMOVED (1047)",
},
{
name: "message is empty",
err: DeviceError{Value: 1047, Name: "SOURCE_ALREADY_REMOVED", Message: ""},
expected: "SOURCE_ALREADY_REMOVED (1047)",
},
{
name: "message is meaningful and distinct from name",
err: DeviceError{Value: 1029, Name: "UNKNOWN_ACTION_ERROR", Message: "This version of SCM does not support spotify create account functionality."},
expected: "UNKNOWN_ACTION_ERROR: This version of SCM does not support spotify create account functionality.",
},
{
name: "name is empty, message carries the detail",
err: DeviceError{Value: 500, Name: "", Message: "internal error"},
expected: "internal error",
},
{
name: "both name and message are empty",
err: DeviceError{Value: 500, Name: "", Message: ""},
expected: "device error 500",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := tt.err.Error(); got != tt.expected {
t.Errorf("expected %q, got %q", tt.expected, got)
}
})
}
}
func TestErrorsResponse_Error(t *testing.T) {
t.Run("delegates to the first DeviceError", func(t *testing.T) {
errs := &ErrorsResponse{
Errors: []DeviceError{
{Value: 1047, Name: "SOURCE_ALREADY_REMOVED", Message: "1047"},
},
}
expected := "SOURCE_ALREADY_REMOVED (1047)"
if got := errs.Error(); got != expected {
t.Errorf("expected %q, got %q", expected, got)
}
})
t.Run("no errors", func(t *testing.T) {
errs := &ErrorsResponse{}
expected := "unknown API error"
if got := errs.Error(); got != expected {
t.Errorf("expected %q, got %q", expected, got)
}
})
}