diff --git a/package.json b/package.json index 9e5e6df..ff0003b 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "@svrooij/sonos": "^2.4.0", "@types/express": "^4.17.13", "@types/fs-extra": "^9.0.13", + "@types/jsonwebtoken": "^8.5.5", "@types/morgan": "^1.9.3", "@types/node": "^16.7.13", "@types/sharp": "^0.28.6", @@ -20,6 +21,7 @@ "express": "^4.17.1", "fp-ts": "^2.11.1", "fs-extra": "^10.0.0", + "jsonwebtoken": "^8.5.1", "libxmljs2": "^0.28.0", "morgan": "^1.10.0", "node-html-parser": "^4.1.4", diff --git a/src/app.ts b/src/app.ts index 6bae963..bf3d4be 100644 --- a/src/app.ts +++ b/src/app.ts @@ -2,6 +2,7 @@ import path from "path"; import fs from "fs"; import server from "./server"; import logger from "./logger"; + import { appendMimeTypeToClientFor, axiosImageFetcher, @@ -9,13 +10,13 @@ import { DEFAULT, Subsonic, } from "./subsonic"; -import encryption from "./encryption"; import { InMemoryAccessTokens, sha256 } from "./access_tokens"; import { InMemoryLinkCodes } from "./link_codes"; import readConfig from "./config"; import sonos, { bonobService } from "./sonos"; import { MusicService } from "./music_service"; import { SystemClock } from "./clock"; +import { jwtTokenSigner } from "./encryption"; const config = readConfig(); @@ -40,7 +41,6 @@ const artistImageFetcher = config.subsonic.artistImageCache const subsonic = new Subsonic( config.subsonic.url, - encryption(config.secret), streamUserAgent, artistImageFetcher ); @@ -88,6 +88,7 @@ const app = server( applyContextPath: true, logRequests: true, version, + tokenSigner: jwtTokenSigner(config.secret) } ); diff --git a/src/encryption.ts b/src/encryption.ts index 7c2ebfb..21b4a33 100644 --- a/src/encryption.ts +++ b/src/encryption.ts @@ -1,33 +1,89 @@ -import { createCipheriv, createDecipheriv, randomBytes, createHash } from "crypto"; +import { + createCipheriv, + createDecipheriv, + randomBytes, + createHash, +} from "crypto"; +import jwt from "jsonwebtoken"; -const ALGORITHM = "aes-256-cbc" +const ALGORITHM = "aes-256-cbc"; const IV = randomBytes(16); +export type Signer = { + sign: (value: string) => string; + verify: (token: string) => string; +}; + +export const pSigner = (signer: Signer) => ({ + sign: (value: string): Promise => { + return new Promise((resolve, reject) => { + try { + return resolve(signer.sign(value)); + } catch(e) { + reject(`Failed to sign value: ${e}`); + } + }); + }, + verify: (token: string): Promise => { + return new Promise((resolve, reject) => { + try { + return resolve(signer.verify(token)); + }catch(e) { + reject(`Failed to verify value: ${e}`); + } + }); + } + }); + +export const jwtTokenSigner = (secret: string) => ({ + sign: (value: string) => jwt.sign(value, secret), + verify: (token: string) => { + try { + return jwt.verify(token, secret) as string; + } catch (e) { + throw `Failed to decode jwt, try re-authorising account`; + } + }, +}); + export type Hash = { - iv: string, - encryptedData: string -} + iv: string; + encryptedData: string; +}; export type Encryption = { - encrypt: (value:string) => Hash - decrypt: (hash: Hash) => string -} + encrypt: (value: string) => Hash; + decrypt: (hash: Hash) => string; +}; const encryption = (secret: string): Encryption => { - const key = createHash('sha256').update(String(secret)).digest('base64').substr(0, 32); + const key = createHash("sha256") + .update(String(secret)) + .digest("base64") + .substr(0, 32); return { encrypt: (value: string) => { const cipher = createCipheriv(ALGORITHM, key, IV); - return { - iv: IV.toString("hex"), - encryptedData: Buffer.concat([cipher.update(value), cipher.final()]).toString("hex") + return { + iv: IV.toString("hex"), + encryptedData: Buffer.concat([ + cipher.update(value), + cipher.final(), + ]).toString("hex"), }; }, decrypt: (hash: Hash) => { - const decipher = createDecipheriv(ALGORITHM, key, Buffer.from(hash.iv, 'hex')); - return Buffer.concat([decipher.update(Buffer.from(hash.encryptedData, 'hex')), decipher.final()]).toString(); - } - } -} + const decipher = createDecipheriv( + ALGORITHM, + key, + Buffer.from(hash.iv, "hex") + ); + return Buffer.concat([ + decipher.update(Buffer.from(hash.encryptedData, "hex")), + decipher.final(), + ]).toString(); + }, + }; +}; export default encryption; diff --git a/src/server.ts b/src/server.ts index 14587b9..0b79cc9 100644 --- a/src/server.ts +++ b/src/server.ts @@ -34,6 +34,7 @@ import { Icon, ICONS, festivals, features } from "./icon"; import _, { shuffle } from "underscore"; import morgan from "morgan"; import { takeWithRepeats } from "./utils"; +import { jwtTokenSigner, Signer } from "./encryption"; export const BONOB_ACCESS_TOKEN_HEADER = "bat"; @@ -85,6 +86,7 @@ export type ServerOpts = { applyContextPath: boolean; logRequests: boolean; version: string; + tokenSigner: Signer; }; const DEFAULT_SERVER_OPTS: ServerOpts = { @@ -95,6 +97,7 @@ const DEFAULT_SERVER_OPTS: ServerOpts = { applyContextPath: true, logRequests: false, version: "v?", + tokenSigner: jwtTokenSigner(`bonob-${uuid()}`), }; function server( @@ -585,7 +588,8 @@ function server( musicService, accessTokens, clock, - i8n + i8n, + serverOpts.tokenSigner ); if (serverOpts.applyContextPath) { diff --git a/src/smapi.ts b/src/smapi.ts index c3f06c8..49cb9b1 100644 --- a/src/smapi.ts +++ b/src/smapi.ts @@ -24,6 +24,7 @@ import { URLBuilder } from "./url_builder"; import { asLANGs, I8N } from "./i8n"; import { ICON, iconForGenre } from "./icon"; import { uniq } from "underscore"; +import { pSigner, Signer } from "./encryption"; export const LOGIN_ROUTE = "/login"; export const CREATE_REGISTRATION_ROUTE = "/registration/add"; @@ -145,10 +146,12 @@ export function searchResult( class SonosSoap { linkCodes: LinkCodes; bonobUrl: URLBuilder; + tokenSigner: Signer - constructor(bonobUrl: URLBuilder, linkCodes: LinkCodes) { + constructor(bonobUrl: URLBuilder, linkCodes: LinkCodes, tokenSigner: Signer) { this.bonobUrl = bonobUrl; this.linkCodes = linkCodes; + this.tokenSigner = tokenSigner } getAppLink(): GetAppLinkResult { @@ -179,7 +182,7 @@ class SonosSoap { if (association) { return { getDeviceAuthTokenResult: { - authToken: association.authToken, + authToken: this.tokenSigner.sign(association.authToken), privateKey: "", userInfo: { nickname: association.nickname, @@ -321,39 +324,6 @@ export const artist = (bonobUrl: URLBuilder, artist: ArtistSummary) => ({ albumArtURI: defaultArtistArtURI(bonobUrl, artist).href(), }); -const auth = async ( - musicService: MusicService, - accessTokens: AccessTokens, - credentials?: Credentials -) => { - if (!credentials) { - throw { - Fault: { - faultcode: "Client.LoginUnsupported", - faultstring: "Missing credentials...", - }, - }; - } - const authToken = credentials.loginToken.token; - const accessToken = accessTokens.mint(authToken); - - return musicService - .login(authToken) - .then((musicLibrary) => ({ - musicLibrary, - authToken, - accessToken, - })) - .catch((_) => { - throw { - Fault: { - faultcode: "Client.LoginUnauthorized", - faultstring: "Credentials not found...", - }, - }; - }); -}; - function splitId(id: string) { const [type, typeId] = id.split(":"); return (t: T) => ({ @@ -375,9 +345,10 @@ function bindSmapiSoapServiceToExpress( musicService: MusicService, accessTokens: AccessTokens, clock: Clock, - i8n: I8N + i8n: I8N, + tokenSigner: Signer, ) { - const sonosSoap = new SonosSoap(bonobUrl, linkCodes); + const sonosSoap = new SonosSoap(bonobUrl, linkCodes, tokenSigner); const urlWithToken = (accessToken: string) => bonobUrl.append({ @@ -386,6 +357,32 @@ function bindSmapiSoapServiceToExpress( }, }); + const auth = async ( + credentials?: Credentials + ) => { + if (!credentials) { + throw { + Fault: { + faultcode: "Client.LoginUnsupported", + faultstring: "Missing credentials...", + }, + }; + } + + return pSigner(tokenSigner) + .verify(credentials.loginToken.token) + .then(authToken => ({ authToken, accessToken: accessTokens.mint(authToken) })) + .then((tokens) => musicService.login(tokens.authToken).then(musicLibrary => ({ ...tokens, musicLibrary }))) + .catch((_) => { + throw { + Fault: { + faultcode: "Client.LoginUnauthorized", + faultstring: "Failed to authenticate, try Reauthorising your account in the sonos app", + }, + }; + }); + }; + const soapyService = listen( app, soapPath, @@ -408,7 +405,7 @@ function bindSmapiSoapServiceToExpress( _, soapyHeaders: SoapyHeaders ) => - auth(musicService, accessTokens, soapyHeaders?.credentials) + auth(soapyHeaders?.credentials) .then(splitId(id)) .then(({ accessToken, type, typeId }) => ({ getMediaURIResult: bonobUrl @@ -423,7 +420,7 @@ function bindSmapiSoapServiceToExpress( _, soapyHeaders: SoapyHeaders ) => - auth(musicService, accessTokens, soapyHeaders?.credentials) + auth(soapyHeaders?.credentials) .then(splitId(id)) .then(async ({ musicLibrary, accessToken, typeId }) => musicLibrary.track(typeId!).then((it) => ({ @@ -435,7 +432,7 @@ function bindSmapiSoapServiceToExpress( _, soapyHeaders: SoapyHeaders ) => - auth(musicService, accessTokens, soapyHeaders?.credentials) + auth(soapyHeaders?.credentials) .then(splitId(id)) .then(async ({ musicLibrary, accessToken }) => { switch (id) { @@ -480,7 +477,7 @@ function bindSmapiSoapServiceToExpress( _, soapyHeaders: SoapyHeaders ) => - auth(musicService, accessTokens, soapyHeaders?.credentials) + auth(soapyHeaders?.credentials) .then(splitId(id)) .then(async ({ musicLibrary, accessToken, type, typeId }) => { const paging = { _index: index, _count: count }; @@ -552,7 +549,7 @@ function bindSmapiSoapServiceToExpress( soapyHeaders: SoapyHeaders, { headers }: Pick ) => - auth(musicService, accessTokens, soapyHeaders?.credentials) + auth(soapyHeaders?.credentials) .then(splitId(id)) .then(({ musicLibrary, accessToken, type, typeId }) => { const paging = { _index: index, _count: count }; @@ -825,7 +822,7 @@ function bindSmapiSoapServiceToExpress( _, soapyHeaders: SoapyHeaders ) => - auth(musicService, accessTokens, soapyHeaders?.credentials) + auth(soapyHeaders?.credentials) .then(({ musicLibrary }) => musicLibrary .createPlaylist(title) @@ -851,7 +848,7 @@ function bindSmapiSoapServiceToExpress( _, soapyHeaders: SoapyHeaders ) => - auth(musicService, accessTokens, soapyHeaders?.credentials) + auth(soapyHeaders?.credentials) .then(({ musicLibrary }) => musicLibrary.deletePlaylist(id)) .then((_) => ({ deleteContainerResult: {} })), addToContainer: async ( @@ -859,7 +856,7 @@ function bindSmapiSoapServiceToExpress( _, soapyHeaders: SoapyHeaders ) => - auth(musicService, accessTokens, soapyHeaders?.credentials) + auth(soapyHeaders?.credentials) .then(splitId(id)) .then(({ musicLibrary, typeId }) => musicLibrary.addToPlaylist(parentId.split(":")[1]!, typeId) @@ -870,7 +867,7 @@ function bindSmapiSoapServiceToExpress( _, soapyHeaders: SoapyHeaders ) => - auth(musicService, accessTokens, soapyHeaders?.credentials) + auth(soapyHeaders?.credentials) .then(splitId(id)) .then((it) => ({ ...it, @@ -893,7 +890,7 @@ function bindSmapiSoapServiceToExpress( _, soapyHeaders: SoapyHeaders ) => - auth(musicService, accessTokens, soapyHeaders?.credentials) + auth(soapyHeaders?.credentials) .then(splitId(id)) .then(({ musicLibrary, typeId }) => musicLibrary.rate(typeId, ratingFromInt(Math.abs(rating))) @@ -905,7 +902,7 @@ function bindSmapiSoapServiceToExpress( _, soapyHeaders: SoapyHeaders ) => - auth(musicService, accessTokens, soapyHeaders?.credentials) + auth(soapyHeaders?.credentials) .then(splitId(id)) .then(({ musicLibrary, type, typeId }) => { switch (type) { diff --git a/src/subsonic.ts b/src/subsonic.ts index ec28ce1..31f8272 100644 --- a/src/subsonic.ts +++ b/src/subsonic.ts @@ -28,7 +28,6 @@ import fse from "fs-extra"; import path from "path"; import axios, { AxiosRequestConfig } from "axios"; -import { Encryption } from "./encryption"; import randomString from "./random_string"; import { b64Encode, b64Decode } from "./b64"; import logger from "./logger"; @@ -369,18 +368,15 @@ const AlbumQueryTypeToSubsonicType: Record = { export class Subsonic implements MusicService { url: string; - encryption: Encryption; streamClientApplication: StreamClientApplication; externalImageFetcher: ImageFetcher; constructor( url: string, - encryption: Encryption, streamClientApplication: StreamClientApplication = DEFAULT, externalImageFetcher: ImageFetcher = axiosImageFetcher ) { this.url = url; - this.encryption = encryption; this.streamClientApplication = streamClientApplication; this.externalImageFetcher = externalImageFetcher; } @@ -428,15 +424,14 @@ export class Subsonic implements MusicService { this.getJSON(credentials, "/rest/ping.view") .then(() => ({ authToken: b64Encode( - JSON.stringify(this.encryption.encrypt(JSON.stringify(credentials))) + JSON.stringify(credentials) ), userId: credentials.username, nickname: credentials.username, })) .catch((e) => ({ message: `${e}` })); - parseToken = (token: string): Credentials => - JSON.parse(this.encryption.decrypt(JSON.parse(b64Decode(token)))); + parseToken = (token: string): Credentials => JSON.parse(b64Decode(token)); getArtists = ( credentials: Credentials diff --git a/tests/builders.ts b/tests/builders.ts index 7d3b6de..2d29fda 100644 --- a/tests/builders.ts +++ b/tests/builders.ts @@ -1,7 +1,7 @@ import { SonosDevice } from "@svrooij/sonos/lib"; import { v4 as uuid } from "uuid"; -import { Credentials } from "../src/smapi"; +import { Credentials } from "../src/smapi"; import { Service, Device } from "../src/sonos"; import { Album, diff --git a/tests/smapi.test.ts b/tests/smapi.test.ts index ef6b772..b9466d4 100644 --- a/tests/smapi.test.ts +++ b/tests/smapi.test.ts @@ -26,6 +26,7 @@ import { sonosifyMimeType, ratingAsInt, ratingFromInt, + Credentials, } from "../src/smapi"; import { keys as i8nKeys } from '../src/i8n'; @@ -54,6 +55,7 @@ import { AccessTokens } from "../src/access_tokens"; import dayjs from "dayjs"; import url, { URLBuilder } from "../src/url_builder"; import { iconForGenre } from "../src/icon"; +import { jwtTokenSigner } from "../src/encryption"; const parseXML = (value: string) => new DOMParserImpl().parseFromString(value); @@ -550,7 +552,7 @@ describe("defaultArtistArtURI", () => { }); }); -describe("api", () => { +describe("wsdl api", () => { const musicService = { generateToken: jest.fn(), login: jest.fn(), @@ -595,6 +597,9 @@ describe("api", () => { [bonobUrlWithoutContextPath, bonobUrlWithContextPath].forEach((bonobUrl) => { describe(`bonob with url ${bonobUrl}`, () => { + const tokenSigner = jwtTokenSigner(`smapi-test-secret-${uuid()}`); + const jwtSign = tokenSigner.sign; + const authToken = `authToken-${uuid()}`; const accessToken = `accessToken-${uuid()}`; @@ -612,6 +617,7 @@ describe("api", () => { linkCodes: () => linkCodes as unknown as LinkCodes, accessTokens: () => accessTokens as unknown as AccessTokens, clock, + tokenSigner } ); @@ -674,7 +680,7 @@ describe("api", () => { expect(result[0]).toEqual({ getDeviceAuthTokenResult: { - authToken: association.authToken, + authToken: jwtSign(association.authToken), privateKey: "", userInfo: { nickname: association.nickname, @@ -743,48 +749,7 @@ describe("api", () => { }); describe("search", () => { - describe("when no credentials header provided", () => { - it("should return a fault of LoginUnsupported", async () => { - const ws = await createClientAsync(`${service.uri}?wsdl`, { - endpoint: service.uri, - httpClient: supersoap(server), - }); - - await ws - .getMetadataAsync({ id: "search", 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 LoginUnauthorized", async () => { - musicService.login.mockRejectedValue("fail!"); - - const ws = await createClientAsync(`${service.uri}?wsdl`, { - endpoint: service.uri, - httpClient: supersoap(server), - }); - - ws.addSoapHeader({ - credentials: someCredentials("someAuthToken"), - }); - await ws - .getMetadataAsync({ id: "search", 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...", - }); - }); - }); - }); + itShouldHandleInvalidCredentials((ws) => ws.getMetadataAsync({ id: "search", index: 0, count: 0 })); describe("when valid credentials are provided", () => { let ws: Client; @@ -797,7 +762,7 @@ describe("api", () => { endpoint: service.uri, httpClient: supersoap(server), }); - ws.addSoapHeader({ credentials: someCredentials(authToken) }); + ws.addSoapHeader({ credentials: someCredentials(jwtSign(authToken)) }); }); describe("searching for albums", () => { @@ -895,49 +860,61 @@ describe("api", () => { }); }); - describe("getMetadata", () => { - describe("when no credentials header provided", () => { - it("should return a fault of LoginUnsupported", async () => { - const ws = await createClientAsync(`${service.uri}?wsdl`, { - endpoint: service.uri, - httpClient: supersoap(server), - }); - - 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...", - }); - }); + async function itShouldReturnALoginUnsupported(action: (ws: Client) => Promise) { + it("should return a fault of LoginUnsupported", async () => { + const ws = await createClientAsync(`${service.uri}?wsdl`, { + endpoint: service.uri, + httpClient: supersoap(server), }); - }); + + await action(ws) + .then(() => fail("shouldnt get here")) + .catch((e: any) => { + expect(e.root.Envelope.Body.Fault).toEqual({ + faultcode: "Client.LoginUnsupported", + faultstring: "Missing credentials...", + }); + }); + }) + } + async function itShouldReturnAFaultOfLoginUnauthorized(credentials: Credentials, action: (ws: Client) => Promise) { + it("should return a fault of LoginUnauthorized", async () => { + musicService.login.mockRejectedValue("fail!"); + + const ws = await createClientAsync(`${service.uri}?wsdl`, { + endpoint: service.uri, + httpClient: supersoap(server), + }); + + ws.addSoapHeader({ + credentials, + }); + await action(ws) + .then(() => fail("shouldnt get here")) + .catch((e: any) => { + expect(e.root.Envelope.Body.Fault).toEqual({ + faultcode: "Client.LoginUnauthorized", + faultstring: "Failed to authenticate, try Reauthorising your account in the sonos app", + }); + }); + }); + } + + function itShouldHandleInvalidCredentials(action: (ws: Client) => Promise) { describe("when invalid credentials are provided", () => { - it("should return a fault of LoginUnauthorized", async () => { - musicService.login.mockRejectedValue("fail!"); - - const ws = await createClientAsync(`${service.uri}?wsdl`, { - endpoint: service.uri, - httpClient: supersoap(server), - }); - - ws.addSoapHeader({ - credentials: someCredentials("someAuthToken"), - }); - 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...", - }); - }); - }); + itShouldReturnALoginUnsupported(action); }); + describe("when invalid credentials are provided", () => { + itShouldReturnAFaultOfLoginUnauthorized(someCredentials(jwtSign("someAuthToken")), action); + }); + describe("when invalid jwt is provided", () => { + itShouldReturnAFaultOfLoginUnauthorized(someCredentials("not a jwt token"), action); + }); + } + + describe("getMetadata", () => { + itShouldHandleInvalidCredentials((ws) => ws.getMetadataAsync({ id: "root", index: 0, count: 0 })) describe("when valid credentials are provided", () => { let ws: Client; @@ -950,7 +927,7 @@ describe("api", () => { endpoint: service.uri, httpClient: supersoap(server), }); - ws.addSoapHeader({ credentials: someCredentials(authToken) }); + ws.addSoapHeader({ credentials: someCredentials(jwtSign(authToken)) }); }); describe("asking for the root container", () => { @@ -2263,48 +2240,7 @@ describe("api", () => { }); describe("getExtendedMetadata", () => { - describe("when no credentials header provided", () => { - it("should return a fault of LoginUnsupported", async () => { - const ws = await createClientAsync(`${service.uri}?wsdl`, { - endpoint: service.uri, - httpClient: supersoap(server), - }); - - await ws - .getExtendedMetadataAsync({ 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 LoginUnauthorized", async () => { - musicService.login.mockRejectedValue("booom!"); - - const ws = await createClientAsync(`${service.uri}?wsdl`, { - endpoint: service.uri, - httpClient: supersoap(server), - }); - - ws.addSoapHeader({ - credentials: someCredentials("someAuthToken"), - }); - await ws - .getExtendedMetadataAsync({ 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...", - }); - }); - }); - }); + itShouldHandleInvalidCredentials((ws) => ws.getExtendedMetadataAsync({ id: "root", index: 0, count: 0 })) describe("when valid credentials are provided", () => { let ws: Client; @@ -2317,7 +2253,7 @@ describe("api", () => { endpoint: service.uri, httpClient: supersoap(server), }); - ws.addSoapHeader({ credentials: someCredentials(authToken) }); + ws.addSoapHeader({ credentials: someCredentials(jwtSign(authToken)) }); }); describe("asking for an artist", () => { @@ -2620,48 +2556,7 @@ describe("api", () => { }); describe("getMediaURI", () => { - describe("when no credentials header provided", () => { - it("should return a fault of LoginUnsupported", async () => { - const ws = await createClientAsync(`${service.uri}?wsdl`, { - endpoint: service.uri, - httpClient: supersoap(server), - }); - - await ws - .getMediaURIAsync({ id: "track:123" }) - .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 LoginUnauthorized", async () => { - musicService.login.mockRejectedValue("Credentials not found"); - - const ws = await createClientAsync(`${service.uri}?wsdl`, { - endpoint: service.uri, - httpClient: supersoap(server), - }); - - ws.addSoapHeader({ - credentials: someCredentials("invalid token"), - }); - await ws - .getMediaURIAsync({ id: "track:123" }) - .then(() => fail("shouldnt get here")) - .catch((e: any) => { - expect(e.root.Envelope.Body.Fault).toEqual({ - faultcode: "Client.LoginUnauthorized", - faultstring: "Credentials not found...", - }); - }); - }); - }); + itShouldHandleInvalidCredentials((ws) => ws.getMediaURIAsync({ id: "track:123" })) describe("when valid credentials are provided", () => { let ws: Client; @@ -2674,7 +2569,7 @@ describe("api", () => { endpoint: service.uri, httpClient: supersoap(server), }); - ws.addSoapHeader({ credentials: someCredentials(authToken) }); + ws.addSoapHeader({ credentials: someCredentials(jwtSign(authToken)) }); }); describe("asking for a URI to stream a track", () => { @@ -2702,48 +2597,7 @@ describe("api", () => { }); describe("getMediaMetadata", () => { - describe("when no credentials header provided", () => { - it("should return a fault of LoginUnsupported", async () => { - const ws = await createClientAsync(`${service.uri}?wsdl`, { - endpoint: service.uri, - httpClient: supersoap(server), - }); - - await ws - .getMediaMetadataAsync({ id: "track:123" }) - .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 LoginUnauthorized", async () => { - musicService.login.mockRejectedValue("Credentials not found!!"); - - const ws = await createClientAsync(`${service.uri}?wsdl`, { - endpoint: service.uri, - httpClient: supersoap(server), - }); - - ws.addSoapHeader({ - credentials: someCredentials("some invalid token"), - }); - await ws - .getMediaMetadataAsync({ id: "track:123" }) - .then(() => fail("shouldnt get here")) - .catch((e: any) => { - expect(e.root.Envelope.Body.Fault).toEqual({ - faultcode: "Client.LoginUnauthorized", - faultstring: "Credentials not found...", - }); - }); - }); - }); + itShouldHandleInvalidCredentials((ws) => ws.getMediaMetadataAsync({ id: "track:123" })) describe("when valid credentials are provided", () => { let ws: Client; @@ -2759,7 +2613,7 @@ describe("api", () => { endpoint: service.uri, httpClient: supersoap(server), }); - ws.addSoapHeader({ credentials: someCredentials(authToken) }); + ws.addSoapHeader({ credentials: someCredentials(jwtSign(authToken)) }); }); describe("asking for media metadata for a track", () => { @@ -2795,65 +2649,69 @@ describe("api", () => { endpoint: service.uri, httpClient: supersoap(server), }); - ws.addSoapHeader({ credentials: someCredentials(authToken) }); + ws.addSoapHeader({ credentials: someCredentials(jwtSign(authToken)) }); }); - describe("with only a title", () => { - const title = "aNewPlaylist"; - const idOfNewPlaylist = uuid(); + itShouldHandleInvalidCredentials((ws) => ws.createContainerAsync({ title: "foobar" })) - it("should create a playlist", async () => { - musicLibrary.createPlaylist.mockResolvedValue({ - id: idOfNewPlaylist, - name: title, + describe("when valid credentials are provided", () => { + describe("with only a title", () => { + const title = "aNewPlaylist"; + const idOfNewPlaylist = uuid(); + + it("should create a playlist", async () => { + musicLibrary.createPlaylist.mockResolvedValue({ + id: idOfNewPlaylist, + name: title, + }); + + const result = await ws.createContainerAsync({ + title, + }); + + expect(result[0]).toEqual({ + createContainerResult: { + id: `playlist:${idOfNewPlaylist}`, + updateId: null, + }, + }); + expect(musicService.login).toHaveBeenCalledWith(authToken); + expect(accessTokens.mint).toHaveBeenCalledWith(authToken); + expect(musicLibrary.createPlaylist).toHaveBeenCalledWith(title); }); - - const result = await ws.createContainerAsync({ - title, - }); - - expect(result[0]).toEqual({ - createContainerResult: { - id: `playlist:${idOfNewPlaylist}`, - updateId: null, - }, - }); - expect(musicService.login).toHaveBeenCalledWith(authToken); - expect(accessTokens.mint).toHaveBeenCalledWith(authToken); - expect(musicLibrary.createPlaylist).toHaveBeenCalledWith(title); }); - }); - - describe("with a title and a seed track", () => { - const title = "aNewPlaylist2"; - const trackId = "track123"; - const idOfNewPlaylist = "playlistId"; - - it("should create a playlist with the track", async () => { - musicLibrary.createPlaylist.mockResolvedValue({ - id: idOfNewPlaylist, - name: title, + + describe("with a title and a seed track", () => { + const title = "aNewPlaylist2"; + const trackId = "track123"; + const idOfNewPlaylist = "playlistId"; + + it("should create a playlist with the track", async () => { + musicLibrary.createPlaylist.mockResolvedValue({ + id: idOfNewPlaylist, + name: title, + }); + musicLibrary.addToPlaylist.mockResolvedValue(true); + + const result = await ws.createContainerAsync({ + title, + seedId: `track:${trackId}`, + }); + + expect(result[0]).toEqual({ + createContainerResult: { + id: `playlist:${idOfNewPlaylist}`, + updateId: null, + }, + }); + expect(musicService.login).toHaveBeenCalledWith(authToken); + expect(accessTokens.mint).toHaveBeenCalledWith(authToken); + expect(musicLibrary.createPlaylist).toHaveBeenCalledWith(title); + expect(musicLibrary.addToPlaylist).toHaveBeenCalledWith( + idOfNewPlaylist, + trackId + ); }); - musicLibrary.addToPlaylist.mockResolvedValue(true); - - const result = await ws.createContainerAsync({ - title, - seedId: `track:${trackId}`, - }); - - expect(result[0]).toEqual({ - createContainerResult: { - id: `playlist:${idOfNewPlaylist}`, - updateId: null, - }, - }); - expect(musicService.login).toHaveBeenCalledWith(authToken); - expect(accessTokens.mint).toHaveBeenCalledWith(authToken); - expect(musicLibrary.createPlaylist).toHaveBeenCalledWith(title); - expect(musicLibrary.addToPlaylist).toHaveBeenCalledWith( - idOfNewPlaylist, - trackId - ); }); }); }); @@ -2871,20 +2729,24 @@ describe("api", () => { endpoint: service.uri, httpClient: supersoap(server), }); - ws.addSoapHeader({ credentials: someCredentials(authToken) }); + ws.addSoapHeader({ credentials: someCredentials(jwtSign(authToken)) }); }); - it("should delete the playlist", async () => { - musicLibrary.deletePlaylist.mockResolvedValue(true); + itShouldHandleInvalidCredentials((ws) => ws.deleteContainerAsync({ id: "foobar" })) - const result = await ws.deleteContainerAsync({ - id, + describe("when valid credentials are provided", () => { + it("should delete the playlist", async () => { + musicLibrary.deletePlaylist.mockResolvedValue(true); + + const result = await ws.deleteContainerAsync({ + id, + }); + + expect(result[0]).toEqual({ deleteContainerResult: null }); + expect(musicService.login).toHaveBeenCalledWith(authToken); + expect(accessTokens.mint).toHaveBeenCalledWith(authToken); + expect(musicLibrary.deletePlaylist).toHaveBeenCalledWith(id); }); - - expect(result[0]).toEqual({ deleteContainerResult: null }); - expect(musicService.login).toHaveBeenCalledWith(authToken); - expect(accessTokens.mint).toHaveBeenCalledWith(authToken); - expect(musicLibrary.deletePlaylist).toHaveBeenCalledWith(id); }); }); @@ -2902,26 +2764,30 @@ describe("api", () => { endpoint: service.uri, httpClient: supersoap(server), }); - ws.addSoapHeader({ credentials: someCredentials(authToken) }); + ws.addSoapHeader({ credentials: someCredentials(jwtSign(authToken)) }); }); - it("should delete the playlist", async () => { - musicLibrary.addToPlaylist.mockResolvedValue(true); + itShouldHandleInvalidCredentials((ws) => ws.addToContainerAsync({ id: "foobar", parentId: "parentId" })) - const result = await ws.addToContainerAsync({ - id: `track:${trackId}`, - parentId: `parent:${playlistId}`, + describe("when valid credentials are provided", () => { + it("should add the item to the playlist", async () => { + musicLibrary.addToPlaylist.mockResolvedValue(true); + + const result = await ws.addToContainerAsync({ + id: `track:${trackId}`, + parentId: `parent:${playlistId}`, + }); + + expect(result[0]).toEqual({ + addToContainerResult: { updateId: null }, + }); + expect(musicService.login).toHaveBeenCalledWith(authToken); + expect(accessTokens.mint).toHaveBeenCalledWith(authToken); + expect(musicLibrary.addToPlaylist).toHaveBeenCalledWith( + playlistId, + trackId + ); }); - - expect(result[0]).toEqual({ - addToContainerResult: { updateId: null }, - }); - expect(musicService.login).toHaveBeenCalledWith(authToken); - expect(accessTokens.mint).toHaveBeenCalledWith(authToken); - expect(musicLibrary.addToPlaylist).toHaveBeenCalledWith( - playlistId, - trackId - ); }); }); @@ -2936,72 +2802,79 @@ describe("api", () => { endpoint: service.uri, httpClient: supersoap(server), }); - ws.addSoapHeader({ credentials: someCredentials(authToken) }); + ws.addSoapHeader({ credentials: someCredentials(jwtSign(authToken)) }); }); - describe("removing tracks from a playlist", () => { - const playlistId = "parent123"; + itShouldHandleInvalidCredentials((ws) => ws.removeFromContainerAsync({ + id: `playlist:123`, + indices: `1,6,9`, + })); - it("should remove the track from playlist", async () => { - musicLibrary.removeFromPlaylist.mockResolvedValue(true); - - const result = await ws.removeFromContainerAsync({ - id: `playlist:${playlistId}`, - indices: `1,6,9`, + describe("when valid credentials are provided", () => { + describe("removing tracks from a playlist", () => { + const playlistId = "parent123"; + + it("should remove the track from playlist", async () => { + musicLibrary.removeFromPlaylist.mockResolvedValue(true); + + const result = await ws.removeFromContainerAsync({ + id: `playlist:${playlistId}`, + indices: `1,6,9`, + }); + + expect(result[0]).toEqual({ + removeFromContainerResult: { updateId: null }, + }); + expect(musicService.login).toHaveBeenCalledWith(authToken); + expect(accessTokens.mint).toHaveBeenCalledWith(authToken); + expect(musicLibrary.removeFromPlaylist).toHaveBeenCalledWith( + playlistId, + [1, 6, 9] + ); }); - - expect(result[0]).toEqual({ - removeFromContainerResult: { updateId: null }, - }); - expect(musicService.login).toHaveBeenCalledWith(authToken); - expect(accessTokens.mint).toHaveBeenCalledWith(authToken); - expect(musicLibrary.removeFromPlaylist).toHaveBeenCalledWith( - playlistId, - [1, 6, 9] - ); }); - }); - describe("removing a playlist", () => { - const playlist1 = aPlaylist({ id: "p1" }); - const playlist2 = aPlaylist({ id: "p2" }); - const playlist3 = aPlaylist({ id: "p3" }); - const playlist4 = aPlaylist({ id: "p4" }); - const playlist5 = aPlaylist({ id: "p5" }); - - it("should delete the playlist", async () => { - musicLibrary.playlists.mockResolvedValue([ - playlist1, - playlist2, - playlist3, - playlist4, - playlist5, - ]); - musicLibrary.deletePlaylist.mockResolvedValue(true); - - const result = await ws.removeFromContainerAsync({ - id: `playlists`, - indices: `0,2,4`, + describe("removing a playlist", () => { + const playlist1 = aPlaylist({ id: "p1" }); + const playlist2 = aPlaylist({ id: "p2" }); + const playlist3 = aPlaylist({ id: "p3" }); + const playlist4 = aPlaylist({ id: "p4" }); + const playlist5 = aPlaylist({ id: "p5" }); + + it("should delete the playlist", async () => { + musicLibrary.playlists.mockResolvedValue([ + playlist1, + playlist2, + playlist3, + playlist4, + playlist5, + ]); + musicLibrary.deletePlaylist.mockResolvedValue(true); + + const result = await ws.removeFromContainerAsync({ + id: `playlists`, + indices: `0,2,4`, + }); + + expect(result[0]).toEqual({ + removeFromContainerResult: { updateId: null }, + }); + expect(musicService.login).toHaveBeenCalledWith(authToken); + expect(accessTokens.mint).toHaveBeenCalledWith(authToken); + expect(musicLibrary.deletePlaylist).toHaveBeenCalledTimes(3); + expect(musicLibrary.deletePlaylist).toHaveBeenNthCalledWith( + 1, + playlist1.id + ); + expect(musicLibrary.deletePlaylist).toHaveBeenNthCalledWith( + 2, + playlist3.id + ); + expect(musicLibrary.deletePlaylist).toHaveBeenNthCalledWith( + 3, + playlist5.id + ); }); - - expect(result[0]).toEqual({ - removeFromContainerResult: { updateId: null }, - }); - expect(musicService.login).toHaveBeenCalledWith(authToken); - expect(accessTokens.mint).toHaveBeenCalledWith(authToken); - expect(musicLibrary.deletePlaylist).toHaveBeenCalledTimes(3); - expect(musicLibrary.deletePlaylist).toHaveBeenNthCalledWith( - 1, - playlist1.id - ); - expect(musicLibrary.deletePlaylist).toHaveBeenNthCalledWith( - 2, - playlist3.id - ); - expect(musicLibrary.deletePlaylist).toHaveBeenNthCalledWith( - 3, - playlist5.id - ); }); }); }); @@ -3017,51 +2890,57 @@ describe("api", () => { endpoint: service.uri, httpClient: supersoap(server), }); - ws.addSoapHeader({ credentials: someCredentials(authToken) }); + ws.addSoapHeader({ credentials: someCredentials(jwtSign(authToken)) }); }); - describe("rating a track with a positive rating value", () => { - const trackId = "123"; - const ratingIntValue = 31; + itShouldHandleInvalidCredentials((ws) => ws.rateItemAsync({ + id: `track:123`, + rating: 4, + })); - it("should give the track a love", async () => { - musicLibrary.rate.mockResolvedValue(true); - - const result = await ws.rateItemAsync({ - id: `track:${trackId}`, - rating: ratingIntValue, + describe("when valid credentials are provided", () => { + describe("rating a track with a positive rating value", () => { + const trackId = "123"; + const ratingIntValue = 31; + + it("should give the track a love", async () => { + musicLibrary.rate.mockResolvedValue(true); + + const result = await ws.rateItemAsync({ + id: `track:${trackId}`, + rating: ratingIntValue, + }); + + expect(result[0]).toEqual({ + rateItemResult: { shouldSkip: false }, + }); + expect(musicService.login).toHaveBeenCalledWith(authToken); + expect(accessTokens.mint).toHaveBeenCalledWith(authToken); + expect(musicLibrary.rate).toHaveBeenCalledWith(trackId, ratingFromInt(ratingIntValue)); }); - - expect(result[0]).toEqual({ - rateItemResult: { shouldSkip: false }, + }); + + describe("rating a track with a negative rating value", () => { + const trackId = "123"; + const ratingIntValue = -20; + + it("should give the track a love", async () => { + musicLibrary.rate.mockResolvedValue(true); + + const result = await ws.rateItemAsync({ + id: `track:${trackId}`, + rating: ratingIntValue, + }); + + expect(result[0]).toEqual({ + rateItemResult: { shouldSkip: false }, + }); + expect(musicService.login).toHaveBeenCalledWith(authToken); + expect(accessTokens.mint).toHaveBeenCalledWith(authToken); + expect(musicLibrary.rate).toHaveBeenCalledWith(trackId, ratingFromInt(Math.abs(ratingIntValue))); }); - expect(musicService.login).toHaveBeenCalledWith(authToken); - expect(accessTokens.mint).toHaveBeenCalledWith(authToken); - expect(musicLibrary.rate).toHaveBeenCalledWith(trackId, ratingFromInt(ratingIntValue)); }); }); - - describe("rating a track with a negative rating value", () => { - const trackId = "123"; - const ratingIntValue = -20; - - it("should give the track a love", async () => { - musicLibrary.rate.mockResolvedValue(true); - - const result = await ws.rateItemAsync({ - id: `track:${trackId}`, - rating: ratingIntValue, - }); - - expect(result[0]).toEqual({ - rateItemResult: { shouldSkip: false }, - }); - expect(musicService.login).toHaveBeenCalledWith(authToken); - expect(accessTokens.mint).toHaveBeenCalledWith(authToken); - expect(musicLibrary.rate).toHaveBeenCalledWith(trackId, ratingFromInt(Math.abs(ratingIntValue))); - }); - }); - }); describe("setPlayedSeconds", () => { @@ -3075,132 +2954,139 @@ describe("api", () => { endpoint: service.uri, httpClient: supersoap(server), }); - ws.addSoapHeader({ credentials: someCredentials(authToken) }); + ws.addSoapHeader({ credentials: someCredentials(jwtSign(authToken)) }); }); - describe("when id is for a track", () => { - const trackId = "123456"; + itShouldHandleInvalidCredentials((ws) => ws.setPlayedSecondsAsync({ + id: `track:123`, + seconds: `33`, + })); - function itShouldScroble({ - trackId, - secondsPlayed, - }: { - trackId: string; - secondsPlayed: number; - }) { - it("should scrobble", async () => { - musicLibrary.scrobble.mockResolvedValue(true); - - const result = await ws.setPlayedSecondsAsync({ - id: `track:${trackId}`, - seconds: `${secondsPlayed}`, + describe("when valid credentials are provided", () => { + describe("when id is for a track", () => { + const trackId = "123456"; + + function itShouldScroble({ + trackId, + secondsPlayed, + }: { + trackId: string; + secondsPlayed: number; + }) { + it("should scrobble", async () => { + musicLibrary.scrobble.mockResolvedValue(true); + + const result = await ws.setPlayedSecondsAsync({ + id: `track:${trackId}`, + seconds: `${secondsPlayed}`, + }); + + expect(result[0]).toEqual({ setPlayedSecondsResult: null }); + expect(musicService.login).toHaveBeenCalledWith(authToken); + expect(accessTokens.mint).toHaveBeenCalledWith(authToken); + expect(musicLibrary.track).toHaveBeenCalledWith(trackId); + expect(musicLibrary.scrobble).toHaveBeenCalledWith(trackId); + }); + } + + function itShouldNotScroble({ + trackId, + secondsPlayed, + }: { + trackId: string; + secondsPlayed: number; + }) { + it("should scrobble", async () => { + const result = await ws.setPlayedSecondsAsync({ + id: `track:${trackId}`, + seconds: `${secondsPlayed}`, + }); + + expect(result[0]).toEqual({ setPlayedSecondsResult: null }); + expect(musicService.login).toHaveBeenCalledWith(authToken); + expect(accessTokens.mint).toHaveBeenCalledWith(authToken); + expect(musicLibrary.track).toHaveBeenCalledWith(trackId); + expect(musicLibrary.scrobble).not.toHaveBeenCalled(); + }); + } + + describe("when the track length is 30 seconds", () => { + beforeEach(() => { + musicLibrary.track.mockResolvedValue( + aTrack({ id: trackId, duration: 30 }) + ); + }); + + describe("when the played length is 30 seconds", () => { + itShouldScroble({ trackId, secondsPlayed: 30 }); + }); + + describe("when the played length is > 30 seconds", () => { + itShouldScroble({ trackId, secondsPlayed: 90 }); + }); + + describe("when the played length is < 30 seconds", () => { + itShouldNotScroble({ trackId, secondsPlayed: 29 }); }); - - expect(result[0]).toEqual({ setPlayedSecondsResult: null }); - expect(musicService.login).toHaveBeenCalledWith(authToken); - expect(accessTokens.mint).toHaveBeenCalledWith(authToken); - expect(musicLibrary.track).toHaveBeenCalledWith(trackId); - expect(musicLibrary.scrobble).toHaveBeenCalledWith(trackId); }); - } - - function itShouldNotScroble({ - trackId, - secondsPlayed, - }: { - trackId: string; - secondsPlayed: number; - }) { - it("should scrobble", async () => { - const result = await ws.setPlayedSecondsAsync({ - id: `track:${trackId}`, - seconds: `${secondsPlayed}`, + + describe("when the track length is > 30 seconds", () => { + beforeEach(() => { + musicLibrary.track.mockResolvedValue( + aTrack({ id: trackId, duration: 31 }) + ); }); - + + describe("when the played length is 30 seconds", () => { + itShouldScroble({ trackId, secondsPlayed: 30 }); + }); + + describe("when the played length is > 30 seconds", () => { + itShouldScroble({ trackId, secondsPlayed: 90 }); + }); + + describe("when the played length is < 30 seconds", () => { + itShouldNotScroble({ trackId, secondsPlayed: 29 }); + }); + }); + + describe("when the track length is 29 seconds", () => { + beforeEach(() => { + musicLibrary.track.mockResolvedValue( + aTrack({ id: trackId, duration: 29 }) + ); + }); + + describe("when the played length is 29 seconds", () => { + itShouldScroble({ trackId, secondsPlayed: 30 }); + }); + + describe("when the played length is > 29 seconds", () => { + itShouldScroble({ trackId, secondsPlayed: 30 }); + }); + + describe("when the played length is 10 seconds", () => { + itShouldScroble({ trackId, secondsPlayed: 10 }); + }); + + describe("when the played length is < 10 seconds", () => { + itShouldNotScroble({ trackId, secondsPlayed: 9 }); + }); + }); + }); + + describe("when the id is for something that isnt a track", () => { + it("should not scrobble", async () => { + const result = await ws.setPlayedSecondsAsync({ + id: `album:666`, + seconds: "100", + }); + expect(result[0]).toEqual({ setPlayedSecondsResult: null }); expect(musicService.login).toHaveBeenCalledWith(authToken); expect(accessTokens.mint).toHaveBeenCalledWith(authToken); - expect(musicLibrary.track).toHaveBeenCalledWith(trackId); expect(musicLibrary.scrobble).not.toHaveBeenCalled(); }); - } - - describe("when the track length is 30 seconds", () => { - beforeEach(() => { - musicLibrary.track.mockResolvedValue( - aTrack({ id: trackId, duration: 30 }) - ); - }); - - describe("when the played length is 30 seconds", () => { - itShouldScroble({ trackId, secondsPlayed: 30 }); - }); - - describe("when the played length is > 30 seconds", () => { - itShouldScroble({ trackId, secondsPlayed: 90 }); - }); - - describe("when the played length is < 30 seconds", () => { - itShouldNotScroble({ trackId, secondsPlayed: 29 }); - }); - }); - - describe("when the track length is > 30 seconds", () => { - beforeEach(() => { - musicLibrary.track.mockResolvedValue( - aTrack({ id: trackId, duration: 31 }) - ); - }); - - describe("when the played length is 30 seconds", () => { - itShouldScroble({ trackId, secondsPlayed: 30 }); - }); - - describe("when the played length is > 30 seconds", () => { - itShouldScroble({ trackId, secondsPlayed: 90 }); - }); - - describe("when the played length is < 30 seconds", () => { - itShouldNotScroble({ trackId, secondsPlayed: 29 }); - }); - }); - - describe("when the track length is 29 seconds", () => { - beforeEach(() => { - musicLibrary.track.mockResolvedValue( - aTrack({ id: trackId, duration: 29 }) - ); - }); - - describe("when the played length is 29 seconds", () => { - itShouldScroble({ trackId, secondsPlayed: 30 }); - }); - - describe("when the played length is > 29 seconds", () => { - itShouldScroble({ trackId, secondsPlayed: 30 }); - }); - - describe("when the played length is 10 seconds", () => { - itShouldScroble({ trackId, secondsPlayed: 10 }); - }); - - describe("when the played length is < 10 seconds", () => { - itShouldNotScroble({ trackId, secondsPlayed: 9 }); - }); - }); - }); - - describe("when the id is for something that isnt a track", () => { - it("should not scrobble", async () => { - const result = await ws.setPlayedSecondsAsync({ - id: `album:666`, - seconds: "100", - }); - - expect(result[0]).toEqual({ setPlayedSecondsResult: null }); - expect(musicService.login).toHaveBeenCalledWith(authToken); - expect(accessTokens.mint).toHaveBeenCalledWith(authToken); - expect(musicLibrary.scrobble).not.toHaveBeenCalled(); }); }); }); diff --git a/tests/subsonic.test.ts b/tests/subsonic.test.ts index 2788667..78f1631 100644 --- a/tests/subsonic.test.ts +++ b/tests/subsonic.test.ts @@ -17,7 +17,6 @@ import { cachingImageFetcher, asTrack, } from "../src/subsonic"; -import encryption from "../src/encryption"; import axios from "axios"; jest.mock("axios"); @@ -593,7 +592,6 @@ describe("Subsonic", () => { const streamClientApplication = jest.fn(); const navidrome = new Subsonic( url, - encryption("secret"), streamClientApplication ); diff --git a/yarn.lock b/yarn.lock index bf704d5..e21d807 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1184,6 +1184,15 @@ __metadata: languageName: node linkType: hard +"@types/jsonwebtoken@npm:^8.5.5": + version: 8.5.5 + resolution: "@types/jsonwebtoken@npm:8.5.5" + dependencies: + "@types/node": "*" + checksum: 33c30354641bc7849be7507e9f48685b1f487e944321a932650eac6c247c85184667f5e207ccfcab0da8cb24bde93a8372c09cacf1849e976bbf2cb90b26ce90 + languageName: node + linkType: hard + "@types/keyv@npm:^3.1.1": version: 3.1.2 resolution: "@types/keyv@npm:3.1.2" @@ -1782,6 +1791,7 @@ __metadata: "@types/express": ^4.17.13 "@types/fs-extra": ^9.0.13 "@types/jest": ^27.0.1 + "@types/jsonwebtoken": ^8.5.5 "@types/mocha": ^9.0.0 "@types/morgan": ^1.9.3 "@types/node": ^16.7.13 @@ -1800,6 +1810,7 @@ __metadata: get-port: ^5.1.1 image-js: ^0.33.0 jest: ^27.1.0 + jsonwebtoken: ^8.5.1 libxmljs2: ^0.28.0 morgan: ^1.10.0 node-html-parser: ^4.1.4 @@ -1903,6 +1914,13 @@ __metadata: languageName: node linkType: hard +"buffer-equal-constant-time@npm:1.0.1": + version: 1.0.1 + resolution: "buffer-equal-constant-time@npm:1.0.1" + checksum: 80bb945f5d782a56f374b292770901065bad21420e34936ecbe949e57724b4a13874f735850dd1cc61f078773c4fb5493a41391e7bda40d1fa388d6bd80daaab + languageName: node + linkType: hard + "buffer-from@npm:^1.0.0": version: 1.1.1 resolution: "buffer-from@npm:1.1.1" @@ -2717,6 +2735,15 @@ __metadata: languageName: node linkType: hard +"ecdsa-sig-formatter@npm:1.0.11": + version: 1.0.11 + resolution: "ecdsa-sig-formatter@npm:1.0.11" + dependencies: + safe-buffer: ^5.0.1 + checksum: 207f9ab1c2669b8e65540bce29506134613dd5f122cccf1e6a560f4d63f2732d427d938f8481df175505aad94583bcb32c688737bb39a6df0625f903d6d93c03 + languageName: node + linkType: hard + "ee-first@npm:1.1.1": version: 1.1.1 resolution: "ee-first@npm:1.1.1" @@ -4641,6 +4668,45 @@ __metadata: languageName: node linkType: hard +"jsonwebtoken@npm:^8.5.1": + version: 8.5.1 + resolution: "jsonwebtoken@npm:8.5.1" + dependencies: + jws: ^3.2.2 + lodash.includes: ^4.3.0 + lodash.isboolean: ^3.0.3 + lodash.isinteger: ^4.0.4 + lodash.isnumber: ^3.0.3 + lodash.isplainobject: ^4.0.6 + lodash.isstring: ^4.0.1 + lodash.once: ^4.0.0 + ms: ^2.1.1 + semver: ^5.6.0 + checksum: 93c9e3f23c59b758ac88ba15f4e4753b3749dfce7a6f7c40fb86663128a1e282db085eec852d4e0cbca4cefdcd3a8275ee255dbd08fcad0df26ad9f6e4cc853a + languageName: node + linkType: hard + +"jwa@npm:^1.4.1": + version: 1.4.1 + resolution: "jwa@npm:1.4.1" + dependencies: + buffer-equal-constant-time: 1.0.1 + ecdsa-sig-formatter: 1.0.11 + safe-buffer: ^5.0.1 + checksum: ff30ea7c2dcc61f3ed2098d868bf89d43701605090c5b21b5544b512843ec6fd9e028381a4dda466cbcdb885c2d1150f7c62e7168394ee07941b4098e1035e2f + languageName: node + linkType: hard + +"jws@npm:^3.2.2": + version: 3.2.2 + resolution: "jws@npm:3.2.2" + dependencies: + jwa: ^1.4.1 + safe-buffer: ^5.0.1 + checksum: f0213fe5b79344c56cd443428d8f65c16bf842dc8cb8f5aed693e1e91d79c20741663ad6eff07a6d2c433d1831acc9814e8d7bada6a0471fbb91d09ceb2bf5c2 + languageName: node + linkType: hard + "keyv@npm:^3.0.0": version: 3.1.0 resolution: "keyv@npm:3.1.0" @@ -4710,6 +4776,55 @@ __metadata: languageName: node linkType: hard +"lodash.includes@npm:^4.3.0": + version: 4.3.0 + resolution: "lodash.includes@npm:4.3.0" + checksum: 71092c130515a67ab3bd928f57f6018434797c94def7f46aafa417771e455ce3a4834889f4267b17887d7f75297dfabd96231bf704fd2b8c5096dc4a913568b6 + languageName: node + linkType: hard + +"lodash.isboolean@npm:^3.0.3": + version: 3.0.3 + resolution: "lodash.isboolean@npm:3.0.3" + checksum: b70068b4a8b8837912b54052557b21fc4774174e3512ed3c5b94621e5aff5eb6c68089d0a386b7e801d679cd105d2e35417978a5e99071750aa2ed90bffd0250 + languageName: node + linkType: hard + +"lodash.isinteger@npm:^4.0.4": + version: 4.0.4 + resolution: "lodash.isinteger@npm:4.0.4" + checksum: 6034821b3fc61a2ffc34e7d5644bb50c5fd8f1c0121c554c21ac271911ee0c0502274852845005f8651d51e199ee2e0cfebfe40aaa49c7fe617f603a8a0b1691 + languageName: node + linkType: hard + +"lodash.isnumber@npm:^3.0.3": + version: 3.0.3 + resolution: "lodash.isnumber@npm:3.0.3" + checksum: 913784275b565346255e6ae6a6e30b760a0da70abc29f3e1f409081585875105138cda4a429ff02577e1bc0a7ae2a90e0a3079a37f3a04c3d6c5aaa532f4cab2 + languageName: node + linkType: hard + +"lodash.isplainobject@npm:^4.0.6": + version: 4.0.6 + resolution: "lodash.isplainobject@npm:4.0.6" + checksum: 29c6351f281e0d9a1d58f1a4c8f4400924b4c79f18dfc4613624d7d54784df07efaff97c1ff2659f3e085ecf4fff493300adc4837553104cef2634110b0d5337 + languageName: node + linkType: hard + +"lodash.isstring@npm:^4.0.1": + version: 4.0.1 + resolution: "lodash.isstring@npm:4.0.1" + checksum: eaac87ae9636848af08021083d796e2eea3d02e80082ab8a9955309569cb3a463ce97fd281d7dc119e402b2e7d8c54a23914b15d2fc7fff56461511dc8937ba0 + languageName: node + linkType: hard + +"lodash.once@npm:^4.0.0": + version: 4.1.1 + resolution: "lodash.once@npm:4.1.1" + checksum: d768fa9f9b4e1dc6453be99b753906f58990e0c45e7b2ca5a3b40a33111e5d17f6edf2f768786e2716af90a8e78f8f91431ab8435f761fef00f9b0c256f6d245 + languageName: node + linkType: hard + "lodash@npm:4.x, lodash@npm:^4.17.21, lodash@npm:^4.17.5, lodash@npm:^4.7.0": version: 4.17.21 resolution: "lodash@npm:4.17.21" @@ -6138,7 +6253,7 @@ resolve@^1.20.0: languageName: node linkType: hard -"semver@npm:^5.4.1, semver@npm:^5.7.1": +"semver@npm:^5.4.1, semver@npm:^5.6.0, semver@npm:^5.7.1": version: 5.7.1 resolution: "semver@npm:5.7.1" bin: