- 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
206 lines
7.2 KiB
TypeScript
206 lines
7.2 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
|
|
import {
|
|
assetInputSchema,
|
|
categoryInputSchema,
|
|
projectInputSchema,
|
|
sectionInputSchema,
|
|
} from "@/lib/portfolio-validation";
|
|
|
|
function category(overrides: Record<string, unknown> = {}) {
|
|
return {
|
|
slug: "branding",
|
|
nameAr: "الهوية", nameEn: "Branding", nameDe: "Branding",
|
|
descriptionAr: "وصف", descriptionEn: "Description", descriptionDe: "Beschreibung",
|
|
sortOrder: 1,
|
|
isActive: true,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function baseMedia(overrides: Record<string, unknown> = {}) {
|
|
return { mode: "external", assetId: "", url: "https://x/y.png", label: "L", kind: "IMAGE", ...overrides };
|
|
}
|
|
|
|
function section(overrides: Record<string, unknown> = {}) {
|
|
return {
|
|
type: "RICH_TEXT",
|
|
titleAr: "ع", titleEn: "t", titleDe: "t",
|
|
bodyAr: "ب", bodyEn: "b", bodyDe: "b",
|
|
imagePath: "",
|
|
linkUrl: "",
|
|
sortOrder: 0,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function project(overrides: Record<string, unknown> = {}) {
|
|
return {
|
|
categoryId: "cat_1",
|
|
slug: "case-study",
|
|
viewMode: "GRID",
|
|
titleAr: "ع", titleEn: "T", titleDe: "T",
|
|
summaryAr: "م", summaryEn: "S", summaryDe: "S",
|
|
clientName: "Client",
|
|
projectYear: 2025,
|
|
serviceLabelAr: "خ", serviceLabelEn: "Svc", serviceLabelDe: "Svc",
|
|
previewUrl: "https://example.com",
|
|
currentCoverImagePath: "",
|
|
sortOrder: 1,
|
|
isFeatured: false,
|
|
isPublished: true,
|
|
sections: [],
|
|
assets: [],
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("categoryInputSchema", () => {
|
|
it("accepts a valid payload", () => {
|
|
expect(categoryInputSchema.parse(category()).slug).toBe("branding");
|
|
});
|
|
|
|
it("requires lowercase hyphenated slugs", () => {
|
|
expect(() => categoryInputSchema.parse(category({ slug: "Not Valid" }))).toThrow(/slug/i);
|
|
expect(() => categoryInputSchema.parse(category({ slug: "-leading" }))).toThrow(/slug/i);
|
|
expect(categoryInputSchema.parse(category({ slug: "multi-word-slug" })).slug).toBe("multi-word-slug");
|
|
});
|
|
|
|
it("requires all name and description locales", () => {
|
|
expect(() => categoryInputSchema.parse(category({ nameEn: "" }))).toThrow(/nameEn/i);
|
|
expect(() => categoryInputSchema.parse(category({ descriptionDe: " " }))).toThrow(/descriptionDe/i);
|
|
});
|
|
|
|
it("coerces sortOrder and enforces its range", () => {
|
|
expect(categoryInputSchema.parse(category({ sortOrder: "5" })).sortOrder).toBe(5);
|
|
expect(() => categoryInputSchema.parse(category({ sortOrder: 10000 }))).toThrow();
|
|
expect(() => categoryInputSchema.parse(category({ sortOrder: -1 }))).toThrow();
|
|
});
|
|
});
|
|
|
|
describe("sectionInputSchema", () => {
|
|
it("accepts a RICH_TEXT section with body", () => {
|
|
expect(sectionInputSchema.parse(section()).type).toBe("RICH_TEXT");
|
|
});
|
|
|
|
it("requires body for RICH_TEXT, STATS and DELIVERABLES", () => {
|
|
for (const type of ["RICH_TEXT", "STATS", "DELIVERABLES"]) {
|
|
expect(() => sectionInputSchema.parse(section({ type, bodyEn: "" }))).toThrow(/body/i);
|
|
}
|
|
});
|
|
|
|
it("requires an image for GALLERY sections", () => {
|
|
expect(() =>
|
|
sectionInputSchema.parse(section({ type: "GALLERY", bodyAr: "", bodyEn: "", bodyDe: "", imagePath: "" })),
|
|
).toThrow(/image/i);
|
|
expect(
|
|
sectionInputSchema.parse(
|
|
section({ type: "GALLERY", bodyAr: "", bodyEn: "", bodyDe: "", imagePath: "/uploads/media/x.svg" }),
|
|
).type,
|
|
).toBe("GALLERY");
|
|
expect(
|
|
sectionInputSchema.parse(
|
|
section({ type: "GALLERY", bodyAr: "", bodyEn: "", bodyDe: "", media: baseMedia({ mode: "library", assetId: "a" }) }),
|
|
).type,
|
|
).toBe("GALLERY");
|
|
});
|
|
|
|
it("requires a link url for LINK sections", () => {
|
|
expect(() =>
|
|
sectionInputSchema.parse(section({ type: "LINK", bodyAr: "", bodyEn: "", bodyDe: "", linkUrl: "" })),
|
|
).toThrow(/link/i);
|
|
expect(
|
|
sectionInputSchema.parse(section({ type: "LINK", bodyAr: "", bodyEn: "", bodyDe: "", linkUrl: "https://x" })).linkUrl,
|
|
).toBe("https://x");
|
|
});
|
|
|
|
it("rejects malformed link urls", () => {
|
|
expect(() => sectionInputSchema.parse(section({ type: "LINK", linkUrl: "javascript:alert(1)" }))).toThrow(/absolute URL/i);
|
|
});
|
|
|
|
it("requires titles in all languages", () => {
|
|
expect(() => sectionInputSchema.parse(section({ titleAr: "" }))).toThrow(/titleAr/i);
|
|
});
|
|
});
|
|
|
|
describe("assetInputSchema", () => {
|
|
it("accepts a valid image asset", () => {
|
|
const parsed = assetInputSchema.parse({
|
|
kind: "IMAGE",
|
|
filePath: "/uploads/media/assets/x.svg",
|
|
fileFieldName: "",
|
|
media: baseMedia(),
|
|
altAr: "ع", altEn: "a", altDe: "a",
|
|
sortOrder: 0,
|
|
});
|
|
expect(parsed.kind).toBe("IMAGE");
|
|
});
|
|
|
|
it("only allows the IMAGE kind", () => {
|
|
expect(() =>
|
|
assetInputSchema.parse({ kind: "DOCUMENT", altAr: "ع", altEn: "a", altDe: "a", sortOrder: 0 }),
|
|
).toThrow();
|
|
});
|
|
|
|
it("requires alt text in all languages", () => {
|
|
expect(() =>
|
|
assetInputSchema.parse({ kind: "IMAGE", altAr: "ع", altEn: "", altDe: "a", sortOrder: 0 }),
|
|
).toThrow(/altEn/i);
|
|
});
|
|
|
|
it("rejects invalid embedded media urls", () => {
|
|
expect(() =>
|
|
assetInputSchema.parse({
|
|
kind: "IMAGE",
|
|
media: baseMedia({ url: "not-a-url" }),
|
|
altAr: "ع", altEn: "a", altDe: "a",
|
|
sortOrder: 0,
|
|
}),
|
|
).toThrow(/url/i);
|
|
});
|
|
});
|
|
|
|
describe("projectInputSchema", () => {
|
|
it("accepts a fully valid project", () => {
|
|
expect(projectInputSchema.parse(project()).slug).toBe("case-study");
|
|
});
|
|
|
|
it("defaults viewMode to GRID and accepts known modes", () => {
|
|
expect(projectInputSchema.parse(project({ viewMode: undefined })).viewMode).toBe("GRID");
|
|
expect(projectInputSchema.parse(project({ viewMode: "CASE_STUDY" })).viewMode).toBe("CASE_STUDY");
|
|
expect(() => projectInputSchema.parse(project({ viewMode: "WILD" }))).toThrow();
|
|
});
|
|
|
|
it("rejects invalid slugs", () => {
|
|
expect(() => projectInputSchema.parse(project({ slug: "Bad Slug" }))).toThrow(/slug/i);
|
|
});
|
|
|
|
it("coerces and bounds projectYear", () => {
|
|
expect(projectInputSchema.parse(project({ projectYear: "2025" })).projectYear).toBe(2025);
|
|
expect(() => projectInputSchema.parse(project({ projectYear: 1999 }))).toThrow();
|
|
expect(() => projectInputSchema.parse(project({ projectYear: 2101 }))).toThrow();
|
|
});
|
|
|
|
it("requires all localized content fields", () => {
|
|
expect(() => projectInputSchema.parse(project({ summaryDe: "" }))).toThrow(/summaryDe/i);
|
|
expect(() => projectInputSchema.parse(project({ serviceLabelAr: "" }))).toThrow(/serviceLabelAr/i);
|
|
expect(() => projectInputSchema.parse(project({ clientName: "" }))).toThrow(/clientName/i);
|
|
});
|
|
|
|
it("allows an empty preview url but rejects a relative one", () => {
|
|
expect(projectInputSchema.parse(project({ previewUrl: "" })).previewUrl).toBe("");
|
|
expect(() => projectInputSchema.parse(project({ previewUrl: "/relative" }))).toThrow(/absolute URL/i);
|
|
});
|
|
|
|
it("validates nested sections and assets", () => {
|
|
expect(() =>
|
|
projectInputSchema.parse(project({ sections: [section({ titleEn: "" })] })),
|
|
).toThrow(/titleEn/i);
|
|
expect(
|
|
projectInputSchema.parse(
|
|
project({ assets: [{ kind: "IMAGE", altAr: "ع", altEn: "a", altDe: "a", sortOrder: 0, media: baseMedia() }] }),
|
|
).assets.length,
|
|
).toBe(1);
|
|
});
|
|
});
|