80 lines
4.1 KiB
TypeScript
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);
|
|
});
|