diff --git a/src/app.ts b/src/app.ts index c9f5a9b..970c18d 100644 --- a/src/app.ts +++ b/src/app.ts @@ -48,6 +48,7 @@ const subsonic = new Subsonic( const featureFlagAwareMusicService: MusicService = { generateToken: subsonic.generateToken, + refreshToken: subsonic.refreshToken, login: (serviceToken: string) => subsonic.login(serviceToken).then((library) => { return { diff --git a/src/music_service.ts b/src/music_service.ts index 5e2bd3e..d7a5065 100644 --- a/src/music_service.ts +++ b/src/music_service.ts @@ -1,27 +1,18 @@ import { BUrn } from "./burn"; +import { taskEither as TE } from "fp-ts"; export type Credentials = { username: string; password: string }; -export function isSuccess( - authResult: AuthSuccess | AuthFailure -): authResult is AuthSuccess { - return (authResult as AuthSuccess).serviceToken !== undefined; -} - -export function isFailure( - authResult: any | AuthFailure -): authResult is AuthFailure { - return (authResult as AuthFailure).message !== undefined; -} - export type AuthSuccess = { serviceToken: string; userId: string; nickname: string; }; -export type AuthFailure = { - message: string; +export class AuthFailure extends Error { + constructor(message: string) { + super(message); + } }; export type ArtistSummary = { @@ -155,7 +146,8 @@ export const asArtistAlbumPairs = (artists: Artist[]): [Artist, Album][] => ); export interface MusicService { - generateToken(credentials: Credentials): Promise; + generateToken(credentials: Credentials): TE.TaskEither; + refreshToken(serviceToken: string): TE.TaskEither; login(serviceToken: string): Promise; } diff --git a/src/server.ts b/src/server.ts index 8b06337..952da68 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,4 +1,4 @@ -import { either as E } from "fp-ts"; +import { either as E, taskEither as TE } from "fp-ts"; import express, { Express, Request } from "express"; import * as Eta from "eta"; import path from "path"; @@ -22,7 +22,7 @@ import { ratingAsInt, } from "./smapi"; import { LinkCodes, InMemoryLinkCodes } from "./link_codes"; -import { MusicService, isSuccess } from "./music_service"; +import { MusicService, AuthFailure, AuthSuccess } from "./music_service"; import bindSmapiSoapServiceToExpress from "./smapi"; import { APITokens, InMemoryAPITokens } from "./api_tokens"; import logger from "./logger"; @@ -36,7 +36,11 @@ import morgan from "morgan"; import { takeWithRepeats } from "./utils"; import { parse } from "./burn"; import { axiosImageFetcher, ImageFetcher } from "./subsonic"; -import { JWTSmapiLoginTokens, SmapiAuthTokens, smapiTokenFromString } from "./smapi_auth"; +import { + JWTSmapiLoginTokens, + SmapiAuthTokens, + smapiTokenFromString, +} from "./smapi_auth"; export const BONOB_ACCESS_TOKEN_HEADER = "bat"; @@ -233,33 +237,36 @@ function server( message: lang("invalidLinkCode"), }); } else { - return musicService - .generateToken({ + return pipe( + musicService.generateToken({ username, password, - }) - .then((authResult) => { - if (isSuccess(authResult)) { - linkCodes.associate(linkCode, authResult); - return res.render("success", { - lang, - message: lang("loginSuccessful"), - }); - } else { - return res.status(403).render("failure", { + }), + TE.match( + (e: AuthFailure) => ({ + status: 403, + template: "failure", + params: { lang, message: lang("loginFailed"), - cause: authResult.message, - }); + cause: e.message, + }, + }), + (success: AuthSuccess) => { + linkCodes.associate(linkCode, success); + return { + status: 200, + template: "success", + params: { + lang, + message: lang("loginSuccessful"), + }, + }; } - }) - .catch((e) => { - return res.status(403).render("failure", { - lang, - message: lang("loginFailed"), - cause: `Unexpected error occured - ${e}`, - }); - }); + ) + )().then(({ status, template, params }) => + res.status(status).render(template, params) + ); } }); @@ -371,22 +378,26 @@ function server( logger.info( `${trace} bnb<- ${req.method} ${req.path}?${JSON.stringify( req.query - )}, headers=${JSON.stringify({ ...req.headers, "authorization": "*****" })}` + )}, headers=${JSON.stringify({ ...req.headers, authorization: "*****" })}` ); const authHeader = E.fromNullable("Missing header"); const bearerToken = E.fromNullable("No Bearer token"); const serviceToken = pipe( authHeader(req.headers["authorization"] as string), - E.chain(authorization => pipe( - authorization.match(/Bearer (?.*)/), - bearerToken, - E.map(match => match[1]!) - )), - E.chain(bearerToken => pipe( - smapiAuthTokens.verify(smapiTokenFromString(bearerToken)), - E.mapLeft(_ => "Bearer token failed to verify") - )), + E.chain((authorization) => + pipe( + authorization.match(/Bearer (?.*)/), + bearerToken, + E.map((match) => match[1]!) + ) + ), + E.chain((bearerToken) => + pipe( + smapiAuthTokens.verify(smapiTokenFromString(bearerToken)), + E.mapLeft((_) => "Bearer token failed to verify") + ) + ), E.getOrElseW(() => undefined) ); diff --git a/src/smapi.ts b/src/smapi.ts index 405f89b..97f6630 100644 --- a/src/smapi.ts +++ b/src/smapi.ts @@ -3,7 +3,7 @@ import { Express, Request } from "express"; import { listen } from "soap"; import { readFileSync } from "fs"; import path from "path"; -import { option as O, either as E } from "fp-ts"; +import { option as O, either as E, taskEither as TE, task as T } from "fp-ts"; import { pipe } from "fp-ts/lib/function"; import logger from "./logger"; @@ -29,11 +29,12 @@ import { ICON, iconForGenre } from "./icon"; import _, { uniq } from "underscore"; import { BUrn, formatForURL } from "./burn"; import { - InvalidTokenError, - isSmapiRefreshTokenResultFault, + isExpiredTokenError, MissingLoginTokenError, SmapiAuthTokens, smapiTokenAsString, + SMAPI_FAULT_LOGIN_UNAUTHORIZED, + ToSmapiFault, } from "./smapi_auth"; export const LOGIN_ROUTE = "/login"; @@ -379,6 +380,16 @@ type SoapyHeaders = { credentials?: Credentials; }; +type Auth = { + serviceToken: string; + credentials: Credentials; + apiKey: string; +}; + +function isAuth(thing: any): thing is Auth { + return thing.serviceToken; +} + function bindSmapiSoapServiceToExpress( app: Express, soapPath: string, @@ -399,7 +410,7 @@ function bindSmapiSoapServiceToExpress( }, }); - const auth = (credentials?: Credentials) => { + const auth = (credentials?: Credentials): E.Either => { const credentialsFrom = E.fromNullable(new MissingLoginTokenError()); return pipe( credentialsFrom(credentials), @@ -424,21 +435,40 @@ function bindSmapiSoapServiceToExpress( }; const login = async (credentials?: Credentials) => { - const tokens = pipe( + const authOrFail = pipe( auth(credentials), - E.getOrElseW((e) => { - throw e.toSmapiFault(smapiAuthTokens); - }) + E.getOrElseW((fault) => fault) ); - - return musicService - .login(tokens.serviceToken) - .then((musicLibrary) => ({ ...tokens, musicLibrary })) - .catch((_) => { - throw new InvalidTokenError("Failed to login").toSmapiFault( - smapiAuthTokens - ); - }); + if (isAuth(authOrFail)) { + return musicService + .login(authOrFail.serviceToken) + .then((musicLibrary) => ({ ...authOrFail, musicLibrary })) + .catch((_) => { + throw SMAPI_FAULT_LOGIN_UNAUTHORIZED; + }); + } else if (isExpiredTokenError(authOrFail)) { + throw await pipe( + musicService.refreshToken(authOrFail.expiredToken), + TE.map((it) => smapiAuthTokens.issue(it.serviceToken)), + TE.map((newToken) => ({ + Fault: { + faultcode: "Client.TokenRefreshRequired", + faultstring: "Token has expired", + detail: { + refreshAuthTokenResult: { + authToken: newToken.token, + privateKey: newToken.key, + }, + }, + }, + })), + TE.getOrElse(() => + T.of(SMAPI_FAULT_LOGIN_UNAUTHORIZED) + ) + )(); + } else { + throw authOrFail.toSmapiFault(); + } }; const soapyService = listen( @@ -458,31 +488,34 @@ function bindSmapiSoapServiceToExpress( pollInterval: 60, }, }), - refreshAuthToken: async (_, _2, soapyHeaders: SoapyHeaders) => - pipe( - auth(soapyHeaders?.credentials), - E.map(({ serviceToken }) => smapiAuthTokens.issue(serviceToken)), - E.map((newToken) => ({ - authToken: newToken.token, - privateKey: newToken.key, - })), - E.orElse((fault) => - pipe( - fault.toSmapiFault(smapiAuthTokens), - E.fromPredicate(isSmapiRefreshTokenResultFault, (_) => fault), - E.map((it) => it.Fault.detail.refreshAuthTokenResult) - ) - ), - E.map((newToken) => ({ - refreshAuthTokenResult: { - authToken: newToken.authToken, - privateKey: newToken.privateKey, - }, - })), - E.getOrElseW((fault) => { - throw fault.toSmapiFault(smapiAuthTokens); - }) - ), + refreshAuthToken: async (_, _2, soapyHeaders: SoapyHeaders) => { + const serviceToken = pipe( + auth(soapyHeaders?.credentials), + E.fold( + (fault) => + isExpiredTokenError(fault) + ? E.right(fault.expiredToken) + : E.left(fault), + (creds) => E.right(creds.serviceToken) + ), + E.getOrElseW((fault) => { + throw fault.toSmapiFault(); + }) + ); + return pipe( + musicService.refreshToken(serviceToken), + TE.map((it) => smapiAuthTokens.issue(it.serviceToken)), + TE.map((it) => ({ + refreshAuthTokenResult: { + authToken: it.token, + privateKey: it.key, + }, + })), + TE.getOrElse((_) => { + throw SMAPI_FAULT_LOGIN_UNAUTHORIZED; + }) + )(); + }, getMediaURI: async ( { id }: { id: string }, _, diff --git a/src/smapi_auth.ts b/src/smapi_auth.ts index 53f811a..585bb65 100644 --- a/src/smapi_auth.ts +++ b/src/smapi_auth.ts @@ -1,17 +1,25 @@ -import { Either, left, right } from "fp-ts/lib/Either"; +import { either as E } from "fp-ts"; import jwt from "jsonwebtoken"; import { v4 as uuid } from "uuid"; import { b64Decode, b64Encode } from "./b64"; import { Clock } from "./clock"; -export type SmapiFault = { Fault: { faultcode: string, faultstring: string } } -export type SmapiRefreshTokenResultFault = SmapiFault & { Fault: { detail: { refreshAuthTokenResult: { authToken: string, privateKey: string } }} } +export type SmapiFault = { Fault: { faultcode: string; faultstring: string } }; +export type SmapiRefreshTokenResultFault = SmapiFault & { + Fault: { + detail: { + refreshAuthTokenResult: { authToken: string; privateKey: string }; + }; + }; +}; function isError(thing: any): thing is Error { - return thing.name && thing.message + return thing.name && thing.message; } -export function isSmapiRefreshTokenResultFault(fault: SmapiFault): fault is SmapiRefreshTokenResultFault { +export function isSmapiRefreshTokenResultFault( + fault: SmapiFault +): fault is SmapiRefreshTokenResultFault { return (fault.Fault as any).detail?.refreshAuthTokenResult != undefined; } @@ -21,9 +29,25 @@ export type SmapiToken = { }; export interface ToSmapiFault { - toSmapiFault(smapiAuthTokens: SmapiAuthTokens): SmapiFault + _tag: string; + toSmapiFault(): SmapiFault } +export const SMAPI_FAULT_LOGIN_UNAUTHORIZED = { + Fault: { + faultcode: "Client.LoginUnauthorized", + faultstring: + "Failed to authenticate, try Re-Authorising your account in the sonos app", + }, +}; + +export const SMAPI_FAULT_LOGIN_UNSUPPORTED = { + Fault: { + faultcode: "Client.LoginUnsupported", + faultstring: "Missing credentials...", + }, +}; + export class MissingLoginTokenError extends Error implements ToSmapiFault { _tag = "MissingLoginTokenError"; @@ -31,12 +55,7 @@ export class MissingLoginTokenError extends Error implements ToSmapiFault { super("Missing Login Token"); } - toSmapiFault = (_: SmapiAuthTokens) => ({ - Fault: { - faultcode: "Client.LoginUnsupported", - faultstring: "Missing credentials...", - }, - }) + toSmapiFault = () => SMAPI_FAULT_LOGIN_UNSUPPORTED; } @@ -47,66 +66,54 @@ export class InvalidTokenError extends Error implements ToSmapiFault { super(message); } - toSmapiFault = (_: SmapiAuthTokens) => ({ - Fault: { - faultcode: "Client.LoginUnauthorized", - faultstring: "Failed to authenticate, try Re-Authorising your account in the sonos app", - }, - }) -} - -export class ExpiredTokenError extends Error implements ToSmapiFault { - _tag = "ExpiredTokenError"; - serviceToken: string; - expiredAt: number; - - constructor(serviceToken: string, expiredAt: number) { - super("SMAPI token has expired"); - this.serviceToken = serviceToken; - this.expiredAt = expiredAt; - } - - toSmapiFault = (smapiAuthTokens: SmapiAuthTokens) => { - const newToken = smapiAuthTokens.issue(this.serviceToken) - return { - Fault: { - faultcode: "Client.TokenRefreshRequired", - faultstring: "Token has expired", - detail: { - refreshAuthTokenResult: { - authToken: newToken.token, - privateKey: newToken.key, - }, - }, - } - }; - } + toSmapiFault = () => SMAPI_FAULT_LOGIN_UNAUTHORIZED; } export function isExpiredTokenError(thing: any): thing is ExpiredTokenError { return thing._tag == "ExpiredTokenError"; } +export class ExpiredTokenError extends Error implements ToSmapiFault { + _tag = "ExpiredTokenError"; + expiredToken: string; + + constructor(expiredToken: string) { + super("SMAPI token has expired"); + this.expiredToken = expiredToken; + } + + toSmapiFault = () => ({ + Fault: { + faultcode: "Client.TokenRefreshRequired", + faultstring: "Token has expired", + }, + }); +} + export type SmapiAuthTokens = { issue: (serviceToken: string) => SmapiToken; - verify: (smapiToken: SmapiToken) => Either; + verify: (smapiToken: SmapiToken) => E.Either; }; type TokenExpiredError = { - name: string, - message: string, - expiredAt: number -} + name: string; + message: string; + expiredAt: number; +}; function isTokenExpiredError(thing: any): thing is TokenExpiredError { - return thing.name == 'TokenExpiredError'; + return thing.name == "TokenExpiredError"; } -export const smapiTokenAsString = (smapiToken: SmapiToken) => b64Encode(JSON.stringify({ - token: smapiToken.token, - key: smapiToken.key -})); -export const smapiTokenFromString = (smapiTokenString: string): SmapiToken => JSON.parse(b64Decode(smapiTokenString)); +export const smapiTokenAsString = (smapiToken: SmapiToken) => + b64Encode( + JSON.stringify({ + token: smapiToken.token, + key: smapiToken.key, + }) + ); +export const smapiTokenFromString = (smapiTokenString: string): SmapiToken => + JSON.parse(b64Decode(smapiTokenString)); export const SMAPI_TOKEN_VERSION = 2; @@ -117,7 +124,13 @@ export class JWTSmapiLoginTokens implements SmapiAuthTokens { private readonly version: number; private readonly keyGenerator: () => string; - constructor(clock: Clock, secret: string, expiresIn: string, keyGenerator: () => string = uuid, version: number = SMAPI_TOKEN_VERSION) { + constructor( + clock: Clock, + secret: string, + expiresIn: string, + keyGenerator: () => string = uuid, + version: number = SMAPI_TOKEN_VERSION + ) { this.clock = clock; this.secret = secret; this.expiresIn = expiresIn; @@ -137,17 +150,28 @@ export class JWTSmapiLoginTokens implements SmapiAuthTokens { }; }; - verify = (smapiToken: SmapiToken): Either => { + verify = (smapiToken: SmapiToken): E.Either => { try { - return right((jwt.verify(smapiToken.token, this.secret + this.version + smapiToken.key) as any).serviceToken); + return E.right( + ( + jwt.verify( + smapiToken.token, + this.secret + this.version + smapiToken.key + ) as any + ).serviceToken + ); } catch (e) { - if(isTokenExpiredError(e)) { - const serviceToken = ((jwt.verify(smapiToken.token, this.secret + this.version + smapiToken.key, { ignoreExpiration: true })) as any).serviceToken; - return left(new ExpiredTokenError(serviceToken, e.expiredAt)) - } else if(isError(e)) - return left(new InvalidTokenError(e.message)); - else - return left(new InvalidTokenError("Failed to verify token")) + if (isTokenExpiredError(e)) { + const serviceToken = ( + jwt.verify( + smapiToken.token, + this.secret + this.version + smapiToken.key, + { ignoreExpiration: true } + ) as any + ).serviceToken; + return E.left(new ExpiredTokenError(serviceToken)); + } else if (isError(e)) return E.left(new InvalidTokenError(e.message)); + else return E.left(new InvalidTokenError("Failed to verify token")); } }; } diff --git a/src/subsonic.ts b/src/subsonic.ts index 6bded74..2bfa3d9 100644 --- a/src/subsonic.ts +++ b/src/subsonic.ts @@ -1,4 +1,4 @@ -import { option as O } from "fp-ts"; +import { option as O, taskEither as TE } from "fp-ts"; import * as A from "fp-ts/Array"; import { ordString } from "fp-ts/lib/Ord"; import { pipe } from "fp-ts/lib/function"; @@ -19,6 +19,7 @@ import { Rating, AlbumQueryType, Artist, + AuthFailure, } from "./music_service"; import sharp from "sharp"; import _ from "underscore"; @@ -209,11 +210,11 @@ type GetStarredResponse = { }; export type PingResponse = { - status: string, - version: string, - type: string, - serverVersion: string -} + status: string; + version: string; + type: string; + serverVersion: string; +}; type Search3Response = SubsonicResponse & { searchResult3: { @@ -234,12 +235,13 @@ type IdName = { name: string; }; -const coverArtURN = (coverArt: string | undefined): BUrn | undefined => pipe( - coverArt, - O.fromNullable, - O.map((it: string) => ({ system: "subsonic", resource: `art:${it}` })), - O.getOrElseW(() => undefined) -) +const coverArtURN = (coverArt: string | undefined): BUrn | undefined => + pipe( + coverArt, + O.fromNullable, + O.map((it: string) => ({ system: "subsonic", resource: `art:${it}` })), + O.getOrElseW(() => undefined) + ); export const artistImageURN = ( spec: Partial<{ @@ -255,7 +257,7 @@ export const artistImageURN = ( if (deets.artistImageURL && isValidImage(deets.artistImageURL)) { return { system: "external", - resource: deets.artistImageURL + resource: deets.artistImageURL, }; } else if (artistIsInLibrary(deets.artistId)) { return { @@ -279,7 +281,9 @@ export const asTrack = (album: Album, song: song): Track => ({ artist: { id: song.artistId, name: song.artist ? song.artist : "?", - image: song.artistId ? artistImageURN({ artistId: song.artistId }) : undefined, + image: song.artistId + ? artistImageURN({ artistId: song.artistId }) + : undefined, }, rating: { love: song.starred != undefined, @@ -390,14 +394,21 @@ const AlbumQueryTypeToSubsonicType: Record = { const artistIsInLibrary = (artistId: string | undefined) => artistId != undefined && artistId != "-1"; -type SubsonicCredentials = Credentials & { type: string, bearer: string | undefined } +type SubsonicCredentials = Credentials & { + type: string; + bearer: string | undefined; +}; -export const asToken = (credentials: SubsonicCredentials) => b64Encode(JSON.stringify(credentials)) -export const parseToken = (token: string): SubsonicCredentials => JSON.parse(b64Decode(token)); +export const asToken = (credentials: SubsonicCredentials) => + b64Encode(JSON.stringify(credentials)); +export const parseToken = (token: string): SubsonicCredentials => + JSON.parse(b64Decode(token)); interface SubsonicMusicLibrary extends MusicLibrary { - flavour(): string - bearerToken(): Promise + flavour(): string; + bearerToken( + credentials: Credentials + ): TE.TaskEither; } export class Subsonic implements MusicService { @@ -457,16 +468,40 @@ export class Subsonic implements MusicService { else return json as unknown as T; }); - generateToken = async (credentials: Credentials) => - this.getJSON(credentials, "/rest/ping.view") - .then(({ type }) => this.libraryFor({ ...credentials, type }).then(library => ({ type, library }))) - .then(({ library, type }) => library.bearerToken().then(bearer => ({ bearer, type }))) - .then(({ bearer, type }) => ({ + generateToken = (credentials: Credentials) => + pipe( + TE.tryCatch( + () => + this.getJSON( + _.pick(credentials, "username", "password"), + "/rest/ping.view" + ), + (e) => new AuthFailure(e as string) + ), + TE.chain(({ type }) => + pipe( + TE.tryCatch( + () => this.libraryFor({ ...credentials, type }), + () => new AuthFailure("Failed to get library") + ), + TE.map((library) => ({ type, library })) + ) + ), + TE.chain(({ library, type }) => + pipe( + library.bearerToken(credentials), + TE.map((bearer) => ({ bearer, type })) + ) + ), + TE.map(({ bearer, type }) => ({ serviceToken: asToken({ ...credentials, bearer, type }), userId: credentials.username, nickname: credentials.username, })) - .catch((e) => ({ message: `${e}` })); + ); + + refreshToken = (serviceToken: string) => + this.generateToken(parseToken(serviceToken)); getArtists = ( credentials: Credentials @@ -639,14 +674,16 @@ export class Subsonic implements MusicService { // albums: it.album.map(asAlbum), // })); - login = async (token: string) => this.libraryFor(parseToken(token)) + login = async (token: string) => this.libraryFor(parseToken(token)); - private libraryFor = (credentials: Credentials & { type: string }) => { + private libraryFor = ( + credentials: Credentials & { type: string } + ): Promise => { const subsonic = this; const genericSubsonic: SubsonicMusicLibrary = { flavour: () => "subsonic", - bearerToken: () => Promise.resolve(undefined), + bearerToken: (_: Credentials) => TE.right(undefined), artists: (q: ArtistQuery): Promise> => subsonic .getArtists(credentials) @@ -766,13 +803,7 @@ export class Subsonic implements MusicService { Promise.resolve(coverArtURN) .then((it) => assertSystem(it, "subsonic")) .then((it) => it.resource.split(":")[1]!) - .then((it) => - subsonic.getCoverArt( - credentials, - it, - size - ) - ) + .then((it) => subsonic.getCoverArt(credentials, it, size)) .then((res) => ({ contentType: res.headers["content-type"], data: Buffer.from(res.data, "binary"), @@ -923,13 +954,25 @@ export class Subsonic implements MusicService { ), }; - if(credentials.type == "navidrome") { + if (credentials.type == "navidrome") { return Promise.resolve({ ...genericSubsonic, - flavour: ()=> "navidrome" - }) + flavour: () => "navidrome", + bearerToken: (credentials: Credentials) => + pipe( + TE.tryCatch( + () => + axios.post( + `${this.url}/auth/login`, + _.pick(credentials, "username", "password") + ), + () => new AuthFailure("Failed to get bearerToken") + ), + TE.map((it) => it.data.token as string | undefined) + ), + }); } else { return Promise.resolve(genericSubsonic); } - } + }; } diff --git a/tests/in_memory_music_service.test.ts b/tests/in_memory_music_service.test.ts index 67714fb..bddf417 100644 --- a/tests/in_memory_music_service.test.ts +++ b/tests/in_memory_music_service.test.ts @@ -1,6 +1,8 @@ +import { taskEither as TE } from "fp-ts"; +import { pipe } from "fp-ts/lib/function"; + import { InMemoryMusicService } from "./in_memory_music_service"; import { - AuthSuccess, MusicLibrary, artistToArtistSummary, albumToAlbumSummary, @@ -28,7 +30,10 @@ describe("InMemoryMusicService", () => { service.hasUser(credentials); - const token = (await service.generateToken(credentials)) as AuthSuccess; + const token = await pipe( + service.generateToken(credentials), + TE.getOrElse(e => { throw e }) + )(); expect(token.userId).toEqual(credentials.username); expect(token.nickname).toEqual(credentials.username); @@ -43,7 +48,10 @@ describe("InMemoryMusicService", () => { service.hasUser(credentials); - const token = (await service.generateToken(credentials)) as AuthSuccess; + const token = await pipe( + service.generateToken(credentials), + TE.getOrElse(e => { throw e }) + )(); service.clear(); @@ -62,7 +70,11 @@ describe("InMemoryMusicService", () => { service.hasUser(user); - const token = (await service.generateToken(user)) as AuthSuccess; + const token = await pipe( + service.generateToken(user), + TE.getOrElse(e => { throw e }) + )(); + musicLibrary = (await service.login(token.serviceToken)) as MusicLibrary; }); diff --git a/tests/in_memory_music_service.ts b/tests/in_memory_music_service.ts index 5ccac21..6d3a30a 100644 --- a/tests/in_memory_music_service.ts +++ b/tests/in_memory_music_service.ts @@ -1,4 +1,4 @@ -import { option as O } from "fp-ts"; +import { option as O, taskEither as TE } from "fp-ts"; import * as A from "fp-ts/Array"; import { fromEquals } from "fp-ts/lib/Eq"; import { pipe } from "fp-ts/lib/function"; @@ -34,23 +34,27 @@ export class InMemoryMusicService implements MusicService { generateToken({ username, password, - }: Credentials): Promise { + }: Credentials): TE.TaskEither { if ( username != undefined && password != undefined && this.users[username] == password ) { - return Promise.resolve({ + return TE.right({ serviceToken: b64Encode(JSON.stringify({ username, password })), userId: username, nickname: username, type: "in-memory" }); } else { - return Promise.resolve({ message: `Invalid user:${username}` }); + return TE.left(new AuthFailure(`Invalid user:${username}`)); } } + refreshToken(serviceToken: string): TE.TaskEither { + return this.generateToken(JSON.parse(b64Decode(serviceToken))) + } + login(serviceToken: string): Promise { const credentials = JSON.parse(b64Decode(serviceToken)) as Credentials; if (this.users[credentials.username] != credentials.password) diff --git a/tests/server.test.ts b/tests/server.test.ts index 9284ff7..2a49480 100644 --- a/tests/server.test.ts +++ b/tests/server.test.ts @@ -3,10 +3,10 @@ import dayjs from "dayjs"; import request from "supertest"; import Image from "image-js"; import fs from "fs"; -import { either as E } from "fp-ts"; +import { either as E, taskEither as TE } from "fp-ts"; import path from "path"; -import { MusicService } from "../src/music_service"; +import { AuthFailure, MusicService } from "../src/music_service"; import makeServer, { BONOB_ACCESS_TOKEN_HEADER, RangeBytesFromFilter, @@ -637,7 +637,7 @@ describe("server", () => { }; linkCodes.has.mockReturnValue(true); - musicService.generateToken.mockResolvedValue(authSuccess); + musicService.generateToken.mockReturnValue(TE.right(authSuccess)) linkCodes.associate.mockReturnValue(true); const res = await request(server) @@ -669,7 +669,7 @@ describe("server", () => { const message = `Invalid user:${username}`; linkCodes.has.mockReturnValue(true); - musicService.generateToken.mockResolvedValue({ message }); + musicService.generateToken.mockReturnValue(TE.left(new AuthFailure(message))) const res = await request(server) .post(bonobUrl.append({ pathname: "/login" }).pathname()) @@ -683,27 +683,6 @@ describe("server", () => { }); }); - describe("when an unexpected failure occurs", () => { - it("should return 403 with message", async () => { - const username = "userDoesntExist"; - const password = "password"; - const linkCode = uuid(); - - linkCodes.has.mockReturnValue(true); - musicService.generateToken.mockRejectedValue("BOOOOOOM"); - - const res = await request(server) - .post(bonobUrl.append({ pathname: "/login" }).pathname()) - .set("accept-language", acceptLanguage) - .type("form") - .send({ username, password, linkCode }) - .expect(403); - - expect(res.text).toContain(lang("loginFailed")); - expect(res.text).toContain('Unexpected error occured - BOOOOOOM'); - }); - }); - describe("when linkCode is invalid", () => { it("should return 400 with message", async () => { const username = "jane"; @@ -777,7 +756,7 @@ describe("server", () => { describe("when the Bearer token has expired", () => { it("should return a 401", async () => { - smapiAuthTokens.verify.mockReturnValue(E.left(new ExpiredTokenError(serviceToken, 0))) + smapiAuthTokens.verify.mockReturnValue(E.left(new ExpiredTokenError(serviceToken))) const res = await request(server).head( bonobUrl @@ -865,7 +844,7 @@ describe("server", () => { describe("when the Bearer token has expired", () => { it("should return a 401", async () => { - smapiAuthTokens.verify.mockReturnValue(E.left(new ExpiredTokenError(serviceToken, 0))) + smapiAuthTokens.verify.mockReturnValue(E.left(new ExpiredTokenError(serviceToken))) const res = await request(server) .get( diff --git a/tests/smapi.test.ts b/tests/smapi.test.ts index 4f1c981..63da529 100644 --- a/tests/smapi.test.ts +++ b/tests/smapi.test.ts @@ -2,7 +2,7 @@ import crypto from "crypto"; import request from "supertest"; import { Client, createClientAsync } from "soap"; import { v4 as uuid } from "uuid"; -import { either as E } from "fp-ts"; +import { either as E, taskEither as TE } from "fp-ts"; import { DOMParserImpl } from "xmldom-ts"; import * as xpath from "xpath-ts"; import { randomInt } from "crypto"; @@ -861,6 +861,7 @@ describe("defaultArtistArtURI", () => { describe("wsdl api", () => { const musicService = { generateToken: jest.fn(), + refreshToken: jest.fn(), login: jest.fn(), }; const linkCodes = { @@ -1079,10 +1080,11 @@ describe("wsdl api", () => { describe("when token has expired", () => { it("should return a refreshed auth token", async () => { - const oneDayAgo = clock.time.subtract(1, "d"); + const refreshedServiceToken = `refreshedServiceToken-${uuid()}` const newSmapiAuthToken = { token: `newToken-${uuid()}`, key: `newKey-${uuid()}` }; - smapiAuthTokens.verify.mockReturnValue(E.left(new ExpiredTokenError(serviceToken, oneDayAgo.unix()))); + smapiAuthTokens.verify.mockReturnValue(E.left(new ExpiredTokenError(serviceToken))); + musicService.refreshToken.mockReturnValue(TE.right({ serviceToken: refreshedServiceToken })); smapiAuthTokens.issue.mockReturnValue(newSmapiAuthToken); const ws = await createClientAsync(`${service.uri}?wsdl`, { @@ -1101,6 +1103,9 @@ describe("wsdl api", () => { privateKey: newSmapiAuthToken.key, }, }); + + expect(musicService.refreshToken).toHaveBeenCalledWith(serviceToken); + expect(smapiAuthTokens.issue).toHaveBeenCalledWith(refreshedServiceToken); }); }); @@ -1128,8 +1133,11 @@ describe("wsdl api", () => { describe("when existing auth token has not expired", () => { it("should return a refreshed auth token", async () => { + const refreshedServiceToken = `refreshedServiceToken-${uuid()}` const newSmapiAuthToken = { token: `newToken-${uuid()}`, key: `newKey-${uuid()}` }; + smapiAuthTokens.verify.mockReturnValue(E.right(serviceToken)); + musicService.refreshToken.mockReturnValue(TE.right({ serviceToken: refreshedServiceToken })); smapiAuthTokens.issue.mockReturnValue(newSmapiAuthToken); const ws = await createClientAsync(`${service.uri}?wsdl`, { @@ -1148,6 +1156,9 @@ describe("wsdl api", () => { privateKey: newSmapiAuthToken.key }, }); + + expect(musicService.refreshToken).toHaveBeenCalledWith(serviceToken); + expect(smapiAuthTokens.issue).toHaveBeenCalledWith(refreshedServiceToken); }); }); }); @@ -1325,13 +1336,14 @@ describe("wsdl api", () => { describe("when token has expired", () => { it("should return a fault of Client.TokenRefreshRequired with a refreshAuthTokenResult", async () => { - const expiry = dayjs().subtract(1, "d"); + const refreshedServiceToken = `refreshedServiceToken-${uuid()}` const newToken = { token: `newToken-${uuid()}`, key: `newKey-${uuid()}` }; - smapiAuthTokens.verify.mockReturnValue(E.left(new ExpiredTokenError(serviceToken, expiry.unix()))) + smapiAuthTokens.verify.mockReturnValue(E.left(new ExpiredTokenError(serviceToken))) + musicService.refreshToken.mockReturnValue(TE.right({ serviceToken: refreshedServiceToken })) smapiAuthTokens.issue.mockReturnValue(newToken) musicService.login.mockRejectedValue( "fail, should not call login!" @@ -1360,7 +1372,8 @@ describe("wsdl api", () => { }); expect(smapiAuthTokens.verify).toHaveBeenCalledWith(smapiAuthToken); - expect(smapiAuthTokens.issue).toHaveBeenCalledWith(serviceToken); + expect(musicService.refreshToken).toHaveBeenCalledWith(serviceToken); + expect(smapiAuthTokens.issue).toHaveBeenCalledWith(refreshedServiceToken); }); }); } diff --git a/tests/smapi_auth.test.ts b/tests/smapi_auth.test.ts index b3f518c..027a442 100644 --- a/tests/smapi_auth.test.ts +++ b/tests/smapi_auth.test.ts @@ -177,8 +177,7 @@ describe("auth", () => { expect(result).toEqual( E.left( new ExpiredTokenError( - authToken, - tokenIssuedAt.add(30, "seconds").unix() + authToken ) ) ); diff --git a/tests/subsonic.test.ts b/tests/subsonic.test.ts index 77f38c9..625f1bf 100644 --- a/tests/subsonic.test.ts +++ b/tests/subsonic.test.ts @@ -3,8 +3,8 @@ import { v4 as uuid } from "uuid"; import tmp from "tmp"; import fse from "fs-extra"; import path from "path"; -import { pipe } from "fp-ts/lib/function"; -import { option as O } from "fp-ts"; +import { pipe } from "fp-ts/lib/function"; +import { option as O, taskEither as TE, task as T, either as E } from "fp-ts"; import { isValidImage, @@ -36,7 +36,6 @@ jest.mock("randomstring"); import { Album, Artist, - AuthSuccess, albumToAlbumSummary, asArtistAlbumPairs, Track, @@ -47,6 +46,8 @@ import { Playlist, SimilarArtist, Rating, + Credentials, + AuthFailure, } from "../src/music_service"; import { aGenre, @@ -382,7 +383,7 @@ const subsonicOK = (body: any = {}) => ({ "subsonic-response": { status: "ok", version: "1.16.1", - type: "navidrome", + type: "subsonic", serverVersion: "0.45.1 (c55e6590)", ...body, }, @@ -536,7 +537,7 @@ const error = (code: string, message: string) => ({ "subsonic-response": { status: "failed", version: "1.16.1", - type: "navidrome", + type: "subsonic", serverVersion: "0.45.1 (c55e6590)", error: { code, message }, }, @@ -546,7 +547,7 @@ const EMPTY = { "subsonic-response": { status: "ok", version: "1.16.1", - type: "navidrome", + type: "subsonic", serverVersion: "0.45.1 (c55e6590)", }, }; @@ -555,7 +556,7 @@ const FAILURE = { "subsonic-response": { status: "failed", version: "1.16.1", - type: "navidrome", + type: "subsonic", serverVersion: "0.45.1 (c55e6590)", error: { code: 10, message: 'Missing required parameter "v"' }, }, @@ -567,7 +568,7 @@ const pingJson = (pingResponse: Partial = {}) => ({ "subsonic-response": { status: "ok", version: "1.16.1", - type: "navidrome", + type: "subsonic", serverVersion: "0.45.1 (c55e6590)", ...pingResponse } @@ -688,12 +689,12 @@ describe("asTrack", () => { describe("Subsonic", () => { const url = "http://127.0.0.22:4567"; - const username = "user1"; - const password = "pass1"; + const username = `user1-${uuid()}`; + const password = `pass1-${uuid()}`; const salt = "saltysalty"; const streamClientApplication = jest.fn(); - const navidrome = new Subsonic( + const subsonic = new Subsonic( url, streamClientApplication ); @@ -730,46 +731,85 @@ describe("Subsonic", () => { "User-Agent": "bonob", }; + const tokenFor = (credentials: Credentials) => pipe( + subsonic.generateToken(credentials), + TE.fold(e => { throw e }, T.of) + ) + + const login = (credentials: Credentials) => tokenFor(credentials)() + .then((it) => subsonic.login(it.serviceToken)) + describe("generateToken", () => { describe("when the credentials are valid", () => { - it("should be able to generate a token and then login using it", async () => { - (axios.get as jest.Mock).mockResolvedValue(ok(PING_OK)); + describe("when the backend is generic subsonic", () => { + it("should be able to generate a token and then login using it", async () => { + (axios.get as jest.Mock).mockResolvedValue(ok(PING_OK)); + + const token = await tokenFor({ + username, + password, + })() + + expect(token.serviceToken).toBeDefined(); + expect(token.nickname).toEqual(username); + expect(token.userId).toEqual(username); + + expect(parseToken(token.serviceToken)).toEqual({ username, password, type: PING_OK["subsonic-response"].type }) + + expect(axios.get).toHaveBeenCalledWith(`${url}/rest/ping.view`, { + params: asURLSearchParams(authParamsPlusJson), + headers, + }); + }); - const token = (await navidrome.generateToken({ - username, - password, - })) as AuthSuccess; - - expect(token.serviceToken).toBeDefined(); - expect(token.nickname).toEqual(username); - expect(token.userId).toEqual(username); - - expect(parseToken(token.serviceToken)).toEqual({ username, password, type: PING_OK["subsonic-response"].type }) - - expect(axios.get).toHaveBeenCalledWith(`${url}/rest/ping.view`, { - params: asURLSearchParams(authParamsPlusJson), - headers, + it("should store the type of the subsonic server on the token", async () => { + const type = "someSubsonicClone"; + (axios.get as jest.Mock).mockResolvedValue(ok(pingJson({ type }))); + + const token = await tokenFor({ + username, + password, + })() + + expect(token.serviceToken).toBeDefined(); + expect(token.nickname).toEqual(username); + expect(token.userId).toEqual(username); + + expect(parseToken(token.serviceToken)).toEqual({ username, password, type }) + + expect(axios.get).toHaveBeenCalledWith(`${url}/rest/ping.view`, { + params: asURLSearchParams(authParamsPlusJson), + headers, + }); }); }); - it("should store the type of the subsonic server on the token", async () => { - const type = "someSubsonicClone"; - (axios.get as jest.Mock).mockResolvedValue(ok(pingJson({ type }))); + describe("when the backend is navidrome", () => { + it("should login to nd and get the nd bearer token", async () => { + const navidromeToken = `nd-${uuid()}`; - const token = (await navidrome.generateToken({ - username, - password, - })) as AuthSuccess; - - expect(token.serviceToken).toBeDefined(); - expect(token.nickname).toEqual(username); - expect(token.userId).toEqual(username); - - expect(parseToken(token.serviceToken)).toEqual({ username, password, type }) - - expect(axios.get).toHaveBeenCalledWith(`${url}/rest/ping.view`, { - params: asURLSearchParams(authParamsPlusJson), - headers, + (axios.get as jest.Mock).mockResolvedValue(ok(pingJson({ type: "navidrome" }))); + (axios.post as jest.Mock).mockResolvedValue(ok({ token: navidromeToken })); + + const token = await tokenFor({ + username, + password, + })() + + expect(token.serviceToken).toBeDefined(); + expect(token.nickname).toEqual(username); + expect(token.userId).toEqual(username); + + expect(parseToken(token.serviceToken)).toEqual({ username, password, type: "navidrome", bearer: navidromeToken }) + + expect(axios.get).toHaveBeenCalledWith(`${url}/rest/ping.view`, { + params: asURLSearchParams(authParamsPlusJson), + headers, + }); + expect(axios.post).toHaveBeenCalledWith(`${url}/auth/login`, { + username, + password, + }); }); }); }); @@ -781,32 +821,107 @@ describe("Subsonic", () => { data: error("40", "Wrong username or password"), }); - const token = await navidrome.generateToken({ username, password }); - expect(token).toEqual({ - message: "Subsonic error:Wrong username or password", + const token = await subsonic.generateToken({ username, password })(); + expect(token).toEqual(E.left(new AuthFailure("Subsonic error:Wrong username or password"))); + }); + }); + }); + + describe("refreshToken", () => { + describe("when the credentials are valid", () => { + describe("when the backend is generic subsonic", () => { + it("should be able to generate a token and then login using it", async () => { + const type = `subsonic-clone-${uuid()}`; + (axios.get as jest.Mock).mockResolvedValue(ok(pingJson({ type }))); + + const credentials = { username, password, type: "foo", bearer: undefined }; + const originalToken = asToken(credentials) + + const refreshedToken = await pipe( + subsonic.refreshToken(originalToken), + TE.fold(e => { throw e }, T.of) + )(); + + expect(refreshedToken.serviceToken).toBeDefined(); + expect(refreshedToken.nickname).toEqual(credentials.username); + expect(refreshedToken.userId).toEqual(credentials.username); + + expect(parseToken(refreshedToken.serviceToken)).toEqual({ username, password, type }) + + expect(axios.get).toHaveBeenCalledWith(`${url}/rest/ping.view`, { + params: asURLSearchParams(authParamsPlusJson), + headers, + }); }); }); + + describe("when the backend is navidrome", () => { + it("should login to nd and get the nd bearer token", async () => { + const navidromeToken = `nd-${uuid()}`; + + (axios.get as jest.Mock).mockResolvedValue(ok(pingJson({ type: "navidrome" }))); + (axios.post as jest.Mock).mockResolvedValue(ok({ token: navidromeToken })); + + const credentials = { username, password, type: "navidrome", bearer: undefined }; + const originalToken = asToken(credentials) + + const refreshedToken = await pipe( + subsonic.refreshToken(originalToken), + TE.fold(e => { throw e }, T.of) + )(); + + expect(refreshedToken.serviceToken).toBeDefined(); + expect(refreshedToken.nickname).toEqual(username); + expect(refreshedToken.userId).toEqual(username); + + expect(parseToken(refreshedToken.serviceToken)).toEqual({ username, password, type: "navidrome", bearer: navidromeToken }) + + expect(axios.get).toHaveBeenCalledWith(`${url}/rest/ping.view`, { + params: asURLSearchParams(authParamsPlusJson), + headers, + }); + expect(axios.post).toHaveBeenCalledWith(`${url}/auth/login`, { + username, + password, + }); + }); + }); + }); + + describe("when the credentials are not valid", () => { + it("should be able to generate a token and then login using it", async () => { + (axios.get as jest.Mock).mockResolvedValue({ + status: 200, + data: error("40", "Wrong username or password"), + }); + + const credentials = { username, password, type: "foo", bearer: undefined }; + const originalToken = asToken(credentials) + + const token = await subsonic.refreshToken(originalToken)(); + expect(token).toEqual(E.left(new AuthFailure("Subsonic error:Wrong username or password"))); + }); }); }); describe("login", () => { describe("when the token is for generic subsonic", () => { it("should return a subsonic client", async () => { - const client = await navidrome.login(asToken({ username: "foo", password: "bar", type: "subsonic", bearer: undefined })); + const client = await subsonic.login(asToken({ username: "foo", password: "bar", type: "subsonic", bearer: undefined })); expect(client.flavour()).toEqual("subsonic"); }); }); describe("when the token is for navidrome", () => { it("should return a navidrome client", async () => { - const client = await navidrome.login(asToken({ username: "foo", password: "bar", type: "navidrome", bearer: undefined })); + const client = await subsonic.login(asToken({ username: "foo", password: "bar", type: "navidrome", bearer: undefined })); expect(client.flavour()).toEqual("navidrome"); }); }); describe("when the token is for gonic", () => { it("should return a subsonic client", async () => { - const client = await navidrome.login(asToken({ username: "foo", password: "bar", type: "gonic", bearer: undefined })); + const client = await subsonic.login(asToken({ username: "foo", password: "bar", type: "gonic", bearer: undefined })); expect(client.flavour()).toEqual("subsonic"); }); }); @@ -821,10 +936,7 @@ describe("Subsonic", () => { }); it("should return empty array", async () => { - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.genres()); expect(result).toEqual([]); @@ -851,10 +963,7 @@ describe("Subsonic", () => { }); it("should return them alphabetically sorted", async () => { - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.genres()); expect(result).toEqual([{ id: b64Encode("genre1"), name: "genre1" }]); @@ -884,10 +993,7 @@ describe("Subsonic", () => { }); it("should return them alphabetically sorted", async () => { - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.genres()); expect(result).toEqual([ @@ -942,10 +1048,7 @@ describe("Subsonic", () => { }); it("should return the similar artists", async () => { - const result: Artist = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result: Artist = await login({ username, password }) .then((it) => it.artist(artist.id!)); expect(result).toEqual({ @@ -1004,10 +1107,7 @@ describe("Subsonic", () => { }); it("should return the similar artists", async () => { - const result: Artist = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result: Artist = await login({ username, password }) .then((it) => it.artist(artist.id!)); expect(result).toEqual({ @@ -1060,10 +1160,7 @@ describe("Subsonic", () => { }); it("should return the similar artists", async () => { - const result: Artist = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result: Artist = await login({ username, password }) .then((it) => it.artist(artist.id!)); expect(result).toEqual({ @@ -1114,10 +1211,7 @@ describe("Subsonic", () => { }); it("should return remove the dodgy looking image uris and return urn for artist:id", async () => { - const result: Artist = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result: Artist = await login({ username, password }) .then((it) => it.artist(artist.id!)); expect(result).toEqual({ @@ -1171,10 +1265,7 @@ describe("Subsonic", () => { }); it("should use the external url", async () => { - const result: Artist = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result: Artist = await login({ username, password }) .then((it) => it.artist(artist.id!)); expect(result).toEqual({ @@ -1225,10 +1316,7 @@ describe("Subsonic", () => { }); it("should use the external url", async () => { - const result: Artist = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result: Artist = await login({ username, password }) .then((it) => it.artist(artist.id!)); expect(result).toEqual({ @@ -1280,10 +1368,7 @@ describe("Subsonic", () => { }); it("should use the external url", async () => { - const result: Artist = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result: Artist = await login({ username, password }) .then((it) => it.artist(artist.id!)); expect(result).toEqual({ @@ -1336,10 +1421,7 @@ describe("Subsonic", () => { }); it("should return it", async () => { - const result: Artist = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result: Artist = await login({ username, password }) .then((it) => it.artist(artist.id!)); expect(result).toEqual({ @@ -1390,10 +1472,7 @@ describe("Subsonic", () => { }); it("should return it", async () => { - const result: Artist = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result: Artist = await login({ username, password }) .then((it) => it.artist(artist.id!)); expect(result).toEqual({ @@ -1442,10 +1521,7 @@ describe("Subsonic", () => { }); it("should return it", async () => { - const result: Artist = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result: Artist = await login({ username, password }) .then((it) => it.artist(artist.id!)); expect(result).toEqual({ @@ -1507,10 +1583,7 @@ describe("Subsonic", () => { }); it("should return empty", async () => { - const artists = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const artists = await login({ username, password }) .then((it) => it.artists({ _index: 0, _count: 100 })); expect(artists).toEqual({ @@ -1536,10 +1609,7 @@ describe("Subsonic", () => { }); it("should return empty", async () => { - const artists = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const artists = await login({ username, password }) .then((it) => it.artists({ _index: 0, _count: 100 })); expect(artists).toEqual({ @@ -1577,10 +1647,7 @@ describe("Subsonic", () => { }); it("should return the single artist", async () => { - const artists = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const artists = await login({ username, password }) .then((it) => it.artists({ _index: 0, _count: 100 })); const expectedResults = [{ @@ -1619,10 +1686,7 @@ describe("Subsonic", () => { }); it("should return all the artists", async () => { - const artists = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const artists = await login({ username, password }) .then((it) => it.artists({ _index: 0, _count: 100 })); const expectedResults = [artist1, artist2, artist3, artist4].map( @@ -1655,10 +1719,7 @@ describe("Subsonic", () => { }); it("should return only the correct page of artists", async () => { - const artists = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const artists = await login({ username, password }) .then((it) => it.artists({ _index: 1, _count: 2 })); const expectedResults = [artist2, artist3].map((it) => ({ @@ -1717,10 +1778,7 @@ describe("Subsonic", () => { genre: b64Encode("Pop"), type: "byGenre", }; - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.albums(q)); expect(result).toEqual({ @@ -1772,10 +1830,7 @@ describe("Subsonic", () => { _count: 100, type: "recentlyAdded", }; - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.albums(q)); expect(result).toEqual({ @@ -1826,10 +1881,7 @@ describe("Subsonic", () => { _count: 100, type: "recentlyPlayed", }; - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.albums(q)); expect(result).toEqual({ @@ -1871,10 +1923,7 @@ describe("Subsonic", () => { it("should pass the filter to navidrome", async () => { const q: AlbumQuery = { _index: 0, _count: 100, type: "mostPlayed" }; - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.albums(q)); expect(result).toEqual({ @@ -1916,10 +1965,7 @@ describe("Subsonic", () => { it("should pass the filter to navidrome", async () => { const q: AlbumQuery = { _index: 0, _count: 100, type: "starred" }; - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.albums(q)); expect(result).toEqual({ @@ -1970,10 +2016,7 @@ describe("Subsonic", () => { _count: 100, type: "alphabeticalByArtist", }; - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.albums(q)); expect(result).toEqual({ @@ -2023,10 +2066,7 @@ describe("Subsonic", () => { _count: 100, type: "alphabeticalByArtist", }; - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.albums(q)); expect(result).toEqual({ @@ -2091,10 +2131,7 @@ describe("Subsonic", () => { _count: 100, type: "alphabeticalByArtist", }; - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.albums(q)); expect(result).toEqual({ @@ -2145,10 +2182,7 @@ describe("Subsonic", () => { _count: 2, type: "alphabeticalByArtist", }; - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.albums(q)); expect(result).toEqual({ @@ -2221,10 +2255,7 @@ describe("Subsonic", () => { _count: 100, type: "alphabeticalByArtist", }; - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.albums(q)); expect(result).toEqual({ @@ -2281,10 +2312,7 @@ describe("Subsonic", () => { _count: 2, type: "alphabeticalByArtist", }; - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.albums(q)); expect(result).toEqual({ @@ -2340,10 +2368,7 @@ describe("Subsonic", () => { _count: 100, type: "alphabeticalByArtist", }; - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.albums(q)); expect(result).toEqual({ @@ -2409,10 +2434,7 @@ describe("Subsonic", () => { _count: 100, type: "alphabeticalByArtist", }; - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.albums(q)); expect(result).toEqual({ @@ -2476,10 +2498,7 @@ describe("Subsonic", () => { _count: 2, type: "alphabeticalByArtist", }; - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.albums(q)); expect(result).toEqual({ @@ -2541,10 +2560,7 @@ describe("Subsonic", () => { _count: 100, type: "alphabeticalByArtist", }; - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.albums(q)); expect(result).toEqual({ @@ -2599,10 +2615,7 @@ describe("Subsonic", () => { }); it("should return the album", async () => { - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.album(album.id)); expect(result).toEqual(album); @@ -2680,10 +2693,7 @@ describe("Subsonic", () => { }); it("should return the album", async () => { - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.tracks(album.id)); expect(result).toEqual([track1, track2, track3, track4]); @@ -2730,10 +2740,7 @@ describe("Subsonic", () => { }); it("should return the album", async () => { - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.tracks(album.id)); expect(result).toEqual([track]); @@ -2768,10 +2775,7 @@ describe("Subsonic", () => { }); it("should empty array", async () => { - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.tracks(album.id)); expect(result).toEqual([]); @@ -2819,10 +2823,7 @@ describe("Subsonic", () => { Promise.resolve(ok(getAlbumJson(artist, album, []))) ); - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.track(track.id)); expect(result).toEqual({ @@ -2869,10 +2870,7 @@ describe("Subsonic", () => { Promise.resolve(ok(getAlbumJson(artist, album, []))) ); - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.track(track.id)); expect(result).toEqual({ @@ -2944,10 +2942,7 @@ describe("Subsonic", () => { ) .mockImplementationOnce(() => Promise.resolve(streamResponse)); - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.stream({ trackId, range: undefined })); expect(result.headers).toEqual({ @@ -2986,10 +2981,7 @@ describe("Subsonic", () => { ) .mockImplementationOnce(() => Promise.resolve(streamResponse)); - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.stream({ trackId, range: undefined })); expect(result.headers).toEqual({ @@ -3030,10 +3022,7 @@ describe("Subsonic", () => { ) .mockImplementationOnce(() => Promise.resolve(streamResponse)); - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.stream({ trackId, range: undefined })); expect(result.headers).toEqual({ @@ -3079,10 +3068,7 @@ describe("Subsonic", () => { ) .mockImplementationOnce(() => Promise.resolve(streamResponse)); - const musicLibrary = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)); + const musicLibrary = await login({ username, password }); return expect( musicLibrary.stream({ trackId, range: undefined }) @@ -3104,10 +3090,7 @@ describe("Subsonic", () => { ) .mockImplementationOnce(() => Promise.reject("IO error occured")); - const musicLibrary = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)); + const musicLibrary = await login({ username, password }); return expect( musicLibrary.stream({ trackId, range: undefined }) @@ -3145,10 +3128,7 @@ describe("Subsonic", () => { ) .mockImplementationOnce(() => Promise.resolve(streamResponse)); - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.stream({ trackId, range })); expect(result.headers).toEqual({ @@ -3198,10 +3178,7 @@ describe("Subsonic", () => { ) .mockImplementationOnce(() => Promise.resolve(streamResponse)); - await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + await login({ username, password }) .then((it) => it.stream({ trackId, range: undefined })); expect(streamClientApplication).toHaveBeenCalledWith(track); @@ -3243,10 +3220,7 @@ describe("Subsonic", () => { ) .mockImplementationOnce(() => Promise.resolve(streamResponse)); - await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + await login({ username, password }) .then((it) => it.stream({ trackId, range })); expect(streamClientApplication).toHaveBeenCalledWith(track); @@ -3285,10 +3259,7 @@ describe("Subsonic", () => { .mockImplementationOnce(() => Promise.resolve(ok(PING_OK))) .mockImplementationOnce(() => Promise.resolve(streamResponse)); - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.coverArt(coverArtURN)); expect(result).toEqual({ @@ -3324,10 +3295,7 @@ describe("Subsonic", () => { .mockImplementationOnce(() => Promise.resolve(ok(PING_OK))) .mockImplementationOnce(() => Promise.resolve(streamResponse)); - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.coverArt(coverArtURN, size)); expect(result).toEqual({ @@ -3355,10 +3323,7 @@ describe("Subsonic", () => { .mockImplementationOnce(() => Promise.resolve(ok(PING_OK))) .mockImplementationOnce(() => Promise.reject("BOOOM")); - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.coverArt({ system: "external", resource: "http://localhost:404" }, size)); expect(result).toBeUndefined(); @@ -3374,10 +3339,7 @@ describe("Subsonic", () => { mockGET .mockImplementationOnce(() => Promise.resolve(ok(PING_OK))); - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.coverArt(covertArtURN, 190)); expect(result).toBeUndefined(); @@ -3401,10 +3363,7 @@ describe("Subsonic", () => { .mockImplementationOnce(() => Promise.resolve(ok(PING_OK))) .mockImplementationOnce(() => Promise.resolve(streamResponse)); - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.coverArt(covertArtURN)); expect(result).toEqual({ @@ -3434,10 +3393,7 @@ describe("Subsonic", () => { .mockImplementationOnce(() => Promise.resolve(ok(PING_OK))) .mockImplementationOnce(() => Promise.reject("BOOOM")); - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.coverArt(covertArtURN)); expect(result).toBeUndefined(); @@ -3464,10 +3420,7 @@ describe("Subsonic", () => { .mockImplementationOnce(() => Promise.resolve(ok(PING_OK))) .mockImplementationOnce(() => Promise.resolve(streamResponse)); - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.coverArt(covertArtURN, size)); expect(result).toEqual({ @@ -3498,10 +3451,7 @@ describe("Subsonic", () => { .mockImplementationOnce(() => Promise.resolve(ok(PING_OK))) .mockImplementationOnce(() => Promise.reject("BOOOM")); - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.coverArt(covertArtURN, size)); expect(result).toBeUndefined(); @@ -3515,10 +3465,7 @@ describe("Subsonic", () => { const trackId = uuid(); const rate = (trackId: string, rating: Rating) => - navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + login({ username, password }) .then((it) => it.rate(trackId, rating)); const artist = anArtist(); @@ -3763,10 +3710,7 @@ describe("Subsonic", () => { .mockImplementationOnce(() => Promise.resolve(ok(PING_OK))) .mockImplementationOnce(() => Promise.resolve(ok(EMPTY))); - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.scrobble(id)); expect(result).toEqual(true); @@ -3795,10 +3739,7 @@ describe("Subsonic", () => { }) ); - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.scrobble(id)); expect(result).toEqual(false); @@ -3824,10 +3765,7 @@ describe("Subsonic", () => { .mockImplementationOnce(() => Promise.resolve(ok(PING_OK))) .mockImplementationOnce(() => Promise.resolve(ok(EMPTY))); - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.nowPlaying(id)); expect(result).toEqual(true); @@ -3856,10 +3794,7 @@ describe("Subsonic", () => { }) ); - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.nowPlaying(id)); expect(result).toEqual(false); @@ -3887,10 +3822,7 @@ describe("Subsonic", () => { Promise.resolve(ok(getSearchResult3Json({ artists: [artist1] }))) ); - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.searchArtists("foo")); expect(result).toEqual([artistToArtistSummary(artist1)]); @@ -3921,10 +3853,7 @@ describe("Subsonic", () => { ) ); - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.searchArtists("foo")); expect(result).toEqual([ @@ -3953,10 +3882,7 @@ describe("Subsonic", () => { Promise.resolve(ok(getSearchResult3Json({ artists: [] }))) ); - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.searchArtists("foo")); expect(result).toEqual([]); @@ -3992,10 +3918,7 @@ describe("Subsonic", () => { ) ); - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.searchAlbums("foo")); expect(result).toEqual([albumToAlbumSummary(album)]); @@ -4042,10 +3965,7 @@ describe("Subsonic", () => { ) ); - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.searchAlbums("moo")); expect(result).toEqual([ @@ -4074,10 +3994,7 @@ describe("Subsonic", () => { Promise.resolve(ok(getSearchResult3Json({ albums: [] }))) ); - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.searchAlbums("foo")); expect(result).toEqual([]); @@ -4123,10 +4040,7 @@ describe("Subsonic", () => { Promise.resolve(ok(getAlbumJson(artist, album, []))) ); - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.searchTracks("foo")); expect(result).toEqual([track]); @@ -4198,10 +4112,7 @@ describe("Subsonic", () => { Promise.resolve(ok(getAlbumJson(artist2, album2, []))) ); - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.searchTracks("moo")); expect(result).toEqual([track1, track2]); @@ -4227,10 +4138,7 @@ describe("Subsonic", () => { Promise.resolve(ok(getSearchResult3Json({ tracks: [] }))) ); - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.searchTracks("foo")); expect(result).toEqual([]); @@ -4261,10 +4169,7 @@ describe("Subsonic", () => { Promise.resolve(ok(getPlayListsJson([playlist]))) ); - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.playlists()); expect(result).toEqual([playlist]); @@ -4289,10 +4194,7 @@ describe("Subsonic", () => { Promise.resolve(ok(getPlayListsJson(playlists))) ); - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.playlists()); expect(result).toEqual(playlists); @@ -4312,10 +4214,7 @@ describe("Subsonic", () => { Promise.resolve(ok(getPlayListsJson([]))) ); - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.playlists()); expect(result).toEqual([]); @@ -4340,10 +4239,7 @@ describe("Subsonic", () => { ); return expect( - navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + login({ username, password }) .then((it) => it.playlist(id)) ).rejects.toEqual("Subsonic error:data not found"); }); @@ -4396,10 +4292,7 @@ describe("Subsonic", () => { ) ); - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.playlist(id)); expect(result).toEqual({ @@ -4433,10 +4326,7 @@ describe("Subsonic", () => { Promise.resolve(ok(getPlayListJson(playlist))) ); - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.playlist(playlist.id)); expect(result).toEqual(playlist); @@ -4464,10 +4354,7 @@ describe("Subsonic", () => { Promise.resolve(ok(createPlayListJson({ id, name }))) ); - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.createPlaylist(name)); expect(result).toEqual({ id, name }); @@ -4491,10 +4378,7 @@ describe("Subsonic", () => { .mockImplementationOnce(() => Promise.resolve(ok(PING_OK))) .mockImplementationOnce(() => Promise.resolve(ok(EMPTY))); - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.deletePlaylist(id)); expect(result).toEqual(true); @@ -4519,11 +4403,8 @@ describe("Subsonic", () => { .mockImplementationOnce(() => Promise.resolve(ok(PING_OK))) .mockImplementationOnce(() => Promise.resolve(ok(EMPTY))); - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) - .then((it) => it.addToPlaylist(playlistId, trackId)); + const result = await login({ username, password }) + .then((it) => it.addToPlaylist(playlistId, trackId)); expect(result).toEqual(true); @@ -4547,11 +4428,8 @@ describe("Subsonic", () => { .mockImplementationOnce(() => Promise.resolve(ok(PING_OK))) .mockImplementationOnce(() => Promise.resolve(ok(EMPTY))); - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) - .then((it) => it.removeFromPlaylist(playlistId, indicies)); + const result = await login({ username, password }) + .then((it) => it.removeFromPlaylist(playlistId, indicies)); expect(result).toEqual(true); @@ -4597,10 +4475,7 @@ describe("Subsonic", () => { Promise.resolve(ok(getAlbumJson(artist1, album1, []))) ); - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.similarSongs(id)); expect(result).toEqual([track1]); @@ -4670,10 +4545,7 @@ describe("Subsonic", () => { Promise.resolve(ok(getAlbumJson(artist1, album1, []))) ); - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.similarSongs(id)); expect(result).toEqual([track1, track2, track3]); @@ -4700,10 +4572,7 @@ describe("Subsonic", () => { Promise.resolve(ok(getSimilarSongsJson([]))) ); - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.similarSongs(id)); expect(result).toEqual([]); @@ -4731,10 +4600,7 @@ describe("Subsonic", () => { ); return expect( - navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + login({ username, password }) .then((it) => it.similarSongs(id)) ).rejects.toEqual("Subsonic error:data not found"); }); @@ -4773,10 +4639,7 @@ describe("Subsonic", () => { Promise.resolve(ok(getAlbumJson(artist, album1, []))) ); - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.topSongs(artistId)); expect(result).toEqual([track1]); @@ -4843,10 +4706,7 @@ describe("Subsonic", () => { Promise.resolve(ok(getAlbumJson(artist, album1, []))) ); - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + const result = await login({ username, password }) .then((it) => it.topSongs(artistId)); expect(result).toEqual([track1, track2, track3]); @@ -4885,10 +4745,8 @@ describe("Subsonic", () => { Promise.resolve(ok(getTopSongsJson([]))) ); - const result = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.serviceToken)) + + const result = await login({ username, password }) .then((it) => it.topSongs(artistId)); expect(result).toEqual([]);