mirror of
https://github.com/wkulhanek/bonob.git
synced 2025-12-21 17:33:29 +01:00
Part of AppLink login process
This commit is contained in:
@@ -5,16 +5,19 @@ import sonos, { bonobService } from "../src/sonos";
|
||||
import server from "../src/server";
|
||||
|
||||
import logger from "../src/logger";
|
||||
import { InMemoryMusicService } from "builders";
|
||||
|
||||
const bonob = bonobService("bonob-test", 247, "http://localhost:1234");
|
||||
const app = server(sonos("disabled"), bonob);
|
||||
const WEB_ADDRESS = "http://localhost:1234"
|
||||
|
||||
const bonob = bonobService("bonob-test", 247, WEB_ADDRESS, 'Anonymous');
|
||||
const app = server(sonos("disabled"), bonob, WEB_ADDRESS, new InMemoryMusicService());
|
||||
|
||||
getPort().then((port) => {
|
||||
logger.debug(`Starting on port ${port}`);
|
||||
app.listen(port);
|
||||
|
||||
createClientAsync(`http://localhost:${port}/ws?wsdl`, {
|
||||
endpoint: `http://localhost:${port}/ws`,
|
||||
createClientAsync(`${bonob.uri}?wsdl`, {
|
||||
endpoint: bonob.uri,
|
||||
}).then((client) => {
|
||||
client
|
||||
.getSessionIdAsync(
|
||||
|
||||
@@ -1,22 +1,23 @@
|
||||
import { SonosDevice } from "@svrooij/sonos/lib";
|
||||
import { v4 as uuid } from 'uuid';
|
||||
import { v4 as uuid } from "uuid";
|
||||
import { MusicService, Credentials } from "../src/music_service";
|
||||
|
||||
import { Service, Device } from "../src/sonos";
|
||||
|
||||
const randomInt = (max: number) => Math.floor(Math.random() * max)
|
||||
const randomIpAddress = () => `127.0.${randomInt(255)}.${randomInt(255)}`
|
||||
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/",
|
||||
uri: "https://sonos-test.example.com/",
|
||||
secureUri: "https://sonos-test.example.com/",
|
||||
strings: {
|
||||
uri: "https://sonos.testmusic.com/strings.xml",
|
||||
uri: "https://sonos-test.example.com/strings.xml",
|
||||
version: "22",
|
||||
},
|
||||
presentation: {
|
||||
uri: "https://sonos.testmusic.com/presentation.xml",
|
||||
uri: "https://sonos-test.example.com/presentation.xml",
|
||||
version: "33",
|
||||
},
|
||||
pollInterval: 1200,
|
||||
@@ -35,9 +36,7 @@ export function aDevice(fields: Partial<Device> = {}): Device {
|
||||
};
|
||||
}
|
||||
|
||||
export function aSonosDevice(
|
||||
fields: Partial<SonosDevice> = {}
|
||||
): SonosDevice {
|
||||
export function aSonosDevice(fields: Partial<SonosDevice> = {}): SonosDevice {
|
||||
return {
|
||||
Name: `device-${uuid()}`,
|
||||
GroupName: `group-${uuid()}`,
|
||||
@@ -46,3 +45,33 @@ export function aSonosDevice(
|
||||
...fields,
|
||||
} as SonosDevice;
|
||||
}
|
||||
|
||||
export function getAppLinkMessage() {
|
||||
return {
|
||||
householdId: "",
|
||||
hardware: "",
|
||||
osVersion: "",
|
||||
sonosAppName: "",
|
||||
callbackPath: "",
|
||||
};
|
||||
}
|
||||
|
||||
export class InMemoryMusicService implements MusicService {
|
||||
users: Record<string, string> = {};
|
||||
|
||||
login({ username, password }: Credentials) {
|
||||
return username != undefined && password != undefined && this.users[username] == password;
|
||||
}
|
||||
|
||||
hasUser(credentials: Credentials) {
|
||||
this.users[credentials.username] = credentials.password;
|
||||
}
|
||||
|
||||
hasNoUsers() {
|
||||
this.users = {};
|
||||
}
|
||||
|
||||
clear() {
|
||||
this.users = {};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,11 +2,11 @@ import request from "supertest";
|
||||
import makeServer from "../src/server";
|
||||
import { SONOS_DISABLED, Sonos, Device } from "../src/sonos";
|
||||
|
||||
import { aDevice, aService } from './builders';
|
||||
import { aDevice, aService, InMemoryMusicService } from './builders';
|
||||
|
||||
describe("index", () => {
|
||||
describe("when sonos integration is disabled", () => {
|
||||
const server = makeServer(SONOS_DISABLED, aService());
|
||||
const server = makeServer(SONOS_DISABLED, aService(), 'http://localhost:1234', new InMemoryMusicService());
|
||||
|
||||
describe("devices list", () => {
|
||||
it("should be empty", async () => {
|
||||
@@ -58,7 +58,7 @@ describe("index", () => {
|
||||
register: () => Promise.resolve(false),
|
||||
};
|
||||
|
||||
const server = makeServer(fakeSonos, missingBonobService);
|
||||
const server = makeServer(fakeSonos, missingBonobService, 'http://localhost:1234', new InMemoryMusicService());
|
||||
|
||||
describe("devices list", () => {
|
||||
it("should contain the devices returned from sonos", async () => {
|
||||
@@ -108,7 +108,7 @@ describe("index", () => {
|
||||
register: () => Promise.resolve(false),
|
||||
};
|
||||
|
||||
const server = makeServer(fakeSonos, bonobService);
|
||||
const server = makeServer(fakeSonos, bonobService, 'http://localhost:1234', new InMemoryMusicService());
|
||||
|
||||
describe("registration status", () => {
|
||||
it("should be registered", async () => {
|
||||
|
||||
124
tests/scenarios.test.ts
Normal file
124
tests/scenarios.test.ts
Normal file
@@ -0,0 +1,124 @@
|
||||
import { createClientAsync } from "soap";
|
||||
import { Express } from "express";
|
||||
|
||||
import request from "supertest";
|
||||
|
||||
import { GetAppLinkResult } from "../src/smapi";
|
||||
import { InMemoryMusicService, getAppLinkMessage } from "./builders";
|
||||
import { InMemoryLinkCodes } from "../src/link_codes";
|
||||
import { Credentials } from "../src/music_service";
|
||||
import makeServer from "../src/server";
|
||||
import { Service, bonobService, SONOS_DISABLED } from "../src/sonos";
|
||||
import supersoap from "./supersoap";
|
||||
|
||||
class SonosDriver {
|
||||
server: Express;
|
||||
rootUrl: string;
|
||||
service: Service;
|
||||
|
||||
constructor(server: Express, rootUrl: string, service: Service) {
|
||||
this.server = server;
|
||||
this.rootUrl = rootUrl;
|
||||
this.service = service;
|
||||
}
|
||||
|
||||
stripServiceRoot = (url: string) => url.replace(this.rootUrl, "");
|
||||
|
||||
async addService() {
|
||||
expect(this.service.authType).toEqual("AppLink");
|
||||
|
||||
await request(this.server)
|
||||
.get(this.stripServiceRoot(this.service.strings.uri!))
|
||||
.expect(200);
|
||||
|
||||
await request(this.server)
|
||||
.get(this.stripServiceRoot(this.service.presentation.uri!))
|
||||
.expect(200);
|
||||
|
||||
return createClientAsync(`${this.service.uri}?wsdl`, {
|
||||
endpoint: this.service.uri,
|
||||
httpClient: supersoap(this.server, this.rootUrl),
|
||||
}).then((client) =>
|
||||
client
|
||||
.getAppLinkAsync(getAppLinkMessage())
|
||||
.then(
|
||||
([result]: [GetAppLinkResult]) =>
|
||||
result.getAppLinkResult.authorizeAccount.deviceLink
|
||||
)
|
||||
.then(({ regUrl, linkCode }: { regUrl: string; linkCode: string }) => ({
|
||||
login: async ({ username, password }: Credentials) => {
|
||||
await request(this.server).get(this.stripServiceRoot(regUrl)).expect(200);
|
||||
|
||||
return request(this.server)
|
||||
.post(this.stripServiceRoot(regUrl))
|
||||
.type("form")
|
||||
.send({ username, password, linkCode })
|
||||
.expect(200)
|
||||
.then(response => ({
|
||||
expectSuccess: () => {
|
||||
expect(response.text).toContain("ok")
|
||||
},
|
||||
expectFailure: () => {
|
||||
expect(response.text).toContain("boo")
|
||||
},
|
||||
}));
|
||||
},
|
||||
}))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
describe("scenarios", () => {
|
||||
const bonobUrl = "http://localhost:1234";
|
||||
const bonob = bonobService("bonob", 123, bonobUrl);
|
||||
const musicService = new InMemoryMusicService();
|
||||
const linkCodes = new InMemoryLinkCodes();
|
||||
const server = makeServer(
|
||||
SONOS_DISABLED,
|
||||
bonob,
|
||||
bonobUrl,
|
||||
musicService,
|
||||
linkCodes
|
||||
);
|
||||
|
||||
const sonosDriver = new SonosDriver(server, bonobUrl, bonob);
|
||||
|
||||
beforeEach(() => {
|
||||
musicService.clear();
|
||||
linkCodes.clear();
|
||||
});
|
||||
|
||||
describe("adding the service", () => {
|
||||
describe("when the user exists within the music service", () => {
|
||||
const username = "validuser";
|
||||
const password = "validpassword";
|
||||
|
||||
it("should successfully sign up", async () => {
|
||||
musicService.hasUser({ username, password });
|
||||
|
||||
await sonosDriver
|
||||
.addService()
|
||||
.then((it) => it.login({ username, password }))
|
||||
.then((it) => it.expectSuccess());
|
||||
|
||||
expect(linkCodes.count()).toEqual(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the user doesnt exists within the music service", () => {
|
||||
const username = "invaliduser";
|
||||
const password = "invalidpassword";
|
||||
|
||||
it("should fail to sign up", async () => {
|
||||
musicService.hasNoUsers();
|
||||
|
||||
await sonosDriver
|
||||
.addService()
|
||||
.then((it) => it.login({ username, password }))
|
||||
.then((it) => it.expectFailure());
|
||||
|
||||
expect(linkCodes.count()).toEqual(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,5 @@
|
||||
global.console = {
|
||||
// log: console.log,
|
||||
log: jest.fn(), // console.log are ignored in tests
|
||||
|
||||
// Keep native behaviour for other methods, use those to print out things in your own tests, not `console.log`
|
||||
|
||||
27
tests/smapi.test.ts
Normal file
27
tests/smapi.test.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import request from "supertest";
|
||||
|
||||
import { DOMParserImpl } from 'xmldom-ts';
|
||||
import * as xpath from 'xpath-ts';
|
||||
|
||||
import makeServer from "../src/server";
|
||||
import { SONOS_DISABLED, STRINGS_PATH } from "../src/sonos";
|
||||
|
||||
import { aService, InMemoryMusicService } from './builders';
|
||||
|
||||
const parseXML = (value: string) => new DOMParserImpl().parseFromString(value);
|
||||
const select = xpath.useNamespaces({"sonos": "http://sonos.com/sonosapi"})
|
||||
|
||||
describe('strings.xml', () => {
|
||||
const server = makeServer(SONOS_DISABLED, aService(), 'http://localhost:1234', new InMemoryMusicService());
|
||||
|
||||
it("should return xml for the strings", async () => {
|
||||
const res = await request(server).get(STRINGS_PATH).send();
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
|
||||
const xml = parseXML(res.text);
|
||||
const x = select("//sonos:string[@stringId='AppLinkMessage']/text()", xml) as Node[]
|
||||
expect(x.length).toEqual(1)
|
||||
expect(x[0]!.nodeValue).toEqual("Linking sonos with bonob")
|
||||
});
|
||||
});
|
||||
@@ -122,7 +122,7 @@ describe("sonos", () => {
|
||||
version: "1",
|
||||
},
|
||||
pollInterval: 1200,
|
||||
authType: "Anonymous",
|
||||
authType: "AppLink",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -145,7 +145,30 @@ describe("sonos", () => {
|
||||
version: "1",
|
||||
},
|
||||
pollInterval: 1200,
|
||||
authType: "Anonymous",
|
||||
authType: "AppLink",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when authType is specified", () => {
|
||||
it("should return a valid bonob service", () => {
|
||||
expect(
|
||||
bonobService("some-bonob", 876, "http://bonob.example.com", 'DeviceLink')
|
||||
).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: "DeviceLink",
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -166,7 +189,7 @@ describe("sonos", () => {
|
||||
version: "27",
|
||||
},
|
||||
pollInterval: 5600,
|
||||
authType: "SpecialAuth",
|
||||
authType: "UserId",
|
||||
};
|
||||
|
||||
expect(asCustomdForm(csrfToken, service)).toEqual({
|
||||
@@ -176,7 +199,7 @@ describe("sonos", () => {
|
||||
uri: "http://aa.example.com",
|
||||
secureUri: "https://aa.example.com",
|
||||
pollInterval: "5600",
|
||||
authType: "SpecialAuth",
|
||||
authType: "UserId",
|
||||
stringsVersion: "26",
|
||||
stringsUri: "http://strings.example.com",
|
||||
presentationMapVersion: "27",
|
||||
|
||||
25
tests/supersoap.ts
Normal file
25
tests/supersoap.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { Express } from "express";
|
||||
import request from "supertest";
|
||||
|
||||
function supersoap(server: Express, rootUrl: string) {
|
||||
return {
|
||||
request: (
|
||||
rurl: string,
|
||||
data: any,
|
||||
callback: (error: any, res?: any, body?: any) => any,
|
||||
exheaders?: any
|
||||
) => {
|
||||
const withoutHost = rurl.replace(rootUrl, "");
|
||||
const req =
|
||||
data == null
|
||||
? request(server).get(withoutHost).send()
|
||||
: request(server).post(withoutHost).send(data);
|
||||
req
|
||||
.set(exheaders || {})
|
||||
.then((response) => callback(null, response, response.text))
|
||||
.catch(callback);
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default supersoap
|
||||
@@ -1,38 +1,22 @@
|
||||
import request from "supertest";
|
||||
import makeServer from "../src/server";
|
||||
import { SONOS_DISABLED } from "../src/sonos";
|
||||
import { SONOS_DISABLED, SOAP_PATH } from "../src/sonos";
|
||||
|
||||
import { aService } from "./builders";
|
||||
import { aService, InMemoryMusicService } from "./builders";
|
||||
import supersoap from './supersoap';
|
||||
|
||||
import { createClientAsync } from "soap";
|
||||
|
||||
describe("ws", () => {
|
||||
describe("can call getSessionId", () => {
|
||||
it("should do something", async () => {
|
||||
const server = makeServer(SONOS_DISABLED, aService());
|
||||
const WEB_ADDRESS = 'http://localhost:7653'
|
||||
const server = makeServer(SONOS_DISABLED, aService(), WEB_ADDRESS, new InMemoryMusicService());
|
||||
|
||||
const { username, sessionId } = await createClientAsync(
|
||||
`http://localhost/ws?wsdl`,
|
||||
`${WEB_ADDRESS}${SOAP_PATH}?wsdl`,
|
||||
{
|
||||
endpoint: `http://localhost/ws`,
|
||||
httpClient: {
|
||||
request: (
|
||||
rurl: string,
|
||||
data: any,
|
||||
callback: (error: any, res?: any, body?: any) => any,
|
||||
exheaders?: any
|
||||
) => {
|
||||
const withoutHost = rurl.replace("http://localhost", "");
|
||||
const req =
|
||||
data == null
|
||||
? request(server).get(withoutHost).send()
|
||||
: request(server).post(withoutHost).send(data);
|
||||
req
|
||||
.set(exheaders || {})
|
||||
.then((response) => callback(null, response, response.text))
|
||||
.catch(callback);
|
||||
},
|
||||
},
|
||||
endpoint: `${WEB_ADDRESS}${SOAP_PATH}`,
|
||||
httpClient: supersoap(server, WEB_ADDRESS),
|
||||
}
|
||||
).then((client) =>
|
||||
client
|
||||
|
||||
Reference in New Issue
Block a user