mirror of
https://github.com/wkulhanek/bonob.git
synced 2025-12-21 17:33:29 +01:00
Ability to stream a track from navidrome
This commit is contained in:
@@ -92,8 +92,8 @@ export function aTrack(fields: Partial<Track> = {}): Track {
|
||||
id,
|
||||
name: `Track ${id}`,
|
||||
mimeType: `audio/mp3-${id}`,
|
||||
duration: `${randomInt(500)}`,
|
||||
number: `${randomInt(100)}`,
|
||||
duration: randomInt(500),
|
||||
number: randomInt(100),
|
||||
genre: randomGenre(),
|
||||
artist: anArtist(),
|
||||
album: anAlbum(),
|
||||
|
||||
@@ -148,33 +148,6 @@ describe("InMemoryMusicService", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("album", () => {
|
||||
describe("when it exists", () => {
|
||||
const albumToLookFor = anAlbum({ id: "albumToLookFor" });
|
||||
const artist1 = anArtist({ albums: [anAlbum(), anAlbum(), anAlbum()] });
|
||||
const artist2 = anArtist({
|
||||
albums: [anAlbum(), albumToLookFor, anAlbum()],
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
service.hasArtists(artist1, artist2);
|
||||
});
|
||||
|
||||
it("should provide an artist", async () => {
|
||||
expect(await musicLibrary.album(albumToLookFor.id)).toEqual(
|
||||
albumToLookFor
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when it doesnt exist", () => {
|
||||
it("should blow up", async () => {
|
||||
return expect(musicLibrary.album("-1")).rejects.toEqual(
|
||||
"No album with id '-1'"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("tracks", () => {
|
||||
const artist1Album1 = anAlbum();
|
||||
@@ -193,13 +166,26 @@ describe("InMemoryMusicService", () => {
|
||||
|
||||
describe("fetching tracks for an album", () => {
|
||||
it("should return only tracks on that album", async () => {
|
||||
expect(await musicLibrary.tracks(artist1Album1.id)).toEqual([track1, track2])
|
||||
expect(await musicLibrary.tracks(artist1Album1.id)).toEqual([
|
||||
track1,
|
||||
track2,
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("fetching tracks for an album that doesnt exist", () => {
|
||||
it("should return empty array", async () => {
|
||||
expect(await musicLibrary.tracks("non existant album id")).toEqual([])
|
||||
expect(await musicLibrary.tracks("non existant album id")).toEqual(
|
||||
[]
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("fetching a single track", () => {
|
||||
describe("when it exists", () => {
|
||||
it("should return the track", async () => {
|
||||
expect(await musicLibrary.track(track3.id)).toEqual(track3);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -235,125 +221,145 @@ describe("InMemoryMusicService", () => {
|
||||
service.hasArtists(artist1, artist2, artist3, artistWithNoAlbums);
|
||||
});
|
||||
|
||||
describe("with no filtering", () => {
|
||||
describe("fetching all on one page", () => {
|
||||
it("should return all the albums for all the artists", async () => {
|
||||
expect(
|
||||
await musicLibrary.albums({ _index: 0, _count: 100 })
|
||||
).toEqual({
|
||||
results: [
|
||||
albumToAlbumSummary(artist1_album1),
|
||||
albumToAlbumSummary(artist1_album2),
|
||||
albumToAlbumSummary(artist1_album3),
|
||||
albumToAlbumSummary(artist1_album4),
|
||||
albumToAlbumSummary(artist1_album5),
|
||||
|
||||
albumToAlbumSummary(artist2_album1),
|
||||
|
||||
albumToAlbumSummary(artist3_album1),
|
||||
albumToAlbumSummary(artist3_album2),
|
||||
],
|
||||
total: totalAlbumCount,
|
||||
describe("fetching multiple albums", () => {
|
||||
describe("with no filtering", () => {
|
||||
describe("fetching all on one page", () => {
|
||||
it("should return all the albums for all the artists", async () => {
|
||||
expect(
|
||||
await musicLibrary.albums({ _index: 0, _count: 100 })
|
||||
).toEqual({
|
||||
results: [
|
||||
albumToAlbumSummary(artist1_album1),
|
||||
albumToAlbumSummary(artist1_album2),
|
||||
albumToAlbumSummary(artist1_album3),
|
||||
albumToAlbumSummary(artist1_album4),
|
||||
albumToAlbumSummary(artist1_album5),
|
||||
|
||||
albumToAlbumSummary(artist2_album1),
|
||||
|
||||
albumToAlbumSummary(artist3_album1),
|
||||
albumToAlbumSummary(artist3_album2),
|
||||
],
|
||||
total: totalAlbumCount,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("fetching a page", () => {
|
||||
it("should return only that page", async () => {
|
||||
expect(await musicLibrary.albums({ _index: 4, _count: 3 })).toEqual(
|
||||
{
|
||||
results: [
|
||||
albumToAlbumSummary(artist1_album5),
|
||||
albumToAlbumSummary(artist2_album1),
|
||||
albumToAlbumSummary(artist3_album1),
|
||||
],
|
||||
total: totalAlbumCount,
|
||||
}
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("fetching the last page", () => {
|
||||
it("should return only that page", async () => {
|
||||
expect(
|
||||
await musicLibrary.albums({ _index: 6, _count: 100 })
|
||||
).toEqual({
|
||||
results: [
|
||||
albumToAlbumSummary(artist3_album1),
|
||||
albumToAlbumSummary(artist3_album2),
|
||||
],
|
||||
total: totalAlbumCount,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("fetching a page", () => {
|
||||
it("should return only that page", async () => {
|
||||
expect(await musicLibrary.albums({ _index: 4, _count: 3 })).toEqual(
|
||||
{
|
||||
|
||||
describe("filtering by genre", () => {
|
||||
describe("fetching all on one page", () => {
|
||||
it("should return all the albums of that genre for all the artists", async () => {
|
||||
expect(
|
||||
await musicLibrary.albums({
|
||||
genre: "Pop",
|
||||
_index: 0,
|
||||
_count: 100,
|
||||
})
|
||||
).toEqual({
|
||||
results: [
|
||||
albumToAlbumSummary(artist1_album1),
|
||||
albumToAlbumSummary(artist1_album4),
|
||||
albumToAlbumSummary(artist1_album5),
|
||||
albumToAlbumSummary(artist2_album1),
|
||||
albumToAlbumSummary(artist3_album1),
|
||||
albumToAlbumSummary(artist3_album2),
|
||||
],
|
||||
total: totalAlbumCount,
|
||||
}
|
||||
);
|
||||
total: 4,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("fetching the last page", () => {
|
||||
it("should return only that page", async () => {
|
||||
|
||||
describe("when the genre has more albums than a single page", () => {
|
||||
describe("can fetch a single page", () => {
|
||||
it("should return only the albums for that page", async () => {
|
||||
expect(
|
||||
await musicLibrary.albums({
|
||||
genre: "Pop",
|
||||
_index: 1,
|
||||
_count: 2,
|
||||
})
|
||||
).toEqual({
|
||||
results: [
|
||||
albumToAlbumSummary(artist1_album4),
|
||||
albumToAlbumSummary(artist1_album5),
|
||||
],
|
||||
total: 4,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("can fetch the last page", () => {
|
||||
it("should return only the albums for the last page", async () => {
|
||||
expect(
|
||||
await musicLibrary.albums({
|
||||
genre: "Pop",
|
||||
_index: 3,
|
||||
_count: 100,
|
||||
})
|
||||
).toEqual({
|
||||
results: [albumToAlbumSummary(artist3_album2)],
|
||||
total: 4,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should return empty list if there are no albums for the genre", async () => {
|
||||
expect(
|
||||
await musicLibrary.albums({ _index: 6, _count: 100 })
|
||||
await musicLibrary.albums({
|
||||
genre: "genre with no albums",
|
||||
_index: 0,
|
||||
_count: 100,
|
||||
})
|
||||
).toEqual({
|
||||
results: [
|
||||
albumToAlbumSummary(artist3_album1),
|
||||
albumToAlbumSummary(artist3_album2),
|
||||
],
|
||||
total: totalAlbumCount,
|
||||
results: [],
|
||||
total: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("filtering by genre", () => {
|
||||
describe("fetching all on one page", () => {
|
||||
it("should return all the albums of that genre for all the artists", async () => {
|
||||
expect(
|
||||
await musicLibrary.albums({
|
||||
genre: "Pop",
|
||||
_index: 0,
|
||||
_count: 100,
|
||||
})
|
||||
).toEqual({
|
||||
results: [
|
||||
albumToAlbumSummary(artist1_album1),
|
||||
albumToAlbumSummary(artist1_album4),
|
||||
albumToAlbumSummary(artist1_album5),
|
||||
albumToAlbumSummary(artist3_album2),
|
||||
],
|
||||
total: 4,
|
||||
});
|
||||
describe("fetching a single album", () => {
|
||||
describe("when it exists", () => {
|
||||
it("should provide an album", async () => {
|
||||
expect(await musicLibrary.album(artist1_album5.id)).toEqual(
|
||||
artist1_album5
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the genre has more albums than a single page", () => {
|
||||
describe("can fetch a single page", () => {
|
||||
it("should return only the albums for that page", async () => {
|
||||
expect(
|
||||
await musicLibrary.albums({
|
||||
genre: "Pop",
|
||||
_index: 1,
|
||||
_count: 2,
|
||||
})
|
||||
).toEqual({
|
||||
results: [
|
||||
albumToAlbumSummary(artist1_album4),
|
||||
albumToAlbumSummary(artist1_album5),
|
||||
],
|
||||
total: 4,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("can fetch the last page", () => {
|
||||
it("should return only the albums for the last page", async () => {
|
||||
expect(
|
||||
await musicLibrary.albums({
|
||||
genre: "Pop",
|
||||
_index: 3,
|
||||
_count: 100,
|
||||
})
|
||||
).toEqual({
|
||||
results: [albumToAlbumSummary(artist3_album2)],
|
||||
total: 4,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it("should return empty list if there are no albums for the genre", async () => {
|
||||
expect(
|
||||
await musicLibrary.albums({
|
||||
genre: "genre with no albums",
|
||||
_index: 0,
|
||||
_count: 100,
|
||||
})
|
||||
).toEqual({
|
||||
results: [],
|
||||
total: 0,
|
||||
|
||||
describe("when it doesnt exist", () => {
|
||||
it("should blow up", async () => {
|
||||
return expect(musicLibrary.album("-1")).rejects.toEqual(
|
||||
"No album with id '-1'"
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -42,7 +42,7 @@ export class InMemoryMusicService implements MusicService {
|
||||
this.users[username] == password
|
||||
) {
|
||||
return Promise.resolve({
|
||||
authToken: JSON.stringify({ username, password }),
|
||||
authToken: Buffer.from(JSON.stringify({ username, password })).toString('base64'),
|
||||
userId: username,
|
||||
nickname: username,
|
||||
});
|
||||
@@ -52,7 +52,7 @@ export class InMemoryMusicService implements MusicService {
|
||||
}
|
||||
|
||||
login(token: string): Promise<MusicLibrary> {
|
||||
const credentials = JSON.parse(token) as Credentials;
|
||||
const credentials = JSON.parse(Buffer.from(token, "base64").toString("ascii")) as Credentials;
|
||||
if (this.users[credentials.username] != credentials.password)
|
||||
return Promise.reject("Invalid auth token");
|
||||
|
||||
@@ -103,7 +103,17 @@ export class InMemoryMusicService implements MusicService {
|
||||
A.sort(ordString)
|
||||
)
|
||||
),
|
||||
tracks: (albumId: string) => Promise.resolve(this.tracks.filter(it => it.album.id === albumId))
|
||||
tracks: (albumId: string) => Promise.resolve(this.tracks.filter(it => it.album.id === albumId)),
|
||||
track: (trackId: string) => pipe(
|
||||
this.tracks.find(it => it.id === trackId),
|
||||
O.fromNullable,
|
||||
O.map(it => Promise.resolve(it)),
|
||||
O.getOrElse(() => Promise.reject(`Failed to find track with id ${trackId}`))
|
||||
),
|
||||
stream: (_: {
|
||||
trackId: string;
|
||||
range: string | undefined;
|
||||
}) => Promise.reject("unsupported operation")
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Md5 } from "ts-md5/dist/md5";
|
||||
import { v4 as uuid } from "uuid";
|
||||
|
||||
import { isDodgyImage, Navidrome, t } from "../src/navidrome";
|
||||
import encryption from "../src/encryption";
|
||||
@@ -18,7 +19,7 @@ import {
|
||||
Track,
|
||||
AlbumSummary,
|
||||
artistToArtistSummary,
|
||||
NO_IMAGES
|
||||
NO_IMAGES,
|
||||
} from "../src/music_service";
|
||||
import { anAlbum, anArtist, aTrack } from "./builders";
|
||||
|
||||
@@ -70,7 +71,11 @@ const artistInfoXml = (
|
||||
</artistInfo>
|
||||
</subsonic-response>`;
|
||||
|
||||
const albumXml = (artist: Artist, album: AlbumSummary, tracks: Track[] = []) => `<album id="${album.id}"
|
||||
const albumXml = (
|
||||
artist: Artist,
|
||||
album: AlbumSummary,
|
||||
tracks: Track[] = []
|
||||
) => `<album id="${album.id}"
|
||||
parent="${artist.id}"
|
||||
isDir="true"
|
||||
title="${album.name}" name="${album.name}" album="${album.name}"
|
||||
@@ -83,7 +88,7 @@ const albumXml = (artist: Artist, album: AlbumSummary, tracks: Track[] = []) =>
|
||||
created="2021-01-07T08:19:55.834207205Z"
|
||||
artistId="${artist.id}"
|
||||
songCount="19"
|
||||
isVideo="false">${tracks.map(track => songXml(track))}</album>`;
|
||||
isVideo="false">${tracks.map((track) => songXml(track))}</album>`;
|
||||
|
||||
const songXml = (track: Track) => `<song
|
||||
id="${track.id}"
|
||||
@@ -112,16 +117,17 @@ const albumListXml = (
|
||||
) => `<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.16.1" type="navidrome" serverVersion="0.40.0 (8799358a)">
|
||||
<albumList>
|
||||
${albums.map(([artist, album]) =>
|
||||
albumXml(artist, album)
|
||||
)}
|
||||
albumXml(artist, album)
|
||||
)}
|
||||
</albumList>
|
||||
</subsonic-response>`;
|
||||
|
||||
const artistXml = (
|
||||
artist: Artist
|
||||
) => `<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.16.1" type="navidrome" serverVersion="0.40.0 (8799358a)">
|
||||
<artist id="${artist.id}" name="${artist.name}" albumCount="${artist.albums.length
|
||||
}" artistImageUrl="....">
|
||||
<artist id="${artist.id}" name="${artist.name}" albumCount="${
|
||||
artist.albums.length
|
||||
}" artistImageUrl="....">
|
||||
${artist.albums.map((album) => albumXml(artist, album))}
|
||||
</artist>
|
||||
</subsonic-response>`;
|
||||
@@ -131,15 +137,31 @@ const genresXml = (
|
||||
) => `<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.16.1" type="navidrome" serverVersion="0.40.0 (8799358a)">
|
||||
<genres>
|
||||
${genres.map(
|
||||
(it) =>
|
||||
`<genre songCount="1475" albumCount="86">${it}</genre>`
|
||||
)}
|
||||
(it) =>
|
||||
`<genre songCount="1475" albumCount="86">${it}</genre>`
|
||||
)}
|
||||
</genres>
|
||||
</subsonic-response>`;
|
||||
|
||||
const getAlbumXml = (artist: Artist, album: Album, tracks: Track[]) => `<subsonic-response status="ok" version="1.8.0">
|
||||
${albumXml(artist, album, tracks)}
|
||||
</subsonic-response>`
|
||||
const getAlbumXml = (
|
||||
artist: Artist,
|
||||
album: Album,
|
||||
tracks: Track[]
|
||||
) => `<subsonic-response status="ok" version="1.8.0">
|
||||
${albumXml(
|
||||
artist,
|
||||
album,
|
||||
tracks
|
||||
)}
|
||||
</subsonic-response>`;
|
||||
|
||||
const getSongXml = (
|
||||
track: Track
|
||||
) => `<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.16.1" type="navidrome" serverVersion="0.40.0 (8799358a)">
|
||||
${songXml(
|
||||
track
|
||||
)}
|
||||
</subsonic-response>`;
|
||||
|
||||
const PING_OK = `<subsonic-response xmlns="http://subsonic.org/restapi" status="ok" version="1.16.1" type="navidrome" serverVersion="0.40.0 (8799358a)"></subsonic-response>`;
|
||||
|
||||
@@ -169,6 +191,9 @@ describe("Navidrome", () => {
|
||||
v: "1.16.1",
|
||||
c: "bonob",
|
||||
};
|
||||
const headers = {
|
||||
"User-Agent": "bonob",
|
||||
};
|
||||
|
||||
describe("generateToken", () => {
|
||||
describe("when the credentials are valid", () => {
|
||||
@@ -186,6 +211,7 @@ describe("Navidrome", () => {
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/ping.view`, {
|
||||
params: authParams,
|
||||
headers,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -226,6 +252,7 @@ describe("Navidrome", () => {
|
||||
params: {
|
||||
...authParams,
|
||||
},
|
||||
headers,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -268,6 +295,7 @@ describe("Navidrome", () => {
|
||||
id: artist.id,
|
||||
...authParams,
|
||||
},
|
||||
headers,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtistInfo`, {
|
||||
@@ -275,6 +303,7 @@ describe("Navidrome", () => {
|
||||
id: artist.id,
|
||||
...authParams,
|
||||
},
|
||||
headers,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -377,30 +406,35 @@ describe("Navidrome", () => {
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtists`, {
|
||||
params: authParams,
|
||||
headers,
|
||||
});
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtistInfo`, {
|
||||
params: {
|
||||
id: artist1.id,
|
||||
...authParams,
|
||||
},
|
||||
headers,
|
||||
});
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtistInfo`, {
|
||||
params: {
|
||||
id: artist2.id,
|
||||
...authParams,
|
||||
},
|
||||
headers,
|
||||
});
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtistInfo`, {
|
||||
params: {
|
||||
id: artist3.id,
|
||||
...authParams,
|
||||
},
|
||||
headers,
|
||||
});
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtistInfo`, {
|
||||
params: {
|
||||
id: artist4.id,
|
||||
...authParams,
|
||||
},
|
||||
headers,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -435,18 +469,21 @@ describe("Navidrome", () => {
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtists`, {
|
||||
params: authParams,
|
||||
headers,
|
||||
});
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtistInfo`, {
|
||||
params: {
|
||||
id: artist2.id,
|
||||
...authParams,
|
||||
},
|
||||
headers,
|
||||
});
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getArtistInfo`, {
|
||||
params: {
|
||||
id: artist3.id,
|
||||
...authParams,
|
||||
},
|
||||
headers,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -498,6 +535,7 @@ describe("Navidrome", () => {
|
||||
offset: 0,
|
||||
...authParams,
|
||||
},
|
||||
headers,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -544,6 +582,7 @@ describe("Navidrome", () => {
|
||||
offset: 0,
|
||||
...authParams,
|
||||
},
|
||||
headers,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -569,6 +608,7 @@ describe("Navidrome", () => {
|
||||
offset: 0,
|
||||
...authParams,
|
||||
},
|
||||
headers,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -620,6 +660,7 @@ describe("Navidrome", () => {
|
||||
offset: 0,
|
||||
...authParams,
|
||||
},
|
||||
headers,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -630,24 +671,20 @@ describe("Navidrome", () => {
|
||||
describe("when it exists", () => {
|
||||
const album = anAlbum();
|
||||
|
||||
const artist = anArtist({ albums: [album] })
|
||||
const artist = anArtist({ albums: [album] });
|
||||
|
||||
const tracks = [
|
||||
const tracks = [
|
||||
aTrack({ artist, album }),
|
||||
aTrack({ artist, album }),
|
||||
aTrack({ artist, album }),
|
||||
aTrack({ artist, album }),
|
||||
]
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
mockGET
|
||||
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve(
|
||||
ok(
|
||||
getAlbumXml(artist, album, tracks)
|
||||
)
|
||||
)
|
||||
Promise.resolve(ok(getAlbumXml(artist, album, tracks)))
|
||||
);
|
||||
});
|
||||
|
||||
@@ -665,6 +702,7 @@ describe("Navidrome", () => {
|
||||
id: album.id,
|
||||
...authParams,
|
||||
},
|
||||
headers,
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -675,49 +713,223 @@ describe("Navidrome", () => {
|
||||
describe("when it exists", () => {
|
||||
const album = anAlbum({ id: "album1", name: "Burnin" });
|
||||
const albumSummary = albumToAlbumSummary(album);
|
||||
|
||||
const artist = anArtist({ id: "artist1", name: "Bob Marley", albums: [album] })
|
||||
|
||||
const artist = anArtist({
|
||||
id: "artist1",
|
||||
name: "Bob Marley",
|
||||
albums: [album],
|
||||
});
|
||||
const artistSummary = {
|
||||
...artistToArtistSummary(artist),
|
||||
image: NO_IMAGES
|
||||
image: NO_IMAGES,
|
||||
};
|
||||
|
||||
const tracks = [
|
||||
|
||||
const tracks = [
|
||||
aTrack({ artist: artistSummary, album: albumSummary }),
|
||||
aTrack({ artist: artistSummary, album: albumSummary }),
|
||||
aTrack({ artist: artistSummary, album: albumSummary }),
|
||||
aTrack({ artist: artistSummary, album: albumSummary }),
|
||||
]
|
||||
|
||||
];
|
||||
|
||||
beforeEach(() => {
|
||||
mockGET
|
||||
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve(
|
||||
ok(
|
||||
getAlbumXml(artist, album, tracks)
|
||||
)
|
||||
)
|
||||
Promise.resolve(ok(getAlbumXml(artist, album, tracks)))
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
it("should return the album", async () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => it.tracks(album.id));
|
||||
|
||||
|
||||
expect(result).toEqual(tracks);
|
||||
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbum`, {
|
||||
params: {
|
||||
id: album.id,
|
||||
...authParams,
|
||||
},
|
||||
headers,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("a single track", () => {
|
||||
const album = anAlbum({ id: "album1", name: "Burnin" });
|
||||
const albumSummary = albumToAlbumSummary(album);
|
||||
|
||||
const artist = anArtist({
|
||||
id: "artist1",
|
||||
name: "Bob Marley",
|
||||
albums: [album],
|
||||
});
|
||||
const artistSummary = {
|
||||
...artistToArtistSummary(artist),
|
||||
image: NO_IMAGES,
|
||||
};
|
||||
|
||||
const track = aTrack({ artist: artistSummary, album: albumSummary });
|
||||
|
||||
beforeEach(() => {
|
||||
mockGET
|
||||
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
|
||||
.mockImplementationOnce(() => Promise.resolve(ok(getSongXml(track))))
|
||||
.mockImplementationOnce(() =>
|
||||
Promise.resolve(ok(getAlbumXml(artist, album, [])))
|
||||
);
|
||||
});
|
||||
|
||||
it("should return the track", async () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => it.track(track.id));
|
||||
|
||||
expect(result).toEqual(track);
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getSong`, {
|
||||
params: {
|
||||
id: track.id,
|
||||
...authParams,
|
||||
},
|
||||
headers,
|
||||
});
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/getAlbum`, {
|
||||
params: {
|
||||
id: album.id,
|
||||
...authParams,
|
||||
},
|
||||
headers,
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("streaming a track", () => {
|
||||
const trackId = uuid();
|
||||
|
||||
describe("with no range specified", () => {
|
||||
describe("navidrome returns a 200", () => {
|
||||
it("should return the content", async () => {
|
||||
const streamResponse = {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "audio/mpeg",
|
||||
"content-length": "1667",
|
||||
"content-range": "-200",
|
||||
"accept-ranges": "bytes",
|
||||
"some-other-header": "some-value"
|
||||
},
|
||||
data: Buffer.from("the track", "ascii"),
|
||||
};
|
||||
|
||||
mockGET
|
||||
.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.authToken))
|
||||
.then((it) => it.stream({ trackId, range: undefined }));
|
||||
|
||||
expect(result.headers).toEqual({
|
||||
"content-type": "audio/mpeg",
|
||||
"content-length": "1667",
|
||||
"content-range": "-200",
|
||||
"accept-ranges": "bytes"
|
||||
});
|
||||
expect(result.data.toString()).toEqual("the track");
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/stream`, {
|
||||
params: {
|
||||
id: trackId,
|
||||
...authParams,
|
||||
},
|
||||
headers: {
|
||||
"User-Agent": "bonob",
|
||||
},
|
||||
responseType: "arraybuffer",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("navidrome returns something other than a 200", () => {
|
||||
it("should return the content", async () => {
|
||||
const trackId = "track123";
|
||||
|
||||
const streamResponse = {
|
||||
status: 400,
|
||||
};
|
||||
|
||||
mockGET
|
||||
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
|
||||
.mockImplementationOnce(() => Promise.resolve(streamResponse));
|
||||
|
||||
const musicLibrary = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken));
|
||||
|
||||
return expect(
|
||||
musicLibrary.stream({ trackId, range: undefined })
|
||||
).rejects.toEqual(`Navidrome failed with a 400`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("with range specified", () => {
|
||||
it("should send the range to navidrome", async () => {
|
||||
const range = "1000-2000";
|
||||
const streamResponse = {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "audio/flac",
|
||||
"content-length": "66",
|
||||
"content-range": "100-200",
|
||||
"accept-ranges": "none",
|
||||
"some-other-header": "some-value"
|
||||
},
|
||||
data: Buffer.from("the track", "ascii"),
|
||||
};
|
||||
|
||||
mockGET
|
||||
.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.authToken))
|
||||
.then((it) => it.stream({ trackId, range }));
|
||||
|
||||
expect(result.headers).toEqual({
|
||||
"content-type": "audio/flac",
|
||||
"content-length": "66",
|
||||
"content-range": "100-200",
|
||||
"accept-ranges": "none"
|
||||
});
|
||||
expect(result.data.toString()).toEqual("the track");
|
||||
|
||||
expect(axios.get).toHaveBeenCalledWith(`${url}/rest/stream`, {
|
||||
params: {
|
||||
id: trackId,
|
||||
...authParams,
|
||||
},
|
||||
headers: {
|
||||
"User-Agent": "bonob",
|
||||
Range: range,
|
||||
},
|
||||
responseType: "arraybuffer",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -41,7 +41,7 @@ class LoggedInSonosDriver {
|
||||
let next = path.shift();
|
||||
while (next) {
|
||||
if (next != "root") {
|
||||
const childIds = this.currentMetadata!.getMetadataResult.mediaCollection.map(
|
||||
const childIds = this.currentMetadata!.getMetadataResult.mediaCollection!.map(
|
||||
(it) => it.id
|
||||
);
|
||||
if (!childIds.includes(next)) {
|
||||
@@ -56,7 +56,7 @@ class LoggedInSonosDriver {
|
||||
|
||||
expectTitles(titles: string[]) {
|
||||
expect(
|
||||
this.currentMetadata!.getMetadataResult.mediaCollection.map(
|
||||
this.currentMetadata!.getMetadataResult.mediaCollection!.map(
|
||||
(it) => it.title
|
||||
)
|
||||
).toEqual(titles);
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { v4 as uuid } from "uuid";
|
||||
import request from "supertest";
|
||||
import { MusicService } from "../src/music_service";
|
||||
import makeServer from "../src/server";
|
||||
import { SONOS_DISABLED, Sonos, Device } from "../src/sonos";
|
||||
|
||||
@@ -185,4 +187,187 @@ describe("server", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("/stream", () => {
|
||||
const musicService = {
|
||||
login: jest.fn(),
|
||||
};
|
||||
const musicLibrary = {
|
||||
stream: jest.fn(),
|
||||
};
|
||||
const server = makeServer(
|
||||
(jest.fn() as unknown) as Sonos,
|
||||
aService(),
|
||||
"http://localhost:1234",
|
||||
(musicService as unknown) as MusicService
|
||||
);
|
||||
|
||||
const authToken = uuid();
|
||||
const trackId = uuid();
|
||||
|
||||
describe("when sonos does not ask for a range", () => {
|
||||
describe("when the music service returns a 200", () => {
|
||||
it("should return a 200 with the data", async () => {
|
||||
const stream = {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "audio/mp3",
|
||||
"content-length": "222",
|
||||
"accept-ranges": "bytes",
|
||||
"content-range": "-100",
|
||||
},
|
||||
data: Buffer.from("some track", "ascii"),
|
||||
};
|
||||
|
||||
musicService.login.mockResolvedValue(musicLibrary);
|
||||
musicLibrary.stream.mockResolvedValue(stream);
|
||||
|
||||
const res = await request(server)
|
||||
.get(`/stream/track/${trackId}`)
|
||||
.set("bonob-token", authToken);
|
||||
|
||||
console.log("testing finished watiting");
|
||||
|
||||
expect(res.status).toEqual(stream.status);
|
||||
expect(res.header["content-type"]).toEqual(
|
||||
stream.headers["content-type"]
|
||||
);
|
||||
expect(res.header["accept-ranges"]).toEqual(
|
||||
stream.headers["accept-ranges"]
|
||||
);
|
||||
expect(res.header["content-range"]).toEqual(
|
||||
stream.headers["content-range"]
|
||||
);
|
||||
|
||||
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
||||
expect(musicLibrary.stream).toHaveBeenCalledWith({ trackId });
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the music service returns a 206", () => {
|
||||
it("should return a 206 with the data", async () => {
|
||||
const stream = {
|
||||
status: 206,
|
||||
headers: {
|
||||
"content-type": "audio/ogg",
|
||||
"content-length": "333",
|
||||
"accept-ranges": "bytez",
|
||||
"content-range": "100-200",
|
||||
},
|
||||
data: Buffer.from("some other track", "ascii"),
|
||||
};
|
||||
|
||||
musicService.login.mockResolvedValue(musicLibrary);
|
||||
musicLibrary.stream.mockResolvedValue(stream);
|
||||
|
||||
const res = await request(server)
|
||||
.get(`/stream/track/${trackId}`)
|
||||
.set("bonob-token", authToken);
|
||||
|
||||
console.log("testing finished watiting");
|
||||
|
||||
expect(res.status).toEqual(stream.status);
|
||||
expect(res.header["content-type"]).toEqual(
|
||||
stream.headers["content-type"]
|
||||
);
|
||||
// expect(res.header["content-length"]).toEqual(stream.headers["content-length"]);
|
||||
expect(res.header["accept-ranges"]).toEqual(
|
||||
stream.headers["accept-ranges"]
|
||||
);
|
||||
expect(res.header["content-range"]).toEqual(
|
||||
stream.headers["content-range"]
|
||||
);
|
||||
|
||||
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
||||
expect(musicLibrary.stream).toHaveBeenCalledWith({ trackId });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when sonos does ask for a range", () => {
|
||||
describe("when the music service returns a 200", () => {
|
||||
it("should return a 200 with the data", async () => {
|
||||
const stream = {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "audio/mp3",
|
||||
"content-length": "222",
|
||||
"accept-ranges": "bytes",
|
||||
"content-range": "-100",
|
||||
},
|
||||
data: Buffer.from("some track", "ascii"),
|
||||
};
|
||||
|
||||
musicService.login.mockResolvedValue(musicLibrary);
|
||||
musicLibrary.stream.mockResolvedValue(stream);
|
||||
|
||||
const res = await request(server)
|
||||
.get(`/stream/track/${trackId}`)
|
||||
.set("bonob-token", authToken)
|
||||
.set("Range", "3000-4000");
|
||||
|
||||
console.log("testing finished watiting");
|
||||
|
||||
expect(res.status).toEqual(stream.status);
|
||||
expect(res.header["content-type"]).toEqual(
|
||||
stream.headers["content-type"]
|
||||
);
|
||||
expect(res.header["accept-ranges"]).toEqual(
|
||||
stream.headers["accept-ranges"]
|
||||
);
|
||||
expect(res.header["content-range"]).toEqual(
|
||||
stream.headers["content-range"]
|
||||
);
|
||||
|
||||
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
||||
expect(musicLibrary.stream).toHaveBeenCalledWith({
|
||||
trackId,
|
||||
range: "3000-4000",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the music service returns a 206", () => {
|
||||
it("should return a 206 with the data", async () => {
|
||||
const stream = {
|
||||
status: 206,
|
||||
headers: {
|
||||
"content-type": "audio/ogg",
|
||||
"content-length": "333",
|
||||
"accept-ranges": "bytez",
|
||||
"content-range": "100-200",
|
||||
},
|
||||
data: Buffer.from("some other track", "ascii"),
|
||||
};
|
||||
|
||||
musicService.login.mockResolvedValue(musicLibrary);
|
||||
musicLibrary.stream.mockResolvedValue(stream);
|
||||
|
||||
const res = await request(server)
|
||||
.get(`/stream/track/${trackId}`)
|
||||
.set("bonob-token", authToken)
|
||||
.set("Range", "4000-5000");
|
||||
|
||||
console.log("testing finished watiting");
|
||||
|
||||
expect(res.status).toEqual(stream.status);
|
||||
expect(res.header["content-type"]).toEqual(
|
||||
stream.headers["content-type"]
|
||||
);
|
||||
expect(res.header["accept-ranges"]).toEqual(
|
||||
stream.headers["accept-ranges"]
|
||||
);
|
||||
expect(res.header["content-range"]).toEqual(
|
||||
stream.headers["content-range"]
|
||||
);
|
||||
|
||||
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
||||
expect(musicLibrary.stream).toHaveBeenCalledWith({
|
||||
trackId,
|
||||
range: "4000-5000",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2,11 +2,17 @@ import crypto from "crypto";
|
||||
import request from "supertest";
|
||||
import { Client, createClientAsync } from "soap";
|
||||
import X2JS from "x2js";
|
||||
import { v4 as uuid } from "uuid";
|
||||
|
||||
import { InMemoryLinkCodes, LinkCodes } from "../src/link_codes";
|
||||
import makeServer from "../src/server";
|
||||
import { bonobService, SONOS_DISABLED } from "../src/sonos";
|
||||
import { STRINGS_ROUTE, LOGIN_ROUTE, getMetadataResult } from "../src/smapi";
|
||||
import {
|
||||
STRINGS_ROUTE,
|
||||
LOGIN_ROUTE,
|
||||
getMetadataResult,
|
||||
getMetadataResult2,
|
||||
} from "../src/smapi";
|
||||
|
||||
import {
|
||||
aService,
|
||||
@@ -276,7 +282,7 @@ describe("api", () => {
|
||||
);
|
||||
|
||||
describe("when no credentials header provided", () => {
|
||||
it("should return a fault of LoginUnauthorized", async () => {
|
||||
it("should return a fault of LoginUnsupported", async () => {
|
||||
const ws = await createClientAsync(`${service.uri}?wsdl`, {
|
||||
endpoint: service.uri,
|
||||
httpClient: supersoap(server, rootUrl),
|
||||
@@ -295,7 +301,7 @@ describe("api", () => {
|
||||
});
|
||||
|
||||
describe("when invalid credentials are provided", () => {
|
||||
it("should return a fault of LoginInvalid", async () => {
|
||||
it("should return a fault of LoginUnauthorized", async () => {
|
||||
const username = "userThatGetsDeleted";
|
||||
const password = "password1";
|
||||
musicService.hasUser({ username, password });
|
||||
@@ -640,11 +646,11 @@ describe("api", () => {
|
||||
albums: [album],
|
||||
});
|
||||
|
||||
const track1 = aTrack({ artist, album, number: "1" });
|
||||
const track2 = aTrack({ artist, album, number: "2" });
|
||||
const track3 = aTrack({ artist, album, number: "3" });
|
||||
const track4 = aTrack({ artist, album, number: "4" });
|
||||
const track5 = aTrack({ artist, album, number: "5" });
|
||||
const track1 = aTrack({ artist, album, number: 1 });
|
||||
const track2 = aTrack({ artist, album, number: 2 });
|
||||
const track3 = aTrack({ artist, album, number: 3 });
|
||||
const track4 = aTrack({ artist, album, number: 4 });
|
||||
const track5 = aTrack({ artist, album, number: 5 });
|
||||
|
||||
beforeEach(() => {
|
||||
musicService.hasArtists(artist);
|
||||
@@ -659,33 +665,29 @@ describe("api", () => {
|
||||
count: 100,
|
||||
});
|
||||
expect(result[0]).toEqual(
|
||||
getMetadataResult({
|
||||
mediaCollection: [
|
||||
track1,
|
||||
track2,
|
||||
track3,
|
||||
track4,
|
||||
track5,
|
||||
].map((track) => ({
|
||||
itemType: "track",
|
||||
id: `track:${track.id}`,
|
||||
mimeType: track.mimeType,
|
||||
title: track.name,
|
||||
getMetadataResult2({
|
||||
mediaMetadata: [track1, track2, track3, track4, track5].map(
|
||||
(track) => ({
|
||||
itemType: "track",
|
||||
id: `track:${track.id}`,
|
||||
mimeType: track.mimeType,
|
||||
title: track.name,
|
||||
|
||||
trackMetadata: {
|
||||
album: track.album.name,
|
||||
albumId: track.album.id,
|
||||
albumArtist: track.artist.name,
|
||||
albumArtistId: track.artist.id,
|
||||
// albumArtURI
|
||||
artist: track.artist.name,
|
||||
artistId: track.artist.id,
|
||||
duration: track.duration,
|
||||
genre: track.album.genre,
|
||||
// genreId
|
||||
trackNumber: track.number,
|
||||
},
|
||||
})),
|
||||
trackMetadata: {
|
||||
album: track.album.name,
|
||||
albumId: track.album.id,
|
||||
albumArtist: track.artist.name,
|
||||
albumArtistId: track.artist.id,
|
||||
// albumArtURI
|
||||
artist: track.artist.name,
|
||||
artistId: track.artist.id,
|
||||
duration: track.duration,
|
||||
genre: track.album.genre,
|
||||
// genreId
|
||||
trackNumber: track.number,
|
||||
},
|
||||
})
|
||||
),
|
||||
index: 0,
|
||||
total: 5,
|
||||
})
|
||||
@@ -701,11 +703,8 @@ describe("api", () => {
|
||||
count: 2,
|
||||
});
|
||||
expect(result[0]).toEqual(
|
||||
getMetadataResult({
|
||||
mediaCollection: [
|
||||
track3,
|
||||
track4,
|
||||
].map((track) => ({
|
||||
getMetadataResult2({
|
||||
mediaMetadata: [track3, track4].map((track) => ({
|
||||
itemType: "track",
|
||||
id: `track:${track.id}`,
|
||||
mimeType: track.mimeType,
|
||||
@@ -735,5 +734,217 @@ describe("api", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getMediaURI", () => {
|
||||
const server = makeServer(
|
||||
SONOS_DISABLED,
|
||||
service,
|
||||
rootUrl,
|
||||
musicService,
|
||||
linkCodes
|
||||
);
|
||||
|
||||
describe("when no credentials header provided", () => {
|
||||
it("should return a fault of LoginUnsupported", async () => {
|
||||
const ws = await createClientAsync(`${service.uri}?wsdl`, {
|
||||
endpoint: service.uri,
|
||||
httpClient: supersoap(server, rootUrl),
|
||||
});
|
||||
|
||||
await ws
|
||||
.getMediaURIAsync({ id: "track:123" })
|
||||
.then(() => fail("shouldnt get here"))
|
||||
.catch((e: any) => {
|
||||
expect(e.root.Envelope.Body.Fault).toEqual({
|
||||
faultcode: "Client.LoginUnsupported",
|
||||
faultstring: "Missing credentials...",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when invalid credentials are provided", () => {
|
||||
it("should return a fault of LoginUnauthorized", async () => {
|
||||
const username = "userThatGetsDeleted";
|
||||
const password = "password1";
|
||||
musicService.hasUser({ username, password });
|
||||
const token = (await musicService.generateToken({
|
||||
username,
|
||||
password,
|
||||
})) as AuthSuccess;
|
||||
musicService.hasNoUsers();
|
||||
|
||||
const ws = await createClientAsync(`${service.uri}?wsdl`, {
|
||||
endpoint: service.uri,
|
||||
httpClient: supersoap(server, rootUrl),
|
||||
});
|
||||
|
||||
ws.addSoapHeader({ credentials: someCredentials(token.authToken) });
|
||||
await ws
|
||||
.getMediaURIAsync({ id: "track:123" })
|
||||
.then(() => fail("shouldnt get here"))
|
||||
.catch((e: any) => {
|
||||
expect(e.root.Envelope.Body.Fault).toEqual({
|
||||
faultcode: "Client.LoginUnauthorized",
|
||||
faultstring: "Credentials not found...",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when valid credentials are provided", () => {
|
||||
const username = "validUser";
|
||||
const password = "validPassword";
|
||||
let token: AuthSuccess;
|
||||
let ws: Client;
|
||||
|
||||
beforeEach(async () => {
|
||||
musicService.hasUser({ username, password });
|
||||
token = (await musicService.generateToken({
|
||||
username,
|
||||
password,
|
||||
})) as AuthSuccess;
|
||||
ws = await createClientAsync(`${service.uri}?wsdl`, {
|
||||
endpoint: service.uri,
|
||||
httpClient: supersoap(server, rootUrl),
|
||||
});
|
||||
ws.addSoapHeader({ credentials: someCredentials(token.authToken) });
|
||||
});
|
||||
|
||||
describe("asking for a URI to stream a track", () => {
|
||||
it("should return it with auth header", async () => {
|
||||
const trackId = uuid();
|
||||
|
||||
const root = await ws.getMediaURIAsync({
|
||||
id: `track:${trackId}`,
|
||||
});
|
||||
expect(root[0]).toEqual({
|
||||
getMediaURIResult: `${rootUrl}/stream/track/${trackId}`,
|
||||
httpHeaders: {
|
||||
header: "bonob-token",
|
||||
value: token.authToken,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("getMediaMetadata", () => {
|
||||
const server = makeServer(
|
||||
SONOS_DISABLED,
|
||||
service,
|
||||
rootUrl,
|
||||
musicService,
|
||||
linkCodes
|
||||
);
|
||||
|
||||
describe("when no credentials header provided", () => {
|
||||
it("should return a fault of LoginUnsupported", async () => {
|
||||
const ws = await createClientAsync(`${service.uri}?wsdl`, {
|
||||
endpoint: service.uri,
|
||||
httpClient: supersoap(server, rootUrl),
|
||||
});
|
||||
|
||||
await ws
|
||||
.getMediaMetadataAsync({ id: "track:123" })
|
||||
.then(() => fail("shouldnt get here"))
|
||||
.catch((e: any) => {
|
||||
expect(e.root.Envelope.Body.Fault).toEqual({
|
||||
faultcode: "Client.LoginUnsupported",
|
||||
faultstring: "Missing credentials...",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when invalid credentials are provided", () => {
|
||||
it("should return a fault of LoginUnauthorized", async () => {
|
||||
const username = "userThatGetsDeleted";
|
||||
const password = "password1";
|
||||
musicService.hasUser({ username, password });
|
||||
const token = (await musicService.generateToken({
|
||||
username,
|
||||
password,
|
||||
})) as AuthSuccess;
|
||||
musicService.hasNoUsers();
|
||||
|
||||
const ws = await createClientAsync(`${service.uri}?wsdl`, {
|
||||
endpoint: service.uri,
|
||||
httpClient: supersoap(server, rootUrl),
|
||||
});
|
||||
|
||||
ws.addSoapHeader({ credentials: someCredentials(token.authToken) });
|
||||
await ws
|
||||
.getMediaMetadataAsync({ id: "track:123" })
|
||||
.then(() => fail("shouldnt get here"))
|
||||
.catch((e: any) => {
|
||||
expect(e.root.Envelope.Body.Fault).toEqual({
|
||||
faultcode: "Client.LoginUnauthorized",
|
||||
faultstring: "Credentials not found...",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when valid credentials are provided", () => {
|
||||
const username = "validUser";
|
||||
const password = "validPassword";
|
||||
let token: AuthSuccess;
|
||||
let ws: Client;
|
||||
|
||||
const album = anAlbum();
|
||||
const artist = anArtist({
|
||||
albums: [album],
|
||||
});
|
||||
const track = aTrack();
|
||||
|
||||
beforeEach(async () => {
|
||||
musicService.hasUser({ username, password });
|
||||
token = (await musicService.generateToken({
|
||||
username,
|
||||
password,
|
||||
})) as AuthSuccess;
|
||||
ws = await createClientAsync(`${service.uri}?wsdl`, {
|
||||
endpoint: service.uri,
|
||||
httpClient: supersoap(server, rootUrl),
|
||||
});
|
||||
ws.addSoapHeader({ credentials: someCredentials(token.authToken) });
|
||||
|
||||
musicService.hasArtists(artist);
|
||||
musicService.hasTracks(track);
|
||||
});
|
||||
|
||||
describe("asking for media metadata for a tack", () => {
|
||||
it("should return it with auth header", async () => {
|
||||
const root = await ws.getMediaMetadataAsync({
|
||||
id: `track:${track.id}`,
|
||||
});
|
||||
expect(root[0]).toEqual({
|
||||
getMediaMetadataResult: {
|
||||
itemType: "track",
|
||||
id: `track:${track.id}`,
|
||||
mimeType: track.mimeType,
|
||||
title: track.name,
|
||||
|
||||
trackMetadata: {
|
||||
album: track.album.name,
|
||||
albumId: track.album.id,
|
||||
albumArtist: track.artist.name,
|
||||
albumArtistId: track.artist.id,
|
||||
// albumArtURI
|
||||
artist: track.artist.name,
|
||||
artistId: track.artist.id,
|
||||
duration: track.duration,
|
||||
genre: track.album.genre,
|
||||
// genreId
|
||||
trackNumber: track.number,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user