feat(service): add 3 speaker-contract routes for parity (refs #451)

Close the speaker/service-contract gaps found comparing against a reference
implementation — three real Bose routes we did not serve:

- DELETE /streaming/account/{account}/source/{sourceID} — removes a configured
  source from every device of the account (HandleMargeDeleteSource +
  marge.RemoveSourceFromAccount), mirroring the account-level POST add-source.
  Bare 200, empty body. Previously source removal was only reachable via the
  admin /setup surface.
- GET /bmx/tunein — bare TuneIn service descriptor (the registry's `self` link),
  HandleTuneInService. chi routes both /bmx/tunein and /bmx/tunein/.
- GET /core02/svc-bmx-adapter-orion/prod/orion — bare Orion (LOCAL_INTERNET_RADIO)
  adapter descriptor, HandleOrionService.

The two descriptors reuse the existing extractBMXService + applyBMXTemplate
helpers (same {BMX_SERVER}/{MEDIA_SERVER} substitution the registry applies).
Contract tests added (delete_source.http, get_bmx_service_descriptors.http);
router + frozen-coverage goldens updated.

make test-http-client: 95 requests, 0 failed. go test + golangci-lint clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Tobias Gesellchen
2026-06-06 19:11:24 +02:00
co-authored by Claude Opus 4.8
parent ea6ee3e097
commit 2dd0143e10
9 changed files with 156 additions and 0 deletions
@@ -0,0 +1,15 @@
### DELETE /streaming/account/{accountId}/source/{sourceId} (remove a configured source)
###
### Frozen Marge route: removes the source from every device of the account
### (HandleMargeDeleteSource), mirroring the account-level POST add-source.
### Idempotent — a 200 is returned whether or not the source was present, so this
### pins the route + contract without depending on prior state.
DELETE {{host}}/streaming/account/{{accountId}}/source/SRC_contract_test
Authorization: Bearer {{token}}
User-Agent: Bose_Lisa/27.0.6
> {%
client.test("Source removal accepted (200)", function () {
client.assert(response.status === 200, "Response status is not 200, got " + response.status);
});
%}
@@ -0,0 +1,50 @@
### GET /bmx/tunein (bare TuneIn service descriptor)
###
### Frozen BMX route — the registry advertises this as TuneIn's `self` link.
### Returns the TUNEIN entry of the service registry with {BMX_SERVER}/{MEDIA_SERVER}
### substituted (HandleTuneInService).
GET {{host}}/bmx/tunein
User-Agent: Bose_Lisa/27.0.6
Accept: */*
> {%
client.test("TuneIn descriptor 200 JSON", function () {
client.assert(response.status === 200, "got " + response.status);
client.assert(response.contentType.mimeType === "application/json",
"expected application/json, got '" + response.contentType.mimeType + "'");
client.assert(response.body.hasOwnProperty("_links"), "descriptor missing _links");
client.assert(response.body.hasOwnProperty("assets"), "descriptor missing assets");
});
%}
### GET /bmx/tunein/ (trailing-slash form — chi routes both to the same descriptor)
GET {{host}}/bmx/tunein/
User-Agent: Bose_Lisa/27.0.6
Accept: */*
> {%
client.test("TuneIn descriptor 200 (trailing slash)", function () {
client.assert(response.status === 200, "got " + response.status);
client.assert(response.body.hasOwnProperty("assets"), "descriptor missing assets");
});
%}
### GET /core02/svc-bmx-adapter-orion/prod/orion (bare Orion adapter descriptor)
###
### Frozen route — the registry advertises this as the Orion (LOCAL_INTERNET_RADIO)
### adapter's `self` link. Returns the LOCAL_INTERNET_RADIO registry entry with the
### same substitution (HandleOrionService).
GET {{host}}/core02/svc-bmx-adapter-orion/prod/orion
User-Agent: Bose_Lisa/27.0.6
Accept: */*
> {%
client.test("Orion descriptor 200 JSON", function () {
client.assert(response.status === 200, "got " + response.status);
client.assert(response.contentType.mimeType === "application/json",
"expected application/json, got '" + response.contentType.mimeType + "'");
client.assert(response.body.hasOwnProperty("baseUrl"), "descriptor missing baseUrl");
client.assert(response.body.id.name === "LOCAL_INTERNET_RADIO",
"expected id.name LOCAL_INTERNET_RADIO, got '" + (response.body.id && response.body.id.name) + "'");
});
%}