Files
com.gregkilmer.www/services/admin/test/suggestions.test.ts
T
Claude e3b0cf60c5 Phase 6: sync jobs, inquiry and order forms
- Sync scheduler in the content service with a per-job cache (sync_cache),
  sync_runs reporting and Healthchecks pings. Jobs: Archidekt, BoardGameGeek,
  cover art (Open Library, TMDB, AniList), Suwayomi and Kavita reading
  progress, Bambuddy filament and Homebox material stock. A note's own value
  always wins over a synced one.
- PersonalReader: updates only Library notes with a matching anilistId,
  changes to Greg's notes are held for review; Reading series with no note
  show as suggestions on the admin Overview.
- /api/inquiries and /api/orders: Turnstile, rate limits, a saved copy in the
  content service (intake token) plus email over SMTP. Orders are re-checked
  against the live notes: accepted only when the category's ordersOpen is
  true and the line has capacity.
- Admin Inbox for saved submissions. Options' inStock is optional so synced
  stock can fill it. All settings from process.env; .env.example per service.
- Sources could not be reached from the build environment; parsers are tested
  against fixtures labelled as not live.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aSbo2D1qP8cMFQCykf7kx
2026-10-11 23:50:06 +00:00

88 lines
3.5 KiB
TypeScript

/**
* Library suggestions on the Overview: one button adds the Library item
* through the normal write API, Dismiss hides it. The suggestion list is
* seeded the way the Suwayomi job leaves it in the sync cache.
*/
import type { AddressInfo } from "node:net";
import type { Server } from "node:http";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { parseTokens } from "../../content/src/config.ts";
import { createHttpServer } from "../../content/src/http.ts";
import { SUGGESTIONS_KEY } from "../../content/src/sync/jobs/reader.ts";
import { rig, type Rig } from "../../content/test/helpers.ts";
import { ContentApi } from "../src/client.ts";
import { createAdminServer } from "../src/server.ts";
const ADMIN = "s".repeat(24);
let r: Rig;
let content: Server;
let admin: Server;
let base: string;
const listen = async (s: Server) => {
await new Promise<void>((ok) => s.listen(0, "127.0.0.1", ok));
return `http://127.0.0.1:${(s.address() as AddressInfo).port}`;
};
const page = (path: string) => fetch(`${base}${path}`).then((res) => res.text());
const flash = (res: Response) => new URL(res.headers.get("location") ?? "/", "http://admin").searchParams;
const post = (path: string, form: Record<string, string>) =>
fetch(`${base}${path}`, {
method: "POST",
redirect: "manual",
headers: { "content-type": "application/x-www-form-urlencoded", "sec-fetch-site": "same-origin" },
body: new URLSearchParams(form).toString(),
});
beforeAll(async () => {
r = rig();
r.service.scan();
r.store.syncCache.set(
"suwayomi",
SUGGESTIONS_KEY,
{
items: [
{ anilistId: 100004, title: "New Sample Series", medium: "manhwa" },
{ anilistId: 100006, title: "Another <Sample>", medium: "manga" },
],
},
"2026-10-11T12:00:00Z",
);
content = createHttpServer(r.service, { tokens: parseTokens(`admin:${ADMIN}`) });
admin = createAdminServer(new ContentApi(await listen(content), ADMIN));
base = await listen(admin);
});
afterAll(() => {
admin.close();
content.close();
r.done();
});
describe("Library suggestions", () => {
it("show on the Overview, escaped", async () => {
const html = await page("/");
expect(html).toContain("Library suggestions");
expect(html).toContain("New Sample Series");
expect(html).toContain("Another &lt;Sample&gt;");
expect(html).toContain('value="anilist-100004"');
});
it("add creates the Library item through the write API, and the suggestion leaves", async () => {
const res = await post("/suggestions/add", { key: "anilist-100004" });
expect(res.status).toBe(303);
expect(flash(res).get("msg")).toBe("Added New Sample Series to the Library as Now.");
const note = r.store.list({ type: "library-item" }).find((n) => n.data.anilistId === 100004)!;
expect(note.data).toMatchObject({ title: "New Sample Series", medium: "manhwa", state: "now", publish: true, updatedBy: "admin" });
expect(r.vault.exists(note.path)).toBe(true);
expect(await page("/")).not.toContain("New Sample Series");
});
it("dismiss hides it for good; the panel goes when nothing is left", async () => {
expect((await post("/suggestions/dismiss", { key: "anilist-100006" })).status).toBe(303);
const html = await page("/");
expect(html).not.toContain("Library suggestions");
// A gone suggestion cannot be added.
const again = await post("/suggestions/add", { key: "anilist-100006" });
expect(flash(again).get("err")).toContain("That suggestion is gone");
});
});