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,12 @@
import { describe } from "vitest";
import { MockPagesRepository } from "@/infrastructure/repositories/mock-pages.repository";
import {
pagesRepositoryContract,
CONTRACT_PAGES_SEED,
} from "@/__contracts__/pages-repository.contract";
describe("MockPagesRepository", () => {
// Pre-seed with the contract fixtures so the contract assertions find what
// they expect (an "about" published page and a "draft-page" draft page).
pagesRepositoryContract.run(() => new MockPagesRepository(CONTRACT_PAGES_SEED));
});

View File

@@ -6,22 +6,28 @@ import type { Page } from "../../entities/page";
const SEED_DATE = new Date("2026-01-01T00:00:00.000Z");
const DEFAULT_SEED: Page[] = [
{
id: "p1",
title: "About",
slug: "about",
hero: { heading: "About us" },
layout: [],
status: "published",
publishedAt: SEED_DATE,
seo: { title: "About — My App" },
createdAt: SEED_DATE,
updatedAt: SEED_DATE,
},
];
@injectable()
export class MockPagesRepository implements IPagesRepository {
private _pages: Page[] = [
{
id: "p1",
title: "About",
slug: "about",
hero: { heading: "About us" },
layout: [],
status: "published",
publishedAt: SEED_DATE,
seo: { title: "About — My App" },
createdAt: SEED_DATE,
updatedAt: SEED_DATE,
},
];
private _pages: Page[];
constructor(initialPages: Page[] = DEFAULT_SEED) {
this._pages = [...initialPages];
}
async getPageBySlug(slug: string): Promise<Page | undefined> {
return this._pages.find((p) => p.slug === slug);

View File

@@ -0,0 +1,7 @@
import { describe } from "vitest";
import { MockSiteSettingsRepository } from "@/infrastructure/repositories/mock-site-settings.repository";
import { siteSettingsRepositoryContract } from "@/__contracts__/site-settings-repository.contract";
describe("MockSiteSettingsRepository", () => {
siteSettingsRepositoryContract.run(() => new MockSiteSettingsRepository());
});

View File

@@ -0,0 +1,83 @@
import { describe, vi } from "vitest";
import { PayloadPagesRepository } from "@/infrastructure/repositories/payload-pages.repository";
import {
pagesRepositoryContract,
CONTRACT_PAGES_SEED,
} from "@/__contracts__/pages-repository.contract";
import { stubPayloadConfig } from "@repo/core-testing/payload/stub-config";
import type { Page } from "@/entities/page";
// ---------------------------------------------------------------------------
// In-memory Payload stub for pages (read-only collection)
// ---------------------------------------------------------------------------
function buildPayloadPagesStub(seed: Page[]) {
const store = new Map<string, Record<string, unknown>>(
seed.map((p) => [
p.id,
{
id: p.id,
title: p.title,
slug: p.slug,
hero: p.hero
? {
heading: p.hero.heading,
subheading: p.hero.subheading,
}
: null,
layout: p.layout,
status: p.status,
publishedAt: p.publishedAt ? p.publishedAt.toISOString() : null,
seo: p.seo ? { title: p.seo.title, description: p.seo.description } : null,
createdAt: p.createdAt.toISOString(),
updatedAt: p.updatedAt.toISOString(),
},
]),
);
return {
find: vi.fn(
async ({
where,
limit,
}: {
collection: string;
where?: { slug?: { equals: string }; status?: { equals: string } };
limit?: number;
page?: number;
overrideAccess?: boolean;
}) => {
let docs = Array.from(store.values());
if (where?.slug) {
docs = docs.filter((d) => d.slug === where.slug?.equals);
}
if (where?.status) {
docs = docs.filter((d) => d.status === where.status?.equals);
}
if (limit !== undefined) {
docs = docs.slice(0, limit);
}
return { docs };
},
),
};
}
vi.mock("payload", () => ({
getPayload: vi.fn(),
}));
// ---------------------------------------------------------------------------
// Contract suite
// ---------------------------------------------------------------------------
describe("PayloadPagesRepository", () => {
describe("contract", () => {
pagesRepositoryContract.run(async () => {
const stub = buildPayloadPagesStub(CONTRACT_PAGES_SEED);
const { getPayload } = await import("payload");
(getPayload as ReturnType<typeof vi.fn>).mockResolvedValue(stub);
return new PayloadPagesRepository(stubPayloadConfig);
});
});
});

View File

@@ -0,0 +1,40 @@
import { describe, vi } from "vitest";
import { PayloadSiteSettingsRepository } from "@/infrastructure/repositories/payload-site-settings.repository";
import { siteSettingsRepositoryContract } from "@/__contracts__/site-settings-repository.contract";
import { stubPayloadConfig } from "@repo/core-testing/payload/stub-config";
// ---------------------------------------------------------------------------
// In-memory Payload stub for site-settings (Global)
// ---------------------------------------------------------------------------
function buildSiteSettingsStub() {
const globalData: Record<string, unknown> = {
siteName: "Contract Site",
siteDescription: "A site for contract testing",
};
return {
findGlobal: vi.fn(async () => {
return { ...globalData };
}),
};
}
vi.mock("payload", () => ({
getPayload: vi.fn(),
}));
// ---------------------------------------------------------------------------
// Contract suite
// ---------------------------------------------------------------------------
describe("PayloadSiteSettingsRepository", () => {
describe("contract", () => {
siteSettingsRepositoryContract.run(async () => {
const stub = buildSiteSettingsStub();
const { getPayload } = await import("payload");
(getPayload as ReturnType<typeof vi.fn>).mockResolvedValue(stub);
return new PayloadSiteSettingsRepository(stubPayloadConfig);
});
});
});