import { beforeAll, describe, expect, it } from "vitest"; import { Timestamp } from "firebase-admin/firestore"; import { auth, firestore } from "../../src/firebase"; import { appointAdmin, listAdminUsers, searchAdminUsers } from "../../src/services/adminUserService"; // 다른 테스트가 만드는 users 문서와 섞이지 않도록 고유 접두어를 쓴다. const PREFIX = "검색전용"; const seedUsers = [ { uid: "admin-user-search-test-1", displayName: `${PREFIX}호랑이`, email: "search-test-1@example.com", provider: "google", active: true, createdAt: Timestamp.fromDate(new Date("2026-07-01T00:00:00Z")), }, { uid: "admin-user-search-test-2", displayName: `${PREFIX}독수리`, email: "search-test-2@example.com", provider: "apple", photoUrl: "https://example.com/p.png", active: false, deactivatedAt: Timestamp.fromDate(new Date("2026-07-10T00:00:00Z")), createdAt: Timestamp.fromDate(new Date("2026-07-02T00:00:00Z")), }, ] as const; beforeAll(async () => { for (const { uid, ...doc } of seedUsers) { await firestore.doc(`users/${uid}`).set(doc); await auth.createUser({ uid, email: doc.email, displayName: doc.displayName }); } }); describe("searchAdminUsers", () => { it("displayName 접두어로 검색되고 날짜는 UTC ISO 문자열로 나온다", async () => { const results = await searchAdminUsers(PREFIX, 10); const uids = results.map((u) => u.uid); expect(uids).toContain("admin-user-search-test-1"); expect(uids).toContain("admin-user-search-test-2"); for (const dto of results) { // Timestamp 인스턴스/_seconds 누출 금지 — 어드민 웹이 브라우저에서 그대로 소비한다. expect(typeof dto.createdAt).toBe("string"); expect(dto.createdAt?.endsWith("Z")).toBe(true); } }); it("비활성 유저도 검색되며 active=false 로 표시된다", async () => { const results = await searchAdminUsers(`${PREFIX}독수리`, 10); expect(results).toHaveLength(1); expect(results[0]).toMatchObject({ uid: "admin-user-search-test-2", active: false, photoUrl: "https://example.com/p.png", }); expect(typeof results[0].deactivatedAt).toBe("string"); }); it("uid 정확 일치로도 찾는다", async () => { const results = await searchAdminUsers("admin-user-search-test-1", 10); expect(results[0]?.uid).toBe("admin-user-search-test-1"); expect(results[0]?.displayName).toBe(`${PREFIX}호랑이`); expect(results[0]?.isAdmin).toBe(false); }); it("빈 q 는 400", async () => { await expect(searchAdminUsers(" ", 10)).rejects.toMatchObject({ status: 400 }); }); }); describe("appointAdmin", () => { it("기존 custom claims를 보존하면서 관리자 권한을 부여하고 재호출은 멱등이다", async () => { const uid = "admin-user-search-test-1"; await auth.setCustomUserClaims(uid, { betaTester: true }); await expect(appointAdmin(uid, "granting-admin")).resolves.toEqual({ uid, isAdmin: true, alreadyAdmin: false, }); const record = await auth.getUser(uid); expect(record.customClaims).toMatchObject({ betaTester: true, admin: true, }); await expect(appointAdmin(uid, "granting-admin")).resolves.toMatchObject({ isAdmin: true, alreadyAdmin: true, }); const [dto] = await searchAdminUsers(uid, 10); expect(dto.isAdmin).toBe(true); }); it("비활성 유저는 관리자로 임명할 수 없다", async () => { await expect( appointAdmin("admin-user-search-test-2", "granting-admin") ).rejects.toMatchObject({ status: 409, code: "INACTIVE_USER" }); }); }); describe("listAdminUsers", () => { it("가입일 내림차순 + 커서 페이지네이션", async () => { const first = await listAdminUsers(1); expect(first.items).toHaveLength(1); expect(first.cursor).toBe(first.items[0].uid); const second = await listAdminUsers(1, first.cursor ?? undefined); if (second.items.length > 0) { const firstDate = first.items[0].createdAt ?? ""; const secondDate = second.items[0].createdAt ?? ""; expect(secondDate <= firstDate).toBe(true); expect(second.items[0].uid).not.toBe(first.items[0].uid); } }); });