import { beforeEach, describe, expect, it, vi } from "vitest"; const state = vi.hoisted(() => ({ rows: [] as unknown[][], insert: vi.fn(), update: vi.fn(), invalidate: vi.fn(), log: vi.fn(() => "news-error-1"), notify: vi.fn(), })); vi.mock("@/lib/admin/guard", () => ({ requirePermission: async () => ({ id: 1, username: "staff" }), })); vi.mock("@/lib/permissions", () => ({ PERMS: { NEWS_EDIT: "news.edit" } })); vi.mock("@/lib/services/webhook", () => ({ notify: state.notify })); vi.mock("@/lib/services/news-cache", () => ({ invalidateNewsCache: state.invalidate, })); vi.mock("@/lib/logger", () => ({ logger: { error: state.log } })); vi.mock("next-intl/server", () => ({ getTranslations: async () => (key: string, args?: { reference: string }) => key + (args?.reference ?? ""), })); vi.mock("next/cache", () => ({ revalidatePath: vi.fn() })); vi.mock("next/navigation", () => ({ redirect: (url: string) => { throw Error(`redirect ${url}`); }, })); vi.mock("@/lib/db", async () => { const database = { select: () => ({ from: () => ({ where: () => ({ limit: () => { const result = Promise.resolve(state.rows.shift() ?? []); return Object.assign(result, { for: () => result }); }, }), }), }), insert: () => ({ values: state.insert }), update: () => ({ set: (value: unknown) => { state.update(value); return { where: async () => [{ affectedRows: 1 }] }; }, }), transaction: async (fn: (tx: unknown) => unknown): Promise => fn(database), }; return { ...(await import("@/db/schema")), db: database }; }); import { articleEditToken } from "@/lib/article-edit-token"; import { createArticle, updateArticle } from "./admin-articles"; function form(extra: Record = {}) { const f = new FormData(); for (const [k, v] of Object.entries({ title: "Test news", slug: "test-news", shortStory: "Summary", fullStory: "Body", status: "published", ...extra, })) f.set(k, v); return f; } describe("news saves", () => { beforeEach(() => { vi.clearAllMocks(); state.rows = []; state.insert.mockResolvedValue([{ insertId: 1 }]); }); it("preserves a recoverable error and logs its reference", async () => { state.insert.mockRejectedValueOnce(Error("Unknown column status")); const result = await createArticle(form()); expect(result).toEqual({ ok: false, error: "saveFailedReferencenews-error-1", }); expect(state.invalidate).not.toHaveBeenCalled(); expect(state.log).toHaveBeenCalled(); }); it("saves drafts without publication notifications", async () => { expect((await createArticle(form({ status: "draft" }))).ok).toBe(true); expect(state.insert).toHaveBeenCalledWith( expect.objectContaining({ status: "draft", publishAt: null, publishedAt: null, }), ); expect(state.notify).not.toHaveBeenCalled(); expect(state.invalidate).toHaveBeenCalledOnce(); }); it("preserves the slug and clears old scheduling for immediate publication", async () => { const existing = { title: "Old", slug: "test-news", image: "", shortStory: "", fullStory: "", status: "draft", publishAt: null, publishedAt: null, }; state.rows = [[existing]]; expect( ( await updateArticle( form({ id: "1", baseToken: articleEditToken(existing), publishAt: "2099-01-01T00:00:00Z", }), ) ).ok, ).toBe(true); expect(state.update).toHaveBeenCalledWith( expect.objectContaining({ slug: "test-news", publishAt: null, status: "published", }), ); expect(state.invalidate).toHaveBeenCalledOnce(); }); it("rejects an outdated editor without inserting a revision or overwriting the article", async () => { state.rows = [ [ { title: "Changed by another editor", slug: "test-news", image: "", shortStory: "", fullStory: "", status: "published", publishAt: null, }, ], ]; expect( await updateArticle(form({ id: "1", baseToken: "outdated" })), ).toEqual({ ok: false, error: "editConflict" }); expect(state.insert).not.toHaveBeenCalled(); expect(state.update).not.toHaveBeenCalled(); expect(state.invalidate).not.toHaveBeenCalled(); }); it("rejects invalid scheduled dates before writing", async () => { expect( (await createArticle(form({ status: "scheduled", publishAt: "invalid" }))) .ok, ).toBe(false); expect(state.insert).not.toHaveBeenCalled(); }); });