Newer
Older
client_dwatch / lib / db / __tests__ / fixture-clean-logs.test.ts
import { describe, expect, it } from "vitest";

import { filterCleanLogs, type CleanLogQuery } from "@/lib/clean-logs/query";
import { filterFixtureCleanLogs } from "@/lib/db/fixture-clean-logs";
import { cleanLogs } from "@/lib/fixtures";
import type { CleanLogDbQuery } from "@/lib/types";

/**
 * 指示書AC 段階2 ★D-5★ — fixtures 版も同じ引数を受け取り、同じ結果を返す。
 *
 * ★★`USE_FIXTURES` の true/false で件数が変わってはいけない★★(指示書AC 禁止事項6)。
 * ★★`null`(条件なし)と `[]`(0件ヒット)を同じ意味で扱わないこと★★(罠9 / 罠1)。
 *
 * ★fixtures の**既存の値**は変更していない★(禁止事項5)— ここは読むだけ。
 */

const ALL: CleanLogDbQuery = { id: null, date_from: null, date_to: null, facility_ids: null };

/** API ルート側の絞り込み(=規則の「正」)で同じ条件を書いたもの */
function memoryQuery(partial: Partial<CleanLogQuery>): CleanLogQuery {
  return {
    dateFrom: null,
    dateTo: null,
    facilityName: "",
    model: "",
    serial: "",
    result: "all",
    // ★絞り込まない(指示書AK 段階2 で足った2つ。`null` = 条件なし)★
    groupId: null,
    customerId: null,
    page: 1,
    perPage: 50,
    ...partial,
  };
}

function jstDate(day: string): Date {
  return new Date(`${day}T00:00:00+09:00`);
}

/** fixtures の中でデータが載っている日(`at()` が相対日で作るので実行日から決める) */
const days = [...new Set(cleanLogs.map((r) => r.scheduled_at.slice(0, 10)))].sort();
const middleDay = days[Math.floor(days.length / 2)];

