mirror of
https://github.com/wkulhanek/bonob.git
synced 2025-12-21 17:33:29 +01:00
Add support for running under a context path, ie. /bonob, replace BONOB_WEB_ADDRESS with BONOB_URL
This commit is contained in:
@@ -63,7 +63,7 @@ Start bonob outside the lan with sonos discovery & registration disabled as they
|
|||||||
```bash
|
```bash
|
||||||
docker run \
|
docker run \
|
||||||
-e BONOB_PORT=4534 \
|
-e BONOB_PORT=4534 \
|
||||||
-e BONOB_WEB_ADDRESS=https://my-bonob-service.com \
|
-e BONOB_URL=https://my-server.example.com/bonob \
|
||||||
-e BONOB_SONOS_AUTO_REGISTER=false \
|
-e BONOB_SONOS_AUTO_REGISTER=false \
|
||||||
-e BONOB_SONOS_DEVICE_DISCOVERY=false \
|
-e BONOB_SONOS_DEVICE_DISCOVERY=false \
|
||||||
-e BONOB_NAVIDROME_URL=https://my-navidrome-service.com:4533 \
|
-e BONOB_NAVIDROME_URL=https://my-navidrome-service.com:4533 \
|
||||||
@@ -71,11 +71,11 @@ docker run \
|
|||||||
simojenki/bonob
|
simojenki/bonob
|
||||||
```
|
```
|
||||||
|
|
||||||
Now inside the lan that contains the sonos devices run bonob registration, using the same BONOB_WEB_ADDRESS as above, and with discovery enabled. Make sure to use host networking so that bonob can find the sonos devices (or provide a BONOB_SONOS_SEED_HOST)
|
Now inside the lan that contains the sonos devices run bonob registration, using the same BONOB_URL as above, and with discovery enabled. Make sure to use host networking so that bonob can find the sonos devices (or provide a BONOB_SONOS_SEED_HOST)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
docker run \
|
docker run \
|
||||||
-e BONOB_WEB_ADDRESS=https://my-bonob-service.com \
|
-e BONOB_URL=https://my-server.example.com/bonob \
|
||||||
-e BONOB_SONOS_DEVICE_DISCOVERY=true \
|
-e BONOB_SONOS_DEVICE_DISCOVERY=true \
|
||||||
--network host \
|
--network host \
|
||||||
simojenki/bonob register
|
simojenki/bonob register
|
||||||
@@ -86,7 +86,7 @@ docker run \
|
|||||||
item | default value | description
|
item | default value | description
|
||||||
---- | ------------- | -----------
|
---- | ------------- | -----------
|
||||||
BONOB_PORT | 4534 | Default http port for bonob to listen on
|
BONOB_PORT | 4534 | Default http port for bonob to listen on
|
||||||
BONOB_WEB_ADDRESS | http://$(hostname):4534 | URL for bonob so that sonos devices can communicate. **This must be either the public IP or DNS entry of the bonob instance so that the sonos devices can communicate with it.**
|
BONOB_URL | http://$(hostname):4534 | URL (including path) for bonob so that sonos devices can communicate. **This must be either the public IP or DNS entry of the bonob instance so that the sonos devices can communicate with it.**
|
||||||
BONOB_SECRET | bonob | secret used for encrypting credentials
|
BONOB_SECRET | bonob | secret used for encrypting credentials
|
||||||
BONOB_SONOS_AUTO_REGISTER | false | Whether or not to try and auto-register on startup
|
BONOB_SONOS_AUTO_REGISTER | false | Whether or not to try and auto-register on startup
|
||||||
BONOB_SONOS_DEVICE_DISCOVERY | true | whether or not sonos device discovery should be enabled
|
BONOB_SONOS_DEVICE_DISCOVERY | true | whether or not sonos device discovery should be enabled
|
||||||
|
|||||||
17
src/app.ts
17
src/app.ts
@@ -7,6 +7,7 @@ import { InMemoryLinkCodes } from "./link_codes";
|
|||||||
import readConfig from "./config";
|
import readConfig from "./config";
|
||||||
import sonos, { bonobService } from "./sonos";
|
import sonos, { bonobService } from "./sonos";
|
||||||
import { MusicService } from "./music_service";
|
import { MusicService } from "./music_service";
|
||||||
|
import { SystemClock } from "./clock";
|
||||||
|
|
||||||
const config = readConfig();
|
const config = readConfig();
|
||||||
|
|
||||||
@@ -15,7 +16,7 @@ logger.info(`Starting bonob with config ${JSON.stringify(config)}`);
|
|||||||
const bonob = bonobService(
|
const bonob = bonobService(
|
||||||
config.sonos.serviceName,
|
config.sonos.serviceName,
|
||||||
config.sonos.sid,
|
config.sonos.sid,
|
||||||
config.webAddress,
|
config.bonobUrl,
|
||||||
"AppLink"
|
"AppLink"
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -40,15 +41,15 @@ const featureFlagAwareMusicService: MusicService = {
|
|||||||
scrobble: (id: string) => {
|
scrobble: (id: string) => {
|
||||||
if (config.scrobbleTracks) return library.scrobble(id);
|
if (config.scrobbleTracks) return library.scrobble(id);
|
||||||
else {
|
else {
|
||||||
logger.info("Track Scrobbling not enabled")
|
logger.info("Track Scrobbling not enabled");
|
||||||
return Promise.resolve(false);
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
nowPlaying: (id: string) => {
|
nowPlaying: (id: string) => {
|
||||||
if (config.reportNowPlaying) return library.nowPlaying(id);
|
if (config.reportNowPlaying) return library.nowPlaying(id);
|
||||||
else {
|
else {
|
||||||
logger.info("Reporting track now playing not enabled");
|
logger.info("Reporting track now playing not enabled");
|
||||||
return Promise.resolve(false);
|
return Promise.resolve(true);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -58,10 +59,12 @@ const featureFlagAwareMusicService: MusicService = {
|
|||||||
const app = server(
|
const app = server(
|
||||||
sonosSystem,
|
sonosSystem,
|
||||||
bonob,
|
bonob,
|
||||||
config.webAddress,
|
config.bonobUrl,
|
||||||
featureFlagAwareMusicService,
|
featureFlagAwareMusicService,
|
||||||
new InMemoryLinkCodes(),
|
new InMemoryLinkCodes(),
|
||||||
new InMemoryAccessTokens(sha256(config.secret))
|
new InMemoryAccessTokens(sha256(config.secret)),
|
||||||
|
SystemClock,
|
||||||
|
true,
|
||||||
);
|
);
|
||||||
|
|
||||||
if (config.sonos.autoRegister) {
|
if (config.sonos.autoRegister) {
|
||||||
@@ -75,7 +78,7 @@ if (config.sonos.autoRegister) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
app.listen(config.port, () => {
|
app.listen(config.port, () => {
|
||||||
logger.info(`Listening on ${config.port} available @ ${config.webAddress}`);
|
logger.info(`Listening on ${config.port} available @ ${config.bonobUrl}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
export default app;
|
export default app;
|
||||||
|
|||||||
@@ -1,28 +1,32 @@
|
|||||||
import { hostname } from "os";
|
import { hostname } from "os";
|
||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
|
import url from "./url_builder";
|
||||||
|
|
||||||
export default function () {
|
export default function () {
|
||||||
const port = +(process.env["BONOB_PORT"] || 4534);
|
const port = +(process.env["BONOB_PORT"] || 4534);
|
||||||
const webAddress =
|
const bonobUrl =
|
||||||
process.env["BONOB_WEB_ADDRESS"] || `http://${hostname()}:${port}`;
|
process.env["BONOB_URL"] ||
|
||||||
|
process.env["BONOB_WEB_ADDRESS"] ||
|
||||||
|
`http://${hostname()}:${port}`;
|
||||||
|
|
||||||
if (webAddress.match("localhost")) {
|
if (bonobUrl.match("localhost")) {
|
||||||
logger.error(
|
logger.error(
|
||||||
"BONOB_WEB_ADDRESS containing localhost is almost certainly incorrect, sonos devices will not be able to communicate with bonob using localhost, please specify either public IP or DNS entry"
|
"BONOB_URL containing localhost is almost certainly incorrect, sonos devices will not be able to communicate with bonob using localhost, please specify either public IP or DNS entry"
|
||||||
);
|
);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
port,
|
port,
|
||||||
webAddress,
|
bonobUrl: url(bonobUrl),
|
||||||
secret: process.env["BONOB_SECRET"] || "bonob",
|
secret: process.env["BONOB_SECRET"] || "bonob",
|
||||||
sonos: {
|
sonos: {
|
||||||
serviceName: process.env["BONOB_SONOS_SERVICE_NAME"] || "bonob",
|
serviceName: process.env["BONOB_SONOS_SERVICE_NAME"] || "bonob",
|
||||||
deviceDiscovery:
|
deviceDiscovery:
|
||||||
(process.env["BONOB_SONOS_DEVICE_DISCOVERY"] || "true") == "true",
|
(process.env["BONOB_SONOS_DEVICE_DISCOVERY"] || "true") == "true",
|
||||||
seedHost: process.env["BONOB_SONOS_SEED_HOST"],
|
seedHost: process.env["BONOB_SONOS_SEED_HOST"],
|
||||||
autoRegister: (process.env["BONOB_SONOS_AUTO_REGISTER"] || "false") == "true",
|
autoRegister:
|
||||||
|
(process.env["BONOB_SONOS_AUTO_REGISTER"] || "false") == "true",
|
||||||
sid: Number(process.env["BONOB_SONOS_SERVICE_ID"] || "246"),
|
sid: Number(process.env["BONOB_SONOS_SERVICE_ID"] || "246"),
|
||||||
},
|
},
|
||||||
navidrome: {
|
navidrome: {
|
||||||
@@ -31,6 +35,7 @@ export default function () {
|
|||||||
process.env["BONOB_NAVIDROME_CUSTOM_CLIENTS"] || undefined,
|
process.env["BONOB_NAVIDROME_CUSTOM_CLIENTS"] || undefined,
|
||||||
},
|
},
|
||||||
scrobbleTracks: (process.env["BONOB_SCROBBLE_TRACKS"] || "true") == "true",
|
scrobbleTracks: (process.env["BONOB_SCROBBLE_TRACKS"] || "true") == "true",
|
||||||
reportNowPlaying: (process.env["BONOB_REPORT_NOW_PLAYING"] || "true") == "true",
|
reportNowPlaying:
|
||||||
|
(process.env["BONOB_REPORT_NOW_PLAYING"] || "true") == "true",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ const config = readConfig();
|
|||||||
const bonob = bonobService(
|
const bonob = bonobService(
|
||||||
config.sonos.serviceName,
|
config.sonos.serviceName,
|
||||||
config.sonos.sid,
|
config.sonos.sid,
|
||||||
config.webAddress,
|
config.bonobUrl,
|
||||||
"AppLink"
|
"AppLink"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
PRESENTATION_MAP_ROUTE,
|
PRESENTATION_MAP_ROUTE,
|
||||||
SONOS_RECOMMENDED_IMAGE_SIZES,
|
SONOS_RECOMMENDED_IMAGE_SIZES,
|
||||||
LOGIN_ROUTE,
|
LOGIN_ROUTE,
|
||||||
|
REGISTER_ROUTE,
|
||||||
} from "./smapi";
|
} from "./smapi";
|
||||||
import { LinkCodes, InMemoryLinkCodes } from "./link_codes";
|
import { LinkCodes, InMemoryLinkCodes } from "./link_codes";
|
||||||
import { MusicService, isSuccess } from "./music_service";
|
import { MusicService, isSuccess } from "./music_service";
|
||||||
@@ -20,6 +21,7 @@ import { AccessTokens, AccessTokenPerAuthToken } from "./access_tokens";
|
|||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
import { Clock, SystemClock } from "./clock";
|
import { Clock, SystemClock } from "./clock";
|
||||||
import { pipe } from "fp-ts/lib/function";
|
import { pipe } from "fp-ts/lib/function";
|
||||||
|
import { URLBuilder } from "./url_builder";
|
||||||
|
|
||||||
export const BONOB_ACCESS_TOKEN_HEADER = "bonob-access-token";
|
export const BONOB_ACCESS_TOKEN_HEADER = "bonob-access-token";
|
||||||
|
|
||||||
@@ -63,17 +65,19 @@ export class RangeBytesFromFilter extends Transform {
|
|||||||
function server(
|
function server(
|
||||||
sonos: Sonos,
|
sonos: Sonos,
|
||||||
service: Service,
|
service: Service,
|
||||||
webAddress: string,
|
bonobUrl: URLBuilder,
|
||||||
musicService: MusicService,
|
musicService: MusicService,
|
||||||
linkCodes: LinkCodes = new InMemoryLinkCodes(),
|
linkCodes: LinkCodes = new InMemoryLinkCodes(),
|
||||||
accessTokens: AccessTokens = new AccessTokenPerAuthToken(),
|
accessTokens: AccessTokens = new AccessTokenPerAuthToken(),
|
||||||
clock: Clock = SystemClock
|
clock: Clock = SystemClock,
|
||||||
|
applyContextPath = true
|
||||||
): Express {
|
): Express {
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
app.use(morgan("combined"));
|
app.use(morgan("combined"));
|
||||||
app.use(express.urlencoded({ extended: false }));
|
app.use(express.urlencoded({ extended: false }));
|
||||||
|
|
||||||
|
// todo: pass options in here?
|
||||||
app.use(express.static("./web/public"));
|
app.use(express.static("./web/public"));
|
||||||
app.engine("eta", Eta.renderFile);
|
app.engine("eta", Eta.renderFile);
|
||||||
|
|
||||||
@@ -91,12 +95,13 @@ function server(
|
|||||||
services,
|
services,
|
||||||
bonobService: service,
|
bonobService: service,
|
||||||
registeredBonobService,
|
registeredBonobService,
|
||||||
|
registerRoute: bonobUrl.append({ pathname: REGISTER_ROUTE }).pathname(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post("/register", (_, res) => {
|
app.post(REGISTER_ROUTE, (_, res) => {
|
||||||
sonos.register(service).then((success) => {
|
sonos.register(service).then((success) => {
|
||||||
if (success) {
|
if (success) {
|
||||||
res.render("success", {
|
res.render("success", {
|
||||||
@@ -114,7 +119,7 @@ function server(
|
|||||||
res.render("login", {
|
res.render("login", {
|
||||||
bonobService: service,
|
bonobService: service,
|
||||||
linkCode: req.query.linkCode,
|
linkCode: req.query.linkCode,
|
||||||
loginRoute: LOGIN_ROUTE,
|
loginRoute: bonobUrl.append({ pathname: LOGIN_ROUTE }).pathname(),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -317,14 +322,20 @@ function server(
|
|||||||
bindSmapiSoapServiceToExpress(
|
bindSmapiSoapServiceToExpress(
|
||||||
app,
|
app,
|
||||||
SOAP_PATH,
|
SOAP_PATH,
|
||||||
webAddress,
|
bonobUrl,
|
||||||
linkCodes,
|
linkCodes,
|
||||||
musicService,
|
musicService,
|
||||||
accessTokens,
|
accessTokens,
|
||||||
clock
|
clock
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (applyContextPath) {
|
||||||
|
const container = express();
|
||||||
|
container.use(bonobUrl.path(), app);
|
||||||
|
return container;
|
||||||
|
} else {
|
||||||
return app;
|
return app;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default server;
|
export default server;
|
||||||
|
|||||||
100
src/smapi.ts
100
src/smapi.ts
@@ -20,8 +20,10 @@ import {
|
|||||||
import { AccessTokens } from "./access_tokens";
|
import { AccessTokens } from "./access_tokens";
|
||||||
import { BONOB_ACCESS_TOKEN_HEADER } from "./server";
|
import { BONOB_ACCESS_TOKEN_HEADER } from "./server";
|
||||||
import { Clock } from "./clock";
|
import { Clock } from "./clock";
|
||||||
|
import { URLBuilder } from "./url_builder";
|
||||||
|
|
||||||
export const LOGIN_ROUTE = "/login";
|
export const LOGIN_ROUTE = "/login";
|
||||||
|
export const REGISTER_ROUTE = "/register";
|
||||||
export const SOAP_PATH = "/ws/sonos";
|
export const SOAP_PATH = "/ws/sonos";
|
||||||
export const STRINGS_ROUTE = "/sonos/strings.xml";
|
export const STRINGS_ROUTE = "/sonos/strings.xml";
|
||||||
export const PRESENTATION_MAP_ROUTE = "/sonos/presentationMap.xml";
|
export const PRESENTATION_MAP_ROUTE = "/sonos/presentationMap.xml";
|
||||||
@@ -131,10 +133,10 @@ export function searchResult(
|
|||||||
|
|
||||||
class SonosSoap {
|
class SonosSoap {
|
||||||
linkCodes: LinkCodes;
|
linkCodes: LinkCodes;
|
||||||
webAddress: string;
|
bonobUrl: URLBuilder;
|
||||||
|
|
||||||
constructor(webAddress: string, linkCodes: LinkCodes) {
|
constructor(bonobUrl: URLBuilder, linkCodes: LinkCodes) {
|
||||||
this.webAddress = webAddress;
|
this.bonobUrl = bonobUrl;
|
||||||
this.linkCodes = linkCodes;
|
this.linkCodes = linkCodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,7 +147,10 @@ class SonosSoap {
|
|||||||
authorizeAccount: {
|
authorizeAccount: {
|
||||||
appUrlStringId: "AppLinkMessage",
|
appUrlStringId: "AppLinkMessage",
|
||||||
deviceLink: {
|
deviceLink: {
|
||||||
regUrl: `${this.webAddress}${LOGIN_ROUTE}?linkCode=${linkCode}`,
|
regUrl: this.bonobUrl
|
||||||
|
.append({ pathname: LOGIN_ROUTE })
|
||||||
|
.with({ searchParams: { linkCode } })
|
||||||
|
.href(),
|
||||||
linkCode: linkCode,
|
linkCode: linkCode,
|
||||||
showLinkCode: false,
|
showLinkCode: false,
|
||||||
},
|
},
|
||||||
@@ -216,31 +221,21 @@ const playlist = (playlist: PlaylistSummary) => ({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const defaultAlbumArtURI = (
|
export const defaultAlbumArtURI = (bonobUrl: URLBuilder, album: AlbumSummary) =>
|
||||||
webAddress: string,
|
bonobUrl.append({ pathname: `/album/${album.id}/art/size/180` });
|
||||||
accessToken: string,
|
|
||||||
album: AlbumSummary
|
|
||||||
) =>
|
|
||||||
`${webAddress}/album/${album.id}/art/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`;
|
|
||||||
|
|
||||||
export const defaultArtistArtURI = (
|
export const defaultArtistArtURI = (
|
||||||
webAddress: string,
|
bonobUrl: URLBuilder,
|
||||||
accessToken: string,
|
|
||||||
artist: ArtistSummary
|
artist: ArtistSummary
|
||||||
) =>
|
) => bonobUrl.append({ pathname: `/artist/${artist.id}/art/size/180` });
|
||||||
`${webAddress}/artist/${artist.id}/art/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`;
|
|
||||||
|
|
||||||
export const album = (
|
export const album = (bonobUrl: URLBuilder, album: AlbumSummary) => ({
|
||||||
webAddress: string,
|
|
||||||
accessToken: string,
|
|
||||||
album: AlbumSummary
|
|
||||||
) => ({
|
|
||||||
itemType: "album",
|
itemType: "album",
|
||||||
id: `album:${album.id}`,
|
id: `album:${album.id}`,
|
||||||
artist: album.artistName,
|
artist: album.artistName,
|
||||||
artistId: album.artistId,
|
artistId: album.artistId,
|
||||||
title: album.name,
|
title: album.name,
|
||||||
albumArtURI: defaultAlbumArtURI(webAddress, accessToken, album),
|
albumArtURI: defaultAlbumArtURI(bonobUrl, album).href(),
|
||||||
canPlay: true,
|
canPlay: true,
|
||||||
// defaults
|
// defaults
|
||||||
// canScroll: false,
|
// canScroll: false,
|
||||||
@@ -248,11 +243,7 @@ export const album = (
|
|||||||
// canAddToFavorites: true
|
// canAddToFavorites: true
|
||||||
});
|
});
|
||||||
|
|
||||||
export const track = (
|
export const track = (bonobUrl: URLBuilder, track: Track) => ({
|
||||||
webAddress: string,
|
|
||||||
accessToken: string,
|
|
||||||
track: Track
|
|
||||||
) => ({
|
|
||||||
itemType: "track",
|
itemType: "track",
|
||||||
id: `track:${track.id}`,
|
id: `track:${track.id}`,
|
||||||
mimeType: track.mimeType,
|
mimeType: track.mimeType,
|
||||||
@@ -263,7 +254,7 @@ export const track = (
|
|||||||
albumId: track.album.id,
|
albumId: track.album.id,
|
||||||
albumArtist: track.artist.name,
|
albumArtist: track.artist.name,
|
||||||
albumArtistId: track.artist.id,
|
albumArtistId: track.artist.id,
|
||||||
albumArtURI: defaultAlbumArtURI(webAddress, accessToken, track.album),
|
albumArtURI: defaultAlbumArtURI(bonobUrl, track.album).href(),
|
||||||
artist: track.artist.name,
|
artist: track.artist.name,
|
||||||
artistId: track.artist.id,
|
artistId: track.artist.id,
|
||||||
duration: track.duration,
|
duration: track.duration,
|
||||||
@@ -273,16 +264,12 @@ export const track = (
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const artist = (
|
export const artist = (bonobUrl: URLBuilder, artist: ArtistSummary) => ({
|
||||||
webAddress: string,
|
|
||||||
accessToken: string,
|
|
||||||
artist: ArtistSummary
|
|
||||||
) => ({
|
|
||||||
itemType: "artist",
|
itemType: "artist",
|
||||||
id: `artist:${artist.id}`,
|
id: `artist:${artist.id}`,
|
||||||
artistId: artist.id,
|
artistId: artist.id,
|
||||||
title: artist.name,
|
title: artist.name,
|
||||||
albumArtURI: defaultArtistArtURI(webAddress, accessToken, artist),
|
albumArtURI: defaultArtistArtURI(bonobUrl, artist).href(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const auth = async (
|
const auth = async (
|
||||||
@@ -334,13 +321,20 @@ type SoapyHeaders = {
|
|||||||
function bindSmapiSoapServiceToExpress(
|
function bindSmapiSoapServiceToExpress(
|
||||||
app: Express,
|
app: Express,
|
||||||
soapPath: string,
|
soapPath: string,
|
||||||
webAddress: string,
|
bonobUrl: URLBuilder,
|
||||||
linkCodes: LinkCodes,
|
linkCodes: LinkCodes,
|
||||||
musicService: MusicService,
|
musicService: MusicService,
|
||||||
accessTokens: AccessTokens,
|
accessTokens: AccessTokens,
|
||||||
clock: Clock
|
clock: Clock
|
||||||
) {
|
) {
|
||||||
const sonosSoap = new SonosSoap(webAddress, linkCodes);
|
const sonosSoap = new SonosSoap(bonobUrl, linkCodes);
|
||||||
|
const urlWithToken = (accessToken: string) =>
|
||||||
|
bonobUrl.append({
|
||||||
|
searchParams: {
|
||||||
|
"bonob-access-token": accessToken,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const soapyService = listen(
|
const soapyService = listen(
|
||||||
app,
|
app,
|
||||||
soapPath,
|
soapPath,
|
||||||
@@ -366,7 +360,11 @@ function bindSmapiSoapServiceToExpress(
|
|||||||
auth(musicService, accessTokens, headers)
|
auth(musicService, accessTokens, headers)
|
||||||
.then(splitId(id))
|
.then(splitId(id))
|
||||||
.then(({ accessToken, type, typeId }) => ({
|
.then(({ accessToken, type, typeId }) => ({
|
||||||
getMediaURIResult: `${webAddress}/stream/${type}/${typeId}`,
|
getMediaURIResult: bonobUrl
|
||||||
|
.append({
|
||||||
|
pathname: `/stream/${type}/${typeId}`,
|
||||||
|
})
|
||||||
|
.href(),
|
||||||
httpHeaders: [
|
httpHeaders: [
|
||||||
{
|
{
|
||||||
header: BONOB_ACCESS_TOKEN_HEADER,
|
header: BONOB_ACCESS_TOKEN_HEADER,
|
||||||
@@ -383,7 +381,10 @@ function bindSmapiSoapServiceToExpress(
|
|||||||
.then(splitId(id))
|
.then(splitId(id))
|
||||||
.then(async ({ musicLibrary, accessToken, typeId }) =>
|
.then(async ({ musicLibrary, accessToken, typeId }) =>
|
||||||
musicLibrary.track(typeId!).then((it) => ({
|
musicLibrary.track(typeId!).then((it) => ({
|
||||||
getMediaMetadataResult: track(webAddress, accessToken, it),
|
getMediaMetadataResult: track(
|
||||||
|
urlWithToken(accessToken),
|
||||||
|
it
|
||||||
|
),
|
||||||
}))
|
}))
|
||||||
),
|
),
|
||||||
search: async (
|
search: async (
|
||||||
@@ -400,7 +401,7 @@ function bindSmapiSoapServiceToExpress(
|
|||||||
searchResult({
|
searchResult({
|
||||||
count: it.length,
|
count: it.length,
|
||||||
mediaCollection: it.map((albumSummary) =>
|
mediaCollection: it.map((albumSummary) =>
|
||||||
album(webAddress, accessToken, albumSummary)
|
album(urlWithToken(accessToken), albumSummary)
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -409,7 +410,7 @@ function bindSmapiSoapServiceToExpress(
|
|||||||
searchResult({
|
searchResult({
|
||||||
count: it.length,
|
count: it.length,
|
||||||
mediaCollection: it.map((artistSummary) =>
|
mediaCollection: it.map((artistSummary) =>
|
||||||
artist(webAddress, accessToken, artistSummary)
|
artist(urlWithToken(accessToken), artistSummary)
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -418,7 +419,7 @@ function bindSmapiSoapServiceToExpress(
|
|||||||
searchResult({
|
searchResult({
|
||||||
count: it.length,
|
count: it.length,
|
||||||
mediaCollection: it.map((aTrack) =>
|
mediaCollection: it.map((aTrack) =>
|
||||||
album(webAddress, accessToken, aTrack.album)
|
album(urlWithToken(accessToken), aTrack.album)
|
||||||
),
|
),
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -452,7 +453,7 @@ function bindSmapiSoapServiceToExpress(
|
|||||||
index: paging._index,
|
index: paging._index,
|
||||||
total,
|
total,
|
||||||
mediaCollection: page.map((it) =>
|
mediaCollection: page.map((it) =>
|
||||||
album(webAddress, accessToken, it)
|
album(urlWithToken(accessToken), it)
|
||||||
),
|
),
|
||||||
relatedBrowse:
|
relatedBrowse:
|
||||||
artist.similarArtists.length > 0
|
artist.similarArtists.length > 0
|
||||||
@@ -483,10 +484,9 @@ function bindSmapiSoapServiceToExpress(
|
|||||||
genreId: it.genre?.id,
|
genreId: it.genre?.id,
|
||||||
duration: it.duration,
|
duration: it.duration,
|
||||||
albumArtURI: defaultAlbumArtURI(
|
albumArtURI: defaultAlbumArtURI(
|
||||||
webAddress,
|
urlWithToken(accessToken),
|
||||||
accessToken,
|
|
||||||
it.album
|
it.album
|
||||||
),
|
).href(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -500,7 +500,7 @@ function bindSmapiSoapServiceToExpress(
|
|||||||
userContent: false,
|
userContent: false,
|
||||||
renameable: false,
|
renameable: false,
|
||||||
},
|
},
|
||||||
...album(webAddress, accessToken, it),
|
...album(urlWithToken(accessToken), it),
|
||||||
},
|
},
|
||||||
// <mediaCollection readonly="true">
|
// <mediaCollection readonly="true">
|
||||||
// </mediaCollection>
|
// </mediaCollection>
|
||||||
@@ -537,7 +537,7 @@ function bindSmapiSoapServiceToExpress(
|
|||||||
musicLibrary.albums(q).then((result) => {
|
musicLibrary.albums(q).then((result) => {
|
||||||
return getMetadataResult({
|
return getMetadataResult({
|
||||||
mediaCollection: result.results.map((it) =>
|
mediaCollection: result.results.map((it) =>
|
||||||
album(webAddress, accessToken, it)
|
album(urlWithToken(accessToken), it)
|
||||||
),
|
),
|
||||||
index: paging._index,
|
index: paging._index,
|
||||||
total: result.total,
|
total: result.total,
|
||||||
@@ -616,7 +616,7 @@ function bindSmapiSoapServiceToExpress(
|
|||||||
return musicLibrary.artists(paging).then((result) => {
|
return musicLibrary.artists(paging).then((result) => {
|
||||||
return getMetadataResult({
|
return getMetadataResult({
|
||||||
mediaCollection: result.results.map((it) =>
|
mediaCollection: result.results.map((it) =>
|
||||||
artist(webAddress, accessToken, it)
|
artist(urlWithToken(accessToken), it)
|
||||||
),
|
),
|
||||||
index: paging._index,
|
index: paging._index,
|
||||||
total: result.total,
|
total: result.total,
|
||||||
@@ -689,7 +689,7 @@ function bindSmapiSoapServiceToExpress(
|
|||||||
.then(([page, total]) => {
|
.then(([page, total]) => {
|
||||||
return getMetadataResult({
|
return getMetadataResult({
|
||||||
mediaMetadata: page.map((it) =>
|
mediaMetadata: page.map((it) =>
|
||||||
track(webAddress, accessToken, it)
|
track(urlWithToken(accessToken), it)
|
||||||
),
|
),
|
||||||
index: paging._index,
|
index: paging._index,
|
||||||
total,
|
total,
|
||||||
@@ -703,7 +703,7 @@ function bindSmapiSoapServiceToExpress(
|
|||||||
.then(([page, total]) => {
|
.then(([page, total]) => {
|
||||||
return getMetadataResult({
|
return getMetadataResult({
|
||||||
mediaCollection: page.map((it) =>
|
mediaCollection: page.map((it) =>
|
||||||
album(webAddress, accessToken, it)
|
album(urlWithToken(accessToken), it)
|
||||||
),
|
),
|
||||||
index: paging._index,
|
index: paging._index,
|
||||||
total,
|
total,
|
||||||
@@ -717,7 +717,7 @@ function bindSmapiSoapServiceToExpress(
|
|||||||
.then(([page, total]) => {
|
.then(([page, total]) => {
|
||||||
return getMetadataResult({
|
return getMetadataResult({
|
||||||
mediaCollection: page.map((it) =>
|
mediaCollection: page.map((it) =>
|
||||||
artist(webAddress, accessToken, it)
|
artist(urlWithToken(accessToken), it)
|
||||||
),
|
),
|
||||||
index: paging._index,
|
index: paging._index,
|
||||||
total,
|
total,
|
||||||
@@ -730,7 +730,7 @@ function bindSmapiSoapServiceToExpress(
|
|||||||
.then(([page, total]) => {
|
.then(([page, total]) => {
|
||||||
return getMetadataResult({
|
return getMetadataResult({
|
||||||
mediaMetadata: page.map((it) =>
|
mediaMetadata: page.map((it) =>
|
||||||
track(webAddress, accessToken, it)
|
track(urlWithToken(accessToken), it)
|
||||||
),
|
),
|
||||||
index: paging._index,
|
index: paging._index,
|
||||||
total,
|
total,
|
||||||
|
|||||||
15
src/sonos.ts
15
src/sonos.ts
@@ -6,6 +6,7 @@ import { head } from "underscore";
|
|||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
import { SOAP_PATH, STRINGS_ROUTE, PRESENTATION_MAP_ROUTE } from "./smapi";
|
import { SOAP_PATH, STRINGS_ROUTE, PRESENTATION_MAP_ROUTE } from "./smapi";
|
||||||
import qs from "querystring";
|
import qs from "querystring";
|
||||||
|
import { URLBuilder } from "./url_builder";
|
||||||
|
|
||||||
export const PRESENTATION_AND_STRINGS_VERSION = "18";
|
export const PRESENTATION_AND_STRINGS_VERSION = "18";
|
||||||
|
|
||||||
@@ -49,25 +50,25 @@ export type Service = {
|
|||||||
authType: "Anonymous" | "AppLink" | "DeviceLink" | "UserId";
|
authType: "Anonymous" | "AppLink" | "DeviceLink" | "UserId";
|
||||||
};
|
};
|
||||||
|
|
||||||
export const stripTailingSlash = (url: string) =>
|
export const stripTrailingSlash = (url: string) =>
|
||||||
url.endsWith("/") ? url.substring(0, url.length - 1) : url;
|
url.endsWith("/") ? url.substring(0, url.length - 1) : url;
|
||||||
|
|
||||||
export const bonobService = (
|
export const bonobService = (
|
||||||
name: string,
|
name: string,
|
||||||
sid: number,
|
sid: number,
|
||||||
bonobRoot: string,
|
bonobUrl: URLBuilder,
|
||||||
authType: "Anonymous" | "AppLink" | "DeviceLink" | "UserId" = "AppLink"
|
authType: "Anonymous" | "AppLink" | "DeviceLink" | "UserId" = "AppLink"
|
||||||
): Service => ({
|
): Service => ({
|
||||||
name,
|
name,
|
||||||
sid,
|
sid,
|
||||||
uri: `${stripTailingSlash(bonobRoot)}${SOAP_PATH}`,
|
uri: bonobUrl.append({pathname: SOAP_PATH }).href(),
|
||||||
secureUri: `${stripTailingSlash(bonobRoot)}${SOAP_PATH}`,
|
secureUri: bonobUrl.append({pathname: SOAP_PATH }).href(),
|
||||||
strings: {
|
strings: {
|
||||||
uri: `${stripTailingSlash(bonobRoot)}${STRINGS_ROUTE}`,
|
uri: bonobUrl.append({pathname: STRINGS_ROUTE }).href(),
|
||||||
version: PRESENTATION_AND_STRINGS_VERSION,
|
version: PRESENTATION_AND_STRINGS_VERSION,
|
||||||
},
|
},
|
||||||
presentation: {
|
presentation: {
|
||||||
uri: `${stripTailingSlash(bonobRoot)}${PRESENTATION_MAP_ROUTE}`,
|
uri: bonobUrl.append({pathname: PRESENTATION_MAP_ROUTE }).href(),
|
||||||
version: PRESENTATION_AND_STRINGS_VERSION,
|
version: PRESENTATION_AND_STRINGS_VERSION,
|
||||||
},
|
},
|
||||||
pollInterval: 1200,
|
pollInterval: 1200,
|
||||||
@@ -83,7 +84,7 @@ export interface Sonos {
|
|||||||
export const SONOS_DISABLED: Sonos = {
|
export const SONOS_DISABLED: Sonos = {
|
||||||
devices: () => Promise.resolve([]),
|
devices: () => Promise.resolve([]),
|
||||||
services: () => Promise.resolve([]),
|
services: () => Promise.resolve([]),
|
||||||
register: (_: Service) => Promise.resolve(false),
|
register: (_: Service) => Promise.resolve(true),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const asService = (musicService: MusicService): Service => ({
|
export const asService = (musicService: MusicService): Service => ({
|
||||||
|
|||||||
72
src/url_builder.ts
Normal file
72
src/url_builder.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
function isURL(url: string | URL): url is URL {
|
||||||
|
return (url as URL).href !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isURLSearchParams(
|
||||||
|
searchParams: Record<string, string> | URLSearchParams
|
||||||
|
): searchParams is URLSearchParams {
|
||||||
|
return (searchParams as URLSearchParams).getAll !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const stripTrailingSlash = (url: string) =>
|
||||||
|
url.endsWith("/") ? url.substring(0, url.length - 1) : url;
|
||||||
|
|
||||||
|
export class URLBuilder {
|
||||||
|
private url: URL;
|
||||||
|
|
||||||
|
constructor(url: string | URL) {
|
||||||
|
this.url = isURL(url) ? url : new URL(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
public append = (
|
||||||
|
bits: Partial<{
|
||||||
|
pathname: string | undefined;
|
||||||
|
searchParams: Record<string, string> | URLSearchParams;
|
||||||
|
}> = { pathname: undefined, searchParams: undefined }
|
||||||
|
) => {
|
||||||
|
let result = new URLBuilder(this.url);
|
||||||
|
if (bits.pathname)
|
||||||
|
result = result.with({
|
||||||
|
pathname: stripTrailingSlash(this.url.pathname) + bits.pathname,
|
||||||
|
});
|
||||||
|
if (bits.searchParams) {
|
||||||
|
const newSearchParams = new URLSearchParams(this.url.searchParams);
|
||||||
|
(isURLSearchParams(bits.searchParams)
|
||||||
|
? bits.searchParams
|
||||||
|
: new URLSearchParams(bits.searchParams)
|
||||||
|
).forEach((v, k) => newSearchParams.append(k, v));
|
||||||
|
result = result.with({ searchParams: newSearchParams });
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
public with = (
|
||||||
|
bits: Partial<{
|
||||||
|
pathname: string | undefined;
|
||||||
|
searchParams: Record<string, string> | URLSearchParams;
|
||||||
|
}> = { pathname: undefined, searchParams: undefined }
|
||||||
|
) => {
|
||||||
|
const result = new URL(this.url.href);
|
||||||
|
if (bits.pathname) result.pathname = bits.pathname;
|
||||||
|
if (bits.searchParams) {
|
||||||
|
const keysToDelete: string[] = [];
|
||||||
|
result.searchParams.forEach((_, k) => keysToDelete.push(k));
|
||||||
|
keysToDelete.forEach((k) => result.searchParams.delete(k));
|
||||||
|
(isURLSearchParams(bits.searchParams)
|
||||||
|
? bits.searchParams
|
||||||
|
: new URLSearchParams(bits.searchParams)
|
||||||
|
).forEach((v, k) => result.searchParams.append(k, v));
|
||||||
|
}
|
||||||
|
return new URLBuilder(result);
|
||||||
|
};
|
||||||
|
|
||||||
|
public href = () => this.url.href;
|
||||||
|
public pathname = () => this.url.pathname;
|
||||||
|
public searchParams = () => this.url.searchParams;
|
||||||
|
public path = () => this.url.pathname + this.url.search;
|
||||||
|
public toString = () => this.url.href;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function url(url: string | URL): URLBuilder {
|
||||||
|
return new URLBuilder(url);
|
||||||
|
}
|
||||||
@@ -13,7 +13,12 @@ describe("config", () => {
|
|||||||
process.env = OLD_ENV;
|
process.env = OLD_ENV;
|
||||||
});
|
});
|
||||||
|
|
||||||
function describeBooleanConfigValue(name: string, envVar: string, expectedDefault: boolean, propertyGetter: (config: any) => any) {
|
function describeBooleanConfigValue(
|
||||||
|
name: string,
|
||||||
|
envVar: string,
|
||||||
|
expectedDefault: boolean,
|
||||||
|
propertyGetter: (config: any) => any
|
||||||
|
) {
|
||||||
describe(name, () => {
|
describe(name, () => {
|
||||||
function expecting({
|
function expecting({
|
||||||
value,
|
value,
|
||||||
@@ -35,7 +40,72 @@ describe("config", () => {
|
|||||||
expecting({ value: "false", expected: false });
|
expecting({ value: "false", expected: false });
|
||||||
expecting({ value: "foo", expected: false });
|
expecting({ value: "foo", expected: false });
|
||||||
});
|
});
|
||||||
};
|
}
|
||||||
|
|
||||||
|
describe("bonobUrl", () => {
|
||||||
|
describe("when BONOB_URL is specified", () => {
|
||||||
|
it("should be used", () => {
|
||||||
|
const url = "http://bonob1.example.com:8877/";
|
||||||
|
process.env["BONOB_URL"] = url;
|
||||||
|
|
||||||
|
expect(config().bonobUrl.href()).toEqual(url);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when BONOB_URL is not specified, however legacy BONOB_WEB_ADDRESS is specified", () => {
|
||||||
|
it("should be used", () => {
|
||||||
|
const url = "http://bonob2.example.com:9988/";
|
||||||
|
process.env["BONOB_URL"] = "";
|
||||||
|
process.env["BONOB_WEB_ADDRESS"] = url;
|
||||||
|
|
||||||
|
expect(config().bonobUrl.href()).toEqual(url);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when neither BONOB_URL nor BONOB_WEB_ADDRESS are specified", () => {
|
||||||
|
describe("when BONOB_PORT is not specified", () => {
|
||||||
|
it(`should default to http://${hostname()}:4534`, () => {
|
||||||
|
expect(config().bonobUrl.href()).toEqual(
|
||||||
|
`http://${hostname()}:4534/`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when BONOB_PORT is specified as 3322", () => {
|
||||||
|
it(`should default to http://${hostname()}:3322`, () => {
|
||||||
|
process.env["BONOB_PORT"] = "3322";
|
||||||
|
expect(config().bonobUrl.href()).toEqual(
|
||||||
|
`http://${hostname()}:3322/`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("navidrome", () => {
|
||||||
|
describe("url", () => {
|
||||||
|
describe("when BONOB_NAVIDROME_URL is not specified", () => {
|
||||||
|
it(`should default to http://${hostname()}:4533`, () => {
|
||||||
|
expect(config().navidrome.url).toEqual(`http://${hostname()}:4533`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when BONOB_NAVIDROME_URL is ''", () => {
|
||||||
|
it(`should default to http://${hostname()}:4533`, () => {
|
||||||
|
process.env["BONOB_NAVIDROME_URL"] = "";
|
||||||
|
expect(config().navidrome.url).toEqual(`http://${hostname()}:4533`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when BONOB_NAVIDROME_URL is specified", () => {
|
||||||
|
it(`should use it`, () => {
|
||||||
|
const url = "http://navidrome.example.com:1234";
|
||||||
|
process.env["BONOB_NAVIDROME_URL"] = url;
|
||||||
|
expect(config().navidrome.url).toEqual(url);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("secret", () => {
|
describe("secret", () => {
|
||||||
it("should default to bonob", () => {
|
it("should default to bonob", () => {
|
||||||
@@ -60,7 +130,12 @@ describe("config", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describeBooleanConfigValue("deviceDiscovery", "BONOB_SONOS_DEVICE_DISCOVERY", true, config => config.sonos.deviceDiscovery);
|
describeBooleanConfigValue(
|
||||||
|
"deviceDiscovery",
|
||||||
|
"BONOB_SONOS_DEVICE_DISCOVERY",
|
||||||
|
true,
|
||||||
|
(config) => config.sonos.deviceDiscovery
|
||||||
|
);
|
||||||
|
|
||||||
describe("seedHost", () => {
|
describe("seedHost", () => {
|
||||||
it("should default to undefined", () => {
|
it("should default to undefined", () => {
|
||||||
@@ -73,7 +148,12 @@ describe("config", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describeBooleanConfigValue("autoRegister", "BONOB_SONOS_AUTO_REGISTER", false, config => config.sonos.autoRegister);
|
describeBooleanConfigValue(
|
||||||
|
"autoRegister",
|
||||||
|
"BONOB_SONOS_AUTO_REGISTER",
|
||||||
|
false,
|
||||||
|
(config) => config.sonos.autoRegister
|
||||||
|
);
|
||||||
|
|
||||||
describe("sid", () => {
|
describe("sid", () => {
|
||||||
it("should default to 246", () => {
|
it("should default to 246", () => {
|
||||||
@@ -111,6 +191,16 @@ describe("config", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describeBooleanConfigValue("scrobbleTracks", "BONOB_SCROBBLE_TRACKS", true, config => config.scrobbleTracks);
|
describeBooleanConfigValue(
|
||||||
describeBooleanConfigValue("reportNowPlaying", "BONOB_REPORT_NOW_PLAYING", true, config => config.reportNowPlaying);
|
"scrobbleTracks",
|
||||||
|
"BONOB_SCROBBLE_TRACKS",
|
||||||
|
true,
|
||||||
|
(config) => config.scrobbleTracks
|
||||||
|
);
|
||||||
|
describeBooleanConfigValue(
|
||||||
|
"reportNowPlaying",
|
||||||
|
"BONOB_REPORT_NOW_PLAYING",
|
||||||
|
true,
|
||||||
|
(config) => config.reportNowPlaying
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import { Credentials } from "../src/music_service";
|
|||||||
import makeServer from "../src/server";
|
import makeServer from "../src/server";
|
||||||
import { Service, bonobService, SONOS_DISABLED } from "../src/sonos";
|
import { Service, bonobService, SONOS_DISABLED } from "../src/sonos";
|
||||||
import supersoap from "./supersoap";
|
import supersoap from "./supersoap";
|
||||||
|
import url, { URLBuilder } from "../src/url_builder";
|
||||||
|
|
||||||
class LoggedInSonosDriver {
|
class LoggedInSonosDriver {
|
||||||
client: Client;
|
client: Client;
|
||||||
@@ -41,7 +42,8 @@ class LoggedInSonosDriver {
|
|||||||
let next = path.shift();
|
let next = path.shift();
|
||||||
while (next) {
|
while (next) {
|
||||||
if (next != "root") {
|
if (next != "root") {
|
||||||
const childIds = this.currentMetadata!.getMetadataResult.mediaCollection!.map(
|
const childIds =
|
||||||
|
this.currentMetadata!.getMetadataResult.mediaCollection!.map(
|
||||||
(it) => it.id
|
(it) => it.id
|
||||||
);
|
);
|
||||||
if (!childIds.includes(next)) {
|
if (!childIds.includes(next)) {
|
||||||
@@ -74,31 +76,50 @@ class LoggedInSonosDriver {
|
|||||||
|
|
||||||
class SonosDriver {
|
class SonosDriver {
|
||||||
server: Express;
|
server: Express;
|
||||||
rootUrl: string;
|
bonobUrl: URLBuilder;
|
||||||
service: Service;
|
service: Service;
|
||||||
|
|
||||||
constructor(server: Express, rootUrl: string, service: Service) {
|
constructor(server: Express, bonobUrl: URLBuilder, service: Service) {
|
||||||
this.server = server;
|
this.server = server;
|
||||||
this.rootUrl = rootUrl;
|
this.bonobUrl = bonobUrl;
|
||||||
this.service = service;
|
this.service = service;
|
||||||
}
|
}
|
||||||
|
|
||||||
stripServiceRoot = (url: string) => url.replace(this.rootUrl, "");
|
extractPathname = (url: string) => new URL(url).pathname;
|
||||||
|
|
||||||
|
async register() {
|
||||||
|
const action = await request(this.server)
|
||||||
|
.get(this.bonobUrl.append({ pathname: "/" }).pathname())
|
||||||
|
.expect(200)
|
||||||
|
.then((response) => {
|
||||||
|
const m = response.text.match(/ action="(.*)" /i);
|
||||||
|
return m![1]!;
|
||||||
|
});
|
||||||
|
|
||||||
|
return request(this.server)
|
||||||
|
.post(action)
|
||||||
|
.type("form")
|
||||||
|
.send({})
|
||||||
|
.expect(200)
|
||||||
|
.then((response) =>
|
||||||
|
expect(response.text).toContain("Successfully registered")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async addService() {
|
async addService() {
|
||||||
expect(this.service.authType).toEqual("AppLink");
|
expect(this.service.authType).toEqual("AppLink");
|
||||||
|
|
||||||
await request(this.server)
|
await request(this.server)
|
||||||
.get(this.stripServiceRoot(this.service.strings!.uri!))
|
.get(this.extractPathname(this.service.strings!.uri!))
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
await request(this.server)
|
await request(this.server)
|
||||||
.get(this.stripServiceRoot(this.service.presentation!.uri!))
|
.get(this.extractPathname(this.service.presentation!.uri!))
|
||||||
.expect(200);
|
.expect(200);
|
||||||
|
|
||||||
const client = await createClientAsync(`${this.service.uri}?wsdl`, {
|
const client = await createClientAsync(`${this.service.uri}?wsdl`, {
|
||||||
endpoint: this.service.uri,
|
endpoint: this.service.uri,
|
||||||
httpClient: supersoap(this.server, this.rootUrl),
|
httpClient: supersoap(this.server),
|
||||||
});
|
});
|
||||||
|
|
||||||
return client
|
return client
|
||||||
@@ -109,12 +130,18 @@ class SonosDriver {
|
|||||||
)
|
)
|
||||||
.then(({ regUrl, linkCode }: { regUrl: string; linkCode: string }) => ({
|
.then(({ regUrl, linkCode }: { regUrl: string; linkCode: string }) => ({
|
||||||
login: async ({ username, password }: Credentials) => {
|
login: async ({ username, password }: Credentials) => {
|
||||||
await request(this.server)
|
const action = await request(this.server)
|
||||||
.get(this.stripServiceRoot(regUrl))
|
.get(this.extractPathname(regUrl))
|
||||||
.expect(200);
|
.expect(200)
|
||||||
|
.then((response) => {
|
||||||
|
const m = response.text.match(/ action="(.*)" /i);
|
||||||
|
return m![1]!;
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`posting to action ${action}`);
|
||||||
|
|
||||||
return request(this.server)
|
return request(this.server)
|
||||||
.post(this.stripServiceRoot(regUrl))
|
.post(action)
|
||||||
.type("form")
|
.type("form")
|
||||||
.send({ username, password, linkCode })
|
.send({ username, password, linkCode })
|
||||||
.then((response) => ({
|
.then((response) => ({
|
||||||
@@ -140,28 +167,24 @@ class SonosDriver {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe("scenarios", () => {
|
describe("scenarios", () => {
|
||||||
const bonobUrl = "http://localhost:1234";
|
|
||||||
const bonob = bonobService("bonob", 123, bonobUrl);
|
|
||||||
const musicService = new InMemoryMusicService().hasArtists(
|
const musicService = new InMemoryMusicService().hasArtists(
|
||||||
BOB_MARLEY,
|
BOB_MARLEY,
|
||||||
BLONDIE
|
BLONDIE
|
||||||
);
|
);
|
||||||
const linkCodes = new InMemoryLinkCodes();
|
const linkCodes = new InMemoryLinkCodes();
|
||||||
const server = makeServer(
|
|
||||||
SONOS_DISABLED,
|
|
||||||
bonob,
|
|
||||||
bonobUrl,
|
|
||||||
musicService,
|
|
||||||
linkCodes
|
|
||||||
);
|
|
||||||
|
|
||||||
const sonosDriver = new SonosDriver(server, bonobUrl, bonob);
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
musicService.clear();
|
musicService.clear();
|
||||||
linkCodes.clear();
|
linkCodes.clear();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
function itShouldBeAbleToAddTheService(sonosDriver: SonosDriver) {
|
||||||
|
describe("registering bonob with the sonos device", () => {
|
||||||
|
it("should complete successfully", async () => {
|
||||||
|
await sonosDriver.register();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("adding the service", () => {
|
describe("adding the service", () => {
|
||||||
describe("when the user doesnt exists within the music service", () => {
|
describe("when the user doesnt exists within the music service", () => {
|
||||||
const username = "invaliduser";
|
const username = "invaliduser";
|
||||||
@@ -205,9 +228,7 @@ describe("scenarios", () => {
|
|||||||
.then((it) => it.navigate("root", "artists"))
|
.then((it) => it.navigate("root", "artists"))
|
||||||
.then((it) =>
|
.then((it) =>
|
||||||
it.expectTitles(
|
it.expectTitles(
|
||||||
[BLONDIE, BOB_MARLEY, MADONNA].map(
|
[BLONDIE, BOB_MARLEY, MADONNA].map((it) => it.name)
|
||||||
(it) => it.name
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
@@ -220,12 +241,63 @@ describe("scenarios", () => {
|
|||||||
.then((it) => it.navigate("root", "albums"))
|
.then((it) => it.navigate("root", "albums"))
|
||||||
.then((it) =>
|
.then((it) =>
|
||||||
it.expectTitles(
|
it.expectTitles(
|
||||||
[...BLONDIE.albums, ...BOB_MARLEY.albums, ...MADONNA.albums].map(
|
[
|
||||||
(it) => it.name
|
...BLONDIE.albums,
|
||||||
)
|
...BOB_MARLEY.albums,
|
||||||
|
...MADONNA.albums,
|
||||||
|
].map((it) => it.name)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("when the bonobUrl has no context path and no trailing slash", () => {
|
||||||
|
const bonobUrl = url("http://localhost:1234");
|
||||||
|
const bonob = bonobService("bonob", 123, bonobUrl);
|
||||||
|
const server = makeServer(
|
||||||
|
SONOS_DISABLED,
|
||||||
|
bonob,
|
||||||
|
bonobUrl,
|
||||||
|
musicService,
|
||||||
|
linkCodes
|
||||||
|
);
|
||||||
|
|
||||||
|
const sonosDriver = new SonosDriver(server, bonobUrl, bonob);
|
||||||
|
|
||||||
|
itShouldBeAbleToAddTheService(sonosDriver);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when the bonobUrl has no context path, but does have a trailing slash", () => {
|
||||||
|
const bonobUrl = url("http://localhost:1234/");
|
||||||
|
const bonob = bonobService("bonob", 123, bonobUrl);
|
||||||
|
const server = makeServer(
|
||||||
|
SONOS_DISABLED,
|
||||||
|
bonob,
|
||||||
|
bonobUrl,
|
||||||
|
musicService,
|
||||||
|
linkCodes
|
||||||
|
);
|
||||||
|
|
||||||
|
const sonosDriver = new SonosDriver(server, bonobUrl, bonob);
|
||||||
|
|
||||||
|
itShouldBeAbleToAddTheService(sonosDriver);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when the bonobUrl has a context path", () => {
|
||||||
|
const bonobUrl = url("http://localhost:1234/context-for-bonob");
|
||||||
|
const bonob = bonobService("bonob", 123, bonobUrl);
|
||||||
|
const server = makeServer(
|
||||||
|
SONOS_DISABLED,
|
||||||
|
bonob,
|
||||||
|
bonobUrl,
|
||||||
|
musicService,
|
||||||
|
linkCodes
|
||||||
|
);
|
||||||
|
|
||||||
|
const sonosDriver = new SonosDriver(server, bonobUrl, bonob);
|
||||||
|
|
||||||
|
itShouldBeAbleToAddTheService(sonosDriver);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,7 +2,11 @@ import { v4 as uuid } from "uuid";
|
|||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import request from "supertest";
|
import request from "supertest";
|
||||||
import { MusicService } from "../src/music_service";
|
import { MusicService } from "../src/music_service";
|
||||||
import makeServer, { BONOB_ACCESS_TOKEN_HEADER, RangeBytesFromFilter, rangeFilterFor } from "../src/server";
|
import makeServer, {
|
||||||
|
BONOB_ACCESS_TOKEN_HEADER,
|
||||||
|
RangeBytesFromFilter,
|
||||||
|
rangeFilterFor,
|
||||||
|
} from "../src/server";
|
||||||
import { SONOS_DISABLED, Sonos, Device } from "../src/sonos";
|
import { SONOS_DISABLED, Sonos, Device } from "../src/sonos";
|
||||||
|
|
||||||
import { aDevice, aService } from "./builders";
|
import { aDevice, aService } from "./builders";
|
||||||
@@ -11,6 +15,7 @@ import { ExpiringAccessTokens } from "../src/access_tokens";
|
|||||||
import { InMemoryLinkCodes } from "../src/link_codes";
|
import { InMemoryLinkCodes } from "../src/link_codes";
|
||||||
import { Response } from "express";
|
import { Response } from "express";
|
||||||
import { Transform } from "stream";
|
import { Transform } from "stream";
|
||||||
|
import url from "../src/url_builder";
|
||||||
|
|
||||||
describe("rangeFilterFor", () => {
|
describe("rangeFilterFor", () => {
|
||||||
describe("invalid range header string", () => {
|
describe("invalid range header string", () => {
|
||||||
@@ -24,12 +29,13 @@ describe("rangeFilterFor", () => {
|
|||||||
"seconds",
|
"seconds",
|
||||||
"seconds=0",
|
"seconds=0",
|
||||||
"seconds=-",
|
"seconds=-",
|
||||||
]
|
];
|
||||||
|
|
||||||
for (let range in cases) {
|
for (let range in cases) {
|
||||||
expect(() => rangeFilterFor(range)).toThrowError(`Unsupported range: ${range}`);
|
expect(() => rangeFilterFor(range)).toThrowError(
|
||||||
|
`Unsupported range: ${range}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -46,7 +52,7 @@ describe("rangeFilterFor", () => {
|
|||||||
|
|
||||||
describe("64-", () => {
|
describe("64-", () => {
|
||||||
it("should return a RangeBytesFromFilter", () => {
|
it("should return a RangeBytesFromFilter", () => {
|
||||||
const filter = rangeFilterFor("bytes=64-")
|
const filter = rangeFilterFor("bytes=64-");
|
||||||
|
|
||||||
expect(filter instanceof RangeBytesFromFilter).toEqual(true);
|
expect(filter instanceof RangeBytesFromFilter).toEqual(true);
|
||||||
expect((filter as RangeBytesFromFilter).from).toEqual(64);
|
expect((filter as RangeBytesFromFilter).from).toEqual(64);
|
||||||
@@ -56,19 +62,25 @@ describe("rangeFilterFor", () => {
|
|||||||
|
|
||||||
describe("-900", () => {
|
describe("-900", () => {
|
||||||
it("should fail", () => {
|
it("should fail", () => {
|
||||||
expect(() => rangeFilterFor("bytes=-900")).toThrowError("Unsupported range: bytes=-900")
|
expect(() => rangeFilterFor("bytes=-900")).toThrowError(
|
||||||
|
"Unsupported range: bytes=-900"
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("100-200", () => {
|
describe("100-200", () => {
|
||||||
it("should fail", () => {
|
it("should fail", () => {
|
||||||
expect(() => rangeFilterFor("bytes=100-200")).toThrowError("Unsupported range: bytes=100-200")
|
expect(() => rangeFilterFor("bytes=100-200")).toThrowError(
|
||||||
|
"Unsupported range: bytes=100-200"
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("100-200, 400-500", () => {
|
describe("100-200, 400-500", () => {
|
||||||
it("should fail", () => {
|
it("should fail", () => {
|
||||||
expect(() => rangeFilterFor("bytes=100-200, 400-500")).toThrowError("Unsupported range: bytes=100-200, 400-500")
|
expect(() => rangeFilterFor("bytes=100-200, 400-500")).toThrowError(
|
||||||
|
"Unsupported range: bytes=100-200, 400-500"
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -78,11 +90,13 @@ describe("rangeFilterFor", () => {
|
|||||||
const cases = [
|
const cases = [
|
||||||
"seconds=0-",
|
"seconds=0-",
|
||||||
"seconds=100-200",
|
"seconds=100-200",
|
||||||
"chickens=100-200, 400-500"
|
"chickens=100-200, 400-500",
|
||||||
]
|
];
|
||||||
|
|
||||||
for (let range in cases) {
|
for (let range in cases) {
|
||||||
expect(() => rangeFilterFor(range)).toThrowError(`Unsupported range: ${range}`);
|
expect(() => rangeFilterFor(range)).toThrowError(
|
||||||
|
`Unsupported range: ${range}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -93,48 +107,48 @@ describe("RangeBytesFromFilter", () => {
|
|||||||
describe("0-", () => {
|
describe("0-", () => {
|
||||||
it("should not filter at all", () => {
|
it("should not filter at all", () => {
|
||||||
const filter = new RangeBytesFromFilter(0);
|
const filter = new RangeBytesFromFilter(0);
|
||||||
const result: any[] = []
|
const result: any[] = [];
|
||||||
|
|
||||||
const callback = (_?: Error | null, data?: any) => {
|
const callback = (_?: Error | null, data?: any) => {
|
||||||
if(data) result.push(...data!)
|
if (data) result.push(...data!);
|
||||||
}
|
};
|
||||||
|
|
||||||
filter._transform(['a', 'b', 'c'], 'ascii', callback)
|
filter._transform(["a", "b", "c"], "ascii", callback);
|
||||||
filter._transform(['d', 'e', 'f'], 'ascii', callback)
|
filter._transform(["d", "e", "f"], "ascii", callback);
|
||||||
|
|
||||||
expect(result).toEqual(['a', 'b', 'c', 'd', 'e', 'f'])
|
expect(result).toEqual(["a", "b", "c", "d", "e", "f"]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("1-", () => {
|
describe("1-", () => {
|
||||||
it("should filter the first byte", () => {
|
it("should filter the first byte", () => {
|
||||||
const filter = new RangeBytesFromFilter(1);
|
const filter = new RangeBytesFromFilter(1);
|
||||||
const result: any[] = []
|
const result: any[] = [];
|
||||||
|
|
||||||
const callback = (_?: Error | null, data?: any) => {
|
const callback = (_?: Error | null, data?: any) => {
|
||||||
if(data) result.push(...data!)
|
if (data) result.push(...data!);
|
||||||
}
|
};
|
||||||
|
|
||||||
filter._transform(['a', 'b', 'c'], 'ascii', callback)
|
filter._transform(["a", "b", "c"], "ascii", callback);
|
||||||
filter._transform(['d', 'e', 'f'], 'ascii', callback)
|
filter._transform(["d", "e", "f"], "ascii", callback);
|
||||||
|
|
||||||
expect(result).toEqual(['b', 'c', 'd', 'e', 'f'])
|
expect(result).toEqual(["b", "c", "d", "e", "f"]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("5-", () => {
|
describe("5-", () => {
|
||||||
it("should filter the first byte", () => {
|
it("should filter the first byte", () => {
|
||||||
const filter = new RangeBytesFromFilter(5);
|
const filter = new RangeBytesFromFilter(5);
|
||||||
const result: any[] = []
|
const result: any[] = [];
|
||||||
|
|
||||||
const callback = (_?: Error | null, data?: any) => {
|
const callback = (_?: Error | null, data?: any) => {
|
||||||
if(data) result.push(...data!)
|
if (data) result.push(...data!);
|
||||||
}
|
};
|
||||||
|
|
||||||
filter._transform(['a', 'b', 'c'], 'ascii', callback)
|
filter._transform(["a", "b", "c"], "ascii", callback);
|
||||||
filter._transform(['d', 'e', 'f'], 'ascii', callback)
|
filter._transform(["d", "e", "f"], "ascii", callback);
|
||||||
|
|
||||||
expect(result).toEqual(['f'])
|
expect(result).toEqual(["f"]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -146,18 +160,25 @@ describe("server", () => {
|
|||||||
jest.resetAllMocks();
|
jest.resetAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const bonobUrlWithNoContextPath = url("http://bonob.localhost:1234");
|
||||||
|
const bonobUrlWithContextPath = url("http://bonob.localhost:1234/aContext");
|
||||||
|
|
||||||
|
[bonobUrlWithNoContextPath, bonobUrlWithContextPath].forEach((bonobUrl) => {
|
||||||
|
describe(`a bonobUrl of ${bonobUrl}`, () => {
|
||||||
describe("/", () => {
|
describe("/", () => {
|
||||||
describe("when sonos integration is disabled", () => {
|
describe("when sonos integration is disabled", () => {
|
||||||
const server = makeServer(
|
const server = makeServer(
|
||||||
SONOS_DISABLED,
|
SONOS_DISABLED,
|
||||||
aService(),
|
aService(),
|
||||||
"http://localhost:1234",
|
bonobUrl,
|
||||||
new InMemoryMusicService()
|
new InMemoryMusicService()
|
||||||
);
|
);
|
||||||
|
|
||||||
describe("devices list", () => {
|
describe("devices list", () => {
|
||||||
it("should be empty", async () => {
|
it("should be empty", async () => {
|
||||||
const res = await request(server).get("/").send();
|
const res = await request(server)
|
||||||
|
.get(bonobUrl.append({ pathname: "/" }).pathname())
|
||||||
|
.send();
|
||||||
|
|
||||||
expect(res.status).toEqual(200);
|
expect(res.status).toEqual(200);
|
||||||
expect(res.text).not.toMatch(/class=device/);
|
expect(res.text).not.toMatch(/class=device/);
|
||||||
@@ -209,13 +230,15 @@ describe("server", () => {
|
|||||||
const server = makeServer(
|
const server = makeServer(
|
||||||
fakeSonos,
|
fakeSonos,
|
||||||
missingBonobService,
|
missingBonobService,
|
||||||
"http://localhost:1234",
|
bonobUrl,
|
||||||
new InMemoryMusicService()
|
new InMemoryMusicService()
|
||||||
);
|
);
|
||||||
|
|
||||||
describe("devices list", () => {
|
describe("devices list", () => {
|
||||||
it("should contain the devices returned from sonos", async () => {
|
it("should contain the devices returned from sonos", async () => {
|
||||||
const res = await request(server).get("/").send();
|
const res = await request(server)
|
||||||
|
.get(bonobUrl.append({ pathname: "/" }).path())
|
||||||
|
.send();
|
||||||
|
|
||||||
expect(res.status).toEqual(200);
|
expect(res.status).toEqual(200);
|
||||||
expect(res.text).toMatch(/device1\s+\(172.0.0.1:4301\)/);
|
expect(res.text).toMatch(/device1\s+\(172.0.0.1:4301\)/);
|
||||||
@@ -225,7 +248,9 @@ describe("server", () => {
|
|||||||
|
|
||||||
describe("services", () => {
|
describe("services", () => {
|
||||||
it("should contain a list of services returned from sonos", async () => {
|
it("should contain a list of services returned from sonos", async () => {
|
||||||
const res = await request(server).get("/").send();
|
const res = await request(server)
|
||||||
|
.get(bonobUrl.append({ pathname: "/" }).path())
|
||||||
|
.send();
|
||||||
|
|
||||||
expect(res.status).toEqual(200);
|
expect(res.status).toEqual(200);
|
||||||
expect(res.text).toMatch(/Services\s+4/);
|
expect(res.text).toMatch(/Services\s+4/);
|
||||||
@@ -238,7 +263,9 @@ describe("server", () => {
|
|||||||
|
|
||||||
describe("registration status", () => {
|
describe("registration status", () => {
|
||||||
it("should be not-registered", async () => {
|
it("should be not-registered", async () => {
|
||||||
const res = await request(server).get("/").send();
|
const res = await request(server)
|
||||||
|
.get(bonobUrl.append({ pathname: "/" }).path())
|
||||||
|
.send();
|
||||||
expect(res.status).toEqual(200);
|
expect(res.status).toEqual(200);
|
||||||
expect(res.text).toMatch(/No existing service registration/);
|
expect(res.text).toMatch(/No existing service registration/);
|
||||||
});
|
});
|
||||||
@@ -264,13 +291,15 @@ describe("server", () => {
|
|||||||
const server = makeServer(
|
const server = makeServer(
|
||||||
fakeSonos,
|
fakeSonos,
|
||||||
bonobService,
|
bonobService,
|
||||||
"http://localhost:1234",
|
bonobUrl,
|
||||||
new InMemoryMusicService()
|
new InMemoryMusicService()
|
||||||
);
|
);
|
||||||
|
|
||||||
describe("registration status", () => {
|
describe("registration status", () => {
|
||||||
it("should be registered", async () => {
|
it("should be registered", async () => {
|
||||||
const res = await request(server).get("/").send();
|
const res = await request(server)
|
||||||
|
.get(bonobUrl.append({ pathname: "/" }).path())
|
||||||
|
.send();
|
||||||
expect(res.status).toEqual(200);
|
expect(res.status).toEqual(200);
|
||||||
expect(res.text).toMatch(/Existing service config/);
|
expect(res.text).toMatch(/Existing service config/);
|
||||||
});
|
});
|
||||||
@@ -289,7 +318,7 @@ describe("server", () => {
|
|||||||
const server = makeServer(
|
const server = makeServer(
|
||||||
sonos as unknown as Sonos,
|
sonos as unknown as Sonos,
|
||||||
theService,
|
theService,
|
||||||
"http://localhost:1234",
|
bonobUrl,
|
||||||
new InMemoryMusicService()
|
new InMemoryMusicService()
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -297,7 +326,9 @@ describe("server", () => {
|
|||||||
it("should return a nice message", async () => {
|
it("should return a nice message", async () => {
|
||||||
sonos.register.mockResolvedValue(true);
|
sonos.register.mockResolvedValue(true);
|
||||||
|
|
||||||
const res = await request(server).post("/register").send();
|
const res = await request(server)
|
||||||
|
.post(bonobUrl.append({ pathname: "/register" }).path())
|
||||||
|
.send();
|
||||||
|
|
||||||
expect(res.status).toEqual(200);
|
expect(res.status).toEqual(200);
|
||||||
expect(res.text).toMatch("Successfully registered");
|
expect(res.text).toMatch("Successfully registered");
|
||||||
@@ -311,7 +342,9 @@ describe("server", () => {
|
|||||||
it("should return a failure message", async () => {
|
it("should return a failure message", async () => {
|
||||||
sonos.register.mockResolvedValue(false);
|
sonos.register.mockResolvedValue(false);
|
||||||
|
|
||||||
const res = await request(server).post("/register").send();
|
const res = await request(server)
|
||||||
|
.post(bonobUrl.append({ pathname: "/register" }).path())
|
||||||
|
.send();
|
||||||
|
|
||||||
expect(res.status).toEqual(500);
|
expect(res.status).toEqual(500);
|
||||||
expect(res.text).toMatch("Registration failed!");
|
expect(res.text).toMatch("Registration failed!");
|
||||||
@@ -337,7 +370,7 @@ describe("server", () => {
|
|||||||
const server = makeServer(
|
const server = makeServer(
|
||||||
jest.fn() as unknown as Sonos,
|
jest.fn() as unknown as Sonos,
|
||||||
aService(),
|
aService(),
|
||||||
"http://localhost:1234",
|
bonobUrl,
|
||||||
musicService as unknown as MusicService,
|
musicService as unknown as MusicService,
|
||||||
new InMemoryLinkCodes(),
|
new InMemoryLinkCodes(),
|
||||||
accessTokens
|
accessTokens
|
||||||
@@ -356,15 +389,17 @@ describe("server", () => {
|
|||||||
return {
|
return {
|
||||||
pipe: (res: Response) => {
|
pipe: (res: Response) => {
|
||||||
res.send(content);
|
res.send(content);
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
})
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
describe("HEAD requests", () => {
|
describe("HEAD requests", () => {
|
||||||
describe("when there is no access-token", () => {
|
describe("when there is no access-token", () => {
|
||||||
it("should return a 401", async () => {
|
it("should return a 401", async () => {
|
||||||
const res = await request(server).head(`/stream/track/${trackId}`);
|
const res = await request(server).head(
|
||||||
|
bonobUrl.append({ pathname: `/stream/track/${trackId}` }).path()
|
||||||
|
);
|
||||||
|
|
||||||
expect(res.status).toEqual(401);
|
expect(res.status).toEqual(401);
|
||||||
});
|
});
|
||||||
@@ -375,7 +410,11 @@ describe("server", () => {
|
|||||||
now = now.add(1, "day");
|
now = now.add(1, "day");
|
||||||
|
|
||||||
const res = await request(server)
|
const res = await request(server)
|
||||||
.head(`/stream/track/${trackId}`)
|
.head(
|
||||||
|
bonobUrl
|
||||||
|
.append({ pathname: `/stream/track/${trackId}` })
|
||||||
|
.path()
|
||||||
|
)
|
||||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
||||||
|
|
||||||
expect(res.status).toEqual(401);
|
expect(res.status).toEqual(401);
|
||||||
@@ -398,16 +437,18 @@ describe("server", () => {
|
|||||||
musicLibrary.stream.mockResolvedValue(trackStream);
|
musicLibrary.stream.mockResolvedValue(trackStream);
|
||||||
|
|
||||||
const res = await request(server)
|
const res = await request(server)
|
||||||
.head(`/stream/track/${trackId}`)
|
.head(
|
||||||
|
bonobUrl
|
||||||
|
.append({ pathname: `/stream/track/${trackId}` })
|
||||||
|
.path()
|
||||||
|
)
|
||||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
||||||
|
|
||||||
expect(res.status).toEqual(trackStream.status);
|
expect(res.status).toEqual(trackStream.status);
|
||||||
expect(res.headers["content-type"]).toEqual(
|
expect(res.headers["content-type"]).toEqual(
|
||||||
"audio/mp3; charset=utf-8"
|
"audio/mp3; charset=utf-8"
|
||||||
);
|
);
|
||||||
expect(res.headers["content-length"]).toEqual(
|
expect(res.headers["content-length"]).toEqual("123");
|
||||||
"123"
|
|
||||||
);
|
|
||||||
expect(res.body).toEqual({});
|
expect(res.body).toEqual({});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -437,7 +478,9 @@ describe("server", () => {
|
|||||||
describe("GET requests", () => {
|
describe("GET requests", () => {
|
||||||
describe("when there is no access-token", () => {
|
describe("when there is no access-token", () => {
|
||||||
it("should return a 401", async () => {
|
it("should return a 401", async () => {
|
||||||
const res = await request(server).get(`/stream/track/${trackId}`);
|
const res = await request(server).get(
|
||||||
|
bonobUrl.append({ pathname: `/stream/track/${trackId}` }).path()
|
||||||
|
);
|
||||||
|
|
||||||
expect(res.status).toEqual(401);
|
expect(res.status).toEqual(401);
|
||||||
});
|
});
|
||||||
@@ -448,7 +491,11 @@ describe("server", () => {
|
|||||||
now = now.add(1, "day");
|
now = now.add(1, "day");
|
||||||
|
|
||||||
const res = await request(server)
|
const res = await request(server)
|
||||||
.get(`/stream/track/${trackId}`)
|
.get(
|
||||||
|
bonobUrl
|
||||||
|
.append({ pathname: `/stream/track/${trackId}` })
|
||||||
|
.path()
|
||||||
|
)
|
||||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
||||||
|
|
||||||
expect(res.status).toEqual(401);
|
expect(res.status).toEqual(401);
|
||||||
@@ -459,8 +506,7 @@ describe("server", () => {
|
|||||||
it("should return a 404", async () => {
|
it("should return a 404", async () => {
|
||||||
const stream = {
|
const stream = {
|
||||||
status: 404,
|
status: 404,
|
||||||
headers: {
|
headers: {},
|
||||||
},
|
|
||||||
stream: streamContent(""),
|
stream: streamContent(""),
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -468,7 +514,11 @@ describe("server", () => {
|
|||||||
musicLibrary.stream.mockResolvedValue(stream);
|
musicLibrary.stream.mockResolvedValue(stream);
|
||||||
|
|
||||||
const res = await request(server)
|
const res = await request(server)
|
||||||
.get(`/stream/track/${trackId}`)
|
.get(
|
||||||
|
bonobUrl
|
||||||
|
.append({ pathname: `/stream/track/${trackId}` })
|
||||||
|
.path()
|
||||||
|
)
|
||||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
||||||
|
|
||||||
expect(res.status).toEqual(404);
|
expect(res.status).toEqual(404);
|
||||||
@@ -496,7 +546,11 @@ describe("server", () => {
|
|||||||
musicLibrary.nowPlaying.mockResolvedValue(true);
|
musicLibrary.nowPlaying.mockResolvedValue(true);
|
||||||
|
|
||||||
const res = await request(server)
|
const res = await request(server)
|
||||||
.get(`/stream/track/${trackId}`)
|
.get(
|
||||||
|
bonobUrl
|
||||||
|
.append({ pathname: `/stream/track/${trackId}` })
|
||||||
|
.path()
|
||||||
|
)
|
||||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
||||||
|
|
||||||
expect(res.status).toEqual(stream.status);
|
expect(res.status).toEqual(stream.status);
|
||||||
@@ -525,7 +579,7 @@ describe("server", () => {
|
|||||||
"accept-ranges": undefined,
|
"accept-ranges": undefined,
|
||||||
"content-range": undefined,
|
"content-range": undefined,
|
||||||
},
|
},
|
||||||
stream: streamContent("")
|
stream: streamContent(""),
|
||||||
};
|
};
|
||||||
|
|
||||||
musicService.login.mockResolvedValue(musicLibrary);
|
musicService.login.mockResolvedValue(musicLibrary);
|
||||||
@@ -533,7 +587,11 @@ describe("server", () => {
|
|||||||
musicLibrary.nowPlaying.mockResolvedValue(true);
|
musicLibrary.nowPlaying.mockResolvedValue(true);
|
||||||
|
|
||||||
const res = await request(server)
|
const res = await request(server)
|
||||||
.get(`/stream/track/${trackId}`)
|
.get(
|
||||||
|
bonobUrl
|
||||||
|
.append({ pathname: `/stream/track/${trackId}` })
|
||||||
|
.path()
|
||||||
|
)
|
||||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
||||||
|
|
||||||
expect(res.status).toEqual(stream.status);
|
expect(res.status).toEqual(stream.status);
|
||||||
@@ -560,7 +618,7 @@ describe("server", () => {
|
|||||||
"content-length": "222",
|
"content-length": "222",
|
||||||
"accept-ranges": "bytes",
|
"accept-ranges": "bytes",
|
||||||
},
|
},
|
||||||
stream: streamContent("")
|
stream: streamContent(""),
|
||||||
};
|
};
|
||||||
|
|
||||||
musicService.login.mockResolvedValue(musicLibrary);
|
musicService.login.mockResolvedValue(musicLibrary);
|
||||||
@@ -568,7 +626,11 @@ describe("server", () => {
|
|||||||
musicLibrary.nowPlaying.mockResolvedValue(true);
|
musicLibrary.nowPlaying.mockResolvedValue(true);
|
||||||
|
|
||||||
const res = await request(server)
|
const res = await request(server)
|
||||||
.get(`/stream/track/${trackId}`)
|
.get(
|
||||||
|
bonobUrl
|
||||||
|
.append({ pathname: `/stream/track/${trackId}` })
|
||||||
|
.path()
|
||||||
|
)
|
||||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
||||||
|
|
||||||
expect(res.status).toEqual(stream.status);
|
expect(res.status).toEqual(stream.status);
|
||||||
@@ -596,7 +658,7 @@ describe("server", () => {
|
|||||||
"accept-ranges": "bytez",
|
"accept-ranges": "bytez",
|
||||||
"content-range": "100-200",
|
"content-range": "100-200",
|
||||||
},
|
},
|
||||||
stream: streamContent("")
|
stream: streamContent(""),
|
||||||
};
|
};
|
||||||
|
|
||||||
musicService.login.mockResolvedValue(musicLibrary);
|
musicService.login.mockResolvedValue(musicLibrary);
|
||||||
@@ -604,7 +666,11 @@ describe("server", () => {
|
|||||||
musicLibrary.nowPlaying.mockResolvedValue(true);
|
musicLibrary.nowPlaying.mockResolvedValue(true);
|
||||||
|
|
||||||
const res = await request(server)
|
const res = await request(server)
|
||||||
.get(`/stream/track/${trackId}`)
|
.get(
|
||||||
|
bonobUrl
|
||||||
|
.append({ pathname: `/stream/track/${trackId}` })
|
||||||
|
.path()
|
||||||
|
)
|
||||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
||||||
|
|
||||||
expect(res.status).toEqual(stream.status);
|
expect(res.status).toEqual(stream.status);
|
||||||
@@ -635,7 +701,7 @@ describe("server", () => {
|
|||||||
"content-length": "222",
|
"content-length": "222",
|
||||||
"accept-ranges": "none",
|
"accept-ranges": "none",
|
||||||
},
|
},
|
||||||
stream: streamContent("")
|
stream: streamContent(""),
|
||||||
};
|
};
|
||||||
|
|
||||||
musicService.login.mockResolvedValue(musicLibrary);
|
musicService.login.mockResolvedValue(musicLibrary);
|
||||||
@@ -645,7 +711,11 @@ describe("server", () => {
|
|||||||
const requestedRange = "40-";
|
const requestedRange = "40-";
|
||||||
|
|
||||||
const res = await request(server)
|
const res = await request(server)
|
||||||
.get(`/stream/track/${trackId}`)
|
.get(
|
||||||
|
bonobUrl
|
||||||
|
.append({ pathname: `/stream/track/${trackId}` })
|
||||||
|
.path()
|
||||||
|
)
|
||||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken)
|
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken)
|
||||||
.set("Range", requestedRange);
|
.set("Range", requestedRange);
|
||||||
|
|
||||||
@@ -677,7 +747,7 @@ describe("server", () => {
|
|||||||
"accept-ranges": "bytez",
|
"accept-ranges": "bytez",
|
||||||
"content-range": "100-200",
|
"content-range": "100-200",
|
||||||
},
|
},
|
||||||
stream: streamContent("")
|
stream: streamContent(""),
|
||||||
};
|
};
|
||||||
|
|
||||||
musicService.login.mockResolvedValue(musicLibrary);
|
musicService.login.mockResolvedValue(musicLibrary);
|
||||||
@@ -685,7 +755,11 @@ describe("server", () => {
|
|||||||
musicLibrary.nowPlaying.mockResolvedValue(true);
|
musicLibrary.nowPlaying.mockResolvedValue(true);
|
||||||
|
|
||||||
const res = await request(server)
|
const res = await request(server)
|
||||||
.get(`/stream/track/${trackId}`)
|
.get(
|
||||||
|
bonobUrl
|
||||||
|
.append({ pathname: `/stream/track/${trackId}` })
|
||||||
|
.path()
|
||||||
|
)
|
||||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken)
|
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken)
|
||||||
.set("Range", "4000-5000");
|
.set("Range", "4000-5000");
|
||||||
|
|
||||||
@@ -725,7 +799,7 @@ describe("server", () => {
|
|||||||
const server = makeServer(
|
const server = makeServer(
|
||||||
jest.fn() as unknown as Sonos,
|
jest.fn() as unknown as Sonos,
|
||||||
aService(),
|
aService(),
|
||||||
"http://localhost:1234",
|
url("http://localhost:1234"),
|
||||||
musicService as unknown as MusicService,
|
musicService as unknown as MusicService,
|
||||||
new InMemoryLinkCodes(),
|
new InMemoryLinkCodes(),
|
||||||
accessTokens
|
accessTokens
|
||||||
@@ -792,7 +866,9 @@ describe("server", () => {
|
|||||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
||||||
|
|
||||||
expect(res.status).toEqual(coverArt.status);
|
expect(res.status).toEqual(coverArt.status);
|
||||||
expect(res.header["content-type"]).toEqual(coverArt.contentType);
|
expect(res.header["content-type"]).toEqual(
|
||||||
|
coverArt.contentType
|
||||||
|
);
|
||||||
|
|
||||||
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
||||||
expect(musicLibrary.coverArt).toHaveBeenCalledWith(
|
expect(musicLibrary.coverArt).toHaveBeenCalledWith(
|
||||||
@@ -855,7 +931,9 @@ describe("server", () => {
|
|||||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
||||||
|
|
||||||
expect(res.status).toEqual(coverArt.status);
|
expect(res.status).toEqual(coverArt.status);
|
||||||
expect(res.header["content-type"]).toEqual(coverArt.contentType);
|
expect(res.header["content-type"]).toEqual(
|
||||||
|
coverArt.contentType
|
||||||
|
);
|
||||||
|
|
||||||
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
||||||
expect(musicLibrary.coverArt).toHaveBeenCalledWith(
|
expect(musicLibrary.coverArt).toHaveBeenCalledWith(
|
||||||
@@ -898,4 +976,6 @@ describe("server", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import * as xpath from "xpath-ts";
|
|||||||
import { randomInt } from "crypto";
|
import { randomInt } from "crypto";
|
||||||
|
|
||||||
import { LinkCodes } from "../src/link_codes";
|
import { LinkCodes } from "../src/link_codes";
|
||||||
import makeServer, { BONOB_ACCESS_TOKEN_HEADER } from "../src/server";
|
import makeServer from "../src/server";
|
||||||
import { bonobService, SONOS_DISABLED } from "../src/sonos";
|
import { bonobService, SONOS_DISABLED } from "../src/sonos";
|
||||||
import {
|
import {
|
||||||
STRINGS_ROUTE,
|
STRINGS_ROUTE,
|
||||||
@@ -46,20 +46,30 @@ import {
|
|||||||
} from "../src/music_service";
|
} from "../src/music_service";
|
||||||
import { AccessTokens } from "../src/access_tokens";
|
import { AccessTokens } from "../src/access_tokens";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
|
import url from "../src/url_builder";
|
||||||
|
|
||||||
const parseXML = (value: string) => new DOMParserImpl().parseFromString(value);
|
const parseXML = (value: string) => new DOMParserImpl().parseFromString(value);
|
||||||
|
|
||||||
describe("service config", () => {
|
describe("service config", () => {
|
||||||
|
const bonobWithNoContextPath = url("http://localhost:1234");
|
||||||
|
const bonobWithContextPath = url("http://localhost:5678/some-context-path");
|
||||||
|
|
||||||
|
[bonobWithNoContextPath, bonobWithContextPath].forEach((bonobUrl) => {
|
||||||
const server = makeServer(
|
const server = makeServer(
|
||||||
SONOS_DISABLED,
|
SONOS_DISABLED,
|
||||||
aService({ name: "music land" }),
|
aService({ name: "music land" }),
|
||||||
"http://localhost:1234",
|
bonobUrl,
|
||||||
new InMemoryMusicService()
|
new InMemoryMusicService()
|
||||||
);
|
);
|
||||||
|
|
||||||
describe(STRINGS_ROUTE, () => {
|
const stringsUrl = bonobUrl.append({ pathname: STRINGS_ROUTE });
|
||||||
|
const presentationUrl = bonobUrl.append({
|
||||||
|
pathname: PRESENTATION_MAP_ROUTE,
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(`${stringsUrl}`, () => {
|
||||||
it("should return xml for the strings", async () => {
|
it("should return xml for the strings", async () => {
|
||||||
const res = await request(server).get(STRINGS_ROUTE).send();
|
const res = await request(server).get(stringsUrl.path()).send();
|
||||||
|
|
||||||
expect(res.status).toEqual(200);
|
expect(res.status).toEqual(200);
|
||||||
|
|
||||||
@@ -83,9 +93,9 @@ describe("service config", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe(PRESENTATION_MAP_ROUTE, () => {
|
describe(`${presentationUrl}`, () => {
|
||||||
it("should have an ArtWorkSizeMap for all sizes recommended by sonos", async () => {
|
it("should have an ArtWorkSizeMap for all sizes recommended by sonos", async () => {
|
||||||
const res = await request(server).get(PRESENTATION_MAP_ROUTE).send();
|
const res = await request(server).get(presentationUrl.path()).send();
|
||||||
|
|
||||||
expect(res.status).toEqual(200);
|
expect(res.status).toEqual(200);
|
||||||
|
|
||||||
@@ -105,6 +115,7 @@ describe("service config", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getMetadataResult", () => {
|
describe("getMetadataResult", () => {
|
||||||
@@ -191,8 +202,7 @@ describe("getMetadataResult", () => {
|
|||||||
|
|
||||||
describe("track", () => {
|
describe("track", () => {
|
||||||
it("should map into a sonos expected track", () => {
|
it("should map into a sonos expected track", () => {
|
||||||
const webAddress = "http://localhost:4567";
|
const bonobUrl = url("http://localhost:4567/foo?access-token=1234");
|
||||||
const accessToken = uuid();
|
|
||||||
const someTrack = aTrack({
|
const someTrack = aTrack({
|
||||||
id: uuid(),
|
id: uuid(),
|
||||||
mimeType: "audio/something",
|
mimeType: "audio/something",
|
||||||
@@ -207,7 +217,7 @@ describe("track", () => {
|
|||||||
artist: anArtist({ name: "great artist", id: uuid() }),
|
artist: anArtist({ name: "great artist", id: uuid() }),
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(track(webAddress, accessToken, someTrack)).toEqual({
|
expect(track(bonobUrl, someTrack)).toEqual({
|
||||||
itemType: "track",
|
itemType: "track",
|
||||||
id: `track:${someTrack.id}`,
|
id: `track:${someTrack.id}`,
|
||||||
mimeType: someTrack.mimeType,
|
mimeType: someTrack.mimeType,
|
||||||
@@ -218,7 +228,7 @@ describe("track", () => {
|
|||||||
albumId: someTrack.album.id,
|
albumId: someTrack.album.id,
|
||||||
albumArtist: someTrack.artist.name,
|
albumArtist: someTrack.artist.name,
|
||||||
albumArtistId: someTrack.artist.id,
|
albumArtistId: someTrack.artist.id,
|
||||||
albumArtURI: `${webAddress}/album/${someTrack.album.id}/art/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`,
|
albumArtURI: `http://localhost:4567/foo/album/${someTrack.album.id}/art/size/180?access-token=1234`,
|
||||||
artist: someTrack.artist.name,
|
artist: someTrack.artist.name,
|
||||||
artistId: someTrack.artist.id,
|
artistId: someTrack.artist.id,
|
||||||
duration: someTrack.duration,
|
duration: someTrack.duration,
|
||||||
@@ -232,15 +242,14 @@ describe("track", () => {
|
|||||||
|
|
||||||
describe("album", () => {
|
describe("album", () => {
|
||||||
it("should map to a sonos album", () => {
|
it("should map to a sonos album", () => {
|
||||||
const webAddress = "http://localhost:9988";
|
const bonobUrl = url("http://localhost:9988/some-context-path?s=hello");
|
||||||
const accessToken = uuid();
|
|
||||||
const someAlbum = anAlbum({ id: "id123", name: "What a great album" });
|
const someAlbum = anAlbum({ id: "id123", name: "What a great album" });
|
||||||
|
|
||||||
expect(album(webAddress, accessToken, someAlbum)).toEqual({
|
expect(album(bonobUrl, someAlbum)).toEqual({
|
||||||
itemType: "album",
|
itemType: "album",
|
||||||
id: `album:${someAlbum.id}`,
|
id: `album:${someAlbum.id}`,
|
||||||
title: someAlbum.name,
|
title: someAlbum.name,
|
||||||
albumArtURI: defaultAlbumArtURI(webAddress, accessToken, someAlbum),
|
albumArtURI: defaultAlbumArtURI(bonobUrl, someAlbum).href(),
|
||||||
canPlay: true,
|
canPlay: true,
|
||||||
artist: someAlbum.artistName,
|
artist: someAlbum.artistName,
|
||||||
artistId: someAlbum.artistId,
|
artistId: someAlbum.artistId,
|
||||||
@@ -250,31 +259,27 @@ describe("album", () => {
|
|||||||
|
|
||||||
describe("defaultAlbumArtURI", () => {
|
describe("defaultAlbumArtURI", () => {
|
||||||
it("should create the correct URI", () => {
|
it("should create the correct URI", () => {
|
||||||
const webAddress = "http://localhost:1234";
|
const bonobUrl = url("http://localhost:1234/context-path?search=yes");
|
||||||
const accessToken = uuid();
|
|
||||||
const album = anAlbum();
|
const album = anAlbum();
|
||||||
|
|
||||||
expect(defaultAlbumArtURI(webAddress, accessToken, album)).toEqual(
|
expect(defaultAlbumArtURI(bonobUrl, album).href()).toEqual(
|
||||||
`${webAddress}/album/${album.id}/art/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
|
`http://localhost:1234/context-path/album/${album.id}/art/size/180?search=yes`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("defaultArtistArtURI", () => {
|
describe("defaultArtistArtURI", () => {
|
||||||
it("should create the correct URI", () => {
|
it("should create the correct URI", () => {
|
||||||
const webAddress = "http://localhost:1234";
|
const bonobUrl = url("http://localhost:1234/something?s=123");
|
||||||
const accessToken = uuid();
|
|
||||||
const artist = anArtist();
|
const artist = anArtist();
|
||||||
|
|
||||||
expect(defaultArtistArtURI(webAddress, accessToken, artist)).toEqual(
|
expect(defaultArtistArtURI(bonobUrl, artist).href()).toEqual(
|
||||||
`${webAddress}/artist/${artist.id}/art/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
|
`http://localhost:1234/something/artist/${artist.id}/art/size/180?s=123`
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("api", () => {
|
describe("api", () => {
|
||||||
const rootUrl = "http://localhost:1234";
|
|
||||||
const service = bonobService("test-api", 133, rootUrl, "AppLink");
|
|
||||||
const musicService = {
|
const musicService = {
|
||||||
generateToken: jest.fn(),
|
generateToken: jest.fn(),
|
||||||
login: jest.fn(),
|
login: jest.fn(),
|
||||||
@@ -312,10 +317,23 @@ describe("api", () => {
|
|||||||
now: jest.fn(),
|
now: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const bonobUrlWithoutContextPath = url("http://localhost:222");
|
||||||
|
const bonobUrlWithContextPath = url("http://localhost:111/path/to/bonob");
|
||||||
|
|
||||||
|
[bonobUrlWithoutContextPath, bonobUrlWithContextPath].forEach((bonobUrl) => {
|
||||||
|
describe(`bonob with url ${bonobUrl}`, () => {
|
||||||
|
const authToken = `authToken-${uuid()}`;
|
||||||
|
const accessToken = `accessToken-${uuid()}`;
|
||||||
|
|
||||||
|
const bonobUrlWithAccessToken = bonobUrl.append({
|
||||||
|
searchParams: { "bonob-access-token": accessToken },
|
||||||
|
});
|
||||||
|
|
||||||
|
const service = bonobService("test-api", 133, bonobUrl, "AppLink");
|
||||||
const server = makeServer(
|
const server = makeServer(
|
||||||
SONOS_DISABLED,
|
SONOS_DISABLED,
|
||||||
service,
|
service,
|
||||||
rootUrl,
|
bonobUrl,
|
||||||
musicService as unknown as MusicService,
|
musicService as unknown as MusicService,
|
||||||
linkCodes as unknown as LinkCodes,
|
linkCodes as unknown as LinkCodes,
|
||||||
accessTokens as unknown as AccessTokens,
|
accessTokens as unknown as AccessTokens,
|
||||||
@@ -328,7 +346,7 @@ describe("api", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("pages", () => {
|
describe("pages", () => {
|
||||||
describe(LOGIN_ROUTE, () => {
|
describe(bonobUrl.append({ pathname: LOGIN_ROUTE }).href(), () => {
|
||||||
describe("when the credentials are valid", () => {
|
describe("when the credentials are valid", () => {
|
||||||
it("should return 200 ok and have associated linkCode with user", async () => {
|
it("should return 200 ok and have associated linkCode with user", async () => {
|
||||||
const username = "jane";
|
const username = "jane";
|
||||||
@@ -345,7 +363,7 @@ describe("api", () => {
|
|||||||
linkCodes.associate.mockReturnValue(true);
|
linkCodes.associate.mockReturnValue(true);
|
||||||
|
|
||||||
const res = await request(server)
|
const res = await request(server)
|
||||||
.post(LOGIN_ROUTE)
|
.post(bonobUrl.append({ pathname: LOGIN_ROUTE }).pathname())
|
||||||
.type("form")
|
.type("form")
|
||||||
.send({ username, password, linkCode })
|
.send({ username, password, linkCode })
|
||||||
.expect(200);
|
.expect(200);
|
||||||
@@ -357,7 +375,10 @@ describe("api", () => {
|
|||||||
password,
|
password,
|
||||||
});
|
});
|
||||||
expect(linkCodes.has).toHaveBeenCalledWith(linkCode);
|
expect(linkCodes.has).toHaveBeenCalledWith(linkCode);
|
||||||
expect(linkCodes.associate).toHaveBeenCalledWith(linkCode, authToken);
|
expect(linkCodes.associate).toHaveBeenCalledWith(
|
||||||
|
linkCode,
|
||||||
|
authToken
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -372,7 +393,7 @@ describe("api", () => {
|
|||||||
musicService.generateToken.mockResolvedValue({ message });
|
musicService.generateToken.mockResolvedValue({ message });
|
||||||
|
|
||||||
const res = await request(server)
|
const res = await request(server)
|
||||||
.post(LOGIN_ROUTE)
|
.post(bonobUrl.append({ pathname: LOGIN_ROUTE }).pathname())
|
||||||
.type("form")
|
.type("form")
|
||||||
.send({ username, password, linkCode })
|
.send({ username, password, linkCode })
|
||||||
.expect(403);
|
.expect(403);
|
||||||
@@ -390,7 +411,7 @@ describe("api", () => {
|
|||||||
linkCodes.has.mockReturnValue(false);
|
linkCodes.has.mockReturnValue(false);
|
||||||
|
|
||||||
const res = await request(server)
|
const res = await request(server)
|
||||||
.post(LOGIN_ROUTE)
|
.post(bonobUrl.append({ pathname: LOGIN_ROUTE }).pathname())
|
||||||
.type("form")
|
.type("form")
|
||||||
.send({ username, password, linkCode })
|
.send({ username, password, linkCode })
|
||||||
.expect(400);
|
.expect(400);
|
||||||
@@ -406,7 +427,7 @@ describe("api", () => {
|
|||||||
it("should do something", async () => {
|
it("should do something", async () => {
|
||||||
const ws = await createClientAsync(`${service.uri}?wsdl`, {
|
const ws = await createClientAsync(`${service.uri}?wsdl`, {
|
||||||
endpoint: service.uri,
|
endpoint: service.uri,
|
||||||
httpClient: supersoap(server, rootUrl),
|
httpClient: supersoap(server),
|
||||||
});
|
});
|
||||||
|
|
||||||
const linkCode = "theLinkCode8899";
|
const linkCode = "theLinkCode8899";
|
||||||
@@ -420,7 +441,12 @@ describe("api", () => {
|
|||||||
authorizeAccount: {
|
authorizeAccount: {
|
||||||
appUrlStringId: "AppLinkMessage",
|
appUrlStringId: "AppLinkMessage",
|
||||||
deviceLink: {
|
deviceLink: {
|
||||||
regUrl: `${rootUrl}/login?linkCode=${linkCode}`,
|
regUrl: bonobUrl
|
||||||
|
.append({
|
||||||
|
pathname: "/login",
|
||||||
|
searchParams: { linkCode },
|
||||||
|
})
|
||||||
|
.href(),
|
||||||
linkCode: linkCode,
|
linkCode: linkCode,
|
||||||
showLinkCode: false,
|
showLinkCode: false,
|
||||||
},
|
},
|
||||||
@@ -443,7 +469,7 @@ describe("api", () => {
|
|||||||
|
|
||||||
const ws = await createClientAsync(`${service.uri}?wsdl`, {
|
const ws = await createClientAsync(`${service.uri}?wsdl`, {
|
||||||
endpoint: service.uri,
|
endpoint: service.uri,
|
||||||
httpClient: supersoap(server, rootUrl),
|
httpClient: supersoap(server),
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await ws.getDeviceAuthTokenAsync({ linkCode });
|
const result = await ws.getDeviceAuthTokenAsync({ linkCode });
|
||||||
@@ -472,7 +498,7 @@ describe("api", () => {
|
|||||||
|
|
||||||
const ws = await createClientAsync(`${service.uri}?wsdl`, {
|
const ws = await createClientAsync(`${service.uri}?wsdl`, {
|
||||||
endpoint: service.uri,
|
endpoint: service.uri,
|
||||||
httpClient: supersoap(server, rootUrl),
|
httpClient: supersoap(server),
|
||||||
});
|
});
|
||||||
|
|
||||||
await ws
|
await ws
|
||||||
@@ -484,7 +510,10 @@ describe("api", () => {
|
|||||||
expect(e.root.Envelope.Body.Fault).toEqual({
|
expect(e.root.Envelope.Body.Fault).toEqual({
|
||||||
faultcode: "Client.NOT_LINKED_RETRY",
|
faultcode: "Client.NOT_LINKED_RETRY",
|
||||||
faultstring: "Link Code not found retry...",
|
faultstring: "Link Code not found retry...",
|
||||||
detail: { ExceptionInfo: "NOT_LINKED_RETRY", SonosError: "5" },
|
detail: {
|
||||||
|
ExceptionInfo: "NOT_LINKED_RETRY",
|
||||||
|
SonosError: "5",
|
||||||
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -498,7 +527,7 @@ describe("api", () => {
|
|||||||
|
|
||||||
const ws = await createClientAsync(`${service.uri}?wsdl`, {
|
const ws = await createClientAsync(`${service.uri}?wsdl`, {
|
||||||
endpoint: service.uri,
|
endpoint: service.uri,
|
||||||
httpClient: supersoap(server, rootUrl),
|
httpClient: supersoap(server),
|
||||||
});
|
});
|
||||||
|
|
||||||
const result = await ws.getLastUpdateAsync({});
|
const result = await ws.getLastUpdateAsync({});
|
||||||
@@ -519,7 +548,7 @@ describe("api", () => {
|
|||||||
it("should return a fault of LoginUnsupported", async () => {
|
it("should return a fault of LoginUnsupported", async () => {
|
||||||
const ws = await createClientAsync(`${service.uri}?wsdl`, {
|
const ws = await createClientAsync(`${service.uri}?wsdl`, {
|
||||||
endpoint: service.uri,
|
endpoint: service.uri,
|
||||||
httpClient: supersoap(server, rootUrl),
|
httpClient: supersoap(server),
|
||||||
});
|
});
|
||||||
|
|
||||||
await ws
|
await ws
|
||||||
@@ -540,10 +569,12 @@ describe("api", () => {
|
|||||||
|
|
||||||
const ws = await createClientAsync(`${service.uri}?wsdl`, {
|
const ws = await createClientAsync(`${service.uri}?wsdl`, {
|
||||||
endpoint: service.uri,
|
endpoint: service.uri,
|
||||||
httpClient: supersoap(server, rootUrl),
|
httpClient: supersoap(server),
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.addSoapHeader({ credentials: someCredentials("someAuthToken") });
|
ws.addSoapHeader({
|
||||||
|
credentials: someCredentials("someAuthToken"),
|
||||||
|
});
|
||||||
await ws
|
await ws
|
||||||
.getMetadataAsync({ id: "search", index: 0, count: 0 })
|
.getMetadataAsync({ id: "search", index: 0, count: 0 })
|
||||||
.then(() => fail("shouldnt get here"))
|
.then(() => fail("shouldnt get here"))
|
||||||
@@ -557,8 +588,6 @@ describe("api", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("when valid credentials are provided", () => {
|
describe("when valid credentials are provided", () => {
|
||||||
const authToken = `authToken-${uuid()}`;
|
|
||||||
const accessToken = `accessToken-${uuid()}`;
|
|
||||||
let ws: Client;
|
let ws: Client;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@@ -567,7 +596,7 @@ describe("api", () => {
|
|||||||
|
|
||||||
ws = await createClientAsync(`${service.uri}?wsdl`, {
|
ws = await createClientAsync(`${service.uri}?wsdl`, {
|
||||||
endpoint: service.uri,
|
endpoint: service.uri,
|
||||||
httpClient: supersoap(server, rootUrl),
|
httpClient: supersoap(server),
|
||||||
});
|
});
|
||||||
ws.addSoapHeader({ credentials: someCredentials(authToken) });
|
ws.addSoapHeader({ credentials: someCredentials(authToken) });
|
||||||
});
|
});
|
||||||
@@ -594,7 +623,7 @@ describe("api", () => {
|
|||||||
expect(result[0]).toEqual(
|
expect(result[0]).toEqual(
|
||||||
searchResult({
|
searchResult({
|
||||||
mediaCollection: albums.map((it) =>
|
mediaCollection: albums.map((it) =>
|
||||||
album(rootUrl, accessToken, albumToAlbumSummary(it))
|
album(bonobUrlWithAccessToken, albumToAlbumSummary(it))
|
||||||
),
|
),
|
||||||
index: 0,
|
index: 0,
|
||||||
total: 2,
|
total: 2,
|
||||||
@@ -626,7 +655,7 @@ describe("api", () => {
|
|||||||
expect(result[0]).toEqual(
|
expect(result[0]).toEqual(
|
||||||
searchResult({
|
searchResult({
|
||||||
mediaCollection: artists.map((it) =>
|
mediaCollection: artists.map((it) =>
|
||||||
artist(rootUrl, accessToken, artistToArtistSummary(it))
|
artist(bonobUrlWithAccessToken, artistToArtistSummary(it))
|
||||||
),
|
),
|
||||||
index: 0,
|
index: 0,
|
||||||
total: 2,
|
total: 2,
|
||||||
@@ -655,7 +684,7 @@ describe("api", () => {
|
|||||||
expect(result[0]).toEqual(
|
expect(result[0]).toEqual(
|
||||||
searchResult({
|
searchResult({
|
||||||
mediaCollection: tracks.map((it) =>
|
mediaCollection: tracks.map((it) =>
|
||||||
album(rootUrl, accessToken, it.album)
|
album(bonobUrlWithAccessToken, it.album)
|
||||||
),
|
),
|
||||||
index: 0,
|
index: 0,
|
||||||
total: 2,
|
total: 2,
|
||||||
@@ -672,7 +701,7 @@ describe("api", () => {
|
|||||||
it("should return a fault of LoginUnsupported", async () => {
|
it("should return a fault of LoginUnsupported", async () => {
|
||||||
const ws = await createClientAsync(`${service.uri}?wsdl`, {
|
const ws = await createClientAsync(`${service.uri}?wsdl`, {
|
||||||
endpoint: service.uri,
|
endpoint: service.uri,
|
||||||
httpClient: supersoap(server, rootUrl),
|
httpClient: supersoap(server),
|
||||||
});
|
});
|
||||||
|
|
||||||
await ws
|
await ws
|
||||||
@@ -693,10 +722,12 @@ describe("api", () => {
|
|||||||
|
|
||||||
const ws = await createClientAsync(`${service.uri}?wsdl`, {
|
const ws = await createClientAsync(`${service.uri}?wsdl`, {
|
||||||
endpoint: service.uri,
|
endpoint: service.uri,
|
||||||
httpClient: supersoap(server, rootUrl),
|
httpClient: supersoap(server),
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.addSoapHeader({ credentials: someCredentials("someAuthToken") });
|
ws.addSoapHeader({
|
||||||
|
credentials: someCredentials("someAuthToken"),
|
||||||
|
});
|
||||||
await ws
|
await ws
|
||||||
.getMetadataAsync({ id: "root", index: 0, count: 0 })
|
.getMetadataAsync({ id: "root", index: 0, count: 0 })
|
||||||
.then(() => fail("shouldnt get here"))
|
.then(() => fail("shouldnt get here"))
|
||||||
@@ -710,8 +741,6 @@ describe("api", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("when valid credentials are provided", () => {
|
describe("when valid credentials are provided", () => {
|
||||||
const authToken = `authToken-${uuid()}`;
|
|
||||||
const accessToken = `accessToken-${uuid()}`;
|
|
||||||
let ws: Client;
|
let ws: Client;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@@ -720,7 +749,7 @@ describe("api", () => {
|
|||||||
|
|
||||||
ws = await createClientAsync(`${service.uri}?wsdl`, {
|
ws = await createClientAsync(`${service.uri}?wsdl`, {
|
||||||
endpoint: service.uri,
|
endpoint: service.uri,
|
||||||
httpClient: supersoap(server, rootUrl),
|
httpClient: supersoap(server),
|
||||||
});
|
});
|
||||||
ws.addSoapHeader({ credentials: someCredentials(authToken) });
|
ws.addSoapHeader({ credentials: someCredentials(authToken) });
|
||||||
});
|
});
|
||||||
@@ -735,7 +764,11 @@ describe("api", () => {
|
|||||||
expect(root[0]).toEqual(
|
expect(root[0]).toEqual(
|
||||||
getMetadataResult({
|
getMetadataResult({
|
||||||
mediaCollection: [
|
mediaCollection: [
|
||||||
{ itemType: "container", id: "artists", title: "Artists" },
|
{
|
||||||
|
itemType: "container",
|
||||||
|
id: "artists",
|
||||||
|
title: "Artists",
|
||||||
|
},
|
||||||
{ itemType: "albumList", id: "albums", title: "Albums" },
|
{ itemType: "albumList", id: "albums", title: "Albums" },
|
||||||
{
|
{
|
||||||
itemType: "playlist",
|
itemType: "playlist",
|
||||||
@@ -941,15 +974,20 @@ describe("api", () => {
|
|||||||
|
|
||||||
expect(result[0]).toEqual(
|
expect(result[0]).toEqual(
|
||||||
getMetadataResult({
|
getMetadataResult({
|
||||||
mediaCollection: artistWithManyAlbums.albums.map((it) => ({
|
mediaCollection: artistWithManyAlbums.albums.map(
|
||||||
|
(it) => ({
|
||||||
itemType: "album",
|
itemType: "album",
|
||||||
id: `album:${it.id}`,
|
id: `album:${it.id}`,
|
||||||
title: it.name,
|
title: it.name,
|
||||||
albumArtURI: defaultAlbumArtURI(rootUrl, accessToken, it),
|
albumArtURI: defaultAlbumArtURI(
|
||||||
|
bonobUrlWithAccessToken,
|
||||||
|
it
|
||||||
|
).href(),
|
||||||
canPlay: true,
|
canPlay: true,
|
||||||
artistId: it.artistId,
|
artistId: it.artistId,
|
||||||
artist: it.artistName,
|
artist: it.artistName,
|
||||||
})),
|
})
|
||||||
|
),
|
||||||
index: 0,
|
index: 0,
|
||||||
total: artistWithManyAlbums.albums.length,
|
total: artistWithManyAlbums.albums.length,
|
||||||
})
|
})
|
||||||
@@ -978,7 +1016,10 @@ describe("api", () => {
|
|||||||
itemType: "album",
|
itemType: "album",
|
||||||
id: `album:${it.id}`,
|
id: `album:${it.id}`,
|
||||||
title: it.name,
|
title: it.name,
|
||||||
albumArtURI: defaultAlbumArtURI(rootUrl, accessToken, it),
|
albumArtURI: defaultAlbumArtURI(
|
||||||
|
bonobUrlWithAccessToken,
|
||||||
|
it
|
||||||
|
).href(),
|
||||||
canPlay: true,
|
canPlay: true,
|
||||||
artistId: it.artistId,
|
artistId: it.artistId,
|
||||||
artist: it.artistName,
|
artist: it.artistName,
|
||||||
@@ -1027,7 +1068,10 @@ describe("api", () => {
|
|||||||
id: `artist:${it.id}`,
|
id: `artist:${it.id}`,
|
||||||
artistId: it.id,
|
artistId: it.id,
|
||||||
title: it.name,
|
title: it.name,
|
||||||
albumArtURI: defaultArtistArtURI(rootUrl, accessToken, it),
|
albumArtURI: defaultArtistArtURI(
|
||||||
|
bonobUrlWithAccessToken,
|
||||||
|
it
|
||||||
|
).href(),
|
||||||
})),
|
})),
|
||||||
index: 0,
|
index: 0,
|
||||||
total: artistSummaries.length,
|
total: artistSummaries.length,
|
||||||
@@ -1069,7 +1113,10 @@ describe("api", () => {
|
|||||||
id: `artist:${it.id}`,
|
id: `artist:${it.id}`,
|
||||||
artistId: it.id,
|
artistId: it.id,
|
||||||
title: it.name,
|
title: it.name,
|
||||||
albumArtURI: defaultArtistArtURI(rootUrl, accessToken, it),
|
albumArtURI: defaultArtistArtURI(
|
||||||
|
bonobUrlWithAccessToken,
|
||||||
|
it
|
||||||
|
).href(),
|
||||||
})),
|
})),
|
||||||
index: 1,
|
index: 1,
|
||||||
total: artistSummaries.length,
|
total: artistSummaries.length,
|
||||||
@@ -1124,10 +1171,9 @@ describe("api", () => {
|
|||||||
artistId: it.id,
|
artistId: it.id,
|
||||||
title: it.name,
|
title: it.name,
|
||||||
albumArtURI: defaultArtistArtURI(
|
albumArtURI: defaultArtistArtURI(
|
||||||
rootUrl,
|
bonobUrlWithAccessToken,
|
||||||
accessToken,
|
|
||||||
it
|
it
|
||||||
),
|
).href(),
|
||||||
})),
|
})),
|
||||||
index: 0,
|
index: 0,
|
||||||
total: 4,
|
total: 4,
|
||||||
@@ -1154,10 +1200,9 @@ describe("api", () => {
|
|||||||
artistId: it.id,
|
artistId: it.id,
|
||||||
title: it.name,
|
title: it.name,
|
||||||
albumArtURI: defaultArtistArtURI(
|
albumArtURI: defaultArtistArtURI(
|
||||||
rootUrl,
|
bonobUrlWithAccessToken,
|
||||||
accessToken,
|
|
||||||
it
|
it
|
||||||
),
|
).href(),
|
||||||
})
|
})
|
||||||
),
|
),
|
||||||
index: 1,
|
index: 1,
|
||||||
@@ -1233,7 +1278,10 @@ describe("api", () => {
|
|||||||
itemType: "album",
|
itemType: "album",
|
||||||
id: `album:${it.id}`,
|
id: `album:${it.id}`,
|
||||||
title: it.name,
|
title: it.name,
|
||||||
albumArtURI: defaultAlbumArtURI(rootUrl, accessToken, it),
|
albumArtURI: defaultAlbumArtURI(
|
||||||
|
bonobUrlWithAccessToken,
|
||||||
|
it
|
||||||
|
).href(),
|
||||||
canPlay: true,
|
canPlay: true,
|
||||||
artistId: it.artistId,
|
artistId: it.artistId,
|
||||||
artist: it.artistName,
|
artist: it.artistName,
|
||||||
@@ -1278,7 +1326,10 @@ describe("api", () => {
|
|||||||
itemType: "album",
|
itemType: "album",
|
||||||
id: `album:${it.id}`,
|
id: `album:${it.id}`,
|
||||||
title: it.name,
|
title: it.name,
|
||||||
albumArtURI: defaultAlbumArtURI(rootUrl, accessToken, it),
|
albumArtURI: defaultAlbumArtURI(
|
||||||
|
bonobUrlWithAccessToken,
|
||||||
|
it
|
||||||
|
).href(),
|
||||||
canPlay: true,
|
canPlay: true,
|
||||||
artistId: it.artistId,
|
artistId: it.artistId,
|
||||||
artist: it.artistName,
|
artist: it.artistName,
|
||||||
@@ -1323,7 +1374,10 @@ describe("api", () => {
|
|||||||
itemType: "album",
|
itemType: "album",
|
||||||
id: `album:${it.id}`,
|
id: `album:${it.id}`,
|
||||||
title: it.name,
|
title: it.name,
|
||||||
albumArtURI: defaultAlbumArtURI(rootUrl, accessToken, it),
|
albumArtURI: defaultAlbumArtURI(
|
||||||
|
bonobUrlWithAccessToken,
|
||||||
|
it
|
||||||
|
).href(),
|
||||||
canPlay: true,
|
canPlay: true,
|
||||||
artistId: it.artistId,
|
artistId: it.artistId,
|
||||||
artist: it.artistName,
|
artist: it.artistName,
|
||||||
@@ -1368,7 +1422,10 @@ describe("api", () => {
|
|||||||
itemType: "album",
|
itemType: "album",
|
||||||
id: `album:${it.id}`,
|
id: `album:${it.id}`,
|
||||||
title: it.name,
|
title: it.name,
|
||||||
albumArtURI: defaultAlbumArtURI(rootUrl, accessToken, it),
|
albumArtURI: defaultAlbumArtURI(
|
||||||
|
bonobUrlWithAccessToken,
|
||||||
|
it
|
||||||
|
).href(),
|
||||||
canPlay: true,
|
canPlay: true,
|
||||||
artistId: it.artistId,
|
artistId: it.artistId,
|
||||||
artist: it.artistName,
|
artist: it.artistName,
|
||||||
@@ -1413,7 +1470,10 @@ describe("api", () => {
|
|||||||
itemType: "album",
|
itemType: "album",
|
||||||
id: `album:${it.id}`,
|
id: `album:${it.id}`,
|
||||||
title: it.name,
|
title: it.name,
|
||||||
albumArtURI: defaultAlbumArtURI(rootUrl, accessToken, it),
|
albumArtURI: defaultAlbumArtURI(
|
||||||
|
bonobUrlWithAccessToken,
|
||||||
|
it
|
||||||
|
).href(),
|
||||||
canPlay: true,
|
canPlay: true,
|
||||||
artistId: it.artistId,
|
artistId: it.artistId,
|
||||||
artist: it.artistName,
|
artist: it.artistName,
|
||||||
@@ -1456,7 +1516,10 @@ describe("api", () => {
|
|||||||
itemType: "album",
|
itemType: "album",
|
||||||
id: `album:${it.id}`,
|
id: `album:${it.id}`,
|
||||||
title: it.name,
|
title: it.name,
|
||||||
albumArtURI: defaultAlbumArtURI(rootUrl, accessToken, it),
|
albumArtURI: defaultAlbumArtURI(
|
||||||
|
bonobUrlWithAccessToken,
|
||||||
|
it
|
||||||
|
).href(),
|
||||||
canPlay: true,
|
canPlay: true,
|
||||||
artistId: it.artistId,
|
artistId: it.artistId,
|
||||||
artist: it.artistName,
|
artist: it.artistName,
|
||||||
@@ -1499,7 +1562,10 @@ describe("api", () => {
|
|||||||
itemType: "album",
|
itemType: "album",
|
||||||
id: `album:${it.id}`,
|
id: `album:${it.id}`,
|
||||||
title: it.name,
|
title: it.name,
|
||||||
albumArtURI: defaultAlbumArtURI(rootUrl, accessToken, it),
|
albumArtURI: defaultAlbumArtURI(
|
||||||
|
bonobUrlWithAccessToken,
|
||||||
|
it
|
||||||
|
).href(),
|
||||||
canPlay: true,
|
canPlay: true,
|
||||||
artistId: it.artistId,
|
artistId: it.artistId,
|
||||||
artist: it.artistName,
|
artist: it.artistName,
|
||||||
@@ -1540,7 +1606,10 @@ describe("api", () => {
|
|||||||
itemType: "album",
|
itemType: "album",
|
||||||
id: `album:${it.id}`,
|
id: `album:${it.id}`,
|
||||||
title: it.name,
|
title: it.name,
|
||||||
albumArtURI: defaultAlbumArtURI(rootUrl, accessToken, it),
|
albumArtURI: defaultAlbumArtURI(
|
||||||
|
bonobUrlWithAccessToken,
|
||||||
|
it
|
||||||
|
).href(),
|
||||||
canPlay: true,
|
canPlay: true,
|
||||||
artistId: it.artistId,
|
artistId: it.artistId,
|
||||||
artist: it.artistName,
|
artist: it.artistName,
|
||||||
@@ -1584,7 +1653,10 @@ describe("api", () => {
|
|||||||
itemType: "album",
|
itemType: "album",
|
||||||
id: `album:${it.id}`,
|
id: `album:${it.id}`,
|
||||||
title: it.name,
|
title: it.name,
|
||||||
albumArtURI: defaultAlbumArtURI(rootUrl, accessToken, it),
|
albumArtURI: defaultAlbumArtURI(
|
||||||
|
bonobUrlWithAccessToken,
|
||||||
|
it
|
||||||
|
).href(),
|
||||||
canPlay: true,
|
canPlay: true,
|
||||||
artistId: it.artistId,
|
artistId: it.artistId,
|
||||||
artist: it.artistName,
|
artist: it.artistName,
|
||||||
@@ -1637,7 +1709,7 @@ describe("api", () => {
|
|||||||
expect(result[0]).toEqual(
|
expect(result[0]).toEqual(
|
||||||
getMetadataResult({
|
getMetadataResult({
|
||||||
mediaMetadata: tracks.map((it) =>
|
mediaMetadata: tracks.map((it) =>
|
||||||
track(rootUrl, accessToken, it)
|
track(bonobUrlWithAccessToken, it)
|
||||||
),
|
),
|
||||||
index: 0,
|
index: 0,
|
||||||
total: tracks.length,
|
total: tracks.length,
|
||||||
@@ -1664,7 +1736,7 @@ describe("api", () => {
|
|||||||
expect(result[0]).toEqual(
|
expect(result[0]).toEqual(
|
||||||
getMetadataResult({
|
getMetadataResult({
|
||||||
mediaMetadata: pageOfTracks.map((it) =>
|
mediaMetadata: pageOfTracks.map((it) =>
|
||||||
track(rootUrl, accessToken, it)
|
track(bonobUrlWithAccessToken, it)
|
||||||
),
|
),
|
||||||
index: paging.index,
|
index: paging.index,
|
||||||
total: tracks.length,
|
total: tracks.length,
|
||||||
@@ -1707,13 +1779,15 @@ describe("api", () => {
|
|||||||
expect(result[0]).toEqual(
|
expect(result[0]).toEqual(
|
||||||
getMetadataResult({
|
getMetadataResult({
|
||||||
mediaMetadata: playlist.entries.map((it) =>
|
mediaMetadata: playlist.entries.map((it) =>
|
||||||
track(rootUrl, accessToken, it)
|
track(bonobUrlWithAccessToken, it)
|
||||||
),
|
),
|
||||||
index: 0,
|
index: 0,
|
||||||
total: playlist.entries.length,
|
total: playlist.entries.length,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
expect(musicLibrary.playlist).toHaveBeenCalledWith(playlist.id);
|
expect(musicLibrary.playlist).toHaveBeenCalledWith(
|
||||||
|
playlist.id
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1734,13 +1808,15 @@ describe("api", () => {
|
|||||||
expect(result[0]).toEqual(
|
expect(result[0]).toEqual(
|
||||||
getMetadataResult({
|
getMetadataResult({
|
||||||
mediaMetadata: pageOfTracks.map((it) =>
|
mediaMetadata: pageOfTracks.map((it) =>
|
||||||
track(rootUrl, accessToken, it)
|
track(bonobUrlWithAccessToken, it)
|
||||||
),
|
),
|
||||||
index: paging.index,
|
index: paging.index,
|
||||||
total: playlist.entries.length,
|
total: playlist.entries.length,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
expect(musicLibrary.playlist).toHaveBeenCalledWith(playlist.id);
|
expect(musicLibrary.playlist).toHaveBeenCalledWith(
|
||||||
|
playlist.id
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1752,7 +1828,7 @@ describe("api", () => {
|
|||||||
it("should return a fault of LoginUnsupported", async () => {
|
it("should return a fault of LoginUnsupported", async () => {
|
||||||
const ws = await createClientAsync(`${service.uri}?wsdl`, {
|
const ws = await createClientAsync(`${service.uri}?wsdl`, {
|
||||||
endpoint: service.uri,
|
endpoint: service.uri,
|
||||||
httpClient: supersoap(server, rootUrl),
|
httpClient: supersoap(server),
|
||||||
});
|
});
|
||||||
|
|
||||||
await ws
|
await ws
|
||||||
@@ -1773,10 +1849,12 @@ describe("api", () => {
|
|||||||
|
|
||||||
const ws = await createClientAsync(`${service.uri}?wsdl`, {
|
const ws = await createClientAsync(`${service.uri}?wsdl`, {
|
||||||
endpoint: service.uri,
|
endpoint: service.uri,
|
||||||
httpClient: supersoap(server, rootUrl),
|
httpClient: supersoap(server),
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.addSoapHeader({ credentials: someCredentials("someAuthToken") });
|
ws.addSoapHeader({
|
||||||
|
credentials: someCredentials("someAuthToken"),
|
||||||
|
});
|
||||||
await ws
|
await ws
|
||||||
.getExtendedMetadataAsync({ id: "root", index: 0, count: 0 })
|
.getExtendedMetadataAsync({ id: "root", index: 0, count: 0 })
|
||||||
.then(() => fail("shouldnt get here"))
|
.then(() => fail("shouldnt get here"))
|
||||||
@@ -1791,8 +1869,6 @@ describe("api", () => {
|
|||||||
|
|
||||||
describe("when valid credentials are provided", () => {
|
describe("when valid credentials are provided", () => {
|
||||||
let ws: Client;
|
let ws: Client;
|
||||||
const authToken = `authToken-${uuid()}`;
|
|
||||||
const accessToken = `accessToken-${uuid()}`;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
musicService.login.mockResolvedValue(musicLibrary);
|
musicService.login.mockResolvedValue(musicLibrary);
|
||||||
@@ -1800,7 +1876,7 @@ describe("api", () => {
|
|||||||
|
|
||||||
ws = await createClientAsync(`${service.uri}?wsdl`, {
|
ws = await createClientAsync(`${service.uri}?wsdl`, {
|
||||||
endpoint: service.uri,
|
endpoint: service.uri,
|
||||||
httpClient: supersoap(server, rootUrl),
|
httpClient: supersoap(server),
|
||||||
});
|
});
|
||||||
ws.addSoapHeader({ credentials: someCredentials(authToken) });
|
ws.addSoapHeader({ credentials: someCredentials(authToken) });
|
||||||
});
|
});
|
||||||
@@ -1838,7 +1914,7 @@ describe("api", () => {
|
|||||||
index: "0",
|
index: "0",
|
||||||
total: "3",
|
total: "3",
|
||||||
mediaCollection: artist.albums.map((it) =>
|
mediaCollection: artist.albums.map((it) =>
|
||||||
album(rootUrl, accessToken, it)
|
album(bonobUrlWithAccessToken, it)
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -1863,7 +1939,7 @@ describe("api", () => {
|
|||||||
index: "1",
|
index: "1",
|
||||||
total: "3",
|
total: "3",
|
||||||
mediaCollection: [album2, album3].map((it) =>
|
mediaCollection: [album2, album3].map((it) =>
|
||||||
album(rootUrl, accessToken, it)
|
album(bonobUrlWithAccessToken, it)
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -1966,10 +2042,9 @@ describe("api", () => {
|
|||||||
genreId: track.genre?.id,
|
genreId: track.genre?.id,
|
||||||
duration: track.duration,
|
duration: track.duration,
|
||||||
albumArtURI: defaultAlbumArtURI(
|
albumArtURI: defaultAlbumArtURI(
|
||||||
rootUrl,
|
bonobUrlWithAccessToken,
|
||||||
accessToken,
|
|
||||||
track.album
|
track.album
|
||||||
),
|
).href(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -1999,7 +2074,10 @@ describe("api", () => {
|
|||||||
itemType: "album",
|
itemType: "album",
|
||||||
id: `album:${album.id}`,
|
id: `album:${album.id}`,
|
||||||
title: album.name,
|
title: album.name,
|
||||||
albumArtURI: defaultAlbumArtURI(rootUrl, accessToken, album),
|
albumArtURI: defaultAlbumArtURI(
|
||||||
|
bonobUrlWithAccessToken,
|
||||||
|
album
|
||||||
|
).href(),
|
||||||
canPlay: true,
|
canPlay: true,
|
||||||
artistId: album.artistId,
|
artistId: album.artistId,
|
||||||
artist: album.artistName,
|
artist: album.artistName,
|
||||||
@@ -2017,7 +2095,7 @@ describe("api", () => {
|
|||||||
it("should return a fault of LoginUnsupported", async () => {
|
it("should return a fault of LoginUnsupported", async () => {
|
||||||
const ws = await createClientAsync(`${service.uri}?wsdl`, {
|
const ws = await createClientAsync(`${service.uri}?wsdl`, {
|
||||||
endpoint: service.uri,
|
endpoint: service.uri,
|
||||||
httpClient: supersoap(server, rootUrl),
|
httpClient: supersoap(server),
|
||||||
});
|
});
|
||||||
|
|
||||||
await ws
|
await ws
|
||||||
@@ -2038,10 +2116,12 @@ describe("api", () => {
|
|||||||
|
|
||||||
const ws = await createClientAsync(`${service.uri}?wsdl`, {
|
const ws = await createClientAsync(`${service.uri}?wsdl`, {
|
||||||
endpoint: service.uri,
|
endpoint: service.uri,
|
||||||
httpClient: supersoap(server, rootUrl),
|
httpClient: supersoap(server),
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.addSoapHeader({ credentials: someCredentials("invalid token") });
|
ws.addSoapHeader({
|
||||||
|
credentials: someCredentials("invalid token"),
|
||||||
|
});
|
||||||
await ws
|
await ws
|
||||||
.getMediaURIAsync({ id: "track:123" })
|
.getMediaURIAsync({ id: "track:123" })
|
||||||
.then(() => fail("shouldnt get here"))
|
.then(() => fail("shouldnt get here"))
|
||||||
@@ -2055,9 +2135,7 @@ describe("api", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("when valid credentials are provided", () => {
|
describe("when valid credentials are provided", () => {
|
||||||
const authToken = `authToken-${uuid()}`;
|
|
||||||
let ws: Client;
|
let ws: Client;
|
||||||
const accessToken = `temporaryAccessToken-${uuid()}`;
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
musicService.login.mockResolvedValue(musicLibrary);
|
musicService.login.mockResolvedValue(musicLibrary);
|
||||||
@@ -2065,7 +2143,7 @@ describe("api", () => {
|
|||||||
|
|
||||||
ws = await createClientAsync(`${service.uri}?wsdl`, {
|
ws = await createClientAsync(`${service.uri}?wsdl`, {
|
||||||
endpoint: service.uri,
|
endpoint: service.uri,
|
||||||
httpClient: supersoap(server, rootUrl),
|
httpClient: supersoap(server),
|
||||||
});
|
});
|
||||||
ws.addSoapHeader({ credentials: someCredentials(authToken) });
|
ws.addSoapHeader({ credentials: someCredentials(authToken) });
|
||||||
});
|
});
|
||||||
@@ -2079,9 +2157,13 @@ describe("api", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(root[0]).toEqual({
|
expect(root[0]).toEqual({
|
||||||
getMediaURIResult: `${rootUrl}/stream/track/${trackId}`,
|
getMediaURIResult: bonobUrl
|
||||||
|
.append({
|
||||||
|
pathname: `/stream/track/${trackId}`,
|
||||||
|
})
|
||||||
|
.href(),
|
||||||
httpHeaders: {
|
httpHeaders: {
|
||||||
header: BONOB_ACCESS_TOKEN_HEADER,
|
header: "bonob-access-token",
|
||||||
value: accessToken,
|
value: accessToken,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -2098,7 +2180,7 @@ describe("api", () => {
|
|||||||
it("should return a fault of LoginUnsupported", async () => {
|
it("should return a fault of LoginUnsupported", async () => {
|
||||||
const ws = await createClientAsync(`${service.uri}?wsdl`, {
|
const ws = await createClientAsync(`${service.uri}?wsdl`, {
|
||||||
endpoint: service.uri,
|
endpoint: service.uri,
|
||||||
httpClient: supersoap(server, rootUrl),
|
httpClient: supersoap(server),
|
||||||
});
|
});
|
||||||
|
|
||||||
await ws
|
await ws
|
||||||
@@ -2119,7 +2201,7 @@ describe("api", () => {
|
|||||||
|
|
||||||
const ws = await createClientAsync(`${service.uri}?wsdl`, {
|
const ws = await createClientAsync(`${service.uri}?wsdl`, {
|
||||||
endpoint: service.uri,
|
endpoint: service.uri,
|
||||||
httpClient: supersoap(server, rootUrl),
|
httpClient: supersoap(server),
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.addSoapHeader({
|
ws.addSoapHeader({
|
||||||
@@ -2138,8 +2220,6 @@ describe("api", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("when valid credentials are provided", () => {
|
describe("when valid credentials are provided", () => {
|
||||||
const authToken = `authToken-${uuid()}`;
|
|
||||||
const accessToken = `accessToken-${uuid()}`;
|
|
||||||
let ws: Client;
|
let ws: Client;
|
||||||
|
|
||||||
const someTrack = aTrack();
|
const someTrack = aTrack();
|
||||||
@@ -2151,7 +2231,7 @@ describe("api", () => {
|
|||||||
|
|
||||||
ws = await createClientAsync(`${service.uri}?wsdl`, {
|
ws = await createClientAsync(`${service.uri}?wsdl`, {
|
||||||
endpoint: service.uri,
|
endpoint: service.uri,
|
||||||
httpClient: supersoap(server, rootUrl),
|
httpClient: supersoap(server),
|
||||||
});
|
});
|
||||||
ws.addSoapHeader({ credentials: someCredentials(authToken) });
|
ws.addSoapHeader({ credentials: someCredentials(authToken) });
|
||||||
});
|
});
|
||||||
@@ -2163,7 +2243,12 @@ describe("api", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(root[0]).toEqual({
|
expect(root[0]).toEqual({
|
||||||
getMediaMetadataResult: track(rootUrl, accessToken, someTrack),
|
getMediaMetadataResult: track(
|
||||||
|
bonobUrl.with({
|
||||||
|
searchParams: { "bonob-access-token": accessToken },
|
||||||
|
}),
|
||||||
|
someTrack
|
||||||
|
),
|
||||||
});
|
});
|
||||||
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
||||||
expect(accessTokens.mint).toHaveBeenCalledWith(authToken);
|
expect(accessTokens.mint).toHaveBeenCalledWith(authToken);
|
||||||
@@ -2174,9 +2259,6 @@ describe("api", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("createContainer", () => {
|
describe("createContainer", () => {
|
||||||
const authToken = `authToken-${uuid()}`;
|
|
||||||
const accessToken = `accessToken-${uuid()}`;
|
|
||||||
|
|
||||||
let ws: Client;
|
let ws: Client;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@@ -2185,7 +2267,7 @@ describe("api", () => {
|
|||||||
|
|
||||||
ws = await createClientAsync(`${service.uri}?wsdl`, {
|
ws = await createClientAsync(`${service.uri}?wsdl`, {
|
||||||
endpoint: service.uri,
|
endpoint: service.uri,
|
||||||
httpClient: supersoap(server, rootUrl),
|
httpClient: supersoap(server),
|
||||||
});
|
});
|
||||||
ws.addSoapHeader({ credentials: someCredentials(authToken) });
|
ws.addSoapHeader({ credentials: someCredentials(authToken) });
|
||||||
});
|
});
|
||||||
@@ -2251,8 +2333,6 @@ describe("api", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("deleteContainer", () => {
|
describe("deleteContainer", () => {
|
||||||
const authToken = `authToken-${uuid()}`;
|
|
||||||
const accessToken = `accessToken-${uuid()}`;
|
|
||||||
const id = "id123";
|
const id = "id123";
|
||||||
|
|
||||||
let ws: Client;
|
let ws: Client;
|
||||||
@@ -2263,7 +2343,7 @@ describe("api", () => {
|
|||||||
|
|
||||||
ws = await createClientAsync(`${service.uri}?wsdl`, {
|
ws = await createClientAsync(`${service.uri}?wsdl`, {
|
||||||
endpoint: service.uri,
|
endpoint: service.uri,
|
||||||
httpClient: supersoap(server, rootUrl),
|
httpClient: supersoap(server),
|
||||||
});
|
});
|
||||||
ws.addSoapHeader({ credentials: someCredentials(authToken) });
|
ws.addSoapHeader({ credentials: someCredentials(authToken) });
|
||||||
});
|
});
|
||||||
@@ -2283,8 +2363,6 @@ describe("api", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("addToContainer", () => {
|
describe("addToContainer", () => {
|
||||||
const authToken = `authToken-${uuid()}`;
|
|
||||||
const accessToken = `accessToken-${uuid()}`;
|
|
||||||
const trackId = "track123";
|
const trackId = "track123";
|
||||||
const playlistId = "parent123";
|
const playlistId = "parent123";
|
||||||
|
|
||||||
@@ -2296,7 +2374,7 @@ describe("api", () => {
|
|||||||
|
|
||||||
ws = await createClientAsync(`${service.uri}?wsdl`, {
|
ws = await createClientAsync(`${service.uri}?wsdl`, {
|
||||||
endpoint: service.uri,
|
endpoint: service.uri,
|
||||||
httpClient: supersoap(server, rootUrl),
|
httpClient: supersoap(server),
|
||||||
});
|
});
|
||||||
ws.addSoapHeader({ credentials: someCredentials(authToken) });
|
ws.addSoapHeader({ credentials: someCredentials(authToken) });
|
||||||
});
|
});
|
||||||
@@ -2309,7 +2387,9 @@ describe("api", () => {
|
|||||||
parentId: `parent:${playlistId}`,
|
parentId: `parent:${playlistId}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(result[0]).toEqual({ addToContainerResult: { updateId: null } });
|
expect(result[0]).toEqual({
|
||||||
|
addToContainerResult: { updateId: null },
|
||||||
|
});
|
||||||
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
||||||
expect(accessTokens.mint).toHaveBeenCalledWith(authToken);
|
expect(accessTokens.mint).toHaveBeenCalledWith(authToken);
|
||||||
expect(musicLibrary.addToPlaylist).toHaveBeenCalledWith(
|
expect(musicLibrary.addToPlaylist).toHaveBeenCalledWith(
|
||||||
@@ -2320,9 +2400,6 @@ describe("api", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("removeFromContainer", () => {
|
describe("removeFromContainer", () => {
|
||||||
const authToken = `authToken-${uuid()}`;
|
|
||||||
const accessToken = `accessToken-${uuid()}`;
|
|
||||||
|
|
||||||
let ws: Client;
|
let ws: Client;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@@ -2331,7 +2408,7 @@ describe("api", () => {
|
|||||||
|
|
||||||
ws = await createClientAsync(`${service.uri}?wsdl`, {
|
ws = await createClientAsync(`${service.uri}?wsdl`, {
|
||||||
endpoint: service.uri,
|
endpoint: service.uri,
|
||||||
httpClient: supersoap(server, rootUrl),
|
httpClient: supersoap(server),
|
||||||
});
|
});
|
||||||
ws.addSoapHeader({ credentials: someCredentials(authToken) });
|
ws.addSoapHeader({ credentials: someCredentials(authToken) });
|
||||||
});
|
});
|
||||||
@@ -2404,9 +2481,6 @@ describe("api", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("setPlayedSeconds", () => {
|
describe("setPlayedSeconds", () => {
|
||||||
const authToken = `authToken-${uuid()}`;
|
|
||||||
const accessToken = `accessToken-${uuid()}`;
|
|
||||||
|
|
||||||
let ws: Client;
|
let ws: Client;
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
@@ -2415,7 +2489,7 @@ describe("api", () => {
|
|||||||
|
|
||||||
ws = await createClientAsync(`${service.uri}?wsdl`, {
|
ws = await createClientAsync(`${service.uri}?wsdl`, {
|
||||||
endpoint: service.uri,
|
endpoint: service.uri,
|
||||||
httpClient: supersoap(server, rootUrl),
|
httpClient: supersoap(server),
|
||||||
});
|
});
|
||||||
ws.addSoapHeader({ credentials: someCredentials(authToken) });
|
ws.addSoapHeader({ credentials: someCredentials(authToken) });
|
||||||
});
|
});
|
||||||
@@ -2547,4 +2621,6 @@ describe("api", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ import sonos, {
|
|||||||
} from "../src/sonos";
|
} from "../src/sonos";
|
||||||
|
|
||||||
import { aSonosDevice, aService } from "./builders";
|
import { aSonosDevice, aService } from "./builders";
|
||||||
|
import url from "../src/url_builder";
|
||||||
|
|
||||||
const mockSonosManagerConstructor = <jest.Mock<SonosManager>>SonosManager;
|
const mockSonosManagerConstructor = <jest.Mock<SonosManager>>SonosManager;
|
||||||
|
|
||||||
@@ -107,10 +108,10 @@ describe("sonos", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("bonobService", () => {
|
describe("bonobService", () => {
|
||||||
describe("when the bonob root does not have a trailing /", () => {
|
describe("when the bonob url does not have a trailing /", () => {
|
||||||
it("should return a valid bonob service", () => {
|
it("should return a valid bonob service", () => {
|
||||||
expect(
|
expect(
|
||||||
bonobService("some-bonob", 876, "http://bonob.example.com")
|
bonobService("some-bonob", 876, url("http://bonob.example.com"))
|
||||||
).toEqual({
|
).toEqual({
|
||||||
name: "some-bonob",
|
name: "some-bonob",
|
||||||
sid: 876,
|
sid: 876,
|
||||||
@@ -130,10 +131,10 @@ describe("sonos", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("when the bonob root does have a trailing /", () => {
|
describe("when the bonob url does have a trailing /", () => {
|
||||||
it("should return a valid bonob service", () => {
|
it("should return a valid bonob service", () => {
|
||||||
expect(
|
expect(
|
||||||
bonobService("some-bonob", 876, "http://bonob.example.com/")
|
bonobService("some-bonob", 876, url("http://bonob.example.com/"))
|
||||||
).toEqual({
|
).toEqual({
|
||||||
name: "some-bonob",
|
name: "some-bonob",
|
||||||
sid: 876,
|
sid: 876,
|
||||||
@@ -153,10 +154,33 @@ describe("sonos", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("when the bonob url has a context of /some-context", () => {
|
||||||
|
it("should return a valid bonob service", () => {
|
||||||
|
expect(
|
||||||
|
bonobService("some-bonob", 876, url("http://bonob.example.com/some-context"))
|
||||||
|
).toEqual({
|
||||||
|
name: "some-bonob",
|
||||||
|
sid: 876,
|
||||||
|
uri: `http://bonob.example.com/some-context/ws/sonos`,
|
||||||
|
secureUri: `http://bonob.example.com/some-context/ws/sonos`,
|
||||||
|
strings: {
|
||||||
|
uri: `http://bonob.example.com/some-context/sonos/strings.xml`,
|
||||||
|
version: PRESENTATION_AND_STRINGS_VERSION,
|
||||||
|
},
|
||||||
|
presentation: {
|
||||||
|
uri: `http://bonob.example.com/some-context/sonos/presentationMap.xml`,
|
||||||
|
version: PRESENTATION_AND_STRINGS_VERSION,
|
||||||
|
},
|
||||||
|
pollInterval: 1200,
|
||||||
|
authType: "AppLink",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("when authType is specified", () => {
|
describe("when authType is specified", () => {
|
||||||
it("should return a valid bonob service", () => {
|
it("should return a valid bonob service", () => {
|
||||||
expect(
|
expect(
|
||||||
bonobService("some-bonob", 876, "http://bonob.example.com", 'DeviceLink')
|
bonobService("some-bonob", 876, url("http://bonob.example.com"), 'DeviceLink')
|
||||||
).toEqual({
|
).toEqual({
|
||||||
name: "some-bonob",
|
name: "some-bonob",
|
||||||
sid: 876,
|
sid: 876,
|
||||||
@@ -242,7 +266,7 @@ describe("sonos", () => {
|
|||||||
expect(disabled).toEqual(SONOS_DISABLED);
|
expect(disabled).toEqual(SONOS_DISABLED);
|
||||||
expect(await disabled.devices()).toEqual([]);
|
expect(await disabled.devices()).toEqual([]);
|
||||||
expect(await disabled.services()).toEqual([]);
|
expect(await disabled.services()).toEqual([]);
|
||||||
expect(await disabled.register(aService())).toEqual(false);
|
expect(await disabled.register(aService())).toEqual(true);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { Express } from "express";
|
import { Express } from "express";
|
||||||
import request from "supertest";
|
import request from "supertest";
|
||||||
|
|
||||||
function supersoap(server: Express, rootUrl: string) {
|
function supersoap(server: Express) {
|
||||||
return {
|
return {
|
||||||
request: (
|
request: (
|
||||||
rurl: string,
|
rurl: string,
|
||||||
@@ -9,7 +9,8 @@ function supersoap(server: Express, rootUrl: string) {
|
|||||||
callback: (error: any, res?: any, body?: any) => any,
|
callback: (error: any, res?: any, body?: any) => any,
|
||||||
exheaders?: any
|
exheaders?: any
|
||||||
) => {
|
) => {
|
||||||
const withoutHost = rurl.replace(rootUrl, "");
|
const url = new URL(rurl);
|
||||||
|
const withoutHost = `${url.pathname}${url.search}`;
|
||||||
const req =
|
const req =
|
||||||
data == null
|
data == null
|
||||||
? request(server).get(withoutHost).send()
|
? request(server).get(withoutHost).send()
|
||||||
|
|||||||
212
tests/url_builder.test.ts
Normal file
212
tests/url_builder.test.ts
Normal file
@@ -0,0 +1,212 @@
|
|||||||
|
import url from "../src/url_builder";
|
||||||
|
|
||||||
|
describe("URLBuilder", () => {
|
||||||
|
describe("construction", () => {
|
||||||
|
it("with a string", () => {
|
||||||
|
expect(url("http://example.com/").href()).toEqual("http://example.com/");
|
||||||
|
expect(url("http://example.com/foobar?name=bob").href()).toEqual(
|
||||||
|
"http://example.com/foobar?name=bob"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("with a URL", () => {
|
||||||
|
expect(url(new URL("http://example.com/")).href()).toEqual(
|
||||||
|
"http://example.com/"
|
||||||
|
);
|
||||||
|
expect(url(new URL("http://example.com/foobar?name=bob")).href()).toEqual(
|
||||||
|
"http://example.com/foobar?name=bob"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("toString", () => {
|
||||||
|
it("should print the href", () => {
|
||||||
|
expect(`${url("http://example.com/")}`).toEqual("http://example.com/");
|
||||||
|
expect(`${url("http://example.com/foobar?name=bob")}`).toEqual(
|
||||||
|
"http://example.com/foobar?name=bob"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("path", () => {
|
||||||
|
it("should be the pathname and search", () => {
|
||||||
|
expect(url("http://example.com/").path()).toEqual("/");
|
||||||
|
expect(url("http://example.com/?whoop=ie").path()).toEqual("/?whoop=ie");
|
||||||
|
expect(url("http://example.com/foo/bar").path()).toEqual("/foo/bar");
|
||||||
|
expect(url("http://example.com/with/search?q=bob&s=100").path()).toEqual("/with/search?q=bob&s=100");
|
||||||
|
expect(url("http://example.com/drops/hash#1234").path()).toEqual("/drops/hash");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("updating the pathname", () => {
|
||||||
|
describe("appending", () => {
|
||||||
|
describe("when there is no existing pathname", ()=>{
|
||||||
|
it("should return a new URLBuilder with the new pathname appended to the existing pathname", () => {
|
||||||
|
const original = url("https://example.com?a=b");
|
||||||
|
const updated = original.append({ pathname: "/the-appended-path" });
|
||||||
|
|
||||||
|
expect(original.href()).toEqual("https://example.com/?a=b");
|
||||||
|
expect(original.pathname()).toEqual("/")
|
||||||
|
|
||||||
|
expect(updated.href()).toEqual("https://example.com/the-appended-path?a=b");
|
||||||
|
expect(updated.pathname()).toEqual("/the-appended-path")
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when the existing pathname is /", ()=>{
|
||||||
|
it("should return a new URLBuilder with the new pathname appended to the existing pathname", () => {
|
||||||
|
const original = url("https://example.com/");
|
||||||
|
const updated = original.append({ pathname: "/the-appended-path" });
|
||||||
|
|
||||||
|
expect(original.href()).toEqual("https://example.com/");
|
||||||
|
expect(original.pathname()).toEqual("/")
|
||||||
|
|
||||||
|
expect(updated.href()).toEqual("https://example.com/the-appended-path");
|
||||||
|
expect(updated.pathname()).toEqual("/the-appended-path")
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when the existing pathname is /first-path", ()=>{
|
||||||
|
it("should return a new URLBuilder with the new pathname appended to the existing pathname", () => {
|
||||||
|
const original = url("https://example.com/first-path");
|
||||||
|
const updated = original.append({ pathname: "/the-appended-path" });
|
||||||
|
|
||||||
|
expect(original.href()).toEqual("https://example.com/first-path");
|
||||||
|
expect(original.pathname()).toEqual("/first-path")
|
||||||
|
|
||||||
|
expect(updated.href()).toEqual("https://example.com/first-path/the-appended-path");
|
||||||
|
expect(updated.pathname()).toEqual("/first-path/the-appended-path")
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when the existing pathname is /first-path/", ()=>{
|
||||||
|
it("should return a new URLBuilder with the new pathname appended to the existing pathname", () => {
|
||||||
|
const original = url("https://example.com/first-path/");
|
||||||
|
const updated = original.append({ pathname: "/the-appended-path" });
|
||||||
|
|
||||||
|
expect(original.href()).toEqual("https://example.com/first-path/");
|
||||||
|
expect(original.pathname()).toEqual("/first-path/")
|
||||||
|
|
||||||
|
expect(updated.href()).toEqual("https://example.com/first-path/the-appended-path");
|
||||||
|
expect(updated.pathname()).toEqual("/first-path/the-appended-path")
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return a new URLBuilder with the new pathname appended to the existing pathname", () => {
|
||||||
|
const original = url("https://example.com/some-path?a=b");
|
||||||
|
const updated = original.append({ pathname: "/some-new-path" });
|
||||||
|
|
||||||
|
expect(original.href()).toEqual("https://example.com/some-path?a=b");
|
||||||
|
expect(original.pathname()).toEqual("/some-path")
|
||||||
|
|
||||||
|
expect(updated.href()).toEqual("https://example.com/some-path/some-new-path?a=b");
|
||||||
|
expect(updated.pathname()).toEqual("/some-path/some-new-path")
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("replacing", () => {
|
||||||
|
it("should return a new URLBuilder with the new pathname", () => {
|
||||||
|
const original = url("https://example.com/some-path?a=b");
|
||||||
|
const updated = original.with({ pathname: "/some-new-path" });
|
||||||
|
|
||||||
|
expect(original.href()).toEqual("https://example.com/some-path?a=b");
|
||||||
|
expect(original.pathname()).toEqual("/some-path")
|
||||||
|
|
||||||
|
expect(updated.href()).toEqual("https://example.com/some-new-path?a=b");
|
||||||
|
expect(updated.pathname()).toEqual("/some-new-path")
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("updating search params", () => {
|
||||||
|
describe("appending", () => {
|
||||||
|
describe("with records", () => {
|
||||||
|
it("should return a new URLBuilder with the new search params appended", () => {
|
||||||
|
const original = url("https://example.com/some-path?a=b&c=d");
|
||||||
|
const updated = original.append({
|
||||||
|
searchParams: { x: "y", z: "1" },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(original.href()).toEqual("https://example.com/some-path?a=b&c=d");
|
||||||
|
expect(`${original.searchParams()}`).toEqual("a=b&c=d")
|
||||||
|
|
||||||
|
expect(updated.href()).toEqual("https://example.com/some-path?a=b&c=d&x=y&z=1");
|
||||||
|
expect(`${updated.searchParams()}`).toEqual("a=b&c=d&x=y&z=1")
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("with URLSearchParams", () => {
|
||||||
|
it("should return a new URLBuilder with the new search params appended", () => {
|
||||||
|
const original = url("https://example.com/some-path?a=b&c=d");
|
||||||
|
const updated = original.append({
|
||||||
|
searchParams: new URLSearchParams({ x: "y", z: "1" }),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(original.href()).toEqual("https://example.com/some-path?a=b&c=d");
|
||||||
|
expect(`${original.searchParams()}`).toEqual("a=b&c=d")
|
||||||
|
|
||||||
|
expect(updated.href()).toEqual("https://example.com/some-path?a=b&c=d&x=y&z=1");
|
||||||
|
expect(`${updated.searchParams()}`).toEqual("a=b&c=d&x=y&z=1")
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("replacing", () => {
|
||||||
|
describe("with records", () => {
|
||||||
|
it("should be able to remove all search params", () => {
|
||||||
|
const original = url("https://example.com/some-path?a=b&c=d");
|
||||||
|
const updated = original.with({
|
||||||
|
searchParams: {},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(original.href()).toEqual("https://example.com/some-path?a=b&c=d");
|
||||||
|
expect(`${original.searchParams()}`).toEqual("a=b&c=d")
|
||||||
|
|
||||||
|
expect(updated.href()).toEqual("https://example.com/some-path");
|
||||||
|
expect(`${updated.searchParams()}`).toEqual("")
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return a new URLBuilder with the new search params", () => {
|
||||||
|
const original = url("https://example.com/some-path?a=b&c=d");
|
||||||
|
const updated = original.with({
|
||||||
|
searchParams: { x: "y", z: "1" },
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(original.href()).toEqual("https://example.com/some-path?a=b&c=d");
|
||||||
|
expect(`${original.searchParams()}`).toEqual("a=b&c=d")
|
||||||
|
|
||||||
|
expect(updated.href()).toEqual("https://example.com/some-path?x=y&z=1");
|
||||||
|
expect(`${updated.searchParams()}`).toEqual("x=y&z=1")
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("with URLSearchParams", () => {
|
||||||
|
it("should be able to remove all search params", () => {
|
||||||
|
const original = url("https://example.com/some-path?a=b&c=d");
|
||||||
|
const updated = original.with({
|
||||||
|
searchParams: new URLSearchParams({}),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(original.href()).toEqual("https://example.com/some-path?a=b&c=d");
|
||||||
|
expect(`${original.searchParams()}`).toEqual("a=b&c=d")
|
||||||
|
|
||||||
|
expect(updated.href()).toEqual("https://example.com/some-path");
|
||||||
|
expect(`${updated.searchParams()}`).toEqual("")
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return a new URLBuilder with the new search params", () => {
|
||||||
|
const original = url("https://example.com/some-path?a=b&c=d");
|
||||||
|
const updated = original.with({
|
||||||
|
searchParams: new URLSearchParams({ x: "y", z: "1" }),
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(original.href()).toEqual("https://example.com/some-path?a=b&c=d");
|
||||||
|
expect(`${original.searchParams()}`).toEqual("a=b&c=d")
|
||||||
|
|
||||||
|
expect(updated.href()).toEqual("https://example.com/some-path?x=y&z=1");
|
||||||
|
expect(`${updated.searchParams()}`).toEqual("x=y&z=1")
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
<% } else { %>
|
<% } else { %>
|
||||||
<h3>No existing service registration</h3>
|
<h3>No existing service registration</h3>
|
||||||
<% } %>
|
<% } %>
|
||||||
<form action="/register" method="POST"><button>Re-register</button></form>
|
<form action="<%= it.registerRoute %>" method="POST"><button>Re-register</button></form>
|
||||||
<h2>Devices</h2>
|
<h2>Devices</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<% it.devices.forEach(function(d){ %>
|
<% it.devices.forEach(function(d){ %>
|
||||||
|
|||||||
Reference in New Issue
Block a user