Files
fanuzu/app/firebase/functions/test/admin_write.test.ts
T

80 lines
4.1 KiB
TypeScript

import { afterAll, beforeAll, beforeEach, expect, it } from 'vitest';
import { getFirestore } from 'firebase-admin/firestore';
import { bootAdmin, shutdownAdmin } from './support.js';
import { fnAdminWrite } from '../src/admin/admin.js';
/**
* `fn-admin-write` 권한 경계 — firestore.rules 에서 master 전용인 문서를 ops 가 이 함수로
* 우회해 쓰지 못한다(S05 조사). 거부도 `adminActionLogs` 에 남는다(FR-ADM-01).
*/
const run = (role: string | undefined, targetPath: string,
patch: Record<string, unknown> = { attendPop: 999 }) =>
fnAdminWrite.run({
data: { targetPath, reason: '시험', patch },
auth: { uid: `admin-${role ?? 'none'}`, token: role ? { role } : {} },
} as Parameters<typeof fnAdminWrite.run>[0]);
const denials = async (actorUid: string) =>
(await getFirestore().collection('adminActionLogs')
.where('actorUid', '==', actorUid).where('denied', '==', true).get()).size;
beforeAll(() => { bootAdmin(); });
beforeEach(async () => {
const db = getFirestore();
await db.recursiveDelete(db.collection('adminActionLogs'));
await db.doc('config/admin-write-test').delete();
await db.doc('notices/admin-write-test').delete();
});
afterAll(async () => { await shutdownAdmin(); });
it('ops 는 master 전용 문서(config)를 못 쓰고 거부가 기록된다 · master 는 쓴다', async () => {
await expect(run('ops', 'config/admin-write-test'))
.rejects.toMatchObject({ details: { code: 'E-PERM-01' } });
expect((await getFirestore().doc('config/admin-write-test').get()).exists).toBe(false);
expect(await denials('admin-ops')).toBe(1);
await run('master', 'config/admin-write-test');
expect((await getFirestore().doc('config/admin-write-test').get()).get('attendPop')).toBe(999);
});
it('ops 는 master 전용이 아닌 문서는 그대로 쓴다 · 역할 없는 호출은 거부가 기록된다', async () => {
await run('ops', 'notices/admin-write-test');
expect((await getFirestore().doc('notices/admin-write-test').get()).exists).toBe(true);
await expect(run(undefined, 'notices/admin-write-test'))
.rejects.toMatchObject({ details: { code: 'E-PERM-01' } });
expect(await denials('admin-none')).toBe(1);
});
it('master 도 원장 · 지표 · 감사 기록과 서버 산출 필드는 못 쓴다 · Eclipse 운영값은 master 만(리뷰 H3)', async () => {
const denied = (role: string, path: string, patch?: Record<string, unknown>) =>
expect(run(role, path, patch)).rejects.toMatchObject({ details: { code: 'E-PERM-01' } });
await denied('master', 'fandoms/f1/fpiEvents/e1');
await denied('master', 'fandoms/f1/eclipseRounds/r1');
await denied('master', 'fandoms/f1/fandomDailyStats/2026-09-20');
await denied('master', 'adminActionLogs/l1');
await denied('master', 'fandoms/admin-write-test', { fpiScore: 1 });
await denied('master', 'fandoms/admin-write-test', { growthPop: 1 });
await denied('master', 'users/u1', { userExp: 1 });
await denied('master', 'users/u1', { passportTier: 'DIAMOND' });
await denied('ops', 'fandoms/admin-write-test', { eclipseSchedule: [] });
expect(await denials('admin-master')).toBe(8);
await run('master', 'fandoms/admin-write-test', { eclipseSchedule: [{ hhmm: '09:00' }] });
await run('ops', 'fandoms/admin-write-test', { description: '운영 문구' });
const doc = await getFirestore().doc('fandoms/admin-write-test').get();
expect(doc.get('eclipseSchedule')).toEqual([{ hhmm: '09:00' }]);
expect(doc.get('fpiScore')).toBeUndefined();
await getFirestore().doc('fandoms/admin-write-test').delete();
});
// 퀘스트 목록 *퀘스트 생성 최신순*(Figma `518:88395`) — 처음 쓴 문서만 생성 시각을 받고, 수정은 그대로 둔다.
it('처음 쓰는 문서에는 createdAt 을 찍고 다시 써도 바꾸지 않는다', async () => {
const ref = getFirestore().doc('notices/admin-write-test');
await run('ops', ref.path, { title: '첫' });
const first = (await ref.get()).get('createdAt');
expect(first).toBeDefined();
await run('ops', ref.path, { title: '둘' });
expect((await ref.get()).get('createdAt').isEqual(first)).toBe(true);
});