Add more e2e tests

This commit is contained in:
Tobias Gesellchen
2026-03-29 19:14:48 +02:00
parent 505a189ce5
commit a06657f3f5
10 changed files with 240 additions and 90 deletions
+1
View File
@@ -121,6 +121,7 @@ test-http-client:
--env ci \
/workdir/create_account.http \
/workdir/register_device.http \
/workdir/customer_support.http \
/workdir/power_on.http \
/workdir/get_provider_settings.http \
/workdir/get_full_account.http \
+4
View File
@@ -679,6 +679,10 @@ func setupRouter(server *handlers.Server) *chi.Mux {
r.Get("/sourceproviders", server.HandleMargeSourceProviders)
r.Post("/account", server.HandleMargeCreateAccount)
r.Post("/account/login", server.HandleMargeLogin)
r.Route("/account/{account}/device", func(r chi.Router) {
r.Post("/", server.HandleMargeAddDevice)
r.Post("/{device}", server.HandleMargeAddDevice)
})
r.Get("/account/{account}/device/{device}/recent", server.HandleMargeRecents)
r.Post("/account/{account}/device/{device}/recent", server.HandleMargeAddRecent)
r.Get("/account/{account}/device/{device}/presets", server.HandleMargePresets)
+102 -75
View File
@@ -707,40 +707,7 @@ func (ds *DataStore) SaveDeviceInfo(account, device string, info *models.Service
}
// Try to load existing device info to avoid overwriting existing details with empty values.
existing, _ := ds.getDeviceInfoNoLock(account, device)
if existing != nil {
if info.Name == "" {
info.Name = existing.Name
}
if info.ProductCode == "" {
info.ProductCode = existing.ProductCode
}
if info.DeviceSerialNumber == "" {
info.DeviceSerialNumber = existing.DeviceSerialNumber
}
if info.ProductSerialNumber == "" {
info.ProductSerialNumber = existing.ProductSerialNumber
}
if info.FirmwareVersion == "" {
info.FirmwareVersion = existing.FirmwareVersion
}
if info.IPAddress == "" {
info.IPAddress = existing.IPAddress
}
if info.MacAddress == "" {
info.MacAddress = existing.MacAddress
}
if info.DiscoveryMethod == "" {
info.DiscoveryMethod = existing.DiscoveryMethod
}
}
ds.mergeWithExistingDeviceInfo(account, device, info)
dir := ds.AccountDeviceDir(account, device)
if err := os.MkdirAll(dir, 0755); err != nil {
@@ -749,12 +716,6 @@ func (ds *DataStore) SaveDeviceInfo(account, device string, info *models.Service
path := filepath.Join(dir, constants.DeviceInfoFile)
type ComponentXML struct {
ComponentCategory string `xml:"componentCategory"`
SoftwareVersion string `xml:"softwareVersion,omitempty"`
SerialNumber string `xml:"serialNumber,omitempty"`
}
type NetworkInfoXML struct {
Type string `xml:"type,attr"`
IPAddress string `xml:"ipAddress"`
@@ -767,24 +728,13 @@ func (ds *DataStore) SaveDeviceInfo(account, device string, info *models.Service
Name string `xml:"name"`
Type string `xml:"type"`
ModuleType string `xml:"moduleType"`
Components []ComponentXML `xml:"components>component"`
Components []componentXML `xml:"components>component"`
NetworkInfo []NetworkInfoXML `xml:"networkInfo"`
DiscoveryMethod string `xml:"discoveryMethod,omitempty"`
}
// Parsing product code back to type and moduleType (best effort)
// Python: f"{type} {module_type}"
devType := info.ProductCode
moduleType := ""
for i := 0; i < len(info.ProductCode); i++ {
if info.ProductCode[i] == ' ' {
devType = info.ProductCode[:i]
moduleType = info.ProductCode[i+1:]
break
}
}
devType, moduleType := ds.parseProductCode(info.ProductCode)
ix := InfoXML{
DeviceID: info.DeviceID,
@@ -798,27 +748,7 @@ func (ds *DataStore) SaveDeviceInfo(account, device string, info *models.Service
ix.DiscoveryMethod = "sync_full"
}
for _, comp := range info.Components {
ix.Components = append(ix.Components, ComponentXML{
ComponentCategory: comp.Category,
SoftwareVersion: comp.SoftwareVersion,
SerialNumber: comp.SerialNumber,
})
}
if len(ix.Components) == 0 {
ix.Components = []ComponentXML{
{
ComponentCategory: "SCM",
SoftwareVersion: info.FirmwareVersion,
SerialNumber: info.DeviceSerialNumber,
},
{
ComponentCategory: "PackagedProduct",
SerialNumber: info.ProductSerialNumber,
},
}
}
ix.Components = ds.buildComponentsXML(info)
ix.NetworkInfo = []NetworkInfoXML{
{
@@ -838,7 +768,104 @@ func (ds *DataStore) SaveDeviceInfo(account, device string, info *models.Service
return os.WriteFile(path, append(header, data...), 0644)
}
// SaveAccountInfo saves account-level metadata to the datastore.
func (ds *DataStore) mergeWithExistingDeviceInfo(account, device string, info *models.ServiceDeviceInfo) {
existing, _ := ds.getDeviceInfoNoLock(account, device)
if existing == nil {
return
}
if info.Name == "" {
info.Name = existing.Name
}
if info.ProductCode == "" {
info.ProductCode = existing.ProductCode
}
if info.DeviceSerialNumber == "" {
info.DeviceSerialNumber = existing.DeviceSerialNumber
}
if info.ProductSerialNumber == "" {
info.ProductSerialNumber = existing.ProductSerialNumber
}
if info.FirmwareVersion == "" {
info.FirmwareVersion = existing.FirmwareVersion
}
if info.IPAddress == "" {
info.IPAddress = existing.IPAddress
}
if info.MacAddress == "" {
info.MacAddress = existing.MacAddress
}
if info.DiscoveryMethod == "" {
info.DiscoveryMethod = existing.DiscoveryMethod
}
}
func (ds *DataStore) parseProductCode(productCode string) (string, string) {
devType := productCode
moduleType := ""
for i := 0; i < len(productCode); i++ {
if productCode[i] == ' ' {
devType = productCode[:i]
moduleType = productCode[i+1:]
break
}
}
return devType, moduleType
}
type componentXML struct {
ComponentCategory string `xml:"componentCategory"`
SoftwareVersion string `xml:"softwareVersion,omitempty"`
SerialNumber string `xml:"serialNumber,omitempty"`
}
func (ds *DataStore) buildComponentsXML(info *models.ServiceDeviceInfo) []componentXML {
var components []componentXML
for _, comp := range info.Components {
components = append(components, componentXML{
ComponentCategory: comp.Category,
SoftwareVersion: comp.SoftwareVersion,
SerialNumber: comp.SerialNumber,
})
}
if len(components) == 0 && (info.FirmwareVersion != "" || info.DeviceSerialNumber != "" || info.ProductSerialNumber != "") {
components = []componentXML{
{
ComponentCategory: "SCM",
SoftwareVersion: info.FirmwareVersion,
SerialNumber: info.DeviceSerialNumber,
},
{
ComponentCategory: "PackagedProduct",
SerialNumber: info.ProductSerialNumber,
},
}
} else if len(components) > 0 {
if info.FirmwareVersion != "" {
for i := range components {
if components[i].ComponentCategory == "SCM" {
components[i].SoftwareVersion = info.FirmwareVersion
break
}
}
}
}
return components
}
// SaveAccountInfo stores account-level metadata in the datastore.
func (ds *DataStore) SaveAccountInfo(accountID string, info *models.ServiceAccountInfo) error {
if ds == nil || ds.DataDir == "" || accountID == "" {
return nil
+30 -1
View File
@@ -543,7 +543,7 @@ func (s *Server) HandleMargeCustomerSupport(w http.ResponseWriter, r *http.Reque
}
var req models.CustomerSupportRequest
if err := xml.Unmarshal(body, &req); err != nil {
if err = xml.Unmarshal(body, &req); err != nil {
// Log error but might still return 200 as Bose expects
log.Printf("Failed to unmarshal CustomerSupportRequest: %v", err)
}
@@ -561,5 +561,34 @@ func (s *Server) HandleMargeCustomerSupport(w http.ResponseWriter, r *http.Reque
},
}
s.ds.AddDeviceEvent(req.Device.ID, event)
// Update DeviceInfo if possible
devices, err := s.ds.ListAllDevices()
if err == nil {
var account string
for i := range devices {
dev := &devices[i]
if dev.DeviceID == req.Device.ID {
account = dev.AccountID
break
}
}
if account != "" {
info, err := s.ds.GetDeviceInfo(account, req.Device.ID)
if err == nil && info != nil {
info.IPAddress = req.DiagnosticData.DeviceLandscape.IPAddress
info.FirmwareVersion = req.Device.FirmwareVersion
if len(req.DiagnosticData.DeviceLandscape.MacAddresses) > 0 {
info.MacAddress = req.DiagnosticData.DeviceLandscape.MacAddresses[0]
}
_ = s.ds.SaveDeviceInfo(account, req.Device.ID, info)
}
}
}
w.WriteHeader(http.StatusOK)
}
+38 -10
View File
@@ -1030,11 +1030,23 @@ func TestMargeAdvancedFeatures(t *testing.T) {
})
t.Run("CustomerSupport", func(t *testing.T) {
payload := `<?xml version="1.0" encoding="UTF-8" ?>
account := "A123"
deviceId := "587A628A4042"
macAddress := "AABBCCDDEEFF"
ipAddress := "192.168.1.100"
firmware := "27.0.6"
// Pre-register device
_ = ds.SaveDeviceInfo(account, deviceId, &models.ServiceDeviceInfo{
DeviceID: deviceId,
Name: "TestDevice",
})
payload := fmt.Sprintf(`<?xml version="1.0" encoding="UTF-8" ?>
<device-data>
<device id="587A628A4042">
<device id="%s">
<serialnumber>P123</serialnumber>
<firmware-version>27.0.6</firmware-version>
<firmware-version>%s</firmware-version>
<product product_code="SoundTouch 10" type="5">
<serialnumber>SN123</serialnumber>
</product>
@@ -1042,10 +1054,13 @@ func TestMargeAdvancedFeatures(t *testing.T) {
<diagnostic-data>
<device-landscape>
<rssi>Good</rssi>
<ip-address>192.168.1.100</ip-address>
<macaddresses>
<macaddress>%s</macaddress>
</macaddresses>
<ip-address>%s</ip-address>
</device-landscape>
</diagnostic-data>
</device-data>`
</device-data>`, deviceId, firmware, macAddress, ipAddress)
res, err := http.Post(ts.URL+"/marge/streaming/support/customersupport", "application/vnd.bose.streaming-v1.2+xml", strings.NewReader(payload))
if err != nil {
@@ -1063,17 +1078,15 @@ func TestMargeAdvancedFeatures(t *testing.T) {
}
// Verify event was recorded
events := ds.GetDeviceEvents("587A628A4042")
events := ds.GetDeviceEvents(deviceId)
found := false
for _, e := range events {
if e.Type == "customer-support-upload" {
found = true
if e.Data["firmware"] != "27.0.6" {
t.Errorf("Expected firmware 27.0.6, got %v", e.Data["firmware"])
if e.Data["firmware"] != firmware {
t.Errorf("Expected firmware %s, got %v", firmware, e.Data["firmware"])
}
break
}
}
@@ -1081,6 +1094,21 @@ func TestMargeAdvancedFeatures(t *testing.T) {
if !found {
t.Error("Customer support event not found in event log")
}
// Verify DeviceInfo was updated
info, err := ds.GetDeviceInfo(account, deviceId)
if err != nil {
t.Fatalf("Failed to get device info: %v", err)
}
if info.IPAddress != ipAddress {
t.Errorf("Expected updated IP %s, got %s", ipAddress, info.IPAddress)
}
if info.MacAddress != macAddress {
t.Errorf("Expected updated MAC %s, got %s", macAddress, info.MacAddress)
}
if info.FirmwareVersion != firmware {
t.Errorf("Expected updated firmware %s, got %s", firmware, info.FirmwareVersion)
}
})
t.Run("AddRecent_Reproduction", func(t *testing.T) {
+3
View File
@@ -20,6 +20,9 @@ var mediaFS embed.FS
//go:embed static/bmx_services.json
var bmxServicesJSON []byte
// Upstream source available at https://worldwide.bose.com/updates/soundtouch?serialnumber=_serial_
// which results in a redirect to https://downloads.bose.com/ced/soundtouch/mr4_22097fe2/index.xml
//
//go:embed static/swupdate.xml
var swUpdateXML []byte
+4
View File
@@ -39,6 +39,10 @@ func setupRouter(targetURL string, ds *datastore.DataStore) (*chi.Mux, *Server)
streamingRoutes := func(r chi.Router) {
r.Get("/sourceproviders", server.HandleMargeSourceProviders)
r.Route("/account/{account}/device", func(r chi.Router) {
r.Post("/", server.HandleMargeAddDevice)
r.Post("/{device}", server.HandleMargeAddDevice)
})
r.Get("/account/{account}/device/{device}/recent", server.HandleMargeRecents)
r.Post("/account/{account}/device/{device}/recent", server.HandleMargeAddRecent)
r.Get("/account/{account}/device/{device}/presets", server.HandleMargePresets)
+6 -4
View File
@@ -971,16 +971,18 @@ func formatRecentResponse(recentObj *models.ServiceRecent, matchingSrc *models.C
// AddDeviceToAccount adds a new device to the specified account.
func AddDeviceToAccount(ds *datastore.DataStore, account string, sourceXML []byte) ([]byte, error) {
var newDeviceElem struct {
DeviceID string `xml:"deviceid,attr"`
Name string `xml:"name"`
DeviceID string `xml:"deviceid,attr"`
Name string `xml:"name"`
MACAddress string `xml:"macaddress"`
}
if err := xml.Unmarshal(sourceXML, &newDeviceElem); err != nil {
return nil, err
}
info := &models.ServiceDeviceInfo{
DeviceID: newDeviceElem.DeviceID,
Name: newDeviceElem.Name,
DeviceID: newDeviceElem.DeviceID,
Name: newDeviceElem.Name,
MacAddress: newDeviceElem.MACAddress,
// Other fields will be filled by discovery later or default
}
@@ -0,0 +1,15 @@
### POST /streaming/support/customersupport
POST {{host}}/streaming/support/customersupport
Host: streaming.bose.com
Content-Type: application/vnd.bose.streaming-v1.2+xml
User-Agent: Bose_Lisa/27.0.6
Accept: application/vnd.bose.streaming-v1.2+xml
Authorization: Bearer {{token}}
<?xml version="1.0" encoding="UTF-8" ?><device-data><device id="{{deviceId}}"><serialnumber>{{serialNumber}}</serialnumber><firmware-version>27.0.6.46330.5043500 epdbuild.trunk.hepdswbld04.2022-08-04T11:20:29</firmware-version><product product_code="SoundTouch 10 sm2" type="5"><serialnumber>{{serialNumber}}</serialnumber></product></device><diagnostic-data><device-landscape><rssi>Excellent</rssi><gateway-ip-address>{{gatewayIp}}</gateway-ip-address><macaddresses><macaddress>{{macAddress1}}</macaddress><macaddress>{{macAddress2}}</macaddress></macaddresses><ip-address>{{deviceIp}}</ip-address><network-connection-type>Wireless</network-connection-type></device-landscape><network-landscape><network-data xmlns="http://www.Bose.com/Schemas/2012-12/NetworkMonitor/" /></network-landscape></diagnostic-data></device-data>
> {%
client.test("Customer support data uploaded successfully", function() {
client.assert(response.status === 200, "Response status is not 200");
});
%}
@@ -6,6 +6,7 @@ Authorization: Bearer {{token}}
<?xml version="1.0" encoding="UTF-8" ?>
<device deviceid="{{deviceId}}">
<name>{{deviceName}}</name>
<macaddress>{{macAddress1}}</macaddress>
</device>
> {%
@@ -17,3 +18,39 @@ Authorization: Bearer {{token}}
client.assert(device.getAttribute("deviceid") === client.variables.environment.get("deviceId"), "Response body should contain the deviceId");
});
%}
### POST /streaming/account/{{accountId}}/device/ (Register Device Variant)
POST {{host}}/streaming/account/{{accountId}}/device/
Content-Type: application/vnd.bose.streaming-v1.2+xml
Authorization: Bearer {{token}}
<?xml version="1.0" encoding="UTF-8" ?>
<device deviceid="{{deviceId}}">
<name>{{deviceName}}</name>
<macaddress>{{macAddress1}}mac</macaddress>
</device>
> {%
client.test("Device registered successfully (variant)", function() {
client.assert(response.status === 200 || response.status === 201, "Response status is not 200 or 201");
const doc = response.body;
const device = doc.getElementsByTagName("device")[0];
client.assert(device !== undefined, "Response body should contain <device>");
client.assert(device.getAttribute("deviceid") === client.variables.environment.get("deviceId"), "Response body should contain the deviceId");
const createdOn = device.getElementsByTagName("createdOn")[0];
client.assert(createdOn !== undefined, "Response body should contain <createdOn>");
client.assert(createdOn.textContent.length > 0, "createdOn should not be empty");
const name = device.getElementsByTagName("name")[0];
client.assert(name !== undefined, "Response body should contain <name>");
client.assert(name.textContent === client.variables.environment.get("deviceName"), "name should match requested name");
const updatedOn = device.getElementsByTagName("updatedOn")[0];
client.assert(updatedOn !== undefined, "Response body should contain <updatedOn>");
client.assert(updatedOn.textContent === createdOn.textContent, "updatedOn should match createdOn for a new device");
const ipaddress = device.getElementsByTagName("ipaddress")[0];
client.assert(ipaddress !== undefined, "Response body should contain <ipaddress>");
});
%}