import { describe, expect, it } from "vitest";
import { parseMasterIdParam } from "@/lib/api/master-id";
import {
filterCustomersByGroup,
masterListResponse,
parseCustomerQuery,
sortCustomers,
sortGroups,
} from "@/lib/masters/query";
import type { Customer, Group } from "@/lib/types";
/**
* 分類マスタのクエリ処理(指示書AK 段階1)の期待値表。
*
* ------------------------------------------------------------------------
* ★★何を守るためのテストか★★
* ------------------------------------------------------------------------
* - ★`group_id=1abc` が `1` として通らないこと★(★D-123 で実際に踏んだ罠★。
* `Number.parseInt()` は前方一致なので、送った文字列とは違うIDで絞り込みが走る)
* - ★`group_id=0` を受け付けないこと★(0 は「ホールディングスなし」のセンチネルで、
* 有効なIDではない。D-011)
* - ★未指定(`null`)と「該当0件」(`[]`)を取り違えないこと★ —
* 取り違えると ★0件のはずが全件表示に化ける★(指示書AK 落とし穴1 と同じ形)
*/
const GROUPS: Group[] = [
{ id: 3, group_name: "グループC" },
{ id: 1, group_name: "グループA" },
{ id: 2, group_name: "グループB" },
];
const CUSTOMERS: Customer[] = [
{ id: 30, customer_name: "屋号C", group_id: null },
{ id: 10, customer_name: "屋号A", group_id: 1 },
{ id: 20, customer_name: "屋号B", group_id: 1 },
{ id: 40, customer_name: "屋号D", group_id: 2 },
];
function parse(qs: string) {
return parseCustomerQuery(new URLSearchParams(qs));
}
describe("parseMasterIdParam", () => {
const CASES: { input: string | null; expected: number | null }[] = [
{ input: null, expected: null },
{ input: "", expected: null },
{ input: " ", expected: null },
{ input: "1", expected: 1 },
{ input: " 2 ", expected: 2 },
{ input: "9007199254740991", expected: 9007199254740991 },
];
for (const { input, expected } of CASES) {
it(`${JSON.stringify(input)} → ${expected}`, () => {
const r = parseMasterIdParam(input, "group_id");
expect(r.ok).toBe(true);
if (r.ok) expect(r.query).toBe(expected);
});
}
// ★2-4〜2-8(指示書AK §5-2 の表)と同じ境界★ — 段階2 のパーサもこの関数を使う
const INVALID = [
"0", // ★センチネル。有効なIDではない★
"1abc", // ★D-123 の罠。parseInt なら 1 として通ってしまう★
"-1",
"1.5",
"1e9",
"9007199254740992", // ★安全整数超★
"1", // 全角数字
"abc",
];
for (const input of INVALID) {
it(`${JSON.stringify(input)} は 422`, () => {
const r = parseMasterIdParam(input, "group_id");
expect(r.ok).toBe(false);
if (!r.ok) {
expect(r.status).toBe(422);
expect(r.error.error.code).toBe("validation_failed");
expect(r.error.error.details).toEqual({
group_id: ["1以上の整数で指定してください"],
});
}
});
}
it("details のキーはパラメータ名になる(customer_id)", () => {
const r = parseMasterIdParam("0", "customer_id");
expect(r.ok).toBe(false);
if (!r.ok) expect(r.error.error.details).toHaveProperty("customer_id");
});
it("エラー文言に入力値をエコーしない", () => {
const r = parseMasterIdParam("<script>", "group_id");
expect(r.ok).toBe(false);
if (!r.ok) expect(JSON.stringify(r.error)).not.toContain("script");
});
});
describe("parseCustomerQuery", () => {
it("未指定なら groupId は null", () => {
const r = parse("");
expect(r.ok).toBe(true);
if (r.ok) expect(r.query.groupId).toBeNull();
});
it("group_id=1 なら 1 が入る", () => {
const r = parse("group_id=1");
expect(r.ok).toBe(true);
if (r.ok) expect(r.query.groupId).toBe(1);
});
it("group_id=0 は 422", () => {
const r = parse("group_id=0");
expect(r.ok).toBe(false);
if (!r.ok) expect(r.status).toBe(422);
});
});
describe("filterCustomersByGroup", () => {
it("null(未指定)は全件", () => {
expect(filterCustomersByGroup(CUSTOMERS, null)).toHaveLength(4);
});
it("group_id を指定すると傘下だけになる", () => {
expect(filterCustomersByGroup(CUSTOMERS, 1).map((c) => c.id)).toEqual([10, 20]);
});
it("★実在しない group_id は空配列★(全件に化けないこと)", () => {
expect(filterCustomersByGroup(CUSTOMERS, 999)).toEqual([]);
});
it("★group_id が null の屋号は、どの group_id でも当たらない★", () => {
for (const id of [1, 2, 999]) {
expect(filterCustomersByGroup(CUSTOMERS, id).some((c) => c.group_id === null)).toBe(
false
);
}
});
});
describe("sortGroups / sortCustomers", () => {
it("id の昇順(契約どおり)", () => {
expect(sortGroups(GROUPS).map((g) => g.id)).toEqual([1, 2, 3]);
expect(sortCustomers(CUSTOMERS).map((c) => c.id)).toEqual([10, 20, 30, 40]);
});
it("引数を破壊しない(純粋関数)", () => {
sortGroups(GROUPS);
expect(GROUPS[0].id).toBe(3);
});
});
describe("masterListResponse", () => {
it("★items を切らない★(セレクトリストが黙って短くならないこと)", () => {
const items = Array.from({ length: 120 }, (_, i) => ({ id: i + 1 }));
const body = masterListResponse(items);
expect(body.items).toHaveLength(120);
expect(body.meta).toEqual({ total: 120, page: 1, per_page: 120 });
});
it("0件でも meta を返す", () => {
expect(masterListResponse([]).meta).toEqual({ total: 0, page: 1, per_page: 0 });
});
});