Files
sass-mohfarawati/tests/integration/actions-portfolio.test.ts
T
MOH 0a5f77d8de REFACTORED - migrate the data layer from Prisma to Drizzle (unify the stack)
- Add lib/db (Drizzle schema: 7 tables + 6 enums + relations, postgres.js client),
  drizzle.config.ts, lib/db/enums.ts (Prisma-compatible enum objects)
- Rewrite all 14 app consumers + 4 admin components to Drizzle
- Move the test DB layer to Drizzle + in-process PGlite; convert all 8 integration
  test files + factories (371 tests green)
- Remove Prisma: deps, prisma/ schema+migrations, lib/prisma.ts, Dockerfile prisma
  generate; wire db:generate/migrate/push/studio to drizzle-kit; update Makefile
- Preserve the old seed as scripts/legacy-prisma-seed.cjs (needs a Drizzle rewrite)
2026-08-07 14:18:41 +02:00

229 lines
9.3 KiB
TypeScript

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<string, string> = {}) {
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<string, string> = {}) {
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("/");
});
});