diff --git a/Makefile b/Makefile index 3902edc..29dc188 100644 --- a/Makefile +++ b/Makefile @@ -177,9 +177,14 @@ test-http-client: /workdir/spotify_registration.http \ /workdir/amazon_registration.http \ /workdir/create_account.http \ + /workdir/get_emailaddress.http \ + /workdir/get_customer_profile.http \ + /workdir/post_customer_profile.http \ /workdir/register_device.http \ /workdir/post_scmudc_event.http \ /workdir/get_speaker_auth.http \ + /workdir/get_blacklist.http \ + /workdir/post_alexa_certificate.http \ /workdir/spotify_full_flow.http \ /workdir/customer_support.http \ /workdir/power_on.http \ diff --git a/tests/integration/http-client/COVERAGE.md b/tests/integration/http-client/COVERAGE.md index a927dc8..a349347 100644 --- a/tests/integration/http-client/COVERAGE.md +++ b/tests/integration/http-client/COVERAGE.md @@ -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. diff --git a/tests/integration/http-client/get_blacklist.http b/tests/integration/http-client/get_blacklist.http new file mode 100644 index 0000000..65fb8b8 --- /dev/null +++ b/tests/integration/http-client/get_blacklist.http @@ -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); + }); +%} diff --git a/tests/integration/http-client/get_customer_profile.http b/tests/integration/http-client/get_customer_profile.http new file mode 100644 index 0000000..614b861 --- /dev/null +++ b/tests/integration/http-client/get_customer_profile.http @@ -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 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 "); + client.assert(response.body.getElementsByTagName("accountID").length > 0, + " should contain "); + }); +%} diff --git a/tests/integration/http-client/get_emailaddress.http b/tests/integration/http-client/get_emailaddress.http new file mode 100644 index 0000000..f2fbc82 --- /dev/null +++ b/tests/integration/http-client/get_emailaddress.http @@ -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 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 "); + }); +%} diff --git a/tests/integration/http-client/post_alexa_certificate.http b/tests/integration/http-client/post_alexa_certificate.http new file mode 100644 index 0000000..164aeeb --- /dev/null +++ b/tests/integration/http-client/post_alexa_certificate.http @@ -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 + "'"); + }); +%} diff --git a/tests/integration/http-client/post_customer_profile.http b/tests/integration/http-client/post_customer_profile.http new file mode 100644 index 0000000..c3fc377 --- /dev/null +++ b/tests/integration/http-client/post_customer_profile.http @@ -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}} + + + + {{accountId}} + SoundTouch + User + + +> {% + 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}} + + +old-secretnew-secret + +> {% + client.test("Password change accepted (200)", function() { + client.assert(response.status === 200, "Response status is not 200, got " + response.status); + }); +%}