125 lines
4.0 KiB
TypeScript
125 lines
4.0 KiB
TypeScript
import { describe, expect, spyOn, test } from "bun:test";
|
|
import { PgDialect } from "drizzle-orm/pg-core";
|
|
import type { tierlistStates } from "./db/schema";
|
|
import {
|
|
finishTierlistWrite,
|
|
lockTierlistList,
|
|
normalizeTierlistPlacements,
|
|
resolveTierlistStateTarget,
|
|
validateTierlistStateInput,
|
|
} from "./tierlist-write";
|
|
|
|
type StateRow = typeof tierlistStates.$inferSelect;
|
|
|
|
const stateRow = (uuid: string, list: string, ref = "char-1") =>
|
|
({ uuid, list, ref }) as StateRow;
|
|
|
|
describe("tierlist write consistency helpers", () => {
|
|
test("validates list, ref, and optional UUID before writing", () => {
|
|
expect(
|
|
validateTierlistStateInput({
|
|
list: "v1",
|
|
ref: "char-1",
|
|
uuid: undefined,
|
|
}),
|
|
).toEqual({ list: "v1", ref: "char-1" });
|
|
expect(() =>
|
|
validateTierlistStateInput({ list: "", ref: "char-1" }),
|
|
).toThrow();
|
|
expect(() => validateTierlistStateInput({ list: "v1", ref: "" })).toThrow();
|
|
expect(() =>
|
|
validateTierlistStateInput({ list: "v1", ref: "char-1", uuid: "bad" }),
|
|
).toThrow();
|
|
});
|
|
|
|
test("rejects cross-list UUIDs and canonicalizes duplicate refs", () => {
|
|
const uuidMatch = stateRow("uuid-1", "v1");
|
|
const canonicalRefMatch = stateRow("uuid-0", "v1");
|
|
expect(() =>
|
|
resolveTierlistStateTarget(stateRow("uuid-2", "v2"), uuidMatch, "v1"),
|
|
).toThrow("belongs to another version");
|
|
expect(resolveTierlistStateTarget(uuidMatch, undefined, "v1")).toBe(
|
|
uuidMatch,
|
|
);
|
|
expect(resolveTierlistStateTarget(uuidMatch, canonicalRefMatch, "v1")).toBe(
|
|
canonicalRefMatch,
|
|
);
|
|
expect(resolveTierlistStateTarget(undefined, uuidMatch, "v1")).toBe(
|
|
uuidMatch,
|
|
);
|
|
});
|
|
|
|
test("uses one transaction-scoped advisory lock per list before mutations", async () => {
|
|
const order: string[] = [];
|
|
const queries: unknown[] = [];
|
|
await lockTierlistList(
|
|
{
|
|
execute: async (query) => {
|
|
order.push("lock");
|
|
queries.push(query);
|
|
},
|
|
},
|
|
"v1",
|
|
);
|
|
order.push("write");
|
|
const compiled = new PgDialect().sqlToQuery(queries[0] as never);
|
|
expect(compiled.sql).toContain("pg_advisory_xact_lock");
|
|
expect(compiled.sql).toContain("hashtextextended");
|
|
expect(compiled.params).toContain("v1");
|
|
expect(order).toEqual(["lock", "write"]);
|
|
});
|
|
|
|
test("normalizes placements without mutating the caller's untiered data", () => {
|
|
const placements = { S: ["a"], untiered: ["b"] };
|
|
expect(normalizeTierlistPlacements(placements)).toEqual({ S: ["a"] });
|
|
expect(placements).toEqual({ S: ["a"], untiered: ["b"] });
|
|
});
|
|
|
|
test("returns persisted success when publication fails after commit", async () => {
|
|
const errorLog = spyOn(console, "error").mockImplementation(() => {});
|
|
try {
|
|
const result = await finishTierlistWrite({
|
|
list: "v1",
|
|
operation: "state",
|
|
data: [{ uuid: "state-1" }],
|
|
audit: async () => {},
|
|
revalidate: () => {},
|
|
publish: async () => {
|
|
throw new Error("redis unavailable");
|
|
},
|
|
});
|
|
expect(result).toEqual({
|
|
persisted: true,
|
|
data: [{ uuid: "state-1" }],
|
|
publication: "failed",
|
|
});
|
|
expect(errorLog).toHaveBeenCalledTimes(1);
|
|
} finally {
|
|
errorLog.mockRestore();
|
|
}
|
|
});
|
|
|
|
test("reports sent when post-commit publication succeeds", async () => {
|
|
const result = await finishTierlistWrite({
|
|
list: "v1",
|
|
operation: "placements",
|
|
data: { S: ["char-1"] },
|
|
audit: async () => {},
|
|
revalidate: () => {},
|
|
publish: async () => 1,
|
|
});
|
|
expect(result).toEqual({
|
|
persisted: true,
|
|
data: { S: ["char-1"] },
|
|
publication: "sent",
|
|
});
|
|
});
|
|
|
|
test("does not turn a precommit transaction failure into persisted success", async () => {
|
|
const failure = new Error("transaction failed");
|
|
await expect(
|
|
lockTierlistList({ execute: async () => Promise.reject(failure) }, "v1"),
|
|
).rejects.toBe(failure);
|
|
});
|
|
});
|