import { beforeEach, describe, expect, it, vi } from "vitest"; vi.mock("next/cache", async () => ({ revalidatePath: (await import("@/tests/helpers/next-mocks")).revalidatePath })); vi.mock("next/navigation", async () => ({ redirect: (await import("@/tests/helpers/next-mocks")).redirect })); vi.mock("next/dist/client/components/redirect-error", async () => ({ isRedirectError: (await import("@/tests/helpers/next-mocks")).isRedirectError, })); vi.mock("@/lib/admin-auth", async () => { const m = await import("@/tests/helpers/next-mocks"); return { isAdminAuthenticated: m.isAdminAuthenticated, clearAdminSessionCookie: m.clearAdminSessionCookie }; }); import { deleteCategoryAction, deleteProjectAction, saveProjectAction, upsertCategoryAction, } from "@/app/_admin/portfolio/actions"; import { and, eq } from "drizzle-orm"; import { db } from "@/lib/db"; import { category as categoryTable, mediaUsage as mediaUsageTable, portfolioAsset as portfolioAssetTable, portfolioProject as portfolioProjectTable, } from "@/lib/db/schema"; import { createCategory, createProject } from "@/tests/helpers/factories"; import { adminAuth, captureRedirect, formDataFrom, resetNextMocks } from "@/tests/helpers/next-mocks"; beforeEach(() => { resetNextMocks(); }); function categoryForm(overrides: Record = {}) { return formDataFrom({ slug: "branding", nameAr: "الهوية", nameEn: "Branding", nameDe: "Branding", descriptionAr: "وصف", descriptionEn: "Description", descriptionDe: "Beschreibung", sortOrder: "1", isActive: "on", ...overrides, }); } function projectForm(categoryId: string, overrides: Record = {}) { const assets = JSON.stringify([ { kind: "IMAGE", altAr: "ع", altEn: "Alt", altDe: "Alt", sortOrder: 0, media: { mode: "external", url: "https://cdn/asset.png", kind: "IMAGE", label: "Asset" }, }, ]); const coverMedia = JSON.stringify({ mode: "external", url: "https://cdn/cover.png", kind: "IMAGE", label: "Cover" }); return formDataFrom({ categoryId, slug: "case-study", viewMode: "GRID", titleAr: "عنوان", titleEn: "Title", titleDe: "Titel", summaryAr: "ملخص", summaryEn: "Summary", summaryDe: "Zusammenfassung", clientName: "Client", projectYear: "2025", serviceLabelAr: "خدمة", serviceLabelEn: "Service", serviceLabelDe: "Service", previewUrl: "https://example.com", sortOrder: "0", isFeatured: "", isPublished: "on", sections: "[]", assets, coverMedia, ...overrides, }); } describe("upsertCategoryAction", () => { it("creates a category", async () => { const url = await captureRedirect(() => upsertCategoryAction(categoryForm())); expect(url).toContain("success="); const category = await db.query.category.findFirst({ where: eq(categoryTable.slug, "branding") }); expect(category?.nameEn).toBe("Branding"); expect(category?.isActive).toBe(true); }); it("updates an existing category", async () => { const existing = await createCategory({ slug: "old", nameEn: "Old" }); const url = await captureRedirect(() => upsertCategoryAction(categoryForm({ id: existing.id, slug: "old", nameEn: "Renamed" })), ); expect(url).toContain("success="); const category = await db.query.category.findFirst({ where: eq(categoryTable.id, existing.id) }); expect(category?.nameEn).toBe("Renamed"); }); it("reports a unique-constraint violation on duplicate slugs", async () => { await createCategory({ slug: "branding" }); const url = await captureRedirect(() => upsertCategoryAction(categoryForm({ slug: "branding" }))); expect(url).toContain("error="); expect(decodeURIComponent(url)).toContain("eindeutig"); }); it("reports validation errors for an invalid slug", async () => { const url = await captureRedirect(() => upsertCategoryAction(categoryForm({ slug: "Not Valid" }))); expect(url).toContain("error="); }); it("redirects unauthenticated callers to the admin root", async () => { adminAuth.authenticated = false; const url = await captureRedirect(() => upsertCategoryAction(categoryForm())); expect(url).toBe("/"); }); }); describe("deleteCategoryAction", () => { it("refuses to delete a category that has projects", async () => { const category = await createCategory(); await createProject({ categoryId: category.id }); const url = await captureRedirect(() => deleteCategoryAction(formDataFrom({ id: category.id }))); expect(url).toContain("error="); expect((await db.query.category.findFirst({ where: eq(categoryTable.id, category.id) })) ?? null).not.toBeNull(); }); it("deletes an empty category", async () => { const category = await createCategory(); const url = await captureRedirect(() => deleteCategoryAction(formDataFrom({ id: category.id }))); expect(url).toContain("success="); expect((await db.query.category.findFirst({ where: eq(categoryTable.id, category.id) })) ?? null).toBeNull(); }); }); describe("saveProjectAction", () => { it("creates a published project with cover and asset media usages", async () => { const category = await createCategory(); const url = await captureRedirect(() => saveProjectAction(projectForm(category.id))); expect(url).toContain("success="); const project = await db.query.portfolioProject.findFirst({ where: eq(portfolioProjectTable.slug, "case-study") }); expect(project).not.toBeNull(); expect(project?.isPublished).toBe(true); expect(project?.publishedAt).not.toBeNull(); expect(project?.coverImagePath).toBe("https://cdn/cover.png"); expect(await db.$count(portfolioAssetTable, eq(portfolioAssetTable.projectId, project!.id))).toBe(1); const usages = await db.select().from(mediaUsageTable).where(and(eq(mediaUsageTable.entityType, "portfolio-project"), eq(mediaUsageTable.entityId, project!.id))); const usageTypes = usages.map((u) => u.usageType).sort(); expect(usageTypes).toEqual(["PORTFOLIO_ASSET", "PORTFOLIO_COVER"]); }); it("updates an existing project and replaces its assets", async () => { const category = await createCategory(); const created = await captureRedirect(() => saveProjectAction(projectForm(category.id))); void created; const project = await db.query.portfolioProject.findFirst({ where: eq(portfolioProjectTable.slug, "case-study") }); const url = await captureRedirect(() => saveProjectAction(projectForm(category.id, { id: project!.id, titleEn: "Updated Title" })), ); expect(url).toContain("success="); const updated = await db.query.portfolioProject.findFirst({ where: eq(portfolioProjectTable.id, project!.id) }); expect(updated?.titleEn).toBe("Updated Title"); // assets are replaced, not duplicated expect(await db.$count(portfolioAssetTable, eq(portfolioAssetTable.projectId, project!.id))).toBe(1); }); it("keeps the original publishedAt when re-saving an already published project", async () => { const category = await createCategory(); await captureRedirect(() => saveProjectAction(projectForm(category.id))); const first = await db.query.portfolioProject.findFirst({ where: eq(portfolioProjectTable.slug, "case-study") }); const originalPublishedAt = first!.publishedAt; await captureRedirect(() => saveProjectAction(projectForm(category.id, { id: first!.id }))); const second = await db.query.portfolioProject.findFirst({ where: eq(portfolioProjectTable.id, first!.id) }); expect(second?.publishedAt?.toISOString()).toBe(originalPublishedAt?.toISOString()); }); it("reports validation errors and creates nothing", async () => { const category = await createCategory(); const url = await captureRedirect(() => saveProjectAction(projectForm(category.id, { titleEn: "" }))); expect(url).toContain("error="); expect(await db.$count(portfolioProjectTable)).toBe(0); }); it("reports a unique-constraint violation on duplicate slugs", async () => { const category = await createCategory(); await createProject({ categoryId: category.id, slug: "case-study" }); const url = await captureRedirect(() => saveProjectAction(projectForm(category.id))); expect(url).toContain("error="); expect(decodeURIComponent(url)).toContain("eindeutig"); }); it("redirects unauthenticated callers to the admin root", async () => { adminAuth.authenticated = false; const url = await captureRedirect(() => saveProjectAction(projectForm("cat"))); expect(url).toBe("/"); }); }); describe("deleteProjectAction", () => { it("deletes a project and its media usages", async () => { const project = await createProject(); const url = await captureRedirect(() => deleteProjectAction(formDataFrom({ id: project.id }))); expect(url).toContain("success="); expect((await db.query.portfolioProject.findFirst({ where: eq(portfolioProjectTable.id, project.id) })) ?? null).toBeNull(); }); it("errors when the project does not exist", async () => { const url = await captureRedirect(() => deleteProjectAction(formDataFrom({ id: "missing" }))); expect(url).toContain("error="); }); it("redirects unauthenticated callers to the admin root", async () => { adminAuth.authenticated = false; const url = await captureRedirect(() => deleteProjectAction(formDataFrom({ id: "x" }))); expect(url).toBe("/"); }); });