Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 128 additions & 0 deletions apps/web/src/__tests__/api.test.tsx
Original file line number Diff line number Diff line change
@@ -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();
});
});
7 changes: 0 additions & 7 deletions apps/web/src/__tests__/app.test.ts

This file was deleted.

23 changes: 23 additions & 0 deletions docs/plan/0002-api-tests.md
Original file line number Diff line number Diff line change
@@ -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 |