- 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·커서 페이지네이션)
87 lines
3.1 KiB
TypeScript
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);
|
|
}
|
|
});
|
|
});
|