import { promises as fs } from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
// ★★ルート本体は**静的 import** すること★★(`app/api/clean-logs/__tests__/route.test.ts` の冒頭コメントと同じ理由)
import { GET, PATCH } from "@/app/api/settings/display-limits/route";
import { logApiError } from "@/lib/logger/api";
import {
__setDisplayLimitStoreForTesting,
createDisplayLimitStore,
type DisplayLimitOverrides,
} from "@/lib/settings/display-limits";
import type { JsonStore } from "@/lib/settings/json-store";
/**
* `GET / PATCH /settings/display-limits` の頑丈さ(指示書BB 表8 T2・J1〜J3。A5・D-182)。
*
* | # | 入力 | 期待 |
* |---|---|---|
* | T2 | GET で `loadDisplayLimitSettings()` が例外 | 500・internal_error・「データの取得に失敗しました」・`api.error` が1件 |
* | J1 | PATCH の本文が `null` | 422・validation_failed・「JSONの形式が不正です」 |
* | J2 | 本文が `[]` / `1` / `"x"` | 422(J1 と同じ) |
* | J3 | 本文が `{}` | 今どおり(422「keyの値が不正です」) |
*/
vi.mock("@/lib/logger/api", () => ({ logApiError: vi.fn() }));
let dir = "";
beforeEach(async () => {
dir = await fs.mkdtemp(path.join(os.tmpdir(), "dwatch-display-limits-route-"));
const filePath = path.join(dir, "display-limits.json");
__setDisplayLimitStoreForTesting(createDisplayLimitStore(filePath, () => {}), filePath);
vi.mocked(logApiError).mockClear();
});
afterEach(async () => {
__setDisplayLimitStoreForTesting(null);
await fs.rm(dir, { recursive: true, force: true });
});
function patchRaw(raw: string): Promise<Response> {
return PATCH(new Request("http://localhost/api/settings/display-limits", { method: "PATCH", body: raw }));
}
describe("GET /settings/display-limits — try の外の例外(指示書BB 表8 T2)", () => {
it("★T2: 読み込みが例外 → 500・標準の形・api.error が1件★", async () => {
const broken: JsonStore<DisplayLimitOverrides> = {
read: async () => {
throw new Error("[stub] 読み込みに失敗");
},
write: async () => {},
};
__setDisplayLimitStoreForTesting(broken, path.join(dir, "display-limits.json"));
const res = await GET(new Request("http://localhost/api/settings/display-limits"));
expect(res.status).toBe(500);
expect(await res.json()).toEqual({
error: { code: "internal_error", message: "データの取得に失敗しました", details: null },
});
expect(vi.mocked(logApiError)).toHaveBeenCalledTimes(1);
expect(vi.mocked(logApiError).mock.calls[0][0]).toMatchObject({
route: "/api/settings/display-limits",
method: "GET",
status: 500,
});
});
it("読み込みが成功すれば今どおり 200", async () => {
const res = await GET(new Request("http://localhost/api/settings/display-limits"));
expect(res.status).toBe(200);
});
});
describe("PATCH /settings/display-limits — 本文がオブジェクトでない(指示書BB 表8 J1〜J3。A5・D19)", () => {
it.each(["null", "[]", "1", '"x"'])("★J1・J2: 本文 %s → 422「JSONの形式が不正です」★", async (raw) => {
const res = await patchRaw(raw);
expect(res.status).toBe(422);
expect(await res.json()).toEqual({
error: { code: "validation_failed", message: "JSONの形式が不正です", details: null },
});
});
it("J3: 本文 {} → 今どおり(項目の検証で422「keyの値が不正です」)", async () => {
const res = await patchRaw("{}");
expect(res.status).toBe(422);
expect(((await res.json()) as { error: { message: string } }).error.message).toBe("keyの値が不正です");
});
});