diff --git a/TODO.md b/TODO.md index 45a1df6..3d4b49e 100644 --- a/TODO.md +++ b/TODO.md @@ -186,7 +186,7 @@ This file is the single source of truth for roadmap and delivery progress. ### Testing -- [ ] [P1] Unit tests for content schemas and service logic +- [x] [P1] Unit tests for content schemas and service logic - [ ] [P1] Component tests for admin forms (pages/media/navigation) - [ ] [P1] Integration tests for owner invariant and hidden support-user protection - [ ] [P1] Integration tests for registration allow/deny behavior @@ -280,6 +280,7 @@ This file is the single source of truth for roadmap and delivery progress. - [2026-02-12] Announcements/news baseline added: admin `/announcements` + `/news` management screens and public announcement rendering slots (`global_top`, `homepage`). - [2026-02-12] Public news routes now exist at `/news` and `/news/:slug` (detail restricted to published posts). - [2026-02-12] Added `e2e/happy-paths.pw.ts` covering admin login, page publish/public rendering, announcement rendering, media upload, and commission status transition. +- [2026-02-12] Expanded unit coverage for content/domain schemas and post service behavior (`packages/content/src/domain-schemas.test.ts`, `packages/db/src/posts.test.ts`). ## How We Use This File diff --git a/packages/content/src/domain-schemas.test.ts b/packages/content/src/domain-schemas.test.ts new file mode 100644 index 0000000..d084c15 --- /dev/null +++ b/packages/content/src/domain-schemas.test.ts @@ -0,0 +1,67 @@ +import { describe, expect, it } from "vitest" + +import { + createAnnouncementInputSchema, + createCommissionInputSchema, + createCustomerInputSchema, + createNavigationMenuInputSchema, + createPageInputSchema, + updateCommissionStatusInputSchema, + updateNavigationItemInputSchema, +} from "./index" + +describe("domain schemas", () => { + it("applies announcement defaults", () => { + const result = createAnnouncementInputSchema.parse({ + title: "Notice", + message: "Open slots", + }) + + expect(result.placement).toBe("global_top") + expect(result.priority).toBe(100) + expect(result.isVisible).toBe(true) + }) + + it("validates customer and commission payloads", () => { + const customer = createCustomerInputSchema.safeParse({ + name: "Ada", + email: "ada@example.com", + }) + const commission = createCommissionInputSchema.safeParse({ + title: "Portrait", + status: "new", + }) + + expect(customer.success).toBe(true) + expect(commission.success).toBe(true) + }) + + it("rejects invalid commission status updates", () => { + const result = updateCommissionStatusInputSchema.safeParse({ + id: "550e8400-e29b-41d4-a716-446655440000", + status: "invalid", + }) + + expect(result.success).toBe(false) + }) + + it("validates page and navigation payload constraints", () => { + const page = createPageInputSchema.safeParse({ + title: "About", + slug: "about", + content: "About page", + }) + const menu = createNavigationMenuInputSchema.safeParse({ + name: "Primary", + slug: "primary", + }) + const navUpdate = updateNavigationItemInputSchema.safeParse({ + id: "550e8400-e29b-41d4-a716-446655440000", + sortOrder: -1, + }) + + expect(page.success).toBe(true) + expect(menu.success).toBe(true) + expect(navUpdate.success).toBe(false) + }) +}) diff --git a/packages/db/src/posts.test.ts b/packages/db/src/posts.test.ts new file mode 100644 index 0000000..1ce93b4 --- /dev/null +++ b/packages/db/src/posts.test.ts @@ -0,0 +1,75 @@ +import { beforeEach, describe, expect, it, vi } from "vitest" + +const { mockDb } = vi.hoisted(() => ({ + mockDb: { + post: { + create: vi.fn(), + findMany: vi.fn(), + findUnique: vi.fn(), + update: vi.fn(), + delete: vi.fn(), + }, + }, +})) + +vi.mock("./client", () => ({ + db: mockDb, +})) + +import { createPost, getPostBySlug, listPosts, updatePost } from "./posts" + +describe("posts service", () => { + beforeEach(() => { + for (const fn of Object.values(mockDb.post)) { + if (typeof fn === "function") { + fn.mockReset() + } + } + }) + + it("lists posts ordered by update date desc", async () => { + mockDb.post.findMany.mockResolvedValue([]) + + await listPosts() + + expect(mockDb.post.findMany).toHaveBeenCalledTimes(1) + expect(mockDb.post.findMany.mock.calls[0]?.[0]).toMatchObject({ + orderBy: { + updatedAt: "desc", + }, + }) + }) + + it("parses create and update payloads through crud service", async () => { + mockDb.post.create.mockResolvedValue({ id: "post-1" }) + mockDb.post.findUnique.mockResolvedValue({ id: "550e8400-e29b-41d4-a716-446655440000" }) + mockDb.post.update.mockResolvedValue({ id: "post-1" }) + + await createPost({ + title: "A title", + slug: "a-title", + body: "Body", + status: "draft", + }) + + await updatePost("550e8400-e29b-41d4-a716-446655440000", { + title: "Updated", + }) + + expect(mockDb.post.create).toHaveBeenCalledTimes(1) + expect(mockDb.post.update).toHaveBeenCalledTimes(1) + }) + + it("finds posts by slug", async () => { + mockDb.post.findUnique.mockResolvedValue({ id: "post-1", slug: "hello" }) + + await getPostBySlug("hello") + + expect(mockDb.post.findUnique).toHaveBeenCalledTimes(1) + expect(mockDb.post.findUnique).toHaveBeenCalledWith({ + where: { + slug: "hello", + }, + }) + }) +})