import { describe, it, expect, vi, beforeEach } from "vitest"; vi.mock("~/models/user", () => ({ findUserById: vi.fn(), getUserDisplayName: vi.fn(), })); vi.mock("~/database/context", () => ({ database: vi.fn(), })); import { createAuditLogEntry, getAuditLogForSeason, logCommissionerAction, } from "../audit-log"; import { findUserById, getUserDisplayName } from "~/models/user"; import { database } from "~/database/context"; const SEASON_ID = "season-1"; const LEAGUE_ID = "league-1"; const ACTOR_USER_ID = "user-uuid-1"; const SAMPLE_ENTRY = { id: "entry-1", seasonId: SEASON_ID, leagueId: LEAGUE_ID, actorClerkId: ACTOR_USER_ID, actorDisplayName: "Alice", action: "draft_rollback" as const, affectedTeamIds: ["team-1"], details: { rolledBackToPickNumber: 5, previousPickNumber: 10 }, createdAt: new Date("2025-01-01T12:00:00Z"), }; /** * Builds a mock db for insert operations. */ function makeInsertDb(returnValue: object) { return { insert: vi.fn().mockReturnValue({ values: vi.fn().mockReturnValue({ returning: vi.fn().mockResolvedValue([returnValue]), }), }), }; } /** * Builds a mock db for getAuditLogForSeason which fires two select queries in * parallel: one for the data rows and one for the count. We use two * mockResolvedValueOnce calls so the first promise.all resolves the data and * the second resolves the count. * * Both calls share the same chain: select().from().where() — after that they * diverge (data adds .orderBy().limit().offset(), count stops). We build a * single chainable mock where the terminal call is a jest.fn that returns * different values on each invocation. */ function makeSelectDb( dataRows: object[], countRow: { count: number } ): object { // Count chain: select().from().where() → resolves to [countRow] const countChain = { from: vi.fn().mockReturnThis(), where: vi.fn().mockResolvedValue([countRow]), }; // Data chain: select().from().where().orderBy().limit().offset() → resolves to dataRows const dataChain = { from: vi.fn().mockReturnThis(), where: vi.fn().mockReturnThis(), orderBy: vi.fn().mockReturnThis(), limit: vi.fn().mockReturnThis(), offset: vi.fn().mockResolvedValue(dataRows), }; let selectCallCount = 0; return { select: vi.fn().mockImplementation(() => { // First call is the data query; second call is the count query selectCallCount++; return selectCallCount === 1 ? dataChain : countChain; }), }; } beforeEach(() => { vi.clearAllMocks(); }); // ─── createAuditLogEntry ────────────────────────────────────────────────────── describe("createAuditLogEntry", () => { it("inserts the entry and returns the persisted record", async () => { vi.mocked(database).mockReturnValue(makeInsertDb(SAMPLE_ENTRY) as never); const result = await createAuditLogEntry({ seasonId: SEASON_ID, leagueId: LEAGUE_ID, actorClerkId: ACTOR_USER_ID, actorDisplayName: "Alice", action: "draft_rollback", affectedTeamIds: ["team-1"], details: { rolledBackToPickNumber: 5, previousPickNumber: 10 }, }); expect(result).toEqual(SAMPLE_ENTRY); }); }); // ─── getAuditLogForSeason ───────────────────────────────────────────────────── describe("getAuditLogForSeason", () => { it("returns entries and computes hasMore=false when on the last page", async () => { vi.mocked(database).mockReturnValue( makeSelectDb([SAMPLE_ENTRY], { count: 1 }) as never ); const result = await getAuditLogForSeason(SEASON_ID, { limit: 50, offset: 0, }); expect(result.entries).toHaveLength(1); expect(result.total).toBe(1); expect(result.hasMore).toBe(false); }); it("returns hasMore=true when more entries remain", async () => { // 51 total, fetching first 50 starting at offset 0 → 50 returned → hasMore const manyEntries = Array.from({ length: 50 }, (_, i) => ({ ...SAMPLE_ENTRY, id: `entry-${i}`, })); vi.mocked(database).mockReturnValue( makeSelectDb(manyEntries, { count: 51 }) as never ); const result = await getAuditLogForSeason(SEASON_ID, { limit: 50, offset: 0, }); expect(result.total).toBe(51); expect(result.hasMore).toBe(true); }); it("returns hasMore=false on the final page", async () => { // 51 total, on page 2 (offset=50), 1 entry returned vi.mocked(database).mockReturnValue( makeSelectDb([SAMPLE_ENTRY], { count: 51 }) as never ); const result = await getAuditLogForSeason(SEASON_ID, { limit: 50, offset: 50, }); expect(result.hasMore).toBe(false); }); it("uses default limit of 50 and offset of 0 when options are omitted", async () => { const db = makeSelectDb([SAMPLE_ENTRY], { count: 1 }); vi.mocked(database).mockReturnValue(db as never); await getAuditLogForSeason(SEASON_ID); // Verify the limit and offset were applied via the data chain const dataChain = (db as { select: ReturnType }).select.mock.results[0].value; expect(dataChain.limit).toHaveBeenCalledWith(50); expect(dataChain.offset).toHaveBeenCalledWith(0); }); it("returns an empty page when no entries exist", async () => { vi.mocked(database).mockReturnValue( makeSelectDb([], { count: 0 }) as never ); const result = await getAuditLogForSeason(SEASON_ID); expect(result.entries).toHaveLength(0); expect(result.total).toBe(0); expect(result.hasMore).toBe(false); }); }); // ─── logCommissionerAction ──────────────────────────────────────────────────── describe("logCommissionerAction", () => { it("resolves the actor display name from the users table", async () => { const mockUser = { username: "alice", displayName: "Alice Smith" }; vi.mocked(findUserById).mockResolvedValue(mockUser as never); vi.mocked(getUserDisplayName).mockReturnValue("alice"); const insertDb = makeInsertDb(SAMPLE_ENTRY); vi.mocked(database).mockReturnValue(insertDb as never); await logCommissionerAction({ seasonId: SEASON_ID, leagueId: LEAGUE_ID, actorUserId: ACTOR_USER_ID, action: "draft_rollback", }); const insertValues = (insertDb.insert as ReturnType).mock.results[0].value.values.mock.calls[0][0]; expect(insertValues.actorDisplayName).toBe("alice"); }); it("falls back to actorClerkId if the user is not found", async () => { vi.mocked(findUserById).mockResolvedValue(undefined); vi.mocked(getUserDisplayName).mockReturnValue(null); const insertDb = makeInsertDb(SAMPLE_ENTRY); vi.mocked(database).mockReturnValue(insertDb as never); await logCommissionerAction({ seasonId: SEASON_ID, leagueId: LEAGUE_ID, actorUserId: ACTOR_USER_ID, action: "draft_rollback", }); const insertValues = (insertDb.insert as ReturnType).mock.results[0].value.values.mock.calls[0][0]; expect(insertValues.actorDisplayName).toBe(ACTOR_USER_ID); }); it("defaults affectedTeamIds to [] when not provided", async () => { vi.mocked(findUserById).mockResolvedValue({ username: "alice", displayName: "Alice" } as never); vi.mocked(getUserDisplayName).mockReturnValue("alice"); const insertDb = makeInsertDb(SAMPLE_ENTRY); vi.mocked(database).mockReturnValue(insertDb as never); await logCommissionerAction({ seasonId: SEASON_ID, leagueId: LEAGUE_ID, actorUserId: ACTOR_USER_ID, action: "draft_started", }); const insertValues = (insertDb.insert as ReturnType).mock.results[0].value.values.mock.calls[0][0]; expect(insertValues.affectedTeamIds).toEqual([]); }); it("defaults details to {} when not provided", async () => { vi.mocked(findUserById).mockResolvedValue({ username: "alice", displayName: "Alice" } as never); vi.mocked(getUserDisplayName).mockReturnValue("alice"); const insertDb = makeInsertDb(SAMPLE_ENTRY); vi.mocked(database).mockReturnValue(insertDb as never); await logCommissionerAction({ seasonId: SEASON_ID, leagueId: LEAGUE_ID, actorUserId: ACTOR_USER_ID, action: "draft_started", }); const insertValues = (insertDb.insert as ReturnType).mock.results[0].value.values.mock.calls[0][0]; expect(insertValues.details).toEqual({}); }); it("passes through provided affectedTeamIds and details", async () => { vi.mocked(findUserById).mockResolvedValue({ username: "alice", displayName: "Alice" } as never); vi.mocked(getUserDisplayName).mockReturnValue("alice"); const insertDb = makeInsertDb(SAMPLE_ENTRY); vi.mocked(database).mockReturnValue(insertDb as never); const details = { pickNumber: 7, teamName: "Team Bravo", participantName: "Max V" }; await logCommissionerAction({ seasonId: SEASON_ID, leagueId: LEAGUE_ID, actorUserId: ACTOR_USER_ID, action: "force_manual_pick", affectedTeamIds: ["team-7"], details, }); const insertValues = (insertDb.insert as ReturnType).mock.results[0].value.values.mock.calls[0][0]; expect(insertValues.affectedTeamIds).toEqual(["team-7"]); expect(insertValues.details).toEqual(details); }); });