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,205 @@
|
||||
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);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user