mirror of
https://github.com/wkulhanek/bonob.git
synced 2025-12-21 17:33:29 +01:00
Ability to register bonob service with sonos via button
This commit is contained in:
@@ -37,7 +37,8 @@ docker run \
|
|||||||
|
|
||||||
item | default value | description
|
item | default value | description
|
||||||
---- | ------------- | -----------
|
---- | ------------- | -----------
|
||||||
PORT | 4534 | Default http port for bonob to listen on
|
BONOB_PORT | 4534 | Default http port for bonob to listen on
|
||||||
|
BONOB_WEB_ADDRESS | http://localhost:4534 | Web address for bonob
|
||||||
BONOB_SONOS_SEED_HOST | undefined | sonos device seed host for auto-discovery, or 'disabled' to turn off device discovery entirely
|
BONOB_SONOS_SEED_HOST | undefined | sonos device seed host for auto-discovery, or 'disabled' to turn off device discovery entirely
|
||||||
BONOB_SONOS_SERVICE_NAME | bonob | service name for sonos
|
BONOB_SONOS_SERVICE_NAME | bonob | service name for sonos
|
||||||
BONOS_SONOS_SERVICE_ID | 246 | service id for sonos
|
BONOS_SONOS_SERVICE_ID | 246 | service id for sonos
|
||||||
|
|||||||
@@ -10,12 +10,15 @@
|
|||||||
"@types/express": "^4.17.11",
|
"@types/express": "^4.17.11",
|
||||||
"@types/node": "^14.14.22",
|
"@types/node": "^14.14.22",
|
||||||
"@types/underscore": "1.10.24",
|
"@types/underscore": "1.10.24",
|
||||||
|
"@types/uuid": "^8.3.0",
|
||||||
"axios": "^0.21.1",
|
"axios": "^0.21.1",
|
||||||
"eta": "^1.12.1",
|
"eta": "^1.12.1",
|
||||||
"express": "^4.17.1",
|
"express": "^4.17.1",
|
||||||
|
"node-html-parser": "^2.1.0",
|
||||||
"ts-md5": "^1.2.7",
|
"ts-md5": "^1.2.7",
|
||||||
"typescript": "^4.1.3",
|
"typescript": "^4.1.3",
|
||||||
"underscore":"^1.12.0",
|
"underscore":"^1.12.0",
|
||||||
|
"uuid": "^8.3.2",
|
||||||
"winston": "^3.3.3"
|
"winston": "^3.3.3"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
15
src/app.ts
15
src/app.ts
@@ -1,20 +1,19 @@
|
|||||||
import sonos, { bonobService } from "./sonos";
|
import sonos, { bonobService } from "./sonos";
|
||||||
import server from "./server";
|
import server from "./server";
|
||||||
import logger from "./logger"
|
import logger from "./logger";
|
||||||
|
|
||||||
const PORT = process.env["PORT"] || 4534;
|
const PORT = process.env["BONOB_PORT"] || 4534;
|
||||||
|
const WEB_ADDRESS = process.env["BONOB_WEB_ADDRESS"] || `http://localhost:${PORT}`;
|
||||||
|
|
||||||
const bonob = bonobService(
|
const bonob = bonobService(
|
||||||
process.env["BONOB_SONOS_SERVICE_NAME"] || "bonob",
|
process.env["BONOB_SONOS_SERVICE_NAME"] || "bonob",
|
||||||
Number(process.env["BONOS_SONOS_SERVICE_ID"] || "246")
|
Number(process.env["BONOS_SONOS_SERVICE_ID"] || "246"),
|
||||||
)
|
WEB_ADDRESS
|
||||||
const app = server(
|
|
||||||
sonos(process.env["BONOB_SONOS_SEED_HOST"]),
|
|
||||||
bonob
|
|
||||||
);
|
);
|
||||||
|
const app = server(sonos(process.env["BONOB_SONOS_SEED_HOST"]), bonob);
|
||||||
|
|
||||||
app.listen(PORT, () => {
|
app.listen(PORT, () => {
|
||||||
logger.info(`Listening on ${PORT}`);
|
logger.info(`Listening on ${PORT} available @ ${WEB_ADDRESS}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
export default app;
|
export default app;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import express, { Express } from "express";
|
import express, { Express } from "express";
|
||||||
import * as Eta from "eta";
|
import * as Eta from "eta";
|
||||||
import { Sonos, servicesFrom, registrationStatus, Service } from "./sonos";
|
import { Sonos, Service } from "./sonos";
|
||||||
|
|
||||||
function server(sonos: Sonos, bonob: Service): Express {
|
function server(sonos: Sonos, bonobService: Service): Express {
|
||||||
const app = express();
|
const app = express();
|
||||||
|
|
||||||
app.use(express.static("./web/public"));
|
app.use(express.static("./web/public"));
|
||||||
@@ -12,15 +12,25 @@ function server(sonos: Sonos, bonob: Service): Express {
|
|||||||
app.set("views", "./web/views");
|
app.set("views", "./web/views");
|
||||||
|
|
||||||
app.get("/", (_, res) => {
|
app.get("/", (_, res) => {
|
||||||
sonos.devices().then((devices) => {
|
Promise.all([
|
||||||
const services = servicesFrom(devices);
|
sonos.devices(),
|
||||||
|
sonos.services()
|
||||||
|
]).then(([devices, services]) => {
|
||||||
|
const registeredBonobService = services.find(it => it.sid == bonobService.sid);
|
||||||
res.render("index", {
|
res.render("index", {
|
||||||
devices,
|
devices,
|
||||||
services,
|
services,
|
||||||
bonob,
|
bonobService,
|
||||||
registration: registrationStatus(services, bonob),
|
registeredBonobService
|
||||||
});
|
});
|
||||||
});
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
app.post("/register", (_, res) => {
|
||||||
|
sonos.register(bonobService).then(success => {
|
||||||
|
if(success) res.send("Yay")
|
||||||
|
else res.send("boo hoo")
|
||||||
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
return app;
|
return app;
|
||||||
|
|||||||
191
src/sonos.ts
191
src/sonos.ts
@@ -1,6 +1,8 @@
|
|||||||
import { SonosManager, SonosDevice } from "@svrooij/sonos";
|
import { SonosManager, SonosDevice } from "@svrooij/sonos";
|
||||||
// import { MusicService } from "@svrooij/sonos/lib/services";
|
import axios from "axios";
|
||||||
import { sortBy, uniq } from "underscore";
|
import { parse } from "node-html-parser";
|
||||||
|
import { MusicService } from "@svrooij/sonos/lib/services";
|
||||||
|
import { head } from "underscore";
|
||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
|
|
||||||
export type Device = {
|
export type Device = {
|
||||||
@@ -8,60 +10,95 @@ export type Device = {
|
|||||||
group: string;
|
group: string;
|
||||||
ip: string;
|
ip: string;
|
||||||
port: number;
|
port: number;
|
||||||
services: Service[];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type Service = {
|
export type Service = {
|
||||||
name: string;
|
name: string;
|
||||||
id: number;
|
sid: number;
|
||||||
|
uri: string;
|
||||||
|
secureUri: string;
|
||||||
|
strings: { uri?: string; version?: string };
|
||||||
|
presentation: { uri?: string; version?: string };
|
||||||
|
pollInterval?: number;
|
||||||
|
authType: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type BonobRegistrationStatus = 'registered' | 'not-registered'
|
const stripTailingSlash = (url: string) =>
|
||||||
|
url.endsWith("/") ? url.substring(0, url.length - 1) : url;
|
||||||
|
|
||||||
export const bonobService = (name: string, id: number): Service => ({
|
export const bonobService = (
|
||||||
|
name: string,
|
||||||
|
sid: number,
|
||||||
|
bonobRoot: string
|
||||||
|
): Service => ({
|
||||||
name,
|
name,
|
||||||
id
|
sid,
|
||||||
})
|
uri: `${stripTailingSlash(bonobRoot)}/ws/sonos`,
|
||||||
|
secureUri: `${stripTailingSlash(bonobRoot)}/ws/sonos`,
|
||||||
|
strings: {
|
||||||
|
uri: `${stripTailingSlash(bonobRoot)}/sonos/strings.xml`,
|
||||||
|
version: "1",
|
||||||
|
},
|
||||||
|
presentation: {
|
||||||
|
uri: `${stripTailingSlash(bonobRoot)}/sonos/presentationMap.xml`,
|
||||||
|
version: "1",
|
||||||
|
},
|
||||||
|
pollInterval: 1200,
|
||||||
|
authType: "Anonymous",
|
||||||
|
});
|
||||||
|
|
||||||
export interface Sonos {
|
export interface Sonos {
|
||||||
devices: () => Promise<Device[]>;
|
devices: () => Promise<Device[]>;
|
||||||
|
services: () => Promise<Service[]>;
|
||||||
|
register: (service: Service) => Promise<boolean>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SONOS_DISABLED: Sonos = {
|
export const SONOS_DISABLED: Sonos = {
|
||||||
devices: () => Promise.resolve([]),
|
devices: () => Promise.resolve([]),
|
||||||
|
services: () => Promise.resolve([]),
|
||||||
|
register: (_: Service) => Promise.resolve(false),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const servicesFrom = (devices: Device[]) =>
|
export const asService = (musicService: MusicService): Service => ({
|
||||||
sortBy(
|
name: musicService.Name,
|
||||||
uniq(
|
sid: musicService.Id,
|
||||||
devices.flatMap((d) => d.services),
|
uri: musicService.Uri,
|
||||||
false,
|
secureUri: musicService.SecureUri,
|
||||||
(s) => s.id
|
strings: {
|
||||||
),
|
uri: musicService.Presentation?.Strings?.Uri,
|
||||||
"name"
|
version: musicService.Presentation?.Strings?.Version,
|
||||||
);
|
},
|
||||||
|
presentation: {
|
||||||
|
uri: musicService.Presentation?.PresentationMap?.Uri,
|
||||||
|
version: musicService.Presentation?.PresentationMap?.Version,
|
||||||
|
},
|
||||||
|
pollInterval: musicService.Policy.PollInterval,
|
||||||
|
authType: musicService.Policy.Auth,
|
||||||
|
});
|
||||||
|
|
||||||
export const registrationStatus = (services: Service[], bonob: Service): BonobRegistrationStatus => {
|
export const asDevice = (sonosDevice: SonosDevice): Device => ({
|
||||||
if(services.find(s => s.id == bonob.id) != undefined) {
|
name: sonosDevice.Name,
|
||||||
return "registered"
|
group: sonosDevice.GroupName || "",
|
||||||
} else {
|
ip: sonosDevice.Host,
|
||||||
return "not-registered"
|
port: sonosDevice.Port,
|
||||||
}
|
});
|
||||||
}
|
|
||||||
|
|
||||||
export const asDevice = (sonosDevice: SonosDevice): Promise<Device> =>
|
export const asCustomdForm = (csrfToken: string, service: Service) => ({
|
||||||
sonosDevice.MusicServicesService.ListAndParseAvailableServices().then(
|
csrfToken,
|
||||||
(services) => ({
|
sid: `${service.sid}`,
|
||||||
name: sonosDevice.Name,
|
name: service.name,
|
||||||
group: sonosDevice.GroupName || "",
|
uri: service.uri,
|
||||||
ip: sonosDevice.Host,
|
secureUri: service.secureUri,
|
||||||
port: sonosDevice.Port,
|
pollInterval: `${service.pollInterval || 1200}`,
|
||||||
services: services.map((s) => ({
|
authType: service.authType,
|
||||||
name: s.Name,
|
stringsVersion: service.strings.version || "",
|
||||||
id: s.Id,
|
stringsUri: service.strings.uri || "",
|
||||||
})),
|
presentationMapVersion: service.presentation.version || "",
|
||||||
})
|
presentationMapUri: service.presentation.uri || "",
|
||||||
);
|
manifestVersion: "0",
|
||||||
|
manifestUri: "",
|
||||||
|
containerType: "MService",
|
||||||
|
});
|
||||||
|
|
||||||
const setupDiscovery = (
|
const setupDiscovery = (
|
||||||
manager: SonosManager,
|
manager: SonosManager,
|
||||||
@@ -77,29 +114,73 @@ const setupDiscovery = (
|
|||||||
};
|
};
|
||||||
|
|
||||||
export function autoDiscoverySonos(sonosSeedHost?: string): Sonos {
|
export function autoDiscoverySonos(sonosSeedHost?: string): Sonos {
|
||||||
return {
|
const sonosDevices = async (): Promise<SonosDevice[]> => {
|
||||||
devices: async () => {
|
const manager = new SonosManager();
|
||||||
const manager = new SonosManager();
|
return setupDiscovery(manager, sonosSeedHost)
|
||||||
return setupDiscovery(manager, sonosSeedHost)
|
.then((success) => {
|
||||||
.then((success) => {
|
if (success) {
|
||||||
if (success) {
|
console.log("had success");
|
||||||
const devices = Promise.all(manager.Devices.map(asDevice));
|
return manager.Devices;
|
||||||
logger.info({ devices });
|
} else {
|
||||||
return devices;
|
logger.warn("Didn't find any sonos devices!");
|
||||||
} else {
|
|
||||||
logger.warn("Didn't find any sonos devices!");
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
logger.error(`Failed looking for sonos devices ${e}`);
|
|
||||||
return [];
|
return [];
|
||||||
});
|
}
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
console.log("booom");
|
||||||
|
logger.error(`Failed looking for sonos devices ${e}`);
|
||||||
|
return [];
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
devices: async () => sonosDevices().then((it) => it.map(asDevice)),
|
||||||
|
|
||||||
|
services: async () =>
|
||||||
|
sonosDevices()
|
||||||
|
.then((it) => head(it))
|
||||||
|
.then(
|
||||||
|
(device) =>
|
||||||
|
device?.MusicServicesService?.ListAndParseAvailableServices() || []
|
||||||
|
)
|
||||||
|
.then((it) => it.map(asService)),
|
||||||
|
|
||||||
|
register: async (service: Service) => {
|
||||||
|
const anyDevice = await sonosDevices().then((devices) => head(devices));
|
||||||
|
|
||||||
|
if (!anyDevice) {
|
||||||
|
logger.warn("Failed to find a device to register with...")
|
||||||
|
return false
|
||||||
|
};
|
||||||
|
|
||||||
|
const customd = `http://${anyDevice.Host}:${anyDevice.Port}/customsd`;
|
||||||
|
|
||||||
|
const csrfToken = await axios.get(customd).then((response) =>
|
||||||
|
parse(response.data)
|
||||||
|
.querySelectorAll("input")
|
||||||
|
.find((it) => it.getAttribute("name") == "csrfToken")
|
||||||
|
?.getAttribute("value")
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!csrfToken) {
|
||||||
|
logger.warn(`Failed to find csrfToken at GET -> ${customd}, cannot register service`)
|
||||||
|
return false
|
||||||
|
};
|
||||||
|
|
||||||
|
return axios
|
||||||
|
.post(customd, new URLSearchParams(asCustomdForm(csrfToken, service)), {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
.then((response) => response.status == 200);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function sonos(sonosSeedHost?: string): Sonos {
|
export default function sonos(
|
||||||
|
sonosSeedHost: string | undefined = undefined
|
||||||
|
): Sonos {
|
||||||
switch (sonosSeedHost) {
|
switch (sonosSeedHost) {
|
||||||
case "disabled":
|
case "disabled":
|
||||||
logger.info("Sonos device discovery disabled");
|
logger.info("Sonos device discovery disabled");
|
||||||
|
|||||||
48
tests/builders.ts
Normal file
48
tests/builders.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { SonosDevice } from "@svrooij/sonos/lib";
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
|
import { Service, Device } from "../src/sonos";
|
||||||
|
|
||||||
|
const randomInt = (max: number) => Math.floor(Math.random() * max)
|
||||||
|
const randomIpAddress = () => `127.0.${randomInt(255)}.${randomInt(255)}`
|
||||||
|
|
||||||
|
export const aService = (fields: Partial<Service> = {}): Service => ({
|
||||||
|
name: `Test Music Service ${uuid()}`,
|
||||||
|
sid: randomInt(500),
|
||||||
|
uri: "https://sonos.testmusic.com/",
|
||||||
|
secureUri: "https://sonos.testmusic.com/",
|
||||||
|
strings: {
|
||||||
|
uri: "https://sonos.testmusic.com/strings.xml",
|
||||||
|
version: "22",
|
||||||
|
},
|
||||||
|
presentation: {
|
||||||
|
uri: "https://sonos.testmusic.com/presentation.xml",
|
||||||
|
version: "33",
|
||||||
|
},
|
||||||
|
pollInterval: 1200,
|
||||||
|
authType: "DeviceLink",
|
||||||
|
|
||||||
|
...fields,
|
||||||
|
});
|
||||||
|
|
||||||
|
export function aDevice(fields: Partial<Device> = {}): Device {
|
||||||
|
return {
|
||||||
|
name: `device-${uuid()}`,
|
||||||
|
group: `group-${uuid()}`,
|
||||||
|
ip: randomIpAddress(),
|
||||||
|
port: randomInt(10_000),
|
||||||
|
...fields,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function aSonosDevice(
|
||||||
|
fields: Partial<SonosDevice> = {}
|
||||||
|
): SonosDevice {
|
||||||
|
return {
|
||||||
|
Name: `device-${uuid()}`,
|
||||||
|
GroupName: `group-${uuid()}`,
|
||||||
|
Host: randomIpAddress(),
|
||||||
|
Port: randomInt(10_000),
|
||||||
|
...fields,
|
||||||
|
} as SonosDevice;
|
||||||
|
}
|
||||||
@@ -1,15 +1,12 @@
|
|||||||
import request from "supertest";
|
import request from "supertest";
|
||||||
import makeServer from "../src/server";
|
import makeServer from "../src/server";
|
||||||
import { SONOS_DISABLED, Sonos, Device, Service } from "../src/sonos";
|
import { SONOS_DISABLED, Sonos, Device } from "../src/sonos";
|
||||||
|
|
||||||
|
import { aDevice, aService } from './builders';
|
||||||
|
|
||||||
describe("index", () => {
|
describe("index", () => {
|
||||||
const BONOB_FOR_TEST: Service = {
|
|
||||||
name: "test bonob",
|
|
||||||
id: 999
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("when sonos integration is disabled", () => {
|
describe("when sonos integration is disabled", () => {
|
||||||
const server = makeServer(SONOS_DISABLED, BONOB_FOR_TEST);
|
const server = makeServer(SONOS_DISABLED, aService());
|
||||||
|
|
||||||
describe("devices list", () => {
|
describe("devices list", () => {
|
||||||
it("should be empty", async () => {
|
it("should be empty", async () => {
|
||||||
@@ -22,57 +19,54 @@ describe("index", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe("when there are 2 devices and bonob is not registered", () => {
|
describe("when there are 2 devices and bonob is not registered", () => {
|
||||||
const device1 : Device = {
|
const service1 = aService({
|
||||||
|
name: "s1",
|
||||||
|
sid: 1,
|
||||||
|
});
|
||||||
|
const service2 = aService({
|
||||||
|
name: "s2",
|
||||||
|
sid: 2,
|
||||||
|
});
|
||||||
|
const service3 = aService({
|
||||||
|
name: "s3",
|
||||||
|
sid: 3,
|
||||||
|
});
|
||||||
|
const service4 = aService({
|
||||||
|
name: "s4",
|
||||||
|
sid: 4,
|
||||||
|
});
|
||||||
|
const missingBonobService = aService({
|
||||||
|
name: "bonobMissing",
|
||||||
|
sid: 88
|
||||||
|
})
|
||||||
|
|
||||||
|
const device1: Device = aDevice({
|
||||||
name: "device1",
|
name: "device1",
|
||||||
group: "group1",
|
|
||||||
ip: "172.0.0.1",
|
ip: "172.0.0.1",
|
||||||
port: 4301,
|
port: 4301,
|
||||||
services: [
|
});
|
||||||
{
|
|
||||||
name: "s1",
|
|
||||||
id: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "s2",
|
|
||||||
id: 2,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const device2: Device = {
|
const device2: Device = aDevice({
|
||||||
name: "device2",
|
name: "device2",
|
||||||
group: "group2",
|
|
||||||
ip: "172.0.0.2",
|
ip: "172.0.0.2",
|
||||||
port: 4302,
|
port: 4302,
|
||||||
services: [
|
});
|
||||||
{
|
|
||||||
name: "s3",
|
|
||||||
id: 3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "s4",
|
|
||||||
id: 4,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
const fakeSonos: Sonos = {
|
const fakeSonos: Sonos = {
|
||||||
devices: () =>Promise.resolve([device1, device2]),
|
devices: () => Promise.resolve([device1, device2]),
|
||||||
|
services: () => Promise.resolve([service1, service2, service3, service4]),
|
||||||
|
register: () => Promise.resolve(false),
|
||||||
};
|
};
|
||||||
|
|
||||||
const server = makeServer(fakeSonos, BONOB_FOR_TEST);
|
const server = makeServer(fakeSonos, missingBonobService);
|
||||||
|
|
||||||
describe("devices list", () => {
|
describe("devices list", () => {
|
||||||
it("should contain the devices returned from sonos", async () => {
|
it("should contain the devices returned from sonos", async () => {
|
||||||
const res = await request(server).get("/").send();
|
const res = await request(server).get("/").send();
|
||||||
|
|
||||||
expect(res.status).toEqual(200);
|
expect(res.status).toEqual(200);
|
||||||
expect(res.text).toMatch(
|
expect(res.text).toMatch(/device1\s+\(172.0.0.1:4301\)/);
|
||||||
/device1\s+\(172.0.0.1:4301\)/
|
expect(res.text).toMatch(/device2\s+\(172.0.0.2:4302\)/);
|
||||||
);
|
|
||||||
expect(res.text).toMatch(
|
|
||||||
/device2\s+\(172.0.0.2:4302\)/
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -93,64 +87,35 @@ describe("index", () => {
|
|||||||
it("should be not-registered", async () => {
|
it("should be not-registered", async () => {
|
||||||
const res = await request(server).get("/").send();
|
const res = await request(server).get("/").send();
|
||||||
expect(res.status).toEqual(200);
|
expect(res.status).toEqual(200);
|
||||||
expect(res.text).toMatch(
|
expect(res.text).toMatch(/No existing service registration/);
|
||||||
/test bonob\s+\(999\) is not-registered/
|
});
|
||||||
);
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("when there are 2 devices and bonob is registered", () => {
|
describe("when there are 2 devices and bonob is registered", () => {
|
||||||
const device1 : Device = {
|
const service1 = aService();
|
||||||
name: "device1",
|
|
||||||
group: "group1",
|
|
||||||
ip: "172.0.0.1",
|
|
||||||
port: 4301,
|
|
||||||
services: [
|
|
||||||
{
|
|
||||||
name: "s1",
|
|
||||||
id: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "s2",
|
|
||||||
id: 2,
|
|
||||||
},
|
|
||||||
BONOB_FOR_TEST
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
const device2: Device = {
|
const service2 = aService();
|
||||||
name: "device2",
|
|
||||||
group: "group2",
|
const bonobService = aService({
|
||||||
ip: "172.0.0.2",
|
name: "bonobNotMissing",
|
||||||
port: 4302,
|
sid: 99
|
||||||
services: [
|
})
|
||||||
{
|
|
||||||
name: "s1",
|
|
||||||
id: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "s4",
|
|
||||||
id: 4,
|
|
||||||
},
|
|
||||||
BONOB_FOR_TEST
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
const fakeSonos: Sonos = {
|
const fakeSonos: Sonos = {
|
||||||
devices: () =>Promise.resolve([device1, device2]),
|
devices: () => Promise.resolve([]),
|
||||||
|
services: () => Promise.resolve([service1, service2, bonobService]),
|
||||||
|
register: () => Promise.resolve(false),
|
||||||
};
|
};
|
||||||
|
|
||||||
const server = makeServer(fakeSonos, BONOB_FOR_TEST);
|
const server = makeServer(fakeSonos, bonobService);
|
||||||
|
|
||||||
describe("registration status", () => {
|
describe("registration status", () => {
|
||||||
it("should be registered", async () => {
|
it("should be registered", async () => {
|
||||||
const res = await request(server).get("/").send();
|
const res = await request(server).get("/").send();
|
||||||
expect(res.status).toEqual(200);
|
expect(res.status).toEqual(200);
|
||||||
expect(res.text).toMatch(
|
expect(res.text).toMatch(/Existing service config/);
|
||||||
/test bonob\s+\(999\) is registered/
|
});
|
||||||
);
|
|
||||||
})
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,19 +1,28 @@
|
|||||||
import { SonosManager, SonosDevice } from "@svrooij/sonos";
|
import { SonosManager, SonosDevice } from "@svrooij/sonos";
|
||||||
import { MusicServicesService } from "@svrooij/sonos/lib/services";
|
import {
|
||||||
import { shuffle } from "underscore";
|
MusicServicesService,
|
||||||
|
MusicService,
|
||||||
|
} from "@svrooij/sonos/lib/services";
|
||||||
jest.mock("@svrooij/sonos");
|
jest.mock("@svrooij/sonos");
|
||||||
|
|
||||||
|
import axios from "axios";
|
||||||
|
jest.mock("axios");
|
||||||
|
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
import { AMAZON_MUSIC, APPLE_MUSIC, AUDIBLE } from "./music_services";
|
import { AMAZON_MUSIC, APPLE_MUSIC, AUDIBLE } from "./music_services";
|
||||||
|
|
||||||
import sonos, {
|
import sonos, {
|
||||||
SONOS_DISABLED,
|
SONOS_DISABLED,
|
||||||
asDevice,
|
asDevice,
|
||||||
Device,
|
asService,
|
||||||
servicesFrom,
|
asCustomdForm,
|
||||||
registrationStatus,
|
bonobService,
|
||||||
|
Service,
|
||||||
} from "../src/sonos";
|
} from "../src/sonos";
|
||||||
|
|
||||||
|
import { aSonosDevice, aService } from "./builders";
|
||||||
|
|
||||||
const mockSonosManagerConstructor = <jest.Mock<SonosManager>>SonosManager;
|
const mockSonosManagerConstructor = <jest.Mock<SonosManager>>SonosManager;
|
||||||
|
|
||||||
describe("sonos", () => {
|
describe("sonos", () => {
|
||||||
@@ -21,40 +30,54 @@ describe("sonos", () => {
|
|||||||
mockSonosManagerConstructor.mockClear();
|
mockSonosManagerConstructor.mockClear();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("bonobRegistrationStatus", () => {
|
describe("asService", () => {
|
||||||
describe("when bonob is registered", () => {
|
it("should convert", () => {
|
||||||
it("should return 'registered'", () => {
|
const musicService: MusicService = {
|
||||||
const bonob = {
|
Name: "Amazon Music",
|
||||||
name: "some bonob",
|
Version: "1.1",
|
||||||
id: 123,
|
Uri: "https://sonos.amazonmusic.com/",
|
||||||
};
|
SecureUri: "https://sonos.amazonmusic.com/",
|
||||||
expect(
|
ContainerType: "MService",
|
||||||
registrationStatus(
|
Capabilities: "2208321",
|
||||||
[
|
Presentation: {
|
||||||
{ id: 1, name: "not bonob" },
|
Strings: {
|
||||||
bonob,
|
Version: "23",
|
||||||
{ id: 2, name: "also not bonob" },
|
Uri: "https://sonos.amazonmusic.com/strings.xml",
|
||||||
],
|
},
|
||||||
bonob
|
PresentationMap: {
|
||||||
)
|
Version: "17",
|
||||||
).toBe("registered");
|
Uri: "https://sonos.amazonmusic.com/PresentationMap.xml",
|
||||||
});
|
},
|
||||||
});
|
},
|
||||||
|
Id: 201,
|
||||||
|
Policy: { Auth: "DeviceLink", PollInterval: 60 },
|
||||||
|
Manifest: {
|
||||||
|
Uri: "",
|
||||||
|
Version: "",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
describe("when bonob is not registered", () => {
|
expect(asService(musicService)).toEqual({
|
||||||
it("should return not-registered", () => {
|
name: "Amazon Music",
|
||||||
expect(
|
sid: 201,
|
||||||
registrationStatus([{ id: 1, name: "not bonob" }], {
|
uri: "https://sonos.amazonmusic.com/",
|
||||||
name: "bonob",
|
secureUri: "https://sonos.amazonmusic.com/",
|
||||||
id: 999,
|
strings: {
|
||||||
})
|
uri: "https://sonos.amazonmusic.com/strings.xml",
|
||||||
).toBe("not-registered");
|
version: "23",
|
||||||
|
},
|
||||||
|
presentation: {
|
||||||
|
uri: "https://sonos.amazonmusic.com/PresentationMap.xml",
|
||||||
|
version: "17",
|
||||||
|
},
|
||||||
|
pollInterval: 60,
|
||||||
|
authType: "DeviceLink",
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("asDevice", () => {
|
describe("asDevice", () => {
|
||||||
it("should convert", async () => {
|
it("should convert", () => {
|
||||||
const musicServicesService = {
|
const musicServicesService = {
|
||||||
ListAndParseAvailableServices: jest.fn(),
|
ListAndParseAvailableServices: jest.fn(),
|
||||||
};
|
};
|
||||||
@@ -71,57 +94,119 @@ describe("sonos", () => {
|
|||||||
APPLE_MUSIC,
|
APPLE_MUSIC,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
expect(await asDevice(device)).toEqual({
|
expect(asDevice(device)).toEqual({
|
||||||
name: "d1",
|
name: "d1",
|
||||||
group: "g1",
|
group: "g1",
|
||||||
ip: "127.0.0.222",
|
ip: "127.0.0.222",
|
||||||
port: 123,
|
port: 123,
|
||||||
services: [
|
|
||||||
{
|
|
||||||
name: AMAZON_MUSIC.Name,
|
|
||||||
id: AMAZON_MUSIC.Id,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: APPLE_MUSIC.Name,
|
|
||||||
id: APPLE_MUSIC.Id,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function someDevice(params: Partial<Device> = {}): Device {
|
describe("bonobService", () => {
|
||||||
const device = {
|
describe("when the bonob root does not have a trailing /", () => {
|
||||||
name: "device123",
|
it("should return a valid bonob service", () => {
|
||||||
group: "",
|
expect(
|
||||||
ip: "127.0.0.11",
|
bonobService("some-bonob", 876, "http://bonob.example.com")
|
||||||
port: 123,
|
).toEqual({
|
||||||
services: [],
|
name: "some-bonob",
|
||||||
};
|
sid: 876,
|
||||||
return { ...device, ...params };
|
uri: `http://bonob.example.com/ws/sonos`,
|
||||||
}
|
secureUri: `http://bonob.example.com/ws/sonos`,
|
||||||
|
strings: {
|
||||||
describe("servicesFrom", () => {
|
uri: `http://bonob.example.com/sonos/strings.xml`,
|
||||||
it("should only return uniq services, sorted by name", () => {
|
version: "1",
|
||||||
const service1 = { id: 1, name: "D" };
|
},
|
||||||
const service2 = { id: 2, name: "B" };
|
presentation: {
|
||||||
const service3 = { id: 3, name: "C" };
|
uri: `http://bonob.example.com/sonos/presentationMap.xml`,
|
||||||
const service4 = { id: 4, name: "A" };
|
version: "1",
|
||||||
|
},
|
||||||
const d1 = someDevice({ services: shuffle([service1, service2]) });
|
pollInterval: 1200,
|
||||||
const d2 = someDevice({
|
authType: "Anonymous",
|
||||||
services: shuffle([service1, service2, service3]),
|
});
|
||||||
});
|
});
|
||||||
const d3 = someDevice({ services: shuffle([service4]) });
|
});
|
||||||
|
|
||||||
const devices: Device[] = [d1, d2, d3];
|
describe("when the bonob root does have a trailing /", () => {
|
||||||
|
it("should return a valid bonob service", () => {
|
||||||
|
expect(
|
||||||
|
bonobService("some-bonob", 876, "http://bonob.example.com/")
|
||||||
|
).toEqual({
|
||||||
|
name: "some-bonob",
|
||||||
|
sid: 876,
|
||||||
|
uri: `http://bonob.example.com/ws/sonos`,
|
||||||
|
secureUri: `http://bonob.example.com/ws/sonos`,
|
||||||
|
strings: {
|
||||||
|
uri: `http://bonob.example.com/sonos/strings.xml`,
|
||||||
|
version: "1",
|
||||||
|
},
|
||||||
|
presentation: {
|
||||||
|
uri: `http://bonob.example.com/sonos/presentationMap.xml`,
|
||||||
|
version: "1",
|
||||||
|
},
|
||||||
|
pollInterval: 1200,
|
||||||
|
authType: "Anonymous",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
expect(servicesFrom(devices)).toEqual([
|
describe("asCustomdForm", () => {
|
||||||
service4,
|
describe("when all values specified", () => {
|
||||||
service2,
|
it("should return a form", () => {
|
||||||
service3,
|
const csrfToken = uuid();
|
||||||
service1,
|
const service: Service = {
|
||||||
]);
|
name: "the new service",
|
||||||
|
sid: 888,
|
||||||
|
uri: "http://aa.example.com",
|
||||||
|
secureUri: "https://aa.example.com",
|
||||||
|
strings: { uri: "http://strings.example.com", version: "26" },
|
||||||
|
presentation: {
|
||||||
|
uri: "http://presentation.example.com",
|
||||||
|
version: "27",
|
||||||
|
},
|
||||||
|
pollInterval: 5600,
|
||||||
|
authType: "SpecialAuth",
|
||||||
|
};
|
||||||
|
|
||||||
|
expect(asCustomdForm(csrfToken, service)).toEqual({
|
||||||
|
csrfToken,
|
||||||
|
sid: "888",
|
||||||
|
name: "the new service",
|
||||||
|
uri: "http://aa.example.com",
|
||||||
|
secureUri: "https://aa.example.com",
|
||||||
|
pollInterval: "5600",
|
||||||
|
authType: "SpecialAuth",
|
||||||
|
stringsVersion: "26",
|
||||||
|
stringsUri: "http://strings.example.com",
|
||||||
|
presentationMapVersion: "27",
|
||||||
|
presentationMapUri: "http://presentation.example.com",
|
||||||
|
manifestVersion: "0",
|
||||||
|
manifestUri: "",
|
||||||
|
containerType: "MService",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when pollInterval undefined", () => {
|
||||||
|
it("should default to 1200", () => {
|
||||||
|
const service: Service = aService({ pollInterval: undefined });
|
||||||
|
expect(asCustomdForm(uuid(), service).pollInterval).toEqual("1200");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when strings and presentation are undefined", () => {
|
||||||
|
it("should default to 1200", () => {
|
||||||
|
const service: Service = aService({
|
||||||
|
strings: { uri: undefined, version: undefined },
|
||||||
|
presentation: { uri: undefined, version: undefined },
|
||||||
|
});
|
||||||
|
const form = asCustomdForm(uuid(), service)
|
||||||
|
expect(form.stringsUri).toEqual("");
|
||||||
|
expect(form.stringsVersion).toEqual("");
|
||||||
|
expect(form.presentationMapUri).toEqual("");
|
||||||
|
expect(form.presentationMapVersion).toEqual("");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -131,37 +216,26 @@ describe("sonos", () => {
|
|||||||
|
|
||||||
expect(disabled).toEqual(SONOS_DISABLED);
|
expect(disabled).toEqual(SONOS_DISABLED);
|
||||||
expect(await disabled.devices()).toEqual([]);
|
expect(await disabled.devices()).toEqual([]);
|
||||||
|
expect(await disabled.services()).toEqual([]);
|
||||||
|
expect(await disabled.register(aService())).toEqual(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("sonos device discovery", () => {
|
describe("sonos device discovery", () => {
|
||||||
const device1_MusicServicesService = {
|
|
||||||
ListAndParseAvailableServices: jest.fn(),
|
|
||||||
};
|
|
||||||
const device1 = {
|
const device1 = {
|
||||||
Name: "device1",
|
Name: "device1",
|
||||||
GroupName: "group1",
|
GroupName: "group1",
|
||||||
Host: "127.0.0.11",
|
Host: "127.0.0.11",
|
||||||
Port: 111,
|
Port: 111,
|
||||||
MusicServicesService: (device1_MusicServicesService as unknown) as MusicServicesService,
|
|
||||||
} as SonosDevice;
|
} as SonosDevice;
|
||||||
|
|
||||||
const device2_MusicServicesService = {
|
|
||||||
ListAndParseAvailableServices: jest.fn(),
|
|
||||||
};
|
|
||||||
const device2 = {
|
const device2 = {
|
||||||
Name: "device2",
|
Name: "device2",
|
||||||
GroupName: "group2",
|
GroupName: "group2",
|
||||||
Host: "127.0.0.22",
|
Host: "127.0.0.22",
|
||||||
Port: 222,
|
Port: 222,
|
||||||
MusicServicesService: (device2_MusicServicesService as unknown) as MusicServicesService,
|
|
||||||
} as SonosDevice;
|
} as SonosDevice;
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
device1_MusicServicesService.ListAndParseAvailableServices.mockClear();
|
|
||||||
device2_MusicServicesService.ListAndParseAvailableServices.mockClear();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("when no sonos seed host is provided", () => {
|
describe("when no sonos seed host is provided", () => {
|
||||||
it("should perform auto-discovery", async () => {
|
it("should perform auto-discovery", async () => {
|
||||||
const sonosManager = {
|
const sonosManager = {
|
||||||
@@ -241,99 +315,323 @@ describe("sonos", () => {
|
|||||||
);
|
);
|
||||||
sonosManager.InitializeWithDiscovery.mockResolvedValue(true);
|
sonosManager.InitializeWithDiscovery.mockResolvedValue(true);
|
||||||
|
|
||||||
device1_MusicServicesService.ListAndParseAvailableServices.mockResolvedValue(
|
|
||||||
[AMAZON_MUSIC, APPLE_MUSIC]
|
|
||||||
);
|
|
||||||
device2_MusicServicesService.ListAndParseAvailableServices.mockResolvedValue(
|
|
||||||
[AUDIBLE]
|
|
||||||
);
|
|
||||||
|
|
||||||
const actualDevices = await sonos(undefined).devices();
|
const actualDevices = await sonos(undefined).devices();
|
||||||
|
|
||||||
expect(
|
|
||||||
device1_MusicServicesService.ListAndParseAvailableServices
|
|
||||||
).toHaveBeenCalled();
|
|
||||||
expect(
|
|
||||||
device2_MusicServicesService.ListAndParseAvailableServices
|
|
||||||
).toHaveBeenCalled();
|
|
||||||
|
|
||||||
expect(actualDevices).toEqual([
|
expect(actualDevices).toEqual([
|
||||||
{
|
{
|
||||||
name: device1.Name,
|
name: device1.Name,
|
||||||
group: device1.GroupName,
|
group: device1.GroupName,
|
||||||
ip: device1.Host,
|
ip: device1.Host,
|
||||||
port: device1.Port,
|
port: device1.Port,
|
||||||
services: [
|
|
||||||
{
|
|
||||||
name: AMAZON_MUSIC.Name,
|
|
||||||
id: AMAZON_MUSIC.Id,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: APPLE_MUSIC.Name,
|
|
||||||
id: APPLE_MUSIC.Id,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: device2.Name,
|
name: device2.Name,
|
||||||
group: device2.GroupName,
|
group: device2.GroupName,
|
||||||
ip: device2.Host,
|
ip: device2.Host,
|
||||||
port: device2.Port,
|
port: device2.Port,
|
||||||
services: [
|
|
||||||
{
|
|
||||||
name: AUDIBLE.Name,
|
|
||||||
id: AUDIBLE.Id,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("when initialisation returns false", () => {
|
describe("when SonosManager initialisation returns false", () => {
|
||||||
it("should return empty []", async () => {
|
it("should return no devices", async () => {
|
||||||
const initialize = jest.fn();
|
|
||||||
const sonosManager = {
|
const sonosManager = {
|
||||||
InitializeWithDiscovery: initialize as (
|
InitializeWithDiscovery: jest.fn(),
|
||||||
x: number
|
|
||||||
) => Promise<boolean>,
|
|
||||||
Devices: [device1, device2],
|
Devices: [device1, device2],
|
||||||
} as SonosManager;
|
};
|
||||||
|
|
||||||
mockSonosManagerConstructor.mockReturnValue(sonosManager);
|
mockSonosManagerConstructor.mockReturnValue(
|
||||||
initialize.mockResolvedValue(false);
|
(sonosManager as unknown) as SonosManager
|
||||||
|
);
|
||||||
|
sonosManager.InitializeWithDiscovery.mockResolvedValue(false);
|
||||||
|
|
||||||
const actualDevices = await sonos("").devices();
|
expect(await sonos("").devices()).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("sonos service discovery", () => {
|
||||||
|
const device1 = {
|
||||||
|
Name: "device1",
|
||||||
|
GroupName: "group1",
|
||||||
|
Host: "127.0.0.11",
|
||||||
|
Port: 111,
|
||||||
|
MusicServicesService: {
|
||||||
|
ListAndParseAvailableServices: jest.fn(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const device2 = {
|
||||||
|
Name: "device2",
|
||||||
|
GroupName: "group2",
|
||||||
|
Host: "127.0.0.22",
|
||||||
|
Port: 222,
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
device1.MusicServicesService.ListAndParseAvailableServices.mockClear();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when there are no devices", () => {
|
||||||
|
it("should return no services", async () => {
|
||||||
|
const sonosManager = {
|
||||||
|
InitializeWithDiscovery: jest.fn(),
|
||||||
|
Devices: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
mockSonosManagerConstructor.mockReturnValue(
|
||||||
|
(sonosManager as unknown) as SonosManager
|
||||||
|
);
|
||||||
|
sonosManager.InitializeWithDiscovery.mockResolvedValue(true);
|
||||||
|
|
||||||
|
const services = await sonos().services();
|
||||||
|
|
||||||
expect(SonosManager).toHaveBeenCalledTimes(1);
|
expect(SonosManager).toHaveBeenCalledTimes(1);
|
||||||
expect(initialize).toHaveBeenCalledWith(10);
|
expect(sonosManager.InitializeWithDiscovery).toHaveBeenCalledWith(10);
|
||||||
|
|
||||||
expect(actualDevices).toEqual([]);
|
expect(services).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when there are some devices", () => {
|
||||||
|
it("should return the services from the first device", async () => {
|
||||||
|
const sonosManager = {
|
||||||
|
InitializeWithDiscovery: jest.fn(),
|
||||||
|
Devices: [device1, device2],
|
||||||
|
};
|
||||||
|
|
||||||
|
mockSonosManagerConstructor.mockReturnValue(
|
||||||
|
(sonosManager as unknown) as SonosManager
|
||||||
|
);
|
||||||
|
sonosManager.InitializeWithDiscovery.mockResolvedValue(true);
|
||||||
|
device1.MusicServicesService.ListAndParseAvailableServices.mockResolvedValue(
|
||||||
|
[AMAZON_MUSIC, APPLE_MUSIC, AUDIBLE]
|
||||||
|
);
|
||||||
|
|
||||||
|
const services = await sonos().services();
|
||||||
|
|
||||||
|
expect(SonosManager).toHaveBeenCalledTimes(1);
|
||||||
|
expect(sonosManager.InitializeWithDiscovery).toHaveBeenCalledWith(10);
|
||||||
|
|
||||||
|
expect(services).toEqual([
|
||||||
|
asService(AMAZON_MUSIC),
|
||||||
|
asService(APPLE_MUSIC),
|
||||||
|
asService(AUDIBLE),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when SonosManager initialisation returns false", () => {
|
||||||
|
it("should return no devices", async () => {
|
||||||
|
const sonosManager = {
|
||||||
|
InitializeWithDiscovery: jest.fn(),
|
||||||
|
Devices: [device1, device2],
|
||||||
|
};
|
||||||
|
|
||||||
|
mockSonosManagerConstructor.mockReturnValue(
|
||||||
|
(sonosManager as unknown) as SonosManager
|
||||||
|
);
|
||||||
|
sonosManager.InitializeWithDiscovery.mockResolvedValue(false);
|
||||||
|
|
||||||
|
const services = await sonos().services();
|
||||||
|
|
||||||
|
expect(SonosManager).toHaveBeenCalledTimes(1);
|
||||||
|
expect(sonosManager.InitializeWithDiscovery).toHaveBeenCalledWith(10);
|
||||||
|
|
||||||
|
expect(services).toEqual([]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("when getting devices fails", () => {
|
describe("when getting devices fails", () => {
|
||||||
it("should return empty []", async () => {
|
it("should return no devices", async () => {
|
||||||
const initialize = jest.fn();
|
const sonosManager = {
|
||||||
|
InitializeWithDiscovery: jest.fn(),
|
||||||
const sonosManager = ({
|
|
||||||
InitializeWithDiscovery: initialize as (
|
|
||||||
x: number
|
|
||||||
) => Promise<boolean>,
|
|
||||||
Devices: () => {
|
Devices: () => {
|
||||||
throw Error("Boom");
|
throw Error("Boom");
|
||||||
},
|
},
|
||||||
} as unknown) as SonosManager;
|
};
|
||||||
|
|
||||||
mockSonosManagerConstructor.mockReturnValue(sonosManager);
|
mockSonosManagerConstructor.mockReturnValue(
|
||||||
initialize.mockResolvedValue(true);
|
(sonosManager as unknown) as SonosManager
|
||||||
|
);
|
||||||
|
sonosManager.InitializeWithDiscovery.mockResolvedValue(true);
|
||||||
|
|
||||||
const actualDevices = await sonos("").devices();
|
const services = await sonos().services();
|
||||||
|
|
||||||
expect(SonosManager).toHaveBeenCalledTimes(1);
|
expect(SonosManager).toHaveBeenCalledTimes(1);
|
||||||
expect(initialize).toHaveBeenCalledWith(10);
|
expect(sonosManager.InitializeWithDiscovery).toHaveBeenCalledWith(10);
|
||||||
|
|
||||||
expect(actualDevices).toEqual([]);
|
expect(services).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("registering a service", () => {
|
||||||
|
const device1 = aSonosDevice({
|
||||||
|
Name: "d1",
|
||||||
|
Host: "127.0.0.11",
|
||||||
|
Port: 111,
|
||||||
|
});
|
||||||
|
|
||||||
|
const device2 = aSonosDevice({
|
||||||
|
Name: "d2",
|
||||||
|
});
|
||||||
|
|
||||||
|
const POST_CONFIG = {
|
||||||
|
headers: {
|
||||||
|
"Content-Type": "application/x-www-form-urlencoded",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const serviceToAdd = aService({
|
||||||
|
name: "new service",
|
||||||
|
sid: 123,
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockGet = jest.fn();
|
||||||
|
const mockPost = jest.fn();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mockGet.mockClear();
|
||||||
|
mockPost.mockClear();
|
||||||
|
|
||||||
|
axios.get = mockGet;
|
||||||
|
axios.post = mockPost;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when successful", () => {
|
||||||
|
it("should post the service into the first found sonos device, returning true", async () => {
|
||||||
|
const sonosManager = {
|
||||||
|
InitializeWithDiscovery: jest.fn(),
|
||||||
|
Devices: [device1, device2],
|
||||||
|
};
|
||||||
|
|
||||||
|
mockSonosManagerConstructor.mockReturnValue(
|
||||||
|
(sonosManager as unknown) as SonosManager
|
||||||
|
);
|
||||||
|
sonosManager.InitializeWithDiscovery.mockResolvedValue(true);
|
||||||
|
|
||||||
|
const csrfToken = `csrfToken-${uuid()}`;
|
||||||
|
|
||||||
|
mockGet.mockResolvedValue({
|
||||||
|
status: 200,
|
||||||
|
data: `<html><input name='csrfToken' value='${csrfToken}'></html>`,
|
||||||
|
});
|
||||||
|
mockPost.mockResolvedValue({ status: 200, data: "" });
|
||||||
|
|
||||||
|
const result = await sonos().register(serviceToAdd);
|
||||||
|
|
||||||
|
expect(mockGet).toHaveBeenCalledWith(
|
||||||
|
`http://${device1.Host}:${device1.Port}/customsd`
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(mockPost).toHaveBeenCalledWith(
|
||||||
|
`http://${device1.Host}:${device1.Port}/customsd`,
|
||||||
|
new URLSearchParams(asCustomdForm(csrfToken, serviceToAdd)),
|
||||||
|
POST_CONFIG
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toEqual(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when cannot find any devices", () => {
|
||||||
|
it("should return false", async () => {
|
||||||
|
const sonosManager = {
|
||||||
|
InitializeWithDiscovery: jest.fn(),
|
||||||
|
Devices: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
mockSonosManagerConstructor.mockReturnValue(
|
||||||
|
(sonosManager as unknown) as SonosManager
|
||||||
|
);
|
||||||
|
sonosManager.InitializeWithDiscovery.mockResolvedValue(true);
|
||||||
|
|
||||||
|
const result = await sonos().register(serviceToAdd);
|
||||||
|
|
||||||
|
expect(mockGet).not.toHaveBeenCalled();
|
||||||
|
expect(mockPost).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
expect(result).toEqual(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when cannot get csrfToken", () => {
|
||||||
|
describe("when the token is missing", () => {
|
||||||
|
it("should return false", async () => {
|
||||||
|
const sonosManager = {
|
||||||
|
InitializeWithDiscovery: jest.fn(),
|
||||||
|
Devices: [device1, device2],
|
||||||
|
};
|
||||||
|
|
||||||
|
mockSonosManagerConstructor.mockReturnValue(
|
||||||
|
(sonosManager as unknown) as SonosManager
|
||||||
|
);
|
||||||
|
sonosManager.InitializeWithDiscovery.mockResolvedValue(true);
|
||||||
|
|
||||||
|
mockGet.mockResolvedValue({
|
||||||
|
status: 200,
|
||||||
|
data: `<html></html>`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await sonos().register(serviceToAdd);
|
||||||
|
|
||||||
|
expect(mockPost).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
expect(result).toEqual(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when the token call returns a non 200", () => {
|
||||||
|
it("should return false", async () => {
|
||||||
|
const sonosManager = {
|
||||||
|
InitializeWithDiscovery: jest.fn(),
|
||||||
|
Devices: [device1, device2],
|
||||||
|
};
|
||||||
|
|
||||||
|
mockSonosManagerConstructor.mockReturnValue(
|
||||||
|
(sonosManager as unknown) as SonosManager
|
||||||
|
);
|
||||||
|
sonosManager.InitializeWithDiscovery.mockResolvedValue(true);
|
||||||
|
|
||||||
|
mockGet.mockResolvedValue({
|
||||||
|
status: 400,
|
||||||
|
data: `<html></html>`,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await sonos().register(serviceToAdd);
|
||||||
|
|
||||||
|
expect(mockPost).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
expect(result).toEqual(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when posting in the service definition fails", () => {
|
||||||
|
it("should return false", async () => {
|
||||||
|
const sonosManager = {
|
||||||
|
InitializeWithDiscovery: jest.fn(),
|
||||||
|
Devices: [device1, device2],
|
||||||
|
};
|
||||||
|
|
||||||
|
mockSonosManagerConstructor.mockReturnValue(
|
||||||
|
(sonosManager as unknown) as SonosManager
|
||||||
|
);
|
||||||
|
sonosManager.InitializeWithDiscovery.mockResolvedValue(true);
|
||||||
|
|
||||||
|
const csrfToken = `csrfToken-${uuid()}`;
|
||||||
|
|
||||||
|
mockGet.mockResolvedValue({
|
||||||
|
status: 200,
|
||||||
|
data: `<html><input name='csrfToken' value='${csrfToken}'></html>`,
|
||||||
|
});
|
||||||
|
mockPost.mockResolvedValue({ status: 500, data: "" });
|
||||||
|
|
||||||
|
const result = await sonos().register(serviceToAdd);
|
||||||
|
|
||||||
|
expect(result).toEqual(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -2,7 +2,16 @@
|
|||||||
|
|
||||||
<div id="content">
|
<div id="content">
|
||||||
<h1>bonob</h1>
|
<h1>bonob</h1>
|
||||||
<h2><%= it.bonob.name %> (<%= it.bonob.id %>) is <%= it.registration %></h2>
|
<h2><%= it.bonobService.name %> (<%= it.bonobService.sid %>)
|
||||||
|
<h3>Expected config</h3>
|
||||||
|
<div><%= JSON.stringify(it.bonobService) %></div>
|
||||||
|
<% if(it.registeredBonobService) { %>
|
||||||
|
<h3>Existing service config</h3>
|
||||||
|
<div><%= JSON.stringify(it.registeredBonobService) %></div>
|
||||||
|
<% } else { %>
|
||||||
|
<h3>No existing service registration</h3>
|
||||||
|
<% } %>
|
||||||
|
<form action="/register" method="POST"><button>Re-register</button></form>
|
||||||
<h2>Devices</h2>
|
<h2>Devices</h2>
|
||||||
<ul>
|
<ul>
|
||||||
<% it.devices.forEach(function(d){ %>
|
<% it.devices.forEach(function(d){ %>
|
||||||
@@ -12,7 +21,7 @@
|
|||||||
<h2>Services <%= it.services.length %></h2>
|
<h2>Services <%= it.services.length %></h2>
|
||||||
<ul>
|
<ul>
|
||||||
<% it.services.forEach(function(s){ %>
|
<% it.services.forEach(function(s){ %>
|
||||||
<li><%= s.name %> (<%= s.id %>)</li>
|
<li><%= s.name %> (<%= s.sid %>)</li>
|
||||||
<% }) %>
|
<% }) %>
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
34
yarn.lock
34
yarn.lock
@@ -691,6 +691,11 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@types/underscore/-/underscore-1.10.24.tgz#dede004deed3b3f99c4db0bdb9ee21cae25befdd"
|
resolved "https://registry.yarnpkg.com/@types/underscore/-/underscore-1.10.24.tgz#dede004deed3b3f99c4db0bdb9ee21cae25befdd"
|
||||||
integrity sha512-T3NQD8hXNW2sRsSbLNjF/aBo18MyJlbw0lSpQHB/eZZtScPdexN4HSa8cByYwTw9Wy7KuOFr81mlDQcQQaZ79w==
|
integrity sha512-T3NQD8hXNW2sRsSbLNjF/aBo18MyJlbw0lSpQHB/eZZtScPdexN4HSa8cByYwTw9Wy7KuOFr81mlDQcQQaZ79w==
|
||||||
|
|
||||||
|
"@types/uuid@^8.3.0":
|
||||||
|
version "8.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.0.tgz#215c231dff736d5ba92410e6d602050cce7e273f"
|
||||||
|
integrity sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ==
|
||||||
|
|
||||||
"@types/yargs-parser@*":
|
"@types/yargs-parser@*":
|
||||||
version "20.2.0"
|
version "20.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-20.2.0.tgz#dd3e6699ba3237f0348cd085e4698780204842f9"
|
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-20.2.0.tgz#dd3e6699ba3237f0348cd085e4698780204842f9"
|
||||||
@@ -2165,6 +2170,11 @@ has@^1.0.3:
|
|||||||
dependencies:
|
dependencies:
|
||||||
function-bind "^1.1.1"
|
function-bind "^1.1.1"
|
||||||
|
|
||||||
|
he@1.2.0:
|
||||||
|
version "1.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/he/-/he-1.2.0.tgz#84ae65fa7eafb165fddb61566ae14baf05664f0f"
|
||||||
|
integrity sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==
|
||||||
|
|
||||||
hosted-git-info@^2.1.4:
|
hosted-git-info@^2.1.4:
|
||||||
version "2.8.8"
|
version "2.8.8"
|
||||||
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488"
|
resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488"
|
||||||
@@ -3112,17 +3122,12 @@ locate-path@^5.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
p-locate "^4.1.0"
|
p-locate "^4.1.0"
|
||||||
|
|
||||||
lodash.memoize@4.x:
|
|
||||||
version "4.1.2"
|
|
||||||
resolved "https://registry.yarnpkg.com/lodash.memoize/-/lodash.memoize-4.1.2.tgz#bcc6c49a42a2840ed997f323eada5ecd182e0bfe"
|
|
||||||
integrity sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=
|
|
||||||
|
|
||||||
lodash.sortby@^4.7.0:
|
lodash.sortby@^4.7.0:
|
||||||
version "4.7.0"
|
version "4.7.0"
|
||||||
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
|
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
|
||||||
integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
|
integrity sha1-7dFMgk4sycHgsKG0K7UhBRakJDg=
|
||||||
|
|
||||||
lodash@^4.17.19, lodash@^4.17.5:
|
lodash@4.x, lodash@^4.17.19, lodash@^4.17.5:
|
||||||
version "4.17.20"
|
version "4.17.20"
|
||||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
|
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
|
||||||
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
|
integrity sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==
|
||||||
@@ -3347,6 +3352,13 @@ node-fetch@^2.6.1:
|
|||||||
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
|
resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.1.tgz#045bd323631f76ed2e2b55573394416b639a0052"
|
||||||
integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
|
integrity sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==
|
||||||
|
|
||||||
|
node-html-parser@^2.1.0:
|
||||||
|
version "2.1.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/node-html-parser/-/node-html-parser-2.1.0.tgz#36345804d743a5a1f672d4821a53f6b0e60629a9"
|
||||||
|
integrity sha512-kbCNfqjrwHAbG+mevL8aqjwVtF0Qv66XurWHoGLOc5G9rPR1L3k602jfeczAUUBldLNnCrdsDmO5G5nqAoMW+g==
|
||||||
|
dependencies:
|
||||||
|
he "1.2.0"
|
||||||
|
|
||||||
node-int64@^0.4.0:
|
node-int64@^0.4.0:
|
||||||
version "0.4.0"
|
version "0.4.0"
|
||||||
resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
|
resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b"
|
||||||
@@ -4513,9 +4525,9 @@ triple-beam@^1.2.0, triple-beam@^1.3.0:
|
|||||||
integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==
|
integrity sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==
|
||||||
|
|
||||||
ts-jest@^26.4.4:
|
ts-jest@^26.4.4:
|
||||||
version "26.4.4"
|
version "26.5.0"
|
||||||
resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.4.4.tgz#61f13fb21ab400853c532270e52cc0ed7e502c49"
|
resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.5.0.tgz#3e3417d91bc40178a6716d7dacc5b0505835aa21"
|
||||||
integrity sha512-3lFWKbLxJm34QxyVNNCgXX1u4o/RV0myvA2y2Bxm46iGIjKlaY0own9gIckbjZJPn+WaJEnfPPJ20HHGpoq4yg==
|
integrity sha512-Ya4IQgvIFNa2Mgq52KaO8yBw2W8tWp61Ecl66VjF0f5JaV8u50nGoptHVILOPGoI7SDnShmEqnYQEmyHdQ+56g==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/jest" "26.x"
|
"@types/jest" "26.x"
|
||||||
bs-logger "0.x"
|
bs-logger "0.x"
|
||||||
@@ -4523,7 +4535,7 @@ ts-jest@^26.4.4:
|
|||||||
fast-json-stable-stringify "2.x"
|
fast-json-stable-stringify "2.x"
|
||||||
jest-util "^26.1.0"
|
jest-util "^26.1.0"
|
||||||
json5 "2.x"
|
json5 "2.x"
|
||||||
lodash.memoize "4.x"
|
lodash "4.x"
|
||||||
make-error "1.x"
|
make-error "1.x"
|
||||||
mkdirp "1.x"
|
mkdirp "1.x"
|
||||||
semver "7.x"
|
semver "7.x"
|
||||||
@@ -4712,7 +4724,7 @@ uuid@^3.3.2:
|
|||||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
|
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
|
||||||
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
|
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
|
||||||
|
|
||||||
uuid@^8.3.0:
|
uuid@^8.3.0, uuid@^8.3.2:
|
||||||
version "8.3.2"
|
version "8.3.2"
|
||||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
|
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
|
||||||
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
|
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
|
||||||
|
|||||||
Reference in New Issue
Block a user