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:
Tobias Gesellchen
2026-05-15 19:04:41 +02:00
co-authored by Claude Opus 4.7
parent 6bc4ee1e73
commit 979374b501
3 changed files with 112 additions and 1 deletions
+1
View File
@@ -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);
});
%}