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
|
||||
---- | ------------- | -----------
|
||||
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_SERVICE_NAME | bonob | service name for sonos
|
||||
BONOS_SONOS_SERVICE_ID | 246 | service id for sonos
|
||||
|
||||
@@ -10,12 +10,15 @@
|
||||
"@types/express": "^4.17.11",
|
||||
"@types/node": "^14.14.22",
|
||||
"@types/underscore": "1.10.24",
|
||||
"@types/uuid": "^8.3.0",
|
||||
"axios": "^0.21.1",
|
||||
"eta": "^1.12.1",
|
||||
"express": "^4.17.1",
|
||||
"node-html-parser": "^2.1.0",
|
||||
"ts-md5": "^1.2.7",
|
||||
"typescript": "^4.1.3",
|
||||
"underscore":"^1.12.0",
|
||||
"uuid": "^8.3.2",
|
||||
"winston": "^3.3.3"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
15
src/app.ts
15
src/app.ts
@@ -1,20 +1,19 @@
|
||||
import sonos, { bonobService } from "./sonos";
|
||||
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(
|
||||
process.env["BONOB_SONOS_SERVICE_NAME"] || "bonob",
|
||||
Number(process.env["BONOS_SONOS_SERVICE_ID"] || "246")
|
||||
)
|
||||
const app = server(
|
||||
sonos(process.env["BONOB_SONOS_SEED_HOST"]),
|
||||
bonob
|
||||
Number(process.env["BONOS_SONOS_SERVICE_ID"] || "246"),
|
||||
WEB_ADDRESS
|
||||
);
|
||||
const app = server(sonos(process.env["BONOB_SONOS_SEED_HOST"]), bonob);
|
||||
|
||||
app.listen(PORT, () => {
|
||||
logger.info(`Listening on ${PORT}`);
|
||||
logger.info(`Listening on ${PORT} available @ ${WEB_ADDRESS}`);
|
||||
});
|
||||
|
||||
export default app;
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import express, { Express } from "express";
|
||||
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();
|
||||
|
||||
app.use(express.static("./web/public"));
|
||||
@@ -12,15 +12,25 @@ function server(sonos: Sonos, bonob: Service): Express {
|
||||
app.set("views", "./web/views");
|
||||
|
||||
app.get("/", (_, res) => {
|
||||
sonos.devices().then((devices) => {
|
||||
const services = servicesFrom(devices);
|
||||
Promise.all([
|
||||
sonos.devices(),
|
||||
sonos.services()
|
||||
]).then(([devices, services]) => {
|
||||
const registeredBonobService = services.find(it => it.sid == bonobService.sid);
|
||||
res.render("index", {
|
||||
devices,
|
||||
services,
|
||||
bonob,
|
||||
registration: registrationStatus(services, bonob),
|
||||
bonobService,
|
||||
registeredBonobService
|
||||
});
|
||||
});
|
||||
})
|
||||
});
|
||||
|
||||
app.post("/register", (_, res) => {
|
||||
sonos.register(bonobService).then(success => {
|
||||
if(success) res.send("Yay")
|
||||
else res.send("boo hoo")
|
||||
})
|
||||
});
|
||||
|
||||
return app;
|
||||
|
||||
191
src/sonos.ts
191
src/sonos.ts
@@ -1,6 +1,8 @@
|
||||
import { SonosManager, SonosDevice } from "@svrooij/sonos";
|
||||
// import { MusicService } from "@svrooij/sonos/lib/services";
|
||||
import { sortBy, uniq } from "underscore";
|
||||
import axios from "axios";
|
||||
import { parse } from "node-html-parser";
|
||||
import { MusicService } from "@svrooij/sonos/lib/services";
|
||||
import { head } from "underscore";
|
||||
import logger from "./logger";
|
||||
|
||||
export type Device = {
|
||||
@@ -8,60 +10,95 @@ export type Device = {
|
||||
group: string;
|
||||
ip: string;
|
||||
port: number;
|
||||
services: Service[];
|
||||
};
|
||||
|
||||
export type Service = {
|
||||
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,
|
||||
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 {
|
||||
devices: () => Promise<Device[]>;
|
||||
services: () => Promise<Service[]>;
|
||||
register: (service: Service) => Promise<boolean>;
|
||||
}
|
||||
|
||||
export const SONOS_DISABLED: Sonos = {
|
||||
devices: () => Promise.resolve([]),
|
||||
services: () => Promise.resolve([]),
|
||||
register: (_: Service) => Promise.resolve(false),
|
||||
};
|
||||
|
||||
export const servicesFrom = (devices: Device[]) =>
|
||||
sortBy(
|
||||
uniq(
|
||||
devices.flatMap((d) => d.services),
|
||||
false,
|
||||
(s) => s.id
|
||||
),
|
||||
"name"
|
||||
);
|
||||
export const asService = (musicService: MusicService): Service => ({
|
||||
name: musicService.Name,
|
||||
sid: musicService.Id,
|
||||
uri: musicService.Uri,
|
||||
secureUri: musicService.SecureUri,
|
||||
strings: {
|
||||
uri: musicService.Presentation?.Strings?.Uri,
|
||||
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 => {
|
||||
if(services.find(s => s.id == bonob.id) != undefined) {
|
||||
return "registered"
|
||||
} else {
|
||||
return "not-registered"
|
||||
}
|
||||
}
|
||||
export const asDevice = (sonosDevice: SonosDevice): Device => ({
|
||||
name: sonosDevice.Name,
|
||||
group: sonosDevice.GroupName || "",
|
||||
ip: sonosDevice.Host,
|
||||
port: sonosDevice.Port,
|
||||
});
|
||||
|
||||
export const asDevice = (sonosDevice: SonosDevice): Promise<Device> =>
|
||||
sonosDevice.MusicServicesService.ListAndParseAvailableServices().then(
|
||||
(services) => ({
|
||||
name: sonosDevice.Name,
|
||||
group: sonosDevice.GroupName || "",
|
||||
ip: sonosDevice.Host,
|
||||
port: sonosDevice.Port,
|
||||
services: services.map((s) => ({
|
||||
name: s.Name,
|
||||
id: s.Id,
|
||||
})),
|
||||
})
|
||||
);
|
||||
export const asCustomdForm = (csrfToken: string, service: Service) => ({
|
||||
csrfToken,
|
||||
sid: `${service.sid}`,
|
||||
name: service.name,
|
||||
uri: service.uri,
|
||||
secureUri: service.secureUri,
|
||||
pollInterval: `${service.pollInterval || 1200}`,
|
||||
authType: service.authType,
|
||||
stringsVersion: service.strings.version || "",
|
||||
stringsUri: service.strings.uri || "",
|
||||
presentationMapVersion: service.presentation.version || "",
|
||||
presentationMapUri: service.presentation.uri || "",
|
||||
manifestVersion: "0",
|
||||
manifestUri: "",
|
||||
containerType: "MService",
|
||||
});
|
||||
|
||||
const setupDiscovery = (
|
||||
manager: SonosManager,
|
||||
@@ -77,29 +114,73 @@ const setupDiscovery = (
|
||||
};
|
||||
|
||||
export function autoDiscoverySonos(sonosSeedHost?: string): Sonos {
|
||||
return {
|
||||
devices: async () => {
|
||||
const manager = new SonosManager();
|
||||
return setupDiscovery(manager, sonosSeedHost)
|
||||
.then((success) => {
|
||||
if (success) {
|
||||
const devices = Promise.all(manager.Devices.map(asDevice));
|
||||
logger.info({ devices });
|
||||
return devices;
|
||||
} else {
|
||||
logger.warn("Didn't find any sonos devices!");
|
||||
return [];
|
||||
}
|
||||
})
|
||||
.catch((e) => {
|
||||
logger.error(`Failed looking for sonos devices ${e}`);
|
||||
const sonosDevices = async (): Promise<SonosDevice[]> => {
|
||||
const manager = new SonosManager();
|
||||
return setupDiscovery(manager, sonosSeedHost)
|
||||
.then((success) => {
|
||||
if (success) {
|
||||
console.log("had success");
|
||||
return manager.Devices;
|
||||
} else {
|
||||
logger.warn("Didn't find any sonos devices!");
|
||||
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) {
|
||||
case "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 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", () => {
|
||||
const BONOB_FOR_TEST: Service = {
|
||||
name: "test bonob",
|
||||
id: 999
|
||||
}
|
||||
|
||||
describe("when sonos integration is disabled", () => {
|
||||
const server = makeServer(SONOS_DISABLED, BONOB_FOR_TEST);
|
||||
const server = makeServer(SONOS_DISABLED, aService());
|
||||
|
||||
describe("devices list", () => {
|
||||
it("should be empty", async () => {
|
||||
@@ -22,57 +19,54 @@ describe("index", () => {
|
||||
});
|
||||
|
||||
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",
|
||||
group: "group1",
|
||||
ip: "172.0.0.1",
|
||||
port: 4301,
|
||||
services: [
|
||||
{
|
||||
name: "s1",
|
||||
id: 1,
|
||||
},
|
||||
{
|
||||
name: "s2",
|
||||
id: 2,
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
|
||||
const device2: Device = {
|
||||
const device2: Device = aDevice({
|
||||
name: "device2",
|
||||
group: "group2",
|
||||
ip: "172.0.0.2",
|
||||
port: 4302,
|
||||
services: [
|
||||
{
|
||||
name: "s3",
|
||||
id: 3,
|
||||
},
|
||||
{
|
||||
name: "s4",
|
||||
id: 4,
|
||||
},
|
||||
],
|
||||
}
|
||||
});
|
||||
|
||||
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", () => {
|
||||
it("should contain the devices returned from sonos", async () => {
|
||||
const res = await request(server).get("/").send();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.text).toMatch(
|
||||
/device1\s+\(172.0.0.1:4301\)/
|
||||
);
|
||||
expect(res.text).toMatch(
|
||||
/device2\s+\(172.0.0.2:4302\)/
|
||||
);
|
||||
expect(res.text).toMatch(/device1\s+\(172.0.0.1:4301\)/);
|
||||
expect(res.text).toMatch(/device2\s+\(172.0.0.2:4302\)/);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -93,64 +87,35 @@ describe("index", () => {
|
||||
it("should be not-registered", async () => {
|
||||
const res = await request(server).get("/").send();
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.text).toMatch(
|
||||
/test bonob\s+\(999\) is not-registered/
|
||||
);
|
||||
})
|
||||
expect(res.text).toMatch(/No existing service registration/);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when there are 2 devices and bonob is registered", () => {
|
||||
const device1 : Device = {
|
||||
name: "device1",
|
||||
group: "group1",
|
||||
ip: "172.0.0.1",
|
||||
port: 4301,
|
||||
services: [
|
||||
{
|
||||
name: "s1",
|
||||
id: 1,
|
||||
},
|
||||
{
|
||||
name: "s2",
|
||||
id: 2,
|
||||
},
|
||||
BONOB_FOR_TEST
|
||||
],
|
||||
};
|
||||
const service1 = aService();
|
||||
|
||||
const device2: Device = {
|
||||
name: "device2",
|
||||
group: "group2",
|
||||
ip: "172.0.0.2",
|
||||
port: 4302,
|
||||
services: [
|
||||
{
|
||||
name: "s1",
|
||||
id: 1,
|
||||
},
|
||||
{
|
||||
name: "s4",
|
||||
id: 4,
|
||||
},
|
||||
BONOB_FOR_TEST
|
||||
],
|
||||
}
|
||||
const service2 = aService();
|
||||
|
||||
const bonobService = aService({
|
||||
name: "bonobNotMissing",
|
||||
sid: 99
|
||||
})
|
||||
|
||||
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", () => {
|
||||
it("should be registered", async () => {
|
||||
const res = await request(server).get("/").send();
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.text).toMatch(
|
||||
/test bonob\s+\(999\) is registered/
|
||||
);
|
||||
})
|
||||
expect(res.text).toMatch(/Existing service config/);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,19 +1,28 @@
|
||||
import { SonosManager, SonosDevice } from "@svrooij/sonos";
|
||||
import { MusicServicesService } from "@svrooij/sonos/lib/services";
|
||||
import { shuffle } from "underscore";
|
||||
|
||||
import {
|
||||
MusicServicesService,
|
||||
MusicService,
|
||||
} from "@svrooij/sonos/lib/services";
|
||||
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 sonos, {
|
||||
SONOS_DISABLED,
|
||||
asDevice,
|
||||
Device,
|
||||
servicesFrom,
|
||||
registrationStatus,
|
||||
asService,
|
||||
asCustomdForm,
|
||||
bonobService,
|
||||
Service,
|
||||
} from "../src/sonos";
|
||||
|
||||
import { aSonosDevice, aService } from "./builders";
|
||||
|
||||
const mockSonosManagerConstructor = <jest.Mock<SonosManager>>SonosManager;
|
||||
|
||||
describe("sonos", () => {
|
||||
@@ -21,40 +30,54 @@ describe("sonos", () => {
|
||||
mockSonosManagerConstructor.mockClear();
|
||||
});
|
||||
|
||||
describe("bonobRegistrationStatus", () => {
|
||||
describe("when bonob is registered", () => {
|
||||
it("should return 'registered'", () => {
|
||||
const bonob = {
|
||||
name: "some bonob",
|
||||
id: 123,
|
||||
};
|
||||
expect(
|
||||
registrationStatus(
|
||||
[
|
||||
{ id: 1, name: "not bonob" },
|
||||
bonob,
|
||||
{ id: 2, name: "also not bonob" },
|
||||
],
|
||||
bonob
|
||||
)
|
||||
).toBe("registered");
|
||||
});
|
||||
});
|
||||
describe("asService", () => {
|
||||
it("should convert", () => {
|
||||
const musicService: MusicService = {
|
||||
Name: "Amazon Music",
|
||||
Version: "1.1",
|
||||
Uri: "https://sonos.amazonmusic.com/",
|
||||
SecureUri: "https://sonos.amazonmusic.com/",
|
||||
ContainerType: "MService",
|
||||
Capabilities: "2208321",
|
||||
Presentation: {
|
||||
Strings: {
|
||||
Version: "23",
|
||||
Uri: "https://sonos.amazonmusic.com/strings.xml",
|
||||
},
|
||||
PresentationMap: {
|
||||
Version: "17",
|
||||
Uri: "https://sonos.amazonmusic.com/PresentationMap.xml",
|
||||
},
|
||||
},
|
||||
Id: 201,
|
||||
Policy: { Auth: "DeviceLink", PollInterval: 60 },
|
||||
Manifest: {
|
||||
Uri: "",
|
||||
Version: "",
|
||||
},
|
||||
};
|
||||
|
||||
describe("when bonob is not registered", () => {
|
||||
it("should return not-registered", () => {
|
||||
expect(
|
||||
registrationStatus([{ id: 1, name: "not bonob" }], {
|
||||
name: "bonob",
|
||||
id: 999,
|
||||
})
|
||||
).toBe("not-registered");
|
||||
expect(asService(musicService)).toEqual({
|
||||
name: "Amazon Music",
|
||||
sid: 201,
|
||||
uri: "https://sonos.amazonmusic.com/",
|
||||
secureUri: "https://sonos.amazonmusic.com/",
|
||||
strings: {
|
||||
uri: "https://sonos.amazonmusic.com/strings.xml",
|
||||
version: "23",
|
||||
},
|
||||
presentation: {
|
||||
uri: "https://sonos.amazonmusic.com/PresentationMap.xml",
|
||||
version: "17",
|
||||
},
|
||||
pollInterval: 60,
|
||||
authType: "DeviceLink",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("asDevice", () => {
|
||||
it("should convert", async () => {
|
||||
it("should convert", () => {
|
||||
const musicServicesService = {
|
||||
ListAndParseAvailableServices: jest.fn(),
|
||||
};
|
||||
@@ -71,57 +94,119 @@ describe("sonos", () => {
|
||||
APPLE_MUSIC,
|
||||
]);
|
||||
|
||||
expect(await asDevice(device)).toEqual({
|
||||
expect(asDevice(device)).toEqual({
|
||||
name: "d1",
|
||||
group: "g1",
|
||||
ip: "127.0.0.222",
|
||||
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 {
|
||||
const device = {
|
||||
name: "device123",
|
||||
group: "",
|
||||
ip: "127.0.0.11",
|
||||
port: 123,
|
||||
services: [],
|
||||
};
|
||||
return { ...device, ...params };
|
||||
}
|
||||
|
||||
describe("servicesFrom", () => {
|
||||
it("should only return uniq services, sorted by name", () => {
|
||||
const service1 = { id: 1, name: "D" };
|
||||
const service2 = { id: 2, name: "B" };
|
||||
const service3 = { id: 3, name: "C" };
|
||||
const service4 = { id: 4, name: "A" };
|
||||
|
||||
const d1 = someDevice({ services: shuffle([service1, service2]) });
|
||||
const d2 = someDevice({
|
||||
services: shuffle([service1, service2, service3]),
|
||||
describe("bonobService", () => {
|
||||
describe("when the bonob root does not 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",
|
||||
});
|
||||
});
|
||||
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([
|
||||
service4,
|
||||
service2,
|
||||
service3,
|
||||
service1,
|
||||
]);
|
||||
describe("asCustomdForm", () => {
|
||||
describe("when all values specified", () => {
|
||||
it("should return a form", () => {
|
||||
const csrfToken = uuid();
|
||||
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(await disabled.devices()).toEqual([]);
|
||||
expect(await disabled.services()).toEqual([]);
|
||||
expect(await disabled.register(aService())).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("sonos device discovery", () => {
|
||||
const device1_MusicServicesService = {
|
||||
ListAndParseAvailableServices: jest.fn(),
|
||||
};
|
||||
const device1 = {
|
||||
Name: "device1",
|
||||
GroupName: "group1",
|
||||
Host: "127.0.0.11",
|
||||
Port: 111,
|
||||
MusicServicesService: (device1_MusicServicesService as unknown) as MusicServicesService,
|
||||
} as SonosDevice;
|
||||
|
||||
const device2_MusicServicesService = {
|
||||
ListAndParseAvailableServices: jest.fn(),
|
||||
};
|
||||
const device2 = {
|
||||
Name: "device2",
|
||||
GroupName: "group2",
|
||||
Host: "127.0.0.22",
|
||||
Port: 222,
|
||||
MusicServicesService: (device2_MusicServicesService as unknown) as MusicServicesService,
|
||||
} as SonosDevice;
|
||||
|
||||
beforeEach(() => {
|
||||
device1_MusicServicesService.ListAndParseAvailableServices.mockClear();
|
||||
device2_MusicServicesService.ListAndParseAvailableServices.mockClear();
|
||||
});
|
||||
|
||||
describe("when no sonos seed host is provided", () => {
|
||||
it("should perform auto-discovery", async () => {
|
||||
const sonosManager = {
|
||||
@@ -241,99 +315,323 @@ describe("sonos", () => {
|
||||
);
|
||||
sonosManager.InitializeWithDiscovery.mockResolvedValue(true);
|
||||
|
||||
device1_MusicServicesService.ListAndParseAvailableServices.mockResolvedValue(
|
||||
[AMAZON_MUSIC, APPLE_MUSIC]
|
||||
);
|
||||
device2_MusicServicesService.ListAndParseAvailableServices.mockResolvedValue(
|
||||
[AUDIBLE]
|
||||
);
|
||||
|
||||
const actualDevices = await sonos(undefined).devices();
|
||||
|
||||
expect(
|
||||
device1_MusicServicesService.ListAndParseAvailableServices
|
||||
).toHaveBeenCalled();
|
||||
expect(
|
||||
device2_MusicServicesService.ListAndParseAvailableServices
|
||||
).toHaveBeenCalled();
|
||||
|
||||
expect(actualDevices).toEqual([
|
||||
{
|
||||
name: device1.Name,
|
||||
group: device1.GroupName,
|
||||
ip: device1.Host,
|
||||
port: device1.Port,
|
||||
services: [
|
||||
{
|
||||
name: AMAZON_MUSIC.Name,
|
||||
id: AMAZON_MUSIC.Id,
|
||||
},
|
||||
{
|
||||
name: APPLE_MUSIC.Name,
|
||||
id: APPLE_MUSIC.Id,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: device2.Name,
|
||||
group: device2.GroupName,
|
||||
ip: device2.Host,
|
||||
port: device2.Port,
|
||||
services: [
|
||||
{
|
||||
name: AUDIBLE.Name,
|
||||
id: AUDIBLE.Id,
|
||||
},
|
||||
],
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when initialisation returns false", () => {
|
||||
it("should return empty []", async () => {
|
||||
const initialize = jest.fn();
|
||||
describe("when SonosManager initialisation returns false", () => {
|
||||
it("should return no devices", async () => {
|
||||
const sonosManager = {
|
||||
InitializeWithDiscovery: initialize as (
|
||||
x: number
|
||||
) => Promise<boolean>,
|
||||
InitializeWithDiscovery: jest.fn(),
|
||||
Devices: [device1, device2],
|
||||
} as SonosManager;
|
||||
};
|
||||
|
||||
mockSonosManagerConstructor.mockReturnValue(sonosManager);
|
||||
initialize.mockResolvedValue(false);
|
||||
mockSonosManagerConstructor.mockReturnValue(
|
||||
(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(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", () => {
|
||||
it("should return empty []", async () => {
|
||||
const initialize = jest.fn();
|
||||
|
||||
const sonosManager = ({
|
||||
InitializeWithDiscovery: initialize as (
|
||||
x: number
|
||||
) => Promise<boolean>,
|
||||
it("should return no devices", async () => {
|
||||
const sonosManager = {
|
||||
InitializeWithDiscovery: jest.fn(),
|
||||
Devices: () => {
|
||||
throw Error("Boom");
|
||||
},
|
||||
} as unknown) as SonosManager;
|
||||
};
|
||||
|
||||
mockSonosManagerConstructor.mockReturnValue(sonosManager);
|
||||
initialize.mockResolvedValue(true);
|
||||
mockSonosManagerConstructor.mockReturnValue(
|
||||
(sonosManager as unknown) as SonosManager
|
||||
);
|
||||
sonosManager.InitializeWithDiscovery.mockResolvedValue(true);
|
||||
|
||||
const actualDevices = await sonos("").devices();
|
||||
const services = await sonos().services();
|
||||
|
||||
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">
|
||||
<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>
|
||||
<ul>
|
||||
<% it.devices.forEach(function(d){ %>
|
||||
@@ -12,7 +21,7 @@
|
||||
<h2>Services <%= it.services.length %></h2>
|
||||
<ul>
|
||||
<% it.services.forEach(function(s){ %>
|
||||
<li><%= s.name %> (<%= s.id %>)</li>
|
||||
<li><%= s.name %> (<%= s.sid %>)</li>
|
||||
<% }) %>
|
||||
</ul>
|
||||
</div>
|
||||
34
yarn.lock
34
yarn.lock
@@ -691,6 +691,11 @@
|
||||
resolved "https://registry.yarnpkg.com/@types/underscore/-/underscore-1.10.24.tgz#dede004deed3b3f99c4db0bdb9ee21cae25befdd"
|
||||
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@*":
|
||||
version "20.2.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/yargs-parser/-/yargs-parser-20.2.0.tgz#dd3e6699ba3237f0348cd085e4698780204842f9"
|
||||
@@ -2165,6 +2170,11 @@ has@^1.0.3:
|
||||
dependencies:
|
||||
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:
|
||||
version "2.8.8"
|
||||
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:
|
||||
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:
|
||||
version "4.7.0"
|
||||
resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438"
|
||||
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"
|
||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.20.tgz#b44a9b6297bcb698f1c51a3545a2b3b368d59c52"
|
||||
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"
|
||||
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:
|
||||
version "0.4.0"
|
||||
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==
|
||||
|
||||
ts-jest@^26.4.4:
|
||||
version "26.4.4"
|
||||
resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.4.4.tgz#61f13fb21ab400853c532270e52cc0ed7e502c49"
|
||||
integrity sha512-3lFWKbLxJm34QxyVNNCgXX1u4o/RV0myvA2y2Bxm46iGIjKlaY0own9gIckbjZJPn+WaJEnfPPJ20HHGpoq4yg==
|
||||
version "26.5.0"
|
||||
resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.5.0.tgz#3e3417d91bc40178a6716d7dacc5b0505835aa21"
|
||||
integrity sha512-Ya4IQgvIFNa2Mgq52KaO8yBw2W8tWp61Ecl66VjF0f5JaV8u50nGoptHVILOPGoI7SDnShmEqnYQEmyHdQ+56g==
|
||||
dependencies:
|
||||
"@types/jest" "26.x"
|
||||
bs-logger "0.x"
|
||||
@@ -4523,7 +4535,7 @@ ts-jest@^26.4.4:
|
||||
fast-json-stable-stringify "2.x"
|
||||
jest-util "^26.1.0"
|
||||
json5 "2.x"
|
||||
lodash.memoize "4.x"
|
||||
lodash "4.x"
|
||||
make-error "1.x"
|
||||
mkdirp "1.x"
|
||||
semver "7.x"
|
||||
@@ -4712,7 +4724,7 @@ uuid@^3.3.2:
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee"
|
||||
integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==
|
||||
|
||||
uuid@^8.3.0:
|
||||
uuid@^8.3.0, uuid@^8.3.2:
|
||||
version "8.3.2"
|
||||
resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2"
|
||||
integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
|
||||
|
||||
Reference in New Issue
Block a user