mmday-firebase/tests/services/adminUserService.test.ts
윤정민 d5aca61f4f Add admin user search and list APIs
- GET /admin/users/search: 닉네임 접두어(Firestore orderBy+startAt/endAt) + 이메일 정확 일치(Auth 조회) + uid 정확 일치를 한 입력으로 통합 검색, uid 기준 중복 제거
- GET /admin/users/list: 가입일 내림차순 + 문서 id 커서 페이지네이션 (주문 목록과 동일 패턴)
- AdminUserDto 추가: 날짜는 UTC ISO 변환, 옵션 필드는 키 생략, streak/티어 등 lazy 보정 필드는 기존 원칙대로 제외, active 미백필 문서는 활성으로 간주
- 비활성(탈퇴) 유저도 검색 대상에 포함해 어드민이 상태를 확인할 수 있게 함
- 에뮬레이터 회귀 테스트 5건 추가 (접두어 검색·uid 일치·비활성 표시·빈 q 400·커서 페이지네이션)
2026-07-21 15:25:37 +09:00

87 lines
3.1 KiB
TypeScript

import { beforeAll, describe, expect, it } from "vitest";
import { Timestamp } from "firebase-admin/firestore";
import { firestore } from "../../src/firebase";
import { 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);
}
});
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}호랑이`);
});
it("빈 q 는 400", async () => {
await expect(searchAdminUsers(" ", 10)).rejects.toMatchObject({ status: 400 });
});
});
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);
}
});
});