mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-18 08:36:13 +00:00
test(integration): pin #285 rename PUT behaviour at the HTTP layer
Adds rename_device.http between get_group.http and unregister_device.http
in the make test-http-client sequence. The new test fires the PUT
the speaker emits after a rename and asserts:
- 200 OK, content type vnd.bose.streaming-v1.2+xml
- the response carries the renamed value
- createdOn matches the value captured during register_device.http
(cross-request global), locking in the "first-paired" semantics
- ipaddress is preserved from the prior power_on, not reset by the
rename body's empty IP field
- a mismatched body deviceid is rejected with 400
register_device.http captures the initial createdOn into a global so
the rename test can assert equality rather than a flakier
updatedOn != createdOn heuristic. The variant POST's stale
updatedOn === createdOn assertion is replaced with an upsert-aware
equality against the same captured global.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
6bc4ee1e73
commit
979374b501
@@ -171,6 +171,7 @@ test-http-client:
|
||||
/workdir/get_full_account.http \
|
||||
/workdir/create_group.http \
|
||||
/workdir/get_group.http \
|
||||
/workdir/rename_device.http \
|
||||
/workdir/unregister_device.http \
|
||||
--report; \
|
||||
EXIT_CODE=$$?; \
|
||||
|
||||
@@ -33,6 +33,10 @@ Authorization: Bearer {{token}}
|
||||
|
||||
const ipaddress = device.getElementsByTagName("ipaddress")[0];
|
||||
client.assert(ipaddress !== undefined, "Response body should contain <ipaddress>");
|
||||
|
||||
// Capture the initial createdOn so rename_device.http can
|
||||
// assert it survives a PUT later in the test sequence.
|
||||
client.global.set("initialCreatedOn", createdOn.textContent);
|
||||
});
|
||||
%}
|
||||
|
||||
@@ -61,6 +65,13 @@ Authorization: Bearer {{token}}
|
||||
const createdOn = device.getElementsByTagName("createdOn")[0];
|
||||
client.assert(createdOn !== undefined, "Response body should contain <createdOn>");
|
||||
client.assert(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|(\+\d{2}:\d{2}))$/.test(createdOn.textContent), "createdOn should be a valid ISO8601 timestamp");
|
||||
// This variant POST re-uses the same deviceId as the first
|
||||
// register above, so it's an upsert — issue #285 made the
|
||||
// datastore preserve the original createdOn unconditionally.
|
||||
// The captured global is the load-bearing assertion: if it
|
||||
// ever differs here, CreatedOn preservation regressed.
|
||||
client.assert(createdOn.textContent === client.global.get("initialCreatedOn"),
|
||||
"createdOn (" + createdOn.textContent + ") differs from the value captured by the first POST (" + client.global.get("initialCreatedOn") + ") — upsert must preserve the original timestamp");
|
||||
|
||||
const name = device.getElementsByTagName("name")[0];
|
||||
client.assert(name !== undefined, "Response body should contain <name>");
|
||||
@@ -68,7 +79,7 @@ Authorization: Bearer {{token}}
|
||||
|
||||
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");
|
||||
client.assert(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|(\+\d{2}:\d{2}))$/.test(updatedOn.textContent), "updatedOn should be a valid ISO8601 timestamp");
|
||||
|
||||
const ipaddress = device.getElementsByTagName("ipaddress")[0];
|
||||
client.assert(ipaddress !== undefined, "Response body should contain <ipaddress>");
|
||||
|
||||
@@ -0,0 +1,99 @@
|
||||
### PUT /streaming/account/{accountId}/device/{deviceId} (Rename Device)
|
||||
###
|
||||
### Speakers fire this against AfterTouch when the user renames them
|
||||
### via the Bose App or `soundtouch-cli name set`. Before issue #285's
|
||||
### route fix the request fell through to the [UNHANDLED] catch-all
|
||||
### and proxied to streaming.bose.com (401), so the speaker retried
|
||||
### in a loop and the Bose App showed the rename spinning forever.
|
||||
### This test runs after register_device.http (which creates the
|
||||
### initial record) and after power_on.http (which seeds the IP at
|
||||
### 192.168.1.100). The rename PUT must:
|
||||
###
|
||||
### - return 200 OK
|
||||
### - echo the new name
|
||||
### - preserve <createdOn> from the initial registration
|
||||
### - preserve <ipaddress> from the power_on update (request body
|
||||
### doesn't carry one; the datastore merge keeps the existing)
|
||||
### - refresh <updatedOn> to "now"
|
||||
PUT {{host}}/streaming/account/{{accountId}}/device/{{deviceId}}
|
||||
Content-Type: application/vnd.bose.streaming-v1.2+xml
|
||||
Authorization: Bearer {{token}}
|
||||
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<device deviceid="{{deviceId}}">
|
||||
<name>{{deviceName}} (renamed)</name>
|
||||
<macaddress>{{macAddress1}}</macaddress>
|
||||
</device>
|
||||
|
||||
> {%
|
||||
client.test("Rename returned 200 OK", function() {
|
||||
client.assert(response.status === 200, "Response status should be 200, got " + response.status);
|
||||
client.assert(response.contentType.mimeType === "application/vnd.bose.streaming-v1.2+xml", "Response Content-Type should be application/vnd.bose.streaming-v1.2+xml");
|
||||
});
|
||||
|
||||
client.test("Response carries the new name and matches the deviceId", function() {
|
||||
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 deviceid should match");
|
||||
|
||||
const name = device.getElementsByTagName("name")[0];
|
||||
client.assert(name !== undefined, "Response body should contain <name>");
|
||||
client.assert(name.textContent === client.variables.environment.get("deviceName") + " (renamed)", "Response should carry the renamed value");
|
||||
});
|
||||
|
||||
client.test("createdOn preserved across rename, updatedOn refreshed", function() {
|
||||
const device = response.body.getElementsByTagName("device")[0];
|
||||
|
||||
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 (it must survive the rename, not be reset)");
|
||||
client.assert(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|(\+\d{2}:\d{2}))$/.test(createdOn.textContent), "createdOn should be ISO8601");
|
||||
|
||||
// The load-bearing assertion: the value must match what
|
||||
// register_device.http captured on the initial POST. If it
|
||||
// doesn't, the rename PUT regressed the "first-paired in
|
||||
// 2017" semantics — the exact bug behind the parity capture
|
||||
// at data/parity_mismatches/1771797308__streaming_account_3230304_device_A81B6A536A98.json.
|
||||
const initial = client.global.get("initialCreatedOn");
|
||||
client.assert(initial !== undefined && initial.length > 0,
|
||||
"initialCreatedOn was not captured by register_device.http — test ordering issue");
|
||||
client.assert(createdOn.textContent === initial,
|
||||
"createdOn (" + createdOn.textContent + ") differs from the value captured at registration (" + initial + ") — the rename PUT must preserve the original timestamp");
|
||||
|
||||
const updatedOn = device.getElementsByTagName("updatedOn")[0];
|
||||
client.assert(updatedOn !== undefined, "Response body should contain <updatedOn>");
|
||||
client.assert(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|(\+\d{2}:\d{2}))$/.test(updatedOn.textContent), "updatedOn should be ISO8601");
|
||||
});
|
||||
|
||||
client.test("ipaddress preserved from earlier power_on payload", function() {
|
||||
const device = response.body.getElementsByTagName("device")[0];
|
||||
const ipaddress = device.getElementsByTagName("ipaddress")[0];
|
||||
client.assert(ipaddress !== undefined, "Response body should contain <ipaddress>");
|
||||
client.assert(ipaddress.textContent === client.variables.environment.get("deviceIp"),
|
||||
"ipaddress should be preserved from the prior power_on (" + client.variables.environment.get("deviceIp") + "), not overwritten — got " + ipaddress.textContent);
|
||||
});
|
||||
%}
|
||||
|
||||
### PUT with mismatched deviceid in body (safety check)
|
||||
###
|
||||
### If the body's deviceid attribute doesn't match the URL's {device}
|
||||
### segment the handler refuses with 400 rather than silently re-key
|
||||
### the persisted record under the wrong account/device. Pins the
|
||||
### second TestIssue285_* unit-test assertion at the integration
|
||||
### layer.
|
||||
PUT {{host}}/streaming/account/{{accountId}}/device/{{deviceId}}
|
||||
Content-Type: application/vnd.bose.streaming-v1.2+xml
|
||||
Authorization: Bearer {{token}}
|
||||
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<device deviceid="DEADBEEFCAFE">
|
||||
<name>Rogue Rename</name>
|
||||
<macaddress>DEADBEEFCAFE</macaddress>
|
||||
</device>
|
||||
|
||||
> {%
|
||||
client.test("Mismatched body deviceid returns 400", function() {
|
||||
client.assert(response.status === 400, "Response status should be 400 for body/url deviceid mismatch, got " + response.status);
|
||||
});
|
||||
%}
|
||||
Reference in New Issue
Block a user