mirror of
https://github.com/wkulhanek/bonob.git
synced 2025-12-21 17:33:29 +01:00
Tests for browsing of artists and albums
This commit is contained in:
@@ -15,6 +15,7 @@
|
|||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"eta": "^1.12.1",
|
"eta": "^1.12.1",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
|
"fp-ts": "^2.9.5",
|
||||||
"morgan": "^1.10.0",
|
"morgan": "^1.10.0",
|
||||||
"node-html-parser": "^2.1.0",
|
"node-html-parser": "^2.1.0",
|
||||||
"soap": "^0.36.0",
|
"soap": "^0.36.0",
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export type Association = {
|
|||||||
export interface LinkCodes {
|
export interface LinkCodes {
|
||||||
mint(): string
|
mint(): string
|
||||||
clear(): any
|
clear(): any
|
||||||
count(): Number
|
count(): number
|
||||||
has(linkCode: string): boolean
|
has(linkCode: string): boolean
|
||||||
associate(linkCode: string, association: Association): any
|
associate(linkCode: string, association: Association): any
|
||||||
associationFor(linkCode: string): Association | undefined
|
associationFor(linkCode: string): Association | undefined
|
||||||
|
|||||||
@@ -6,29 +6,72 @@ const navidrome = process.env["BONOB_NAVIDROME_URL"];
|
|||||||
const u = process.env["BONOB_USER"];
|
const u = process.env["BONOB_USER"];
|
||||||
const t = Md5.hashStr(`${process.env["BONOB_PASSWORD"]}${s}`);
|
const t = Md5.hashStr(`${process.env["BONOB_PASSWORD"]}${s}`);
|
||||||
|
|
||||||
export type Credentials = { username: string, password: string }
|
export type Credentials = { username: string; password: string };
|
||||||
|
|
||||||
export function isSuccess(authResult: AuthSuccess | AuthFailure): authResult is AuthSuccess {
|
export function isSuccess(
|
||||||
|
authResult: AuthSuccess | AuthFailure
|
||||||
|
): authResult is AuthSuccess {
|
||||||
return (authResult as AuthSuccess).authToken !== undefined;
|
return (authResult as AuthSuccess).authToken !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AuthSuccess = {
|
export function isFailure(
|
||||||
authToken: string
|
authResult: any | AuthFailure
|
||||||
userId: string
|
): authResult is AuthFailure {
|
||||||
nickname: string
|
return (authResult as AuthFailure).message !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type AuthSuccess = {
|
||||||
|
authToken: string;
|
||||||
|
userId: string;
|
||||||
|
nickname: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type AuthFailure = {
|
export type AuthFailure = {
|
||||||
message: string
|
message: string;
|
||||||
}
|
};
|
||||||
|
|
||||||
export interface MusicService {
|
export interface MusicService {
|
||||||
login(credentials: Credentials): AuthSuccess | AuthFailure
|
generateToken(credentials: Credentials): AuthSuccess | AuthFailure;
|
||||||
|
login(authToken: string): MusicLibrary | AuthFailure;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type Artist = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type Album = {
|
||||||
|
id: string;
|
||||||
|
name: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface MusicLibrary {
|
||||||
|
artists(): Artist[];
|
||||||
|
artist(id: string): Artist;
|
||||||
|
albums({ artistId, _index, _count }: { artistId?: string, _index?: number, _count?: number }): Album[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Navidrome implements MusicService {
|
export class Navidrome implements MusicService {
|
||||||
login({ username }: Credentials) {
|
generateToken({ username }: Credentials) {
|
||||||
return { authToken: `v1:${username}`, userId: username, nickname: username }
|
return {
|
||||||
|
authToken: `v1:${username}`,
|
||||||
|
userId: username,
|
||||||
|
nickname: username,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
login(_: string) {
|
||||||
|
return {
|
||||||
|
artists: () => [],
|
||||||
|
artist: (id: string) => ({
|
||||||
|
id,
|
||||||
|
name: id,
|
||||||
|
}),
|
||||||
|
albums: ({ artistId }: { artistId?: string }) => {
|
||||||
|
console.log(artistId)
|
||||||
|
return []
|
||||||
|
},
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
ping = (): Promise<boolean> =>
|
ping = (): Promise<boolean> =>
|
||||||
@@ -42,6 +85,3 @@ export class Navidrome implements MusicService {
|
|||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ function server(
|
|||||||
|
|
||||||
app.post(LOGIN_ROUTE, (req, res) => {
|
app.post(LOGIN_ROUTE, (req, res) => {
|
||||||
const { username, password, linkCode } = req.body;
|
const { username, password, linkCode } = req.body;
|
||||||
const authResult = musicService.login({
|
const authResult = musicService.generateToken({
|
||||||
username,
|
username,
|
||||||
password,
|
password,
|
||||||
});
|
});
|
||||||
@@ -107,7 +107,7 @@ function server(
|
|||||||
res.send("");
|
res.send("");
|
||||||
});
|
});
|
||||||
|
|
||||||
bindSmapiSoapServiceToExpress(app, SOAP_PATH, webAddress, linkCodes);
|
bindSmapiSoapServiceToExpress(app, SOAP_PATH, webAddress, linkCodes, musicService);
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
}
|
}
|
||||||
|
|||||||
138
src/smapi.ts
138
src/smapi.ts
@@ -6,8 +6,9 @@ import path from "path";
|
|||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
|
|
||||||
import { LinkCodes } from "./link_codes";
|
import { LinkCodes } from "./link_codes";
|
||||||
|
import { isFailure, MusicLibrary, MusicService } from "./music_service";
|
||||||
|
|
||||||
export const LOGIN_ROUTE = "/login"
|
export const LOGIN_ROUTE = "/login";
|
||||||
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";
|
||||||
@@ -17,6 +18,15 @@ const WSDL_FILE = path.resolve(
|
|||||||
"Sonoswsdl-1.19.4-20190411.142401-3.wsdl"
|
"Sonoswsdl-1.19.4-20190411.142401-3.wsdl"
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export type Credentials = {
|
||||||
|
loginToken: {
|
||||||
|
token: string;
|
||||||
|
householdId: string;
|
||||||
|
};
|
||||||
|
deviceId: string;
|
||||||
|
deviceProvider: string;
|
||||||
|
};
|
||||||
|
|
||||||
export type GetAppLinkResult = {
|
export type GetAppLinkResult = {
|
||||||
getAppLinkResult: {
|
getAppLinkResult: {
|
||||||
authorizeAccount: {
|
authorizeAccount: {
|
||||||
@@ -37,6 +47,36 @@ export type GetDeviceAuthTokenResult = {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type MediaCollection = {
|
||||||
|
id: string;
|
||||||
|
itemType: "collection";
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetMetadataResponse = {
|
||||||
|
getMetadataResult: {
|
||||||
|
count: number;
|
||||||
|
index: number;
|
||||||
|
total: number;
|
||||||
|
mediaCollection: MediaCollection[];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export function getMetadataResult({
|
||||||
|
mediaCollection,
|
||||||
|
}: {
|
||||||
|
mediaCollection: any[] | undefined;
|
||||||
|
}): GetMetadataResponse {
|
||||||
|
return {
|
||||||
|
getMetadataResult: {
|
||||||
|
count: mediaCollection?.length || 0,
|
||||||
|
index: 0,
|
||||||
|
total: mediaCollection?.length || 0,
|
||||||
|
mediaCollection: mediaCollection || [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
class SonosSoap {
|
class SonosSoap {
|
||||||
linkCodes: LinkCodes;
|
linkCodes: LinkCodes;
|
||||||
webAddress: string;
|
webAddress: string;
|
||||||
@@ -97,7 +137,35 @@ class SonosSoap {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function bindSmapiSoapServiceToExpress(app: Express, soapPath:string, webAddress: string, linkCodes: LinkCodes) {
|
export type Container = {
|
||||||
|
itemType: "container";
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const container = ({
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
}: {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
}): Container => ({
|
||||||
|
itemType: "container",
|
||||||
|
id,
|
||||||
|
title,
|
||||||
|
});
|
||||||
|
|
||||||
|
type SoapyHeaders = {
|
||||||
|
credentials?: Credentials;
|
||||||
|
};
|
||||||
|
|
||||||
|
function bindSmapiSoapServiceToExpress(
|
||||||
|
app: Express,
|
||||||
|
soapPath: string,
|
||||||
|
webAddress: string,
|
||||||
|
linkCodes: LinkCodes,
|
||||||
|
musicService: MusicService
|
||||||
|
) {
|
||||||
const sonosSoap = new SonosSoap(webAddress, linkCodes);
|
const sonosSoap = new SonosSoap(webAddress, linkCodes);
|
||||||
const soapyService = listen(
|
const soapyService = listen(
|
||||||
app,
|
app,
|
||||||
@@ -108,6 +176,72 @@ function bindSmapiSoapServiceToExpress(app: Express, soapPath:string, webAddress
|
|||||||
getAppLink: () => sonosSoap.getAppLink(),
|
getAppLink: () => sonosSoap.getAppLink(),
|
||||||
getDeviceAuthToken: ({ linkCode }: { linkCode: string }) =>
|
getDeviceAuthToken: ({ linkCode }: { linkCode: string }) =>
|
||||||
sonosSoap.getDeviceAuthToken({ linkCode }),
|
sonosSoap.getDeviceAuthToken({ linkCode }),
|
||||||
|
getMetadata: (
|
||||||
|
{
|
||||||
|
id,
|
||||||
|
index,
|
||||||
|
count,
|
||||||
|
// recursive,
|
||||||
|
}: { id: string; index: number; count: number; recursive: boolean },
|
||||||
|
_,
|
||||||
|
headers?: SoapyHeaders
|
||||||
|
) => {
|
||||||
|
if (!headers?.credentials) {
|
||||||
|
throw {
|
||||||
|
Fault: {
|
||||||
|
faultcode: "Client.LoginUnsupported",
|
||||||
|
faultstring: "Missing credentials...",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const login = musicService.login(
|
||||||
|
headers.credentials.loginToken.token
|
||||||
|
);
|
||||||
|
if (isFailure(login)) {
|
||||||
|
throw {
|
||||||
|
Fault: {
|
||||||
|
faultcode: "Client.LoginUnauthorized",
|
||||||
|
faultstring: "Credentials not found...",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const musicLibrary = login as MusicLibrary;
|
||||||
|
|
||||||
|
// const [type, typeId] = id.split(":");
|
||||||
|
const type = id;
|
||||||
|
switch (type) {
|
||||||
|
case "root":
|
||||||
|
return getMetadataResult({
|
||||||
|
mediaCollection: [
|
||||||
|
container({ id: "artists", title: "Artists" }),
|
||||||
|
container({ id: "albums", title: "Albums" }),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
case "artists":
|
||||||
|
return getMetadataResult({
|
||||||
|
mediaCollection: musicLibrary.artists().map((it) =>
|
||||||
|
container({
|
||||||
|
id: `artist:${it.id}`,
|
||||||
|
title: it.name,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
});
|
||||||
|
case "albums":
|
||||||
|
return getMetadataResult({
|
||||||
|
mediaCollection: musicLibrary
|
||||||
|
.albums({ _index: index, _count: count })
|
||||||
|
.map((it) =>
|
||||||
|
container({
|
||||||
|
id: `album:${it.id}`,
|
||||||
|
title: it.name,
|
||||||
|
})
|
||||||
|
),
|
||||||
|
});
|
||||||
|
default:
|
||||||
|
throw `Unsupported id:${id}`;
|
||||||
|
}
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import { SonosDevice } from "@svrooij/sonos/lib";
|
import { SonosDevice } from "@svrooij/sonos/lib";
|
||||||
|
import { ArtistWithAlbums } from "in_memory_music_service";
|
||||||
import { v4 as uuid } from "uuid";
|
import { v4 as uuid } from "uuid";
|
||||||
|
import { Credentials } from "../src/smapi";
|
||||||
|
|
||||||
import { Service, Device } from "../src/sonos";
|
import { Service, Device } from "../src/sonos";
|
||||||
|
|
||||||
@@ -54,3 +56,39 @@ export function getAppLinkMessage() {
|
|||||||
callbackPath: "",
|
callbackPath: "",
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function someCredentials(token: string): Credentials {
|
||||||
|
return {
|
||||||
|
loginToken: {
|
||||||
|
token,
|
||||||
|
householdId: "hh1"
|
||||||
|
},
|
||||||
|
deviceId: "d1",
|
||||||
|
deviceProvider: "dp1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BOB_MARLEY: ArtistWithAlbums = {
|
||||||
|
id: uuid(),
|
||||||
|
name: "Bob Marley",
|
||||||
|
albums: [
|
||||||
|
{ id: uuid(), name: "Burin'" },
|
||||||
|
{ id: uuid(), name: "Exodus" },
|
||||||
|
{ id: uuid(), name: "Kaya" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BLONDIE: ArtistWithAlbums = {
|
||||||
|
id: uuid(),
|
||||||
|
name: "Blondie",
|
||||||
|
albums: [
|
||||||
|
{ id: uuid(), name: "Blondie" },
|
||||||
|
{ id: uuid(), name: "Parallel Lines" },
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const MADONNA: ArtistWithAlbums = {
|
||||||
|
id: uuid(),
|
||||||
|
name: "Madonna",
|
||||||
|
albums: [],
|
||||||
|
};
|
||||||
|
|||||||
159
tests/in_memory_music_service.test.ts
Normal file
159
tests/in_memory_music_service.test.ts
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
import {
|
||||||
|
InMemoryMusicService,
|
||||||
|
} from "./in_memory_music_service";
|
||||||
|
import { AuthSuccess, MusicLibrary } from "../src/music_service";
|
||||||
|
import { v4 as uuid } from "uuid";
|
||||||
|
import { BOB_MARLEY, MADONNA, BLONDIE } from './builders'
|
||||||
|
|
||||||
|
describe("InMemoryMusicService", () => {
|
||||||
|
const service = new InMemoryMusicService();
|
||||||
|
|
||||||
|
describe("generateToken", () => {
|
||||||
|
it("should be able to generate a token and then use it to log in", () => {
|
||||||
|
const credentials = { username: "bob", password: "smith" };
|
||||||
|
|
||||||
|
service.hasUser(credentials);
|
||||||
|
|
||||||
|
const token = service.generateToken(credentials) as AuthSuccess;
|
||||||
|
|
||||||
|
expect(token.userId).toEqual(credentials.username);
|
||||||
|
expect(token.nickname).toEqual(credentials.username);
|
||||||
|
|
||||||
|
const musicLibrary = service.login(token.authToken);
|
||||||
|
|
||||||
|
expect(musicLibrary).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should fail with an exception if an invalid token is used", () => {
|
||||||
|
const credentials = { username: "bob", password: "smith" };
|
||||||
|
|
||||||
|
service.hasUser(credentials);
|
||||||
|
|
||||||
|
const token = service.generateToken(credentials) as AuthSuccess;
|
||||||
|
|
||||||
|
service.clear();
|
||||||
|
|
||||||
|
expect(service.login(token.authToken)).toEqual({
|
||||||
|
message: "Invalid auth token",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Music Library", () => {
|
||||||
|
|
||||||
|
|
||||||
|
const user = { username: "user100", password: "password100" };
|
||||||
|
let musicLibrary: MusicLibrary;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service.clear();
|
||||||
|
|
||||||
|
service.hasArtists(BOB_MARLEY, MADONNA, BLONDIE);
|
||||||
|
service.hasUser(user);
|
||||||
|
|
||||||
|
const token = service.generateToken(user) as AuthSuccess;
|
||||||
|
musicLibrary = service.login(token.authToken) as MusicLibrary;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("artists", () => {
|
||||||
|
it("should provide an array of artists", () => {
|
||||||
|
expect(musicLibrary.artists()).toEqual([
|
||||||
|
{ id: BOB_MARLEY.id, name: BOB_MARLEY.name },
|
||||||
|
{ id: MADONNA.id, name: MADONNA.name },
|
||||||
|
{ id: BLONDIE.id, name: BLONDIE.name },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("artist", () => {
|
||||||
|
describe("when it exists", () => {
|
||||||
|
it("should provide an artist", () => {
|
||||||
|
expect(musicLibrary.artist(MADONNA.id)).toEqual({
|
||||||
|
id: MADONNA.id,
|
||||||
|
name: MADONNA.name,
|
||||||
|
});
|
||||||
|
expect(musicLibrary.artist(BLONDIE.id)).toEqual({
|
||||||
|
id: BLONDIE.id,
|
||||||
|
name: BLONDIE.name,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when it doesnt exist", () => {
|
||||||
|
it("should provide an artist", () => {
|
||||||
|
expect(() => musicLibrary.artist("-1")).toThrow(
|
||||||
|
"No artist with id '-1'"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("albums", () => {
|
||||||
|
describe("fetching with no filtering", () => {
|
||||||
|
it("should return all the albums for all the artists", () => {
|
||||||
|
expect(musicLibrary.albums({})).toEqual([
|
||||||
|
...BOB_MARLEY.albums,
|
||||||
|
...BLONDIE.albums,
|
||||||
|
...MADONNA.albums,
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("fetching for a single artist", () => {
|
||||||
|
it("should return them all if the artist has some", () => {
|
||||||
|
expect(musicLibrary.albums({ artistId: BLONDIE.id })).toEqual(
|
||||||
|
BLONDIE.albums
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return empty list of the artists does not have any", () => {
|
||||||
|
expect(musicLibrary.albums({ artistId: MADONNA.id })).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should return empty list if the artist id is not valid", () => {
|
||||||
|
expect(musicLibrary.albums({ artistId: uuid() })).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("fetching with just index", () => {
|
||||||
|
it("should return everything after", () => {
|
||||||
|
expect(musicLibrary.albums({ _index: 2 })).toEqual([
|
||||||
|
BOB_MARLEY.albums[2],
|
||||||
|
BLONDIE.albums[0],
|
||||||
|
BLONDIE.albums[1],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("fetching with just count", () => {
|
||||||
|
it("should return first n items", () => {
|
||||||
|
expect(musicLibrary.albums({ _count: 3 })).toEqual([
|
||||||
|
BOB_MARLEY.albums[0],
|
||||||
|
BOB_MARLEY.albums[1],
|
||||||
|
BOB_MARLEY.albums[2],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("fetching with index and count", () => {
|
||||||
|
it("should be able to return the first page", () => {
|
||||||
|
expect(musicLibrary.albums({ _index: 0, _count: 2 })).toEqual([
|
||||||
|
BOB_MARLEY.albums[0],
|
||||||
|
BOB_MARLEY.albums[1],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
it("should be able to return the second page", () => {
|
||||||
|
expect(musicLibrary.albums({ _index: 2, _count: 2 })).toEqual([
|
||||||
|
BOB_MARLEY.albums[2],
|
||||||
|
BLONDIE.albums[0],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
it("should be able to return the last page", () => {
|
||||||
|
expect(musicLibrary.albums({ _index: 4, _count: 2 })).toEqual([
|
||||||
|
BLONDIE.albums[1],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,35 +1,116 @@
|
|||||||
|
import { option as O } from "fp-ts";
|
||||||
|
import { pipe } from "fp-ts/lib/function";
|
||||||
import {
|
import {
|
||||||
MusicService,
|
MusicService,
|
||||||
Credentials,
|
Credentials,
|
||||||
AuthSuccess,
|
AuthSuccess,
|
||||||
AuthFailure,
|
AuthFailure,
|
||||||
|
Artist,
|
||||||
|
Album,
|
||||||
|
MusicLibrary,
|
||||||
} from "../src/music_service";
|
} from "../src/music_service";
|
||||||
|
|
||||||
|
export type ArtistWithAlbums = Artist & {
|
||||||
|
albums: Album[];
|
||||||
|
};
|
||||||
|
|
||||||
|
const artistWithAlbumsToArtist = (it: ArtistWithAlbums): Artist => ({
|
||||||
|
id: it.id,
|
||||||
|
name: it.name,
|
||||||
|
});
|
||||||
|
|
||||||
|
const getOrThrow = (message: string) =>
|
||||||
|
O.getOrElseW(() => {
|
||||||
|
throw message;
|
||||||
|
});
|
||||||
|
|
||||||
|
type P<T> = (t: T) => boolean;
|
||||||
|
const all: P<any> = (_: any) => true;
|
||||||
|
const artistWithId = (id: string): P<Artist> => (artist: Artist) =>
|
||||||
|
artist.id === id;
|
||||||
|
|
||||||
export class InMemoryMusicService implements MusicService {
|
export class InMemoryMusicService implements MusicService {
|
||||||
users: Record<string, string> = {};
|
users: Record<string, string> = {};
|
||||||
|
artists: ArtistWithAlbums[] = [];
|
||||||
|
|
||||||
login({ username, password }: Credentials): AuthSuccess | AuthFailure {
|
generateToken({
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
}: Credentials): AuthSuccess | AuthFailure {
|
||||||
if (
|
if (
|
||||||
username != undefined &&
|
username != undefined &&
|
||||||
password != undefined &&
|
password != undefined &&
|
||||||
this.users[username] == password
|
this.users[username] == password
|
||||||
) {
|
) {
|
||||||
return { authToken: "v1:token123", userId: username, nickname: username };
|
return {
|
||||||
|
authToken: JSON.stringify({ username, password }),
|
||||||
|
userId: username,
|
||||||
|
nickname: username,
|
||||||
|
};
|
||||||
} else {
|
} else {
|
||||||
return { message: `Invalid user:${username}` };
|
return { message: `Invalid user:${username}` };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
login(token: string): MusicLibrary | AuthFailure {
|
||||||
|
const credentials = JSON.parse(token) as Credentials;
|
||||||
|
if (this.users[credentials.username] != credentials.password) {
|
||||||
|
return {
|
||||||
|
message: "Invalid auth token",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
artists: () => this.artists.map(artistWithAlbumsToArtist),
|
||||||
|
artist: (id: string) =>
|
||||||
|
pipe(
|
||||||
|
this.artists.find((it) => it.id === id),
|
||||||
|
O.fromNullable,
|
||||||
|
O.map(artistWithAlbumsToArtist),
|
||||||
|
getOrThrow(`No artist with id '${id}'`)
|
||||||
|
),
|
||||||
|
albums: ({
|
||||||
|
artistId,
|
||||||
|
_index,
|
||||||
|
_count,
|
||||||
|
}: {
|
||||||
|
artistId?: string;
|
||||||
|
_index?: number;
|
||||||
|
_count?: number;
|
||||||
|
}) => {
|
||||||
|
const i0 = _index || 0;
|
||||||
|
const i1 = _count ? i0 + _count : undefined;
|
||||||
|
return this.artists
|
||||||
|
.filter(
|
||||||
|
pipe(
|
||||||
|
O.fromNullable(artistId),
|
||||||
|
O.map(artistWithId),
|
||||||
|
O.getOrElse(() => all)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.flatMap((it) => it.albums)
|
||||||
|
.slice(i0, i1);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
hasUser(credentials: Credentials) {
|
hasUser(credentials: Credentials) {
|
||||||
this.users[credentials.username] = credentials.password;
|
this.users[credentials.username] = credentials.password;
|
||||||
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
hasNoUsers() {
|
hasNoUsers() {
|
||||||
this.users = {};
|
this.users = {};
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasArtists(...newArtists: ArtistWithAlbums[]) {
|
||||||
|
this.artists = [...this.artists, ...newArtists];
|
||||||
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
clear() {
|
clear() {
|
||||||
this.users = {};
|
this.users = {};
|
||||||
|
this.artists = [];
|
||||||
|
return this;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,8 +3,18 @@ import { Express } from "express";
|
|||||||
|
|
||||||
import request from "supertest";
|
import request from "supertest";
|
||||||
|
|
||||||
import { GetAppLinkResult, GetDeviceAuthTokenResult } from "../src/smapi";
|
import {
|
||||||
import { getAppLinkMessage } from "./builders";
|
GetAppLinkResult,
|
||||||
|
GetDeviceAuthTokenResult,
|
||||||
|
GetMetadataResponse,
|
||||||
|
} from "../src/smapi";
|
||||||
|
import {
|
||||||
|
BLONDIE,
|
||||||
|
BOB_MARLEY,
|
||||||
|
getAppLinkMessage,
|
||||||
|
MADONNA,
|
||||||
|
someCredentials,
|
||||||
|
} from "./builders";
|
||||||
import { InMemoryMusicService } from "./in_memory_music_service";
|
import { InMemoryMusicService } from "./in_memory_music_service";
|
||||||
import { InMemoryLinkCodes } from "../src/link_codes";
|
import { InMemoryLinkCodes } from "../src/link_codes";
|
||||||
import { Credentials } from "../src/music_service";
|
import { Credentials } from "../src/music_service";
|
||||||
@@ -12,13 +22,53 @@ 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";
|
||||||
|
|
||||||
class FooDriver {
|
class LoggedInSonosDriver {
|
||||||
client: Client;
|
client: Client;
|
||||||
token: GetDeviceAuthTokenResult;
|
token: GetDeviceAuthTokenResult;
|
||||||
|
currentMetadata?: GetMetadataResponse = undefined;
|
||||||
|
|
||||||
constructor(client: Client, token: GetDeviceAuthTokenResult) {
|
constructor(client: Client, token: GetDeviceAuthTokenResult) {
|
||||||
this.client = client;
|
this.client = client;
|
||||||
this.token = token;
|
this.token = token;
|
||||||
|
this.client.addSoapHeader({
|
||||||
|
credentials: someCredentials(
|
||||||
|
this.token.getDeviceAuthTokenResult.authToken
|
||||||
|
),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async navigate(...path: string[]) {
|
||||||
|
let next = path.shift();
|
||||||
|
while (next) {
|
||||||
|
if (next != "root") {
|
||||||
|
const childIds = this.currentMetadata!.getMetadataResult.mediaCollection.map(
|
||||||
|
(it) => it.id
|
||||||
|
);
|
||||||
|
if (!childIds.includes(next)) {
|
||||||
|
throw `Expected to find a child element with id=${next} in order to browse, but found only ${childIds}`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.currentMetadata = (await this.getMetadata(next))[0];
|
||||||
|
next = path.shift();
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
expectTitles(titles: string[]) {
|
||||||
|
expect(
|
||||||
|
this.currentMetadata!.getMetadataResult.mediaCollection.map(
|
||||||
|
(it) => it.title
|
||||||
|
)
|
||||||
|
).toEqual(titles);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMetadata(id: string) {
|
||||||
|
return await this.client.getMetadataAsync({
|
||||||
|
id,
|
||||||
|
index: 0,
|
||||||
|
count: 100,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,7 +124,10 @@ class SonosDriver {
|
|||||||
|
|
||||||
return client
|
return client
|
||||||
.getDeviceAuthTokenAsync({ linkCode })
|
.getDeviceAuthTokenAsync({ linkCode })
|
||||||
.then((authToken: [GetDeviceAuthTokenResult, any]) => new FooDriver(client, authToken[0]));
|
.then(
|
||||||
|
(authToken: [GetDeviceAuthTokenResult, any]) =>
|
||||||
|
new LoggedInSonosDriver(client, authToken[0])
|
||||||
|
);
|
||||||
},
|
},
|
||||||
expectFailure: () => {
|
expectFailure: () => {
|
||||||
expect(response.status).toEqual(403);
|
expect(response.status).toEqual(403);
|
||||||
@@ -89,7 +142,10 @@ class SonosDriver {
|
|||||||
describe("scenarios", () => {
|
describe("scenarios", () => {
|
||||||
const bonobUrl = "http://localhost:1234";
|
const bonobUrl = "http://localhost:1234";
|
||||||
const bonob = bonobService("bonob", 123, bonobUrl);
|
const bonob = bonobService("bonob", 123, bonobUrl);
|
||||||
const musicService = new InMemoryMusicService();
|
const musicService = new InMemoryMusicService().hasArtists(
|
||||||
|
BOB_MARLEY,
|
||||||
|
BLONDIE
|
||||||
|
);
|
||||||
const linkCodes = new InMemoryLinkCodes();
|
const linkCodes = new InMemoryLinkCodes();
|
||||||
const server = makeServer(
|
const server = makeServer(
|
||||||
SONOS_DISABLED,
|
SONOS_DISABLED,
|
||||||
@@ -107,22 +163,6 @@ describe("scenarios", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("adding the service", () => {
|
describe("adding the service", () => {
|
||||||
describe("when the user exists within the music service", () => {
|
|
||||||
const username = "validuser";
|
|
||||||
const password = "validpassword";
|
|
||||||
|
|
||||||
it("should successfuly sign up", async () => {
|
|
||||||
musicService.hasUser({ username, password });
|
|
||||||
|
|
||||||
await sonosDriver
|
|
||||||
.addService()
|
|
||||||
.then((it) => it.login({ username, password }))
|
|
||||||
.then((it) => it.expectSuccess());
|
|
||||||
|
|
||||||
expect(linkCodes.count()).toEqual(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
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";
|
||||||
const password = "invalidpassword";
|
const password = "invalidpassword";
|
||||||
@@ -138,5 +178,54 @@ describe("scenarios", () => {
|
|||||||
expect(linkCodes.count()).toEqual(1);
|
expect(linkCodes.count()).toEqual(1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("when the user exists within the music service", () => {
|
||||||
|
const username = "validuser";
|
||||||
|
const password = "validpassword";
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
musicService.hasUser({ username, password });
|
||||||
|
musicService.hasArtists(BLONDIE, BOB_MARLEY, MADONNA);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should successfuly sign up", async () => {
|
||||||
|
await sonosDriver
|
||||||
|
.addService()
|
||||||
|
.then((it) => it.login({ username, password }))
|
||||||
|
.then((it) => it.expectSuccess());
|
||||||
|
|
||||||
|
expect(linkCodes.count()).toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be able to list the artists", async () => {
|
||||||
|
await sonosDriver
|
||||||
|
.addService()
|
||||||
|
.then((it) => it.login({ username, password }))
|
||||||
|
.then((it) => it.expectSuccess())
|
||||||
|
.then((it) => it.navigate("root", "artists"))
|
||||||
|
.then((it) =>
|
||||||
|
it.expectTitles(
|
||||||
|
[BLONDIE, BOB_MARLEY, MADONNA].map(
|
||||||
|
(it) => it.name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should be able to list the albums", async () => {
|
||||||
|
await sonosDriver
|
||||||
|
.addService()
|
||||||
|
.then((it) => it.login({ username, password }))
|
||||||
|
.then((it) => it.expectSuccess())
|
||||||
|
.then((it) => it.navigate("root", "albums"))
|
||||||
|
.then((it) =>
|
||||||
|
it.expectTitles(
|
||||||
|
[...BLONDIE.albums, ...BOB_MARLEY.albums, ...MADONNA.albums].map(
|
||||||
|
(it) => it.name
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
import request from "supertest";
|
import request from "supertest";
|
||||||
import { createClientAsync } from "soap";
|
import { Client, createClientAsync } from "soap";
|
||||||
|
|
||||||
import { DOMParserImpl } from "xmldom-ts";
|
import { DOMParserImpl } from "xmldom-ts";
|
||||||
import * as xpath from "xpath-ts";
|
import * as xpath from "xpath-ts";
|
||||||
@@ -8,11 +8,12 @@ import * as xpath from "xpath-ts";
|
|||||||
import { InMemoryLinkCodes, LinkCodes } from "../src/link_codes";
|
import { InMemoryLinkCodes, LinkCodes } from "../src/link_codes";
|
||||||
import makeServer from "../src/server";
|
import makeServer from "../src/server";
|
||||||
import { bonobService, SONOS_DISABLED } from "../src/sonos";
|
import { bonobService, SONOS_DISABLED } from "../src/sonos";
|
||||||
import { STRINGS_ROUTE, LOGIN_ROUTE } from "../src/smapi";
|
import { STRINGS_ROUTE, LOGIN_ROUTE, getMetadataResult, container } from "../src/smapi";
|
||||||
|
|
||||||
import { aService, getAppLinkMessage } from "./builders";
|
import { aService, BLONDIE, BOB_MARLEY, getAppLinkMessage, someCredentials } from "./builders";
|
||||||
import { InMemoryMusicService } from "./in_memory_music_service";
|
import { InMemoryMusicService } from "./in_memory_music_service";
|
||||||
import supersoap from "./supersoap";
|
import supersoap from "./supersoap";
|
||||||
|
import { AuthSuccess } from "../src/music_service";
|
||||||
|
|
||||||
const parseXML = (value: string) => new DOMParserImpl().parseFromString(value);
|
const parseXML = (value: string) => new DOMParserImpl().parseFromString(value);
|
||||||
const select = xpath.useNamespaces({ sonos: "http://sonos.com/sonosapi" });
|
const select = xpath.useNamespaces({ sonos: "http://sonos.com/sonosapi" });
|
||||||
@@ -42,6 +43,31 @@ describe("service config", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("getMetadataResult", () => {
|
||||||
|
describe("when there are a zero mediaCollections", () => {
|
||||||
|
it("should have zero count", () => {
|
||||||
|
const result = getMetadataResult({ mediaCollection: [] });
|
||||||
|
|
||||||
|
expect(result.getMetadataResult.count).toEqual(0);
|
||||||
|
expect(result.getMetadataResult.index).toEqual(0);
|
||||||
|
expect(result.getMetadataResult.total).toEqual(0);
|
||||||
|
expect(result.getMetadataResult.mediaCollection).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when there are a number of mediaCollections", () => {
|
||||||
|
it("should add correct counts", () => {
|
||||||
|
const mediaCollection = [{}, {}];
|
||||||
|
const result = getMetadataResult({ mediaCollection });
|
||||||
|
|
||||||
|
expect(result.getMetadataResult.count).toEqual(2);
|
||||||
|
expect(result.getMetadataResult.index).toEqual(0);
|
||||||
|
expect(result.getMetadataResult.total).toEqual(2);
|
||||||
|
expect(result.getMetadataResult.mediaCollection).toEqual(mediaCollection);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("api", () => {
|
describe("api", () => {
|
||||||
const rootUrl = "http://localhost:1234";
|
const rootUrl = "http://localhost:1234";
|
||||||
const service = bonobService("test-api", 133, rootUrl, "AppLink");
|
const service = bonobService("test-api", 133, rootUrl, "AppLink");
|
||||||
@@ -175,7 +201,11 @@ describe("api", () => {
|
|||||||
describe("when there is a linkCode association", () => {
|
describe("when there is a linkCode association", () => {
|
||||||
it("should return a device auth token", async () => {
|
it("should return a device auth token", async () => {
|
||||||
const linkCode = linkCodes.mint();
|
const linkCode = linkCodes.mint();
|
||||||
const association = { authToken: "at", userId: "uid", nickname: "nn" };
|
const association = {
|
||||||
|
authToken: "at",
|
||||||
|
userId: "uid",
|
||||||
|
nickname: "nn",
|
||||||
|
};
|
||||||
linkCodes.associate(linkCode, association);
|
linkCodes.associate(linkCode, association);
|
||||||
|
|
||||||
const ws = await createClientAsync(`${service.uri}?wsdl`, {
|
const ws = await createClientAsync(`${service.uri}?wsdl`, {
|
||||||
@@ -213,7 +243,7 @@ describe("api", () => {
|
|||||||
await ws
|
await ws
|
||||||
.getDeviceAuthTokenAsync({ linkCode })
|
.getDeviceAuthTokenAsync({ linkCode })
|
||||||
.then(() => {
|
.then(() => {
|
||||||
throw "Shouldnt get here";
|
fail("Shouldnt get here");
|
||||||
})
|
})
|
||||||
.catch((e: any) => {
|
.catch((e: any) => {
|
||||||
expect(e.root.Envelope.Body.Fault).toEqual({
|
expect(e.root.Envelope.Body.Fault).toEqual({
|
||||||
@@ -226,6 +256,150 @@ describe("api", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
describe("getMetadata", () => {
|
||||||
|
const server = makeServer(
|
||||||
|
SONOS_DISABLED,
|
||||||
|
service,
|
||||||
|
rootUrl,
|
||||||
|
musicService,
|
||||||
|
linkCodes
|
||||||
|
);
|
||||||
|
|
||||||
|
describe("when no credentials header provided", () => {
|
||||||
|
it("should return a fault of LoginUnauthorized", async () => {
|
||||||
|
const ws = await createClientAsync(`${service.uri}?wsdl`, {
|
||||||
|
endpoint: service.uri,
|
||||||
|
httpClient: supersoap(server, rootUrl),
|
||||||
|
});
|
||||||
|
|
||||||
|
await ws
|
||||||
|
.getMetadataAsync({ id: "root", index: 0, count: 0 })
|
||||||
|
.then(() => fail("shouldnt get here"))
|
||||||
|
.catch((e: any) => {
|
||||||
|
expect(e.root.Envelope.Body.Fault).toEqual({
|
||||||
|
faultcode: "Client.LoginUnsupported",
|
||||||
|
faultstring: "Missing credentials...",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when invalid credentials are provided", () => {
|
||||||
|
it("should return a fault of LoginInvalid", async () => {
|
||||||
|
const username = "userThatGetsDeleted";
|
||||||
|
const password = "password1";
|
||||||
|
musicService.hasUser({ username, password });
|
||||||
|
const token = musicService.generateToken({
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
}) as AuthSuccess;
|
||||||
|
musicService.hasNoUsers();
|
||||||
|
|
||||||
|
const ws = await createClientAsync(`${service.uri}?wsdl`, {
|
||||||
|
endpoint: service.uri,
|
||||||
|
httpClient: supersoap(server, rootUrl),
|
||||||
|
});
|
||||||
|
|
||||||
|
ws.addSoapHeader({ credentials: someCredentials(token.authToken) });
|
||||||
|
await ws
|
||||||
|
.getMetadataAsync({ id: "root", index: 0, count: 0 })
|
||||||
|
.then(() => fail("shouldnt get here"))
|
||||||
|
.catch((e: any) => {
|
||||||
|
expect(e.root.Envelope.Body.Fault).toEqual({
|
||||||
|
faultcode: "Client.LoginUnauthorized",
|
||||||
|
faultstring: "Credentials not found...",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when valid credentials are provided", () => {
|
||||||
|
const username = "validUser";
|
||||||
|
const password = "validPassword";
|
||||||
|
let token: AuthSuccess;
|
||||||
|
let ws: Client;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
musicService.hasUser({ username, password });
|
||||||
|
token = musicService.generateToken({
|
||||||
|
username,
|
||||||
|
password,
|
||||||
|
}) as AuthSuccess;
|
||||||
|
ws = await createClientAsync(`${service.uri}?wsdl`, {
|
||||||
|
endpoint: service.uri,
|
||||||
|
httpClient: supersoap(server, rootUrl),
|
||||||
|
});
|
||||||
|
ws.addSoapHeader({ credentials: someCredentials(token.authToken) });
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("asking for the root container", () => {
|
||||||
|
it("should return it", async () => {
|
||||||
|
const root = await ws.getMetadataAsync({
|
||||||
|
id: "root",
|
||||||
|
index: 0,
|
||||||
|
count: 100,
|
||||||
|
});
|
||||||
|
expect(root[0]).toEqual(getMetadataResult({
|
||||||
|
mediaCollection: [
|
||||||
|
container({ id: "artists", title: "Artists" }),
|
||||||
|
container({ id: "albums", title: "Albums" }),
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("asking for artists", () => {
|
||||||
|
it("should return it", async () => {
|
||||||
|
musicService.hasArtists(BLONDIE, BOB_MARLEY);
|
||||||
|
|
||||||
|
const artists = await ws.getMetadataAsync({
|
||||||
|
id: "artists",
|
||||||
|
index: 0,
|
||||||
|
count: 100,
|
||||||
|
});
|
||||||
|
expect(artists[0]).toEqual(getMetadataResult({
|
||||||
|
mediaCollection: [BLONDIE, BOB_MARLEY].map(it => container({ id: `artist:${it.id}`, title: it.name })),
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("asking for all albums", () => {
|
||||||
|
it("should return it", async () => {
|
||||||
|
musicService.hasArtists(BLONDIE, BOB_MARLEY);
|
||||||
|
|
||||||
|
const albums = await ws.getMetadataAsync({
|
||||||
|
id: "albums",
|
||||||
|
index: 0,
|
||||||
|
count: 100,
|
||||||
|
});
|
||||||
|
expect(albums[0]).toEqual(getMetadataResult({
|
||||||
|
mediaCollection: [...BLONDIE.albums, ...BOB_MARLEY.albums].map(it => container({ id: `album:${it.id}`, title: it.name })),
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("asking for albums with paging", () => {
|
||||||
|
it("should return it", async () => {
|
||||||
|
musicService.hasArtists(BLONDIE, BOB_MARLEY);
|
||||||
|
|
||||||
|
expect(BLONDIE.albums.length).toEqual(2);
|
||||||
|
expect(BOB_MARLEY.albums.length).toEqual(3);
|
||||||
|
|
||||||
|
const albums = await ws.getMetadataAsync({
|
||||||
|
id: "albums",
|
||||||
|
index: 2,
|
||||||
|
count: 2,
|
||||||
|
});
|
||||||
|
expect(albums[0]).toEqual(getMetadataResult({
|
||||||
|
mediaCollection: [
|
||||||
|
container({ id: `album:${BOB_MARLEY.albums[0]!.id}`, title: BOB_MARLEY.albums[0]!.name }),
|
||||||
|
container({ id: `album:${BOB_MARLEY.albums[1]!.id}`, title: BOB_MARLEY.albums[1]!.name }),
|
||||||
|
],
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
"experimentalDecorators": true,
|
"experimentalDecorators": true,
|
||||||
"strictPropertyInitialization": false,
|
"strictPropertyInitialization": false,
|
||||||
"isolatedModules": false,
|
"isolatedModules": false,
|
||||||
"strict": false,
|
"strict": true,
|
||||||
"noImplicitAny": false,
|
"noImplicitAny": false,
|
||||||
"typeRoots" : [
|
"typeRoots" : [
|
||||||
"../node_modules/@types"
|
"../node_modules/@types"
|
||||||
|
|||||||
@@ -1987,6 +1987,11 @@ forwarded@~0.1.2:
|
|||||||
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
|
resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84"
|
||||||
integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=
|
integrity sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=
|
||||||
|
|
||||||
|
fp-ts@^2.9.5:
|
||||||
|
version "2.9.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/fp-ts/-/fp-ts-2.9.5.tgz#6690cd8b76b84214a38fc77cbbbd04a38f86ea90"
|
||||||
|
integrity sha512-MiHrA5teO6t8zKArE3DdMPT/Db6v2GUt5yfWnhBTrrsVfeCJUUnV6sgFvjGNBKDmEMqVwRFkEePL7wPwqrLKKA==
|
||||||
|
|
||||||
fragment-cache@^0.2.1:
|
fragment-cache@^0.2.1:
|
||||||
version "0.2.1"
|
version "0.2.1"
|
||||||
resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19"
|
resolved "https://registry.yarnpkg.com/fragment-cache/-/fragment-cache-0.2.1.tgz#4290fad27f13e89be7f33799c6bc5a0abfff0d19"
|
||||||
|
|||||||
Reference in New Issue
Block a user