feat(features): contract suites for all repository interfaces

Each repository interface now has a contract suite under
src/__contracts__/. Both Mock and Payload implementations run the
same suite, eliminating mock-vs-real drift. Payload impls back the
contract with an in-memory stub via vi.mock('payload') + a small
buildPayloadStub helper.

Spec: §5.2, §6.4
This commit is contained in:
2026-05-05 15:28:38 +02:00
parent a74f217703
commit e1355e6bc7
17 changed files with 751 additions and 62 deletions

View File

@@ -0,0 +1,99 @@
import { it, expect, beforeEach } from "vitest";
import { defineContractSuite } from "@repo/core-testing/contract";
import type { IPagesRepository } from "../application/repositories/pages-repository.interface.js";
import type { Page } from "../entities/page.js";
const SEED_DATE = new Date("2026-01-01T00:00:00.000Z");
/**
* Known fixtures that every implementation's `buildSubject` must pre-seed.
* Exported so that test files can pass them to `MockPagesRepository` or the
* Payload stub without duplicating definitions.
*/
export const CONTRACT_PAGES_SEED: Page[] = [
{
id: "cp-1",
title: "About",
slug: "about",
hero: { heading: "About us" },
layout: [],
status: "published",
publishedAt: SEED_DATE,
seo: { title: "About — Site" },
createdAt: SEED_DATE,
updatedAt: SEED_DATE,
},
{
id: "cp-2",
title: "Draft Page",
slug: "draft-page",
hero: { heading: "Draft" },
layout: [],
status: "draft",
publishedAt: null,
seo: { title: "Draft — Site" },
createdAt: SEED_DATE,
updatedAt: SEED_DATE,
},
];
/**
* Contract for IPagesRepository.
*
* IPagesRepository is read-only (no createPage). Each `buildSubject`
* must return a repo pre-loaded with CONTRACT_PAGES_SEED (two pages:
* one published with slug "about", one draft with slug "draft-page").
*/
export const pagesRepositoryContract =
defineContractSuite<IPagesRepository>(
"IPagesRepository",
({ buildSubject }) => {
let repo: IPagesRepository;
beforeEach(async () => {
repo = await buildSubject();
});
// --- getPageBySlug ---
it("getPageBySlug returns the published page by slug", async () => {
const result = await repo.getPageBySlug("about");
expect(result).toBeDefined();
expect(result?.slug).toBe("about");
expect(result?.status).toBe("published");
expect(result?.id).toBeDefined();
});
it("getPageBySlug returns undefined for an unknown slug", async () => {
expect(await repo.getPageBySlug("no-such-page")).toBeUndefined();
});
// --- getPages ---
it("getPages with no filter returns all seeded pages", async () => {
const list = await repo.getPages();
expect(list.length).toBeGreaterThanOrEqual(2);
for (const page of list) {
expect(page.id).toBeDefined();
expect(page.slug).toBeDefined();
expect(["draft", "published"]).toContain(page.status);
}
});
it("getPages({ status: 'published' }) returns only published pages", async () => {
const published = await repo.getPages({ status: "published" });
expect(published.length).toBeGreaterThanOrEqual(1);
for (const page of published) {
expect(page.status).toBe("published");
}
});
it("getPages({ status: 'draft' }) returns only draft pages", async () => {
const drafts = await repo.getPages({ status: "draft" });
expect(drafts.length).toBeGreaterThanOrEqual(1);
for (const page of drafts) {
expect(page.status).toBe("draft");
}
});
},
);

View File

@@ -0,0 +1,38 @@
import { it, expect, beforeEach } from "vitest";
import { defineContractSuite } from "@repo/core-testing/contract";
import type { ISiteSettingsRepository } from "../application/repositories/site-settings-repository.interface.js";
/**
* Contract for ISiteSettingsRepository.
*
* SiteSettings is a singleton (Payload Global). The interface exposes
* only getSiteSettings(). The contract verifies the shape of the return value.
*/
export const siteSettingsRepositoryContract =
defineContractSuite<ISiteSettingsRepository>(
"ISiteSettingsRepository",
({ buildSubject }) => {
let repo: ISiteSettingsRepository;
beforeEach(async () => {
repo = await buildSubject();
});
// --- getSiteSettings ---
it("getSiteSettings returns an object with a non-empty siteName", async () => {
const settings = await repo.getSiteSettings();
expect(settings).toBeDefined();
expect(typeof settings.siteName).toBe("string");
expect(settings.siteName.length).toBeGreaterThan(0);
});
it("getSiteSettings siteDescription is string or undefined", async () => {
const settings = await repo.getSiteSettings();
expect(
settings.siteDescription === undefined ||
typeof settings.siteDescription === "string",
).toBe(true);
});
},
);