diff --git a/packages/blog/src/interface-adapters/controllers/articles.controller.test.ts b/packages/blog/src/interface-adapters/controllers/articles.controller.test.ts new file mode 100644 index 0000000..1e8940c --- /dev/null +++ b/packages/blog/src/interface-adapters/controllers/articles.controller.test.ts @@ -0,0 +1,71 @@ +import { beforeEach, describe, expect, it } from "vitest"; +import { blogContainer } from "@/di/container"; +import { BLOG_SYMBOLS } from "@/di/symbols"; +import { MockArticlesRepository } from "@/infrastructure/repositories/mock-articles.repository"; +import type { IArticlesRepository } from "@/application/repositories/articles-repository.interface"; +import { InputParseError } from "@/entities/errors"; +import { + createArticleController, + getArticlesController, + getArticleBySlugController, +} from "./articles.controller"; + +describe("articles controller", () => { + let repo: MockArticlesRepository; + + beforeEach(() => { + if (blogContainer.isBound(BLOG_SYMBOLS.IArticlesRepository)) { + blogContainer.unbind(BLOG_SYMBOLS.IArticlesRepository); + } + repo = new MockArticlesRepository(); + blogContainer + .bind(BLOG_SYMBOLS.IArticlesRepository) + .toConstantValue(repo); + }); + + describe("createArticleController", () => { + it("creates an article on valid input", async () => { + const result = await createArticleController({ + title: "Hello", + content: "body", + authorId: "u1", + }); + expect(result.title).toBe("Hello"); + }); + + it("throws InputParseError on missing title", async () => { + await expect( + createArticleController({ content: "body", authorId: "u1" }), + ).rejects.toBeInstanceOf(InputParseError); + }); + }); + + describe("getArticlesController", () => { + it("returns array on valid input", async () => { + const result = await getArticlesController({}); + expect(result).toEqual([]); + }); + + it("throws InputParseError on invalid input shape", async () => { + await expect( + getArticlesController({ limit: "not a number" } as unknown as Record< + string, + unknown + >), + ).rejects.toBeInstanceOf(InputParseError); + }); + }); + + describe("getArticleBySlugController", () => { + it("returns undefined for missing slug", async () => { + const result = await getArticleBySlugController({ slug: "nope" }); + expect(result).toBeUndefined(); + }); + + it("throws InputParseError on missing slug", async () => { + await expect( + getArticleBySlugController({} as { slug: string }), + ).rejects.toBeInstanceOf(InputParseError); + }); + }); +}); diff --git a/packages/blog/src/interface-adapters/controllers/articles.controller.ts b/packages/blog/src/interface-adapters/controllers/articles.controller.ts new file mode 100644 index 0000000..5d04e29 --- /dev/null +++ b/packages/blog/src/interface-adapters/controllers/articles.controller.ts @@ -0,0 +1,66 @@ +import { z } from "zod"; + +import { InputParseError } from "@/entities/errors"; +import type { Article } from "@/entities/article"; +import { blogContainer } from "@/di/container"; +import { BLOG_SYMBOLS } from "@/di/symbols"; +import type { IArticlesRepository } from "@/application/repositories/articles-repository.interface"; +import { getArticlesUseCase } from "@/application/use-cases/get-articles.use-case"; +import { createArticleUseCase } from "@/application/use-cases/create-article.use-case"; + +const createInputSchema = z.object({ + title: z.string().min(1).max(255), + content: z.unknown(), + authorId: z.string(), + slug: z.string().optional(), +}); + +const getInputSchema = z.object({ + status: z.string().optional(), + authorId: z.string().optional(), + limit: z.number().optional(), + offset: z.number().optional(), +}); + +const getBySlugInputSchema = z.object({ + slug: z.string().min(1), +}); + +export async function createArticleController( + input: Partial>, +): Promise
{ + const parsed = createInputSchema.safeParse(input); + if (!parsed.success) { + throw new InputParseError("Invalid create-article input", { + cause: parsed.error, + }); + } + return createArticleUseCase(parsed.data); +} + +export async function getArticlesController( + input: Partial>, +): Promise { + const parsed = getInputSchema.safeParse(input); + if (!parsed.success) { + throw new InputParseError("Invalid get-articles input", { + cause: parsed.error, + }); + } + return getArticlesUseCase(parsed.data); +} + +export async function getArticleBySlugController(input: { + slug: string; +}): Promise
{ + const parsed = getBySlugInputSchema.safeParse(input); + if (!parsed.success) { + throw new InputParseError("Invalid get-article-by-slug input", { + cause: parsed.error, + }); + } + const repo = blogContainer.get( + BLOG_SYMBOLS.IArticlesRepository, + ); + return repo.getArticleBySlug(parsed.data.slug); +}