mmday-firebase/tests/repositories/orderRepository.test.ts
윤정민 c635f16952 Add admin console read APIs, claim script, and CORS hedge
- 어드민 웹(fanit_admin) 지원: GET products/list(비노출 포함 전체 상품, AdminProductDto)·GET orders/list(전 유저 주문, createdAt desc 커서 페이지네이션) 추가 — Hosting rewrite·직접 호출 양쪽에서 일관되는 2세그먼트 라우트
- 지갑·원장 어드민 응답도 DTO로 감싸 Timestamp({_seconds}) 유출 제거
- admin 함수에 단일 origin CORS 헤지 추가(OPTIONS preflight를 requireAdmin보다 먼저 처리, 에러 응답에도 헤더 적용)
- scripts/set-admin-claim.ts로 운영자 admin 클레임 부여(부여 후 재로그인 필요)
- Storage 규칙: write만 admin 클레임으로 제한, read는 불변(앱 이미지 표시 계약 유지)
- 신규 조회·DTO vitest 테스트 추가
2026-07-21 13:57:42 +09:00

72 lines
3.1 KiB
TypeScript

import { beforeEach, describe, expect, it } from "vitest";
import { Timestamp } from "firebase-admin/firestore";
import { firestore } from "../../src/firebase";
import { listAllOrders } from "../../src/repositories/orderRepository";
import type { OrderDoc } from "../../src/types/reward";
function makeOrder(uid: string, createdAt: Timestamp, overrides: Partial<OrderDoc> = {}): OrderDoc {
return {
uid,
items: [{ productId: "p1", qty: 1, pointPrice: 500, name: "굿즈" }],
totalPoints: 500,
recipient: { name: "홍길동", phone: "01000000000", address1: "서울", postalCode: "00000" },
status: "reserved",
clientIdempotencyKey: `key-${uid}-${createdAt.toMillis()}`,
reserveLedgerTxIds: [],
orderedAt: createdAt,
statusHistory: [{ status: "reserved", at: createdAt, actor: uid }],
createdAt,
updatedAt: createdAt,
...overrides,
};
}
describe("orderRepository.listAllOrders", () => {
beforeEach(async () => {
await firestore.recursiveDelete(firestore.collection("orders"));
});
it("uid 필터 없이 서로 다른 유저의 주문을 모두 createdAt desc로 반환한다", async () => {
const t1 = Timestamp.fromDate(new Date("2026-07-01T00:00:00.000Z"));
const t2 = Timestamp.fromDate(new Date("2026-07-02T00:00:00.000Z"));
const t3 = Timestamp.fromDate(new Date("2026-07-03T00:00:00.000Z"));
await firestore.doc("orders/o1").set(makeOrder("uid-a", t1));
await firestore.doc("orders/o2").set(makeOrder("uid-b", t2));
await firestore.doc("orders/o3").set(makeOrder("uid-a", t3));
const page = await listAllOrders(20);
expect(page.items.map((o) => o.id)).toEqual(["o3", "o2", "o1"]);
expect(page.items.map((o) => o.uid).sort()).toEqual(["uid-a", "uid-a", "uid-b"].sort());
});
it("cursor 기반 페이지네이션이 동작한다", async () => {
const t1 = Timestamp.fromDate(new Date("2026-07-01T00:00:00.000Z"));
const t2 = Timestamp.fromDate(new Date("2026-07-02T00:00:00.000Z"));
const t3 = Timestamp.fromDate(new Date("2026-07-03T00:00:00.000Z"));
await firestore.doc("orders/o1").set(makeOrder("uid-a", t1));
await firestore.doc("orders/o2").set(makeOrder("uid-b", t2));
await firestore.doc("orders/o3").set(makeOrder("uid-a", t3));
const firstPage = await listAllOrders(2);
expect(firstPage.items.map((o) => o.id)).toEqual(["o3", "o2"]);
expect(firstPage.cursor).toBe("o2");
const secondPage = await listAllOrders(2, firstPage.cursor ?? undefined);
expect(secondPage.items.map((o) => o.id)).toEqual(["o1"]);
expect(secondPage.cursor).toBe("o1");
});
it("status 필터를 걸면 해당 상태 주문만 반환한다 (기존 status+createdAt 인덱스 재사용)", async () => {
const t1 = Timestamp.fromDate(new Date("2026-07-01T00:00:00.000Z"));
const t2 = Timestamp.fromDate(new Date("2026-07-02T00:00:00.000Z"));
await firestore.doc("orders/o1").set(makeOrder("uid-a", t1, { status: "reserved" }));
await firestore.doc("orders/o2").set(makeOrder("uid-b", t2, { status: "confirmed" }));
const page = await listAllOrders(20, undefined, "confirmed");
expect(page.items.map((o) => o.id)).toEqual(["o2"]);
});
});