Cloud Functions 도메인 함수 — POP 원장 · 퀘스트 · 이클립스 · 행성 성장 · 캠페인 · 결제 · 알림 · 브리핑 · INSIDE · 추천 · 번역 · 서버 테스트

This commit is contained in:
Sn-Kinos
2026-09-29 17:06:48 +09:00
parent f483e8d94b
commit 316821e7d5
61 changed files with 14660 additions and 60 deletions
@@ -0,0 +1,78 @@
import { afterAll, beforeAll, expect, it } from 'vitest';
import { getFirestore, Timestamp } from 'firebase-admin/firestore';
import { fnPlanetHistory } from '../src/fandom/history.js';
import { bootAdmin, shutdownAdmin } from './support.js';
const fandomId = 'history-test';
const uid = 'history-reader';
const call = (data: Record<string, unknown>, user = uid) =>
fnPlanetHistory.run({ data, ...(user ? { auth: { uid: user, token: {} } } : {}) } as never);
beforeAll(async () => {
bootAdmin();
const db = getFirestore();
for (let i = 1; i <= 22; i++) {
await db.doc(`fandoms/${fandomId}/stateHistory/${i}`).set({
kind: 'evolution', fromStage: 1, toStage: 2,
occurredAt: Timestamp.fromMillis(i * 1000), sourceEventId: 'private-source',
settings: { private: true },
});
}
await db.doc(`users/${uid}/growthEvents/one`).set({
fandomIdAtPayout: fandomId, amount: 10, sourceType: 'zap', reversalOf: null,
createdAt: Timestamp.fromMillis(10_000),
});
});
afterAll(async () => {
const db = getFirestore();
await db.recursiveDelete(db.doc(`fandoms/${fandomId}`));
await db.recursiveDelete(db.doc(`users/${uid}`));
await shutdownAdmin();
});
it('자신의 과거 기여만 확인하며 다른 사용자 ID나 내부 기록을 노출하지 않는다', async () => {
const result = await call({ fandomId });
expect(result.items).toHaveLength(20);
expect(result.items[0]!.participationConfirmed).toBe(true);
expect(result.items.at(-1)!.participationConfirmed).toBe(false);
expect(JSON.stringify(result)).not.toContain('private');
const stranger = await call({ fandomId, uid }, 'other-user');
expect(stranger.items.every((item) => item.participationConfirmed === false)).toBe(true);
});
it('커서 페이지가 겹치지 않으며 인증·경로·커서 오류를 거부한다', async () => {
const first = await call({ fandomId });
const second = await call({ fandomId, cursor: first.nextCursor });
expect(second.items).toHaveLength(2);
expect(second.nextCursor).toBeNull();
expect(new Set([...first.items, ...second.items].map((item) => item.id)).size).toBe(22);
await expect(call({ fandomId }, '')).rejects.toThrow();
await expect(call({ fandomId: '../users/private' })).rejects.toThrow();
await expect(call({ fandomId, cursor: 'missing' })).rejects.toThrow();
});
it('수량이 잘못된 기록은 개인 참여 증거로 쓰지 않는다', async () => {
await getFirestore().doc(`users/${uid}/growthEvents/one`).update({ amount: '10' });
const result = await call({ fandomId });
expect(result.items.every((item) => item.participationConfirmed === false)).toBe(true);
});
it('내 활동은 호출자와 지급 당시 팬덤으로 제한하고 다른 팬덤 커서를 거부한다', async () => {
const events = getFirestore().collection(`users/${uid}/growthEvents`);
await events.doc('one').update({ amount: 10 });
for (const [index, sourceType] of ['quest_reward', 'company_ad_bonus', 'zap', 'funding'].entries()) {
await events.doc(`activity-${index}`).set({
fandomIdAtPayout: fandomId, sourceType, amount: 10 + index, reversalOf: null,
createdAt: Timestamp.fromMillis(30_000 + index), sourceEventId: 'private-ledger',
});
}
await events.doc('other-fandom').set({ fandomIdAtPayout: 'other-fandom', sourceType: 'zap',
amount: 999, createdAt: Timestamp.fromMillis(40_000) });
const result = await call({ fandomId, scope: 'personal' });
expect(result.items).toHaveLength(5);
expect(result.items[0]!.kind).toBe('funding');
expect(result.items[0]!.amount).toBe(13);
expect(JSON.stringify(result)).not.toContain('private-ledger');
expect((await call({ fandomId, scope: 'personal', uid }, 'other-user')).items).toEqual([]);
await expect(call({ fandomId, scope: 'personal', cursor: 'other-fandom' })).rejects.toThrow();
await expect(call({ fandomId, scope: 'all-users' })).rejects.toThrow();
});