mirror of
https://github.com/gesellix/Bose-SoundTouch.git
synced 2026-08-18 08:36:13 +00:00
Shrink the route surface the #451 refactor must preserve by retiring the /accounts/{account}/* compatibility mirror. Across the full recording corpus (all _/backup/*, _/mitm, _/i195, _/issue-94, captures + data/ + tests/, 139k+ .http files) no speaker or app uses the /accounts prefix, and every operation it offered is served by the /streaming/account/* paths real clients actually use. - New HandleUnsupported: returns 501 and logs the full request + client IP + a "please report this" message, so any real-world use surfaces instead of being silently dropped, and the prefix becomes a clean removal candidate. - Re-point every /accounts/* route to it. The frozen /streaming/* contract is left entirely on its real handlers (those stay even where our corpus didn't exercise them — absence of capture is not proof of disuse). - Migrate the integration tests off the /accounts mirror onto their recorded /streaming/account/* equivalents (register/unregister/spotify_full_flow), then pin the mirror's 501 contract in unsupported_routes.http. - Router + frozen-route-coverage golden files updated accordingly. make test-http-client: 91 requests, 0 failed. go test + golangci-lint clean. Note for release time: call out the intentional /accounts/* 501 breakage in the release notes' Noteworthy section (use /streaming/account/* instead). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
218 lines
7.8 KiB
HTTP
218 lines
7.8 KiB
HTTP
### Spotify Full Flow: Registration, Preset, and Token Refresh
|
|
|
|
# 1. Initialize Spotify Flow
|
|
# @name Init Spotify Flow
|
|
POST {{host}}/mgmt/spotify/init
|
|
Authorization: Basic admin change_me!
|
|
|
|
###
|
|
|
|
# 2. Spotify Callback
|
|
# Simulates the user being redirected back from Spotify with a code
|
|
# This should also trigger the Bridge (Marge registration + Speaker notification)
|
|
# @name Spotify Callback
|
|
GET {{host}}/mgmt/spotify/callback?code=mock-auth-code&account={{accountId}}
|
|
|
|
###
|
|
|
|
# 3. Verify Spotify Account and extract Bose Secret
|
|
# @name Verify Accounts
|
|
GET {{host}}/mgmt/spotify/accounts
|
|
Authorization: Basic admin change_me!
|
|
|
|
> {%
|
|
client.test("Account exists and has Bose Secret", function() {
|
|
client.assert(response.body.accounts.length > 0, "No accounts found");
|
|
var found = false;
|
|
for (var i = 0; i < response.body.accounts.length; i++) {
|
|
if (response.body.accounts[i].user_id === "spotify-user-id") {
|
|
found = true;
|
|
client.assert(response.body.accounts[i].bose_secret !== undefined, "Bose Secret missing");
|
|
client.global.set("bose_secret", response.body.accounts[i].bose_secret);
|
|
break;
|
|
}
|
|
}
|
|
client.assert(found, "Account 'spotify-user-id' not found");
|
|
});
|
|
%}
|
|
|
|
###
|
|
|
|
# 4. Verify Marge Source Registration and extract Spotify Source ID
|
|
# @name Get Marge Sources
|
|
GET {{host}}/streaming/account/{{accountId}}/sources
|
|
|
|
> {%
|
|
client.test("Spotify source registered in Marge", function() {
|
|
const doc = response.body;
|
|
|
|
const spotifySourceProviderID = client.variables.environment.get("spotifyProviderID");
|
|
|
|
function findSpotifySource(parent) {
|
|
const children = parent.childNodes;
|
|
if (!children) return null;
|
|
for (var i = 0; i < children.length; i++) {
|
|
const node = children[i];
|
|
if (node.nodeName && (node.nodeName.toLowerCase() === "source")) {
|
|
// Check sourceKey
|
|
const sourceproviderids = node.getElementsByTagName("sourceproviderid");
|
|
for (var j = 0; j < (sourceproviderids ? sourceproviderids.length : 0); j++) {
|
|
const sourceproviderid = sourceproviderids[j].textContent;
|
|
if (sourceproviderid == spotifySourceProviderID) {
|
|
return node;
|
|
}
|
|
}
|
|
}
|
|
// Recurse
|
|
const found = findSpotifySource(node);
|
|
if (found) return found;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
const spotifySource = findSpotifySource(doc);
|
|
client.assert(spotifySource !== null, "Spotify source not found in Marge for 'spotify-user-id'");
|
|
|
|
const id = spotifySource.getAttribute("id");
|
|
client.assert(id !== undefined && id !== "", "Spotify source ID missing");
|
|
client.global.set("spotify_source_id", id);
|
|
|
|
function getCredentialType(source) {
|
|
const credentials = source.getElementsByTagName("credential");
|
|
if (credentials && credentials.length > 0) {
|
|
return credentials[0].getAttribute("type");
|
|
}
|
|
return null;
|
|
}
|
|
|
|
const secretType = getCredentialType(spotifySource);
|
|
client.assert(secretType === "token_version_3", "Wrong secret type in Marge: " + secretType);
|
|
});
|
|
%}
|
|
|
|
###
|
|
|
|
# 5. Add a Spotify Preset
|
|
# @name Update Preset
|
|
PUT {{host}}/streaming/account/{{accountId}}/device/{{deviceId}}/preset/1
|
|
Content-Type: application/xml
|
|
|
|
<preset id="1">
|
|
<name>Test Track</name>
|
|
<sourceid>{{spotify_source_id}}</sourceid>
|
|
<contentItemType>track</contentItemType>
|
|
<location>spotify:track:123</location>
|
|
</preset>
|
|
|
|
> {%
|
|
client.test("Preset added successfully", function() {
|
|
client.assert(response.status === 200, "Failed to update preset");
|
|
});
|
|
%}
|
|
|
|
###
|
|
|
|
# 6. Verify Preset in Marge
|
|
# @name Get Presets
|
|
GET {{host}}/streaming/account/{{accountId}}/device/{{deviceId}}/presets
|
|
|
|
> {%
|
|
client.test("Verify preset content", function() {
|
|
const doc = response.body;
|
|
const spotifySourceProviderID = client.variables.environment.get("spotifyProviderID");
|
|
|
|
function findSpotifyPreset(parent) {
|
|
const children = parent.childNodes;
|
|
if (!children) return null;
|
|
for (var i = 0; i < children.length; i++) {
|
|
const node = children[i];
|
|
// Handle both uppercase and lowercase tag names just in case
|
|
const nodeName = node.nodeName ? node.nodeName.toLowerCase() : "";
|
|
if (nodeName === "preset") {
|
|
// Check source provider inside the preset
|
|
const providers = node.getElementsByTagName("sourceproviderid");
|
|
for (var j = 0; j < (providers ? providers.length : 0); j++) {
|
|
if (providers[j].textContent === spotifySourceProviderID) {
|
|
return node;
|
|
}
|
|
}
|
|
}
|
|
// Recurse
|
|
const found = findSpotifyPreset(node);
|
|
if (found) return found;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
const spotifyPreset = findSpotifyPreset(doc);
|
|
client.assert(spotifyPreset !== null, "Spotify preset not found in Marge for provider '" + spotifySourceProviderID + "'");
|
|
|
|
// Verify preset elements (flat structure)
|
|
function getTagContent(parent, tagName) {
|
|
const elements = parent.getElementsByTagName(tagName);
|
|
if (elements && elements.length > 0) return elements[0].textContent;
|
|
// Try case-insensitive fallback
|
|
const all = parent.getElementsByTagName("*");
|
|
for (var i = 0; i < all.length; i++) {
|
|
if (all[i].nodeName.toLowerCase() === tagName.toLowerCase()) return all[i].textContent;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
client.assert(getTagContent(spotifyPreset, "contentItemType") === "track", "Wrong contentItemType");
|
|
client.assert(getTagContent(spotifyPreset, "location") === "spotify:track:123", "Wrong location");
|
|
client.assert(getTagContent(spotifyPreset, "name") === "Test Track", "Wrong name");
|
|
|
|
// Verify nested source info
|
|
const sources = spotifyPreset.getElementsByTagName("source");
|
|
var source = (sources && sources.length > 0) ? sources[0] : null;
|
|
if (!source) {
|
|
// Fallback search for 'Source'
|
|
const all = spotifyPreset.getElementsByTagName("*");
|
|
for (var i = 0; i < all.length; i++) {
|
|
if (all[i].nodeName.toLowerCase() === "source") {
|
|
source = all[i];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
client.assert(source !== null, "Missing nested source");
|
|
|
|
const credentials = source.getElementsByTagName("credential");
|
|
var credential = (credentials && credentials.length > 0) ? credentials[0] : null;
|
|
if (!credential) {
|
|
const all = source.getElementsByTagName("*");
|
|
for (var i = 0; i < all.length; i++) {
|
|
if (all[i].nodeName.toLowerCase() === "credential") {
|
|
credential = all[i];
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
client.assert(credential !== null, "Missing source credential");
|
|
client.assert(credential.getAttribute("type") === "token_version_3", "Wrong credential type in preset");
|
|
client.assert(credential.textContent !== "", "Credential value (secret) missing");
|
|
});
|
|
%}
|
|
|
|
###
|
|
|
|
# 7. Token Refresh via Surrogate (Bose Secret)
|
|
# Simulates the speaker requesting a fresh access token
|
|
# @name Token Refresh
|
|
POST {{host}}/oauth/device/{{deviceId}}/music/musicprovider/15/token/cs3
|
|
Content-Type: application/json
|
|
|
|
{
|
|
"grant_type": "refresh_token",
|
|
"refresh_token": "{{bose_secret}}"
|
|
}
|
|
|
|
> {%
|
|
client.test("Token refreshed successfully", function() {
|
|
client.assert(response.status === 200, "Refresh failed");
|
|
client.assert(response.body.access_token !== undefined, "No access token in response");
|
|
client.assert(response.body.token_type === "Bearer", "Wrong token type");
|
|
});
|
|
%}
|