93 lines
4.7 KiB
TypeScript
93 lines
4.7 KiB
TypeScript
import { afterEach, beforeEach, expect, test } from "bun:test";
|
|
import { collect, createFixtureHttp, createTestContext } from "@sttracker/plugin-sdk/testing";
|
|
import plugin from "../index.ts";
|
|
import { setSleep } from "../src/options.ts";
|
|
|
|
const sleeps: number[] = [];
|
|
beforeEach(() => { sleeps.length = 0; setSleep(async (ms) => { sleeps.push(ms); }); });
|
|
afterEach(() => setSleep(null));
|
|
|
|
const URL0 = "GET https://ws.audioscrobbler.com/2.0/";
|
|
const rt = (name: string, uts: string, mbid = "", extra = {}) => ({
|
|
name, mbid, artist: { "#text": "Art", mbid: "" }, album: { "#text": "Alb", mbid: "" }, date: { uts }, ...extra,
|
|
});
|
|
|
|
function routes(handler: (q: URLSearchParams) => unknown) {
|
|
return { [URL0]: (req: Request) => handler(new URL(req.url).searchParams) };
|
|
}
|
|
|
|
test("skips nowplaying, omits empty mbids, includes loved entries", async () => {
|
|
const f = createFixtureHttp(routes((q) => {
|
|
expect(q.get("api_key")).toBe("key");
|
|
expect(q.get("format")).toBe("json");
|
|
if (q.get("method") === "user.getLovedTracks")
|
|
return { lovedtracks: { track: [{ name: "Fav", mbid: "m9", artist: { name: "Art" } }], "@attr": { totalPages: "1" } } };
|
|
return { recenttracks: { track: [
|
|
{ name: "Now", artist: { "#text": "Art" }, date: { uts: "1700000005" }, "@attr": { nowplaying: "true" } },
|
|
rt("One", "1700000000", "rec1", { artist: { "#text": "Art", mbid: "a1" } }),
|
|
rt("Two", "1699999999"),
|
|
], "@attr": { totalPages: "1" } } };
|
|
}));
|
|
const ctx = createTestContext({ fetch: f.fetch, config: { username: "u", apiKey: "key" } });
|
|
const recs = await collect(plugin.pull!(ctx));
|
|
expect(recs.map((r) => r.key)).toEqual(["1700000000:Art:One", "1699999999:Art:Two", "loved:Art:Fav"]);
|
|
const one = recs[0]!, two = recs[1]!, fav = recs[2]!;
|
|
expect(one.kind === "listen" && one.listen.mbids).toEqual({ recording: "rec1", artist: "a1" });
|
|
expect(two.kind === "listen" && two.listen.mbids).toBeUndefined();
|
|
expect(fav.kind === "entry" && fav.entry.favorite).toBe(true);
|
|
expect(fav.kind === "entry" && fav.work.externalIds).toEqual({ musicbrainz_recording: "m9" });
|
|
expect(ctx.saved.cursor).toEqual(["1700000000"]);
|
|
});
|
|
|
|
test("paginates recent tracks", async () => {
|
|
const pagesSeen: (string | null)[] = [];
|
|
const f = createFixtureHttp(routes((q) => {
|
|
if (q.get("method") === "user.getLovedTracks") return { lovedtracks: { track: [], "@attr": { totalPages: "0" } } };
|
|
pagesSeen.push(q.get("page"));
|
|
return { recenttracks: { track: [rt(`T${q.get("page")}`, `10${q.get("page")}`)], "@attr": { totalPages: "2" } } };
|
|
}));
|
|
const recs = await collect(plugin.pull!(createTestContext({ fetch: f.fetch, config: { username: "u", apiKey: "k" } })));
|
|
expect(pagesSeen).toEqual(["1", "2"]);
|
|
expect(recs).toHaveLength(2);
|
|
});
|
|
|
|
test("error 29 is retried with exponential sleeps (body on HTTP 200 and 403)", async () => {
|
|
let n = 0;
|
|
const f = createFixtureHttp(routes((q) => {
|
|
if (q.get("method") === "user.getLovedTracks") return { lovedtracks: { track: [] } };
|
|
n++;
|
|
if (n === 1) return { error: 29, message: "Rate Limit Exceeded" };
|
|
if (n === 2) return new Response(JSON.stringify({ error: 29, message: "Rate Limit Exceeded" }), { status: 403 });
|
|
return { recenttracks: { track: rt("X", "5") } };
|
|
}));
|
|
const recs = await collect(plugin.pull!(createTestContext({ fetch: f.fetch, config: { username: "u", apiKey: "k" } })));
|
|
expect(n).toBe(3);
|
|
expect(sleeps).toEqual([2000, 4000]);
|
|
expect(recs).toHaveLength(1);
|
|
});
|
|
|
|
test("error 29 gives up after 3 attempts; other codes throw immediately", async () => {
|
|
const f29 = createFixtureHttp(routes(() => ({ error: 29, message: "Rate Limit Exceeded" })));
|
|
const p = collect(plugin.pull!(createTestContext({ fetch: f29.fetch, config: { username: "u", apiKey: "k" } })));
|
|
await expect(p).rejects.toMatchObject({ code: 29 });
|
|
expect(f29.calls).toHaveLength(3);
|
|
|
|
const f6 = createFixtureHttp(routes(() => new Response(JSON.stringify({ error: 6, message: "User not found" }), { status: 404 })));
|
|
const p6 = collect(plugin.pull!(createTestContext({ fetch: f6.fetch, config: { username: "u", apiKey: "k" } })));
|
|
await expect(p6).rejects.toThrow(/Last\.fm error 6: User not found/);
|
|
expect(f6.calls).toHaveLength(1);
|
|
});
|
|
|
|
test("pullChanges sends from=<cursor> and advances it", async () => {
|
|
let from: string | null = null;
|
|
const f = createFixtureHttp(routes((q) => {
|
|
from = q.get("from");
|
|
return { recenttracks: { track: [rt("N", "2000")], "@attr": { totalPages: "1" } } };
|
|
}));
|
|
const ctx = createTestContext({ fetch: f.fetch, config: { username: "u", apiKey: "k" } });
|
|
await ctx.cursor.set("1500");
|
|
await collect(plugin.pullChanges!(ctx));
|
|
expect(from as string | null).toBe("1500");
|
|
expect(await ctx.cursor.get()).toBe("2000");
|
|
});
|