mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-18 00:26:29 +00:00
Add more e2e tests
This commit is contained in:
@@ -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 \
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>");
|
||||
});
|
||||
%}
|
||||
|
||||
Reference in New Issue
Block a user