describe("filterFixtureCleanLogs(指示書AC 段階2 D-5)", () => {
  it("#1 範囲なし(全部 null)→ fixtures の全件", () => {
    expect(filterFixtureCleanLogs(cleanLogs, ALL)).toHaveLength(cleanLogs.length);
  });

  it("#2 日付範囲 → ★API ルート側の `filterCleanLogs()` と完全一致★", () => {
    const from = days[1];
    const to = days[3];
    const sql = filterFixtureCleanLogs(cleanLogs, {
      id: null,
      date_from: from,
      date_to: to,
      facility_ids: null,
    });
    const memory = filterCleanLogs(
      [...cleanLogs],
      memoryQuery({ dateFrom: jstDate(from), dateTo: jstDate(to) })
    );
    // ★件数だけでなく id の集合まで比べる★(指示書AC G-3)
    expect(sql.map((r) => r.id)).toEqual(memory.map((r) => r.id));
    expect(sql).toEqual(memory);
    expect(sql.length).toBeGreaterThan(0);
  });

  it("#3 `date_from = date_to` → その日の行だけ", () => {
    const rows = filterFixtureCleanLogs(cleanLogs, {
      id: null,
      date_from: middleDay,
      date_to: middleDay,
      facility_ids: null,
    });
    expect(rows.length).toBeGreaterThan(0);
    expect([...new Set(rows.map((r) => r.scheduled_at.slice(0, 10)))]).toEqual([middleDay]);
  });

  it("#3 上限はその日を**含む**(★翌日 00:00 未満★)", () => {
    const rows = filterFixtureCleanLogs(cleanLogs, {
      id: null,
      date_from: null,
      date_to: middleDay,
      facility_ids: null,
    });
    expect(rows.some((r) => r.scheduled_at.slice(0, 10) === middleDay)).toBe(true);
    expect(rows.every((r) => r.scheduled_at.slice(0, 10) <= middleDay)).toBe(true);
  });

  it("#4 `date_from` がデータの最古日より前 → #1 と同じ件数", () => {
    const rows = filterFixtureCleanLogs(cleanLogs, {
      id: null,
      date_from: "2000-01-01",
      date_to: null,
      facility_ids: null,
    });
    expect(rows).toHaveLength(cleanLogs.length);
  });

  it("#6 `facility_ids: [A, B]` → その2施設だけ(★メモリ絞り込みと一致★)", () => {
    const ids = [...new Set(cleanLogs.map((r) => r.facility_id))].sort((a, b) => a - b);
    const two = [ids[0], ids[1]];
    const rows = filterFixtureCleanLogs(cleanLogs, {
      id: null,
      date_from: null,
      date_to: null,
      facility_ids: two,
    });
    expect(rows).toEqual(cleanLogs.filter((r) => two.includes(r.facility_id)));
    expect(rows.length).toBeGreaterThan(0);
  });

  it("#6 1施設だけのケース", () => {
    const id = cleanLogs[0].facility_id;
    const rows = filterFixtureCleanLogs(cleanLogs, {
      id: null,
      date_from: null,
      date_to: null,
      facility_ids: [id],
    });
    expect(rows.every((r) => r.facility_id === id)).toBe(true);
    expect(rows.length).toBeGreaterThan(0);
  });

  it("★#6b★ `facility_ids: []` → ★★0件★★(★`in: undefined` に化けていないことの検出。罠1★)", () => {
    // ★★このテストが唯一の検出手段★★ —
    //   `[]` を「条件なし」と読んで全件返す実装にすると、Prisma 版(`in: []` = 0件)と
    //   件数が食い違い、`USE_FIXTURES` の true/false で結果が変わる(禁止事項6)
    expect(
      filterFixtureCleanLogs(cleanLogs, {
        id: null,
        date_from: null,
        date_to: null,
        facility_ids: [],
      })
    ).toEqual([]);
  });

  it("★#6c★ `facility_ids: null` → ★全件(#1 と同じ)★", () => {
    expect(
      filterFixtureCleanLogs(cleanLogs, {
        id: null,
        date_from: null,
        date_to: null,
        facility_ids: null,
      })
    ).toHaveLength(cleanLogs.length);
  });

  it("存在しない施設IDだけを渡すと0件(★`[]` との区別がついている★)", () => {
    expect(
      filterFixtureCleanLogs(cleanLogs, {
        id: null,
        date_from: null,
        date_to: null,
        facility_ids: [999_999],
      })
    ).toEqual([]);
  });

  it("日付と施設の AND が効く", () => {
    const id = cleanLogs[0].facility_id;
    const rows = filterFixtureCleanLogs(cleanLogs, {
      id: null,
      date_from: middleDay,
      date_to: middleDay,
      facility_ids: [id],
    });
    expect(
      rows.every(
        (r) => r.facility_id === id && r.scheduled_at.slice(0, 10) === middleDay
      )
    ).toBe(true);
  });

  it("★★段階3: `id` を指定すると1件だけ★★(★Prisma 版の `where.id` と同じ意味★)", () => {
    const target = cleanLogs[0];
    const rows = filterFixtureCleanLogs(cleanLogs, { ...ALL, id: target.id });
    expect(rows).toEqual([target]);
  });

  it("★★段階3: 存在しない `id` → 0件★★(★`null` に化けて全件になっていないことの検出★)", () => {
    expect(filterFixtureCleanLogs(cleanLogs, { ...ALL, id: 999_999_999 })).toEqual([]);
  });

  it("★段階3: `id` は他の条件と AND で重なる★(★範囲外の `id` は引けない★)", () => {
    const target = cleanLogs[0];
    const otherDay = days.find((d) => d !== target.scheduled_at.slice(0, 10))!;
    expect(
      filterFixtureCleanLogs(cleanLogs, {
        ...ALL,
        id: target.id,
        date_from: otherDay,
        date_to: otherDay,
      })
    ).toEqual([]);
  });

  it("★入力の配列を壊さない(純粋関数)★", () => {
    const before = cleanLogs.length;
    filterFixtureCleanLogs(cleanLogs, { id: null, date_from: middleDay, date_to: middleDay, facility_ids: [] });
    expect(cleanLogs).toHaveLength(before);
  });
});