Add support for running under a context path, ie. /bonob, replace BONOB_WEB_ADDRESS with BONOB_URL

This commit is contained in:
simojenki
2021-07-23 18:11:32 +10:00
parent 8ed9bef7d8
commit 1153f8e318
16 changed files with 3700 additions and 3053 deletions

View File

@@ -13,7 +13,12 @@ describe("config", () => {
process.env = OLD_ENV;
});
function describeBooleanConfigValue(name: string, envVar: string, expectedDefault: boolean, propertyGetter: (config: any) => any) {
function describeBooleanConfigValue(
name: string,
envVar: string,
expectedDefault: boolean,
propertyGetter: (config: any) => any
) {
describe(name, () => {
function expecting({
value,
@@ -35,7 +40,72 @@ describe("config", () => {
expecting({ value: "false", expected: false });
expecting({ value: "foo", expected: false });
});
};
}
describe("bonobUrl", () => {
describe("when BONOB_URL is specified", () => {
it("should be used", () => {
const url = "http://bonob1.example.com:8877/";
process.env["BONOB_URL"] = url;
expect(config().bonobUrl.href()).toEqual(url);
});
});
describe("when BONOB_URL is not specified, however legacy BONOB_WEB_ADDRESS is specified", () => {
it("should be used", () => {
const url = "http://bonob2.example.com:9988/";
process.env["BONOB_URL"] = "";
process.env["BONOB_WEB_ADDRESS"] = url;
expect(config().bonobUrl.href()).toEqual(url);
});
});
describe("when neither BONOB_URL nor BONOB_WEB_ADDRESS are specified", () => {
describe("when BONOB_PORT is not specified", () => {
it(`should default to http://${hostname()}:4534`, () => {
expect(config().bonobUrl.href()).toEqual(
`http://${hostname()}:4534/`
);
});
});
describe("when BONOB_PORT is specified as 3322", () => {
it(`should default to http://${hostname()}:3322`, () => {
process.env["BONOB_PORT"] = "3322";
expect(config().bonobUrl.href()).toEqual(
`http://${hostname()}:3322/`
);
});
});
});
});
describe("navidrome", () => {
describe("url", () => {
describe("when BONOB_NAVIDROME_URL is not specified", () => {
it(`should default to http://${hostname()}:4533`, () => {
expect(config().navidrome.url).toEqual(`http://${hostname()}:4533`);
});
});
describe("when BONOB_NAVIDROME_URL is ''", () => {
it(`should default to http://${hostname()}:4533`, () => {
process.env["BONOB_NAVIDROME_URL"] = "";
expect(config().navidrome.url).toEqual(`http://${hostname()}:4533`);
});
});
describe("when BONOB_NAVIDROME_URL is specified", () => {
it(`should use it`, () => {
const url = "http://navidrome.example.com:1234";
process.env["BONOB_NAVIDROME_URL"] = url;
expect(config().navidrome.url).toEqual(url);
});
});
});
});
describe("secret", () => {
it("should default to bonob", () => {
@@ -60,7 +130,12 @@ describe("config", () => {
});
});
describeBooleanConfigValue("deviceDiscovery", "BONOB_SONOS_DEVICE_DISCOVERY", true, config => config.sonos.deviceDiscovery);
describeBooleanConfigValue(
"deviceDiscovery",
"BONOB_SONOS_DEVICE_DISCOVERY",
true,
(config) => config.sonos.deviceDiscovery
);
describe("seedHost", () => {
it("should default to undefined", () => {
@@ -73,7 +148,12 @@ describe("config", () => {
});
});
describeBooleanConfigValue("autoRegister", "BONOB_SONOS_AUTO_REGISTER", false, config => config.sonos.autoRegister);
describeBooleanConfigValue(
"autoRegister",
"BONOB_SONOS_AUTO_REGISTER",
false,
(config) => config.sonos.autoRegister
);
describe("sid", () => {
it("should default to 246", () => {
@@ -111,6 +191,16 @@ describe("config", () => {
});
});
describeBooleanConfigValue("scrobbleTracks", "BONOB_SCROBBLE_TRACKS", true, config => config.scrobbleTracks);
describeBooleanConfigValue("reportNowPlaying", "BONOB_REPORT_NOW_PLAYING", true, config => config.reportNowPlaying);
describeBooleanConfigValue(
"scrobbleTracks",
"BONOB_SCROBBLE_TRACKS",
true,
(config) => config.scrobbleTracks
);
describeBooleanConfigValue(
"reportNowPlaying",
"BONOB_REPORT_NOW_PLAYING",
true,
(config) => config.reportNowPlaying
);
});

View File

@@ -21,6 +21,7 @@ import { Credentials } from "../src/music_service";
import makeServer from "../src/server";
import { Service, bonobService, SONOS_DISABLED } from "../src/sonos";
import supersoap from "./supersoap";
import url, { URLBuilder } from "../src/url_builder";
class LoggedInSonosDriver {
client: Client;
@@ -41,9 +42,10 @@ class LoggedInSonosDriver {
let next = path.shift();
while (next) {
if (next != "root") {
const childIds = this.currentMetadata!.getMetadataResult.mediaCollection!.map(
(it) => it.id
);
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}`;
}
@@ -74,31 +76,50 @@ class LoggedInSonosDriver {
class SonosDriver {
server: Express;
rootUrl: string;
bonobUrl: URLBuilder;
service: Service;
constructor(server: Express, rootUrl: string, service: Service) {
constructor(server: Express, bonobUrl: URLBuilder, service: Service) {
this.server = server;
this.rootUrl = rootUrl;
this.bonobUrl = bonobUrl;
this.service = service;
}
stripServiceRoot = (url: string) => url.replace(this.rootUrl, "");
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.stripServiceRoot(this.service.strings!.uri!))
.get(this.extractPathname(this.service.strings!.uri!))
.expect(200);
await request(this.server)
.get(this.stripServiceRoot(this.service.presentation!.uri!))
.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, this.rootUrl),
httpClient: supersoap(this.server),
});
return client
@@ -109,12 +130,18 @@ class SonosDriver {
)
.then(({ regUrl, linkCode }: { regUrl: string; linkCode: string }) => ({
login: async ({ username, password }: Credentials) => {
await request(this.server)
.get(this.stripServiceRoot(regUrl))
.expect(200);
const action = await request(this.server)
.get(this.extractPathname(regUrl))
.expect(200)
.then((response) => {
const m = response.text.match(/ action="(.*)" /i);
return m![1]!;
});
console.log(`posting to action ${action}`);
return request(this.server)
.post(this.stripServiceRoot(regUrl))
.post(action)
.type("form")
.send({ username, password, linkCode })
.then((response) => ({
@@ -140,92 +167,137 @@ class SonosDriver {
}
describe("scenarios", () => {
const bonobUrl = "http://localhost:1234";
const bonob = bonobService("bonob", 123, bonobUrl);
const musicService = new InMemoryMusicService().hasArtists(
BOB_MARLEY,
BLONDIE
);
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 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);
function itShouldBeAbleToAddTheService(sonosDriver: SonosDriver) {
describe("registering bonob with the sonos device", () => {
it("should complete successfully", async () => {
await sonosDriver.register();
});
});
describe("when the user exists within the music service", () => {
const username = "validuser";
const password = "validpassword";
describe("adding the service", () => {
describe("when the user doesnt exists within the music service", () => {
const username = "invaliduser";
const password = "invalidpassword";
beforeEach(() => {
musicService.hasUser({ username, password });
musicService.hasArtists(BLONDIE, BOB_MARLEY, MADONNA);
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);
});
});
it("should successfuly sign up", async () => {
await sonosDriver
.addService()
.then((it) => it.login({ username, password }))
.then((it) => it.expectSuccess());
describe("when the user exists within the music service", () => {
const username = "validuser";
const password = "validpassword";
expect(linkCodes.count()).toEqual(1);
});
beforeEach(() => {
musicService.hasUser({ username, password });
musicService.hasArtists(BLONDIE, BOB_MARLEY, MADONNA);
});
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 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
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)
)
)
);
);
});
});
});
}
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(
SONOS_DISABLED,
bonob,
bonobUrl,
musicService,
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(
SONOS_DISABLED,
bonob,
bonobUrl,
musicService,
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(
SONOS_DISABLED,
bonob,
bonobUrl,
musicService,
linkCodes
);
const sonosDriver = new SonosDriver(server, bonobUrl, bonob);
itShouldBeAbleToAddTheService(sonosDriver);
});
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -25,6 +25,7 @@ import sonos, {
} from "../src/sonos";
import { aSonosDevice, aService } from "./builders";
import url from "../src/url_builder";
const mockSonosManagerConstructor = <jest.Mock<SonosManager>>SonosManager;
@@ -107,10 +108,10 @@ describe("sonos", () => {
});
describe("bonobService", () => {
describe("when the bonob root does not have a trailing /", () => {
describe("when the bonob url does not have a trailing /", () => {
it("should return a valid bonob service", () => {
expect(
bonobService("some-bonob", 876, "http://bonob.example.com")
bonobService("some-bonob", 876, url("http://bonob.example.com"))
).toEqual({
name: "some-bonob",
sid: 876,
@@ -130,10 +131,10 @@ describe("sonos", () => {
});
});
describe("when the bonob root does have a trailing /", () => {
describe("when the bonob url does have a trailing /", () => {
it("should return a valid bonob service", () => {
expect(
bonobService("some-bonob", 876, "http://bonob.example.com/")
bonobService("some-bonob", 876, url("http://bonob.example.com/"))
).toEqual({
name: "some-bonob",
sid: 876,
@@ -153,10 +154,33 @@ describe("sonos", () => {
});
});
describe("when the bonob url has a context of /some-context", () => {
it("should return a valid bonob service", () => {
expect(
bonobService("some-bonob", 876, url("http://bonob.example.com/some-context"))
).toEqual({
name: "some-bonob",
sid: 876,
uri: `http://bonob.example.com/some-context/ws/sonos`,
secureUri: `http://bonob.example.com/some-context/ws/sonos`,
strings: {
uri: `http://bonob.example.com/some-context/sonos/strings.xml`,
version: PRESENTATION_AND_STRINGS_VERSION,
},
presentation: {
uri: `http://bonob.example.com/some-context/sonos/presentationMap.xml`,
version: PRESENTATION_AND_STRINGS_VERSION,
},
pollInterval: 1200,
authType: "AppLink",
});
});
});
describe("when authType is specified", () => {
it("should return a valid bonob service", () => {
expect(
bonobService("some-bonob", 876, "http://bonob.example.com", 'DeviceLink')
bonobService("some-bonob", 876, url("http://bonob.example.com"), 'DeviceLink')
).toEqual({
name: "some-bonob",
sid: 876,
@@ -242,7 +266,7 @@ describe("sonos", () => {
expect(disabled).toEqual(SONOS_DISABLED);
expect(await disabled.devices()).toEqual([]);
expect(await disabled.services()).toEqual([]);
expect(await disabled.register(aService())).toEqual(false);
expect(await disabled.register(aService())).toEqual(true);
});
});

View File

@@ -1,7 +1,7 @@
import { Express } from "express";
import request from "supertest";
function supersoap(server: Express, rootUrl: string) {
function supersoap(server: Express) {
return {
request: (
rurl: string,
@@ -9,7 +9,8 @@ function supersoap(server: Express, rootUrl: string) {
callback: (error: any, res?: any, body?: any) => any,
exheaders?: any
) => {
const withoutHost = rurl.replace(rootUrl, "");
const url = new URL(rurl);
const withoutHost = `${url.pathname}${url.search}`;
const req =
data == null
? request(server).get(withoutHost).send()

212
tests/url_builder.test.ts Normal file
View File

@@ -0,0 +1,212 @@
import url from "../src/url_builder";
describe("URLBuilder", () => {
describe("construction", () => {
it("with a string", () => {
expect(url("http://example.com/").href()).toEqual("http://example.com/");
expect(url("http://example.com/foobar?name=bob").href()).toEqual(
"http://example.com/foobar?name=bob"
);
});
it("with a URL", () => {
expect(url(new URL("http://example.com/")).href()).toEqual(
"http://example.com/"
);
expect(url(new URL("http://example.com/foobar?name=bob")).href()).toEqual(
"http://example.com/foobar?name=bob"
);
});
});
describe("toString", () => {
it("should print the href", () => {
expect(`${url("http://example.com/")}`).toEqual("http://example.com/");
expect(`${url("http://example.com/foobar?name=bob")}`).toEqual(
"http://example.com/foobar?name=bob"
);
});
});
describe("path", () => {
it("should be the pathname and search", () => {
expect(url("http://example.com/").path()).toEqual("/");
expect(url("http://example.com/?whoop=ie").path()).toEqual("/?whoop=ie");
expect(url("http://example.com/foo/bar").path()).toEqual("/foo/bar");
expect(url("http://example.com/with/search?q=bob&s=100").path()).toEqual("/with/search?q=bob&s=100");
expect(url("http://example.com/drops/hash#1234").path()).toEqual("/drops/hash");
});
});
describe("updating the pathname", () => {
describe("appending", () => {
describe("when there is no existing pathname", ()=>{
it("should return a new URLBuilder with the new pathname appended to the existing pathname", () => {
const original = url("https://example.com?a=b");
const updated = original.append({ pathname: "/the-appended-path" });
expect(original.href()).toEqual("https://example.com/?a=b");
expect(original.pathname()).toEqual("/")
expect(updated.href()).toEqual("https://example.com/the-appended-path?a=b");
expect(updated.pathname()).toEqual("/the-appended-path")
});
});
describe("when the existing pathname is /", ()=>{
it("should return a new URLBuilder with the new pathname appended to the existing pathname", () => {
const original = url("https://example.com/");
const updated = original.append({ pathname: "/the-appended-path" });
expect(original.href()).toEqual("https://example.com/");
expect(original.pathname()).toEqual("/")
expect(updated.href()).toEqual("https://example.com/the-appended-path");
expect(updated.pathname()).toEqual("/the-appended-path")
});
});
describe("when the existing pathname is /first-path", ()=>{
it("should return a new URLBuilder with the new pathname appended to the existing pathname", () => {
const original = url("https://example.com/first-path");
const updated = original.append({ pathname: "/the-appended-path" });
expect(original.href()).toEqual("https://example.com/first-path");
expect(original.pathname()).toEqual("/first-path")
expect(updated.href()).toEqual("https://example.com/first-path/the-appended-path");
expect(updated.pathname()).toEqual("/first-path/the-appended-path")
});
});
describe("when the existing pathname is /first-path/", ()=>{
it("should return a new URLBuilder with the new pathname appended to the existing pathname", () => {
const original = url("https://example.com/first-path/");
const updated = original.append({ pathname: "/the-appended-path" });
expect(original.href()).toEqual("https://example.com/first-path/");
expect(original.pathname()).toEqual("/first-path/")
expect(updated.href()).toEqual("https://example.com/first-path/the-appended-path");
expect(updated.pathname()).toEqual("/first-path/the-appended-path")
});
});
it("should return a new URLBuilder with the new pathname appended to the existing pathname", () => {
const original = url("https://example.com/some-path?a=b");
const updated = original.append({ pathname: "/some-new-path" });
expect(original.href()).toEqual("https://example.com/some-path?a=b");
expect(original.pathname()).toEqual("/some-path")
expect(updated.href()).toEqual("https://example.com/some-path/some-new-path?a=b");
expect(updated.pathname()).toEqual("/some-path/some-new-path")
});
});
describe("replacing", () => {
it("should return a new URLBuilder with the new pathname", () => {
const original = url("https://example.com/some-path?a=b");
const updated = original.with({ pathname: "/some-new-path" });
expect(original.href()).toEqual("https://example.com/some-path?a=b");
expect(original.pathname()).toEqual("/some-path")
expect(updated.href()).toEqual("https://example.com/some-new-path?a=b");
expect(updated.pathname()).toEqual("/some-new-path")
});
});
});
describe("updating search params", () => {
describe("appending", () => {
describe("with records", () => {
it("should return a new URLBuilder with the new search params appended", () => {
const original = url("https://example.com/some-path?a=b&c=d");
const updated = original.append({
searchParams: { x: "y", z: "1" },
});
expect(original.href()).toEqual("https://example.com/some-path?a=b&c=d");
expect(`${original.searchParams()}`).toEqual("a=b&c=d")
expect(updated.href()).toEqual("https://example.com/some-path?a=b&c=d&x=y&z=1");
expect(`${updated.searchParams()}`).toEqual("a=b&c=d&x=y&z=1")
});
});
describe("with URLSearchParams", () => {
it("should return a new URLBuilder with the new search params appended", () => {
const original = url("https://example.com/some-path?a=b&c=d");
const updated = original.append({
searchParams: new URLSearchParams({ x: "y", z: "1" }),
});
expect(original.href()).toEqual("https://example.com/some-path?a=b&c=d");
expect(`${original.searchParams()}`).toEqual("a=b&c=d")
expect(updated.href()).toEqual("https://example.com/some-path?a=b&c=d&x=y&z=1");
expect(`${updated.searchParams()}`).toEqual("a=b&c=d&x=y&z=1")
});
});
});
describe("replacing", () => {
describe("with records", () => {
it("should be able to remove all search params", () => {
const original = url("https://example.com/some-path?a=b&c=d");
const updated = original.with({
searchParams: {},
});
expect(original.href()).toEqual("https://example.com/some-path?a=b&c=d");
expect(`${original.searchParams()}`).toEqual("a=b&c=d")
expect(updated.href()).toEqual("https://example.com/some-path");
expect(`${updated.searchParams()}`).toEqual("")
});
it("should return a new URLBuilder with the new search params", () => {
const original = url("https://example.com/some-path?a=b&c=d");
const updated = original.with({
searchParams: { x: "y", z: "1" },
});
expect(original.href()).toEqual("https://example.com/some-path?a=b&c=d");
expect(`${original.searchParams()}`).toEqual("a=b&c=d")
expect(updated.href()).toEqual("https://example.com/some-path?x=y&z=1");
expect(`${updated.searchParams()}`).toEqual("x=y&z=1")
});
});
describe("with URLSearchParams", () => {
it("should be able to remove all search params", () => {
const original = url("https://example.com/some-path?a=b&c=d");
const updated = original.with({
searchParams: new URLSearchParams({}),
});
expect(original.href()).toEqual("https://example.com/some-path?a=b&c=d");
expect(`${original.searchParams()}`).toEqual("a=b&c=d")
expect(updated.href()).toEqual("https://example.com/some-path");
expect(`${updated.searchParams()}`).toEqual("")
});
it("should return a new URLBuilder with the new search params", () => {
const original = url("https://example.com/some-path?a=b&c=d");
const updated = original.with({
searchParams: new URLSearchParams({ x: "y", z: "1" }),
});
expect(original.href()).toEqual("https://example.com/some-path?a=b&c=d");
expect(`${original.searchParams()}`).toEqual("a=b&c=d")
expect(updated.href()).toEqual("https://example.com/some-path?x=y&z=1");
expect(`${updated.searchParams()}`).toEqual("x=y&z=1")
});
});
});
});
});