Files
fanuzu/app/firebase/functions/test/planet_history.test.ts

105 lines
5.6 KiB
TypeScript

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();
});
it('내 기록 첫 장에 PASSPORT Stamp(진화 Emblem · 최초 100명 · Eclipse 성공 참여)를 함께 준다', async () => {
const db = getFirestore();
const { recordEclipseSuccess } = await import('../src/fandom/fandom.js');
const round = db.doc(`fandoms/${fandomId}/eclipseRounds/r1`);
await round.collection('participants').doc(uid).set({ qualified: true, validShakes: 60 });
await round.collection('participants').doc('not-qualified').set({ qualified: false, validShakes: 10 });
expect(await recordEclipseSuccess(fandomId, round)).toBe(1);
expect(await recordEclipseSuccess(fandomId, round)).toBe(1);
expect((await db.collection(`users/${uid}/passportHistory`).get()).size).toBe(1);
expect((await db.doc(`users/not-qualified/passportHistory/eclipse_${fandomId}_r1`).get()).exists).toBe(false);
await db.doc(`users/${uid}/badges/evolution_${fandomId}_2`).set({
kind: 'evolution', fandomId, toStage: 2, awardedAt: Timestamp.fromMillis(50_000) });
await db.doc(`users/${uid}/badges/founder100_${fandomId}`).set({
kind: 'founder100', fandomId, memberNo: 7, awardedAt: Timestamp.fromMillis(1_000) });
await db.doc(`users/${uid}/badges/founder100_other`).set({
kind: 'founder100', fandomId: 'other', memberNo: 1, awardedAt: Timestamp.fromMillis(1_000) });
const result = await call({ fandomId, scope: 'personal' });
const kinds = result.items.map((item) => item.kind);
expect(kinds[0]).toBe('eclipse_success');
expect(kinds).toContain('emblem_evolution');
expect(kinds.filter((k) => k === 'badge_founder100')).toHaveLength(1);
// 팬덤 범위에는 섞지 않는다.
expect((await call({ fandomId })).items.every((item) => item.kind === 'evolution')).toBe(true);
await db.recursiveDelete(db.doc('users/not-qualified'));
});