diff --git a/apps/web/src/__tests__/api.test.tsx b/apps/web/src/__tests__/api.test.tsx new file mode 100644 index 0000000..b480c3d --- /dev/null +++ b/apps/web/src/__tests__/api.test.tsx @@ -0,0 +1,128 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const mockFrom = vi.fn(); +const mockSelect = vi.fn(() => ({ from: mockFrom })); + +const mockReturning = vi.fn(); +const mockValues = vi.fn(() => ({ returning: mockReturning })); +const mockInsert = vi.fn(() => ({ values: mockValues })); + +const mockWhere = vi.fn(); +const mockDelete = vi.fn(() => ({ where: mockWhere })); + +vi.mock("../db", () => ({ + getDb: () => ({ + select: mockSelect, + insert: mockInsert, + delete: mockDelete, + }), +})); + +import app from "../routes/api"; + +describe("GET /examples", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("returns 200 with HTML containing example items", async () => { + mockFrom.mockResolvedValueOnce([ + { id: "uuid-1", title: "Test Item 1", createdAt: new Date() }, + { id: "uuid-2", title: "Test Item 2", createdAt: new Date() }, + ]); + + const res = await app.request("/examples"); + + expect(res.status).toBe(200); + const html = await res.text(); + expect(html).toContain("Test Item 1"); + expect(html).toContain("Test Item 2"); + }); + + it("returns 200 with empty content when no examples exist", async () => { + mockFrom.mockResolvedValueOnce([]); + + const res = await app.request("/examples"); + + expect(res.status).toBe(200); + }); +}); + +describe("POST /examples", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("returns 200 with HTML partial on valid input", async () => { + mockReturning.mockResolvedValueOnce([ + { id: "uuid-new", title: "New Item", createdAt: new Date() }, + ]); + + const res = await app.request("/examples", { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: "title=New+Item", + }); + + expect(res.status).toBe(200); + const html = await res.text(); + expect(html).toContain("New Item"); + expect(mockInsert).toHaveBeenCalled(); + }); + + it("returns 400 when title is empty", async () => { + const res = await app.request("/examples", { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: "title=", + }); + + expect(res.status).toBe(400); + const text = await res.text(); + expect(text).toBe("Invalid input"); + expect(mockInsert).not.toHaveBeenCalled(); + }); + + it("returns 400 when title field is missing", async () => { + const res = await app.request("/examples", { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: "", + }); + + expect(res.status).toBe(400); + expect(mockInsert).not.toHaveBeenCalled(); + }); + + it("returns 500 when database returns no rows", async () => { + mockReturning.mockResolvedValueOnce([]); + + const res = await app.request("/examples", { + method: "POST", + headers: { "Content-Type": "application/x-www-form-urlencoded" }, + body: "title=Something", + }); + + expect(res.status).toBe(500); + const text = await res.text(); + expect(text).toBe("Failed to create"); + }); +}); + +describe("DELETE /examples/:id", () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + it("returns 200 on successful delete", async () => { + mockWhere.mockResolvedValueOnce(undefined); + + const res = await app.request("/examples/some-uuid-123", { + method: "DELETE", + }); + + expect(res.status).toBe(200); + expect(mockDelete).toHaveBeenCalled(); + expect(mockWhere).toHaveBeenCalled(); + }); +}); diff --git a/apps/web/src/__tests__/app.test.ts b/apps/web/src/__tests__/app.test.ts deleted file mode 100644 index 8954ceb..0000000 --- a/apps/web/src/__tests__/app.test.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { describe, expect, it } from "vitest"; - -describe("app", () => { - it("works", () => { - expect(true).toBe(true); - }); -}); diff --git a/docs/plan/0002-api-tests.md b/docs/plan/0002-api-tests.md new file mode 100644 index 0000000..1ef2a5e --- /dev/null +++ b/docs/plan/0002-api-tests.md @@ -0,0 +1,23 @@ +# 0002: APIルートのテスト追加 + +## 目的 + +placeholderテストを実際のAPIルートテストに置き換え、スターターテンプレートの品質を担保する。 + +## 方針 + +- Honoの `app.request()` でHTTPリクエストをシミュレート(サーバー起動不要) +- `vi.mock` で `getDb()` をモックし、DB接続なしでテスト実行 +- Drizzleのチェーンパターンをモックで再現 + +## テストケース + +| エンドポイント | ケース | 期待 | +|--------------|--------|------| +| GET /examples | アイテムあり | 200 + HTML | +| GET /examples | アイテムなし | 200 | +| POST /examples | 有効な入力 | 200 + HTML partial | +| POST /examples | タイトル空 | 400 | +| POST /examples | タイトルなし | 400 | +| POST /examples | DB挿入失敗 | 500 | +| DELETE /examples/:id | 正常削除 | 200 |