mirror of
https://github.com/wkulhanek/bonob.git
synced 2025-12-21 17:33:29 +01:00
SmapiAuthTokens that expire, with sonos refreshAuthToken functionality (#81)
Bearer token to Authorization header for stream requests Versioned SMAPI Tokens
This commit is contained in:
@@ -1,263 +0,0 @@
|
||||
import { v4 as uuid } from "uuid";
|
||||
import dayjs from "dayjs";
|
||||
|
||||
import {
|
||||
AccessTokenPerAuthToken,
|
||||
EncryptedAccessTokens,
|
||||
ExpiringAccessTokens,
|
||||
InMemoryAccessTokens,
|
||||
sha256
|
||||
} from "../src/access_tokens";
|
||||
import { Encryption } from "../src/encryption";
|
||||
|
||||
describe("ExpiringAccessTokens", () => {
|
||||
let now = dayjs();
|
||||
|
||||
const accessTokens = new ExpiringAccessTokens({ now: () => now });
|
||||
|
||||
describe("tokens", () => {
|
||||
it("they should be unique", () => {
|
||||
const authToken = uuid();
|
||||
expect(accessTokens.mint(authToken)).not.toEqual(
|
||||
accessTokens.mint(authToken)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("tokens that dont exist", () => {
|
||||
it("should return undefined", () => {
|
||||
expect(accessTokens.authTokenFor("doesnt exist")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("tokens that have not expired", () => {
|
||||
it("should be able to return them", () => {
|
||||
const authToken = uuid();
|
||||
|
||||
const accessToken = accessTokens.mint(authToken);
|
||||
|
||||
expect(accessTokens.authTokenFor(accessToken)).toEqual(authToken);
|
||||
});
|
||||
|
||||
it("should be able to have many per authToken", () => {
|
||||
const authToken = uuid();
|
||||
|
||||
const accessToken1 = accessTokens.mint(authToken);
|
||||
const accessToken2 = accessTokens.mint(authToken);
|
||||
|
||||
expect(accessTokens.authTokenFor(accessToken1)).toEqual(authToken);
|
||||
expect(accessTokens.authTokenFor(accessToken2)).toEqual(authToken);
|
||||
});
|
||||
});
|
||||
|
||||
describe("tokens that have expired", () => {
|
||||
describe("retrieving it", () => {
|
||||
it("should return undefined", () => {
|
||||
const authToken = uuid();
|
||||
|
||||
now = dayjs();
|
||||
const accessToken = accessTokens.mint(authToken);
|
||||
|
||||
now = now.add(12, "hours").add(1, "second");
|
||||
|
||||
expect(accessTokens.authTokenFor(accessToken)).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("should be cleared out", () => {
|
||||
const authToken1 = uuid();
|
||||
const authToken2 = uuid();
|
||||
|
||||
now = dayjs();
|
||||
|
||||
const accessToken1_1 = accessTokens.mint(authToken1);
|
||||
const accessToken2_1 = accessTokens.mint(authToken2);
|
||||
|
||||
expect(accessTokens.count()).toEqual(2);
|
||||
expect(accessTokens.authTokenFor(accessToken1_1)).toEqual(authToken1);
|
||||
expect(accessTokens.authTokenFor(accessToken2_1)).toEqual(authToken2);
|
||||
|
||||
now = now.add(12, "hours").add(1, "second");
|
||||
|
||||
const accessToken1_2 = accessTokens.mint(authToken1);
|
||||
|
||||
expect(accessTokens.count()).toEqual(1);
|
||||
expect(accessTokens.authTokenFor(accessToken1_1)).toBeUndefined();
|
||||
expect(accessTokens.authTokenFor(accessToken2_1)).toBeUndefined();
|
||||
expect(accessTokens.authTokenFor(accessToken1_2)).toEqual(authToken1);
|
||||
|
||||
now = now.add(6, "hours");
|
||||
|
||||
const accessToken2_2 = accessTokens.mint(authToken2);
|
||||
|
||||
expect(accessTokens.count()).toEqual(2);
|
||||
expect(accessTokens.authTokenFor(accessToken1_1)).toBeUndefined();
|
||||
expect(accessTokens.authTokenFor(accessToken2_1)).toBeUndefined();
|
||||
expect(accessTokens.authTokenFor(accessToken1_2)).toEqual(authToken1);
|
||||
expect(accessTokens.authTokenFor(accessToken2_2)).toEqual(authToken2);
|
||||
|
||||
now = now.add(6, "hours").add(1, "minute");
|
||||
|
||||
expect(accessTokens.authTokenFor(accessToken1_1)).toBeUndefined();
|
||||
expect(accessTokens.authTokenFor(accessToken2_1)).toBeUndefined();
|
||||
expect(accessTokens.authTokenFor(accessToken1_2)).toBeUndefined();
|
||||
expect(accessTokens.authTokenFor(accessToken2_2)).toEqual(authToken2);
|
||||
expect(accessTokens.count()).toEqual(1);
|
||||
|
||||
now = now.add(6, "hours").add(1, "minute");
|
||||
|
||||
expect(accessTokens.authTokenFor(accessToken1_1)).toBeUndefined();
|
||||
expect(accessTokens.authTokenFor(accessToken2_1)).toBeUndefined();
|
||||
expect(accessTokens.authTokenFor(accessToken1_2)).toBeUndefined();
|
||||
expect(accessTokens.authTokenFor(accessToken2_2)).toBeUndefined();
|
||||
expect(accessTokens.count()).toEqual(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("EncryptedAccessTokens", () => {
|
||||
const encryption = {
|
||||
encrypt: jest.fn(),
|
||||
decrypt: jest.fn(),
|
||||
};
|
||||
|
||||
const accessTokens = new EncryptedAccessTokens(
|
||||
(encryption as unknown) as Encryption
|
||||
);
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.resetAllMocks();
|
||||
});
|
||||
|
||||
describe("encrypt and decrypt", () => {
|
||||
it("should be able to round trip the token", () => {
|
||||
const authToken = `the token - ${uuid()}`;
|
||||
const hash = "the encrypted token";
|
||||
|
||||
encryption.encrypt.mockReturnValue(hash);
|
||||
encryption.decrypt.mockReturnValue(authToken);
|
||||
|
||||
const accessToken = accessTokens.mint(authToken);
|
||||
|
||||
expect(accessToken).not.toContain(authToken);
|
||||
expect(accessToken).toEqual(hash);
|
||||
|
||||
expect(accessTokens.authTokenFor(accessToken)).toEqual(authToken);
|
||||
|
||||
expect(encryption.encrypt).toHaveBeenCalledWith(authToken);
|
||||
expect(encryption.decrypt).toHaveBeenCalledWith(hash);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the token is a valid Hash but doesnt decrypt", () => {
|
||||
it("should return undefined", () => {
|
||||
const hash = "valid hash";
|
||||
encryption.decrypt.mockImplementation(() => {
|
||||
throw "Boooooom decryption failed!!!";
|
||||
});
|
||||
expect(
|
||||
accessTokens.authTokenFor(hash)
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the token is not even a valid hash", () => {
|
||||
it("should return undefined", () => {
|
||||
encryption.decrypt.mockImplementation(() => {
|
||||
throw "Boooooom decryption failed!!!";
|
||||
});
|
||||
expect(accessTokens.authTokenFor("some rubbish")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("AccessTokenPerAuthToken", () => {
|
||||
const accessTokens = new AccessTokenPerAuthToken();
|
||||
|
||||
it("should return the same access token for the same auth token", () => {
|
||||
const authToken = "token1";
|
||||
|
||||
const accessToken1 = accessTokens.mint(authToken);
|
||||
const accessToken2 = accessTokens.mint(authToken);
|
||||
|
||||
expect(accessToken1).not.toEqual(authToken);
|
||||
expect(accessToken1).toEqual(accessToken2);
|
||||
});
|
||||
|
||||
describe("when there is an auth token for the access token", () => {
|
||||
it("should be able to retrieve it", () => {
|
||||
const authToken = uuid();
|
||||
const accessToken = accessTokens.mint(authToken);
|
||||
|
||||
expect(accessTokens.authTokenFor(accessToken)).toEqual(authToken);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when there is no auth token for the access token", () => {
|
||||
it("should return undefined", () => {
|
||||
expect(accessTokens.authTokenFor(uuid())).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('sha256 minter', () => {
|
||||
it('should return the same value for the same salt and authToken', () => {
|
||||
const authToken = uuid();
|
||||
const token1 = sha256("salty")(authToken);
|
||||
const token2 = sha256("salty")(authToken);
|
||||
|
||||
expect(token1).not.toEqual(authToken);
|
||||
expect(token1).toEqual(token2);
|
||||
});
|
||||
|
||||
it('should returrn different values for the same salt but different authTokens', () => {
|
||||
const authToken1 = uuid();
|
||||
const authToken2 = uuid();
|
||||
|
||||
const token1 = sha256("salty")(authToken1);
|
||||
const token2= sha256("salty")(authToken2);
|
||||
|
||||
expect(token1).not.toEqual(token2);
|
||||
});
|
||||
|
||||
it('should return different values for the same authToken but different salts', () => {
|
||||
const authToken = uuid();
|
||||
|
||||
const token1 = sha256("salt1")(authToken);
|
||||
const token2= sha256("salt2")(authToken);
|
||||
|
||||
expect(token1).not.toEqual(token2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("InMemoryAccessTokens", () => {
|
||||
const reverseAuthToken = (authToken: string) => authToken.split("").reverse().join("");
|
||||
|
||||
const accessTokens = new InMemoryAccessTokens(reverseAuthToken);
|
||||
|
||||
it("should return the same access token for the same auth token", () => {
|
||||
const authToken = "token1";
|
||||
|
||||
const accessToken1 = accessTokens.mint(authToken);
|
||||
const accessToken2 = accessTokens.mint(authToken);
|
||||
|
||||
expect(accessToken1).not.toEqual(authToken);
|
||||
expect(accessToken1).toEqual(accessToken2);
|
||||
});
|
||||
|
||||
describe("when there is an auth token for the access token", () => {
|
||||
it("should be able to retrieve it", () => {
|
||||
const authToken = uuid();
|
||||
const accessToken = accessTokens.mint(authToken);
|
||||
|
||||
expect(accessTokens.authTokenFor(accessToken)).toEqual(authToken);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when there is no auth token for the access token", () => {
|
||||
it("should return undefined", () => {
|
||||
expect(accessTokens.authTokenFor(uuid())).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
67
tests/api_tokens.test.ts
Normal file
67
tests/api_tokens.test.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import { v4 as uuid } from "uuid";
|
||||
|
||||
import {
|
||||
InMemoryAPITokens,
|
||||
sha256
|
||||
} from "../src/api_tokens";
|
||||
|
||||
describe('sha256 minter', () => {
|
||||
it('should return the same value for the same salt and authToken', () => {
|
||||
const authToken = uuid();
|
||||
const token1 = sha256("salty")(authToken);
|
||||
const token2 = sha256("salty")(authToken);
|
||||
|
||||
expect(token1).not.toEqual(authToken);
|
||||
expect(token1).toEqual(token2);
|
||||
});
|
||||
|
||||
it('should returrn different values for the same salt but different authTokens', () => {
|
||||
const authToken1 = uuid();
|
||||
const authToken2 = uuid();
|
||||
|
||||
const token1 = sha256("salty")(authToken1);
|
||||
const token2= sha256("salty")(authToken2);
|
||||
|
||||
expect(token1).not.toEqual(token2);
|
||||
});
|
||||
|
||||
it('should return different values for the same authToken but different salts', () => {
|
||||
const authToken = uuid();
|
||||
|
||||
const token1 = sha256("salt1")(authToken);
|
||||
const token2= sha256("salt2")(authToken);
|
||||
|
||||
expect(token1).not.toEqual(token2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("InMemoryAPITokens", () => {
|
||||
const reverseAuthToken = (authToken: string) => authToken.split("").reverse().join("");
|
||||
|
||||
const accessTokens = new InMemoryAPITokens(reverseAuthToken);
|
||||
|
||||
it("should return the same access token for the same auth token", () => {
|
||||
const authToken = "token1";
|
||||
|
||||
const accessToken1 = accessTokens.mint(authToken);
|
||||
const accessToken2 = accessTokens.mint(authToken);
|
||||
|
||||
expect(accessToken1).not.toEqual(authToken);
|
||||
expect(accessToken1).toEqual(accessToken2);
|
||||
});
|
||||
|
||||
describe("when there is an auth token for the access token", () => {
|
||||
it("should be able to retrieve it", () => {
|
||||
const authToken = uuid();
|
||||
const accessToken = accessTokens.mint(authToken);
|
||||
|
||||
expect(accessTokens.authTokenFor(accessToken)).toEqual(authToken);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when there is no auth token for the access token", () => {
|
||||
it("should return undefined", () => {
|
||||
expect(accessTokens.authTokenFor(uuid())).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -90,10 +90,11 @@ export function getAppLinkMessage() {
|
||||
};
|
||||
}
|
||||
|
||||
export function someCredentials(token: string): Credentials {
|
||||
export function someCredentials({ token, key } : { token: string, key: string }): Credentials {
|
||||
return {
|
||||
loginToken: {
|
||||
token,
|
||||
key,
|
||||
householdId: "hh1",
|
||||
},
|
||||
deviceId: "d1",
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import dayjs from "dayjs";
|
||||
import libxmljs from "libxmljs2";
|
||||
import { FixedClock } from "../src/clock";
|
||||
|
||||
import {
|
||||
contains,
|
||||
@@ -556,12 +557,11 @@ describe("festivals", () => {
|
||||
backgroundColor: "black",
|
||||
foregroundColor: "black",
|
||||
});
|
||||
let now = dayjs();
|
||||
const clock = { now: () => now };
|
||||
const clock = new FixedClock(dayjs());
|
||||
|
||||
describe("on a day that isn't festive", () => {
|
||||
beforeEach(() => {
|
||||
now = dayjs("2022/10/12");
|
||||
clock.time = dayjs("2022/10/12");
|
||||
});
|
||||
|
||||
it("should use the given colors", () => {
|
||||
@@ -587,7 +587,7 @@ describe("festivals", () => {
|
||||
|
||||
describe("on christmas day", () => {
|
||||
beforeEach(() => {
|
||||
now = dayjs("2022/12/25");
|
||||
clock.time = dayjs("2022/12/25");
|
||||
});
|
||||
|
||||
it("should use the christmas theme colors", () => {
|
||||
@@ -613,7 +613,7 @@ describe("festivals", () => {
|
||||
|
||||
describe("on halloween", () => {
|
||||
beforeEach(() => {
|
||||
now = dayjs("2022/10/31");
|
||||
clock.time = dayjs("2022/10/31");
|
||||
});
|
||||
|
||||
it("should use the given colors", () => {
|
||||
@@ -638,7 +638,7 @@ describe("festivals", () => {
|
||||
|
||||
describe("on may 4", () => {
|
||||
beforeEach(() => {
|
||||
now = dayjs("2022/5/4");
|
||||
clock.time = dayjs("2022/5/4");
|
||||
});
|
||||
|
||||
it("should use the undefined colors, so no color", () => {
|
||||
@@ -664,7 +664,7 @@ describe("festivals", () => {
|
||||
describe("on cny", () => {
|
||||
describe("2022", () => {
|
||||
beforeEach(() => {
|
||||
now = dayjs("2022/02/01");
|
||||
clock.time = dayjs("2022/02/01");
|
||||
});
|
||||
|
||||
it("should use the cny theme", () => {
|
||||
@@ -689,7 +689,7 @@ describe("festivals", () => {
|
||||
|
||||
describe("2023", () => {
|
||||
beforeEach(() => {
|
||||
now = dayjs("2023/01/22");
|
||||
clock.time = dayjs("2023/01/22");
|
||||
});
|
||||
|
||||
it("should use the cny theme", () => {
|
||||
@@ -714,7 +714,7 @@ describe("festivals", () => {
|
||||
|
||||
describe("2024", () => {
|
||||
beforeEach(() => {
|
||||
now = dayjs("2024/02/10");
|
||||
clock.time = dayjs("2024/02/10");
|
||||
});
|
||||
|
||||
it("should use the cny theme", () => {
|
||||
@@ -740,7 +740,7 @@ describe("festivals", () => {
|
||||
|
||||
describe("on holi", () => {
|
||||
beforeEach(() => {
|
||||
now = dayjs("2022/03/18");
|
||||
clock.time = dayjs("2022/03/18");
|
||||
});
|
||||
|
||||
it("should use the given colors", () => {
|
||||
|
||||
@@ -33,7 +33,7 @@ describe("InMemoryMusicService", () => {
|
||||
expect(token.userId).toEqual(credentials.username);
|
||||
expect(token.nickname).toEqual(credentials.username);
|
||||
|
||||
const musicLibrary = service.login(token.authToken);
|
||||
const musicLibrary = service.login(token.serviceToken);
|
||||
|
||||
expect(musicLibrary).toBeDefined();
|
||||
});
|
||||
@@ -47,7 +47,7 @@ describe("InMemoryMusicService", () => {
|
||||
|
||||
service.clear();
|
||||
|
||||
return expect(service.login(token.authToken)).rejects.toEqual(
|
||||
return expect(service.login(token.serviceToken)).rejects.toEqual(
|
||||
"Invalid auth token"
|
||||
);
|
||||
});
|
||||
@@ -63,7 +63,7 @@ describe("InMemoryMusicService", () => {
|
||||
service.hasUser(user);
|
||||
|
||||
const token = (await service.generateToken(user)) as AuthSuccess;
|
||||
musicLibrary = (await service.login(token.authToken)) as MusicLibrary;
|
||||
musicLibrary = (await service.login(token.serviceToken)) as MusicLibrary;
|
||||
});
|
||||
|
||||
describe("artists", () => {
|
||||
|
||||
@@ -41,7 +41,7 @@ export class InMemoryMusicService implements MusicService {
|
||||
this.users[username] == password
|
||||
) {
|
||||
return Promise.resolve({
|
||||
authToken: b64Encode(JSON.stringify({ username, password })),
|
||||
serviceToken: b64Encode(JSON.stringify({ username, password })),
|
||||
userId: username,
|
||||
nickname: username,
|
||||
});
|
||||
@@ -50,8 +50,8 @@ export class InMemoryMusicService implements MusicService {
|
||||
}
|
||||
}
|
||||
|
||||
login(token: string): Promise<MusicLibrary> {
|
||||
const credentials = JSON.parse(b64Decode(token)) as Credentials;
|
||||
login(serviceToken: string): Promise<MusicLibrary> {
|
||||
const credentials = JSON.parse(b64Decode(serviceToken)) as Credentials;
|
||||
if (this.users[credentials.username] != credentials.password)
|
||||
return Promise.reject("Invalid auth token");
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ describe("InMemoryLinkCodes", () => {
|
||||
describe('when token is valid', () => {
|
||||
it('should associate the token', () => {
|
||||
const linkCode = linkCodes.mint();
|
||||
const association = { authToken: "token123", nickname: "bob", userId: "1" };
|
||||
const association = { serviceToken: "token123", nickname: "bob", userId: "1" };
|
||||
|
||||
linkCodes.associate(linkCode, association);
|
||||
|
||||
@@ -29,7 +29,7 @@ describe("InMemoryLinkCodes", () => {
|
||||
describe('when token is valid', () => {
|
||||
it('should throw an error', () => {
|
||||
const invalidLinkCode = "invalidLinkCode";
|
||||
const association = { authToken: "token456", nickname: "bob", userId: "1" };
|
||||
const association = { serviceToken: "token456", nickname: "bob", userId: "1" };
|
||||
|
||||
expect(() => linkCodes.associate(invalidLinkCode, association)).toThrow(`Invalid linkCode ${invalidLinkCode}`)
|
||||
});
|
||||
|
||||
@@ -33,9 +33,10 @@ class LoggedInSonosDriver {
|
||||
this.client = client;
|
||||
this.token = token;
|
||||
this.client.addSoapHeader({
|
||||
credentials: someCredentials(
|
||||
this.token.getDeviceAuthTokenResult.authToken
|
||||
),
|
||||
credentials: someCredentials({
|
||||
token: this.token.getDeviceAuthTokenResult.authToken,
|
||||
key: this.token.getDeviceAuthTokenResult.privateKey
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
@@ -272,7 +273,7 @@ describe("scenarios", () => {
|
||||
bonobUrl,
|
||||
musicService,
|
||||
{
|
||||
linkCodes: () => linkCodes
|
||||
linkCodes: () => linkCodes,
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import dayjs from "dayjs";
|
||||
import request from "supertest";
|
||||
import Image from "image-js";
|
||||
import fs from "fs";
|
||||
import { either as E } from "fp-ts";
|
||||
import path from "path";
|
||||
|
||||
import { MusicService } from "../src/music_service";
|
||||
@@ -16,7 +17,7 @@ import { SONOS_DISABLED, Sonos, Device } from "../src/sonos";
|
||||
|
||||
import { aDevice, aService } from "./builders";
|
||||
import { InMemoryMusicService } from "./in_memory_music_service";
|
||||
import { AccessTokens, ExpiringAccessTokens } from "../src/access_tokens";
|
||||
import { APITokens, InMemoryAPITokens } from "../src/api_tokens";
|
||||
import { InMemoryLinkCodes, LinkCodes } from "../src/link_codes";
|
||||
import { Response } from "express";
|
||||
import { Transform } from "stream";
|
||||
@@ -25,6 +26,7 @@ import i8n, { randomLang } from "../src/i8n";
|
||||
import { SONOS_RECOMMENDED_IMAGE_SIZES } from "../src/smapi";
|
||||
import { Clock, SystemClock } from "../src/clock";
|
||||
import { formatForURL } from "../src/burn";
|
||||
import { ExpiredTokenError, SmapiAuthTokens } from "../src/smapi_auth";
|
||||
|
||||
describe("rangeFilterFor", () => {
|
||||
describe("invalid range header string", () => {
|
||||
@@ -579,7 +581,7 @@ describe("server", () => {
|
||||
associate: jest.fn(),
|
||||
associationFor: jest.fn(),
|
||||
};
|
||||
const accessTokens = {
|
||||
const apiTokens = {
|
||||
mint: jest.fn(),
|
||||
authTokenFor: jest.fn(),
|
||||
};
|
||||
@@ -594,7 +596,7 @@ describe("server", () => {
|
||||
musicService as unknown as MusicService,
|
||||
{
|
||||
linkCodes: () => linkCodes as unknown as LinkCodes,
|
||||
accessTokens: () => accessTokens as unknown as AccessTokens,
|
||||
apiTokens: () => apiTokens as unknown as APITokens,
|
||||
clock,
|
||||
}
|
||||
);
|
||||
@@ -628,14 +630,14 @@ describe("server", () => {
|
||||
const username = "jane";
|
||||
const password = "password100";
|
||||
const linkCode = `linkCode-${uuid()}`;
|
||||
const authToken = {
|
||||
authToken: `authtoken-${uuid()}`,
|
||||
const authSuccess = {
|
||||
serviceToken: `serviceToken-${uuid()}`,
|
||||
userId: `${username}-uid`,
|
||||
nickname: `${username}-nickname`,
|
||||
};
|
||||
|
||||
linkCodes.has.mockReturnValue(true);
|
||||
musicService.generateToken.mockResolvedValue(authToken);
|
||||
musicService.generateToken.mockResolvedValue(authSuccess);
|
||||
linkCodes.associate.mockReturnValue(true);
|
||||
|
||||
const res = await request(server)
|
||||
@@ -654,7 +656,7 @@ describe("server", () => {
|
||||
expect(linkCodes.has).toHaveBeenCalledWith(linkCode);
|
||||
expect(linkCodes.associate).toHaveBeenCalledWith(
|
||||
linkCode,
|
||||
authToken
|
||||
authSuccess
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -731,8 +733,10 @@ describe("server", () => {
|
||||
scrobble: jest.fn(),
|
||||
nowPlaying: jest.fn(),
|
||||
};
|
||||
let now = dayjs();
|
||||
const accessTokens = new ExpiringAccessTokens({ now: () => now });
|
||||
const smapiAuthTokens = {
|
||||
verify: jest.fn(),
|
||||
}
|
||||
const apiTokens = new InMemoryAPITokens();
|
||||
|
||||
const server = makeServer(
|
||||
jest.fn() as unknown as Sonos,
|
||||
@@ -741,17 +745,14 @@ describe("server", () => {
|
||||
musicService as unknown as MusicService,
|
||||
{
|
||||
linkCodes: () => new InMemoryLinkCodes(),
|
||||
accessTokens: () => accessTokens,
|
||||
apiTokens: () => apiTokens,
|
||||
smapiAuthTokens: smapiAuthTokens as unknown as SmapiAuthTokens
|
||||
}
|
||||
);
|
||||
|
||||
const authToken = uuid();
|
||||
const serviceToken = uuid();
|
||||
const trackId = uuid();
|
||||
let accessToken: string;
|
||||
|
||||
beforeEach(() => {
|
||||
accessToken = accessTokens.mint(authToken);
|
||||
});
|
||||
const smapiAuthToken = `smapiAuthToken-${uuid()}`;
|
||||
|
||||
const streamContent = (content: string) => ({
|
||||
pipe: (_: Transform) => {
|
||||
@@ -764,7 +765,7 @@ describe("server", () => {
|
||||
});
|
||||
|
||||
describe("HEAD requests", () => {
|
||||
describe("when there is no access-token", () => {
|
||||
describe("when there is no Bearer token", () => {
|
||||
it("should return a 401", async () => {
|
||||
const res = await request(server).head(
|
||||
bonobUrl.append({ pathname: `/stream/track/${trackId}` }).path()
|
||||
@@ -774,24 +775,27 @@ describe("server", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the access-token has expired", () => {
|
||||
describe("when the Bearer token has expired", () => {
|
||||
it("should return a 401", async () => {
|
||||
now = now.add(1, "day");
|
||||
smapiAuthTokens.verify.mockReturnValue(E.left(new ExpiredTokenError(smapiAuthToken, 0)))
|
||||
|
||||
const res = await request(server).head(
|
||||
bonobUrl
|
||||
.append({
|
||||
pathname: `/stream/track/${trackId}`,
|
||||
searchParams: { bat: accessToken },
|
||||
pathname: `/stream/track/${trackId}`
|
||||
})
|
||||
.path()
|
||||
);
|
||||
.path(),
|
||||
).set('Authorization', `Bearer ${smapiAuthToken}`);
|
||||
|
||||
expect(res.status).toEqual(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the access-token is valid", () => {
|
||||
describe("when the Bearer token is valid", () => {
|
||||
beforeEach(() => {
|
||||
smapiAuthTokens.verify.mockReturnValue(E.right(serviceToken));
|
||||
});
|
||||
|
||||
describe("and the track exists", () => {
|
||||
it("should return a 200", async () => {
|
||||
const trackStream = {
|
||||
@@ -810,9 +814,9 @@ describe("server", () => {
|
||||
const res = await request(server)
|
||||
.head(
|
||||
bonobUrl
|
||||
.append({ pathname: `/stream/track/${trackId}`, searchParams: { bat: accessToken } })
|
||||
.append({ pathname: `/stream/track/${trackId}`})
|
||||
.path()
|
||||
);
|
||||
).set('Authorization', `Bearer ${smapiAuthToken}`);
|
||||
|
||||
expect(res.status).toEqual(trackStream.status);
|
||||
expect(res.headers["content-type"]).toEqual(
|
||||
@@ -836,9 +840,10 @@ describe("server", () => {
|
||||
|
||||
const res = await request(server)
|
||||
.head(bonobUrl
|
||||
.append({ pathname: `/stream/track/${trackId}`, searchParams: { bat: accessToken } })
|
||||
.append({ pathname: `/stream/track/${trackId}` })
|
||||
.path()
|
||||
);
|
||||
)
|
||||
.set('Authorization', `Bearer ${smapiAuthToken}`);
|
||||
|
||||
expect(res.status).toEqual(404);
|
||||
expect(res.body).toEqual({});
|
||||
@@ -848,7 +853,7 @@ describe("server", () => {
|
||||
});
|
||||
|
||||
describe("GET requests", () => {
|
||||
describe("when there is no access-token", () => {
|
||||
describe("when there is no Bearer token", () => {
|
||||
it("should return a 401", async () => {
|
||||
const res = await request(server).get(
|
||||
bonobUrl.append({ pathname: `/stream/track/${trackId}` }).path()
|
||||
@@ -858,296 +863,305 @@ describe("server", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the access-token has expired", () => {
|
||||
describe("when the Bearer token has expired", () => {
|
||||
it("should return a 401", async () => {
|
||||
now = now.add(1, "day");
|
||||
smapiAuthTokens.verify.mockReturnValue(E.left(new ExpiredTokenError(smapiAuthToken, 0)))
|
||||
|
||||
const res = await request(server)
|
||||
.get(
|
||||
bonobUrl
|
||||
.append({ pathname: `/stream/track/${trackId}`, searchParams: { bat: accessToken } })
|
||||
.append({ pathname: `/stream/track/${trackId}` })
|
||||
.path()
|
||||
);
|
||||
).set('Authorization', `Bearer ${smapiAuthToken}`);
|
||||
|
||||
expect(res.status).toEqual(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the track doesnt exist", () => {
|
||||
it("should return a 404", async () => {
|
||||
const stream = {
|
||||
status: 404,
|
||||
headers: {},
|
||||
stream: streamContent(""),
|
||||
};
|
||||
|
||||
musicService.login.mockResolvedValue(musicLibrary);
|
||||
musicLibrary.stream.mockResolvedValue(stream);
|
||||
|
||||
const res = await request(server)
|
||||
.get(
|
||||
bonobUrl
|
||||
.append({ pathname: `/stream/track/${trackId}`, searchParams: { bat: accessToken } })
|
||||
.path()
|
||||
);
|
||||
|
||||
expect(res.status).toEqual(404);
|
||||
|
||||
expect(musicLibrary.nowPlaying).not.toHaveBeenCalled();
|
||||
expect(musicLibrary.stream).toHaveBeenCalledWith({ trackId });
|
||||
describe("when the Bearer token is valid", () => {
|
||||
beforeEach(() => {
|
||||
smapiAuthTokens.verify.mockReturnValue(E.right(serviceToken));
|
||||
});
|
||||
});
|
||||
|
||||
describe("when sonos does not ask for a range", () => {
|
||||
describe("when the music service does not return a content-range, content-length or accept-ranges", () => {
|
||||
it("should return a 200 with the data, without adding the undefined headers", async () => {
|
||||
const content = "some-track";
|
||||
|
||||
describe("when the track doesnt exist", () => {
|
||||
it("should return a 404", async () => {
|
||||
const stream = {
|
||||
status: 200,
|
||||
headers: {
|
||||
// audio/x-flac should be mapped to audio/flac
|
||||
"content-type": "audio/x-flac; charset=utf-8",
|
||||
},
|
||||
stream: streamContent(content),
|
||||
status: 404,
|
||||
headers: {},
|
||||
stream: streamContent(""),
|
||||
};
|
||||
|
||||
|
||||
musicService.login.mockResolvedValue(musicLibrary);
|
||||
musicLibrary.stream.mockResolvedValue(stream);
|
||||
musicLibrary.nowPlaying.mockResolvedValue(true);
|
||||
|
||||
|
||||
const res = await request(server)
|
||||
.get(
|
||||
bonobUrl
|
||||
.append({ pathname: `/stream/track/${trackId}`, searchParams: { bat: accessToken } })
|
||||
.append({ pathname: `/stream/track/${trackId}` })
|
||||
.path()
|
||||
);
|
||||
|
||||
expect(res.status).toEqual(stream.status);
|
||||
expect(res.headers["content-type"]).toEqual(
|
||||
"audio/flac; charset=utf-8"
|
||||
);
|
||||
expect(res.header["accept-ranges"]).toBeUndefined();
|
||||
expect(res.headers["content-length"]).toEqual(
|
||||
`${content.length}`
|
||||
);
|
||||
expect(Object.keys(res.headers)).not.toContain("content-range");
|
||||
|
||||
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
||||
expect(musicLibrary.nowPlaying).toHaveBeenCalledWith(trackId);
|
||||
).set('Authorization', `Bearer ${smapiAuthToken}`);
|
||||
|
||||
expect(res.status).toEqual(404);
|
||||
|
||||
expect(musicLibrary.nowPlaying).not.toHaveBeenCalled();
|
||||
expect(musicLibrary.stream).toHaveBeenCalledWith({ trackId });
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the music service returns undefined values for content-range, content-length or accept-ranges", () => {
|
||||
it("should return a 200 with the data, without adding the undefined headers", async () => {
|
||||
const stream = {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "audio/mp3",
|
||||
"content-length": undefined,
|
||||
"accept-ranges": undefined,
|
||||
"content-range": undefined,
|
||||
},
|
||||
stream: streamContent(""),
|
||||
};
|
||||
|
||||
musicService.login.mockResolvedValue(musicLibrary);
|
||||
musicLibrary.stream.mockResolvedValue(stream);
|
||||
musicLibrary.nowPlaying.mockResolvedValue(true);
|
||||
|
||||
const res = await request(server)
|
||||
.get(
|
||||
bonobUrl
|
||||
.append({ pathname: `/stream/track/${trackId}`, searchParams: { bat: accessToken } })
|
||||
.path()
|
||||
|
||||
describe("when sonos does not ask for a range", () => {
|
||||
describe("when the music service does not return a content-range, content-length or accept-ranges", () => {
|
||||
it("should return a 200 with the data, without adding the undefined headers", async () => {
|
||||
const content = "some-track";
|
||||
|
||||
const stream = {
|
||||
status: 200,
|
||||
headers: {
|
||||
// audio/x-flac should be mapped to audio/flac
|
||||
"content-type": "audio/x-flac; charset=utf-8",
|
||||
},
|
||||
stream: streamContent(content),
|
||||
};
|
||||
|
||||
musicService.login.mockResolvedValue(musicLibrary);
|
||||
musicLibrary.stream.mockResolvedValue(stream);
|
||||
musicLibrary.nowPlaying.mockResolvedValue(true);
|
||||
|
||||
const res = await request(server)
|
||||
.get(
|
||||
bonobUrl
|
||||
.append({ pathname: `/stream/track/${trackId}` })
|
||||
.path()
|
||||
).set('Authorization', `Bearer ${smapiAuthToken}`);
|
||||
|
||||
expect(res.status).toEqual(stream.status);
|
||||
expect(res.headers["content-type"]).toEqual(
|
||||
"audio/flac; charset=utf-8"
|
||||
);
|
||||
|
||||
expect(res.status).toEqual(stream.status);
|
||||
expect(res.headers["content-type"]).toEqual(
|
||||
"audio/mp3; charset=utf-8"
|
||||
);
|
||||
expect(res.header["accept-ranges"]).toEqual(
|
||||
stream.headers["accept-ranges"]
|
||||
);
|
||||
expect(Object.keys(res.headers)).not.toContain("content-range");
|
||||
|
||||
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
||||
expect(musicLibrary.nowPlaying).toHaveBeenCalledWith(trackId);
|
||||
expect(musicLibrary.stream).toHaveBeenCalledWith({ trackId });
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the music service returns a 200", () => {
|
||||
it("should return a 200 with the data", async () => {
|
||||
const stream = {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "audio/mp3",
|
||||
"content-length": "222",
|
||||
"accept-ranges": "bytes",
|
||||
},
|
||||
stream: streamContent(""),
|
||||
};
|
||||
|
||||
musicService.login.mockResolvedValue(musicLibrary);
|
||||
musicLibrary.stream.mockResolvedValue(stream);
|
||||
musicLibrary.nowPlaying.mockResolvedValue(true);
|
||||
|
||||
const res = await request(server)
|
||||
.get(
|
||||
bonobUrl
|
||||
.append({ pathname: `/stream/track/${trackId}`, searchParams: { bat: accessToken } })
|
||||
.path()
|
||||
expect(res.header["accept-ranges"]).toBeUndefined();
|
||||
expect(res.headers["content-length"]).toEqual(
|
||||
`${content.length}`
|
||||
);
|
||||
|
||||
expect(res.status).toEqual(stream.status);
|
||||
expect(res.header["content-type"]).toEqual(
|
||||
`${stream.headers["content-type"]}; charset=utf-8`
|
||||
);
|
||||
expect(res.header["accept-ranges"]).toEqual(
|
||||
stream.headers["accept-ranges"]
|
||||
);
|
||||
expect(res.header["content-range"]).toBeUndefined();
|
||||
|
||||
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
||||
expect(musicLibrary.nowPlaying).toHaveBeenCalledWith(trackId);
|
||||
expect(musicLibrary.stream).toHaveBeenCalledWith({ trackId });
|
||||
expect(Object.keys(res.headers)).not.toContain("content-range");
|
||||
|
||||
expect(musicService.login).toHaveBeenCalledWith(serviceToken);
|
||||
expect(musicLibrary.nowPlaying).toHaveBeenCalledWith(trackId);
|
||||
expect(musicLibrary.stream).toHaveBeenCalledWith({ trackId });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the music service returns a 206", () => {
|
||||
it("should return a 206 with the data", async () => {
|
||||
const stream = {
|
||||
status: 206,
|
||||
headers: {
|
||||
"content-type": "audio/ogg",
|
||||
"content-length": "333",
|
||||
"accept-ranges": "bytez",
|
||||
"content-range": "100-200",
|
||||
},
|
||||
stream: streamContent(""),
|
||||
};
|
||||
|
||||
musicService.login.mockResolvedValue(musicLibrary);
|
||||
musicLibrary.stream.mockResolvedValue(stream);
|
||||
musicLibrary.nowPlaying.mockResolvedValue(true);
|
||||
|
||||
const res = await request(server)
|
||||
.get(
|
||||
bonobUrl
|
||||
.append({ pathname: `/stream/track/${trackId}`, searchParams: { bat: accessToken } })
|
||||
.path()
|
||||
|
||||
describe("when the music service returns undefined values for content-range, content-length or accept-ranges", () => {
|
||||
it("should return a 200 with the data, without adding the undefined headers", async () => {
|
||||
const stream = {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "audio/mp3",
|
||||
"content-length": undefined,
|
||||
"accept-ranges": undefined,
|
||||
"content-range": undefined,
|
||||
},
|
||||
stream: streamContent(""),
|
||||
};
|
||||
|
||||
musicService.login.mockResolvedValue(musicLibrary);
|
||||
musicLibrary.stream.mockResolvedValue(stream);
|
||||
musicLibrary.nowPlaying.mockResolvedValue(true);
|
||||
|
||||
const res = await request(server)
|
||||
.get(
|
||||
bonobUrl
|
||||
.append({ pathname: `/stream/track/${trackId}` })
|
||||
.path()
|
||||
).set('Authorization', `Bearer ${smapiAuthToken}`);
|
||||
|
||||
expect(res.status).toEqual(stream.status);
|
||||
expect(res.headers["content-type"]).toEqual(
|
||||
"audio/mp3; charset=utf-8"
|
||||
);
|
||||
|
||||
expect(res.status).toEqual(stream.status);
|
||||
expect(res.header["content-type"]).toEqual(
|
||||
`${stream.headers["content-type"]}; charset=utf-8`
|
||||
);
|
||||
expect(res.header["accept-ranges"]).toEqual(
|
||||
stream.headers["accept-ranges"]
|
||||
);
|
||||
expect(res.header["content-range"]).toEqual(
|
||||
stream.headers["content-range"]
|
||||
);
|
||||
|
||||
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
||||
expect(musicLibrary.nowPlaying).toHaveBeenCalledWith(trackId);
|
||||
expect(musicLibrary.stream).toHaveBeenCalledWith({ trackId });
|
||||
expect(res.header["accept-ranges"]).toEqual(
|
||||
stream.headers["accept-ranges"]
|
||||
);
|
||||
expect(Object.keys(res.headers)).not.toContain("content-range");
|
||||
|
||||
expect(musicService.login).toHaveBeenCalledWith(serviceToken);
|
||||
expect(musicLibrary.nowPlaying).toHaveBeenCalledWith(trackId);
|
||||
expect(musicLibrary.stream).toHaveBeenCalledWith({ trackId });
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when sonos does ask for a range", () => {
|
||||
describe("when the music service returns a 200", () => {
|
||||
it("should return a 200 with the data", async () => {
|
||||
const stream = {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "audio/mp3",
|
||||
"content-length": "222",
|
||||
"accept-ranges": "none",
|
||||
},
|
||||
stream: streamContent(""),
|
||||
};
|
||||
|
||||
musicService.login.mockResolvedValue(musicLibrary);
|
||||
musicLibrary.stream.mockResolvedValue(stream);
|
||||
musicLibrary.nowPlaying.mockResolvedValue(true);
|
||||
|
||||
const requestedRange = "40-";
|
||||
|
||||
const res = await request(server)
|
||||
.get(
|
||||
bonobUrl
|
||||
.append({ pathname: `/stream/track/${trackId}`, searchParams: { bat: accessToken } })
|
||||
.path()
|
||||
)
|
||||
.set("Range", requestedRange);
|
||||
|
||||
expect(res.status).toEqual(stream.status);
|
||||
expect(res.header["content-type"]).toEqual(
|
||||
`${stream.headers["content-type"]}; charset=utf-8`
|
||||
);
|
||||
expect(res.header["accept-ranges"]).toEqual(
|
||||
stream.headers["accept-ranges"]
|
||||
);
|
||||
expect(res.header["content-range"]).toBeUndefined();
|
||||
|
||||
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
||||
expect(musicLibrary.nowPlaying).toHaveBeenCalledWith(trackId);
|
||||
expect(musicLibrary.stream).toHaveBeenCalledWith({
|
||||
trackId,
|
||||
range: requestedRange,
|
||||
|
||||
describe("when the music service returns a 200", () => {
|
||||
it("should return a 200 with the data", async () => {
|
||||
const stream = {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "audio/mp3",
|
||||
"content-length": "222",
|
||||
"accept-ranges": "bytes",
|
||||
},
|
||||
stream: streamContent(""),
|
||||
};
|
||||
|
||||
musicService.login.mockResolvedValue(musicLibrary);
|
||||
musicLibrary.stream.mockResolvedValue(stream);
|
||||
musicLibrary.nowPlaying.mockResolvedValue(true);
|
||||
|
||||
const res = await request(server)
|
||||
.get(
|
||||
bonobUrl
|
||||
.append({ pathname: `/stream/track/${trackId}` })
|
||||
.path()
|
||||
).set('Authorization', `Bearer ${smapiAuthToken}`);
|
||||
|
||||
expect(res.status).toEqual(stream.status);
|
||||
expect(res.header["content-type"]).toEqual(
|
||||
`${stream.headers["content-type"]}; charset=utf-8`
|
||||
);
|
||||
expect(res.header["accept-ranges"]).toEqual(
|
||||
stream.headers["accept-ranges"]
|
||||
);
|
||||
expect(res.header["content-range"]).toBeUndefined();
|
||||
|
||||
expect(musicService.login).toHaveBeenCalledWith(serviceToken);
|
||||
expect(musicLibrary.nowPlaying).toHaveBeenCalledWith(trackId);
|
||||
expect(musicLibrary.stream).toHaveBeenCalledWith({ trackId });
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the music service returns a 206", () => {
|
||||
it("should return a 206 with the data", async () => {
|
||||
const stream = {
|
||||
status: 206,
|
||||
headers: {
|
||||
"content-type": "audio/ogg",
|
||||
"content-length": "333",
|
||||
"accept-ranges": "bytez",
|
||||
"content-range": "100-200",
|
||||
},
|
||||
stream: streamContent(""),
|
||||
};
|
||||
|
||||
musicService.login.mockResolvedValue(musicLibrary);
|
||||
musicLibrary.stream.mockResolvedValue(stream);
|
||||
musicLibrary.nowPlaying.mockResolvedValue(true);
|
||||
|
||||
const res = await request(server)
|
||||
.get(
|
||||
bonobUrl
|
||||
.append({ pathname: `/stream/track/${trackId}` })
|
||||
.path()
|
||||
).set('Authorization', `Bearer ${smapiAuthToken}`);
|
||||
|
||||
expect(res.status).toEqual(stream.status);
|
||||
expect(res.header["content-type"]).toEqual(
|
||||
`${stream.headers["content-type"]}; charset=utf-8`
|
||||
);
|
||||
expect(res.header["accept-ranges"]).toEqual(
|
||||
stream.headers["accept-ranges"]
|
||||
);
|
||||
expect(res.header["content-range"]).toEqual(
|
||||
stream.headers["content-range"]
|
||||
);
|
||||
|
||||
expect(musicService.login).toHaveBeenCalledWith(serviceToken);
|
||||
expect(musicLibrary.nowPlaying).toHaveBeenCalledWith(trackId);
|
||||
expect(musicLibrary.stream).toHaveBeenCalledWith({ trackId });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the music service returns a 206", () => {
|
||||
it("should return a 206 with the data", async () => {
|
||||
const stream = {
|
||||
status: 206,
|
||||
headers: {
|
||||
"content-type": "audio/ogg",
|
||||
"content-length": "333",
|
||||
"accept-ranges": "bytez",
|
||||
"content-range": "100-200",
|
||||
},
|
||||
stream: streamContent(""),
|
||||
};
|
||||
|
||||
musicService.login.mockResolvedValue(musicLibrary);
|
||||
musicLibrary.stream.mockResolvedValue(stream);
|
||||
musicLibrary.nowPlaying.mockResolvedValue(true);
|
||||
|
||||
const res = await request(server)
|
||||
.get(
|
||||
bonobUrl
|
||||
.append({ pathname: `/stream/track/${trackId}`, searchParams: { bat: accessToken } })
|
||||
.path()
|
||||
)
|
||||
.set("Range", "4000-5000");
|
||||
|
||||
expect(res.status).toEqual(stream.status);
|
||||
expect(res.header["content-type"]).toEqual(
|
||||
`${stream.headers["content-type"]}; charset=utf-8`
|
||||
);
|
||||
expect(res.header["accept-ranges"]).toEqual(
|
||||
stream.headers["accept-ranges"]
|
||||
);
|
||||
expect(res.header["content-range"]).toEqual(
|
||||
stream.headers["content-range"]
|
||||
);
|
||||
|
||||
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
||||
expect(musicLibrary.nowPlaying).toHaveBeenCalledWith(trackId);
|
||||
expect(musicLibrary.stream).toHaveBeenCalledWith({
|
||||
trackId,
|
||||
range: "4000-5000",
|
||||
|
||||
describe("when sonos does ask for a range", () => {
|
||||
describe("when the music service returns a 200", () => {
|
||||
it("should return a 200 with the data", async () => {
|
||||
const stream = {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "audio/mp3",
|
||||
"content-length": "222",
|
||||
"accept-ranges": "none",
|
||||
},
|
||||
stream: streamContent(""),
|
||||
};
|
||||
|
||||
musicService.login.mockResolvedValue(musicLibrary);
|
||||
musicLibrary.stream.mockResolvedValue(stream);
|
||||
musicLibrary.nowPlaying.mockResolvedValue(true);
|
||||
|
||||
const requestedRange = "40-";
|
||||
|
||||
const res = await request(server)
|
||||
.get(
|
||||
bonobUrl
|
||||
.append({ pathname: `/stream/track/${trackId}` })
|
||||
.path()
|
||||
)
|
||||
.set('Authorization', `Bearer ${smapiAuthToken}`)
|
||||
.set("Range", requestedRange);
|
||||
|
||||
expect(res.status).toEqual(stream.status);
|
||||
expect(res.header["content-type"]).toEqual(
|
||||
`${stream.headers["content-type"]}; charset=utf-8`
|
||||
);
|
||||
expect(res.header["accept-ranges"]).toEqual(
|
||||
stream.headers["accept-ranges"]
|
||||
);
|
||||
expect(res.header["content-range"]).toBeUndefined();
|
||||
|
||||
expect(musicService.login).toHaveBeenCalledWith(serviceToken);
|
||||
expect(musicLibrary.nowPlaying).toHaveBeenCalledWith(trackId);
|
||||
expect(musicLibrary.stream).toHaveBeenCalledWith({
|
||||
trackId,
|
||||
range: requestedRange,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the music service returns a 206", () => {
|
||||
it("should return a 206 with the data", async () => {
|
||||
const stream = {
|
||||
status: 206,
|
||||
headers: {
|
||||
"content-type": "audio/ogg",
|
||||
"content-length": "333",
|
||||
"accept-ranges": "bytez",
|
||||
"content-range": "100-200",
|
||||
},
|
||||
stream: streamContent(""),
|
||||
};
|
||||
|
||||
musicService.login.mockResolvedValue(musicLibrary);
|
||||
musicLibrary.stream.mockResolvedValue(stream);
|
||||
musicLibrary.nowPlaying.mockResolvedValue(true);
|
||||
|
||||
const res = await request(server)
|
||||
.get(
|
||||
bonobUrl
|
||||
.append({ pathname: `/stream/track/${trackId}` })
|
||||
.path()
|
||||
)
|
||||
.set('Authorization', `Bearer ${smapiAuthToken}`)
|
||||
.set("Range", "4000-5000");
|
||||
|
||||
expect(res.status).toEqual(stream.status);
|
||||
expect(res.header["content-type"]).toEqual(
|
||||
`${stream.headers["content-type"]}; charset=utf-8`
|
||||
);
|
||||
expect(res.header["accept-ranges"]).toEqual(
|
||||
stream.headers["accept-ranges"]
|
||||
);
|
||||
expect(res.header["content-range"]).toEqual(
|
||||
stream.headers["content-range"]
|
||||
);
|
||||
|
||||
expect(musicService.login).toHaveBeenCalledWith(serviceToken);
|
||||
expect(musicLibrary.nowPlaying).toHaveBeenCalledWith(trackId);
|
||||
expect(musicLibrary.stream).toHaveBeenCalledWith({
|
||||
trackId,
|
||||
range: "4000-5000",
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1158,8 +1172,7 @@ describe("server", () => {
|
||||
const musicLibrary = {
|
||||
coverArt: jest.fn(),
|
||||
};
|
||||
let now = dayjs();
|
||||
const accessTokens = new ExpiringAccessTokens({ now: () => now });
|
||||
const apiTokens = new InMemoryAPITokens();
|
||||
|
||||
const server = makeServer(
|
||||
jest.fn() as unknown as Sonos,
|
||||
@@ -1168,13 +1181,13 @@ describe("server", () => {
|
||||
musicService as unknown as MusicService,
|
||||
{
|
||||
linkCodes: () => new InMemoryLinkCodes(),
|
||||
accessTokens: () => accessTokens,
|
||||
apiTokens: () => apiTokens,
|
||||
}
|
||||
);
|
||||
|
||||
const authToken = uuid();
|
||||
const serviceToken = uuid();
|
||||
const albumId = uuid();
|
||||
let accessToken: string;
|
||||
let apiToken: string;
|
||||
|
||||
const coverArtResponse = (
|
||||
opt: Partial<{ status: number; contentType: string; data: Buffer }>
|
||||
@@ -1186,7 +1199,7 @@ describe("server", () => {
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
accessToken = accessTokens.mint(authToken);
|
||||
apiToken = apiTokens.mint(serviceToken);
|
||||
});
|
||||
|
||||
describe("when there is no access-token", () => {
|
||||
@@ -1197,18 +1210,6 @@ describe("server", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the access-token has expired", () => {
|
||||
it("should return a 401", async () => {
|
||||
now = now.add(1, "day");
|
||||
|
||||
const res = await request(server).get(
|
||||
`/art/${encodeURIComponent(formatForURL({ system: "subsonic", resource: "art:whatever" }))}/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
|
||||
);
|
||||
|
||||
expect(res.status).toEqual(401);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when there is a valid access token", () => {
|
||||
describe("art", () => {
|
||||
["0", "-1", "foo"].forEach((size) => {
|
||||
@@ -1219,9 +1220,9 @@ describe("server", () => {
|
||||
musicService.login.mockResolvedValue(musicLibrary);
|
||||
const res = await request(server)
|
||||
.get(
|
||||
`/art/${encodeURIComponent(formatForURL(coverArtURN))}/size/${size}?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
|
||||
`/art/${encodeURIComponent(formatForURL(coverArtURN))}/size/${size}?${BONOB_ACCESS_TOKEN_HEADER}=${apiToken}`
|
||||
)
|
||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
||||
.set(BONOB_ACCESS_TOKEN_HEADER, apiToken);
|
||||
|
||||
expect(res.status).toEqual(400);
|
||||
});
|
||||
@@ -1241,16 +1242,16 @@ describe("server", () => {
|
||||
|
||||
const res = await request(server)
|
||||
.get(
|
||||
`/art/${encodeURIComponent(formatForURL(coverArtURN))}/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
|
||||
`/art/${encodeURIComponent(formatForURL(coverArtURN))}/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${apiToken}`
|
||||
)
|
||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
||||
.set(BONOB_ACCESS_TOKEN_HEADER, apiToken);
|
||||
|
||||
expect(res.status).toEqual(coverArt.status);
|
||||
expect(res.header["content-type"]).toEqual(
|
||||
coverArt.contentType
|
||||
);
|
||||
|
||||
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
||||
expect(musicService.login).toHaveBeenCalledWith(serviceToken);
|
||||
expect(musicLibrary.coverArt).toHaveBeenCalledWith(
|
||||
coverArtURN,
|
||||
180
|
||||
@@ -1267,9 +1268,9 @@ describe("server", () => {
|
||||
|
||||
const res = await request(server)
|
||||
.get(
|
||||
`/art/${encodeURIComponent(formatForURL(coverArtURN))}/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
|
||||
`/art/${encodeURIComponent(formatForURL(coverArtURN))}/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${apiToken}`
|
||||
)
|
||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
||||
.set(BONOB_ACCESS_TOKEN_HEADER, apiToken);
|
||||
|
||||
expect(res.status).toEqual(404);
|
||||
});
|
||||
@@ -1310,14 +1311,14 @@ describe("server", () => {
|
||||
.get(
|
||||
`/art/${urns.map(it => encodeURIComponent(formatForURL(it))).join(
|
||||
"&"
|
||||
)}/size/200?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
|
||||
)}/size/200?${BONOB_ACCESS_TOKEN_HEADER}=${apiToken}`
|
||||
)
|
||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
||||
.set(BONOB_ACCESS_TOKEN_HEADER, apiToken);
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.header["content-type"]).toEqual("image/png");
|
||||
|
||||
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
||||
expect(musicService.login).toHaveBeenCalledWith(serviceToken);
|
||||
urns.forEach((it) => {
|
||||
expect(musicLibrary.coverArt).toHaveBeenCalledWith(it, 200);
|
||||
});
|
||||
@@ -1348,9 +1349,9 @@ describe("server", () => {
|
||||
.get(
|
||||
`/art/${urns.map(it => encodeURIComponent(formatForURL(it))).join(
|
||||
"&"
|
||||
)}/size/200?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
|
||||
)}/size/200?${BONOB_ACCESS_TOKEN_HEADER}=${apiToken}`
|
||||
)
|
||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
||||
.set(BONOB_ACCESS_TOKEN_HEADER, apiToken);
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.header["content-type"]).toEqual(
|
||||
@@ -1373,9 +1374,9 @@ describe("server", () => {
|
||||
.get(
|
||||
`/art/${urns.map(it => encodeURIComponent(formatForURL(it))).join(
|
||||
"&"
|
||||
)}/size/200?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
|
||||
)}/size/200?${BONOB_ACCESS_TOKEN_HEADER}=${apiToken}`
|
||||
)
|
||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
||||
.set(BONOB_ACCESS_TOKEN_HEADER, apiToken);
|
||||
|
||||
expect(res.status).toEqual(404);
|
||||
});
|
||||
@@ -1409,14 +1410,14 @@ describe("server", () => {
|
||||
.get(
|
||||
`/art/${urns.map(it => encodeURIComponent(formatForURL(it))).join(
|
||||
"&"
|
||||
)}/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
|
||||
)}/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${apiToken}`
|
||||
)
|
||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
||||
.set(BONOB_ACCESS_TOKEN_HEADER, apiToken);
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.header["content-type"]).toEqual("image/png");
|
||||
|
||||
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
||||
expect(musicService.login).toHaveBeenCalledWith(serviceToken);
|
||||
urns.forEach((it) => {
|
||||
expect(musicLibrary.coverArt).toHaveBeenCalledWith(it, 180);
|
||||
});
|
||||
@@ -1465,14 +1466,14 @@ describe("server", () => {
|
||||
.get(
|
||||
`/art/${urns.map(it => encodeURIComponent(formatForURL(it))).join(
|
||||
"&"
|
||||
)}/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
|
||||
)}/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${apiToken}`
|
||||
)
|
||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
||||
.set(BONOB_ACCESS_TOKEN_HEADER, apiToken);
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.header["content-type"]).toEqual("image/png");
|
||||
|
||||
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
||||
expect(musicService.login).toHaveBeenCalledWith(serviceToken);
|
||||
urns.forEach((urn) => {
|
||||
expect(musicLibrary.coverArt).toHaveBeenCalledWith(urn, 180);
|
||||
});
|
||||
@@ -1513,14 +1514,14 @@ describe("server", () => {
|
||||
.get(
|
||||
`/art/${urns.map(it => encodeURIComponent(formatForURL(it))).join(
|
||||
"&"
|
||||
)}/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
|
||||
)}/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${apiToken}`
|
||||
)
|
||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
||||
.set(BONOB_ACCESS_TOKEN_HEADER, apiToken);
|
||||
|
||||
expect(res.status).toEqual(200);
|
||||
expect(res.header["content-type"]).toEqual("image/png");
|
||||
|
||||
expect(musicService.login).toHaveBeenCalledWith(authToken);
|
||||
expect(musicService.login).toHaveBeenCalledWith(serviceToken);
|
||||
urns.forEach((it) => {
|
||||
expect(musicLibrary.coverArt).toHaveBeenCalledWith(it, 180);
|
||||
});
|
||||
@@ -1540,9 +1541,9 @@ describe("server", () => {
|
||||
|
||||
const res = await request(server)
|
||||
.get(
|
||||
`/art/${encodeURIComponent(formatForURL(coverArtURN))}/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
|
||||
`/art/${encodeURIComponent(formatForURL(coverArtURN))}/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${apiToken}`
|
||||
)
|
||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
||||
.set(BONOB_ACCESS_TOKEN_HEADER, apiToken);
|
||||
|
||||
expect(res.status).toEqual(404);
|
||||
});
|
||||
@@ -1557,9 +1558,9 @@ describe("server", () => {
|
||||
|
||||
const res = await request(server)
|
||||
.get(
|
||||
`/art/artist:${albumId}/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${accessToken}`
|
||||
`/art/artist:${albumId}/size/180?${BONOB_ACCESS_TOKEN_HEADER}=${apiToken}`
|
||||
)
|
||||
.set(BONOB_ACCESS_TOKEN_HEADER, accessToken);
|
||||
.set(BONOB_ACCESS_TOKEN_HEADER, apiToken);
|
||||
|
||||
expect(res.status).toEqual(500);
|
||||
});
|
||||
@@ -1583,7 +1584,7 @@ describe("server", () => {
|
||||
jest.fn() as unknown as MusicService,
|
||||
{
|
||||
linkCodes: () => new InMemoryLinkCodes(),
|
||||
accessTokens: () => jest.fn() as unknown as AccessTokens,
|
||||
apiTokens: () => jest.fn() as unknown as APITokens,
|
||||
clock,
|
||||
iconColors,
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
188
tests/smapi_auth.test.ts
Normal file
188
tests/smapi_auth.test.ts
Normal file
@@ -0,0 +1,188 @@
|
||||
import { v4 as uuid } from "uuid";
|
||||
import jwt from "jsonwebtoken";
|
||||
|
||||
import {
|
||||
ExpiredTokenError,
|
||||
InvalidTokenError,
|
||||
isSmapiRefreshTokenResultFault,
|
||||
JWTSmapiLoginTokens,
|
||||
smapiTokenAsString,
|
||||
smapiTokenFromString,
|
||||
SMAPI_TOKEN_VERSION,
|
||||
} from "../src/smapi_auth";
|
||||
import { either as E } from "fp-ts";
|
||||
import { FixedClock } from "../src/clock";
|
||||
import dayjs from "dayjs";
|
||||
import { b64Encode } from "../src/b64";
|
||||
|
||||
describe("smapiTokenAsString", () => {
|
||||
it("can round trip token to and from string", () => {
|
||||
const smapiToken = { token: uuid(), key: uuid(), someOtherStuff: 'this needs to be explicitly ignored' };
|
||||
const asString = smapiTokenAsString(smapiToken)
|
||||
|
||||
expect(asString).toEqual(b64Encode(JSON.stringify({
|
||||
token: smapiToken.token,
|
||||
key: smapiToken.key,
|
||||
})));
|
||||
expect(smapiTokenFromString(asString)).toEqual({
|
||||
token: smapiToken.token,
|
||||
key: smapiToken.key
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("isSmapiRefreshTokenResultFault", () => {
|
||||
it("should return true for a refreshAuthTokenResult fault", () => {
|
||||
const faultWithRefreshAuthToken = {
|
||||
Fault: {
|
||||
faultcode: "",
|
||||
faultstring: "",
|
||||
detail: {
|
||||
refreshAuthTokenResult: {
|
||||
authToken: "x",
|
||||
privateKey: "x",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
expect(isSmapiRefreshTokenResultFault(faultWithRefreshAuthToken)).toEqual(
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
it("should return false when is not a refreshAuthTokenResult", () => {
|
||||
expect(isSmapiRefreshTokenResultFault({ Fault: { faultcode: "", faultstring:" " }})).toEqual(
|
||||
false
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("auth", () => {
|
||||
describe("JWTSmapiLoginTokens", () => {
|
||||
const clock = new FixedClock(dayjs());
|
||||
|
||||
const expiresIn = "1h";
|
||||
const secret = `secret-${uuid()}`;
|
||||
const smapiLoginTokens = new JWTSmapiLoginTokens(clock, secret, expiresIn);
|
||||
|
||||
describe("issuing a new token", () => {
|
||||
it("should issue a token that can then be verified", () => {
|
||||
const serviceToken = uuid();
|
||||
|
||||
const smapiToken = smapiLoginTokens.issue(serviceToken);
|
||||
|
||||
expect(smapiToken.token).toEqual(
|
||||
jwt.sign(
|
||||
{
|
||||
serviceToken,
|
||||
iat: Math.floor(clock.now().toDate().getDate() / 1000),
|
||||
},
|
||||
secret + SMAPI_TOKEN_VERSION + smapiToken.key,
|
||||
{ expiresIn }
|
||||
)
|
||||
);
|
||||
expect(smapiToken.token).not.toContain(serviceToken);
|
||||
expect(smapiToken.token).not.toContain(secret);
|
||||
expect(smapiToken.token).not.toContain(":");
|
||||
|
||||
const roundTripped = smapiLoginTokens.verify(smapiToken);
|
||||
|
||||
expect(roundTripped).toEqual(E.right(serviceToken));
|
||||
});
|
||||
});
|
||||
|
||||
describe("when verifying the token fails", () => {
|
||||
describe("due to the version changing", () => {
|
||||
it("should return an error", () => {
|
||||
const authToken = uuid();
|
||||
|
||||
const v1SmapiTokens = new JWTSmapiLoginTokens(
|
||||
clock,
|
||||
secret,
|
||||
expiresIn,
|
||||
() => uuid(),
|
||||
"1"
|
||||
);
|
||||
|
||||
const v2SmapiTokens = new JWTSmapiLoginTokens(
|
||||
clock,
|
||||
secret,
|
||||
expiresIn,
|
||||
() => uuid(),
|
||||
"2"
|
||||
);
|
||||
|
||||
const v1Token = v1SmapiTokens.issue(authToken);
|
||||
expect(v1SmapiTokens.verify(v1Token)).toEqual(E.right(authToken));
|
||||
|
||||
const result = v2SmapiTokens.verify(v1Token);
|
||||
expect(result).toEqual(
|
||||
E.left(new InvalidTokenError("invalid signature"))
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("due to secret changing", () => {
|
||||
it("should return an error", () => {
|
||||
const authToken = uuid();
|
||||
|
||||
const smapiToken = new JWTSmapiLoginTokens(
|
||||
clock,
|
||||
"A different secret",
|
||||
expiresIn
|
||||
).issue(authToken);
|
||||
|
||||
const result = smapiLoginTokens.verify(smapiToken);
|
||||
expect(result).toEqual(
|
||||
E.left(new InvalidTokenError("invalid signature"))
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("due to key changing", () => {
|
||||
it("should return an error", () => {
|
||||
const authToken = uuid();
|
||||
|
||||
const smapiToken = smapiLoginTokens.issue(authToken);
|
||||
|
||||
const result = smapiLoginTokens.verify({
|
||||
...smapiToken,
|
||||
key: "some other key",
|
||||
});
|
||||
expect(result).toEqual(
|
||||
E.left(new InvalidTokenError("invalid signature"))
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the token has expired", () => {
|
||||
it("should return an ExpiredTokenError, with the authToken", () => {
|
||||
const authToken = uuid();
|
||||
const now = dayjs();
|
||||
const tokenIssuedAt = now.subtract(31, "seconds");
|
||||
|
||||
const tokensWith30SecondExpiry = new JWTSmapiLoginTokens(
|
||||
clock,
|
||||
uuid(),
|
||||
"30s"
|
||||
);
|
||||
|
||||
clock.time = tokenIssuedAt;
|
||||
const expiredToken = tokensWith30SecondExpiry.issue(authToken);
|
||||
|
||||
clock.time = now;
|
||||
|
||||
const result = tokensWith30SecondExpiry.verify(expiredToken);
|
||||
expect(result).toEqual(
|
||||
E.left(
|
||||
new ExpiredTokenError(
|
||||
authToken,
|
||||
tokenIssuedAt.add(30, "seconds").unix()
|
||||
)
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -725,7 +725,7 @@ describe("Subsonic", () => {
|
||||
password,
|
||||
})) as AuthSuccess;
|
||||
|
||||
expect(token.authToken).toBeDefined();
|
||||
expect(token.serviceToken).toBeDefined();
|
||||
expect(token.nickname).toEqual(username);
|
||||
expect(token.userId).toEqual(username);
|
||||
|
||||
@@ -763,7 +763,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.genres());
|
||||
|
||||
expect(result).toEqual([]);
|
||||
@@ -793,7 +793,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.genres());
|
||||
|
||||
expect(result).toEqual([{ id: b64Encode("genre1"), name: "genre1" }]);
|
||||
@@ -826,7 +826,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.genres());
|
||||
|
||||
expect(result).toEqual([
|
||||
@@ -884,7 +884,7 @@ describe("Subsonic", () => {
|
||||
const result: Artist = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.artist(artist.id!));
|
||||
|
||||
expect(result).toEqual({
|
||||
@@ -946,7 +946,7 @@ describe("Subsonic", () => {
|
||||
const result: Artist = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.artist(artist.id!));
|
||||
|
||||
expect(result).toEqual({
|
||||
@@ -1002,7 +1002,7 @@ describe("Subsonic", () => {
|
||||
const result: Artist = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.artist(artist.id!));
|
||||
|
||||
expect(result).toEqual({
|
||||
@@ -1056,7 +1056,7 @@ describe("Subsonic", () => {
|
||||
const result: Artist = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.artist(artist.id!));
|
||||
|
||||
expect(result).toEqual({
|
||||
@@ -1113,7 +1113,7 @@ describe("Subsonic", () => {
|
||||
const result: Artist = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.artist(artist.id!));
|
||||
|
||||
expect(result).toEqual({
|
||||
@@ -1167,7 +1167,7 @@ describe("Subsonic", () => {
|
||||
const result: Artist = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.artist(artist.id!));
|
||||
|
||||
expect(result).toEqual({
|
||||
@@ -1222,7 +1222,7 @@ describe("Subsonic", () => {
|
||||
const result: Artist = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.artist(artist.id!));
|
||||
|
||||
expect(result).toEqual({
|
||||
@@ -1278,7 +1278,7 @@ describe("Subsonic", () => {
|
||||
const result: Artist = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.artist(artist.id!));
|
||||
|
||||
expect(result).toEqual({
|
||||
@@ -1332,7 +1332,7 @@ describe("Subsonic", () => {
|
||||
const result: Artist = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.artist(artist.id!));
|
||||
|
||||
expect(result).toEqual({
|
||||
@@ -1384,7 +1384,7 @@ describe("Subsonic", () => {
|
||||
const result: Artist = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.artist(artist.id!));
|
||||
|
||||
expect(result).toEqual({
|
||||
@@ -1449,7 +1449,7 @@ describe("Subsonic", () => {
|
||||
const artists = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.artists({ _index: 0, _count: 100 }));
|
||||
|
||||
expect(artists).toEqual({
|
||||
@@ -1478,7 +1478,7 @@ describe("Subsonic", () => {
|
||||
const artists = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.artists({ _index: 0, _count: 100 }));
|
||||
|
||||
expect(artists).toEqual({
|
||||
@@ -1519,7 +1519,7 @@ describe("Subsonic", () => {
|
||||
const artists = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.artists({ _index: 0, _count: 100 }));
|
||||
|
||||
const expectedResults = [{
|
||||
@@ -1561,7 +1561,7 @@ describe("Subsonic", () => {
|
||||
const artists = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.artists({ _index: 0, _count: 100 }));
|
||||
|
||||
const expectedResults = [artist1, artist2, artist3, artist4].map(
|
||||
@@ -1597,7 +1597,7 @@ describe("Subsonic", () => {
|
||||
const artists = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.artists({ _index: 1, _count: 2 }));
|
||||
|
||||
const expectedResults = [artist2, artist3].map((it) => ({
|
||||
@@ -1659,7 +1659,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.albums(q));
|
||||
|
||||
expect(result).toEqual({
|
||||
@@ -1714,7 +1714,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.albums(q));
|
||||
|
||||
expect(result).toEqual({
|
||||
@@ -1768,7 +1768,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.albums(q));
|
||||
|
||||
expect(result).toEqual({
|
||||
@@ -1813,7 +1813,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.albums(q));
|
||||
|
||||
expect(result).toEqual({
|
||||
@@ -1858,7 +1858,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.albums(q));
|
||||
|
||||
expect(result).toEqual({
|
||||
@@ -1912,7 +1912,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.albums(q));
|
||||
|
||||
expect(result).toEqual({
|
||||
@@ -1965,7 +1965,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.albums(q));
|
||||
|
||||
expect(result).toEqual({
|
||||
@@ -2033,7 +2033,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.albums(q));
|
||||
|
||||
expect(result).toEqual({
|
||||
@@ -2087,7 +2087,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.albums(q));
|
||||
|
||||
expect(result).toEqual({
|
||||
@@ -2163,7 +2163,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.albums(q));
|
||||
|
||||
expect(result).toEqual({
|
||||
@@ -2223,7 +2223,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.albums(q));
|
||||
|
||||
expect(result).toEqual({
|
||||
@@ -2282,7 +2282,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.albums(q));
|
||||
|
||||
expect(result).toEqual({
|
||||
@@ -2351,7 +2351,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.albums(q));
|
||||
|
||||
expect(result).toEqual({
|
||||
@@ -2418,7 +2418,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.albums(q));
|
||||
|
||||
expect(result).toEqual({
|
||||
@@ -2483,7 +2483,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.albums(q));
|
||||
|
||||
expect(result).toEqual({
|
||||
@@ -2541,7 +2541,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.album(album.id));
|
||||
|
||||
expect(result).toEqual(album);
|
||||
@@ -2622,7 +2622,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.tracks(album.id));
|
||||
|
||||
expect(result).toEqual([track1, track2, track3, track4]);
|
||||
@@ -2672,7 +2672,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.tracks(album.id));
|
||||
|
||||
expect(result).toEqual([track]);
|
||||
@@ -2710,7 +2710,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.tracks(album.id));
|
||||
|
||||
expect(result).toEqual([]);
|
||||
@@ -2761,7 +2761,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.track(track.id));
|
||||
|
||||
expect(result).toEqual({
|
||||
@@ -2811,7 +2811,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.track(track.id));
|
||||
|
||||
expect(result).toEqual({
|
||||
@@ -2886,7 +2886,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.stream({ trackId, range: undefined }));
|
||||
|
||||
expect(result.headers).toEqual({
|
||||
@@ -2928,7 +2928,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.stream({ trackId, range: undefined }));
|
||||
|
||||
expect(result.headers).toEqual({
|
||||
@@ -2972,7 +2972,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.stream({ trackId, range: undefined }));
|
||||
|
||||
expect(result.headers).toEqual({
|
||||
@@ -3021,7 +3021,7 @@ describe("Subsonic", () => {
|
||||
const musicLibrary = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken));
|
||||
.then((it) => navidrome.login(it.serviceToken));
|
||||
|
||||
return expect(
|
||||
musicLibrary.stream({ trackId, range: undefined })
|
||||
@@ -3046,7 +3046,7 @@ describe("Subsonic", () => {
|
||||
const musicLibrary = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken));
|
||||
.then((it) => navidrome.login(it.serviceToken));
|
||||
|
||||
return expect(
|
||||
musicLibrary.stream({ trackId, range: undefined })
|
||||
@@ -3087,7 +3087,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.stream({ trackId, range }));
|
||||
|
||||
expect(result.headers).toEqual({
|
||||
@@ -3140,7 +3140,7 @@ describe("Subsonic", () => {
|
||||
await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.stream({ trackId, range: undefined }));
|
||||
|
||||
expect(streamClientApplication).toHaveBeenCalledWith(track);
|
||||
@@ -3185,7 +3185,7 @@ describe("Subsonic", () => {
|
||||
await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.stream({ trackId, range }));
|
||||
|
||||
expect(streamClientApplication).toHaveBeenCalledWith(track);
|
||||
@@ -3227,7 +3227,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.coverArt(coverArtURN));
|
||||
|
||||
expect(result).toEqual({
|
||||
@@ -3266,7 +3266,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.coverArt(coverArtURN, size));
|
||||
|
||||
expect(result).toEqual({
|
||||
@@ -3297,7 +3297,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.coverArt({ system: "external", resource: "http://localhost:404" }, size));
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
@@ -3316,7 +3316,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.coverArt(covertArtURN, 190));
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
@@ -3343,7 +3343,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.coverArt(covertArtURN));
|
||||
|
||||
expect(result).toEqual({
|
||||
@@ -3376,7 +3376,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.coverArt(covertArtURN));
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
@@ -3406,7 +3406,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.coverArt(covertArtURN, size));
|
||||
|
||||
expect(result).toEqual({
|
||||
@@ -3440,7 +3440,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.coverArt(covertArtURN, size));
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
@@ -3457,7 +3457,7 @@ describe("Subsonic", () => {
|
||||
navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.rate(trackId, rating));
|
||||
|
||||
const artist = anArtist();
|
||||
@@ -3705,7 +3705,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.scrobble(id));
|
||||
|
||||
expect(result).toEqual(true);
|
||||
@@ -3737,7 +3737,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.scrobble(id));
|
||||
|
||||
expect(result).toEqual(false);
|
||||
@@ -3766,7 +3766,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.nowPlaying(id));
|
||||
|
||||
expect(result).toEqual(true);
|
||||
@@ -3798,7 +3798,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.nowPlaying(id));
|
||||
|
||||
expect(result).toEqual(false);
|
||||
@@ -3829,7 +3829,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.searchArtists("foo"));
|
||||
|
||||
expect(result).toEqual([artistToArtistSummary(artist1)]);
|
||||
@@ -3863,7 +3863,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.searchArtists("foo"));
|
||||
|
||||
expect(result).toEqual([
|
||||
@@ -3895,7 +3895,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.searchArtists("foo"));
|
||||
|
||||
expect(result).toEqual([]);
|
||||
@@ -3934,7 +3934,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.searchAlbums("foo"));
|
||||
|
||||
expect(result).toEqual([albumToAlbumSummary(album)]);
|
||||
@@ -3984,7 +3984,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.searchAlbums("moo"));
|
||||
|
||||
expect(result).toEqual([
|
||||
@@ -4016,7 +4016,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.searchAlbums("foo"));
|
||||
|
||||
expect(result).toEqual([]);
|
||||
@@ -4065,7 +4065,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.searchTracks("foo"));
|
||||
|
||||
expect(result).toEqual([track]);
|
||||
@@ -4140,7 +4140,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.searchTracks("moo"));
|
||||
|
||||
expect(result).toEqual([track1, track2]);
|
||||
@@ -4169,7 +4169,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.searchTracks("foo"));
|
||||
|
||||
expect(result).toEqual([]);
|
||||
@@ -4203,7 +4203,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.playlists());
|
||||
|
||||
expect(result).toEqual([playlist]);
|
||||
@@ -4231,7 +4231,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.playlists());
|
||||
|
||||
expect(result).toEqual(playlists);
|
||||
@@ -4254,7 +4254,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.playlists());
|
||||
|
||||
expect(result).toEqual([]);
|
||||
@@ -4282,7 +4282,7 @@ describe("Subsonic", () => {
|
||||
navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.playlist(id))
|
||||
).rejects.toEqual("Subsonic error:data not found");
|
||||
});
|
||||
@@ -4338,7 +4338,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.playlist(id));
|
||||
|
||||
expect(result).toEqual({
|
||||
@@ -4375,7 +4375,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.playlist(playlist.id));
|
||||
|
||||
expect(result).toEqual(playlist);
|
||||
@@ -4406,7 +4406,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.createPlaylist(name));
|
||||
|
||||
expect(result).toEqual({ id, name });
|
||||
@@ -4433,7 +4433,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.deletePlaylist(id));
|
||||
|
||||
expect(result).toEqual(true);
|
||||
@@ -4461,7 +4461,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.addToPlaylist(playlistId, trackId));
|
||||
|
||||
expect(result).toEqual(true);
|
||||
@@ -4489,7 +4489,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.removeFromPlaylist(playlistId, indicies));
|
||||
|
||||
expect(result).toEqual(true);
|
||||
@@ -4539,7 +4539,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.similarSongs(id));
|
||||
|
||||
expect(result).toEqual([track1]);
|
||||
@@ -4612,7 +4612,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.similarSongs(id));
|
||||
|
||||
expect(result).toEqual([track1, track2, track3]);
|
||||
@@ -4642,7 +4642,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.similarSongs(id));
|
||||
|
||||
expect(result).toEqual([]);
|
||||
@@ -4673,7 +4673,7 @@ describe("Subsonic", () => {
|
||||
navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.similarSongs(id))
|
||||
).rejects.toEqual("Subsonic error:data not found");
|
||||
});
|
||||
@@ -4715,7 +4715,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.topSongs(artistId));
|
||||
|
||||
expect(result).toEqual([track1]);
|
||||
@@ -4785,7 +4785,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.topSongs(artistId));
|
||||
|
||||
expect(result).toEqual([track1, track2, track3]);
|
||||
@@ -4827,7 +4827,7 @@ describe("Subsonic", () => {
|
||||
const result = await navidrome
|
||||
.generateToken({ username, password })
|
||||
.then((it) => it as AuthSuccess)
|
||||
.then((it) => navidrome.login(it.authToken))
|
||||
.then((it) => navidrome.login(it.serviceToken))
|
||||
.then((it) => it.topSongs(artistId));
|
||||
|
||||
expect(result).toEqual([]);
|
||||
|
||||
@@ -1,23 +1,23 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "es2019",
|
||||
"baseUrl": "./",
|
||||
"module": "commonjs",
|
||||
"experimentalDecorators": true,
|
||||
"strictPropertyInitialization": false,
|
||||
"isolatedModules": false,
|
||||
"strict": true,
|
||||
"noImplicitAny": false,
|
||||
"typeRoots" : [
|
||||
"../typings",
|
||||
"../node_modules/@types"
|
||||
]
|
||||
},
|
||||
"exclude": [
|
||||
"../node_modules"
|
||||
],
|
||||
"include": [
|
||||
"./**/*.ts"
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"target": "es2019",
|
||||
"baseUrl": "./",
|
||||
"module": "commonjs",
|
||||
"experimentalDecorators": true,
|
||||
"strictPropertyInitialization": false,
|
||||
"isolatedModules": false,
|
||||
"strict": true,
|
||||
"noImplicitAny": false,
|
||||
"typeRoots" : [
|
||||
"../typings",
|
||||
"../node_modules/@types"
|
||||
]
|
||||
}
|
||||
},
|
||||
"exclude": [
|
||||
"../node_modules"
|
||||
],
|
||||
"include": [
|
||||
"./**/*.ts"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user