import { beforeEach, describe, expect, it, vi } from "vitest"; const mocks = vi.hoisted(() => ({ admin: vi.fn(), updateTag: vi.fn(), save: vi.fn() })); vi.mock("next/cache", () => ({ updateTag: mocks.updateTag })); vi.mock("@/lib/auth/server", () => ({ requireAdmin: mocks.admin })); vi.mock("@/lib/audit-log", () => ({ auditActor: () => ({ id: "admin", label: "Admin" }) })); vi.mock("@/lib/glossary-repository", () => ({ createGlossaryAlias: vi.fn(), deleteGlossaryAlias: vi.fn() })); vi.mock("@/lib/catalog/sync", () => ({ cancelCatalogSyncJob: vi.fn(), getCatalogSyncJob: vi.fn(), startCatalogSyncJob: vi.fn() })); vi.mock("@/lib/guides/queries", () => ({ getAdminGuideContentById: vi.fn(), getAdminTeamImportGuide: vi.fn(), listAdminTeamImportsForCharacter: vi.fn(), listAdminTeamImportGuides: vi.fn(), })); vi.mock("@/lib/guides/mutations", () => ({ createExtraSection: mocks.save, createGuide: mocks.save, deleteExtraSection: mocks.save, permanentlyDeleteGuide: mocks.save, reorderExtraSections: mocks.save, restoreGuide: mocks.save, saveArtifacts: mocks.save, saveConstellations: mocks.save, saveExtraSection: mocks.save, saveTeams: mocks.save, setGuideVisibility: mocks.save, saveWeapons: mocks.save, trashGuide: mocks.save, updateGuideBasics: mocks.save, GuideVersionConflictError: class extends Error { currentVersion = 9; }, })); import { moveGuideToTrash, restoreStructuredGuide, saveArtifactSection, saveGuideExtra, saveGuideOverview, saveWeaponSection } from "./actions"; import { GuideVersionConflictError } from "@/lib/guides/mutations"; beforeEach(() => { vi.resetAllMocks(); mocks.admin.mockResolvedValue({ user: { id: "admin" } }); mocks.save.mockResolvedValue({ version: 10 }); }); describe("guide save cache freshness", () => { const guideId = "550e8400-e29b-41d4-a716-446655440000"; it.each([saveArtifactSection, saveGuideOverview, saveWeaponSection, moveGuideToTrash, restoreStructuredGuide])( "expires the public page immediately after a successful action", async (action) => { expect(await action(guideId, {})).toMatchObject({ status: "saved", version: 10 }); expect(mocks.updateTag).toHaveBeenCalledWith(`buzz:page:${guideId}`); }, ); it("invalidates custom extras without changing their section id or version input", async () => { const sectionId = "550e8400-e29b-41d4-a716-446655440001"; const input = { expectedVersion: 9 }; await saveGuideExtra(guideId, sectionId, input); expect(mocks.save).toHaveBeenCalledWith(guideId, sectionId, input, expect.any(Object)); expect(mocks.updateTag).toHaveBeenCalledWith(`buzz:page:${guideId}`); }); it("preserves conflicts without invalidating or reporting a save", async () => { mocks.save.mockRejectedValue(new GuideVersionConflictError(9)); expect(await saveArtifactSection(guideId, {})).toMatchObject({ status: "conflict", currentVersion: 9 }); expect(mocks.updateTag).not.toHaveBeenCalled(); }); it("authorizes before writes or cache invalidation", async () => { mocks.admin.mockRejectedValue(new Error("unauthorized")); await expect(saveArtifactSection(guideId, {})).rejects.toThrow("unauthorized"); expect(mocks.save).not.toHaveBeenCalled(); expect(mocks.updateTag).not.toHaveBeenCalled(); }); });