Files
bonob/tests/scenarios.test.ts

320 lines
8.8 KiB
TypeScript

import { createClientAsync, Client } from "soap";
import { Express } from "express";
import request from "supertest";
import {
GetAppLinkResult,
GetDeviceAuthTokenResult,
GetMetadataResponse,
} from "../src/smapi";
import {
aDevice,
BLONDIE,
BOB_MARLEY,
getAppLinkMessage,
MADONNA,
someCredentials,
} from "./builders";
import { InMemoryMusicService } from "./in_memory_music_service";
import { InMemoryLinkCodes } from "../src/link_codes";
import { Credentials } from "../src/music_service";
import makeServer from "../src/server";
import { Service, bonobService, Sonos } from "../src/sonos";
import supersoap from "./supersoap";
import url, { URLBuilder } from "../src/url_builder";
class LoggedInSonosDriver {
client: Client;
token: GetDeviceAuthTokenResult;
currentMetadata?: GetMetadataResponse = undefined;
constructor(client: Client, token: GetDeviceAuthTokenResult) {
this.client = client;
this.token = token;
this.client.addSoapHeader({
credentials: someCredentials(
this.token.getDeviceAuthTokenResult.authToken
),
});
}
async navigate(...path: string[]) {
let next = path.shift();
while (next) {
if (next != "root") {
const childIds =
this.currentMetadata!.getMetadataResult.mediaCollection!.map(
(it) => it.id
);
if (!childIds.includes(next)) {
throw `Expected to find a child element with id=${next} in order to browse, but found only ${childIds}`;
}
}
this.currentMetadata = (await this.getMetadata(next))[0];
next = path.shift();
}
return this;
}
expectTitles(titles: string[]) {
expect(
this.currentMetadata!.getMetadataResult.mediaCollection!.map(
(it) => it.title
)
).toEqual(titles);
return this;
}
async getMetadata(id: string) {
return await this.client.getMetadataAsync({
id,
index: 0,
count: 100,
});
}
}
class SonosDriver {
server: Express;
bonobUrl: URLBuilder;
service: Service;
constructor(server: Express, bonobUrl: URLBuilder, service: Service) {
this.server = server;
this.bonobUrl = bonobUrl;
this.service = service;
}
extractPathname = (url: string) => new URL(url).pathname;
async register() {
const action = await request(this.server)
.get(this.bonobUrl.append({ pathname: "/" }).pathname())
.expect(200)
.then((response) => {
const m = response.text.match(/ action="(.*)" /i);
return m![1]!;
});
return request(this.server)
.post(action)
.type("form")
.send({})
.expect(200)
.then((response) =>
expect(response.text).toContain("Successfully registered")
);
}
async addService() {
expect(this.service.authType).toEqual("AppLink");
await request(this.server)
.get(this.extractPathname(this.service.strings!.uri!))
.expect(200);
await request(this.server)
.get(this.extractPathname(this.service.presentation!.uri!))
.expect(200);
const client = await createClientAsync(`${this.service.uri}?wsdl`, {
endpoint: this.service.uri,
httpClient: supersoap(this.server),
});
return client
.getAppLinkAsync(getAppLinkMessage())
.then(
([result]: [GetAppLinkResult]) =>
result.getAppLinkResult.authorizeAccount.deviceLink
)
.then(({ regUrl, linkCode }: { regUrl: string; linkCode: string }) => ({
login: async ({ username, password }: Credentials) => {
const action = await request(this.server)
.get(this.extractPathname(regUrl))
.expect(200)
.then((response) => {
const m = response.text.match(/ action="(.*)" /i);
return m![1]!;
});
return request(this.server)
.post(action)
.type("form")
.send({ username, password, linkCode })
.then((response) => ({
expectSuccess: async () => {
expect(response.status).toEqual(200);
expect(response.text).toContain("Login successful");
return client
.getDeviceAuthTokenAsync({ linkCode })
.then(
(authToken: [GetDeviceAuthTokenResult, any]) =>
new LoggedInSonosDriver(client, authToken[0])
);
},
expectFailure: () => {
expect(response.status).toEqual(403);
expect(response.text).toContain("Login failed");
},
}));
},
}));
}
}
describe("scenarios", () => {
const musicService = new InMemoryMusicService().hasArtists(
BOB_MARLEY,
BLONDIE
);
const linkCodes = new InMemoryLinkCodes();
const fakeSonos: Sonos = {
devices: () => Promise.resolve([aDevice({
name: "device1",
ip: "172.0.0.1",
port: 4301,
})]),
services: () => Promise.resolve([]),
remove: () => Promise.resolve(true),
register: () => Promise.resolve(true),
};
beforeEach(() => {
musicService.clear();
linkCodes.clear();
});
function itShouldBeAbleToAddTheService(sonosDriver: SonosDriver) {
describe("registering bonob with the sonos device", () => {
it("should complete successfully", async () => {
await sonosDriver.register();
});
});
describe("adding the service", () => {
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);
});
});
describe("when the user exists within the music service", () => {
const username = "validuser";
const password = "validpassword";
beforeEach(() => {
musicService.hasUser({ username, password });
musicService.hasArtists(BLONDIE, BOB_MARLEY, MADONNA);
});
it("should successfuly sign up", async () => {
await sonosDriver
.addService()
.then((it) => it.login({ username, password }))
.then((it) => it.expectSuccess());
expect(linkCodes.count()).toEqual(1);
});
it("should be able to list the artists", async () => {
await sonosDriver
.addService()
.then((it) => it.login({ username, password }))
.then((it) => it.expectSuccess())
.then((it) => it.navigate("root", "artists"))
.then((it) =>
it.expectTitles(
[BLONDIE, BOB_MARLEY, MADONNA].map((it) => it.name)
)
);
});
it("should be able to list the albums", async () => {
await sonosDriver
.addService()
.then((it) => it.login({ username, password }))
.then((it) => it.expectSuccess())
.then((it) => it.navigate("root", "albums"))
.then((it) =>
it.expectTitles(
[
...BLONDIE.albums,
...BOB_MARLEY.albums,
...MADONNA.albums,
].map((it) => it.name).sort()
)
);
});
});
});
}
describe("when the bonobUrl has no context path and no trailing slash", () => {
const bonobUrl = url("http://localhost:1234");
const bonob = bonobService("bonob", 123, bonobUrl);
const server = makeServer(
fakeSonos,
bonob,
bonobUrl,
musicService,
{
linkCodes: () => linkCodes
}
);
const sonosDriver = new SonosDriver(server, bonobUrl, bonob);
itShouldBeAbleToAddTheService(sonosDriver);
});
describe("when the bonobUrl has no context path, but does have a trailing slash", () => {
const bonobUrl = url("http://localhost:1234/");
const bonob = bonobService("bonob", 123, bonobUrl);
const server = makeServer(
fakeSonos,
bonob,
bonobUrl,
musicService,
{
linkCodes: () => linkCodes
}
);
const sonosDriver = new SonosDriver(server, bonobUrl, bonob);
itShouldBeAbleToAddTheService(sonosDriver);
});
describe("when the bonobUrl has a context path", () => {
const bonobUrl = url("http://localhost:1234/context-for-bonob");
const bonob = bonobService("bonob", 123, bonobUrl);
const server = makeServer(
fakeSonos,
bonob,
bonobUrl,
musicService,
{
linkCodes: () => linkCodes
}
);
const sonosDriver = new SonosDriver(server, bonobUrl, bonob);
itShouldBeAbleToAddTheService(sonosDriver);
});
});