import { describe, expect, mock, test } from "bun:test"; import { addSubmissionNote, calculatePrice, cancelSubmission, compactExpiredSubmissions, deleteSubmissionNote, isDuplicateSlipError, messages, type PaymentRepository, paySubmission, type RubgramActor, type RubgramConfig, registerSubmission, requireOwner, runAdminOperation, validateRegistration, } from "./service"; const types = [ { id: "abyss", price: 100 }, { id: "theater", price: 80 }, ]; const config: RubgramConfig = { locked: false, full: false, limit: 10, free: 0, allDiscount: 20, types, }; const owner: RubgramActor = { userId: "user-1", isAdmin: mock(async () => false), }; const anonymous = (): RubgramActor => ({ isAdmin: mock(async () => false), }); const admin = (): RubgramActor => ({ isAdmin: mock(async () => true), }); describe("Rubgram pricing", () => { test("sums selected service prices", () => { expect(calculatePrice(["abyss"], config)).toBe(100); }); test("applies the all-services discount", () => { expect(calculatePrice(["abyss", "theater"], config)).toBe(160); }); test("does not discount a partial or unknown selection", () => { expect(calculatePrice(["unknown"], config)).toBe(0); }); test("makes any selection free while a free slot is advertised", () => { expect(calculatePrice(["abyss", "theater"], { ...config, free: 1 })).toBe( 0, ); }); }); describe("registration validation", () => { const valid = { name: "Buzz", server: "as", service: ["abyss"], user: "user-1", }; test.each([ [{ ...valid, name: "" }, messages.incomplete], [{ ...valid, server: "" }, messages.incomplete], [{ ...valid, service: [] }, messages.incomplete], [{ ...valid, user: undefined }, messages.incomplete], [{ ...valid, server: "mars" }, messages.invalidServer], [{ ...valid, service: ["fake"] }, messages.invalidService], [{ ...valid, name: "x".repeat(33) }, messages.nameTooLong], ])("rejects invalid registration %#", (input, expected) => { expect(validateRegistration(input, types)).toBe(expected); }); test("accepts valid input at the name length boundary", () => { expect( validateRegistration({ ...valid, name: "x".repeat(32) }, types), ).toBe(undefined); }); }); describe("registration workflow", () => { const input = { name: "Buzz", server: "as", service: ["abyss"], user: "user-1", }; function dependencies( overrides: Partial[1]> = {}, ) { return { getConfig: mock(async () => config), removeExpired: mock(async () => {}), findDuplicate: mock(async () => undefined), insert: mock(async () => ({ id: "sub-1", queue: 4 })), consumeFreeSlot: mock(async () => {}), ...overrides, }; } test("rejects a forged or absent form user before mutations", async () => { const deps = dependencies(); expect(await registerSubmission({ ...input, user: undefined }, deps)).toBe( messages.incomplete, ); expect(deps.removeExpired).not.toHaveBeenCalled(); expect(deps.insert).not.toHaveBeenCalled(); }); test("stops at the registration lock", async () => { const deps = dependencies({ getConfig: mock(async () => ({ ...config, locked: true })), }); expect(await registerSubmission(input, deps)).toBe(messages.locked); expect(deps.removeExpired).not.toHaveBeenCalled(); }); test("compacts expirations before enforcing the refreshed limit", async () => { const getConfig = mock() .mockResolvedValueOnce({ ...config, full: true }) .mockResolvedValueOnce(config); const deps = dependencies({ getConfig }); expect(await registerSubmission(input, deps)).toEqual({ id: "sub-1", queue: 4, }); expect(deps.removeExpired).toHaveBeenCalledTimes(1); }); test("rejects a genuinely full refreshed queue", async () => { const deps = dependencies({ getConfig: mock(async () => ({ ...config, full: true, limit: 3 })), }); expect(await registerSubmission(input, deps)).toBe( "คิวลงทะเบียนเต็มแล้ว (3 ครั้ง)", ); expect(deps.findDuplicate).not.toHaveBeenCalled(); }); test("returns an existing unpaid queue instead of inserting", async () => { const duplicate = { id: "old", queue: 2 }; const deps = dependencies({ findDuplicate: mock(async () => duplicate), }); expect(await registerSubmission(input, deps)).toEqual(duplicate); expect(deps.insert).not.toHaveBeenCalled(); }); test("inserts the authenticated user with the calculated price", async () => { const deps = dependencies(); await registerSubmission(input, deps); expect(deps.insert).toHaveBeenCalledWith({ ...input, price: 100, }); expect(deps.consumeFreeSlot).not.toHaveBeenCalled(); }); test("consumes one slot only for a free registration", async () => { const deps = dependencies({ getConfig: mock(async () => ({ ...config, free: 1 })), }); await registerSubmission(input, deps); expect(deps.insert).toHaveBeenCalledWith( expect.objectContaining({ price: 0 }), ); expect(deps.consumeFreeSlot).toHaveBeenCalledTimes(1); }); }); describe("ownership", () => { test("allows the owner without checking admin auth", async () => { await requireOwner(owner, "user-1"); expect(owner.isAdmin).not.toHaveBeenCalled(); }); test("allows an admin to act on another user's submission", async () => { await expect(requireOwner(admin(), "user-1")).resolves.toBeUndefined(); }); test("rejects another user", async () => { await expect(requireOwner(anonymous(), "user-1")).rejects.toThrow( messages.unauthorized, ); }); }); describe("payment workflow", () => { const active = { id: "sub-1", user: "user-1", service: ["abyss"], price: 100, }; function setup( options: { active?: typeof active; expired?: { id: string; user: string; service: string[]; name: string; server: "as"; }; insertSlip?: PaymentRepository["insertSlip"]; slipResult?: Awaited< ReturnType[2]["checkSlip"]> >; } = {}, ) { const repository: PaymentRepository = { findSubmission: mock(async () => options.active), findExpired: mock(async () => options.expired), restoreExpired: mock(async () => {}), insertSlip: options.insertSlip ?? mock(async () => "slip-1"), markPaid: mock(async () => ({ id: "sub-1", queue: 8 })), }; const checkSlip = mock( async () => options.slipResult ?? { success: true as const, data: { transRef: "ref-1", amount: 100 }, }, ); const dependencies = { transaction: async (run: (repo: PaymentRepository) => Promise) => run(repository), calculatePrice: mock(async () => 160), checkSlip, isDuplicateSlipError, }; return { repository, dependencies, checkSlip }; } const input = { id: "sub-1", slip: Buffer.from(new ArrayBuffer(2)), type: "image/png", }; test("returns not registered when active and expired records are absent", async () => { const { dependencies, checkSlip } = setup(); expect(await paySubmission(input, owner, dependencies)).toBe( messages.notRegistered, ); expect(checkSlip).not.toHaveBeenCalled(); }); test("requires ownership before checking a slip", async () => { const { dependencies, checkSlip } = setup({ active }); await expect( paySubmission(input, anonymous(), dependencies), ).rejects.toThrow(messages.unauthorized); expect(checkSlip).not.toHaveBeenCalled(); }); test("checks and attaches a valid slip at the stored active price", async () => { const { repository, dependencies, checkSlip } = setup({ active }); expect(await paySubmission(input, owner, dependencies)).toEqual({ id: "sub-1", queue: 8, }); expect(checkSlip).toHaveBeenCalledWith(input.slip, "image/png", 100); expect(repository.markPaid).toHaveBeenCalledWith("sub-1", "slip-1"); }); test("recalculates an expired price and uses it for slip validation", async () => { const expired = { id: "sub-1", user: "user-1", service: ["abyss", "theater"], name: "Buzz", server: "as" as const, }; const { repository, dependencies, checkSlip } = setup({ expired }); await paySubmission(input, owner, dependencies); expect(dependencies.calculatePrice).toHaveBeenCalledWith(expired.service); expect(repository.restoreExpired).toHaveBeenCalledWith(expired, 160); expect(checkSlip).toHaveBeenCalledWith(input.slip, "image/png", 160); }); test("returns processor failures without inserting a slip", async () => { const { repository, dependencies } = setup({ active, slipResult: { success: false, code: 422, message: "bad slip" }, }); expect(await paySubmission(input, owner, dependencies)).toBe( "422: bad slip", ); expect(repository.insertSlip).not.toHaveBeenCalled(); }); test("maps only a duplicate reference violation to duplicate slip", async () => { const error = { code: "23505", constraint: "slips_ref_unique" }; const { dependencies } = setup({ active, insertSlip: mock(async () => { throw error; }), }); expect(await paySubmission(input, owner, dependencies)).toBe( messages.duplicateSlip, ); }); test("propagates unrelated database failures", async () => { const error = Object.assign(new Error("connection lost"), { code: "08006", }); const { dependencies } = setup({ active, insertSlip: mock(async () => { throw error; }), }); await expect(paySubmission(input, owner, dependencies)).rejects.toBe(error); }); }); describe("expiration compaction", () => { test("archives, removes, and compacts expired queues in descending order", async () => { const calls: Array = []; const expired = [ { id: "one", queue: 2 }, { id: "two", queue: 5 }, ]; const result = await compactExpiredSubmissions( { findExpired: mock(async () => expired), archive: mock(async (rows) => { calls.push(`archive:${rows.length}`); }), remove: mock(async (ids) => { calls.push(`remove:${ids.join(",")}`); }), shiftAfter: mock(async (queue) => { calls.push(queue); }), getMaxQueue: mock(async () => 7), setNextQueue: mock(async (next) => { calls.push(`next:${next}`); }), }, new Date("2026-01-01"), ); expect(result).toEqual({ removed: 2 }); expect(calls).toEqual(["archive:2", "remove:one,two", 5, 2, "next:8"]); }); test("skips archive/delete/shift and resets an empty sequence to one", async () => { const archive = mock(async () => {}); const remove = mock(async () => {}); const shiftAfter = mock(async () => {}); const setNextQueue = mock(async () => {}); expect( await compactExpiredSubmissions({ findExpired: mock(async () => []), archive, remove, shiftAfter, getMaxQueue: mock(async () => null), setNextQueue, }), ).toEqual({ removed: 0 }); expect(archive).not.toHaveBeenCalled(); expect(remove).not.toHaveBeenCalled(); expect(shiftAfter).not.toHaveBeenCalled(); expect(setNextQueue).toHaveBeenCalledWith(1); }); }); describe("cancellation", () => { test("allows the owner to soft-delete", async () => { const cancel = mock(async () => {}); expect( await cancelSubmission("sub-1", owner, { findSubmission: mock(async () => ({ user: "user-1" })), cancel, }), ).toBeTrue(); expect(cancel).toHaveBeenCalledWith("sub-1"); }); test("allows admin cancellation", async () => { const cancel = mock(async () => {}); await cancelSubmission("sub-1", admin(), { findSubmission: mock(async () => ({ user: "someone" })), cancel, }); expect(cancel).toHaveBeenCalled(); }); test("does not mutate missing or unauthorized submissions", async () => { const cancel = mock(async () => {}); expect( await cancelSubmission("missing", anonymous(), { findSubmission: mock(async () => undefined), cancel, }), ).toBeFalse(); await expect( cancelSubmission("sub-1", anonymous(), { findSubmission: mock(async () => ({ user: "someone" })), cancel, }), ).rejects.toThrow(messages.unauthorized); expect(cancel).not.toHaveBeenCalled(); }); }); describe("notes and admin operations", () => { const notes = [ { id: "old", text: "old note", createdAt: "2025-01-01T00:00:00.000Z" }, ]; test("admin note creation appends deterministic metadata", async () => { const saveNotes = mock(async () => {}); const note = await addSubmissionNote("sub-1", "new note", admin(), { getNotes: mock(async () => notes), saveNotes, createId: () => "new", now: () => new Date("2026-02-03T04:05:06.000Z"), }); expect(note).toEqual({ id: "new", text: "new note", createdAt: "2026-02-03T04:05:06.000Z", }); expect(saveNotes).toHaveBeenCalledWith("sub-1", [...notes, note]); }); test("admin note deletion preserves all other notes", async () => { const saveNotes = mock(async () => {}); await deleteSubmissionNote("sub-1", "old", admin(), { getNotes: mock(async () => [ ...notes, { id: "keep", text: "keep", createdAt: "now" }, ]), saveNotes, }); expect(saveNotes).toHaveBeenCalledWith("sub-1", [ { id: "keep", text: "keep", createdAt: "now" }, ]); }); test("non-admins cannot change notes", async () => { const saveNotes = mock(async () => {}); await expect( addSubmissionNote("sub-1", "no", anonymous(), { getNotes: mock(async () => notes), saveNotes, createId: () => "new", now: () => new Date(), }), ).rejects.toThrow(messages.unauthorized); expect(saveNotes).not.toHaveBeenCalled(); }); test("admin operation runs once and returns its result", async () => { const operation = mock(async () => "updated"); expect(await runAdminOperation(admin(), operation)).toBe("updated"); expect(operation).toHaveBeenCalledTimes(1); }); test("unauthorized admin operation never invokes its dependency", async () => { const operation = mock(async () => "updated"); await expect(runAdminOperation(anonymous(), operation)).rejects.toThrow( messages.unauthorized, ); expect(operation).not.toHaveBeenCalled(); }); }); describe("duplicate slip classification", () => { test("recognizes direct and wrapped slip-reference unique violations", () => { const duplicate = { code: "23505", constraint: "slips_ref_unique" }; expect(isDuplicateSlipError(duplicate)).toBeTrue(); expect(isDuplicateSlipError({ cause: duplicate })).toBeTrue(); }); test("rejects other constraints and database errors", () => { expect( isDuplicateSlipError({ code: "23505", constraint: "users_email_unique" }), ).toBeFalse(); expect(isDuplicateSlipError({ code: "23505" })).toBeFalse(); expect(isDuplicateSlipError({ code: "08006" })).toBeFalse(); expect(isDuplicateSlipError(new Error("boom"))).toBeFalse(); }); });