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:
Moh
2026-08-06 02:27:20 +02:00
parent 0f48381894
commit e2e06be86e
50 changed files with 4975 additions and 26 deletions
+40
View File
@@ -0,0 +1,40 @@
import { describe, expect, it } from "vitest";
import { prisma } from "@/lib/prisma";
describe("integration harness smoke test", () => {
it("connects to the migrated test database and performs CRUD", async () => {
const created = await prisma.category.create({
data: {
slug: "smoke",
nameAr: "a",
nameEn: "b",
nameDe: "c",
descriptionAr: "a",
descriptionEn: "b",
descriptionDe: "c",
},
});
expect(created.id).toBeTruthy();
expect(created.isActive).toBe(true);
const found = await prisma.category.findUnique({ where: { slug: "smoke" } });
expect(found?.nameEn).toBe("b");
});
it("resets the database between tests", async () => {
const count = await prisma.category.count();
expect(count).toBe(0);
});
it("supports enums and appconfig upsert", async () => {
await prisma.appConfig.upsert({
where: { key: "k" },
update: { value: "v2" },
create: { key: "k", value: "v1" },
});
const row = await prisma.appConfig.findUnique({ where: { key: "k" } });
expect(row?.value).toBe("v1");
});
});
+92
View File
@@ -0,0 +1,92 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
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,
}));
const { sendContactMessage } = vi.hoisted(() => ({ sendContactMessage: vi.fn(async () => {}) }));
vi.mock("@/lib/mail", () => ({ sendContactMessage }));
import { submitContactFormAction } from "@/app/[locale]/(site)/contact/actions";
import { captureRedirect, formDataFrom } from "@/tests/helpers/next-mocks";
beforeEach(() => {
sendContactMessage.mockClear();
});
describe("submitContactFormAction", () => {
it("sends a valid message and redirects to the localized success page", async () => {
const url = await captureRedirect(() =>
submitContactFormAction(
formDataFrom({
locale: "en",
name: "Jane Doe",
email: "jane@example.com",
message: "Hello, I would like to work together on a project.",
}),
),
);
expect(url).toBe("/en/success");
expect(sendContactMessage).toHaveBeenCalledTimes(1);
expect(sendContactMessage).toHaveBeenCalledWith(
expect.objectContaining({ locale: "en", name: "Jane Doe", email: "jane@example.com" }),
);
});
it("uses the default locale (de) and its bare success path", async () => {
const url = await captureRedirect(() =>
submitContactFormAction(
formDataFrom({
locale: "de",
name: "Max Mustermann",
email: "max@example.com",
message: "Ich interessiere mich fuer eine Zusammenarbeit.",
}),
),
);
expect(url).toBe("/success");
});
it("redirects back with an error for an invalid email and does not send", async () => {
const url = await captureRedirect(() =>
submitContactFormAction(
formDataFrom({
locale: "en",
name: "Jane",
email: "not-an-email",
message: "This is a long enough message body.",
}),
),
);
expect(url).toContain("/en/contact?error=");
expect(sendContactMessage).not.toHaveBeenCalled();
});
it("rejects a too-short message", async () => {
const url = await captureRedirect(() =>
submitContactFormAction(
formDataFrom({ locale: "de", name: "Jane Doe", email: "jane@example.com", message: "short" }),
),
);
expect(url).toContain("/contact?error=");
expect(sendContactMessage).not.toHaveBeenCalled();
});
it("redirects with an error when delivery fails", async () => {
sendContactMessage.mockRejectedValueOnce(new Error("smtp down"));
const url = await captureRedirect(() =>
submitContactFormAction(
formDataFrom({
locale: "en",
name: "Jane Doe",
email: "jane@example.com",
message: "A perfectly valid message body here.",
}),
),
);
expect(url).toContain("/en/contact?error=");
});
});
@@ -0,0 +1,41 @@
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 { updateMaintenanceModeAction } from "@/app/_admin/maintenance/actions";
import { getMaintenanceMode } from "@/lib/app-config";
import { adminAuth, captureRedirect, formDataFrom, resetNextMocks } from "@/tests/helpers/next-mocks";
beforeEach(() => {
resetNextMocks();
});
describe("updateMaintenanceModeAction", () => {
it("enables maintenance mode and redirects with a success flash", async () => {
const url = await captureRedirect(() => updateMaintenanceModeAction(formDataFrom({ enabled: "true" })));
expect(url).toContain("success=");
expect(await getMaintenanceMode()).toBe(true);
});
it("disables maintenance mode", async () => {
await captureRedirect(() => updateMaintenanceModeAction(formDataFrom({ enabled: "true" })));
await captureRedirect(() => updateMaintenanceModeAction(formDataFrom({ enabled: "false" })));
expect(await getMaintenanceMode()).toBe(false);
});
it("redirects unauthenticated callers to the admin root", async () => {
adminAuth.authenticated = false;
const url = await captureRedirect(() => updateMaintenanceModeAction(formDataFrom({ enabled: "true" })));
expect(url).toBe("/");
// state unchanged
expect(await getMaintenanceMode()).toBe(false);
});
});
+51
View File
@@ -0,0 +1,51 @@
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 { saveMarqueeSettingsAction } from "@/app/_admin/marquee/actions";
import { getMarqueeSettings } from "@/lib/app-config";
import { adminAuth, captureRedirect, formDataFrom, resetNextMocks } from "@/tests/helpers/next-mocks";
beforeEach(() => {
resetNextMocks();
});
const validRows = {
"row1-de": "A\nB",
"row2-de": "C\nD",
"row3-de": "E\nF",
"row4-de": "G\nH",
};
describe("saveMarqueeSettingsAction", () => {
it("saves german rows and mirrors them across locales", async () => {
const url = await captureRedirect(() => saveMarqueeSettingsAction(formDataFrom(validRows)));
expect(url).toContain("success=");
const settings = await getMarqueeSettings();
expect(settings.locales.de.row1).toBe("A\nB");
expect(settings.locales.en.row1).toBe("A\nB");
expect(settings.locales.ar.row4).toBe("G\nH");
});
it("redirects with an error when a required row is empty", async () => {
const url = await captureRedirect(() =>
saveMarqueeSettingsAction(formDataFrom({ ...validRows, "row2-de": " " })),
);
expect(url).toContain("error=");
});
it("redirects unauthenticated callers to the admin root", async () => {
adminAuth.authenticated = false;
const url = await captureRedirect(() => saveMarqueeSettingsAction(formDataFrom(validRows)));
expect(url).toBe("/");
});
});
+76
View File
@@ -0,0 +1,76 @@
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 { createMediaAssetAction, deleteMediaAssetAction } from "@/app/_admin/media/actions";
import { prisma } from "@/lib/prisma";
import { removeManagedMediaFile } from "@/lib/media-storage";
import { createMediaAsset, createMediaUsage } from "@/tests/helpers/factories";
import { canManageUploads } from "@/tests/helpers/fs-capability";
import { adminAuth, captureRedirect, formDataFrom, resetNextMocks } from "@/tests/helpers/next-mocks";
beforeEach(() => {
resetNextMocks();
});
describe("createMediaAssetAction", () => {
it("errors when no file is provided", async () => {
const url = await captureRedirect(() => createMediaAssetAction(formDataFrom({ kind: "IMAGE", label: "L" })));
expect(url).toContain("error=");
expect(await prisma.mediaAsset.count()).toBe(0);
});
it.skipIf(!canManageUploads)("creates an asset from an uploaded file", async () => {
const file = new File([new Uint8Array([0x89, 0x50, 0x4e, 0x47])], "pic.png", { type: "image/png" });
const url = await captureRedirect(() =>
createMediaAssetAction(formDataFrom({ kind: "IMAGE", label: "Pic", file })),
);
expect(url).toContain("success=");
const assets = await prisma.mediaAsset.findMany();
expect(assets.length).toBe(1);
expect(assets[0].source).toBe("UPLOAD");
await removeManagedMediaFile(assets[0].url);
});
it("redirects unauthenticated callers to the admin root", async () => {
adminAuth.authenticated = false;
const url = await captureRedirect(() => createMediaAssetAction(formDataFrom({ kind: "IMAGE", label: "L" })));
expect(url).toBe("/");
});
});
describe("deleteMediaAssetAction", () => {
it("errors when the asset does not exist", async () => {
const url = await captureRedirect(() => deleteMediaAssetAction(formDataFrom({ assetId: "missing" })));
expect(url).toContain("error=");
});
it("refuses to delete an asset that is still in use", async () => {
const asset = await createMediaAsset();
await createMediaUsage(asset.id);
const url = await captureRedirect(() => deleteMediaAssetAction(formDataFrom({ assetId: asset.id })));
expect(url).toContain("error=");
expect(await prisma.mediaAsset.findUnique({ where: { id: asset.id } })).not.toBeNull();
});
it("deletes an unused external asset", async () => {
const asset = await createMediaAsset({ url: "https://cdn/external.png" });
const url = await captureRedirect(() => deleteMediaAssetAction(formDataFrom({ assetId: asset.id })));
expect(url).toContain("success=");
expect(await prisma.mediaAsset.findUnique({ where: { id: asset.id } })).toBeNull();
});
it("redirects unauthenticated callers to the admin root", async () => {
adminAuth.authenticated = false;
const url = await captureRedirect(() => deleteMediaAssetAction(formDataFrom({ assetId: "x" })));
expect(url).toBe("/");
});
});
+222
View File
@@ -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("/");
});
});
@@ -0,0 +1,104 @@
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 {
saveSiteBrandSettingsAction,
saveSiteLocalizationSettingsAction,
} from "@/app/_admin/site-settings/actions";
import { getSiteSettings, getSiteSettingsMediaBindings } from "@/lib/app-config";
import { adminAuth, captureRedirect, formDataFrom, resetNextMocks } from "@/tests/helpers/next-mocks";
beforeEach(() => {
resetNextMocks();
});
describe("saveSiteBrandSettingsAction", () => {
it("saves a normalized primary color", async () => {
const url = await captureRedirect(() => saveSiteBrandSettingsAction(formDataFrom({ primaryColor: "#123456" })));
expect(url).toContain("success=");
expect((await getSiteSettings()).brand.primaryColor).toBe("#123456");
});
it("falls back to the default color for invalid input", async () => {
await captureRedirect(() => saveSiteBrandSettingsAction(formDataFrom({ primaryColor: "not-a-color" })));
expect((await getSiteSettings()).brand.primaryColor).toBe("#dc5a35");
});
it("wires an external favicon into media bindings", async () => {
const faviconMedia = JSON.stringify({ mode: "external", url: "https://cdn/f.svg", kind: "IMAGE", label: "F" });
const url = await captureRedirect(() =>
saveSiteBrandSettingsAction(formDataFrom({ primaryColor: "#222222", faviconMedia })),
);
expect(url).toContain("success=");
const bindings = await getSiteSettingsMediaBindings();
expect(bindings.favicon?.url).toBe("https://cdn/f.svg");
});
it("errors on an invalid media json payload", async () => {
const url = await captureRedirect(() =>
saveSiteBrandSettingsAction(formDataFrom({ primaryColor: "#222222", faviconMedia: "{not json" })),
);
expect(url).toContain("error=");
});
it("redirects unauthenticated callers to the admin root", async () => {
adminAuth.authenticated = false;
const url = await captureRedirect(() => saveSiteBrandSettingsAction(formDataFrom({ primaryColor: "#123456" })));
expect(url).toBe("/");
});
});
const localizationForm = {
defaultLocale: "en",
siteNameAr: "الموقع",
siteNameEn: "The Site",
siteNameDe: "Die Seite",
titleTemplateAr: "{pageTitle} | {siteName}",
titleTemplateEn: "{pageTitle} | {siteName}",
titleTemplateDe: "{pageTitle} | {siteName}",
siteDescriptionAr: "وصف",
siteDescriptionEn: "Description",
siteDescriptionDe: "Beschreibung",
subheadAr: "",
subheadEn: "",
subheadDe: "",
};
describe("saveSiteLocalizationSettingsAction", () => {
it("saves valid localization settings and the default locale", async () => {
const url = await captureRedirect(() => saveSiteLocalizationSettingsAction(formDataFrom(localizationForm)));
expect(url).toContain("success=");
const settings = await getSiteSettings();
expect(settings.defaultLocale).toBe("en");
expect(settings.locales.en.siteName).toBe("The Site");
});
it("requires a site name for every locale", async () => {
const url = await captureRedirect(() =>
saveSiteLocalizationSettingsAction(formDataFrom({ ...localizationForm, siteNameEn: "" })),
);
expect(url).toContain("error=");
});
it("requires the {pageTitle} token in every title template", async () => {
const url = await captureRedirect(() =>
saveSiteLocalizationSettingsAction(formDataFrom({ ...localizationForm, titleTemplateDe: "{siteName} only" })),
);
expect(url).toContain("error=");
});
it("redirects unauthenticated callers to the admin root", async () => {
adminAuth.authenticated = false;
const url = await captureRedirect(() => saveSiteLocalizationSettingsAction(formDataFrom(localizationForm)));
expect(url).toBe("/");
});
});
+91
View File
@@ -0,0 +1,91 @@
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 };
});
const { sendTestEmail } = vi.hoisted(() => ({ sendTestEmail: vi.fn(async () => {}) }));
vi.mock("@/lib/mail", () => ({ sendTestEmail }));
import { saveMailSettingsAction, sendTestEmailAction } from "@/app/_admin/smtp/actions";
import { getMailSettings } from "@/lib/app-config";
import { adminAuth, captureRedirect, formDataFrom, resetNextMocks } from "@/tests/helpers/next-mocks";
beforeEach(() => {
resetNextMocks();
sendTestEmail.mockClear();
});
const validForm = {
smtpHost: "smtp.example.com",
smtpPort: "465",
smtpUsername: "mailer",
smtpPassword: "secret",
smtpSecure: "on",
mailFromEmail: "from@example.com",
mailFromName: "Studio",
mailContactRecipient: "contact@example.com",
mailTestRecipient: "test@example.com",
};
describe("saveMailSettingsAction", () => {
it("persists valid settings and redirects with success", async () => {
const url = await captureRedirect(() => saveMailSettingsAction(formDataFrom(validForm)));
expect(url).toContain("success=");
const settings = await getMailSettings();
expect(settings.smtp.host).toBe("smtp.example.com");
expect(settings.smtp.port).toBe(465);
expect(settings.smtp.secure).toBe(true);
expect(settings.recipients.contact).toBe("contact@example.com");
});
it("rejects an invalid port with an error flash", async () => {
const url = await captureRedirect(() =>
saveMailSettingsAction(formDataFrom({ ...validForm, smtpPort: "not-a-number" })),
);
expect(url).toContain("error=");
});
it("retains the existing password when the field is left blank", async () => {
await captureRedirect(() => saveMailSettingsAction(formDataFrom(validForm)));
await captureRedirect(() =>
saveMailSettingsAction(formDataFrom({ ...validForm, smtpPassword: "" })),
);
const settings = await getMailSettings();
expect(settings.smtp.password).toBe("secret");
});
it("redirects unauthenticated callers to the admin root", async () => {
adminAuth.authenticated = false;
const url = await captureRedirect(() => saveMailSettingsAction(formDataFrom(validForm)));
expect(url).toBe("/");
});
});
describe("sendTestEmailAction", () => {
it("sends a test email and redirects with success", async () => {
const url = await captureRedirect(() => sendTestEmailAction());
expect(sendTestEmail).toHaveBeenCalledTimes(1);
expect(url).toContain("success=");
});
it("redirects with an error when sending fails", async () => {
sendTestEmail.mockRejectedValueOnce(new Error("SMTP host is required."));
const url = await captureRedirect(() => sendTestEmailAction());
expect(url).toContain("error=");
});
it("redirects unauthenticated callers to the admin root", async () => {
adminAuth.authenticated = false;
const url = await captureRedirect(() => sendTestEmailAction());
expect(url).toBe("/");
expect(sendTestEmail).not.toHaveBeenCalled();
});
});
+81
View File
@@ -0,0 +1,81 @@
import { afterEach, describe, expect, it, vi } from "vitest";
vi.mock("next/headers", () => ({
headers: async () => new Headers({ "x-forwarded-for": "203.0.113.7" }),
cookies: async () => ({ get: () => undefined, set: () => {}, delete: () => {} }),
}));
import {
getAdminLockState,
isAdminAuthConfigured,
isPasswordValid,
registerFailedAdminAttempt,
resetAdminFailedAttempts,
} from "@/lib/admin-auth";
afterEach(() => {
vi.unstubAllEnvs();
});
describe("admin auth configuration", () => {
it("is configured only when both password and secret are set", () => {
vi.stubEnv("ADMIN_PASSWORD", "");
vi.stubEnv("ADMIN_AUTH_SECRET", "");
expect(isAdminAuthConfigured()).toBe(false);
vi.stubEnv("ADMIN_PASSWORD", "pw");
vi.stubEnv("ADMIN_AUTH_SECRET", "");
expect(isAdminAuthConfigured()).toBe(false);
vi.stubEnv("ADMIN_PASSWORD", "pw");
vi.stubEnv("ADMIN_AUTH_SECRET", "secret");
expect(isAdminAuthConfigured()).toBe(true);
});
});
describe("isPasswordValid", () => {
it("returns false when auth is not configured", () => {
vi.stubEnv("ADMIN_PASSWORD", "");
vi.stubEnv("ADMIN_AUTH_SECRET", "");
expect(isPasswordValid("anything")).toBe(false);
});
it("accepts the correct password and rejects wrong ones", () => {
vi.stubEnv("ADMIN_PASSWORD", "s3cret-password");
vi.stubEnv("ADMIN_AUTH_SECRET", "hmac-secret");
expect(isPasswordValid("s3cret-password")).toBe(true);
expect(isPasswordValid("wrong")).toBe(false);
expect(isPasswordValid("s3cret-passwordX")).toBe(false); // length mismatch
});
});
describe("login lockout", () => {
it("locks the IP after the failed-attempt threshold", async () => {
expect((await getAdminLockState()).locked).toBe(false);
for (let i = 0; i < 4; i += 1) {
const state = await registerFailedAdminAttempt();
expect(state.locked).toBe(false);
}
expect((await getAdminLockState()).locked).toBe(false);
const fifth = await registerFailedAdminAttempt();
expect(fifth.locked).toBe(true);
expect(fifth.remainingSeconds).toBeGreaterThan(0);
const lockState = await getAdminLockState();
expect(lockState.locked).toBe(true);
expect(lockState.remainingSeconds).toBeGreaterThan(0);
expect(lockState.remainingSeconds).toBeLessThanOrEqual(15 * 60);
});
it("clears the lock on reset", async () => {
for (let i = 0; i < 5; i += 1) {
await registerFailedAdminAttempt();
}
expect((await getAdminLockState()).locked).toBe(true);
await resetAdminFailedAttempts();
expect((await getAdminLockState()).locked).toBe(false);
});
});
+52
View File
@@ -0,0 +1,52 @@
import { afterEach, describe, expect, it, vi } from "vitest";
import { GET as healthGet } from "@/app/api/health/route";
import { GET as defaultLocaleGet } from "@/app/api/site/default-locale/route";
import { setMaintenanceMode, updateSiteSettings, getSiteSettings } from "@/lib/app-config";
import { prisma } from "@/lib/prisma";
afterEach(() => {
vi.restoreAllMocks();
});
describe("GET /api/health", () => {
it("reports ok when the database responds", async () => {
const response = await healthGet();
expect(response.status).toBe(200);
const body = await response.json();
expect(body.status).toBe("ok");
expect(body.checks.database).toBe("up");
expect(typeof body.timestamp).toBe("string");
});
it("reports degraded (503) when the database query throws", async () => {
vi.spyOn(prisma, "$queryRaw").mockRejectedValueOnce(new Error("db down"));
const response = await healthGet();
expect(response.status).toBe(503);
const body = await response.json();
expect(body.status).toBe("degraded");
expect(body.checks.database).toBe("down");
});
});
describe("GET /api/site/default-locale", () => {
it("returns the runtime default locale and maintenance flag with no-store", async () => {
const response = await defaultLocaleGet();
expect(response.headers.get("Cache-Control")).toBe("no-store, max-age=0");
const body = await response.json();
expect(body.defaultLocale).toBe("de");
expect(body.maintenanceEnabled).toBe(false);
});
it("reflects updated settings and maintenance state", async () => {
const settings = await getSiteSettings();
settings.defaultLocale = "ar";
await updateSiteSettings(settings);
await setMaintenanceMode(true);
const response = await defaultLocaleGet();
const body = await response.json();
expect(body.defaultLocale).toBe("ar");
expect(body.maintenanceEnabled).toBe(true);
});
});
+142
View File
@@ -0,0 +1,142 @@
import { describe, expect, it } from "vitest";
import {
DEFAULT_SITE_NAME,
MAINTENANCE_MODE_KEY,
SITE_NAME_KEY,
SITE_SETTINGS_ENTITY_ID,
SITE_SETTINGS_ENTITY_TYPE,
SITE_SETTINGS_FAVICON_FIELD_KEY,
SITE_SETTINGS_LOGO_LIGHT_FIELD_KEY,
buildDefaultMailSettings,
buildDefaultMarqueeSettings,
getMailSettings,
getMaintenanceMode,
getMarqueeSettings,
getSiteSettings,
getSiteSettingsMediaBindings,
setMaintenanceMode,
updateMailSettings,
updateMarqueeSettings,
updateSiteSettings,
} from "@/lib/app-config";
import { prisma } from "@/lib/prisma";
import { createMediaAsset } from "@/tests/helpers/factories";
describe("maintenance mode", () => {
it("defaults to false when unset", async () => {
expect(await getMaintenanceMode()).toBe(false);
});
it("persists and reads back the enabled flag", async () => {
await setMaintenanceMode(true);
expect(await getMaintenanceMode()).toBe(true);
const row = await prisma.appConfig.findUnique({ where: { key: MAINTENANCE_MODE_KEY } });
expect(row?.value).toBe("true");
await setMaintenanceMode(false);
expect(await getMaintenanceMode()).toBe(false);
});
});
describe("site settings", () => {
it("returns defaults (with fallback name) when nothing stored", async () => {
const settings = await getSiteSettings();
expect(settings.defaultLocale).toBe("de");
expect(settings.locales.en.siteName).toBe(DEFAULT_SITE_NAME);
});
it("uses the stored siteName key as the fallback name", async () => {
await prisma.appConfig.create({ data: { key: SITE_NAME_KEY, value: "My Studio" } });
const settings = await getSiteSettings();
expect(settings.locales.ar.siteName).toBe("My Studio");
});
it("round-trips an updated settings object", async () => {
const next = await getSiteSettings();
next.defaultLocale = "ar";
next.brand.primaryColor = "#123456";
next.locales.en.siteName = "Updated EN";
await updateSiteSettings(next);
const reloaded = await getSiteSettings();
expect(reloaded.defaultLocale).toBe("ar");
expect(reloaded.brand.primaryColor).toBe("#123456");
expect(reloaded.locales.en.siteName).toBe("Updated EN");
});
});
describe("mail settings", () => {
it("returns defaults when unset", async () => {
expect(await getMailSettings()).toEqual(buildDefaultMailSettings());
});
it("round-trips stored mail settings", async () => {
const next = buildDefaultMailSettings();
next.smtp.host = "smtp.test";
next.smtp.port = 465;
next.sender.email = "from@test";
next.recipients.contact = "c@test";
await updateMailSettings(next);
const reloaded = await getMailSettings();
expect(reloaded.smtp.host).toBe("smtp.test");
expect(reloaded.smtp.port).toBe(465);
expect(reloaded.recipients.contact).toBe("c@test");
});
});
describe("marquee settings", () => {
it("returns defaults when unset", async () => {
const settings = await getMarqueeSettings();
expect(settings.locales.de.row1).toBe(buildDefaultMarqueeSettings().locales.de.row1);
});
it("stores german-synced values", async () => {
const next = buildDefaultMarqueeSettings();
next.locales.de.row1 = "GERMAN ROW";
next.locales.en.row1 = "will be overwritten";
await updateMarqueeSettings(next);
const reloaded = await getMarqueeSettings();
expect(reloaded.locales.de.row1).toBe("GERMAN ROW");
expect(reloaded.locales.en.row1).toBe("GERMAN ROW");
expect(reloaded.locales.ar.row1).toBe("GERMAN ROW");
});
});
describe("getSiteSettingsMediaBindings", () => {
it("returns nulls when there are no usages", async () => {
const bindings = await getSiteSettingsMediaBindings();
expect(bindings).toEqual({ siteLogoLight: null, siteLogoDark: null, favicon: null, defaultOgImage: null });
});
it("maps media usages to their field bindings", async () => {
const logo = await createMediaAsset({ url: "https://cdn/logo.png" });
const favicon = await createMediaAsset({ url: "https://cdn/favicon.svg" });
await prisma.mediaUsage.create({
data: {
assetId: logo.id,
usageType: "GENERIC",
entityType: SITE_SETTINGS_ENTITY_TYPE,
entityId: SITE_SETTINGS_ENTITY_ID,
fieldKey: SITE_SETTINGS_LOGO_LIGHT_FIELD_KEY,
},
});
await prisma.mediaUsage.create({
data: {
assetId: favicon.id,
usageType: "GENERIC",
entityType: SITE_SETTINGS_ENTITY_TYPE,
entityId: SITE_SETTINGS_ENTITY_ID,
fieldKey: SITE_SETTINGS_FAVICON_FIELD_KEY,
},
});
const bindings = await getSiteSettingsMediaBindings();
expect(bindings.siteLogoLight?.assetId).toBe(logo.id);
expect(bindings.siteLogoLight?.url).toBe("https://cdn/logo.png");
expect(bindings.favicon?.assetId).toBe(favicon.id);
expect(bindings.favicon?.version).toMatch(/\d{4}-\d{2}-\d{2}T/); // updatedAt ISO string
expect(bindings.siteLogoDark).toBeNull();
});
});
+120
View File
@@ -0,0 +1,120 @@
import { readFile } from "fs/promises";
import { describe, expect, it } from "vitest";
import { resolveMediaSelection } from "@/lib/media-service";
import { resolveMediaUploadPath } from "@/lib/media-storage";
import { prisma } from "@/lib/prisma";
import { createMediaAsset } from "@/tests/helpers/factories";
import { canManageUploads } from "@/tests/helpers/fs-capability";
describe("resolveMediaSelection — library mode", () => {
it("returns the referenced asset", async () => {
const asset = await createMediaAsset({ url: "https://cdn/lib.png" });
const result = await resolveMediaSelection({
media: { mode: "library", assetId: asset.id, url: "", label: "", kind: "IMAGE" },
uploadFile: null,
folder: "covers",
fallbackLabel: "Cover",
required: false,
});
expect(result.assetId).toBe(asset.id);
expect(result.url).toBe("https://cdn/lib.png");
});
it("throws when the referenced asset is missing", async () => {
await expect(
resolveMediaSelection({
media: { mode: "library", assetId: "nope", url: "", label: "", kind: "IMAGE" },
uploadFile: null,
folder: "covers",
fallbackLabel: "Cover",
required: true,
}),
).rejects.toThrow(/not found/i);
});
});
describe("resolveMediaSelection — external mode", () => {
it("creates a new external asset from the url", async () => {
const result = await resolveMediaSelection({
media: { mode: "external", assetId: "", url: "https://cdn/new/photo.png", label: "Photo", kind: "IMAGE" },
uploadFile: null,
folder: "covers",
fallbackLabel: "Cover",
required: true,
});
expect(result.createdAssetId).toBeTruthy();
expect(result.url).toBe("https://cdn/new/photo.png");
const stored = await prisma.mediaAsset.findUnique({ where: { id: result.assetId! } });
expect(stored?.source).toBe("EXTERNAL");
expect(stored?.fileName).toBe("photo.png");
expect(stored?.label).toBe("Photo");
});
it("returns empty selection for a not-required empty url", async () => {
const result = await resolveMediaSelection({
media: { mode: "external", assetId: "", url: "", label: "", kind: "IMAGE" },
uploadFile: null,
folder: "covers",
fallbackLabel: "Cover",
required: false,
});
expect(result.assetId).toBeNull();
expect(result.url).toBe("");
});
});
describe("resolveMediaSelection — missing configuration", () => {
it("throws when required and no media object is present", async () => {
await expect(
resolveMediaSelection({ media: undefined, uploadFile: null, folder: "covers", fallbackLabel: "L", required: true }),
).rejects.toThrow(/missing/i);
});
it("returns empty selection when not required and no media object is present", async () => {
const result = await resolveMediaSelection({
media: undefined,
uploadFile: null,
folder: "covers",
fallbackLabel: "L",
required: false,
});
expect(result.assetId).toBeNull();
});
it("throws for a required upload with no file", async () => {
await expect(
resolveMediaSelection({
media: { mode: "upload", assetId: "", url: "", label: "", kind: "IMAGE" },
uploadFile: null,
folder: "covers",
fallbackLabel: "L",
required: true,
}),
).rejects.toThrow(/required/i);
});
});
describe("resolveMediaSelection — upload mode (filesystem)", () => {
it.skipIf(!canManageUploads)("saves the file and creates an UPLOAD asset", async () => {
const file = new File([new Uint8Array([0x89, 0x50, 0x4e, 0x47])], "shot.png", { type: "image/png" });
const result = await resolveMediaSelection({
media: { mode: "upload", assetId: "", url: "", label: "Shot", kind: "IMAGE" },
uploadFile: file,
folder: "tests",
fallbackLabel: "L",
required: true,
});
expect(result.uploadedUrl).toBeTruthy();
const stored = await prisma.mediaAsset.findUnique({ where: { id: result.assetId! } });
expect(stored?.source).toBe("UPLOAD");
// File actually written to disk
const bytes = await readFile(resolveMediaUploadPath(result.url));
expect(bytes.length).toBeGreaterThan(0);
// cleanup
const { removeManagedMediaFile } = await import("@/lib/media-storage");
await removeManagedMediaFile(result.url);
});
});
+137
View File
@@ -0,0 +1,137 @@
import { describe, expect, it } from "vitest";
import {
countMediaUsageReferences,
createMediaAsset,
deleteEntityMediaUsages,
getAdminMediaAssets,
getMediaAssetById,
getMediaOptions,
getPortfolioMediaBindings,
replaceEntityMediaUsages,
} from "@/lib/media";
import { prisma } from "@/lib/prisma";
import { createMediaAsset as seedAsset } from "@/tests/helpers/factories";
describe("createMediaAsset / getMediaAssetById", () => {
it("creates and reads back an asset with usages", async () => {
const created = await createMediaAsset({
source: "EXTERNAL",
kind: "IMAGE",
url: "https://cdn/x.png",
fileName: "x.png",
label: "X",
});
const found = await getMediaAssetById(created.id);
expect(found?.url).toBe("https://cdn/x.png");
expect(found?.usages).toEqual([]);
});
it("returns null for a missing asset", async () => {
expect(await getMediaAssetById("nope")).toBeNull();
});
});
describe("getMediaOptions", () => {
it("filters by kind", async () => {
await seedAsset({ kind: "IMAGE" });
await seedAsset({ kind: "DOCUMENT" });
const images = await getMediaOptions({ kind: "IMAGE" });
expect(images.every((a) => a.kind === "IMAGE")).toBe(true);
const all = await getMediaOptions();
expect(all.length).toBe(2);
});
});
describe("getAdminMediaAssets", () => {
it("returns newest first with usage details", async () => {
const a = await seedAsset();
await createMediaUsageFor(a.id);
const list = await getAdminMediaAssets();
expect(list.length).toBe(1);
expect(list[0].usages.length).toBe(1);
});
});
describe("replaceEntityMediaUsages", () => {
it("replaces existing usages transactionally", async () => {
const a1 = await seedAsset();
const a2 = await seedAsset();
await replaceEntityMediaUsages({
entityType: "portfolio-project",
entityId: "p1",
usages: [{ assetId: a1.id, usageType: "PORTFOLIO_COVER", fieldKey: "cover" }],
});
expect(await countMediaUsageReferences(a1.id)).toBe(1);
await replaceEntityMediaUsages({
entityType: "portfolio-project",
entityId: "p1",
usages: [{ assetId: a2.id, usageType: "PORTFOLIO_COVER", fieldKey: "cover" }],
});
expect(await countMediaUsageReferences(a1.id)).toBe(0);
expect(await countMediaUsageReferences(a2.id)).toBe(1);
});
it("clears usages when given an empty list", async () => {
const a1 = await seedAsset();
await replaceEntityMediaUsages({
entityType: "portfolio-project",
entityId: "p2",
usages: [{ assetId: a1.id, usageType: "PORTFOLIO_ASSET", fieldKey: "a" }],
});
await replaceEntityMediaUsages({ entityType: "portfolio-project", entityId: "p2", usages: [] });
expect(await countMediaUsageReferences(a1.id)).toBe(0);
});
});
describe("deleteEntityMediaUsages", () => {
it("removes only the target entity's usages", async () => {
const a1 = await seedAsset();
await replaceEntityMediaUsages({
entityType: "portfolio-project",
entityId: "keep",
usages: [{ assetId: a1.id, usageType: "PORTFOLIO_ASSET", fieldKey: "a" }],
});
await replaceEntityMediaUsages({
entityType: "portfolio-project",
entityId: "drop",
usages: [{ assetId: a1.id, usageType: "PORTFOLIO_ASSET", fieldKey: "b" }],
});
await deleteEntityMediaUsages("portfolio-project", "drop");
expect(await countMediaUsageReferences(a1.id)).toBe(1);
});
});
describe("getPortfolioMediaBindings", () => {
it("routes usages into cover / section / asset buckets", async () => {
const cover = await seedAsset();
const section = await seedAsset();
const asset = await seedAsset();
await prisma.mediaUsage.createMany({
data: [
{ assetId: cover.id, usageType: "PORTFOLIO_COVER", entityType: "portfolio-project", entityId: "proj", fieldKey: "cover" },
{ assetId: section.id, usageType: "PORTFOLIO_SECTION", entityType: "portfolio-project", entityId: "proj", fieldKey: "sec_1" },
{ assetId: asset.id, usageType: "PORTFOLIO_ASSET", entityType: "portfolio-project", entityId: "proj", fieldKey: "ast_1" },
],
});
const bindings = await getPortfolioMediaBindings("proj");
expect(bindings.coverAssetId).toBe(cover.id);
expect(bindings.sectionAssetIds.sec_1).toBe(section.id);
expect(bindings.assetIds.ast_1).toBe(asset.id);
});
it("returns empty bindings for an unknown project", async () => {
const bindings = await getPortfolioMediaBindings("missing");
expect(bindings).toEqual({ coverAssetId: null, sectionAssetIds: {}, assetIds: {} });
});
});
async function createMediaUsageFor(assetId: string) {
await prisma.mediaUsage.create({
data: { assetId, usageType: "GENERIC", entityType: "e", entityId: "1", fieldKey: "f" },
});
}
+141
View File
@@ -0,0 +1,141 @@
import { describe, expect, it } from "vitest";
import {
getActivePortfolioCategories,
getActivePortfolioCategoryBySlug,
getAdminPortfolioCategories,
getAdminPortfolioProjectById,
getAdminPortfolioProjects,
getPublishedPortfolioProjectBySlug,
getPublishedPortfolioProjects,
} from "@/lib/portfolio";
import { prisma } from "@/lib/prisma";
import {
createAsset,
createCategory,
createMediaAsset,
createProject,
createSection,
} from "@/tests/helpers/factories";
describe("categories", () => {
it("lists admin categories with project counts, ordered", async () => {
const a = await createCategory({ slug: "a", sortOrder: 2 });
await createCategory({ slug: "b", sortOrder: 1 });
await createProject({ categoryId: a.id });
const categories = await getAdminPortfolioCategories();
expect(categories.map((c) => c.slug)).toEqual(["b", "a"]); // sortOrder asc
expect(categories.find((c) => c.slug === "a")?.projectCount).toBe(1);
expect(categories.find((c) => c.slug === "b")?.projectCount).toBe(0);
});
it("returns only active categories publicly", async () => {
await createCategory({ slug: "on", isActive: true });
await createCategory({ slug: "off", isActive: false });
const active = await getActivePortfolioCategories();
expect(active.map((c) => c.slug)).toEqual(["on"]);
});
it("finds an active category by slug and ignores inactive ones", async () => {
await createCategory({ slug: "visible", isActive: true });
await createCategory({ slug: "hidden", isActive: false });
expect((await getActivePortfolioCategoryBySlug("visible"))?.slug).toBe("visible");
expect(await getActivePortfolioCategoryBySlug("hidden")).toBeNull();
});
});
describe("admin projects", () => {
it("filters by status and category", async () => {
const cat = await createCategory();
await createProject({ categoryId: cat.id, slug: "pub", isPublished: true });
await createProject({ categoryId: cat.id, slug: "draft", isPublished: false });
const published = await getAdminPortfolioProjects({ status: "published" });
expect(published.map((p) => p.slug)).toEqual(["pub"]);
const drafts = await getAdminPortfolioProjects({ status: "draft" });
expect(drafts.map((p) => p.slug)).toEqual(["draft"]);
const byCategory = await getAdminPortfolioProjects({ categoryId: cat.id });
expect(byCategory.length).toBe(2);
});
it("maps localized content and nested sections/assets", async () => {
const project = await createProject({ slug: "mapped" });
await createSection(project.id, { titleEn: "Intro" });
await createAsset(project.id, { altEn: "Cover" });
const detail = await getAdminPortfolioProjectById(project.id);
expect(detail?.title.en).toBe("Title");
expect(detail?.sections[0].title.en).toBe("Intro");
expect(detail?.assets[0].alt.en).toBe("Cover");
});
it("attaches media bindings to a project fetched by id", async () => {
const project = await createProject();
const cover = await createMediaAsset();
await prisma.mediaUsage.create({
data: {
assetId: cover.id,
usageType: "PORTFOLIO_COVER",
entityType: "portfolio-project",
entityId: project.id,
fieldKey: "cover",
},
});
const detail = await getAdminPortfolioProjectById(project.id);
expect(detail?.coverMediaAssetId).toBe(cover.id);
});
it("returns null for a missing project id", async () => {
expect(await getAdminPortfolioProjectById("missing")).toBeNull();
});
});
describe("published projects", () => {
it("returns only published projects in active categories", async () => {
const activeCat = await createCategory({ isActive: true });
const inactiveCat = await createCategory({ isActive: false });
await createProject({ categoryId: activeCat.id, slug: "shown", isPublished: true });
await createProject({ categoryId: activeCat.id, slug: "hidden-draft", isPublished: false });
await createProject({ categoryId: inactiveCat.id, slug: "hidden-cat", isPublished: true });
const projects = await getPublishedPortfolioProjects();
expect(projects.map((p) => p.slug)).toEqual(["shown"]);
});
it("filters published projects by category slug", async () => {
const catA = await createCategory({ slug: "cat-a", isActive: true });
const catB = await createCategory({ slug: "cat-b", isActive: true });
await createProject({ categoryId: catA.id, slug: "in-a", isPublished: true });
await createProject({ categoryId: catB.id, slug: "in-b", isPublished: true });
const projects = await getPublishedPortfolioProjects({ categorySlug: "cat-a" });
expect(projects.map((p) => p.slug)).toEqual(["in-a"]);
});
it("finds a published project by slug and hides drafts", async () => {
await createProject({ slug: "live", isPublished: true });
await createProject({ slug: "wip", isPublished: false });
expect((await getPublishedPortfolioProjectBySlug("live"))?.slug).toBe("live");
expect(await getPublishedPortfolioProjectBySlug("wip")).toBeNull();
});
});
describe("referential integrity", () => {
it("restricts deleting a category that still has projects", async () => {
const cat = await createCategory();
await createProject({ categoryId: cat.id });
await expect(prisma.category.delete({ where: { id: cat.id } })).rejects.toThrow();
});
it("cascades section and asset deletion when a project is removed", async () => {
const project = await createProject();
await createSection(project.id);
await createAsset(project.id);
await prisma.portfolioProject.delete({ where: { id: project.id } });
expect(await prisma.portfolioSection.count()).toBe(0);
expect(await prisma.portfolioAsset.count()).toBe(0);
});
});