- POST /admin/users/admin 신설 — 활성 유저에게 custom claims(admin:true) 부여, 기존 claims 보존, 이미 관리자면 alreadyAdmin으로 멱등 응답 - 비활성 유저·미존재 유저는 INACTIVE_USER/USER_NOT_FOUND로 거부 - 유저 목록·검색 응답에 isAdmin 표시(auth.getUsers로 일괄 조회) - adminUserService 테스트 보강
119 lines
4.2 KiB
TypeScript
119 lines
4.2 KiB
TypeScript
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);
|
|
}
|
|
});
|
|
});
|