Payload paginates by page, so floor(offset/limit)+1 alone returned the wrong window whenever offset % limit != 0 (B9). Fetch the straddled pages and slice the intra-page remainder. The test stub now honours page like real Payload (it silently ignored it before), and the contract pins aligned + non-aligned windows on mock and stub alike. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
219 lines
8.5 KiB
TypeScript
219 lines
8.5 KiB
TypeScript
import { it, expect, beforeEach, describe } from "vitest";
|
|
import { defineContractSuite } from "@repo/core-testing/contract";
|
|
import type { IArticlesRepository } from "../application/repositories/articles.repository.interface";
|
|
import { articleFactory } from "../__factories__/article.factory";
|
|
|
|
export const articlesRepositoryContract =
|
|
defineContractSuite<IArticlesRepository>(
|
|
"IArticlesRepository",
|
|
({ buildSubject, getTracer }) => {
|
|
let repo: IArticlesRepository;
|
|
|
|
beforeEach(async () => {
|
|
articleFactory.reset();
|
|
repo = await buildSubject();
|
|
});
|
|
|
|
// --- createArticle ---
|
|
|
|
it("createArticle returns an article with an id and the correct fields", async () => {
|
|
const seed = articleFactory.build({ title: "Hello World" });
|
|
const created = await repo.createArticle(seed);
|
|
// Implementations may assign their own id (e.g. Payload), so we only
|
|
// verify the id is a non-empty string and the other fields match.
|
|
expect(typeof created.id).toBe("string");
|
|
expect(created.id.length).toBeGreaterThan(0);
|
|
expect(created.title).toBe("Hello World");
|
|
expect(created.slug).toBe(seed.slug);
|
|
expect(created.status).toBe(seed.status);
|
|
expect(created.authorId).toBe(seed.authorId);
|
|
});
|
|
|
|
// --- getArticle ---
|
|
|
|
it("createArticle then getArticle returns it by the returned id", async () => {
|
|
const seed = articleFactory.build();
|
|
const created = await repo.createArticle(seed);
|
|
// Use the id returned by createArticle (Payload may differ from seed.id)
|
|
const result = await repo.getArticle(created.id);
|
|
expect(result).toBeDefined();
|
|
expect(result?.id).toBe(created.id);
|
|
expect(result?.slug).toBe(seed.slug);
|
|
});
|
|
|
|
it("getArticle returns undefined for missing id", async () => {
|
|
expect(await repo.getArticle("does-not-exist")).toBeUndefined();
|
|
});
|
|
|
|
// --- getArticleBySlug ---
|
|
|
|
it("createArticle then getArticleBySlug returns it by slug", async () => {
|
|
const seed = articleFactory.build({ slug: "my-slug" });
|
|
const created = await repo.createArticle(seed);
|
|
const result = await repo.getArticleBySlug("my-slug");
|
|
expect(result).toBeDefined();
|
|
expect(result?.id).toBe(created.id);
|
|
expect(result?.slug).toBe("my-slug");
|
|
});
|
|
|
|
it("getArticleBySlug returns undefined for missing slug", async () => {
|
|
expect(await repo.getArticleBySlug("does-not-exist")).toBeUndefined();
|
|
});
|
|
|
|
// --- getArticles ---
|
|
|
|
it("getArticles returns empty array when no articles", async () => {
|
|
const list = await repo.getArticles();
|
|
expect(list).toHaveLength(0);
|
|
});
|
|
|
|
it("getArticles returns all articles when no filter", async () => {
|
|
await repo.createArticle(articleFactory.build());
|
|
await repo.createArticle(articleFactory.build());
|
|
const list = await repo.getArticles();
|
|
expect(list).toHaveLength(2);
|
|
});
|
|
|
|
it("getArticles filters by status", async () => {
|
|
await repo.createArticle(articleFactory.build({ status: "draft" }));
|
|
await repo.createArticle(articleFactory.build({ status: "published" }));
|
|
const drafts = await repo.getArticles({ status: "draft" });
|
|
expect(drafts).toHaveLength(1);
|
|
expect(drafts[0]?.status).toBe("draft");
|
|
});
|
|
|
|
it("getArticles filters by authorId", async () => {
|
|
await repo.createArticle(
|
|
articleFactory.build({ authorId: "author-a" }),
|
|
);
|
|
await repo.createArticle(
|
|
articleFactory.build({ authorId: "author-b" }),
|
|
);
|
|
const result = await repo.getArticles({ authorId: "author-a" });
|
|
expect(result).toHaveLength(1);
|
|
expect(result[0]?.authorId).toBe("author-a");
|
|
});
|
|
|
|
it("getArticles returns the exact window for an aligned offset", async () => {
|
|
const created = [];
|
|
for (let i = 0; i < 5; i++) {
|
|
created.push(await repo.createArticle(articleFactory.build()));
|
|
}
|
|
const window = await repo.getArticles({ offset: 2, limit: 2 });
|
|
expect(window.map((a) => a.id)).toEqual([
|
|
created[2]?.id,
|
|
created[3]?.id,
|
|
]);
|
|
});
|
|
|
|
it("getArticles returns the exact window for a non-aligned offset", async () => {
|
|
const created = [];
|
|
for (let i = 0; i < 5; i++) {
|
|
created.push(await repo.createArticle(articleFactory.build()));
|
|
}
|
|
// offset 3 with limit 2 straddles two limit-sized pages
|
|
const window = await repo.getArticles({ offset: 3, limit: 2 });
|
|
expect(window.map((a) => a.id)).toEqual([
|
|
created[3]?.id,
|
|
created[4]?.id,
|
|
]);
|
|
});
|
|
|
|
it("getArticles non-aligned offset near the end returns only the remaining items", async () => {
|
|
const created = [];
|
|
for (let i = 0; i < 5; i++) {
|
|
created.push(await repo.createArticle(articleFactory.build()));
|
|
}
|
|
const window = await repo.getArticles({ offset: 4, limit: 3 });
|
|
expect(window.map((a) => a.id)).toEqual([created[4]?.id]);
|
|
});
|
|
|
|
it("getArticles offset past the end returns an empty array", async () => {
|
|
await repo.createArticle(articleFactory.build());
|
|
const window = await repo.getArticles({ offset: 7, limit: 3 });
|
|
expect(window).toEqual([]);
|
|
});
|
|
|
|
// --- updateArticle ---
|
|
|
|
it("updateArticle changes fields and returns updated article", async () => {
|
|
const seed = articleFactory.build({ status: "draft" });
|
|
const created = await repo.createArticle(seed);
|
|
// Use the returned id for the update lookup
|
|
const updated = await repo.updateArticle(created.id, {
|
|
status: "published",
|
|
});
|
|
expect(updated).toBeDefined();
|
|
expect(updated?.id).toBe(created.id);
|
|
expect(updated?.status).toBe("published");
|
|
});
|
|
|
|
it("updateArticle returns undefined for missing id", async () => {
|
|
const result = await repo.updateArticle("no-such-id", {
|
|
title: "new",
|
|
});
|
|
expect(result).toBeUndefined();
|
|
});
|
|
|
|
describe("span emission", () => {
|
|
it("getArticles emits articles.getArticles span with op=repository", async () => {
|
|
if (!getTracer) return;
|
|
const tracer = getTracer();
|
|
tracer.reset();
|
|
await repo.getArticles({ limit: 5 });
|
|
const span = tracer.findSpan("articles.getArticles");
|
|
expect(span).toBeDefined();
|
|
expect(span!.op).toBe("repository");
|
|
expect(span!.attributes.limit).toBe(5);
|
|
});
|
|
|
|
it("getArticle emits articles.getArticle span with id attribute", async () => {
|
|
if (!getTracer) return;
|
|
const tracer = getTracer();
|
|
tracer.reset();
|
|
await repo.getArticle("nonexistent");
|
|
const span = tracer.findSpan("articles.getArticle");
|
|
expect(span).toBeDefined();
|
|
expect(span!.op).toBe("repository");
|
|
expect(span!.attributes.id).toBe("nonexistent");
|
|
});
|
|
|
|
it("getArticleBySlug emits articles.getArticleBySlug span with slug attribute", async () => {
|
|
if (!getTracer) return;
|
|
const tracer = getTracer();
|
|
tracer.reset();
|
|
await repo.getArticleBySlug("nonexistent");
|
|
const span = tracer.findSpan("articles.getArticleBySlug");
|
|
expect(span).toBeDefined();
|
|
expect(span!.op).toBe("repository");
|
|
expect(span!.attributes.slug).toBe("nonexistent");
|
|
});
|
|
|
|
it("createArticle emits articles.createArticle span", async () => {
|
|
if (!getTracer) return;
|
|
const tracer = getTracer();
|
|
tracer.reset();
|
|
const seed = articleFactory.build();
|
|
await repo.createArticle(seed);
|
|
const span = tracer.findSpan("articles.createArticle");
|
|
expect(span).toBeDefined();
|
|
expect(span!.op).toBe("repository");
|
|
expect(span!.attributes.slug).toBe(seed.slug);
|
|
});
|
|
|
|
it("updateArticle emits articles.updateArticle span", async () => {
|
|
if (!getTracer) return;
|
|
const tracer = getTracer();
|
|
tracer.reset();
|
|
const seed = articleFactory.build();
|
|
const created = await repo.createArticle(seed);
|
|
await repo.updateArticle(created.id, { title: "Updated" });
|
|
const span = tracer.findSpan("articles.updateArticle");
|
|
expect(span).toBeDefined();
|
|
expect(span!.op).toBe("repository");
|
|
expect(span!.attributes.id).toBe(created.id);
|
|
});
|
|
});
|
|
},
|
|
);
|