test: add comprehensive automated test coverage
- Vitest multi-project setup (unit / integration / component) - Real-Postgres integration harness via in-process PGlite (TEST_DATABASE_URL override), migrations applied per worker; production code untouched - Unit: routing, locale, validation/Zod schemas, metadata, mail, site-theme, marquee, media, portfolio helpers, plus architecture-rule tests - Integration: Prisma data layer, API routes, and all server actions - Component: UI primitives and form components (jsdom + Testing Library) - 371 tests passing
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
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 { prisma } from "@/lib/prisma";
|
||||
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 prisma.category.findUnique({ where: { 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 prisma.category.findUnique({ where: { 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 prisma.category.findUnique({ where: { id: category.id } })).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 prisma.category.findUnique({ where: { id: category.id } })).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 prisma.portfolioProject.findUnique({ where: { 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 prisma.portfolioAsset.count({ where: { projectId: project!.id } })).toBe(1);
|
||||
const usages = await prisma.mediaUsage.findMany({
|
||||
where: { entityType: "portfolio-project", 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 prisma.portfolioProject.findUnique({ where: { slug: "case-study" } });
|
||||
|
||||
const url = await captureRedirect(() =>
|
||||
saveProjectAction(projectForm(category.id, { id: project!.id, titleEn: "Updated Title" })),
|
||||
);
|
||||
expect(url).toContain("success=");
|
||||
const updated = await prisma.portfolioProject.findUnique({ where: { id: project!.id } });
|
||||
expect(updated?.titleEn).toBe("Updated Title");
|
||||
// assets are replaced, not duplicated
|
||||
expect(await prisma.portfolioAsset.count({ where: { 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 prisma.portfolioProject.findUnique({ where: { slug: "case-study" } });
|
||||
const originalPublishedAt = first!.publishedAt;
|
||||
|
||||
await captureRedirect(() => saveProjectAction(projectForm(category.id, { id: first!.id })));
|
||||
const second = await prisma.portfolioProject.findUnique({ where: { 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 prisma.portfolioProject.count()).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 prisma.portfolioProject.findUnique({ where: { id: project.id } })).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("/");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user