mirror of
https://github.com/wkulhanek/bonob.git
synced 2025-12-21 17:33:29 +01:00
Login flow working
This commit is contained in:
@@ -1,20 +1,39 @@
|
|||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
|
export type Association = {
|
||||||
|
authToken: string
|
||||||
|
userId: string
|
||||||
|
nickname: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface LinkCodes {
|
export interface LinkCodes {
|
||||||
mint(): string
|
mint(): string
|
||||||
clear(): any
|
clear(): any
|
||||||
count(): Number
|
count(): Number
|
||||||
|
has(linkCode: string): boolean
|
||||||
|
associate(linkCode: string, association: Association): any
|
||||||
|
associationFor(linkCode: string): Association | undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
export class InMemoryLinkCodes implements LinkCodes {
|
export class InMemoryLinkCodes implements LinkCodes {
|
||||||
linkCodes: Record<string, string> = {}
|
linkCodes: Record<string, Association | undefined> = {}
|
||||||
|
|
||||||
mint() {
|
mint() {
|
||||||
const linkCode = uuid();
|
const linkCode = uuid();
|
||||||
this.linkCodes[linkCode] = ""
|
this.linkCodes[linkCode] = undefined
|
||||||
return linkCode
|
return linkCode
|
||||||
}
|
}
|
||||||
clear = () => { this.linkCodes = {} }
|
clear = () => { this.linkCodes = {} }
|
||||||
count = () => Object.keys(this.linkCodes).length
|
count = () => Object.keys(this.linkCodes).length
|
||||||
|
has = (linkCode: string) => Object.keys(this.linkCodes).includes(linkCode)
|
||||||
|
associate = (linkCode: string, association: Association) => {
|
||||||
|
if(this.has(linkCode))
|
||||||
|
this.linkCodes[linkCode] = association;
|
||||||
|
else
|
||||||
|
throw `Invalid linkCode ${linkCode}`
|
||||||
|
}
|
||||||
|
associationFor = (linkCode: string) => {
|
||||||
|
return this.linkCodes[linkCode]!;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,13 +8,27 @@ const t = Md5.hashStr(`${process.env["BONOB_PASSWORD"]}${s}`);
|
|||||||
|
|
||||||
export type Credentials = { username: string, password: string }
|
export type Credentials = { username: string, password: string }
|
||||||
|
|
||||||
|
export function isSuccess(authResult: AuthSuccess | AuthFailure): authResult is AuthSuccess {
|
||||||
|
return (authResult as AuthSuccess).authToken !== undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AuthSuccess = {
|
||||||
|
authToken: string
|
||||||
|
userId: string
|
||||||
|
nickname: string
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AuthFailure = {
|
||||||
|
message: string
|
||||||
|
}
|
||||||
|
|
||||||
export interface MusicService {
|
export interface MusicService {
|
||||||
login(credentials: Credentials): boolean
|
login(credentials: Credentials): AuthSuccess | AuthFailure
|
||||||
}
|
}
|
||||||
|
|
||||||
export class Navidrome implements MusicService {
|
export class Navidrome implements MusicService {
|
||||||
login(_: Credentials) {
|
login({ username }: Credentials) {
|
||||||
return false
|
return { authToken: `${username}`, userId: username, nickname: username }
|
||||||
}
|
}
|
||||||
|
|
||||||
ping = (): Promise<boolean> =>
|
ping = (): Promise<boolean> =>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { listen } from "soap";
|
|||||||
import { readFileSync } from "fs";
|
import { readFileSync } from "fs";
|
||||||
import path from "path";
|
import path from "path";
|
||||||
import morgan from "morgan";
|
import morgan from "morgan";
|
||||||
|
import crypto from "crypto";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Sonos,
|
Sonos,
|
||||||
@@ -12,8 +13,8 @@ import {
|
|||||||
STRINGS_PATH,
|
STRINGS_PATH,
|
||||||
PRESENTATION_MAP_PATH,
|
PRESENTATION_MAP_PATH,
|
||||||
} from "./sonos";
|
} from "./sonos";
|
||||||
import { LinkCodes, InMemoryLinkCodes } from './link_codes'
|
import { LinkCodes, InMemoryLinkCodes } from "./link_codes";
|
||||||
import { MusicService } from './music_service'
|
import { MusicService, isSuccess } from "./music_service";
|
||||||
import logger from "./logger";
|
import logger from "./logger";
|
||||||
|
|
||||||
const WSDL_FILE = path.resolve(
|
const WSDL_FILE = path.resolve(
|
||||||
@@ -65,16 +66,30 @@ function server(
|
|||||||
app.get("/login", (req, res) => {
|
app.get("/login", (req, res) => {
|
||||||
res.render("login", {
|
res.render("login", {
|
||||||
bonobService,
|
bonobService,
|
||||||
linkCode: req.query.linkCode
|
linkCode: req.query.linkCode,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
app.post("/login", (req, res) => {
|
app.post("/login", (req, res) => {
|
||||||
const canLogIn = musicService.login({ username: req.body.username, password: req.body.password})
|
const { username, password, linkCode } = req.body;
|
||||||
if(canLogIn)
|
const authResult = musicService.login({
|
||||||
res.send("ok")
|
username,
|
||||||
else
|
password,
|
||||||
res.send("boo")
|
});
|
||||||
|
if (isSuccess(authResult)) {
|
||||||
|
if (linkCodes.has(linkCode)) {
|
||||||
|
linkCodes.associate(linkCode, authResult);
|
||||||
|
res.render("loginOK");
|
||||||
|
} else {
|
||||||
|
res.status(400).render("failure", {
|
||||||
|
message: `Invalid linkCode!`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
res.status(403).render("failure", {
|
||||||
|
message: `Login failed, ${authResult.message}!`,
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
app.get(STRINGS_PATH, (_, res) => {
|
app.get(STRINGS_PATH, (_, res) => {
|
||||||
@@ -99,15 +114,44 @@ function server(
|
|||||||
return {
|
return {
|
||||||
getAppLinkResult: {
|
getAppLinkResult: {
|
||||||
authorizeAccount: {
|
authorizeAccount: {
|
||||||
appUrlStringId: 'AppLinkMessage',
|
appUrlStringId: "AppLinkMessage",
|
||||||
deviceLink: {
|
deviceLink: {
|
||||||
regUrl: `${webAddress}/login?linkCode=${linkCode}`,
|
regUrl: `${webAddress}/login?linkCode=${linkCode}`,
|
||||||
linkCode: linkCode,
|
linkCode: linkCode,
|
||||||
showLinkCode: false,
|
showLinkCode: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
getDeviceAuthToken: ({ linkCode }: { linkCode: string }) => {
|
||||||
|
const association = linkCodes.associationFor(linkCode);
|
||||||
|
if (association) {
|
||||||
|
return {
|
||||||
|
getDeviceAuthTokenResult: {
|
||||||
|
authToken: association.authToken,
|
||||||
|
privateKey: "v1",
|
||||||
|
userInfo: {
|
||||||
|
nickname: association.nickname,
|
||||||
|
userIdHashCode: crypto
|
||||||
|
.createHash("sha256")
|
||||||
|
.update(association.userId)
|
||||||
|
.digest("hex"),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
} else {
|
||||||
|
throw {
|
||||||
|
Fault: {
|
||||||
|
faultcode: "Client.NOT_LINKED_RETRY",
|
||||||
|
faultstring: "Link Code not found retry...",
|
||||||
|
detail: {
|
||||||
|
ExceptionInfo: "NOT_LINKED_RETRY",
|
||||||
|
SonosError: "5"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
getSessionId: ({
|
getSessionId: ({
|
||||||
username,
|
username,
|
||||||
@@ -134,16 +178,16 @@ function server(
|
|||||||
x.log = (type, data) => {
|
x.log = (type, data) => {
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case "info":
|
case "info":
|
||||||
logger.info({ level:"info", data });
|
logger.info({ level: "info", data });
|
||||||
break;
|
break;
|
||||||
case "warn":
|
case "warn":
|
||||||
logger.warn({ level:"warn", data });
|
logger.warn({ level: "warn", data });
|
||||||
break;
|
break;
|
||||||
case "error":
|
case "error":
|
||||||
logger.error({ level:"error", data });
|
logger.error({ level: "error", data });
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
logger.debug({ level:"debug", data });
|
logger.debug({ level: "debug", data });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import sonos, { bonobService } from "../src/sonos";
|
|||||||
import server from "../src/server";
|
import server from "../src/server";
|
||||||
|
|
||||||
import logger from "../src/logger";
|
import logger from "../src/logger";
|
||||||
import { InMemoryMusicService } from "builders";
|
import { InMemoryMusicService } from "./in_memory_music_service";
|
||||||
|
|
||||||
const WEB_ADDRESS = "http://localhost:1234"
|
const WEB_ADDRESS = "http://localhost:1234"
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { SonosDevice } from "@svrooij/sonos/lib";
|
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";
|
import { Service, Device } from "../src/sonos";
|
||||||
|
|
||||||
@@ -55,23 +54,3 @@ export function getAppLinkMessage() {
|
|||||||
callbackPath: "",
|
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 = {};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
35
tests/in_memory_music_service.ts
Normal file
35
tests/in_memory_music_service.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
import {
|
||||||
|
MusicService,
|
||||||
|
Credentials,
|
||||||
|
AuthSuccess,
|
||||||
|
AuthFailure,
|
||||||
|
} from "../src/music_service";
|
||||||
|
|
||||||
|
|
||||||
|
export class InMemoryMusicService implements MusicService {
|
||||||
|
users: Record<string, string> = {};
|
||||||
|
|
||||||
|
login({ username, password }: Credentials): AuthSuccess | AuthFailure {
|
||||||
|
if (
|
||||||
|
username != undefined &&
|
||||||
|
password != undefined &&
|
||||||
|
this.users[username] == password
|
||||||
|
) {
|
||||||
|
return { authToken: "token123", userId: username, nickname: username };
|
||||||
|
} else {
|
||||||
|
return { message: `Invalid user:${username}` };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hasUser(credentials: Credentials) {
|
||||||
|
this.users[credentials.username] = credentials.password;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasNoUsers() {
|
||||||
|
this.users = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
clear() {
|
||||||
|
this.users = {};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,7 +2,8 @@ import request from "supertest";
|
|||||||
import makeServer from "../src/server";
|
import makeServer from "../src/server";
|
||||||
import { SONOS_DISABLED, Sonos, Device } from "../src/sonos";
|
import { SONOS_DISABLED, Sonos, Device } from "../src/sonos";
|
||||||
|
|
||||||
import { aDevice, aService, InMemoryMusicService } from './builders';
|
import { aDevice, aService } from './builders';
|
||||||
|
import { InMemoryMusicService } from "./in_memory_music_service";
|
||||||
|
|
||||||
describe("index", () => {
|
describe("index", () => {
|
||||||
describe("when sonos integration is disabled", () => {
|
describe("when sonos integration is disabled", () => {
|
||||||
|
|||||||
47
tests/link_codes.test.ts
Normal file
47
tests/link_codes.test.ts
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
import { InMemoryLinkCodes } from "../src/link_codes"
|
||||||
|
|
||||||
|
describe("InMemoryLinkCodes", () => {
|
||||||
|
const linkCodes = new InMemoryLinkCodes()
|
||||||
|
|
||||||
|
describe('minting', () => {
|
||||||
|
it('should be able to mint unique codes', () => {
|
||||||
|
const code1 = linkCodes.mint()
|
||||||
|
const code2 = linkCodes.mint()
|
||||||
|
const code3 = linkCodes.mint()
|
||||||
|
|
||||||
|
expect(code1).not.toEqual(code2);
|
||||||
|
expect(code1).not.toEqual(code3);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("associating a code with a user", () => {
|
||||||
|
describe('when token is valid', () => {
|
||||||
|
it('should associate the token', () => {
|
||||||
|
const linkCode = linkCodes.mint();
|
||||||
|
const association = { authToken: "token123", nickname: "bob", userId: "1" };
|
||||||
|
|
||||||
|
linkCodes.associate(linkCode, association);
|
||||||
|
|
||||||
|
expect(linkCodes.associationFor(linkCode)).toEqual(association);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when token is valid', () => {
|
||||||
|
it('should throw an error', () => {
|
||||||
|
const invalidLinkCode = "invalidLinkCode";
|
||||||
|
const association = { authToken: "token123", nickname: "bob", userId: "1" };
|
||||||
|
|
||||||
|
expect(() => linkCodes.associate(invalidLinkCode, association)).toThrow(`Invalid linkCode ${invalidLinkCode}`)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('fetching an association for a linkCode', () => {
|
||||||
|
describe('when the token doesnt exist', () => {
|
||||||
|
it('should return undefined', () => {
|
||||||
|
const missingLinkCode = 'someLinkCodeThatDoesntExist';
|
||||||
|
expect(linkCodes.associationFor(missingLinkCode)).toBeUndefined()
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
||||||
|
})
|
||||||
@@ -4,7 +4,8 @@ import { Express } from "express";
|
|||||||
import request from "supertest";
|
import request from "supertest";
|
||||||
|
|
||||||
import { GetAppLinkResult } from "../src/smapi";
|
import { GetAppLinkResult } from "../src/smapi";
|
||||||
import { InMemoryMusicService, getAppLinkMessage } from "./builders";
|
import { getAppLinkMessage } from "./builders";
|
||||||
|
import { InMemoryMusicService } from "./in_memory_music_service";
|
||||||
import { InMemoryLinkCodes } from "../src/link_codes";
|
import { InMemoryLinkCodes } from "../src/link_codes";
|
||||||
import { Credentials } from "../src/music_service";
|
import { Credentials } from "../src/music_service";
|
||||||
import makeServer from "../src/server";
|
import makeServer from "../src/server";
|
||||||
@@ -53,13 +54,14 @@ class SonosDriver {
|
|||||||
.post(this.stripServiceRoot(regUrl))
|
.post(this.stripServiceRoot(regUrl))
|
||||||
.type("form")
|
.type("form")
|
||||||
.send({ username, password, linkCode })
|
.send({ username, password, linkCode })
|
||||||
.expect(200)
|
|
||||||
.then(response => ({
|
.then(response => ({
|
||||||
expectSuccess: () => {
|
expectSuccess: () => {
|
||||||
expect(response.text).toContain("ok")
|
expect(response.status).toEqual(200);
|
||||||
|
expect(response.text).toContain("Login Successful")
|
||||||
},
|
},
|
||||||
expectFailure: () => {
|
expectFailure: () => {
|
||||||
expect(response.text).toContain("boo")
|
expect(response.status).toEqual(403);
|
||||||
|
expect(response.text).toContain("Login failed")
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,18 +1,29 @@
|
|||||||
|
import crypto from "crypto";
|
||||||
import request from "supertest";
|
import request from "supertest";
|
||||||
|
import { createClientAsync } from "soap";
|
||||||
|
|
||||||
import { DOMParserImpl } from 'xmldom-ts';
|
import { DOMParserImpl } from "xmldom-ts";
|
||||||
import * as xpath from 'xpath-ts';
|
import * as xpath from "xpath-ts";
|
||||||
|
|
||||||
|
import { InMemoryLinkCodes, LinkCodes } from "../src/link_codes";
|
||||||
import makeServer from "../src/server";
|
import makeServer from "../src/server";
|
||||||
import { SONOS_DISABLED, STRINGS_PATH } from "../src/sonos";
|
import { bonobService, SONOS_DISABLED, STRINGS_PATH } from "../src/sonos";
|
||||||
|
|
||||||
import { aService, InMemoryMusicService } from './builders';
|
import { aService, getAppLinkMessage } from "./builders";
|
||||||
|
import { InMemoryMusicService } from "./in_memory_music_service";
|
||||||
|
import supersoap from "./supersoap";
|
||||||
|
|
||||||
const parseXML = (value: string) => new DOMParserImpl().parseFromString(value);
|
const parseXML = (value: string) => new DOMParserImpl().parseFromString(value);
|
||||||
const select = xpath.useNamespaces({"sonos": "http://sonos.com/sonosapi"})
|
const select = xpath.useNamespaces({ sonos: "http://sonos.com/sonosapi" });
|
||||||
|
|
||||||
describe('strings.xml', () => {
|
describe("service config", () => {
|
||||||
const server = makeServer(SONOS_DISABLED, aService(), 'http://localhost:1234', new InMemoryMusicService());
|
describe("strings.xml", () => {
|
||||||
|
const server = makeServer(
|
||||||
|
SONOS_DISABLED,
|
||||||
|
aService(),
|
||||||
|
"http://localhost:1234",
|
||||||
|
new InMemoryMusicService()
|
||||||
|
);
|
||||||
|
|
||||||
it("should return xml for the strings", async () => {
|
it("should return xml for the strings", async () => {
|
||||||
const res = await request(server).get(STRINGS_PATH).send();
|
const res = await request(server).get(STRINGS_PATH).send();
|
||||||
@@ -20,8 +31,198 @@ describe('strings.xml', () => {
|
|||||||
expect(res.status).toEqual(200);
|
expect(res.status).toEqual(200);
|
||||||
|
|
||||||
const xml = parseXML(res.text);
|
const xml = parseXML(res.text);
|
||||||
const x = select("//sonos:string[@stringId='AppLinkMessage']/text()", xml) as Node[]
|
const x = select(
|
||||||
expect(x.length).toEqual(1)
|
"//sonos:string[@stringId='AppLinkMessage']/text()",
|
||||||
expect(x[0]!.nodeValue).toEqual("Linking sonos with bonob")
|
xml
|
||||||
|
) as Node[];
|
||||||
|
expect(x.length).toEqual(1);
|
||||||
|
expect(x[0]!.nodeValue).toEqual("Linking sonos with bonob");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("api", () => {
|
||||||
|
const rootUrl = "http://localhost:1234";
|
||||||
|
const service = bonobService("test-api", 133, rootUrl, "AppLink");
|
||||||
|
const musicService = new InMemoryMusicService();
|
||||||
|
const linkCodes = new InMemoryLinkCodes();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
musicService.clear();
|
||||||
|
linkCodes.clear();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("pages", () => {
|
||||||
|
const server = makeServer(
|
||||||
|
SONOS_DISABLED,
|
||||||
|
service,
|
||||||
|
rootUrl,
|
||||||
|
musicService,
|
||||||
|
linkCodes
|
||||||
|
);
|
||||||
|
|
||||||
|
describe("/login", () => {
|
||||||
|
describe("when the credentials are valid", () => {
|
||||||
|
it("should return 200 ok and have associated linkCode with user", async () => {
|
||||||
|
const username = "jane";
|
||||||
|
const password = "password100";
|
||||||
|
const linkCode = linkCodes.mint();
|
||||||
|
|
||||||
|
musicService.hasUser({ username, password });
|
||||||
|
|
||||||
|
const res = await request(server)
|
||||||
|
.post("/login")
|
||||||
|
.type("form")
|
||||||
|
.send({ username, password, linkCode })
|
||||||
|
.expect(200);
|
||||||
|
|
||||||
|
expect(res.text).toContain("Login successful");
|
||||||
|
|
||||||
|
const association = linkCodes.associationFor(linkCode);
|
||||||
|
expect(association.nickname).toEqual(username);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when credentials are invalid", () => {
|
||||||
|
it("should return 403 with message", async () => {
|
||||||
|
const username = "userDoesntExist";
|
||||||
|
const password = "password";
|
||||||
|
const linkCode = linkCodes.mint();
|
||||||
|
|
||||||
|
musicService.hasNoUsers();
|
||||||
|
|
||||||
|
const res = await request(server)
|
||||||
|
.post("/login")
|
||||||
|
.type("form")
|
||||||
|
.send({ username, password, linkCode })
|
||||||
|
.expect(403);
|
||||||
|
|
||||||
|
expect(res.text).toContain(`Login failed, Invalid user:${username}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when linkCode is invalid", () => {
|
||||||
|
it("should return 400 with message", async () => {
|
||||||
|
const username = "jane";
|
||||||
|
const password = "password100";
|
||||||
|
const linkCode = "someLinkCodeThatDoesntExist";
|
||||||
|
|
||||||
|
musicService.hasUser({ username, password });
|
||||||
|
|
||||||
|
const res = await request(server)
|
||||||
|
.post("/login")
|
||||||
|
.type("form")
|
||||||
|
.send({ username, password, linkCode })
|
||||||
|
.expect(400);
|
||||||
|
|
||||||
|
expect(res.text).toContain("Invalid linkCode!");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("soap api", () => {
|
||||||
|
describe("getAppLink", () => {
|
||||||
|
const mockLinkCodes = {
|
||||||
|
mint: jest.fn(),
|
||||||
|
};
|
||||||
|
const server = makeServer(
|
||||||
|
SONOS_DISABLED,
|
||||||
|
service,
|
||||||
|
rootUrl,
|
||||||
|
musicService,
|
||||||
|
(mockLinkCodes as unknown) as LinkCodes
|
||||||
|
);
|
||||||
|
|
||||||
|
it("should do something", async () => {
|
||||||
|
const ws = await createClientAsync(`${service.uri}?wsdl`, {
|
||||||
|
endpoint: service.uri,
|
||||||
|
httpClient: supersoap(server, rootUrl),
|
||||||
|
});
|
||||||
|
|
||||||
|
const linkCode = "theLinkCode8899";
|
||||||
|
|
||||||
|
mockLinkCodes.mint.mockReturnValue(linkCode);
|
||||||
|
|
||||||
|
const result = await ws.getAppLinkAsync(getAppLinkMessage());
|
||||||
|
|
||||||
|
expect(result[0]).toEqual({
|
||||||
|
getAppLinkResult: {
|
||||||
|
authorizeAccount: {
|
||||||
|
appUrlStringId: "AppLinkMessage",
|
||||||
|
deviceLink: {
|
||||||
|
regUrl: `${rootUrl}/login?linkCode=${linkCode}`,
|
||||||
|
linkCode: linkCode,
|
||||||
|
showLinkCode: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("getDeviceAuthToken", () => {
|
||||||
|
const linkCodes = new InMemoryLinkCodes();
|
||||||
|
const server = makeServer(
|
||||||
|
SONOS_DISABLED,
|
||||||
|
service,
|
||||||
|
rootUrl,
|
||||||
|
musicService,
|
||||||
|
linkCodes
|
||||||
|
);
|
||||||
|
|
||||||
|
describe("when there is a linkCode association", () => {
|
||||||
|
it("should return a device auth token", async () => {
|
||||||
|
const linkCode = linkCodes.mint();
|
||||||
|
const association = { authToken: "at", userId: "uid", nickname: "nn" };
|
||||||
|
linkCodes.associate(linkCode, association);
|
||||||
|
|
||||||
|
const ws = await createClientAsync(`${service.uri}?wsdl`, {
|
||||||
|
endpoint: service.uri,
|
||||||
|
httpClient: supersoap(server, rootUrl),
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await ws.getDeviceAuthTokenAsync({ linkCode });
|
||||||
|
|
||||||
|
expect(result[0]).toEqual({
|
||||||
|
getDeviceAuthTokenResult: {
|
||||||
|
authToken: association.authToken,
|
||||||
|
privateKey: "v1",
|
||||||
|
userInfo: {
|
||||||
|
nickname: association.nickname,
|
||||||
|
userIdHashCode: crypto
|
||||||
|
.createHash("sha256")
|
||||||
|
.update(association.userId)
|
||||||
|
.digest("hex"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when there is no linkCode association", () => {
|
||||||
|
it("should return a device auth token", async () => {
|
||||||
|
const linkCode = "invalidLinkCode";
|
||||||
|
|
||||||
|
const ws = await createClientAsync(`${service.uri}?wsdl`, {
|
||||||
|
endpoint: service.uri,
|
||||||
|
httpClient: supersoap(server, rootUrl),
|
||||||
|
});
|
||||||
|
|
||||||
|
await ws
|
||||||
|
.getDeviceAuthTokenAsync({ linkCode })
|
||||||
|
.then(() => {
|
||||||
|
throw "Shouldnt get here";
|
||||||
|
})
|
||||||
|
.catch((e: any) => {
|
||||||
|
expect(e.root.Envelope.Body.Fault).toEqual({
|
||||||
|
faultcode: "Client.NOT_LINKED_RETRY",
|
||||||
|
faultstring: "Link Code not found retry...",
|
||||||
|
detail: { ExceptionInfo: "NOT_LINKED_RETRY", SonosError: "5" },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1,7 +1,8 @@
|
|||||||
import makeServer from "../src/server";
|
import makeServer from "../src/server";
|
||||||
import { SONOS_DISABLED, SOAP_PATH } from "../src/sonos";
|
import { SONOS_DISABLED, SOAP_PATH } from "../src/sonos";
|
||||||
|
|
||||||
import { aService, InMemoryMusicService } from "./builders";
|
import { aService } from "./builders";
|
||||||
|
import { InMemoryMusicService } from "./in_memory_music_service";
|
||||||
import supersoap from './supersoap';
|
import supersoap from './supersoap';
|
||||||
|
|
||||||
import { createClientAsync } from "soap";
|
import { createClientAsync } from "soap";
|
||||||
|
|||||||
5
web/views/failure.eta
Normal file
5
web/views/failure.eta
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<% layout('./layout', { title: "Failure" }) %>
|
||||||
|
|
||||||
|
<div id="content">
|
||||||
|
<h1><%= it.message %></h1>
|
||||||
|
</div>
|
||||||
5
web/views/loginOK.eta
Normal file
5
web/views/loginOK.eta
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
<% layout('./layout', { title: "Login Successful" }) %>
|
||||||
|
|
||||||
|
<div id="content">
|
||||||
|
<h1>Login successful</h1>
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user