mirror of
https://github.com/wkulhanek/bonob.git
synced 2025-12-21 17:33:29 +01:00
Icons for root menu
This commit is contained in:
@@ -3717,7 +3717,6 @@ describe("Navidrome", () => {
|
||||
const id = "idWithNoTracks";
|
||||
|
||||
const xml = similarSongsXml([]);
|
||||
console.log(`xml = ${xml}`)
|
||||
mockGET
|
||||
.mockImplementationOnce(() => Promise.resolve(ok(PING_OK)))
|
||||
.mockImplementationOnce(() =>
|
||||
|
||||
@@ -138,8 +138,6 @@ class SonosDriver {
|
||||
return m![1]!;
|
||||
});
|
||||
|
||||
console.log(`posting to action ${action}`);
|
||||
|
||||
return request(this.server)
|
||||
.post(action)
|
||||
.type("form")
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { v4 as uuid } from "uuid";
|
||||
import dayjs from "dayjs";
|
||||
import request from "supertest";
|
||||
import Image from "image-js";
|
||||
|
||||
import { MusicService } from "../src/music_service";
|
||||
import makeServer, {
|
||||
BONOB_ACCESS_TOKEN_HEADER,
|
||||
ICONS,
|
||||
RangeBytesFromFilter,
|
||||
rangeFilterFor,
|
||||
} from "../src/server";
|
||||
|
||||
import { SONOS_DISABLED, Sonos, Device } from "../src/sonos";
|
||||
|
||||
import { aDevice, aService } from "./builders";
|
||||
@@ -17,6 +21,9 @@ import { Response } from "express";
|
||||
import { Transform } from "stream";
|
||||
import url from "../src/url_builder";
|
||||
import i8n, { randomLang } from "../src/i8n";
|
||||
import {
|
||||
SONOS_RECOMMENDED_IMAGE_SIZES,
|
||||
} from "../src/smapi";
|
||||
|
||||
describe("rangeFilterFor", () => {
|
||||
describe("invalid range header string", () => {
|
||||
@@ -278,10 +285,16 @@ describe("server", () => {
|
||||
.set("accept-language", acceptLanguage)
|
||||
.send();
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.text).toMatch(`<input type="submit" value="${lang("register")}">`);
|
||||
expect(res.text).toMatch(
|
||||
`<input type="submit" value="${lang("register")}">`
|
||||
);
|
||||
expect(res.text).toMatch(`<h3>${lang("expectedConfig")}</h3>`);
|
||||
expect(res.text).toMatch(`<h3>${lang("noExistingServiceRegistration")}</h3>`);
|
||||
expect(res.text).not.toMatch(`<input type="submit" value="${lang("removeRegistration")}">`);
|
||||
expect(res.text).toMatch(
|
||||
`<h3>${lang("noExistingServiceRegistration")}</h3>`
|
||||
);
|
||||
expect(res.text).not.toMatch(
|
||||
`<input type="submit" value="${lang("removeRegistration")}">`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -317,10 +330,16 @@ describe("server", () => {
|
||||
.set("accept-language", acceptLanguage)
|
||||
.send();
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.text).toMatch(`<input type="submit" value="${lang("register")}">`);
|
||||
expect(res.text).toMatch(
|
||||
`<input type="submit" value="${lang("register")}">`
|
||||
);
|
||||
expect(res.text).toMatch(`<h3>${lang("expectedConfig")}</h3>`);
|
||||
expect(res.text).toMatch(`<h3>${lang("existingServiceConfig")}</h3>`);
|
||||
expect(res.text).toMatch(`<input type="submit" value="${lang("removeRegistration")}">`);
|
||||
expect(res.text).toMatch(
|
||||
`<h3>${lang("existingServiceConfig")}</h3>`
|
||||
);
|
||||
expect(res.text).toMatch(
|
||||
`<input type="submit" value="${lang("removeRegistration")}">`
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -343,15 +362,16 @@ describe("server", () => {
|
||||
|
||||
it("should report some information about the service", async () => {
|
||||
const res = await request(server)
|
||||
.get(bonobUrl.append({ pathname: "/about" }).path())
|
||||
.send();
|
||||
.get(bonobUrl.append({ pathname: "/about" }).path())
|
||||
.send();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.body).toEqual({
|
||||
service: {
|
||||
name: theService.name,
|
||||
sid: theService.sid
|
||||
}});
|
||||
sid: theService.sid,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -375,21 +395,21 @@ describe("server", () => {
|
||||
describe("when is successful", () => {
|
||||
it("should return a nice message", async () => {
|
||||
sonos.register.mockResolvedValue(true);
|
||||
|
||||
|
||||
const res = await request(server)
|
||||
.post(bonobUrl.append({ pathname: "/registration/add" }).path())
|
||||
.set("accept-language", acceptLanguage)
|
||||
.send();
|
||||
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.text).toMatch(`<title>${lang("success")}</title>`);
|
||||
expect(res.text).toMatch(lang("successfullyRegistered"));
|
||||
|
||||
|
||||
expect(sonos.register.mock.calls.length).toEqual(1);
|
||||
expect(sonos.register.mock.calls[0][0]).toBe(theService);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("when is unsuccessful", () => {
|
||||
it("should return a failure message", async () => {
|
||||
sonos.register.mockResolvedValue(false);
|
||||
@@ -398,11 +418,11 @@ describe("server", () => {
|
||||
.post(bonobUrl.append({ pathname: "/registration/add" }).path())
|
||||
.set("accept-language", acceptLanguage)
|
||||
.send();
|
||||
|
||||
|
||||
expect(res.status).toEqual(500);
|
||||
expect(res.text).toMatch(`<title>${lang("failure")}</title>`);
|
||||
expect(res.text).toMatch(lang("registrationFailed"));
|
||||
|
||||
|
||||
expect(sonos.register.mock.calls.length).toEqual(1);
|
||||
expect(sonos.register.mock.calls[0][0]).toBe(theService);
|
||||
});
|
||||
@@ -413,34 +433,38 @@ describe("server", () => {
|
||||
describe("when is successful", () => {
|
||||
it("should return a nice message", async () => {
|
||||
sonos.remove.mockResolvedValue(true);
|
||||
|
||||
|
||||
const res = await request(server)
|
||||
.post(bonobUrl.append({ pathname: "/registration/remove" }).path())
|
||||
.post(
|
||||
bonobUrl.append({ pathname: "/registration/remove" }).path()
|
||||
)
|
||||
.set("accept-language", acceptLanguage)
|
||||
.send();
|
||||
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.text).toMatch(`<title>${lang("success")}</title>`);
|
||||
expect(res.text).toMatch(lang("successfullyRemovedRegistration"));
|
||||
|
||||
|
||||
expect(sonos.remove.mock.calls.length).toEqual(1);
|
||||
expect(sonos.remove.mock.calls[0][0]).toBe(theService.sid);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
describe("when is unsuccessful", () => {
|
||||
it("should return a failure message", async () => {
|
||||
sonos.remove.mockResolvedValue(false);
|
||||
|
||||
|
||||
const res = await request(server)
|
||||
.post(bonobUrl.append({ pathname: "/registration/remove" }).path())
|
||||
.post(
|
||||
bonobUrl.append({ pathname: "/registration/remove" }).path()
|
||||
)
|
||||
.set("accept-language", acceptLanguage)
|
||||
.send();
|
||||
|
||||
|
||||
expect(res.status).toEqual(500);
|
||||
expect(res.text).toMatch(`<title>${lang("failure")}</title>`);
|
||||
expect(res.text).toMatch(lang("failedToRemoveRegistration"));
|
||||
|
||||
|
||||
expect(sonos.remove.mock.calls.length).toEqual(1);
|
||||
expect(sonos.remove.mock.calls[0][0]).toBe(theService.sid);
|
||||
});
|
||||
@@ -454,7 +478,7 @@ describe("server", () => {
|
||||
remove: jest.fn(),
|
||||
};
|
||||
const theService = aService({
|
||||
name: serviceNameForLang
|
||||
name: serviceNameForLang,
|
||||
});
|
||||
|
||||
const musicService = {
|
||||
@@ -474,7 +498,6 @@ describe("server", () => {
|
||||
const clock = {
|
||||
now: jest.fn(),
|
||||
};
|
||||
|
||||
|
||||
const server = makeServer(
|
||||
sonos as unknown as Sonos,
|
||||
@@ -496,10 +519,18 @@ describe("server", () => {
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.text).toMatch(`<title>${lang("login")}</title>`);
|
||||
expect(res.text).toMatch(`<h1 class="login one-word-per-line">${lang("logInToBonob")}</h1>`);
|
||||
expect(res.text).toMatch(`<label for="username">${lang("username")}:</label>`);
|
||||
expect(res.text).toMatch(`<label for="password">${lang("password")}:</label>`);
|
||||
expect(res.text).toMatch(`<input type="submit" value="${lang("login")}" id="submit">`);
|
||||
expect(res.text).toMatch(
|
||||
`<h1 class="login one-word-per-line">${lang("logInToBonob")}</h1>`
|
||||
);
|
||||
expect(res.text).toMatch(
|
||||
`<label for="username">${lang("username")}:</label>`
|
||||
);
|
||||
expect(res.text).toMatch(
|
||||
`<label for="password">${lang("password")}:</label>`
|
||||
);
|
||||
expect(res.text).toMatch(
|
||||
`<input type="submit" value="${lang("login")}" id="submit">`
|
||||
);
|
||||
});
|
||||
|
||||
describe("when the credentials are valid", () => {
|
||||
@@ -578,7 +609,7 @@ describe("server", () => {
|
||||
expect(res.text).toContain(lang("invalidLinkCode"));
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("/stream", () => {
|
||||
const musicService = {
|
||||
@@ -1011,7 +1042,7 @@ describe("server", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("art", () => {
|
||||
describe("/art", () => {
|
||||
const musicService = {
|
||||
login: jest.fn(),
|
||||
};
|
||||
@@ -1040,7 +1071,7 @@ describe("server", () => {
|
||||
|
||||
describe("when there is no access-token", () => {
|
||||
it("should return a 401", async () => {
|
||||
const res = await request(server).get(`/album/123/art/size/180`);
|
||||
const res = await request(server).get(`/art/album/123/size/180`);
|
||||
|
||||
expect(res.status).toEqual(401);
|
||||
});
|
||||
@@ -1051,7 +1082,7 @@ describe("server", () => {
|
||||
now = now.add(1, "day");
|
||||
|
||||
const res = await request(server).get(
|
||||
`/album/123/art/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
|
||||
`/art/album/123/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
|
||||
);
|
||||
|
||||
expect(res.status).toEqual(401);
|
||||
@@ -1063,7 +1094,7 @@ describe("server", () => {
|
||||
it("should return a 400", async () => {
|
||||
const res = await request(server)
|
||||
.get(
|
||||
`/foo/${albumId}/art/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
|
||||
`/art/foo/${albumId}/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
|
||||
)
|
||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
||||
|
||||
@@ -1072,6 +1103,21 @@ describe("server", () => {
|
||||
});
|
||||
|
||||
describe("artist art", () => {
|
||||
["0", "-1", "foo"].forEach((size) => {
|
||||
describe(`when the size is ${size}`, () => {
|
||||
it(`should return a 400`, async () => {
|
||||
musicService.login.mockResolvedValue(musicLibrary);
|
||||
const res = await request(server)
|
||||
.get(
|
||||
`/art/artist/${albumId}/size/${size}?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
|
||||
)
|
||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
||||
|
||||
expect(res.status).toEqual(400);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when there is some", () => {
|
||||
it("should return the image and a 200", async () => {
|
||||
const coverArt = {
|
||||
@@ -1086,7 +1132,7 @@ describe("server", () => {
|
||||
|
||||
const res = await request(server)
|
||||
.get(
|
||||
`/artist/${albumId}/art/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
|
||||
`/art/artist/${albumId}/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
|
||||
)
|
||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
||||
|
||||
@@ -1112,7 +1158,7 @@ describe("server", () => {
|
||||
|
||||
const res = await request(server)
|
||||
.get(
|
||||
`/artist/${albumId}/art/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
|
||||
`/art/artist/${albumId}/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
|
||||
)
|
||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
||||
|
||||
@@ -1128,7 +1174,7 @@ describe("server", () => {
|
||||
|
||||
const res = await request(server)
|
||||
.get(
|
||||
`/artist/${albumId}/art/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
|
||||
`/art/artist/${albumId}/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
|
||||
)
|
||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
||||
|
||||
@@ -1138,6 +1184,21 @@ describe("server", () => {
|
||||
});
|
||||
|
||||
describe("album art", () => {
|
||||
["0", "-1", "foo"].forEach((size) => {
|
||||
describe(`when the size is ${size}`, () => {
|
||||
it(`should return a 400`, async () => {
|
||||
musicService.login.mockResolvedValue(musicLibrary);
|
||||
const res = await request(server)
|
||||
.get(
|
||||
`/art/album/${albumId}/size/${size}?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
|
||||
)
|
||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
||||
|
||||
expect(res.status).toEqual(400);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when there is some", () => {
|
||||
it("should return the image and a 200", async () => {
|
||||
const coverArt = {
|
||||
@@ -1151,7 +1212,7 @@ describe("server", () => {
|
||||
|
||||
const res = await request(server)
|
||||
.get(
|
||||
`/album/${albumId}/art/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
|
||||
`/art/album/${albumId}/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
|
||||
)
|
||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
||||
|
||||
@@ -1176,7 +1237,7 @@ describe("server", () => {
|
||||
|
||||
const res = await request(server)
|
||||
.get(
|
||||
`/album/${albumId}/art/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
|
||||
`/art/album/${albumId}/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
|
||||
)
|
||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
||||
|
||||
@@ -1191,7 +1252,7 @@ describe("server", () => {
|
||||
|
||||
const res = await request(server)
|
||||
.get(
|
||||
`/album/${albumId}/art/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
|
||||
`/art/album/${albumId}/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
|
||||
)
|
||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
||||
|
||||
@@ -1201,6 +1262,94 @@ describe("server", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("/icon", () => {
|
||||
const server = makeServer(
|
||||
jest.fn() as unknown as Sonos,
|
||||
aService(),
|
||||
url("http://localhost:1234"),
|
||||
jest.fn() as unknown as MusicService,
|
||||
new InMemoryLinkCodes(),
|
||||
jest.fn() as unknown as AccessTokens
|
||||
);
|
||||
|
||||
describe("invalid icon names", () => {
|
||||
[
|
||||
"..%2F..%2Ffoo",
|
||||
"%2Fetc%2Fpasswd",
|
||||
".%2Fbob.js",
|
||||
".",
|
||||
"..",
|
||||
"1",
|
||||
"%23%24",
|
||||
"notAValidIcon",
|
||||
].forEach((type) => {
|
||||
describe(`trying to retrieve an icon with name ${type}`, () => {
|
||||
it(`should fail`, async () => {
|
||||
const response = await request(server).get(
|
||||
`/icon/${type}/size/legacy`
|
||||
);
|
||||
|
||||
expect(response.status).toEqual(404);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("invalid size", () => {
|
||||
["-1", "0", "59", "foo"].forEach((size) => {
|
||||
describe(`trying to retrieve an icon with size ${size}`, () => {
|
||||
it(`should fail`, async () => {
|
||||
const response = await request(server).get(
|
||||
`/icon/artists/size/${size}`
|
||||
);
|
||||
|
||||
expect(response.status).toEqual(400);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("fetching", () => {
|
||||
Object.keys(ICONS).forEach((type) => {
|
||||
describe(`type=${type}`, () => {
|
||||
describe(`legacy icon`, () => {
|
||||
it("should return the png image", async () => {
|
||||
const response = await request(server).get(
|
||||
`/icon/${type}/size/legacy`
|
||||
);
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.header["content-type"]).toEqual("image/png");
|
||||
const image = await Image.load(response.body);
|
||||
expect(image.width).toEqual(80);
|
||||
expect(image.height).toEqual(80);
|
||||
});
|
||||
});
|
||||
|
||||
describe("svg icon", () => {
|
||||
SONOS_RECOMMENDED_IMAGE_SIZES.forEach((size) => {
|
||||
it(`should return an svg image for size = ${size}`, async () => {
|
||||
const response = await request(server).get(
|
||||
`/icon/${type}/size/${size}`
|
||||
);
|
||||
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.header["content-type"]).toEqual(
|
||||
"image/svg+xml; charset=utf-8"
|
||||
);
|
||||
const svg = Buffer.from(response.body).toString();
|
||||
expect(svg).toContain(`viewBox="0 0 ${size} ${size}"`);
|
||||
expect(svg).toContain(
|
||||
` xmlns="http://www.w3.org/2000/svg" `
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,6 +21,7 @@ import {
|
||||
defaultAlbumArtURI,
|
||||
defaultArtistArtURI,
|
||||
searchResult,
|
||||
iconArtURI,
|
||||
} from "../src/smapi";
|
||||
|
||||
import {
|
||||
@@ -54,83 +55,109 @@ describe("service config", () => {
|
||||
const bonobWithContextPath = url("http://localhost:5678/some-context-path");
|
||||
|
||||
[bonobWithNoContextPath, bonobWithContextPath].forEach((bonobUrl) => {
|
||||
const server = makeServer(
|
||||
SONOS_DISABLED,
|
||||
aService({ name: "music land" }),
|
||||
bonobUrl,
|
||||
new InMemoryMusicService()
|
||||
);
|
||||
describe(bonobUrl.href(), () => {
|
||||
const server = makeServer(
|
||||
SONOS_DISABLED,
|
||||
aService({ name: "music land" }),
|
||||
bonobUrl,
|
||||
new InMemoryMusicService()
|
||||
);
|
||||
|
||||
const stringsUrl = bonobUrl.append({ pathname: STRINGS_ROUTE });
|
||||
const presentationUrl = bonobUrl.append({
|
||||
pathname: PRESENTATION_MAP_ROUTE,
|
||||
});
|
||||
|
||||
describe(`${stringsUrl}`, () => {
|
||||
async function fetchStringsXml() {
|
||||
const res = await request(server).get(stringsUrl.path()).send();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
|
||||
// removing the sonos xml ns as makes xpath queries with xpath-ts painful
|
||||
return parseXML(
|
||||
res.text.replace('xmlns="http://sonos.com/sonosapi"', "")
|
||||
);
|
||||
}
|
||||
|
||||
it("should return xml for the strings", async () => {
|
||||
const xml = await fetchStringsXml();
|
||||
|
||||
const sonosString = (id: string, lang: string) =>
|
||||
xpath.select(
|
||||
`string(/stringtables/stringtable[@xml:lang="${lang}"]/string[@stringId="${id}"])`,
|
||||
xml
|
||||
);
|
||||
|
||||
expect(sonosString("AppLinkMessage", "en-US")).toEqual(
|
||||
"Linking sonos with music land"
|
||||
);
|
||||
expect(sonosString("AppLinkMessage", "nl-NL")).toEqual(
|
||||
"Sonos koppelen aan music land"
|
||||
);
|
||||
|
||||
// no fr-FR translation, so use en-US
|
||||
expect(sonosString("AppLinkMessage", "fr-FR")).toEqual(
|
||||
"Linking sonos with music land"
|
||||
);
|
||||
const stringsUrl = bonobUrl.append({ pathname: STRINGS_ROUTE });
|
||||
const presentationUrl = bonobUrl.append({
|
||||
pathname: PRESENTATION_MAP_ROUTE,
|
||||
});
|
||||
|
||||
it("should return a section for all sonos supported languages", async () => {
|
||||
const xml = await fetchStringsXml();
|
||||
SONOS_LANG.forEach(lang => {
|
||||
expect(xpath.select(
|
||||
`string(/stringtables/stringtable[@xml:lang="${lang}"]/string[@stringId="AppLinkMessage"])`,
|
||||
xml
|
||||
)).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
describe(STRINGS_ROUTE, () => {
|
||||
async function fetchStringsXml() {
|
||||
const res = await request(server).get(stringsUrl.path()).send();
|
||||
|
||||
describe(`${presentationUrl}`, () => {
|
||||
it("should have an ArtWorkSizeMap for all sizes recommended by sonos", async () => {
|
||||
const res = await request(server).get(presentationUrl.path()).send();
|
||||
expect(res.status).toEqual(200);
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
// removing the sonos xml ns as makes xpath queries with xpath-ts painful
|
||||
return parseXML(
|
||||
res.text.replace('xmlns="http://sonos.com/sonosapi"', "")
|
||||
);
|
||||
}
|
||||
|
||||
// removing the sonos xml ns as makes xpath queries with xpath-ts painful
|
||||
const xml = parseXML(
|
||||
res.text.replace('xmlns="http://sonos.com/sonosapi"', "")
|
||||
);
|
||||
it("should return xml for the strings", async () => {
|
||||
const xml = await fetchStringsXml();
|
||||
|
||||
const imageSizeMap = (size: string) =>
|
||||
xpath.select(
|
||||
`string(/Presentation/PresentationMap[@type="ArtWorkSizeMap"]/Match/imageSizeMap/sizeEntry[@size="${size}"]/@substitution)`,
|
||||
xml
|
||||
const sonosString = (id: string, lang: string) =>
|
||||
xpath.select(
|
||||
`string(/stringtables/stringtable[@xml:lang="${lang}"]/string[@stringId="${id}"])`,
|
||||
xml
|
||||
);
|
||||
|
||||
expect(sonosString("AppLinkMessage", "en-US")).toEqual(
|
||||
"Linking sonos with music land"
|
||||
);
|
||||
expect(sonosString("AppLinkMessage", "nl-NL")).toEqual(
|
||||
"Sonos koppelen aan music land"
|
||||
);
|
||||
|
||||
SONOS_RECOMMENDED_IMAGE_SIZES.forEach((size) => {
|
||||
expect(imageSizeMap(size)).toEqual(`/art/size/${size}`);
|
||||
// no fr-FR translation, so use en-US
|
||||
expect(sonosString("AppLinkMessage", "fr-FR")).toEqual(
|
||||
"Linking sonos with music land"
|
||||
);
|
||||
});
|
||||
|
||||
it("should return a section for all sonos supported languages", async () => {
|
||||
const xml = await fetchStringsXml();
|
||||
SONOS_LANG.forEach((lang) => {
|
||||
expect(
|
||||
xpath.select(
|
||||
`string(/stringtables/stringtable[@xml:lang="${lang}"]/string[@stringId="AppLinkMessage"])`,
|
||||
xml
|
||||
)
|
||||
).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe(PRESENTATION_MAP_ROUTE, () => {
|
||||
it("should have an ArtWorkSizeMap for all sizes recommended by sonos", async () => {
|
||||
const res = await request(server).get(presentationUrl.path()).send();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
|
||||
// removing the sonos xml ns as makes xpath queries with xpath-ts painful
|
||||
const xml = parseXML(
|
||||
res.text.replace('xmlns="http://sonos.com/sonosapi"', "")
|
||||
);
|
||||
|
||||
const imageSizeMap = (size: string) =>
|
||||
xpath.select(
|
||||
`string(/Presentation/PresentationMap[@type="ArtWorkSizeMap"]/Match/imageSizeMap/sizeEntry[@size="${size}"]/@substitution)`,
|
||||
xml
|
||||
);
|
||||
|
||||
SONOS_RECOMMENDED_IMAGE_SIZES.forEach((size) => {
|
||||
expect(imageSizeMap(size)).toEqual(`/size/${size}`);
|
||||
});
|
||||
});
|
||||
|
||||
it("should have an BrowseIconSizeMap for all sizes recommended by sonos", async () => {
|
||||
const res = await request(server).get(presentationUrl.path()).send();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
|
||||
// removing the sonos xml ns as makes xpath queries with xpath-ts painful
|
||||
const xml = parseXML(
|
||||
res.text.replace('xmlns="http://sonos.com/sonosapi"', "")
|
||||
);
|
||||
|
||||
const imageSizeMap = (size: string) =>
|
||||
xpath.select(
|
||||
`string(/Presentation/PresentationMap[@type="BrowseIconSizeMap"]/Match/browseIconSizeMap/sizeEntry[@size="${size}"]/@substitution)`,
|
||||
xml
|
||||
);
|
||||
|
||||
SONOS_RECOMMENDED_IMAGE_SIZES.forEach((size) => {
|
||||
expect(imageSizeMap(size)).toEqual(`/size/${size}`);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -246,7 +273,7 @@ describe("track", () => {
|
||||
albumId: someTrack.album.id,
|
||||
albumArtist: someTrack.artist.name,
|
||||
albumArtistId: someTrack.artist.id,
|
||||
albumArtURI: `http://localhost:4567/foo/album/${someTrack.album.id}/art/size/180?access-token=1234`,
|
||||
albumArtURI: `http://localhost:4567/foo/art/album/${someTrack.album.id}/size/180?access-token=1234`,
|
||||
artist: someTrack.artist.name,
|
||||
artistId: someTrack.artist.id,
|
||||
duration: someTrack.duration,
|
||||
@@ -281,7 +308,7 @@ describe("defaultAlbumArtURI", () => {
|
||||
const album = anAlbum();
|
||||
|
||||
expect(defaultAlbumArtURI(bonobUrl, album).href()).toEqual(
|
||||
`http://localhost:1234/context-path/album/${album.id}/art/size/180?search=yes`
|
||||
`http://localhost:1234/context-path/art/album/${album.id}/size/180?search=yes`
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -292,7 +319,7 @@ describe("defaultArtistArtURI", () => {
|
||||
const artist = anArtist();
|
||||
|
||||
expect(defaultArtistArtURI(bonobUrl, artist).href()).toEqual(
|
||||
`http://localhost:1234/something/artist/${artist.id}/art/size/180?s=123`
|
||||
`http://localhost:1234/something/art/artist/${artist.id}/size/180?s=123`
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -450,7 +477,8 @@ describe("api", () => {
|
||||
.catch((e: any) => {
|
||||
expect(e.root.Envelope.Body.Fault).toEqual({
|
||||
faultcode: "Client.NOT_LINKED_RETRY",
|
||||
faultstring: "Link Code not found yet, sonos app will keep polling until you log in to bonob",
|
||||
faultstring:
|
||||
"Link Code not found yet, sonos app will keep polling until you log in to bonob",
|
||||
detail: {
|
||||
ExceptionInfo: "NOT_LINKED_RETRY",
|
||||
SonosError: "5",
|
||||
@@ -707,46 +735,72 @@ describe("api", () => {
|
||||
getMetadataResult({
|
||||
mediaCollection: [
|
||||
{
|
||||
itemType: "container",
|
||||
id: "artists",
|
||||
title: "Artists",
|
||||
albumArtURI: iconArtURI(bonobUrl, "artists").href(),
|
||||
itemType: "container",
|
||||
},
|
||||
{
|
||||
id: "albums",
|
||||
title: "Albums",
|
||||
albumArtURI: iconArtURI(bonobUrl, "albums").href(),
|
||||
itemType: "albumList",
|
||||
},
|
||||
{ itemType: "albumList", id: "albums", title: "Albums" },
|
||||
{
|
||||
itemType: "playlist",
|
||||
id: "playlists",
|
||||
title: "Playlists",
|
||||
albumArtURI: iconArtURI(bonobUrl, "playlists").href(),
|
||||
itemType: "playlist",
|
||||
attributes: {
|
||||
readOnly: "false",
|
||||
renameable: "false",
|
||||
userContent: "true",
|
||||
},
|
||||
},
|
||||
{ itemType: "container", id: "genres", title: "Genres" },
|
||||
{
|
||||
itemType: "albumList",
|
||||
id: "genres",
|
||||
title: "Genres",
|
||||
albumArtURI: iconArtURI(bonobUrl, "genres").href(),
|
||||
itemType: "container",
|
||||
},
|
||||
{
|
||||
id: "randomAlbums",
|
||||
title: "Random",
|
||||
albumArtURI: iconArtURI(bonobUrl, "random").href(),
|
||||
itemType: "albumList",
|
||||
},
|
||||
{
|
||||
itemType: "albumList",
|
||||
id: "starredAlbums",
|
||||
title: "Starred",
|
||||
albumArtURI: iconArtURI(bonobUrl, "starred").href(),
|
||||
itemType: "albumList",
|
||||
},
|
||||
{
|
||||
itemType: "albumList",
|
||||
id: "recentlyAdded",
|
||||
title: "Recently added",
|
||||
albumArtURI: iconArtURI(
|
||||
bonobUrl,
|
||||
"recentlyAdded"
|
||||
).href(),
|
||||
itemType: "albumList",
|
||||
},
|
||||
{
|
||||
itemType: "albumList",
|
||||
id: "recentlyPlayed",
|
||||
title: "Recently played",
|
||||
albumArtURI: iconArtURI(
|
||||
bonobUrl,
|
||||
"recentlyPlayed"
|
||||
).href(),
|
||||
itemType: "albumList",
|
||||
},
|
||||
{
|
||||
itemType: "albumList",
|
||||
id: "mostPlayed",
|
||||
title: "Most played",
|
||||
albumArtURI: iconArtURI(
|
||||
bonobUrl,
|
||||
"mostPlayed"
|
||||
).href(),
|
||||
itemType: "albumList",
|
||||
},
|
||||
],
|
||||
index: 0,
|
||||
@@ -758,7 +812,7 @@ describe("api", () => {
|
||||
|
||||
describe("when an accept-language header is present with value nl-NL", () => {
|
||||
it("should return nl-NL", async () => {
|
||||
ws.addHttpHeader("accept-language", "nl-NL, en-US;q=0.9")
|
||||
ws.addHttpHeader("accept-language", "nl-NL, en-US;q=0.9");
|
||||
const root = await ws.getMetadataAsync({
|
||||
id: "root",
|
||||
index: 0,
|
||||
@@ -768,46 +822,72 @@ describe("api", () => {
|
||||
getMetadataResult({
|
||||
mediaCollection: [
|
||||
{
|
||||
itemType: "container",
|
||||
id: "artists",
|
||||
title: "Artiesten",
|
||||
albumArtURI: iconArtURI(bonobUrl, "artists").href(),
|
||||
itemType: "container",
|
||||
},
|
||||
{
|
||||
id: "albums",
|
||||
title: "Albums",
|
||||
albumArtURI: iconArtURI(bonobUrl, "albums").href(),
|
||||
itemType: "albumList",
|
||||
},
|
||||
{ itemType: "albumList", id: "albums", title: "Albums" },
|
||||
{
|
||||
itemType: "playlist",
|
||||
id: "playlists",
|
||||
title: "Afspeellijsten",
|
||||
albumArtURI: iconArtURI(bonobUrl, "playlists").href(),
|
||||
itemType: "playlist",
|
||||
attributes: {
|
||||
readOnly: "false",
|
||||
renameable: "false",
|
||||
userContent: "true",
|
||||
},
|
||||
},
|
||||
{ itemType: "container", id: "genres", title: "Genres" },
|
||||
{
|
||||
itemType: "albumList",
|
||||
id: "genres",
|
||||
title: "Genres",
|
||||
albumArtURI: iconArtURI(bonobUrl, "genres").href(),
|
||||
itemType: "container",
|
||||
},
|
||||
{
|
||||
id: "randomAlbums",
|
||||
title: "Willekeurig",
|
||||
albumArtURI: iconArtURI(bonobUrl, "random").href(),
|
||||
itemType: "albumList",
|
||||
},
|
||||
{
|
||||
itemType: "albumList",
|
||||
id: "starredAlbums",
|
||||
title: "Favorieten",
|
||||
albumArtURI: iconArtURI(bonobUrl, "starred").href(),
|
||||
itemType: "albumList",
|
||||
},
|
||||
{
|
||||
itemType: "albumList",
|
||||
id: "recentlyAdded",
|
||||
title: "Onlangs toegevoegd",
|
||||
albumArtURI: iconArtURI(
|
||||
bonobUrl,
|
||||
"recentlyAdded"
|
||||
).href(),
|
||||
itemType: "albumList",
|
||||
},
|
||||
{
|
||||
itemType: "albumList",
|
||||
id: "recentlyPlayed",
|
||||
title: "Onlangs afgespeeld",
|
||||
albumArtURI: iconArtURI(
|
||||
bonobUrl,
|
||||
"recentlyPlayed"
|
||||
).href(),
|
||||
itemType: "albumList",
|
||||
},
|
||||
{
|
||||
itemType: "albumList",
|
||||
id: "mostPlayed",
|
||||
title: "Meest afgespeeld",
|
||||
albumArtURI: iconArtURI(
|
||||
bonobUrl,
|
||||
"mostPlayed"
|
||||
).href(),
|
||||
itemType: "albumList",
|
||||
},
|
||||
],
|
||||
index: 0,
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"strict": true,
|
||||
"noImplicitAny": false,
|
||||
"typeRoots" : [
|
||||
"../typings",
|
||||
"../node_modules/@types"
|
||||
]
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user