Repository: kubotama/linkpage
Pull request #101: test: ブックマークを追加するAPIのオンメモリDBテストを実装
Author: login: kubotama
Created: 2025-05-13T13:32:15Z | Merged: 2025-05-13T14:15:19Z

誤ってクローズしていないかを確認するためにテストを追加した。


Diff:
diff --git a/package.json b/package.json
index aa931a06..924df20b 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "linkpage",
-  "version": "0.9.0",
+  "version": "0.9.1",
   "private": true,
   "scripts": {
     "dev": "next dev --turbopack",
diff --git a/src/app/api/bookmark/add/route-onmemory.test.ts b/src/app/api/bookmark/add/route-onmemory.test.ts
new file mode 100644
index 00000000..4e0a47a8
--- /dev/null
+++ b/src/app/api/bookmark/add/route-onmemory.test.ts
@@ -0,0 +1,155 @@
+import "@testing-library/jest-dom";
+import ActualDatabase from "better-sqlite3"; // Import the actual library
+
+import { Bookmark, createBookmark } from "../../../types/Bookmark";
+import { getDb } from "../database";
+import { POST } from "./route";
+
+// We will mock getDb to return our in-memory instance.
+// The actual getDb function is simple, but mocking allows us to inject the in-memory DB.
+jest.mock("../database");
+
+let inMemoryDbInstance: ActualDatabase.Database;
+
+const API_URL = "http://localhost:3000/api/bookmark/add";
+
+function createPostRequest(body: string): Request {
+  return new Request(API_URL, {
+    method: "POST",
+    headers: {
+      "Content-Type": "application/json",
+    },
+    body: body,
+  });
+}
+describe("ブックマーク追加APIのテスト (オンメモリDB)", () => {
+  beforeEach(() => {
+    // Reset mocks before each test
+    jest.resetAllMocks();
+
+    // Create a new in-memory database for each test
+    inMemoryDbInstance = new ActualDatabase(":memory:");
+    // Initialize the schema (same as in the original database.ts)
+    inMemoryDbInstance.exec(`
+      CREATE TABLE IF NOT EXISTS bookmarks (
+        id INTEGER PRIMARY KEY AUTOINCREMENT,
+        url TEXT NOT NULL UNIQUE,
+        title TEXT NOT NULL
+      )
+    `);
+
+    // Configure the getDb mock to return this instance
+    (getDb as jest.Mock).mockReturnValue(inMemoryDbInstance);
+  });
+
+  afterEach(() => {
+    if (inMemoryDbInstance) {
+      inMemoryDbInstance.close();
+    }
+  });
+
+  // --- POST Tests ---
+
+  // Utility function to add a bookmark and verify the response
+  async function addBookmarkAndVerify(bookmark: Bookmark) {
+    const response = await POST(createPostRequest(JSON.stringify(bookmark)));
+
+    expect(response.status).toBe(200);
+    const json = await response.json();
+    expect(json).toEqual({
+      id: expect.any(Number),
+      url: bookmark.url,
+      title: bookmark.title,
+    });
+
+    // Verify data in the in-memory database
+    const stmt = inMemoryDbInstance.prepare(
+      "SELECT url, title FROM bookmarks WHERE id = ?"
+    );
+    const dbData = stmt.get(json.id); // json.id comes from the response (lastInsertRowid)
+    expect(dbData).toEqual({
+      url: bookmark.url,
+      title: bookmark.title,
+    });
+  }
+
+  it("POST: ブックマークのデータが追加できる", async () => {
+    await addBookmarkAndVerify(
+      createBookmark({
+        url: "https://github.com/kubotama/linkpage",
+        title: "kubotama/linkpage",
+      })
+    );
+  });
+
+  it("POST: ブックマークのデータを2回、追加できる", async () => {
+    await addBookmarkAndVerify(
+      createBookmark({
+        url: "https://github.com/kubotama/linkpage",
+        title: "kubotama/linkpage",
+      })
+    );
+
+    await addBookmarkAndVerify(
+      createBookmark({
+        url: "https://www.google.com/",
+        title: "Google",
+      })
+    );
+  });
+
+  it("POST: 不正なJSONデータの場合はエラーを返す", async () => {
+    const response = await POST(createPostRequest("invalid json"));
+
+    expect(response.status).toBe(500);
+    const text = await response.text();
+    expect(text).toMatch(/Unexpected token|JSON.parse/i); // Check for JSON parsing error message
+  });
+
+  it("POST: 重複したURLのブックマーク追加時にデータベースエラーを返す", async () => {
+    const initialBookmark = {
+      url: "https://example.com",
+      title: "Example",
+    };
+    // Pre-populate the database with one entry
+    const insertStmt = inMemoryDbInstance.prepare(
+      "INSERT INTO bookmarks (url, title) VALUES (?, ?)"
+    );
+    insertStmt.run(initialBookmark.url, initialBookmark.title);
+
+    const bookmark: Bookmark = createBookmark({
+      url: initialBookmark.url, // Same URL
+      title: "Another Title For Same URL",
+    });
+
+    const response = await POST(createPostRequest(JSON.stringify(bookmark)));
+
+    expect(response.status).toBe(500);
+    const text = await response.text();
+    expect(text).toMatch(/UNIQUE constraint failed: bookmarks.url/i);
+  });
+
+  it("POST: URLが空文字の場合にエラーを返す", async () => {
+    const bookmark: Bookmark = createBookmark({
+      title: "Example",
+    });
+
+    const response = await POST(createPostRequest(JSON.stringify(bookmark)));
+
+    expect(response.status).toBe(400);
+    const text = await response.text();
+    expect(text).toEqual("URL cannot be empty");
+  });
+
+  it("POST: タイトルが空文字の場合にエラーを返す", async () => {
+    const bookmark: Bookmark = createBookmark({
+      url: "https://example.com",
+    });
+
+    const response = await POST(createPostRequest(JSON.stringify(bookmark)));
+
+    expect(response.status).toBe(400);
+    const text = await response.text();
+    expect(text).toEqual("Title cannot be empty");
+  });
+});


Reviews:
user: login: gemini-code-assist[bot]
body: Hello @kubotama, I'm Gemini Code Assist[^1]! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

## Summary of Changes
This pull request adds a test suite for the bookmark addition API using an in-memory database. The tests cover various scenarios, including successful bookmark creation, handling duplicate URLs, invalid JSON data, and empty URL/title fields. The purpose is to ensure that the API functions correctly and handles edge cases appropriately, and to prevent accidental closure of the related issue.

### Highlights
* **Test Implementation**: Implements a new test file `src/app/api/bookmark/add/route-onmemory.test.ts` to test the bookmark addition API.
* **In-Memory Database**: Utilizes an in-memory SQLite database for testing, ensuring isolation and reproducibility of tests.
* **Comprehensive Test Coverage**: Covers scenarios such as successful bookmark creation, duplicate URL handling, invalid JSON payloads, and missing URL/title fields.

### Changelog
* **src/app/api/bookmark/add/route-onmemory.test.ts**
  * Added a new test file to verify the functionality of the bookmark addition API.
  * Configured an in-memory SQLite database for isolated testing.
  * Implemented tests for successful bookmark creation, including verifying data persistence.
  * Added tests to handle duplicate URL entries, ensuring database integrity.
  * Included tests for invalid JSON payloads, validating error responses.
  * Implemented tests to handle missing URL or title fields, ensuring proper validation.


<details>

<summary><b>Using Gemini Code Assist</b></summary>
<br>

The full guide for Gemini Code Assist can be found on our [documentation page](https://developers.google.com/gemini-code-assist/docs/review-github-code), here are some quick tips.

<b>Invoking Gemini</b>

You can request assistance from Gemini at any point in your pull request via creating an issue comment (i.e. comment on the pull request page) using either `/gemini <command>` or `@gemini-code-assist <command>`. Below is a summary of the supported commands.

Feature | Command | Description
--- | --- | ---
Code Review | `/gemini review` | Performs a code review for the current pull request in its current state.
Pull Request Summary | `/gemini summary` | Provides a summary of the current pull request in its current state.
Comment | @gemini-code-assist | Responds in comments when explicitly tagged, both in issue comments and review comments.
Help | `/gemini help` | Displays a list of available commands.

<b>Customization</b>

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a `.gemini/` folder in the base of the repository. Detailed instructions can be found [here](https://developers.google.com/gemini-code-assist/docs/customize-gemini-behavior-github).

<b>Limitations & Feedback</b>

Gemini Code Assist is currently in preview and may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with :thumbsup: and :thumbsdown: on @gemini-code-assist comments to provide feedback.

</details>

<b>You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the [Gemini Code Assist IDE Extension](https://cloud.google.com/products/gemini/code-assist).</b>


[^1]: Review the [Privacy Notices](https://policies.google.com/privacy), [Generative AI Prohibited Use Policy](https://policies.google.com/terms/generative-ai/use-policy), [Terms of Service](https://policies.google.com/terms), and learn how to configure Gemini Code Assist in GitHub [here](https://developers.google.com/gemini-code-assist/docs/review-github-code). Gemini can make mistakes, so double check it and [use code with caution](https://support.google.com/legal/answer/13505487).

---

> In memory's realm,
> A database takes its form,
> Tests ensure it's sound.


state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T13:32:25Z
user: login: gemini-code-assist[bot]
body: ## Code Review
ブックマーク追加APIのオンメモリDBテストが実装され、誤ってクローズしていないかを確認するテストが追加されました。全体的に良く書けていますが、いくつかの改善点があります。

### Summary of Findings
* **テストケースタイトルの具体性**: テストケースのタイトルは、具体的に何を確認しているかを明確にすることが重要です。より詳細な説明にすることで、テストの意図が伝わりやすくなります。
* **テストコードの共通化**: 同じようなテストコードが繰り返されています。共通化することで、コードの可読性と保守性が向上します。
* **エラーメッセージの具体的な検証**: エラーメッセージの検証は、具体的なエラー内容を確認することが重要です。`toMatch` を使用する代わりに、`toEqual` で具体的なエラーメッセージを検証することで、テストの信頼性が向上します。

### Merge Readiness
全体的に見て、このプルリクエストはよくできていますが、上記の改善点を考慮することで、コードの品質がさらに向上します。これらの点が修正されれば、マージの準備が整うでしょう。私はプルリクエストを直接承認することはできませんので、他のレビュアーにもレビューと承認を依頼してください。
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T13:32:38Z
user: login: kubotama
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-13T13:40:24Z
user: login: kubotama
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-13T13:40:39Z
user: login: kubotama
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-13T13:41:58Z
user: login: gemini-code-assist[bot]
body: ## Code Review
ブックマーク追加APIのオンメモリDBテストが実装され、誤ってクローズしていないかを確認するテストが追加されました。全体的に良い出来ですが、いくつかの改善点があります。

### Summary of Findings
* **devDependencies**: `ActualDatabase`をdevDependenciesに移動することを検討してください。
* **Requestオブジェクトの再利用**: 各テストケースで同じRequestオブジェクトを生成していますが、ユーティリティ関数に移動して再利用することを検討してください。

### Merge Readiness
全体的にテストはよく書かれていますが、上記の提案を考慮して、より堅牢で保守しやすいテストスイートにすることを推奨します。特に重要な問題はないため、他のレビュアーの承認が得られればマージ可能です。私は承認する権限を持っていません。
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T13:42:53Z
user: login: kubotama
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-13T13:50:32Z
user: login: gemini-code-assist[bot]
body: ## Code Review
ブックマーク追加APIのオンメモリDBテストが実装され、誤ってクローズしていないかを確認するテストが追加されました。全体的に良い出来ですが、いくつかの改善点があります。

### Summary of Findings
* **devDependencies**: `ActualDatabase`をdevDependenciesに移動することを検討してください。
* **変数のスコープ**: `API_URL`変数をdescribeブロック内で定義することを検討してください。
* **レスポンスの検証**: `response.ok`を検証することを検討してください。
* **エラーメッセージの検証**: エラーメッセージの一部が含まれていることを確認する方が、より良いアプローチかもしれません。

### Merge Readiness
全体的に見て、このプルリクエストはよくできていますが、上記の改善点を考慮に入れることをお勧めします。特に、エラーメッセージの検証に関するコメントは重要です。これらの点が修正されれば、プルリクエストはマージの準備が整うでしょう。私はプルリクエストを直接承認することはできませんので、他のレビュアーにもレビューと承認を依頼してください。
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-13T13:59:47Z
user: login: kubotama
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-13T14:03:18Z
user: login: kubotama
state: COMMENTED
author_association: OWNER
submitted_at: 2025-05-13T14:03:46Z

Review comments:
diff_hunk: @@ -0,0 +1,230 @@
+import "@testing-library/jest-dom";
+import ActualDatabase from "better-sqlite3"; // Import the actual library
+
+import { Bookmark, createBookmark } from "../../../types/Bookmark";
+import { getDb } from "../database";
+import { POST } from "./route";
+
+// We will mock getDb to return our in-memory instance.
+// The actual getDb function is simple, but mocking allows us to inject the in-memory DB.
+jest.mock("../database");
+
+let inMemoryDbInstance: ActualDatabase.Database;
+
+describe("ブックマーク追加APIのテスト (オンメモリDB)", () => {
+  beforeEach(() => {
+    // Reset mocks before each test
+    jest.resetAllMocks();
+
+    // Create a new in-memory database for each test
+    inMemoryDbInstance = new ActualDatabase(":memory:");
+    // Initialize the schema (same as in the original database.ts)
+    inMemoryDbInstance.exec(`
+      CREATE TABLE IF NOT EXISTS bookmarks (
+        id INTEGER PRIMARY KEY AUTOINCREMENT,
+        url TEXT NOT NULL UNIQUE,
+        title TEXT NOT NULL
+      )
+    `);
+
+    // Configure the getDb mock to return this instance
+    (getDb as jest.Mock).mockReturnValue(inMemoryDbInstance);
+  });
+
+  afterEach(() => {
+    if (inMemoryDbInstance) {
+      inMemoryDbInstance.close();
+    }
+  });
+
+  // --- POST Tests ---
+
+  it("POST: ブックマークのデータが追加できる", async () => {
path: src/app/api/bookmark/add/route-onmemory.test.ts
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

テストケースのタイトルは、具体的に何を確認しているかを明確にすることが重要です。例えば、「URLとタイトルが正しくデータベースに保存されること」のように、より詳細な説明にすることで、テストの意図が伝わりやすくなります。

```suggestion
it("POST: URLとタイトルが正しくデータベースに保存されること", async () => {
```
created_at: 2025-05-13T13:32:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,230 @@
+import "@testing-library/jest-dom";
+import ActualDatabase from "better-sqlite3"; // Import the actual library
+
+import { Bookmark, createBookmark } from "../../../types/Bookmark";
+import { getDb } from "../database";
+import { POST } from "./route";
+
+// We will mock getDb to return our in-memory instance.
+// The actual getDb function is simple, but mocking allows us to inject the in-memory DB.
+jest.mock("../database");
+
+let inMemoryDbInstance: ActualDatabase.Database;
+
+describe("ブックマーク追加APIのテスト (オンメモリDB)", () => {
+  beforeEach(() => {
+    // Reset mocks before each test
+    jest.resetAllMocks();
+
+    // Create a new in-memory database for each test
+    inMemoryDbInstance = new ActualDatabase(":memory:");
+    // Initialize the schema (same as in the original database.ts)
+    inMemoryDbInstance.exec(`
+      CREATE TABLE IF NOT EXISTS bookmarks (
+        id INTEGER PRIMARY KEY AUTOINCREMENT,
+        url TEXT NOT NULL UNIQUE,
+        title TEXT NOT NULL
+      )
+    `);
+
+    // Configure the getDb mock to return this instance
+    (getDb as jest.Mock).mockReturnValue(inMemoryDbInstance);
+  });
+
+  afterEach(() => {
+    if (inMemoryDbInstance) {
+      inMemoryDbInstance.close();
+    }
+  });
+
+  // --- POST Tests ---
+
+  it("POST: ブックマークのデータが追加できる", async () => {
+    const bookmark: Bookmark = createBookmark({
+      url: "https://github.com/kubotama/linkpage",
+      title: "kubotama/linkpage",
+    });
+    const response = await POST(
+      new Request("http://localhost:3000/api/bookmark/add", {
+        method: "POST",
+        headers: {
+          "Content-Type": "application/json",
+        },
+        body: JSON.stringify(bookmark),
+      })
+    );
+
+    expect(response.status).toBe(200);
+    const json = await response.json();
+    expect(json).toEqual({
+      id: expect.any(Number),
+      url: "https://github.com/kubotama/linkpage",
+      title: "kubotama/linkpage",
+    });
+
+    // Verify data in the in-memory database
+    const stmt = inMemoryDbInstance.prepare(
+      "SELECT url, title FROM bookmarks WHERE id = ?"
+    );
+    const dbData = stmt.get(json.id); // json.id comes from the response (lastInsertRowid)
+    expect(dbData).toEqual({
+      url: bookmark.url,
+      title: bookmark.title,
+    });
+  });
+
+  it("POST: ブックマークのデータを2回、追加できる", async () => {
+    // 1回めの追加
+    const bookmark1: Bookmark = createBookmark({
+      url: "https://github.com/kubotama/linkpage",
+      title: "kubotama/linkpage",
+    });
+    const response1 = await POST(
+      new Request("http://localhost:3000/api/bookmark/add", {
+        method: "POST",
+        headers: {
+          "Content-Type": "application/json",
+        },
+        body: JSON.stringify(bookmark1),
+      })
+    );
+
+    expect(response1.status).toBe(200);
+    const json1 = await response1.json();
+    expect(json1).toEqual({
+      id: expect.any(Number),
+      url: "https://github.com/kubotama/linkpage",
+      title: "kubotama/linkpage",
+    });
+
+    // Verify data in the in-memory database
+    const stmt1 = inMemoryDbInstance.prepare(
+      "SELECT url, title FROM bookmarks WHERE id = ?"
+    );
+    const dbData1 = stmt1.get(json1.id); // json.id comes from the response (lastInsertRowid)
+    expect(dbData1).toEqual({
+      url: bookmark1.url,
+      title: bookmark1.title,
+    });
+
+    // 2回めの追加
+    const bookmark2: Bookmark = createBookmark({
+      url: "https://www.google.com/",
+      title: "Google",
+    });
+    const response2 = await POST(
+      new Request("http://localhost:3000/api/bookmark/add", {
+        method: "POST",
+        headers: {
+          "Content-Type": "application/json",
+        },
+        body: JSON.stringify(bookmark2),
+      })
+    );
+
+    expect(response2.status).toBe(200);
+    const json2 = await response2.json();
+    expect(json2).toEqual({
+      id: expect.any(Number),
+      url: "https://www.google.com/",
+      title: "Google",
+    });
+
+    // Verify data in the in-memory database
+    const stmt2 = inMemoryDbInstance.prepare(
+      "SELECT url, title FROM bookmarks WHERE id = ?"
+    );
+    const dbData2 = stmt2.get(json2.id); // json.id comes from the response (lastInsertRowid)
+    expect(dbData2).toEqual({
+      url: bookmark2.url,
+      title: bookmark2.title,
+    });
+  });
path: src/app/api/bookmark/add/route-onmemory.test.ts
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

同じようなテストコードが繰り返されています。共通化することで、コードの可読性と保守性が向上します。例えば、ブックマークの追加と検証を共通の関数にまとめることができます。

```typescript
// Utility function to add a bookmark and verify the response
async function addBookmarkAndVerify(bookmark: Bookmark) {
  const response = await POST(
    new Request("http://localhost:3000/api/bookmark/add", {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
      },
      body: JSON.stringify(bookmark),
    })
  );

  expect(response.status).toBe(200);
  const json = await response.json();
  expect(json).toEqual({
    id: expect.any(Number),
    url: bookmark.url,
    title: bookmark.title,
  });

  // Verify data in the in-memory database
  const stmt = inMemoryDbInstance.prepare(
    "SELECT url, title FROM bookmarks WHERE id = ?"
  );
  const dbData = stmt.get(json.id); // json.id comes from the response (lastInsertRowid)
  expect(dbData).toEqual({
    url: bookmark.url,
    title: bookmark.title,
  });
}

it("POST: ブックマークのデータが追加できる", async () => {
  const bookmark: Bookmark = createBookmark({
    url: "https://github.com/kubotama/linkpage",
    title: "kubotama/linkpage",
  });
  await addBookmarkAndVerify(bookmark);
});

it("POST: ブックマークのデータを2回、追加できる", async () => {
  // 1回めの追加
  const bookmark1: Bookmark = createBookmark({
    url: "https://github.com/kubotama/linkpage",
    title: "kubotama/linkpage",
  });
  await addBookmarkAndVerify(bookmark1);

  // 2回めの追加
  const bookmark2: Bookmark = createBookmark({
    url: "https://www.google.com/",
    title: "Google",
  });
  await addBookmarkAndVerify(bookmark2);
});
```
created_at: 2025-05-13T13:32:37Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,230 @@
+import "@testing-library/jest-dom";
+import ActualDatabase from "better-sqlite3"; // Import the actual library
+
+import { Bookmark, createBookmark } from "../../../types/Bookmark";
+import { getDb } from "../database";
+import { POST } from "./route";
+
+// We will mock getDb to return our in-memory instance.
+// The actual getDb function is simple, but mocking allows us to inject the in-memory DB.
+jest.mock("../database");
+
+let inMemoryDbInstance: ActualDatabase.Database;
+
+describe("ブックマーク追加APIのテスト (オンメモリDB)", () => {
+  beforeEach(() => {
+    // Reset mocks before each test
+    jest.resetAllMocks();
+
+    // Create a new in-memory database for each test
+    inMemoryDbInstance = new ActualDatabase(":memory:");
+    // Initialize the schema (same as in the original database.ts)
+    inMemoryDbInstance.exec(`
+      CREATE TABLE IF NOT EXISTS bookmarks (
+        id INTEGER PRIMARY KEY AUTOINCREMENT,
+        url TEXT NOT NULL UNIQUE,
+        title TEXT NOT NULL
+      )
+    `);
+
+    // Configure the getDb mock to return this instance
+    (getDb as jest.Mock).mockReturnValue(inMemoryDbInstance);
+  });
+
+  afterEach(() => {
+    if (inMemoryDbInstance) {
+      inMemoryDbInstance.close();
+    }
+  });
+
+  // --- POST Tests ---
+
+  it("POST: ブックマークのデータが追加できる", async () => {
+    const bookmark: Bookmark = createBookmark({
+      url: "https://github.com/kubotama/linkpage",
+      title: "kubotama/linkpage",
+    });
+    const response = await POST(
+      new Request("http://localhost:3000/api/bookmark/add", {
+        method: "POST",
+        headers: {
+          "Content-Type": "application/json",
+        },
+        body: JSON.stringify(bookmark),
+      })
+    );
+
+    expect(response.status).toBe(200);
+    const json = await response.json();
+    expect(json).toEqual({
+      id: expect.any(Number),
+      url: "https://github.com/kubotama/linkpage",
+      title: "kubotama/linkpage",
+    });
+
+    // Verify data in the in-memory database
+    const stmt = inMemoryDbInstance.prepare(
+      "SELECT url, title FROM bookmarks WHERE id = ?"
+    );
+    const dbData = stmt.get(json.id); // json.id comes from the response (lastInsertRowid)
+    expect(dbData).toEqual({
+      url: bookmark.url,
+      title: bookmark.title,
+    });
+  });
+
+  it("POST: ブックマークのデータを2回、追加できる", async () => {
+    // 1回めの追加
+    const bookmark1: Bookmark = createBookmark({
+      url: "https://github.com/kubotama/linkpage",
+      title: "kubotama/linkpage",
+    });
+    const response1 = await POST(
+      new Request("http://localhost:3000/api/bookmark/add", {
+        method: "POST",
+        headers: {
+          "Content-Type": "application/json",
+        },
+        body: JSON.stringify(bookmark1),
+      })
+    );
+
+    expect(response1.status).toBe(200);
+    const json1 = await response1.json();
+    expect(json1).toEqual({
+      id: expect.any(Number),
+      url: "https://github.com/kubotama/linkpage",
+      title: "kubotama/linkpage",
+    });
+
+    // Verify data in the in-memory database
+    const stmt1 = inMemoryDbInstance.prepare(
+      "SELECT url, title FROM bookmarks WHERE id = ?"
+    );
+    const dbData1 = stmt1.get(json1.id); // json.id comes from the response (lastInsertRowid)
+    expect(dbData1).toEqual({
+      url: bookmark1.url,
+      title: bookmark1.title,
+    });
+
+    // 2回めの追加
+    const bookmark2: Bookmark = createBookmark({
+      url: "https://www.google.com/",
+      title: "Google",
+    });
+    const response2 = await POST(
+      new Request("http://localhost:3000/api/bookmark/add", {
+        method: "POST",
+        headers: {
+          "Content-Type": "application/json",
+        },
+        body: JSON.stringify(bookmark2),
+      })
+    );
+
+    expect(response2.status).toBe(200);
+    const json2 = await response2.json();
+    expect(json2).toEqual({
+      id: expect.any(Number),
+      url: "https://www.google.com/",
+      title: "Google",
+    });
+
+    // Verify data in the in-memory database
+    const stmt2 = inMemoryDbInstance.prepare(
+      "SELECT url, title FROM bookmarks WHERE id = ?"
+    );
+    const dbData2 = stmt2.get(json2.id); // json.id comes from the response (lastInsertRowid)
+    expect(dbData2).toEqual({
+      url: bookmark2.url,
+      title: bookmark2.title,
+    });
+  });
+
+  it("POST: 不正なJSONデータの場合はエラーを返す", async () => {
+    const response = await POST(
+      new Request("http://localhost:3000/api/bookmark/add", {
+        method: "POST",
+        headers: {
+          "Content-Type": "application/json",
+        },
+        body: "invalid json",
+      })
+    );
+
+    expect(response.status).toBe(500);
+    const text = await response.text();
+    expect(text).toMatch(/Unexpected token|JSON.parse/i); // Check for JSON parsing error message
+  });
+
+  it("POST: 重複したURLのブックマーク追加時にデータベースエラーを返す", async () => {
+    const initialBookmark = {
+      url: "https://example.com",
+      title: "Example",
+    };
+    // Pre-populate the database with one entry
+    const insertStmt = inMemoryDbInstance.prepare(
+      "INSERT INTO bookmarks (url, title) VALUES (?, ?)"
+    );
+    insertStmt.run(initialBookmark.url, initialBookmark.title);
+
+    const bookmark: Bookmark = createBookmark({
+      url: initialBookmark.url, // Same URL
+      title: "Another Title For Same URL",
+    });
+
+    const response = await POST(
+      new Request("http://localhost:3000/api/bookmark/add", {
+        method: "POST",
+        headers: {
+          "Content-Type": "application/json",
+        },
+        body: JSON.stringify(bookmark),
+      })
+    );
+
+    expect(response.status).toBe(500);
+    const text = await response.text();
+    expect(text).toMatch(/UNIQUE constraint failed: bookmarks.url/i);
+  });
+
+  it("POST: URLが空文字の場合にエラーを返す", async () => {
+    const bookmark: Bookmark = createBookmark({
+      title: "Example",
+    });
+
+    const response = await POST(
+      new Request("http://localhost:3000/api/bookmark/add", {
+        method: "POST",
+        headers: {
+          "Content-Type": "application/json",
+        },
+        body: JSON.stringify(bookmark),
+      })
+    );
+
+    expect(response.status).toBe(400);
+    const text = await response.text();
+    expect(text).toBe("URL cannot be empty");
path: src/app/api/bookmark/add/route-onmemory.test.ts
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

エラーメッセージの検証は、具体的なエラー内容を確認することが重要です。`toMatch` を使用する代わりに、`toEqual` で具体的なエラーメッセージを検証することで、テストの信頼性が向上します。

```typescript
expect(text).toEqual("URL cannot be empty");
```
created_at: 2025-05-13T13:32:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,230 @@
+import "@testing-library/jest-dom";
+import ActualDatabase from "better-sqlite3"; // Import the actual library
+
+import { Bookmark, createBookmark } from "../../../types/Bookmark";
+import { getDb } from "../database";
+import { POST } from "./route";
+
+// We will mock getDb to return our in-memory instance.
+// The actual getDb function is simple, but mocking allows us to inject the in-memory DB.
+jest.mock("../database");
+
+let inMemoryDbInstance: ActualDatabase.Database;
+
+describe("ブックマーク追加APIのテスト (オンメモリDB)", () => {
+  beforeEach(() => {
+    // Reset mocks before each test
+    jest.resetAllMocks();
+
+    // Create a new in-memory database for each test
+    inMemoryDbInstance = new ActualDatabase(":memory:");
+    // Initialize the schema (same as in the original database.ts)
+    inMemoryDbInstance.exec(`
+      CREATE TABLE IF NOT EXISTS bookmarks (
+        id INTEGER PRIMARY KEY AUTOINCREMENT,
+        url TEXT NOT NULL UNIQUE,
+        title TEXT NOT NULL
+      )
+    `);
+
+    // Configure the getDb mock to return this instance
+    (getDb as jest.Mock).mockReturnValue(inMemoryDbInstance);
+  });
+
+  afterEach(() => {
+    if (inMemoryDbInstance) {
+      inMemoryDbInstance.close();
+    }
+  });
+
+  // --- POST Tests ---
+
+  it("POST: ブックマークのデータが追加できる", async () => {
path: src/app/api/bookmark/add/route-onmemory.test.ts
user: login: kubotama
body: レビュー結果を反映するように修正した。
created_at: 2025-05-13T13:40:24Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,230 @@
+import "@testing-library/jest-dom";
+import ActualDatabase from "better-sqlite3"; // Import the actual library
+
+import { Bookmark, createBookmark } from "../../../types/Bookmark";
+import { getDb } from "../database";
+import { POST } from "./route";
+
+// We will mock getDb to return our in-memory instance.
+// The actual getDb function is simple, but mocking allows us to inject the in-memory DB.
+jest.mock("../database");
+
+let inMemoryDbInstance: ActualDatabase.Database;
+
+describe("ブックマーク追加APIのテスト (オンメモリDB)", () => {
+  beforeEach(() => {
+    // Reset mocks before each test
+    jest.resetAllMocks();
+
+    // Create a new in-memory database for each test
+    inMemoryDbInstance = new ActualDatabase(":memory:");
+    // Initialize the schema (same as in the original database.ts)
+    inMemoryDbInstance.exec(`
+      CREATE TABLE IF NOT EXISTS bookmarks (
+        id INTEGER PRIMARY KEY AUTOINCREMENT,
+        url TEXT NOT NULL UNIQUE,
+        title TEXT NOT NULL
+      )
+    `);
+
+    // Configure the getDb mock to return this instance
+    (getDb as jest.Mock).mockReturnValue(inMemoryDbInstance);
+  });
+
+  afterEach(() => {
+    if (inMemoryDbInstance) {
+      inMemoryDbInstance.close();
+    }
+  });
+
+  // --- POST Tests ---
+
+  it("POST: ブックマークのデータが追加できる", async () => {
+    const bookmark: Bookmark = createBookmark({
+      url: "https://github.com/kubotama/linkpage",
+      title: "kubotama/linkpage",
+    });
+    const response = await POST(
+      new Request("http://localhost:3000/api/bookmark/add", {
+        method: "POST",
+        headers: {
+          "Content-Type": "application/json",
+        },
+        body: JSON.stringify(bookmark),
+      })
+    );
+
+    expect(response.status).toBe(200);
+    const json = await response.json();
+    expect(json).toEqual({
+      id: expect.any(Number),
+      url: "https://github.com/kubotama/linkpage",
+      title: "kubotama/linkpage",
+    });
+
+    // Verify data in the in-memory database
+    const stmt = inMemoryDbInstance.prepare(
+      "SELECT url, title FROM bookmarks WHERE id = ?"
+    );
+    const dbData = stmt.get(json.id); // json.id comes from the response (lastInsertRowid)
+    expect(dbData).toEqual({
+      url: bookmark.url,
+      title: bookmark.title,
+    });
+  });
+
+  it("POST: ブックマークのデータを2回、追加できる", async () => {
+    // 1回めの追加
+    const bookmark1: Bookmark = createBookmark({
+      url: "https://github.com/kubotama/linkpage",
+      title: "kubotama/linkpage",
+    });
+    const response1 = await POST(
+      new Request("http://localhost:3000/api/bookmark/add", {
+        method: "POST",
+        headers: {
+          "Content-Type": "application/json",
+        },
+        body: JSON.stringify(bookmark1),
+      })
+    );
+
+    expect(response1.status).toBe(200);
+    const json1 = await response1.json();
+    expect(json1).toEqual({
+      id: expect.any(Number),
+      url: "https://github.com/kubotama/linkpage",
+      title: "kubotama/linkpage",
+    });
+
+    // Verify data in the in-memory database
+    const stmt1 = inMemoryDbInstance.prepare(
+      "SELECT url, title FROM bookmarks WHERE id = ?"
+    );
+    const dbData1 = stmt1.get(json1.id); // json.id comes from the response (lastInsertRowid)
+    expect(dbData1).toEqual({
+      url: bookmark1.url,
+      title: bookmark1.title,
+    });
+
+    // 2回めの追加
+    const bookmark2: Bookmark = createBookmark({
+      url: "https://www.google.com/",
+      title: "Google",
+    });
+    const response2 = await POST(
+      new Request("http://localhost:3000/api/bookmark/add", {
+        method: "POST",
+        headers: {
+          "Content-Type": "application/json",
+        },
+        body: JSON.stringify(bookmark2),
+      })
+    );
+
+    expect(response2.status).toBe(200);
+    const json2 = await response2.json();
+    expect(json2).toEqual({
+      id: expect.any(Number),
+      url: "https://www.google.com/",
+      title: "Google",
+    });
+
+    // Verify data in the in-memory database
+    const stmt2 = inMemoryDbInstance.prepare(
+      "SELECT url, title FROM bookmarks WHERE id = ?"
+    );
+    const dbData2 = stmt2.get(json2.id); // json.id comes from the response (lastInsertRowid)
+    expect(dbData2).toEqual({
+      url: bookmark2.url,
+      title: bookmark2.title,
+    });
+  });
path: src/app/api/bookmark/add/route-onmemory.test.ts
user: login: kubotama
body: レビュー結果を反映するように修正した。
created_at: 2025-05-13T13:40:39Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,230 @@
+import "@testing-library/jest-dom";
+import ActualDatabase from "better-sqlite3"; // Import the actual library
+
+import { Bookmark, createBookmark } from "../../../types/Bookmark";
+import { getDb } from "../database";
+import { POST } from "./route";
+
+// We will mock getDb to return our in-memory instance.
+// The actual getDb function is simple, but mocking allows us to inject the in-memory DB.
+jest.mock("../database");
+
+let inMemoryDbInstance: ActualDatabase.Database;
+
+describe("ブックマーク追加APIのテスト (オンメモリDB)", () => {
+  beforeEach(() => {
+    // Reset mocks before each test
+    jest.resetAllMocks();
+
+    // Create a new in-memory database for each test
+    inMemoryDbInstance = new ActualDatabase(":memory:");
+    // Initialize the schema (same as in the original database.ts)
+    inMemoryDbInstance.exec(`
+      CREATE TABLE IF NOT EXISTS bookmarks (
+        id INTEGER PRIMARY KEY AUTOINCREMENT,
+        url TEXT NOT NULL UNIQUE,
+        title TEXT NOT NULL
+      )
+    `);
+
+    // Configure the getDb mock to return this instance
+    (getDb as jest.Mock).mockReturnValue(inMemoryDbInstance);
+  });
+
+  afterEach(() => {
+    if (inMemoryDbInstance) {
+      inMemoryDbInstance.close();
+    }
+  });
+
+  // --- POST Tests ---
+
+  it("POST: ブックマークのデータが追加できる", async () => {
+    const bookmark: Bookmark = createBookmark({
+      url: "https://github.com/kubotama/linkpage",
+      title: "kubotama/linkpage",
+    });
+    const response = await POST(
+      new Request("http://localhost:3000/api/bookmark/add", {
+        method: "POST",
+        headers: {
+          "Content-Type": "application/json",
+        },
+        body: JSON.stringify(bookmark),
+      })
+    );
+
+    expect(response.status).toBe(200);
+    const json = await response.json();
+    expect(json).toEqual({
+      id: expect.any(Number),
+      url: "https://github.com/kubotama/linkpage",
+      title: "kubotama/linkpage",
+    });
+
+    // Verify data in the in-memory database
+    const stmt = inMemoryDbInstance.prepare(
+      "SELECT url, title FROM bookmarks WHERE id = ?"
+    );
+    const dbData = stmt.get(json.id); // json.id comes from the response (lastInsertRowid)
+    expect(dbData).toEqual({
+      url: bookmark.url,
+      title: bookmark.title,
+    });
+  });
+
+  it("POST: ブックマークのデータを2回、追加できる", async () => {
+    // 1回めの追加
+    const bookmark1: Bookmark = createBookmark({
+      url: "https://github.com/kubotama/linkpage",
+      title: "kubotama/linkpage",
+    });
+    const response1 = await POST(
+      new Request("http://localhost:3000/api/bookmark/add", {
+        method: "POST",
+        headers: {
+          "Content-Type": "application/json",
+        },
+        body: JSON.stringify(bookmark1),
+      })
+    );
+
+    expect(response1.status).toBe(200);
+    const json1 = await response1.json();
+    expect(json1).toEqual({
+      id: expect.any(Number),
+      url: "https://github.com/kubotama/linkpage",
+      title: "kubotama/linkpage",
+    });
+
+    // Verify data in the in-memory database
+    const stmt1 = inMemoryDbInstance.prepare(
+      "SELECT url, title FROM bookmarks WHERE id = ?"
+    );
+    const dbData1 = stmt1.get(json1.id); // json.id comes from the response (lastInsertRowid)
+    expect(dbData1).toEqual({
+      url: bookmark1.url,
+      title: bookmark1.title,
+    });
+
+    // 2回めの追加
+    const bookmark2: Bookmark = createBookmark({
+      url: "https://www.google.com/",
+      title: "Google",
+    });
+    const response2 = await POST(
+      new Request("http://localhost:3000/api/bookmark/add", {
+        method: "POST",
+        headers: {
+          "Content-Type": "application/json",
+        },
+        body: JSON.stringify(bookmark2),
+      })
+    );
+
+    expect(response2.status).toBe(200);
+    const json2 = await response2.json();
+    expect(json2).toEqual({
+      id: expect.any(Number),
+      url: "https://www.google.com/",
+      title: "Google",
+    });
+
+    // Verify data in the in-memory database
+    const stmt2 = inMemoryDbInstance.prepare(
+      "SELECT url, title FROM bookmarks WHERE id = ?"
+    );
+    const dbData2 = stmt2.get(json2.id); // json.id comes from the response (lastInsertRowid)
+    expect(dbData2).toEqual({
+      url: bookmark2.url,
+      title: bookmark2.title,
+    });
+  });
+
+  it("POST: 不正なJSONデータの場合はエラーを返す", async () => {
+    const response = await POST(
+      new Request("http://localhost:3000/api/bookmark/add", {
+        method: "POST",
+        headers: {
+          "Content-Type": "application/json",
+        },
+        body: "invalid json",
+      })
+    );
+
+    expect(response.status).toBe(500);
+    const text = await response.text();
+    expect(text).toMatch(/Unexpected token|JSON.parse/i); // Check for JSON parsing error message
+  });
+
+  it("POST: 重複したURLのブックマーク追加時にデータベースエラーを返す", async () => {
+    const initialBookmark = {
+      url: "https://example.com",
+      title: "Example",
+    };
+    // Pre-populate the database with one entry
+    const insertStmt = inMemoryDbInstance.prepare(
+      "INSERT INTO bookmarks (url, title) VALUES (?, ?)"
+    );
+    insertStmt.run(initialBookmark.url, initialBookmark.title);
+
+    const bookmark: Bookmark = createBookmark({
+      url: initialBookmark.url, // Same URL
+      title: "Another Title For Same URL",
+    });
+
+    const response = await POST(
+      new Request("http://localhost:3000/api/bookmark/add", {
+        method: "POST",
+        headers: {
+          "Content-Type": "application/json",
+        },
+        body: JSON.stringify(bookmark),
+      })
+    );
+
+    expect(response.status).toBe(500);
+    const text = await response.text();
+    expect(text).toMatch(/UNIQUE constraint failed: bookmarks.url/i);
+  });
+
+  it("POST: URLが空文字の場合にエラーを返す", async () => {
+    const bookmark: Bookmark = createBookmark({
+      title: "Example",
+    });
+
+    const response = await POST(
+      new Request("http://localhost:3000/api/bookmark/add", {
+        method: "POST",
+        headers: {
+          "Content-Type": "application/json",
+        },
+        body: JSON.stringify(bookmark),
+      })
+    );
+
+    expect(response.status).toBe(400);
+    const text = await response.text();
+    expect(text).toBe("URL cannot be empty");
path: src/app/api/bookmark/add/route-onmemory.test.ts
user: login: kubotama
body: レビュー結果を反映するように修正した。
created_at: 2025-05-13T13:41:58Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,184 @@
+import "@testing-library/jest-dom";
+import ActualDatabase from "better-sqlite3"; // Import the actual library
path: src/app/api/bookmark/add/route-onmemory.test.ts
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

`ActualDatabase`をインポートしていますが、これはテストでのみ使用されるべきです。本番コードに影響を与えないように、`devDependencies`に移動することを検討してください。
created_at: 2025-05-13T13:42:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,184 @@
+import "@testing-library/jest-dom";
+import ActualDatabase from "better-sqlite3"; // Import the actual library
path: src/app/api/bookmark/add/route-onmemory.test.ts
user: login: kubotama
body: importしているのはActureDatabaseではなくbetter-sqlite3モジュールで、これは本番環境でも利用されるため移動しない。
created_at: 2025-05-13T13:50:32Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,156 @@
+import "@testing-library/jest-dom";
+import ActualDatabase from "better-sqlite3"; // Import the actual library
path: src/app/api/bookmark/add/route-onmemory.test.ts
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

`ActualDatabase`をインポートしていますが、これはテストでのみ使用されるべきです。本番コードに影響を与えないように、`devDependencies`に移動することを検討してください。
created_at: 2025-05-13T13:59:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,156 @@
+import "@testing-library/jest-dom";
+import ActualDatabase from "better-sqlite3"; // Import the actual library
+
+import { Bookmark, createBookmark } from "../../../types/Bookmark";
+import { getDb } from "../database";
+import { POST } from "./route";
+
+// We will mock getDb to return our in-memory instance.
+// The actual getDb function is simple, but mocking allows us to inject the in-memory DB.
+jest.mock("../database");
+
+let inMemoryDbInstance: ActualDatabase.Database;
+
+const API_URL = "http://localhost:3000/api/bookmark/add";
+
+function createPostRequest(body: string): Request {
+  return new Request(API_URL, {
+    method: "POST",
+    headers: {
+      "Content-Type": "application/json",
+    },
+    body: body,
+  });
+}
+describe("ブックマーク追加APIのテスト (オンメモリDB)", () => {
+  beforeEach(() => {
+    // Reset mocks before each test
+    jest.resetAllMocks();
+
+    // Create a new in-memory database for each test
+    inMemoryDbInstance = new ActualDatabase(":memory:");
+    // Initialize the schema (same as in the original database.ts)
+    inMemoryDbInstance.exec(`
+      CREATE TABLE IF NOT EXISTS bookmarks (
+        id INTEGER PRIMARY KEY AUTOINCREMENT,
+        url TEXT NOT NULL UNIQUE,
+        title TEXT NOT NULL
+      )
+    `);
+
+    // Configure the getDb mock to return this instance
+    (getDb as jest.Mock).mockReturnValue(inMemoryDbInstance);
+  });
+
+  afterEach(() => {
+    if (inMemoryDbInstance) {
+      inMemoryDbInstance.close();
+    }
+  });
+
+  // --- POST Tests ---
+
+  // Utility function to add a bookmark and verify the response
+  async function addBookmarkAndVerify(bookmark: Bookmark) {
+    const response = await POST(createPostRequest(JSON.stringify(bookmark)));
+
+    expect(response.status).toBe(200);
+    const json = await response.json();
+    expect(json).toEqual({
+      id: expect.any(Number),
+      url: bookmark.url,
+      title: bookmark.title,
+    });
+
+    // Verify data in the in-memory database
+    const stmt = inMemoryDbInstance.prepare(
+      "SELECT url, title FROM bookmarks WHERE id = ?"
+    );
+    const dbData = stmt.get(json.id); // json.id comes from the response (lastInsertRowid)
+    expect(dbData).toEqual({
+      url: bookmark.url,
+      title: bookmark.title,
+    });
+  }
+
+  it("POST: ブックマークのデータが追加できる", async () => {
+    await addBookmarkAndVerify(
+      createBookmark({
+        url: "https://github.com/kubotama/linkpage",
+        title: "kubotama/linkpage",
+      })
+    );
+  });
+
+  it("POST: ブックマークのデータを2回、追加できる", async () => {
+    await addBookmarkAndVerify(
+      createBookmark({
+        url: "https://github.com/kubotama/linkpage",
+        title: "kubotama/linkpage",
+      })
+    );
+
+    await addBookmarkAndVerify(
+      createBookmark({
+        url: "https://www.google.com/",
+        title: "Google",
+      })
+    );
+  });
+
+  it("POST: 不正なJSONデータの場合はエラーを返す", async () => {
+    const response = await POST(createPostRequest("invalid json"));
+
+    expect(response.status).toBe(500);
+    const text = await response.text();
+    expect(text).toMatch(/Unexpected token|JSON.parse/i); // Check for JSON parsing error message
+  });
+
+  it("POST: 重複したURLのブックマーク追加時にデータベースエラーを返す", async () => {
+    const initialBookmark = {
+      url: "https://example.com",
+      title: "Example",
+    };
+    // Pre-populate the database with one entry
+    const insertStmt = inMemoryDbInstance.prepare(
+      "INSERT INTO bookmarks (url, title) VALUES (?, ?)"
+    );
+    insertStmt.run(initialBookmark.url, initialBookmark.title);
+
+    const bookmark: Bookmark = createBookmark({
+      url: initialBookmark.url, // Same URL
+      title: "Another Title For Same URL",
+    });
+
+    const response = await POST(createPostRequest(JSON.stringify(bookmark)));
+
+    expect(response.status).toBe(500);
+    const text = await response.text();
+    // expect(text).toMatch(/UNIQUE constraint failed: bookmarks.url/i);
+    expect(text).toEqual("UNIQUE constraint failed: bookmarks.url");
path: src/app/api/bookmark/add/route-onmemory.test.ts
user: login: gemini-code-assist[bot]
body: ![medium](https://img.shields.io/badge/priority-medium-yellow)

130行目では、エラーメッセージが完全に一致することを期待していますが、これはデータベースのエラーメッセージが変更された場合にテストが失敗する可能性があるため、柔軟性に欠けます。エラーメッセージの一部（例えば、`UNIQUE constraint failed`）が含まれていることを確認する方が、より良いアプローチかもしれません。
created_at: 2025-05-13T13:59:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,156 @@
+import "@testing-library/jest-dom";
+import ActualDatabase from "better-sqlite3"; // Import the actual library
+
+import { Bookmark, createBookmark } from "../../../types/Bookmark";
+import { getDb } from "../database";
+import { POST } from "./route";
+
+// We will mock getDb to return our in-memory instance.
+// The actual getDb function is simple, but mocking allows us to inject the in-memory DB.
+jest.mock("../database");
+
+let inMemoryDbInstance: ActualDatabase.Database;
+
+const API_URL = "http://localhost:3000/api/bookmark/add";
+
+function createPostRequest(body: string): Request {
+  return new Request(API_URL, {
+    method: "POST",
+    headers: {
+      "Content-Type": "application/json",
+    },
+    body: body,
+  });
+}
+describe("ブックマーク追加APIのテスト (オンメモリDB)", () => {
+  beforeEach(() => {
+    // Reset mocks before each test
+    jest.resetAllMocks();
+
+    // Create a new in-memory database for each test
+    inMemoryDbInstance = new ActualDatabase(":memory:");
+    // Initialize the schema (same as in the original database.ts)
+    inMemoryDbInstance.exec(`
+      CREATE TABLE IF NOT EXISTS bookmarks (
+        id INTEGER PRIMARY KEY AUTOINCREMENT,
+        url TEXT NOT NULL UNIQUE,
+        title TEXT NOT NULL
+      )
+    `);
+
+    // Configure the getDb mock to return this instance
+    (getDb as jest.Mock).mockReturnValue(inMemoryDbInstance);
+  });
+
+  afterEach(() => {
+    if (inMemoryDbInstance) {
+      inMemoryDbInstance.close();
+    }
+  });
+
+  // --- POST Tests ---
+
+  // Utility function to add a bookmark and verify the response
+  async function addBookmarkAndVerify(bookmark: Bookmark) {
+    const response = await POST(createPostRequest(JSON.stringify(bookmark)));
+
+    expect(response.status).toBe(200);
+    const json = await response.json();
+    expect(json).toEqual({
+      id: expect.any(Number),
+      url: bookmark.url,
+      title: bookmark.title,
+    });
+
+    // Verify data in the in-memory database
+    const stmt = inMemoryDbInstance.prepare(
+      "SELECT url, title FROM bookmarks WHERE id = ?"
+    );
+    const dbData = stmt.get(json.id); // json.id comes from the response (lastInsertRowid)
+    expect(dbData).toEqual({
+      url: bookmark.url,
+      title: bookmark.title,
+    });
+  }
+
+  it("POST: ブックマークのデータが追加できる", async () => {
+    await addBookmarkAndVerify(
+      createBookmark({
+        url: "https://github.com/kubotama/linkpage",
+        title: "kubotama/linkpage",
+      })
+    );
+  });
+
+  it("POST: ブックマークのデータを2回、追加できる", async () => {
+    await addBookmarkAndVerify(
+      createBookmark({
+        url: "https://github.com/kubotama/linkpage",
+        title: "kubotama/linkpage",
+      })
+    );
+
+    await addBookmarkAndVerify(
+      createBookmark({
+        url: "https://www.google.com/",
+        title: "Google",
+      })
+    );
+  });
+
+  it("POST: 不正なJSONデータの場合はエラーを返す", async () => {
+    const response = await POST(createPostRequest("invalid json"));
+
+    expect(response.status).toBe(500);
+    const text = await response.text();
+    expect(text).toMatch(/Unexpected token|JSON.parse/i); // Check for JSON parsing error message
+  });
+
+  it("POST: 重複したURLのブックマーク追加時にデータベースエラーを返す", async () => {
+    const initialBookmark = {
+      url: "https://example.com",
+      title: "Example",
+    };
+    // Pre-populate the database with one entry
+    const insertStmt = inMemoryDbInstance.prepare(
+      "INSERT INTO bookmarks (url, title) VALUES (?, ?)"
+    );
+    insertStmt.run(initialBookmark.url, initialBookmark.title);
+
+    const bookmark: Bookmark = createBookmark({
+      url: initialBookmark.url, // Same URL
+      title: "Another Title For Same URL",
+    });
+
+    const response = await POST(createPostRequest(JSON.stringify(bookmark)));
+
+    expect(response.status).toBe(500);
+    const text = await response.text();
+    // expect(text).toMatch(/UNIQUE constraint failed: bookmarks.url/i);
+    expect(text).toEqual("UNIQUE constraint failed: bookmarks.url");
path: src/app/api/bookmark/add/route-onmemory.test.ts
user: login: kubotama
body: 前のレビューでは反対のことが書かれていたため修正したが、もとに戻すことにする。
created_at: 2025-05-13T14:03:18Z
author_association: OWNER
diff_hunk: @@ -0,0 +1,156 @@
+import "@testing-library/jest-dom";
+import ActualDatabase from "better-sqlite3"; // Import the actual library
path: src/app/api/bookmark/add/route-onmemory.test.ts
user: login: kubotama
body: importしているのはActureDatabaseではなくbetter-sqlite3モジュールで、これは本番環境でも利用されるため移動しない。
created_at: 2025-05-13T14:03:46Z
author_association: OWNER

PR comments:
user: login: kubotama
created_at: 2025-05-13T13:42:39Z
body: /gemini review
author_association: OWNER
user: login: kubotama
created_at: 2025-05-13T13:59:34Z
body: /gemini review
author_association: OWNER