import { beforeEach, describe, expect, it, vi } from "vitest"; import type { HousekeepingCapabilityContext } from "../../../foundation/contracts"; import { executeContentDatabaseMutation } from "./mutation-runtime-database"; const sqlMocks = vi.hoisted(() => ({ join: vi.fn((chunks: unknown[], separator: unknown) => ({ chunks, separator, })), raw: vi.fn((statement: string) => statement), tagged: vi.fn((strings: TemplateStringsArray, ...values: unknown[]) => ({ strings: Array.from(strings), values, })), })); const database = vi.hoisted(() => { let selected: Record = { id: 7, title: "Existing" }; const set = vi.fn((values: Record) => ({ where: vi.fn(async () => undefined), values, })); const update = vi.fn(() => ({ set })); const limit = vi.fn(async () => [selected]); const where = vi.fn(() => ({ limit })); const from = vi.fn(() => ({ where })); const select = vi.fn(() => ({ from })); const execute = vi.fn(async () => undefined); return { execute, set, update, select, selected(value: Record) { selected = value; }, }; }); vi.mock("drizzle-orm", () => ({ eq: vi.fn(), sql: Object.assign(sqlMocks.tagged, { join: sqlMocks.join, raw: sqlMocks.raw, }), })); vi.mock("@/lib/db", () => { const table = new Proxy({}, { get: (_target, key) => String(key) }); return { db: { execute: database.execute, select: database.select, update: database.update, }, EmailTemplates: table, Taggables: table, Tags: table, User: table, WebsiteAds: table, WebsiteArticleComments: table, WebsiteArticleReactions: table, WebsiteArticles: table, WebsiteBanner: table, WebsiteEvent: table, WebsiteEventPrize: table, WebsiteEventType: table, WebsiteEventWinner: table, WebsiteHelpCenterCategories: table, WebsitePoll: table, WebsitePollQuestion: table, WebsiteWriteableBoxes: table, }; }); vi.mock("@/lib/services/staff-activity", () => ({ logStaffActivity: vi.fn(async () => undefined), })); const capability = { actor: { id: 42, username: "operator", rank: 7 }, isSuperAdmin: false, has: () => true, hasAny: () => true, hasAll: () => true, } satisfies HousekeepingCapabilityContext; const context = { capability, correlationId: "database-runtime", legacy: false, }; describe("Content database mutation runtime partial updates", () => { beforeEach(() => { vi.clearAllMocks(); database.selected({ id: 7, title: "Existing" }); }); it("does not reset omitted banner fields", async () => { await executeContentDatabaseMutation( "banner.change", { action: "update", id: 7, title: "Renamed" }, context, undefined, ); expect(database.set).toHaveBeenCalledWith({ title: "Renamed" }); }); it("does not reset a tag color omitted by the update form", async () => { await executeContentDatabaseMutation( "tag.change", { action: "update", id: "7", name: "News" }, context, undefined, ); const values = database.set.mock.calls[0]?.[0]; expect(values).toMatchObject({ name: "News" }); expect(values).not.toHaveProperty("backgroundColor"); }); it.each([ ["help-question.change", { name: "Updated question" }, "name"], ["writeable-box.change", { title: "Updated box" }, "title"], ["email-template.change", { subject: "Updated subject" }, "subject"], ] as const)( "updates only supplied fields for %s", async (operation, patch, expectedKey) => { await executeContentDatabaseMutation( operation, { action: "update", id: "7", ...patch }, context, undefined, ); const values = database.set.mock.calls[0]?.[0]; expect(values).toHaveProperty(expectedKey); for (const destructiveKey of [ "content", "position", "isActive", "body", "variables", "imageUrl", ]) { expect(values).not.toHaveProperty(destructiveKey); } }, ); it.each([ ["help-question.change", { answer: "Updated answer" }, "content"], ["writeable-box.change", { content: "Updated content" }, "content"], ["email-template.change", { body: "Updated body" }, "body"], ] as const)( "accepts a non-title partial patch for %s", async (operation, patch, expectedKey) => { await executeContentDatabaseMutation( operation, { action: "update", id: "7", ...patch }, context, undefined, ); expect(database.set.mock.calls[0]?.[0]).toHaveProperty(expectedKey); }, ); it("updates only submitted prefix columns and preserves omitted icon, effect, and active", async () => { const snapshot = await executeContentDatabaseMutation( "prefix.change", { action: "update", id: 7, text: "Renamed" }, context, undefined, ); const assignments = sqlMocks.join.mock.calls.at(-1)?.[0] as | Array<{ strings: string[]; values: unknown[] }> | undefined; expect(assignments).toHaveLength(1); expect(assignments?.[0]?.strings.join(" ")).toContain("text ="); expect(assignments?.[0]?.strings.join(" ")).not.toMatch( /icon|effect|active/u, ); expect(snapshot?.after).toEqual({ id: 7, text: "Renamed" }); }); it("submits an explicit false prefix active patch without touching other columns", async () => { const snapshot = await executeContentDatabaseMutation( "prefix.change", { action: "update", id: 7, active: false }, context, undefined, ); const assignments = sqlMocks.join.mock.calls.at(-1)?.[0] as | Array<{ strings: string[]; values: unknown[] }> | undefined; expect(assignments).toHaveLength(1); expect(assignments?.[0]?.strings.join(" ")).toContain("active ="); expect(assignments?.[0]?.values).toEqual([0]); expect(snapshot?.after).toEqual({ id: 7, active: 0 }); }); });