From 439c2eae872410dcb5004101a996d36a293ba901 Mon Sep 17 00:00:00 2001 From: simojenki Date: Sat, 13 Mar 2021 18:56:46 +1100 Subject: [PATCH] Add similar artists to Artist --- src/music_service.ts | 1 + src/navidrome.ts | 21 +- tests/builders.ts | 8 + tests/navidrome.test.ts | 571 +++++++++++++++++++++++++++------------- 4 files changed, 414 insertions(+), 187 deletions(-) diff --git a/src/music_service.ts b/src/music_service.ts index a4037ea..6b72d3b 100644 --- a/src/music_service.ts +++ b/src/music_service.ts @@ -42,6 +42,7 @@ export const NO_IMAGES: Images = { export type Artist = ArtistSummary & { image: Images albums: AlbumSummary[]; + similarArtists: ArtistSummary[] }; export type AlbumSummary = { diff --git a/src/navidrome.ts b/src/navidrome.ts index 33acdd4..02f161f 100644 --- a/src/navidrome.ts +++ b/src/navidrome.ts @@ -24,6 +24,7 @@ import sharp from "sharp"; import axios, { AxiosRequestConfig } from "axios"; import { Encryption } from "./encryption"; import randomString from "./random_string"; +import { fold } from "fp-ts/lib/Option"; export const BROWSER_HEADERS = { accept: @@ -117,10 +118,12 @@ export type artistInfo = { smallImageUrl: string | undefined; mediumImageUrl: string | undefined; largeImageUrl: string | undefined; + similarArtist: artistSummary[] }; export type ArtistInfo = { image: Images; + similarArtist: {id:string, name:string}[] }; export type GetArtistInfoResponse = SubsonicResponse & { @@ -255,6 +258,7 @@ export class Navidrome implements MusicService { "subsonic-response.albumList.album", "subsonic-response.album.song", "subsonic-response.genres.genre", + "subsonic-response.artistInfo.similarArtist" ], }).xml2js(response.data) as SubconicEnvelope ) @@ -301,6 +305,7 @@ export class Navidrome implements MusicService { medium: validate(it.artistInfo.mediumImageUrl), large: validate(it.artistInfo.largeImageUrl), }, + similarArtist: (it.artistInfo.similarArtist || []).map(artist => ({id: artist._id, name: artist._name})) })); getAlbum = (credentials: Credentials, id: string): Promise => @@ -341,6 +346,7 @@ export class Navidrome implements MusicService { name: artist.name, image: artistInfo.image, albums: artist.albums, + similarArtists: artistInfo.similarArtist })); getCoverArt = (credentials: Credentials, id: string, size?: number) => @@ -372,16 +378,15 @@ export class Navidrome implements MusicService { albums: (q: AlbumQuery): Promise> => navidrome .getJSON(credentials, "/rest/getAlbumList", { - ...pipe( - O.fromNullable(q.genre), - O.map((genre) => ({ + ...fold( + () => ({ + type: "alphabeticalByArtist", + }), + (genre) => ({ type: "byGenre", genre, - })), - O.getOrElse(() => ({ - type: "alphabeticalByArtist", - })) - ), + }) + )(O.fromNullable(q.genre)), size: MAX_ALBUM_LIST, offset: 0, }) diff --git a/tests/builders.ts b/tests/builders.ts index 233bbd5..9fa1d4c 100644 --- a/tests/builders.ts +++ b/tests/builders.ts @@ -79,6 +79,10 @@ export function anArtist(fields: Partial = {}): Artist { medium: `/artist/art/${id}/small`, large: `/artist/art/${id}/large`, }, + similarArtists: [ + { id: uuid(), name: "Similar artist1"}, + { id: uuid(), name: "Similar artist2"}, + ], ...fields, }; } @@ -134,6 +138,7 @@ export const BLONDIE: Artist = { medium: undefined, large: undefined, }, + similarArtists: [] }; export const BOB_MARLEY: Artist = { @@ -149,6 +154,7 @@ export const BOB_MARLEY: Artist = { medium: "http://localhost/BOB_MARLEY/med", large: "http://localhost/BOB_MARLEY/lge", }, + similarArtists: [] }; export const MADONNA: Artist = { @@ -160,6 +166,7 @@ export const MADONNA: Artist = { medium: undefined, large: "http://localhost/MADONNA/lge", }, + similarArtists: [] }; export const METALLICA: Artist = { @@ -184,6 +191,7 @@ export const METALLICA: Artist = { medium: "http://localhost/METALLICA/med", large: "http://localhost/METALLICA/lge", }, + similarArtists: [] }; export const ALL_ARTISTS = [BOB_MARLEY, BLONDIE, MADONNA, METALLICA]; diff --git a/tests/navidrome.test.ts b/tests/navidrome.test.ts index 3f465d3..b5d41ac 100644 --- a/tests/navidrome.test.ts +++ b/tests/navidrome.test.ts @@ -68,15 +68,16 @@ const ok = (data: string) => ({ }); const artistInfoXml = ( - images: Images + artist: Artist, ) => ` - ${images.small || ""} - ${images.medium || ""} - ${images.large || ""} + ${artist.image.small || ""} + ${artist.image.medium || ""} + ${artist.image.large || ""} + ${artist.similarArtists.map(it => ``)} `; @@ -296,205 +297,412 @@ describe("Navidrome", () => { }); describe("getting an artist", () => { - describe("when the artist exists and has dodgy looking artist image uris", () => { - const album1: Album = anAlbum(); + describe("when the artist exists", () => { + describe("and has many similar artists", () => { + const album1: Album = anAlbum(); - const album2: Album = anAlbum(); + const album2: Album = anAlbum(); - const artist: Artist = anArtist({ - albums: [album1, album2], - image: { - small: `http://localhost:80/${DODGY_IMAGE_NAME}`, - medium: `http://localhost:80/${DODGY_IMAGE_NAME}`, - large: `http://localhost:80/${DODGY_IMAGE_NAME}`, - }, - }); - - beforeEach(() => { - mockGET - .mockImplementationOnce(() => Promise.resolve(ok(PING_OK))) - .mockImplementationOnce(() => Promise.resolve(ok(artistXml(artist)))) - .mockImplementationOnce(() => - Promise.resolve(ok(artistInfoXml(artist.image))) - ); - }); - - it("should return remove the dodgy looking image uris and return undefined", async () => { - const result: Artist = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.authToken)) - .then((it) => it.artist(artist.id)); - - expect(result).toEqual({ - id: artist.id, - name: artist.name, + const artist: Artist = anArtist({ + albums: [album1, album2], image: { - small: undefined, - medium: undefined, - large: undefined, + small: `http://localhost:80/${DODGY_IMAGE_NAME}`, + medium: `http://localhost:80/${DODGY_IMAGE_NAME}`, + large: `http://localhost:80/${DODGY_IMAGE_NAME}`, }, - albums: artist.albums, + similarArtists: [{ id: "similar1.id", name: "similar1" }, { id: "similar2.id", name: "similar2" }], }); - expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtist`, { - params: { + beforeEach(() => { + mockGET + .mockImplementationOnce(() => Promise.resolve(ok(PING_OK))) + .mockImplementationOnce(() => + Promise.resolve(ok(artistXml(artist))) + ) + .mockImplementationOnce(() => + Promise.resolve(ok(artistInfoXml(artist))) + ); + }); + + 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.authToken)) + .then((it) => it.artist(artist.id)); + + expect(result).toEqual({ id: artist.id, - ...authParams, + name: artist.name, + image: { + small: undefined, + medium: undefined, + large: undefined, + }, + albums: artist.albums, + similarArtists: artist.similarArtists + }); + + expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtist`, { + params: { + id: artist.id, + ...authParams, + }, + headers, + }); + + expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtistInfo`, { + params: { + id: artist.id, + ...authParams, + }, + headers, + }); + }); + }); + + describe("and has one similar artists", () => { + const album1: Album = anAlbum(); + + const album2: Album = anAlbum(); + + const artist: Artist = anArtist({ + albums: [album1, album2], + image: { + small: `http://localhost:80/${DODGY_IMAGE_NAME}`, + medium: `http://localhost:80/${DODGY_IMAGE_NAME}`, + large: `http://localhost:80/${DODGY_IMAGE_NAME}`, }, - headers, + similarArtists: [{ id: "similar1.id", name: "similar1" }], }); - expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtistInfo`, { - params: { + beforeEach(() => { + mockGET + .mockImplementationOnce(() => Promise.resolve(ok(PING_OK))) + .mockImplementationOnce(() => + Promise.resolve(ok(artistXml(artist))) + ) + .mockImplementationOnce(() => + Promise.resolve(ok(artistInfoXml(artist))) + ); + }); + + 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.authToken)) + .then((it) => it.artist(artist.id)); + + expect(result).toEqual({ id: artist.id, - ...authParams, + name: artist.name, + image: { + small: undefined, + medium: undefined, + large: undefined, + }, + albums: artist.albums, + similarArtists: artist.similarArtists + }); + + expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtist`, { + params: { + id: artist.id, + ...authParams, + }, + headers, + }); + + expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtistInfo`, { + params: { + id: artist.id, + ...authParams, + }, + headers, + }); + }); + }); + + describe("and has no similar artists", () => { + const album1: Album = anAlbum(); + + const album2: Album = anAlbum(); + + const artist: Artist = anArtist({ + albums: [album1, album2], + image: { + small: `http://localhost:80/${DODGY_IMAGE_NAME}`, + medium: `http://localhost:80/${DODGY_IMAGE_NAME}`, + large: `http://localhost:80/${DODGY_IMAGE_NAME}`, }, - headers, - }); - }); - }); - - describe("when the artist exists and has multiple albums", () => { - const album1: Album = anAlbum(); - - const album2: Album = anAlbum(); - - const artist: Artist = anArtist({ - albums: [album1, album2], - }); - - beforeEach(() => { - mockGET - .mockImplementationOnce(() => Promise.resolve(ok(PING_OK))) - .mockImplementationOnce(() => Promise.resolve(ok(artistXml(artist)))) - .mockImplementationOnce(() => - Promise.resolve(ok(artistInfoXml(artist.image))) - ); - }); - - it("should return it", async () => { - const result: Artist = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.authToken)) - .then((it) => it.artist(artist.id)); - - expect(result).toEqual({ - id: artist.id, - name: artist.name, - image: artist.image, - albums: artist.albums, + similarArtists: [], }); - expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtist`, { - params: { + beforeEach(() => { + mockGET + .mockImplementationOnce(() => Promise.resolve(ok(PING_OK))) + .mockImplementationOnce(() => + Promise.resolve(ok(artistXml(artist))) + ) + .mockImplementationOnce(() => + Promise.resolve(ok(artistInfoXml(artist))) + ); + }); + + 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.authToken)) + .then((it) => it.artist(artist.id)); + + expect(result).toEqual({ id: artist.id, - ...authParams, + name: artist.name, + image: { + small: undefined, + medium: undefined, + large: undefined, + }, + albums: artist.albums, + similarArtists: artist.similarArtists + }); + + expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtist`, { + params: { + id: artist.id, + ...authParams, + }, + headers, + }); + + expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtistInfo`, { + params: { + id: artist.id, + ...authParams, + }, + headers, + }); + }); + }); + + describe("and has dodgy looking artist image uris", () => { + const album1: Album = anAlbum(); + + const album2: Album = anAlbum(); + + const artist: Artist = anArtist({ + albums: [album1, album2], + image: { + small: `http://localhost:80/${DODGY_IMAGE_NAME}`, + medium: `http://localhost:80/${DODGY_IMAGE_NAME}`, + large: `http://localhost:80/${DODGY_IMAGE_NAME}`, }, - headers, + similarArtists: [], }); - expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtistInfo`, { - params: { + beforeEach(() => { + mockGET + .mockImplementationOnce(() => Promise.resolve(ok(PING_OK))) + .mockImplementationOnce(() => + Promise.resolve(ok(artistXml(artist))) + ) + .mockImplementationOnce(() => + Promise.resolve(ok(artistInfoXml(artist))) + ); + }); + + it("should return remove the dodgy looking image uris and return undefined", async () => { + const result: Artist = await navidrome + .generateToken({ username, password }) + .then((it) => it as AuthSuccess) + .then((it) => navidrome.login(it.authToken)) + .then((it) => it.artist(artist.id)); + + expect(result).toEqual({ id: artist.id, - ...authParams, - }, - headers, + name: artist.name, + image: { + small: undefined, + medium: undefined, + large: undefined, + }, + albums: artist.albums, + similarArtists: [] + }); + + expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtist`, { + params: { + id: artist.id, + ...authParams, + }, + headers, + }); + + expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtistInfo`, { + params: { + id: artist.id, + ...authParams, + }, + headers, + }); }); }); - }); - describe("when the artist exists and has only 1 album", () => { - const album: Album = anAlbum(); + describe("and has multiple albums", () => { + const album1: Album = anAlbum(); - const artist: Artist = anArtist({ - albums: [album], - }); + const album2: Album = anAlbum(); - beforeEach(() => { - mockGET - .mockImplementationOnce(() => Promise.resolve(ok(PING_OK))) - .mockImplementationOnce(() => Promise.resolve(ok(artistXml(artist)))) - .mockImplementationOnce(() => - Promise.resolve(ok(artistInfoXml(artist.image))) - ); - }); - - it("should return it", async () => { - const result: Artist = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.authToken)) - .then((it) => it.artist(artist.id)); - - expect(result).toEqual({ - id: artist.id, - name: artist.name, - image: artist.image, - albums: artist.albums, + const artist: Artist = anArtist({ + albums: [album1, album2], + similarArtists: [] }); - expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtist`, { - params: { + beforeEach(() => { + mockGET + .mockImplementationOnce(() => Promise.resolve(ok(PING_OK))) + .mockImplementationOnce(() => + Promise.resolve(ok(artistXml(artist))) + ) + .mockImplementationOnce(() => + Promise.resolve(ok(artistInfoXml(artist))) + ); + }); + + it("should return it", async () => { + const result: Artist = await navidrome + .generateToken({ username, password }) + .then((it) => it as AuthSuccess) + .then((it) => navidrome.login(it.authToken)) + .then((it) => it.artist(artist.id)); + + expect(result).toEqual({ id: artist.id, - ...authParams, - }, - headers, + name: artist.name, + image: artist.image, + albums: artist.albums, + similarArtists: [] + }); + + expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtist`, { + params: { + id: artist.id, + ...authParams, + }, + headers, + }); + + expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtistInfo`, { + params: { + id: artist.id, + ...authParams, + }, + headers, + }); + }); + }); + + describe("and has only 1 album", () => { + const album: Album = anAlbum(); + + const artist: Artist = anArtist({ + albums: [album], + similarArtists: [] }); - expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtistInfo`, { - params: { + beforeEach(() => { + mockGET + .mockImplementationOnce(() => Promise.resolve(ok(PING_OK))) + .mockImplementationOnce(() => + Promise.resolve(ok(artistXml(artist))) + ) + .mockImplementationOnce(() => + Promise.resolve(ok(artistInfoXml(artist))) + ); + }); + + it("should return it", async () => { + const result: Artist = await navidrome + .generateToken({ username, password }) + .then((it) => it as AuthSuccess) + .then((it) => navidrome.login(it.authToken)) + .then((it) => it.artist(artist.id)); + + expect(result).toEqual({ id: artist.id, - ...authParams, - }, - headers, + name: artist.name, + image: artist.image, + albums: artist.albums, + similarArtists: [] + }); + + expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtist`, { + params: { + id: artist.id, + ...authParams, + }, + headers, + }); + + expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtistInfo`, { + params: { + id: artist.id, + ...authParams, + }, + headers, + }); }); }); - }); - describe("when the artist exists and has no albums", () => { - const artist: Artist = anArtist({ - albums: [], - }); - - beforeEach(() => { - mockGET - .mockImplementationOnce(() => Promise.resolve(ok(PING_OK))) - .mockImplementationOnce(() => Promise.resolve(ok(artistXml(artist)))) - .mockImplementationOnce(() => - Promise.resolve(ok(artistInfoXml(artist.image))) - ); - }); - - it("should return it", async () => { - const result: Artist = await navidrome - .generateToken({ username, password }) - .then((it) => it as AuthSuccess) - .then((it) => navidrome.login(it.authToken)) - .then((it) => it.artist(artist.id)); - - expect(result).toEqual({ - id: artist.id, - name: artist.name, - image: artist.image, + describe("and has no albums", () => { + const artist: Artist = anArtist({ albums: [], + similarArtists: [] }); - expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtist`, { - params: { - id: artist.id, - ...authParams, - }, - headers, + beforeEach(() => { + mockGET + .mockImplementationOnce(() => Promise.resolve(ok(PING_OK))) + .mockImplementationOnce(() => + Promise.resolve(ok(artistXml(artist))) + ) + .mockImplementationOnce(() => + Promise.resolve(ok(artistInfoXml(artist))) + ); }); - expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtistInfo`, { - params: { + it("should return it", async () => { + const result: Artist = await navidrome + .generateToken({ username, password }) + .then((it) => it as AuthSuccess) + .then((it) => navidrome.login(it.authToken)) + .then((it) => it.artist(artist.id)); + + expect(result).toEqual({ id: artist.id, - ...authParams, - }, - headers, + name: artist.name, + image: artist.image, + albums: [], + similarArtists: [] + }); + + expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtist`, { + params: { + id: artist.id, + ...authParams, + }, + headers, + }); + + expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtistInfo`, { + params: { + id: artist.id, + ...authParams, + }, + headers, + }); }); }); }); @@ -1388,7 +1596,7 @@ describe("Navidrome", () => { data: Buffer.from("the image", "ascii"), }; - const artist = anArtist({ id: artistId }); + const artist = anArtist({ id: artistId, image: images }); mockGET .mockImplementationOnce(() => Promise.resolve(ok(PING_OK))) @@ -1396,7 +1604,7 @@ describe("Navidrome", () => { Promise.resolve(ok(artistXml(artist))) ) .mockImplementationOnce(() => - Promise.resolve(ok(artistInfoXml(images))) + Promise.resolve(ok(artistInfoXml(artist))) ) .mockImplementationOnce(() => Promise.resolve(streamResponse)); @@ -1454,6 +1662,7 @@ describe("Navidrome", () => { const artist = anArtist({ id: artistId, albums: [album1, album2], + image: images }); mockGET @@ -1462,7 +1671,7 @@ describe("Navidrome", () => { Promise.resolve(ok(artistXml(artist))) ) .mockImplementationOnce(() => - Promise.resolve(ok(artistInfoXml(images))) + Promise.resolve(ok(artistInfoXml(artist))) ) .mockImplementationOnce(() => Promise.resolve(streamResponse)); @@ -1528,7 +1737,7 @@ describe("Navidrome", () => { data: Buffer.from("the image", "ascii"), }; - const artist = anArtist({ id: artistId, albums: [] }); + const artist = anArtist({ id: artistId, albums: [], image: images }); mockGET .mockImplementationOnce(() => Promise.resolve(ok(PING_OK))) @@ -1536,7 +1745,7 @@ describe("Navidrome", () => { Promise.resolve(ok(artistXml(artist))) ) .mockImplementationOnce(() => - Promise.resolve(ok(artistInfoXml(images))) + Promise.resolve(ok(artistInfoXml(artist))) ) .mockImplementationOnce(() => Promise.resolve(streamResponse)); @@ -1595,7 +1804,7 @@ describe("Navidrome", () => { data: originalImage, }; - const artist = anArtist({ id: artistId }); + const artist = anArtist({ id: artistId, image: images }); mockGET .mockImplementationOnce(() => Promise.resolve(ok(PING_OK))) @@ -1603,13 +1812,15 @@ describe("Navidrome", () => { Promise.resolve(ok(artistXml(artist))) ) .mockImplementationOnce(() => - Promise.resolve(ok(artistInfoXml(images))) + Promise.resolve(ok(artistInfoXml(artist))) ) .mockImplementationOnce(() => Promise.resolve(streamResponse)); const resize = jest.fn(); - (sharp as unknown as jest.Mock).mockReturnValue({ resize }); - resize.mockReturnValue({ toBuffer: () => Promise.resolve(resizedImage) }) + ((sharp as unknown) as jest.Mock).mockReturnValue({ resize }); + resize.mockReturnValue({ + toBuffer: () => Promise.resolve(resizedImage), + }); const result = await navidrome .generateToken({ username, password }) @@ -1668,6 +1879,7 @@ describe("Navidrome", () => { const artist = anArtist({ id: artistId, albums: [album1, album2], + image: images }); mockGET @@ -1676,7 +1888,7 @@ describe("Navidrome", () => { Promise.resolve(ok(artistXml(artist))) ) .mockImplementationOnce(() => - Promise.resolve(ok(artistInfoXml(images))) + Promise.resolve(ok(artistInfoXml(artist))) ) .mockImplementationOnce(() => Promise.resolve(streamResponse)); @@ -1743,7 +1955,7 @@ describe("Navidrome", () => { data: Buffer.from("the image", "ascii"), }; - const artist = anArtist({ id: artistId, albums: [] }); + const artist = anArtist({ id: artistId, albums: [], image: images }); mockGET .mockImplementationOnce(() => Promise.resolve(ok(PING_OK))) @@ -1751,7 +1963,7 @@ describe("Navidrome", () => { Promise.resolve(ok(artistXml(artist))) ) .mockImplementationOnce(() => - Promise.resolve(ok(artistInfoXml(images))) + Promise.resolve(ok(artistInfoXml(artist))) ) .mockImplementationOnce(() => Promise.resolve(streamResponse)); @@ -1810,6 +2022,7 @@ describe("Navidrome", () => { const artist = anArtist({ id: artistId, albums: [album1, album2], + image: images }); mockGET @@ -1818,7 +2031,7 @@ describe("Navidrome", () => { Promise.resolve(ok(artistXml(artist))) ) .mockImplementationOnce(() => - Promise.resolve(ok(artistInfoXml(images))) + Promise.resolve(ok(artistInfoXml(artist))) ) .mockImplementationOnce(() => Promise.resolve(streamResponse)); @@ -1885,7 +2098,7 @@ describe("Navidrome", () => { data: Buffer.from("the image", "ascii"), }; - const artist = anArtist({ id: artistId, albums: [] }); + const artist = anArtist({ id: artistId, albums: [], image: images }); mockGET .mockImplementationOnce(() => Promise.resolve(ok(PING_OK))) @@ -1893,7 +2106,7 @@ describe("Navidrome", () => { Promise.resolve(ok(artistXml(artist))) ) .mockImplementationOnce(() => - Promise.resolve(ok(artistInfoXml(images))) + Promise.resolve(ok(artistInfoXml(artist))) ) .mockImplementationOnce(() => Promise.resolve(streamResponse));