Newer
Older
client_dwatch / app / api / ds-alerts / __tests__ / route.fixtures.test.ts
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";

import { PATCH } from "@/app/api/errors/[error_id]/status/route";
import { GET } from "@/app/api/ds-alerts/route";
import { resetFixtureErrorReportOverrides } from "@/lib/db/fixture-error-report-overrides";
import type { DsAlertSummary } from "@/lib/ds-alerts/query";
import type { DsAlert, SummaryListResponse } from "@/lib/types";

/**
 * `GET /ds-alerts` → `PATCH /errors/{error_id}/status` → `GET /ds-alerts`(`USE_FIXTURES=true`。指示書AX 段階3-4)。
 *
 * ★D-Sec アラートは `errorReportRows` の `D-S_CONTROL_FAILURE` 行から作る★(D13)ので、
 *   エラー一覧の PATCH で書いた対応状況が D-Sec にも出る(U4。上書き表を共有)。
 *
 * ★★なぜ別ファイルなのか★★ — `lib/db/index.ts` の `db` はモジュール読み込み時に束縛されるので、
 * 同じファイルで true/false の両方は試せない(`app/api/errors/__tests__/route.fixtures.test.ts` と同じ)。
 * ★テストごとに `resetFixtureErrorReportOverrides()` で空に戻す★。
 */

vi.mock("@/lib/db/use-fixtures", () => ({ USE_FIXTURES: true }));

vi.mock("@/lib/logger", async (importOriginal) => {
  const actual = await importOriginal<typeof import("@/lib/logger")>();
  return { ...actual, logEvent: vi.fn() };
});

const NOW_ISO = "2026-10-06T08:15:00+09:00";

/** 出る7行(`FX-ERR-10`・`12`〜`17`)。未解消3(10・12・16)・対応中2(13・17)・解消2(14・15) */
const VISIBLE_DS_ALERT_IDS = [
  "FX-ERR-10",
  "FX-ERR-12",
  "FX-ERR-13",
  "FX-ERR-14",
  "FX-ERR-15",
  "FX-ERR-16",
  "FX-ERR-17",
];

beforeEach(() => {
  resetFixtureErrorReportOverrides();
  vi.useFakeTimers({ toFake: ["Date"] });
  vi.setSystemTime(new Date(NOW_ISO));
});

afterEach(() => {
  vi.useRealTimers();
  resetFixtureErrorReportOverrides();
});

async function list(qs = ""): Promise<SummaryListResponse<DsAlert, DsAlertSummary>> {
  const res = await GET(new Request(`http://localhost/api/ds-alerts${qs ? `?${qs}` : ""}`));
  expect(res.status).toBe(200);
  return (await res.json()) as SummaryListResponse<DsAlert, DsAlertSummary>;
}

async function patch(id: string, body: unknown): Promise<Response> {
  return PATCH(
    new Request(`http://localhost/api/errors/${encodeURIComponent(id)}/status`, {
      method: "PATCH",
      body: JSON.stringify(body),
    }),
    { params: Promise.resolve({ error_id: id }) }
  );
}

describe("GET /ds-alerts(USE_FIXTURES=true)", () => {
  it("★既定(status=open)で5件・status=all で7件★", async () => {
    const def = await list();
    expect(def.meta.total).toBe(5);
    expect(def.items.map((i) => i.id).sort()).toEqual(["FX-ERR-10", "FX-ERR-12", "FX-ERR-13", "FX-ERR-16", "FX-ERR-17"]);
    const all = await list("status=all");
    expect(all.meta.total).toBe(7);
    expect(all.items.map((i) => i.id).sort()).toEqual(VISIBLE_DS_ALERT_IDS);
  });

  it("★summary は {unresolved:3, in_progress:2, resolved:2}(status に依らない)★", async () => {
    for (const qs of ["", "status=all", "status=resolved"]) {
      expect((await list(qs)).summary).toEqual({ unresolved: 3, in_progress: 2, resolved: 2 });
    }
  });

  it("★SECURITY_ON(FX-ERR-08)・NOT_STARTED(FX-ERR-11)・削除済みの清掃エリア(FX-ERR-19)は出ない★", async () => {
    const ids = (await list("status=all")).items.map((i) => i.id);
    expect(ids).not.toContain("FX-ERR-08");
    expect(ids).not.toContain("FX-ERR-11");
    expect(ids).not.toContain("FX-ERR-19");
  });
});

describe("PATCH /errors → GET /ds-alerts(USE_FIXTURES=true)", () => {
  it("★FX-ERR-10 を解消にすると status=open から消え、summary.resolved が3になる★", async () => {
    expect((await patch("FX-ERR-10", { status: "resolved" })).status).toBe(200);

    const open = await list();
    expect(open.items.map((i) => i.id)).not.toContain("FX-ERR-10");
    expect(open.meta.total).toBe(4);
    expect(open.summary).toEqual({ unresolved: 2, in_progress: 2, resolved: 3 });

    const resolved = await list("status=resolved");
    expect(resolved.items.find((i) => i.id === "FX-ERR-10")).toMatchObject({
      resolution_status: "resolved",
      responded_at: NOW_ISO,
      resolved_at: NOW_ISO,
    });
  });
});