mirror of
https://github.com/wkulhanek/bonob.git
synced 2025-12-21 17:33:29 +01:00
refactor
This commit is contained in:
35
src/http.ts
35
src/http.ts
@@ -1,8 +1,16 @@
|
|||||||
import { AxiosPromise, AxiosRequestConfig, ResponseType } from "axios";
|
import {
|
||||||
|
AxiosPromise,
|
||||||
|
AxiosRequestConfig,
|
||||||
|
Method,
|
||||||
|
ResponseType,
|
||||||
|
} from "axios";
|
||||||
|
|
||||||
export interface Http {
|
export interface Http {
|
||||||
(config: AxiosRequestConfig): AxiosPromise<any>;
|
(config: AxiosRequestConfig): AxiosPromise<any>;
|
||||||
}
|
}
|
||||||
|
export interface Http2 extends Http {
|
||||||
|
with: (defaults: Partial<RequestParams>) => Http2;
|
||||||
|
}
|
||||||
|
|
||||||
export type RequestParams = {
|
export type RequestParams = {
|
||||||
baseURL: string;
|
baseURL: string;
|
||||||
@@ -10,11 +18,25 @@ export type RequestParams = {
|
|||||||
params: any;
|
params: any;
|
||||||
headers: any;
|
headers: any;
|
||||||
responseType: ResponseType;
|
responseType: ResponseType;
|
||||||
|
method: Method;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const http =
|
const wrap = (http2: Http2, defaults: Partial<RequestParams>): Http2 => {
|
||||||
(base: Http, defaults: Partial<RequestParams>): Http =>
|
const f = ((config: AxiosRequestConfig) => http2(merge(defaults, config))) as Http2;
|
||||||
(config: AxiosRequestConfig) => {
|
f.with = (defaults: Partial<RequestParams>) => wrap(f, defaults);
|
||||||
|
return f;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const http2From = (http: Http): Http2 => {
|
||||||
|
const f = ((config: AxiosRequestConfig) => http(config)) as Http2;
|
||||||
|
f.with = (defaults: Partial<RequestParams>) => wrap(f, defaults);
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
const merge = (
|
||||||
|
defaults: Partial<RequestParams>,
|
||||||
|
config: AxiosRequestConfig
|
||||||
|
) => {
|
||||||
let toApply = {
|
let toApply = {
|
||||||
...defaults,
|
...defaults,
|
||||||
...config,
|
...config,
|
||||||
@@ -37,5 +59,8 @@ export const http =
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return base(toApply);
|
return toApply;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const http =
|
||||||
|
(base: Http, defaults: Partial<RequestParams>): Http => (config: AxiosRequestConfig) => base(merge(defaults, config));
|
||||||
|
|||||||
@@ -33,13 +33,10 @@ import makeI8N, { asLANGs, KEY, keys as i8nKeys, LANG } from "./i8n";
|
|||||||
import { Icon, ICONS, festivals, features } from "./icon";
|
import { Icon, ICONS, festivals, features } from "./icon";
|
||||||
import _, { shuffle } from "underscore";
|
import _, { shuffle } from "underscore";
|
||||||
import morgan from "morgan";
|
import morgan from "morgan";
|
||||||
import { takeWithRepeats } from "./utils";
|
import { mask, takeWithRepeats } from "./utils";
|
||||||
import { parse } from "./burn";
|
import { parse } from "./burn";
|
||||||
import { axiosImageFetcher, ImageFetcher } from "./images";
|
import { axiosImageFetcher, ImageFetcher } from "./images";
|
||||||
import {
|
import { JWTSmapiLoginTokens, SmapiAuthTokens } from "./smapi_auth";
|
||||||
JWTSmapiLoginTokens,
|
|
||||||
SmapiAuthTokens,
|
|
||||||
} from "./smapi_auth";
|
|
||||||
|
|
||||||
export const BONOB_ACCESS_TOKEN_HEADER = "bat";
|
export const BONOB_ACCESS_TOKEN_HEADER = "bat";
|
||||||
|
|
||||||
@@ -377,23 +374,28 @@ function server(
|
|||||||
logger.info(
|
logger.info(
|
||||||
`${trace} bnb<- ${req.method} ${req.path}?${JSON.stringify(
|
`${trace} bnb<- ${req.method} ${req.path}?${JSON.stringify(
|
||||||
req.query
|
req.query
|
||||||
)}, headers=${JSON.stringify({ ...req.headers, "bnbt": "*****", "bnbk": "*****" })}`
|
)}, headers=${JSON.stringify(mask(req.headers, ["bnbt", "bnbk"]))}`
|
||||||
);
|
);
|
||||||
|
|
||||||
const serviceToken = pipe(
|
const serviceToken = pipe(
|
||||||
E.fromNullable("Missing bnbt header")(req.headers["bnbt"] as string),
|
E.fromNullable("Missing bnbt header")(req.headers["bnbt"] as string),
|
||||||
E.chain(token => pipe(
|
E.chain((token) =>
|
||||||
|
pipe(
|
||||||
E.fromNullable("Missing bnbk header")(req.headers["bnbk"] as string),
|
E.fromNullable("Missing bnbk header")(req.headers["bnbk"] as string),
|
||||||
E.map(key => ({ token, key }))
|
E.map((key) => ({ token, key }))
|
||||||
)),
|
)
|
||||||
|
),
|
||||||
E.chain((auth) =>
|
E.chain((auth) =>
|
||||||
pipe(
|
pipe(
|
||||||
smapiAuthTokens.verify(auth),
|
smapiAuthTokens.verify(auth),
|
||||||
E.mapLeft((_) => "Auth token failed to verify")
|
E.mapLeft((_) => "Auth token failed to verify")
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
E.getOrElseW(() => undefined)
|
E.getOrElseW((e: string) => {
|
||||||
)
|
logger.error(`Failed to get serviceToken for stream: ${e}`);
|
||||||
|
return undefined;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
if (!serviceToken) {
|
if (!serviceToken) {
|
||||||
return res.status(401).send();
|
return res.status(401).send();
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import axios from "axios";
|
|||||||
import randomstring from "randomstring";
|
import randomstring from "randomstring";
|
||||||
import _ from "underscore";
|
import _ from "underscore";
|
||||||
// todo: rename http2 to http
|
// todo: rename http2 to http
|
||||||
import { Http, http as http2, RequestParams } from "../http";
|
import { Http2, http2From } from "../http";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
Credentials,
|
Credentials,
|
||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
import { b64Encode, b64Decode } from "../b64";
|
import { b64Encode, b64Decode } from "../b64";
|
||||||
import { axiosImageFetcher, ImageFetcher } from "../images";
|
import { axiosImageFetcher, ImageFetcher } from "../images";
|
||||||
import { navidromeMusicLibrary, SubsonicGenericMusicLibrary } from "./library";
|
import { navidromeMusicLibrary, SubsonicGenericMusicLibrary } from "./library";
|
||||||
import { getJSON as getJSON } from "./subsonic_http";
|
import { client } from "./subsonic_http";
|
||||||
|
|
||||||
export const t = (password: string, s: string) =>
|
export const t = (password: string, s: string) =>
|
||||||
Md5.hashStr(`${password}${s}`);
|
Md5.hashStr(`${password}${s}`);
|
||||||
@@ -101,7 +101,7 @@ export class Subsonic implements MusicService {
|
|||||||
// todo: why is this in here?
|
// todo: why is this in here?
|
||||||
externalImageFetcher: ImageFetcher;
|
externalImageFetcher: ImageFetcher;
|
||||||
|
|
||||||
subsonicHttp: Http;
|
subsonic: Http2;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
url: string,
|
url: string,
|
||||||
@@ -111,29 +111,22 @@ export class Subsonic implements MusicService {
|
|||||||
this.url = url;
|
this.url = url;
|
||||||
this.streamClientApplication = streamClientApplication;
|
this.streamClientApplication = streamClientApplication;
|
||||||
this.externalImageFetcher = externalImageFetcher;
|
this.externalImageFetcher = externalImageFetcher;
|
||||||
this.subsonicHttp = http2(axios, {
|
this.subsonic = http2From(axios).with({
|
||||||
baseURL: this.url,
|
baseURL: this.url,
|
||||||
params: { v: "1.16.1", c: DEFAULT_CLIENT_APPLICATION },
|
params: { v: "1.16.1", c: DEFAULT_CLIENT_APPLICATION },
|
||||||
headers: { "User-Agent": "bonob" },
|
headers: { "User-Agent": "bonob" },
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
authenticatedSubsonicHttp = (credentials: Credentials) =>
|
asAuthParams = (credentials: Credentials) => ({
|
||||||
http2(this.subsonicHttp, {
|
|
||||||
params: {
|
|
||||||
u: credentials.username,
|
u: credentials.username,
|
||||||
...t_and_s(credentials.password),
|
...t_and_s(credentials.password),
|
||||||
},
|
})
|
||||||
});
|
|
||||||
|
|
||||||
GET = (query: Partial<RequestParams>) => ({
|
|
||||||
asJSON: <T>() => getJSON<T>(http2(this.subsonicHttp, query)),
|
|
||||||
});
|
|
||||||
|
|
||||||
generateToken = (credentials: Credentials) =>
|
generateToken = (credentials: Credentials) =>
|
||||||
pipe(
|
pipe(
|
||||||
TE.tryCatch(
|
TE.tryCatch(
|
||||||
() => getJSON<PingResponse>(http2(this.authenticatedSubsonicHttp(credentials), { url: "/rest/ping.view" })),
|
() => client(this.subsonic.with({ params: this.asAuthParams(credentials) } ))({ method: 'get', url: "/rest/ping.view" }).asJSON<PingResponse>(),
|
||||||
(e) => new AuthFailure(e as string)
|
(e) => new AuthFailure(e as string)
|
||||||
),
|
),
|
||||||
TE.chain(({ type }) =>
|
TE.chain(({ type }) =>
|
||||||
@@ -168,7 +161,7 @@ export class Subsonic implements MusicService {
|
|||||||
): Promise<SubsonicMusicLibrary> => {
|
): Promise<SubsonicMusicLibrary> => {
|
||||||
const subsonicGenericLibrary = new SubsonicGenericMusicLibrary(
|
const subsonicGenericLibrary = new SubsonicGenericMusicLibrary(
|
||||||
this.streamClientApplication,
|
this.streamClientApplication,
|
||||||
this.authenticatedSubsonicHttp(credentials)
|
this.subsonic.with({ params: this.asAuthParams(credentials) } )
|
||||||
);
|
);
|
||||||
if (credentials.type == "navidrome") {
|
if (credentials.type == "navidrome") {
|
||||||
return Promise.resolve(
|
return Promise.resolve(
|
||||||
|
|||||||
@@ -38,8 +38,8 @@ import {
|
|||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { asURLSearchParams } from "../utils";
|
import { asURLSearchParams } from "../utils";
|
||||||
import { artistSummaryFromNDArtist, NDArtist } from "./navidrome";
|
import { artistSummaryFromNDArtist, NDArtist } from "./navidrome";
|
||||||
import { Http, http as newHttp, RequestParams } from "../http";
|
import { Http2, RequestParams } from "../http";
|
||||||
import { getRaw2, getJSON } from "./subsonic_http";
|
import { client } from "./subsonic_http";
|
||||||
|
|
||||||
type album = {
|
type album = {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -276,20 +276,17 @@ const maybeAsGenre = (genreName: string | undefined): Genre | undefined =>
|
|||||||
|
|
||||||
export class SubsonicGenericMusicLibrary implements SubsonicMusicLibrary {
|
export class SubsonicGenericMusicLibrary implements SubsonicMusicLibrary {
|
||||||
streamClientApplication: StreamClientApplication;
|
streamClientApplication: StreamClientApplication;
|
||||||
subsonicHttp: Http;
|
subsonicHttp: Http2;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
streamClientApplication: StreamClientApplication,
|
streamClientApplication: StreamClientApplication,
|
||||||
subsonicHttp: Http
|
subsonicHttp: Http2
|
||||||
) {
|
) {
|
||||||
this.streamClientApplication = streamClientApplication;
|
this.streamClientApplication = streamClientApplication;
|
||||||
this.subsonicHttp = subsonicHttp;
|
this.subsonicHttp = subsonicHttp;
|
||||||
}
|
}
|
||||||
|
|
||||||
GET = (query: Partial<RequestParams>) => ({
|
GET = (query: Partial<RequestParams>) => client(this.subsonicHttp)({ method: 'get', ...query });
|
||||||
asRAW: () => getRaw2(newHttp(this.subsonicHttp, query)),
|
|
||||||
asJSON: <T>() => getJSON<T>(newHttp(this.subsonicHttp, query)),
|
|
||||||
});
|
|
||||||
|
|
||||||
flavour = () => "subsonic";
|
flavour = () => "subsonic";
|
||||||
|
|
||||||
@@ -390,7 +387,6 @@ export class SubsonicGenericMusicLibrary implements SubsonicMusicLibrary {
|
|||||||
trackId: string;
|
trackId: string;
|
||||||
range: string | undefined;
|
range: string | undefined;
|
||||||
}) =>
|
}) =>
|
||||||
// todo: all these headers and stuff can be rolled into httpeee
|
|
||||||
this.getTrack(trackId).then((track) =>
|
this.getTrack(trackId).then((track) =>
|
||||||
this.GET({
|
this.GET({
|
||||||
url: "/rest/stream",
|
url: "/rest/stream",
|
||||||
@@ -398,20 +394,10 @@ export class SubsonicGenericMusicLibrary implements SubsonicMusicLibrary {
|
|||||||
id: trackId,
|
id: trackId,
|
||||||
c: this.streamClientApplication(track),
|
c: this.streamClientApplication(track),
|
||||||
},
|
},
|
||||||
headers: pipe(
|
headers: range != undefined ? { Range: range } : {},
|
||||||
range,
|
|
||||||
O.fromNullable,
|
|
||||||
O.map((range) => ({
|
|
||||||
// "User-Agent": USER_AGENT,
|
|
||||||
Range: range,
|
|
||||||
})),
|
|
||||||
O.getOrElse(() => ({
|
|
||||||
// "User-Agent": USER_AGENT,
|
|
||||||
}))
|
|
||||||
),
|
|
||||||
responseType: "stream",
|
responseType: "stream",
|
||||||
})
|
})
|
||||||
.asRAW()
|
.asRaw()
|
||||||
.then((res) => ({
|
.then((res) => ({
|
||||||
status: res.status,
|
status: res.status,
|
||||||
headers: {
|
headers: {
|
||||||
@@ -717,7 +703,7 @@ export class SubsonicGenericMusicLibrary implements SubsonicMusicLibrary {
|
|||||||
url: "/rest/getCoverArt",
|
url: "/rest/getCoverArt",
|
||||||
params: { id, size },
|
params: { id, size },
|
||||||
responseType: "arraybuffer",
|
responseType: "arraybuffer",
|
||||||
}).asRAW();
|
}).asRaw();
|
||||||
|
|
||||||
private getTrack = (id: string) =>
|
private getTrack = (id: string) =>
|
||||||
this.GET({
|
this.GET({
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
import {
|
import { AxiosResponse } from "axios";
|
||||||
isError,
|
import { isError, SubsonicEnvelope } from ".";
|
||||||
SubsonicEnvelope,
|
|
||||||
} from ".";
|
|
||||||
// todo: rename http2 to http
|
// todo: rename http2 to http
|
||||||
import { Http, http as newHttp } from "../http";
|
import { Http2, RequestParams } from "../http";
|
||||||
|
|
||||||
export type HttpResponse = {
|
export type HttpResponse = {
|
||||||
data: any;
|
data: any;
|
||||||
@@ -11,21 +9,7 @@ export type HttpResponse = {
|
|||||||
headers: any;
|
headers: any;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getRaw2 = (http: Http) =>
|
const asJSON = <T>(response: HttpResponse): T => {
|
||||||
http({ method: "get" })
|
|
||||||
.catch((e) => {
|
|
||||||
throw `Subsonic failed with: ${e}`;
|
|
||||||
})
|
|
||||||
.then((response) => {
|
|
||||||
if (response.status != 200 && response.status != 206) {
|
|
||||||
throw `Subsonic failed with a ${response.status || "no!"} status`;
|
|
||||||
} else return response;
|
|
||||||
});
|
|
||||||
|
|
||||||
export const getJSON = async <T>(http: Http): Promise<T> =>
|
|
||||||
getRaw2(newHttp(http, { params: { f: "json" } })).then(asJSON) as Promise<T>;
|
|
||||||
|
|
||||||
export const asJSON = <T>(response: HttpResponse): T => {
|
|
||||||
const subsonicResponse = (response.data as SubsonicEnvelope)[
|
const subsonicResponse = (response.data as SubsonicEnvelope)[
|
||||||
"subsonic-response"
|
"subsonic-response"
|
||||||
];
|
];
|
||||||
@@ -33,6 +17,35 @@ export const asJSON = <T>(response: HttpResponse): T => {
|
|||||||
throw `Subsonic error:${subsonicResponse.error.message}`;
|
throw `Subsonic error:${subsonicResponse.error.message}`;
|
||||||
else return subsonicResponse as unknown as T;
|
else return subsonicResponse as unknown as T;
|
||||||
};
|
};
|
||||||
|
const throwUp = (error: any) => {
|
||||||
|
throw `Subsonic failed with: ${error}`;
|
||||||
|
};
|
||||||
|
const verifyResponse = (response: AxiosResponse<any>) => {
|
||||||
|
if (response.status != 200 && response.status != 206) {
|
||||||
|
throw `Subsonic failed with a ${response.status || "no!"} status`;
|
||||||
|
} else return response;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface SubsonicHttpResponse {
|
||||||
|
asRaw(): Promise<AxiosResponse<any>>;
|
||||||
|
asJSON<T>(): Promise<T>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SubsonicHttp {
|
||||||
|
(query: Partial<RequestParams>): SubsonicHttpResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const client = (http: Http2): SubsonicHttp => {
|
||||||
|
return (query: Partial<RequestParams>): SubsonicHttpResponse => {
|
||||||
|
return {
|
||||||
|
asRaw: () => http(query).catch(throwUp).then(verifyResponse),
|
||||||
|
|
||||||
|
asJSON: <T>() =>
|
||||||
|
http
|
||||||
|
.with({ params: { f: "json" } })(query)
|
||||||
|
.catch(throwUp)
|
||||||
|
.then(verifyResponse)
|
||||||
|
.then(asJSON) as Promise<T>,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|||||||
14
src/utils.ts
14
src/utils.ts
@@ -22,11 +22,21 @@ export const asURLSearchParams = (q: any) => {
|
|||||||
return urlSearchParams;
|
return urlSearchParams;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export function takeWithRepeats<T>(things: T[], count: number) {
|
export function takeWithRepeats<T>(things: T[], count: number) {
|
||||||
const result = [];
|
const result = [];
|
||||||
for (let i = 0; i < count; i++) {
|
for (let i = 0; i < count; i++) {
|
||||||
result.push(things[i % things.length])
|
result.push(things[i % things.length]);
|
||||||
}
|
}
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const mask = (thing: any, fields: string[]) =>
|
||||||
|
fields.reduce(
|
||||||
|
(res: any, key: string) => {
|
||||||
|
if (Object.keys(res).includes(key)) {
|
||||||
|
res[key] = "****";
|
||||||
|
}
|
||||||
|
return res;
|
||||||
|
},
|
||||||
|
{ ...thing }
|
||||||
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { http, } from "../src/http";
|
import { http, http2From, } from "../src/http";
|
||||||
|
|
||||||
describe("http", () => {
|
describe("http", () => {
|
||||||
const mockAxios = jest.fn();
|
const mockAxios = jest.fn();
|
||||||
@@ -11,6 +11,7 @@ describe("http", () => {
|
|||||||
describe.each([
|
describe.each([
|
||||||
["baseURL"],
|
["baseURL"],
|
||||||
["url"],
|
["url"],
|
||||||
|
["method"],
|
||||||
])('%s', (field) => {
|
])('%s', (field) => {
|
||||||
const getValue = (value: string) => {
|
const getValue = (value: string) => {
|
||||||
const thing = {} as any;
|
const thing = {} as any;
|
||||||
@@ -136,3 +137,141 @@ describe("http", () => {
|
|||||||
});
|
});
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("http2", () => {
|
||||||
|
const mockAxios = jest.fn();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
jest.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe.each([
|
||||||
|
["baseURL"],
|
||||||
|
["url"],
|
||||||
|
["method"],
|
||||||
|
])('%s', (field) => {
|
||||||
|
const fieldWithValue = (value: string) => {
|
||||||
|
const thing = {} as any;
|
||||||
|
thing[field] = value;
|
||||||
|
return thing;
|
||||||
|
};
|
||||||
|
|
||||||
|
const base = http2From(mockAxios).with(fieldWithValue('default'));
|
||||||
|
|
||||||
|
describe("using default", () => {
|
||||||
|
it("should use the default", () => {
|
||||||
|
base({})
|
||||||
|
expect(mockAxios).toHaveBeenCalledWith(fieldWithValue('default'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("overriding", () => {
|
||||||
|
it("should use the override", () => {
|
||||||
|
base(fieldWithValue('override'))
|
||||||
|
expect(mockAxios).toHaveBeenCalledWith(fieldWithValue('override'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("wrapping", () => {
|
||||||
|
const firstLayer = http2From(base).with(fieldWithValue('level1'));
|
||||||
|
const secondLayer = firstLayer.with(fieldWithValue('level2'));
|
||||||
|
|
||||||
|
describe("when the outter call provides a value", () => {
|
||||||
|
it("should apply it", () => {
|
||||||
|
secondLayer(fieldWithValue('outter'))
|
||||||
|
expect(mockAxios).toHaveBeenCalledWith(fieldWithValue('outter'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when the outter call does not provide a value", () => {
|
||||||
|
it("should use the second layer", () => {
|
||||||
|
secondLayer({ })
|
||||||
|
expect(mockAxios).toHaveBeenCalledWith(fieldWithValue('level2'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("requestType", () => {
|
||||||
|
const base = http2From(mockAxios).with({ responseType: 'stream' });
|
||||||
|
|
||||||
|
describe("using default", () => {
|
||||||
|
it("should use the default", () => {
|
||||||
|
base({})
|
||||||
|
expect(mockAxios).toHaveBeenCalledWith({ responseType: 'stream' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("overriding", () => {
|
||||||
|
it("should use the override", () => {
|
||||||
|
base({ responseType: 'arraybuffer' })
|
||||||
|
expect(mockAxios).toHaveBeenCalledWith({ responseType: 'arraybuffer' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("wrapping", () => {
|
||||||
|
const firstLayer = base.with({ responseType: 'arraybuffer' });
|
||||||
|
const secondLayer = firstLayer.with({ responseType: 'blob' });
|
||||||
|
|
||||||
|
describe("when the outter call provides a value", () => {
|
||||||
|
it("should apply it", () => {
|
||||||
|
secondLayer({ responseType: 'text' })
|
||||||
|
expect(mockAxios).toHaveBeenCalledWith({ responseType: 'text' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when the outter call does not provide a value", () => {
|
||||||
|
it("should use the second layer", () => {
|
||||||
|
secondLayer({ })
|
||||||
|
expect(mockAxios).toHaveBeenCalledWith({ responseType: 'blob' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe.each([
|
||||||
|
["params"],
|
||||||
|
["headers"],
|
||||||
|
])('%s', (field) => {
|
||||||
|
const fieldWithValues = (values: any) => {
|
||||||
|
const thing = {} as any;
|
||||||
|
thing[field] = values;
|
||||||
|
return thing;
|
||||||
|
}
|
||||||
|
const base = http2From(mockAxios).with(fieldWithValues({ a: 1, b: 2, c: 3, d: 4 }));
|
||||||
|
|
||||||
|
describe("using default", () => {
|
||||||
|
it("should use the default", () => {
|
||||||
|
base({});
|
||||||
|
expect(mockAxios).toHaveBeenCalledWith(fieldWithValues({ a: 1, b: 2, c: 3, d: 4 }));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("overriding", () => {
|
||||||
|
it("should use the override", () => {
|
||||||
|
base(fieldWithValues({ b: 22, e: 5 }));
|
||||||
|
expect(mockAxios).toHaveBeenCalledWith(fieldWithValues({ a: 1, b: 22, c: 3, d: 4, e: 5 }));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("wrapping", () => {
|
||||||
|
const firstLayer = base.with(fieldWithValues({ b: 22 }));
|
||||||
|
const secondLayer = firstLayer.with(fieldWithValues({ c: 33 }));
|
||||||
|
|
||||||
|
describe("when the outter call provides a value", () => {
|
||||||
|
it("should apply it", () => {
|
||||||
|
secondLayer(fieldWithValues({ a: 11, e: 5 }));
|
||||||
|
expect(mockAxios).toHaveBeenCalledWith(fieldWithValues({ a: 11, b: 22, c: 33, d: 4, e: 5 }));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("when the outter call does not provide a value", () => {
|
||||||
|
it("should use the second layer", () => {
|
||||||
|
secondLayer({ });
|
||||||
|
expect(mockAxios).toHaveBeenCalledWith(fieldWithValues({ a: 1, b: 22, c: 33, d: 4 }));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ import {
|
|||||||
import { EMPTY, error, FAILURE, subsonicOK, ok } from "../subsonic.test";
|
import { EMPTY, error, FAILURE, subsonicOK, ok } from "../subsonic.test";
|
||||||
import { DODGY_IMAGE_NAME, t } from "../../src/subsonic";
|
import { DODGY_IMAGE_NAME, t } from "../../src/subsonic";
|
||||||
import { b64Encode } from "../../src/b64";
|
import { b64Encode } from "../../src/b64";
|
||||||
import { http as http2 } from "../../src/http";
|
import { http2From } from "../../src/http";
|
||||||
|
|
||||||
const maybeIdFromCoverArtUrn = (coverArt: BUrn | undefined) =>
|
const maybeIdFromCoverArtUrn = (coverArt: BUrn | undefined) =>
|
||||||
pipe(
|
pipe(
|
||||||
@@ -511,7 +511,7 @@ describe("SubsonicGenericMusicLibrary", () => {
|
|||||||
const generic = new SubsonicGenericMusicLibrary(
|
const generic = new SubsonicGenericMusicLibrary(
|
||||||
streamClientApplication,
|
streamClientApplication,
|
||||||
// todo: all this stuff doesnt need to be defaulted in here.
|
// todo: all this stuff doesnt need to be defaulted in here.
|
||||||
http2(mockAxios, {
|
http2From(mockAxios).with({
|
||||||
baseURL,
|
baseURL,
|
||||||
params: authParams,
|
params: authParams,
|
||||||
headers
|
headers
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { asURLSearchParams, takeWithRepeats } from "../src/utils";
|
import { asURLSearchParams, mask, takeWithRepeats } from "../src/utils";
|
||||||
|
|
||||||
describe("asURLSearchParams", () => {
|
describe("asURLSearchParams", () => {
|
||||||
describe("empty q", () => {
|
describe("empty q", () => {
|
||||||
@@ -46,8 +46,6 @@ describe("asURLSearchParams", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
describe("takeWithRepeat", () => {
|
describe("takeWithRepeat", () => {
|
||||||
describe("when there is nothing in the input", () => {
|
describe("when there is nothing in the input", () => {
|
||||||
it("should return an array of undefineds", () => {
|
it("should return an array of undefineds", () => {
|
||||||
@@ -77,7 +75,32 @@ describe("takeWithRepeat", () => {
|
|||||||
describe("when there more than the amount required", () => {
|
describe("when there more than the amount required", () => {
|
||||||
it("should return the first n items", () => {
|
it("should return the first n items", () => {
|
||||||
expect(takeWithRepeats(["a", "b", "c"], 2)).toEqual(["a", "b"]);
|
expect(takeWithRepeats(["a", "b", "c"], 2)).toEqual(["a", "b"]);
|
||||||
expect(takeWithRepeats(["a", undefined, "c"], 2)).toEqual(["a", undefined]);
|
expect(takeWithRepeats(["a", undefined, "c"], 2)).toEqual([
|
||||||
|
"a",
|
||||||
|
undefined,
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("mask", () => {
|
||||||
|
it.each([
|
||||||
|
[{}, ["a", "b"], {}],
|
||||||
|
[{ foo: "bar" }, ["a", "b"], { foo: "bar" }],
|
||||||
|
[{ a: 1 }, ["a", "b"], { a: "****" }],
|
||||||
|
[{ a: 1, b: "dog" }, ["a", "b"], { a: "****", b: "****" }],
|
||||||
|
[
|
||||||
|
{ a: 1, b: "dog", foo: "bar" },
|
||||||
|
["a", "b"],
|
||||||
|
{ a: "****", b: "****", foo: "bar" },
|
||||||
|
],
|
||||||
|
])(
|
||||||
|
"masking of %s, keys = %s, should result in %s",
|
||||||
|
(original: any, keys: string[], expected: any) => {
|
||||||
|
const copyOfOrig = JSON.parse(JSON.stringify(original));
|
||||||
|
const masked = mask(original, keys);
|
||||||
|
expect(masked).toEqual(expected);
|
||||||
|
expect(original).toEqual(copyOfOrig);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user