test(http-client): pin ignored edges + app/provisioning surface (refs #451)

Two deliberately-unimplemented routes, pinned as "currently ignored" so a future
change to them is conscious:
- GET  /v1/blacklist/{deviceId}  -> 405 (inline stub)
- POST /alexa/certificate        -> 501 (no AWS IoT integration)

App / provisioning surface (app-called, not the speaker data-plane). Shapes come
from the _/mitm capture where one exists, otherwise from the handler (canned /
stub responses):
- GET  /streaming/account/{a}/emailaddress  -> 200 (<emailAddress>, _/mitm)
- GET  /customer/account/{a}                -> 200 (<customer> profile, canned)
- POST /customer/account/{a}                -> 200 (profile update, stub)
- POST /customer/account/{a}/password       -> 200 (password change, stub)

COVERAGE.md gains an app/provisioning section and records the source (mitm vs
handler) for each. make test-http-client: 73 requests, 0 failed.

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 e91e7d8ad4
commit d5b298fc43
7 changed files with 136 additions and 11 deletions
+21 -11
View File
@@ -59,8 +59,8 @@ Legend: ✅ covered · ⬜ gap · 〰️ partial (some status/variant uncovered)
| GET | `/updates/soundtouch` | 200 | `get_soundtouch_updates.http` | ✅ |
| GET | `/v1/auth` | 200, 403, 404 | `get_speaker_auth.http` | ✅ (200; 403/404 probe/edge) |
| POST | `/v1/scmudc/{d}` | 200 | `post_scmudc_event.http` | ✅ |
| GET | `/v1/blacklist/{d}` | 405 | | (edge) |
| POST | `/alexa/certificate` | 501 (rare 200) | | (edge) |
| GET | `/v1/blacklist/{d}` | 405 | `get_blacklist.http` | (currently ignored: 405 stub) |
| POST | `/alexa/certificate` | 501 (rare 200) | `post_alexa_certificate.http` | (currently ignored: 501 stub) |
| GET | `/bmx/registry/v1/services` | 200 | `get_bmx_services.http` | ✅ |
| GET | `/bmx/registry/v1/servicesAvailability` | 200 | `get_bmx_services_availability.http` | ✅ |
| POST | `/bmx/tunein/v1/token` | 200 | `tunein_playback_station.http` | ✅ |
@@ -77,18 +77,28 @@ Legend: ✅ covered · ⬜ gap · 〰️ partial (some status/variant uncovered)
| POST | `/oauth/device/{d}/.../15/token/cs3` | 200 | `post_oauth_token.http` | ✅ |
| POST | `/oauth/device/{d}/.../20/token/cs1` | 200 | `post_oauth_token_amazon.http` | ✅ |
## Not observed from the speaker (lower priority / different audience)
## App / provisioning surface (app-called, not the speaker data-plane)
The SoundTouch app (not the speaker) drives these. They are part of the frozen
contract but a different audience; shapes are taken from `_/mitm` where a capture
exists, otherwise from the handler (the current responses are canned / stubs).
| Method | Route | Status | Covered by | Source |
|--------|-------|--------|------------|--------|
| GET | `/streaming/account/{a}/emailaddress` | 200 | `get_emailaddress.http` | `_/mitm` capture |
| GET | `/customer/account/{a}` | 200 | `get_customer_profile.http` | handler (canned profile) |
| POST | `/customer/account/{a}` | 200 | `post_customer_profile.http` | handler (stub accept) |
| POST | `/customer/account/{a}/password` | 200 | `post_customer_profile.http` | handler (stub accept) |
| POST | `/streaming/account/login` | 200 | `create_account.http` | `_/mitm` capture |
## Not observed (lower priority / no fixture)
- `/bmx/tunein/v1/navigate`, `/search`, `/search/next` — registered (frozen),
but in the corpus the speaker uses `/playback/*`; the search/navigate layer is
driven by the app/UI (`/api/tunein/*`), not the speaker. Covered conceptually,
no speaker recording to replay (and no upstream fixture yet, see
TUNEIN-MOCK-MISSING.md).
driven by the app/UI (`/api/tunein/*`), not the speaker. No upstream fixture
yet, see TUNEIN-MOCK-MISSING.md.
- `/core02/svc-bmx-adapter-siriusxm-*` — registered, but not present in this
corpus (no SiriusXM device). Left as a known blank.
- App / provisioning surface (`/customer/account*`, account profile/password,
`/streaming/account/login` beyond create) — app-called, not the speaker
data-plane; out of scope for the speaker-contract net.
## Remaining gaps
@@ -97,5 +107,5 @@ Legend: ✅ covered · ⬜ gap · 〰️ partial (some status/variant uncovered)
- `/media/tts/{hash}.mp3` — returns 200 only after a TTS has been generated
(otherwise a 404 miss). Needs a prior `/setup/tts/speak` step to be a
deterministic 200.
- Edge statuses (quirky-status pins, add on demand): PUT-device `401`,
`/v1/blacklist` `405`, `/alexa/certificate` `501`.
- PUT-device `401` (rename with a mismatched/blocked payload) — the only
remaining status variant on an otherwise-covered route.
@@ -0,0 +1,15 @@
### GET /v1/blacklist/{deviceId} (currently ignored: 405 Method Not Allowed)
###
### The speaker probes a notification blacklist endpoint. AfterTouch does not
### implement it and deliberately returns 405 (inline stub in setupRouter).
### Pinning the current 405 documents this as a conscious no-op, so any future
### behaviour change here is deliberate rather than accidental.
GET {{host}}/v1/blacklist/{{deviceId}}
User-Agent: Bose_Lisa/27.0.6
Accept: */*
> {%
client.test("Blacklist probe is the ignored 405 stub", function() {
client.assert(response.status === 405, "Response status is not 405, got " + response.status);
});
%}
@@ -0,0 +1,19 @@
### GET /customer/account/{accountId} (app-called account profile)
###
### App / provisioning surface (not the speaker data-plane). No live recording in
### _/mitm for this exact route; the response is a canned profile from
### HandleMargeAccountProfile, returned as a <customer> XML document. App user-agent.
GET {{host}}/customer/account/{{accountId}}
User-Agent: Mozilla/5.0 (Linux; Android 13) SOUNDTOUCH_MOBILE_APP
Accept: application/xml
Authorization: Bearer {{token}}
> {%
client.test("Account profile returned as XML 200", function() {
client.assert(response.status === 200, "Response status is not 200, got " + response.status);
client.assert(response.body.getElementsByTagName("customer").length > 0,
"Response should contain <customer>");
client.assert(response.body.getElementsByTagName("accountID").length > 0,
"<customer> should contain <accountID>");
});
%}
@@ -0,0 +1,17 @@
### GET /streaming/account/{accountId}/emailaddress (app-called account surface)
###
### The SoundTouch app fetches the account email (captured in _/mitm). Not part of
### the speaker data-plane, but part of the frozen app/provisioning contract.
### HandleMargeGetEmailAddress returns an <emailAddress> XML body. App user-agent.
GET {{host}}/streaming/account/{{accountId}}/emailaddress
User-Agent: Mozilla/5.0 (Linux; Android 13) SOUNDTOUCH_MOBILE_APP
Accept: application/vnd.bose.streaming-v1.1+xml
Authorization: Bearer {{token}}
> {%
client.test("Email address returned as XML 200", function() {
client.assert(response.status === 200, "Response status is not 200, got " + response.status);
client.assert(response.body.getElementsByTagName("emailAddress").length > 0,
"Response should contain <emailAddress>");
});
%}
@@ -0,0 +1,20 @@
### POST /alexa/certificate (currently ignored: 501 Not Implemented)
###
### Speakers request an Alexa IoT provisioning certificate. AfterTouch has no AWS
### IoT integration, so HandleAlexaCertificate deliberately returns 501 with an
### explanatory JSON body. Pinning the current 501 documents this as intentionally
### unimplemented. (data is a form field carrying {"device":"..."}.)
POST {{host}}/alexa/certificate
User-Agent: Bose_Lisa/27.0.6
Accept: */*
Content-Type: application/x-www-form-urlencoded
data=%7B%22device%22%3A%22{{deviceId}}%22%7D
> {%
client.test("Alexa certificate provisioning is the ignored 501 stub", function() {
client.assert(response.status === 501, "Response status is not 501, got " + response.status);
client.assert(response.contentType.mimeType === "application/json",
"Expected application/json, got '" + response.contentType.mimeType + "'");
});
%}
@@ -0,0 +1,39 @@
### POST /customer/account/{accountId} (update account profile)
###
### App / provisioning surface. HandleMargeUpdateAccountProfile is a stub that
### accepts the update with 200. No live recording; this pins the current accept.
POST {{host}}/customer/account/{{accountId}}
User-Agent: Mozilla/5.0 (Linux; Android 13) SOUNDTOUCH_MOBILE_APP
Content-Type: application/xml
Authorization: Bearer {{token}}
<?xml version="1.0" encoding="UTF-8"?>
<customer>
<accountID>{{accountId}}</accountID>
<firstName>SoundTouch</firstName>
<lastName>User</lastName>
</customer>
> {%
client.test("Profile update accepted (200)", function() {
client.assert(response.status === 200, "Response status is not 200, got " + response.status);
});
%}
### POST /customer/account/{accountId}/password (change account password)
###
### App / provisioning surface. HandleMargeChangePassword is a stub that accepts
### the change with 200. No live recording; this pins the current accept.
POST {{host}}/customer/account/{{accountId}}/password
User-Agent: Mozilla/5.0 (Linux; Android 13) SOUNDTOUCH_MOBILE_APP
Content-Type: application/xml
Authorization: Bearer {{token}}
<?xml version="1.0" encoding="UTF-8"?>
<password><old>old-secret</old><new>new-secret</new></password>
> {%
client.test("Password change accepted (200)", function() {
client.assert(response.status === 200, "Response status is not 200, got " + response.status);
});
%}