Files
STTracker/packages/core/test/lists.test.ts
T
Lucas WintherandClaude Opus 5.5 c7471f7761 feat(core): engagements, lists and copies with history restore
Engagements store partial dates as value + precision and track field_sources;
lists (list/grouping/thread, grouping-only rating/review) keep dense item
order and snapshot items in history; copies store price in cents. All three
restore through restoreVersion.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 19:35:15 +01:00

105 lines
5.2 KiB
TypeScript

import { expect, test } from "bun:test";
import { forEachDialect } from "@sttracker/db/testing";
import { NotFoundError, ValidationError } from "../src/errors.ts";
import { getHistory, restoreVersion } from "../src/history.ts";
import {
addListItem,
createList,
getList,
listLists,
moveListItem,
removeListItem,
softDeleteList,
updateList,
} from "../src/lists.ts";
import { makeUser, makeWork } from "./user-layer-fixtures.ts";
forEachDialect("lists", (getDb) => {
test("grouping list can be rated", async () => {
const db = getDb();
const userId = await makeUser(db);
const g = await createList(db, userId, { name: "Silent Hill, everything", kind: "grouping", rating: 90, review: "Dread." });
expect(g.kind).toBe("grouping");
expect(g.rating).toBe(90);
expect(g.review).toBe("Dread.");
expect(g.visibility).toBe("private");
const up = await updateList(db, userId, g.id, { rating: 95 });
expect(up.rating).toBe(95);
await expect(createList(db, userId, { name: "Plain", kind: "list", rating: 50 })).rejects.toThrow(ValidationError);
await expect(createList(db, userId, { name: "Bad", kind: "grouping", rating: 101 })).rejects.toThrow(
ValidationError,
);
await expect(updateList(db, userId, g.id, { kind: "thread" })).rejects.toThrow(ValidationError);
await expect(createList(db, userId, { name: " " })).rejects.toThrow(ValidationError);
await expect(createList(db, userId, { name: "x", kind: "box" as never })).rejects.toThrow(ValidationError);
});
test("items keep order through add, move and remove", async () => {
const db = getDb();
const userId = await makeUser(db);
const l = await createList(db, userId, { name: "Cosmic dread", kind: "thread" });
const [a, b, c] = [await makeWork(db, { title: "A" }), await makeWork(db, { title: "B" }), await makeWork(db, { title: "C" })];
const ia = await addListItem(db, userId, l.id, { workId: a, note: "start here" });
const ib = await addListItem(db, userId, l.id, { workId: b });
const ic = await addListItem(db, userId, l.id, { workId: c });
const titles = async () => (await getList(db, userId, l.id)).items.map((i) => i.work.title);
expect(await titles()).toEqual(["A", "B", "C"]);
expect((await getList(db, userId, l.id)).items[0]!.note).toBe("start here");
await moveListItem(db, userId, ic.id, 0);
expect(await titles()).toEqual(["C", "A", "B"]);
await moveListItem(db, userId, ic.id, 99);
expect(await titles()).toEqual(["A", "B", "C"]);
await removeListItem(db, userId, ib.id);
expect(await titles()).toEqual(["A", "C"]);
expect((await getList(db, userId, l.id)).items.map((i) => i.position)).toEqual([0, 1]);
const inserted = await addListItem(db, userId, l.id, { workId: b, position: 1 });
expect(await titles()).toEqual(["A", "B", "C"]);
expect(inserted.position).toBe(1);
expect(ia.position).toBe(0);
});
test("removing an item is recorded and restorable", async () => {
const db = getDb();
const userId = await makeUser(db);
const l = await createList(db, userId, { name: "Reading order" });
await addListItem(db, userId, l.id, { workId: await makeWork(db, { title: "A" }) });
const ib = await addListItem(db, userId, l.id, { workId: await makeWork(db, { title: "B" }) });
await removeListItem(db, userId, ib.id);
const [h] = await getHistory(db, userId, "list", l.id);
await restoreVersion(db, userId, h!.id);
expect((await getList(db, userId, l.id)).items.map((i) => i.work.title)).toEqual(["A", "B"]);
});
test("listLists and soft delete", async () => {
const db = getDb();
const userId = await makeUser(db);
const l1 = await createList(db, userId, { name: "One" });
const l2 = await createList(db, userId, { name: "Two", kind: "grouping" });
expect((await listLists(db, userId)).map((l) => l.id).sort()).toEqual([l1.id, l2.id].sort());
expect((await listLists(db, userId, { kind: "grouping" })).map((l) => l.id)).toEqual([l2.id]);
await softDeleteList(db, userId, l1.id);
expect((await listLists(db, userId)).map((l) => l.id)).toEqual([l2.id]);
await expect(getList(db, userId, l1.id)).rejects.toThrow(NotFoundError);
});
test("another user's lists and private works are not found", async () => {
const db = getDb();
const ada = await makeUser(db);
const bob = await makeUser(db, "bob");
const l = await createList(db, ada, { name: "Mine" });
const item = await addListItem(db, ada, l.id, { workId: await makeWork(db) });
await expect(getList(db, bob, l.id)).rejects.toThrow(NotFoundError);
await expect(updateList(db, bob, l.id, { name: "x" })).rejects.toThrow(NotFoundError);
await expect(addListItem(db, bob, l.id, { workId: await makeWork(db) })).rejects.toThrow(NotFoundError);
await expect(moveListItem(db, bob, item.id, 0)).rejects.toThrow(NotFoundError);
await expect(removeListItem(db, bob, item.id)).rejects.toThrow(NotFoundError);
expect(await listLists(db, bob)).toEqual([]);
const adaPrivate = await makeWork(db, { ownerUserId: ada });
const bobList = await createList(db, bob, { name: "Bob's" });
await expect(addListItem(db, bob, bobList.id, { workId: adaPrivate })).rejects.toThrow(NotFoundError);
});
});