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

84 lines
4.6 KiB
TypeScript

import { afterAll, beforeAll, beforeEach, describe, expect, it } from 'vitest';
import { getFirestore, Timestamp } from 'firebase-admin/firestore';
import { bootAdmin, shutdownAdmin, wipeUser } from './support.js';
import { countParticipant, participateStreaming } from '../src/quest/quest.js';
import { TEST_PAYOUT_MARK, popIssuance } from '../src/admin/admin.js';
/**
* 운영 Quest 「참여 인원」(10/6 관찰 「참여 인원 0」) · C-123 테스트 지급분 집계 제외.
* 트리거 본체([countParticipant])를 진행 문서 값으로 직접 부른다 — 에뮬레이터에 함수 트리거가 없다.
*/
const A = 'test-part-a';
const B = 'test-part-b';
const db = () => getFirestore();
const countOf = async (id: string) => (await db().doc(`quests/${id}`).get()).get('participantCount') as number | undefined;
beforeAll(() => { bootAdmin(); });
afterAll(async () => { await shutdownAdmin(); });
beforeEach(async () => {
for (const uid of [A, B]) await wipeUser(uid);
for (const id of ['op-stream', 'op-shake', 'ugc-q']) await db().recursiveDelete(db().doc(`quests/${id}`));
await db().doc(`users/${A}`).set({ uid: A });
await db().doc(`users/${B}`).set({ uid: B });
});
describe('운영 Quest 참여 인원 — 사람 단위 · 멱등', () => {
it('완료 진행 문서로 1명 · 같은 사람 재전달 · 다음 날 재참여는 그대로 · 다른 사람은 +1', async () => {
await db().doc('quests/op-stream').set({ type: 'streaming', title: '스밍', dailyLimit: 1 });
await participateStreaming(A, 'op-stream');
const progress = (await db().doc(`users/${A}/questProgress/op-stream`).get()).data()!;
expect(await countParticipant(A, 'op-stream', progress)).toBe(true);
expect(await countParticipant(A, 'op-stream', progress)).toBe(false);
expect(await countParticipant(A, 'op-stream', { count: 1 })).toBe(false);
expect(await countOf('op-stream')).toBe(1);
expect(await countParticipant(B, 'op-stream', { count: 1 })).toBe(true);
expect(await countOf('op-stream')).toBe(2);
});
it('진행 세션 시작(Music Shake 「참여」)도 1명 · 빈 진행 문서는 세지 않는다', async () => {
await db().doc('quests/op-shake').set({ type: 'musicShake', title: '셰이크' });
expect(await countParticipant(A, 'op-shake', {})).toBe(false);
expect(await countParticipant(A, 'op-shake', { session: { state: 'inProgress' } })).toBe(true);
expect(await countParticipant(A, 'op-shake', { count: 1 })).toBe(false);
expect(await countOf('op-shake')).toBe(1);
});
it('사용자 생성 Quest · 퀘스트 문서가 없는 진행 문서(ad)는 건너뛴다', async () => {
await db().doc('quests/ugc-q').set({ type: 'place', authorUid: 'someone', participantCount: 3 });
expect(await countParticipant(A, 'ugc-q', { count: 1 })).toBe(false);
expect(await countOf('ugc-q')).toBe(3);
expect(await countParticipant(A, 'ad', { count: 1 })).toBe(false);
expect((await db().doc('quests/ad').get()).exists).toBe(false);
});
});
describe('C-123 테스트 지급분 — 원장 불변 · 발행량 대조에서 제외', () => {
const at = (h: number) => Timestamp.fromDate(new Date(Date.UTC(2020, 0, 1, h)));
const row = (uid: string, id: string, data: Record<string, unknown>) =>
db().doc(`users/${uid}/popTransactions/${id}`).set({ type: 'grant', ...data });
it('표시된 행은 bySource 에서 빠지고 testPayout 에 따로 · 원장 행은 그대로', async () => {
await row(A, 'tx-test', { popType: 'activity', reasonCode: 'AD_REWARD', amount: 10, createdAt: at(1) });
await row(A, 'tx-ok', { popType: 'support', reasonCode: 'AD_REWARD', amount: 10, createdAt: at(2) });
await row(B, 'tx-attend', { popType: 'activity', reasonCode: 'QUEST_ATTEND', amount: 10, createdAt: at(3) });
await db().doc('popTransactionMarks/tx-test').set({
mark: TEST_PAYOUT_MARK, reason: 'C-123', path: `users/${A}/popTransactions/tx-test`, markedAt: at(5),
});
try {
const report = await popIssuance(at(0).toDate(), at(4).toDate());
expect(report.bySource).toEqual({
'grant:support:AD_REWARD': { count: 1, amount: 10 },
'grant:activity:QUEST_ATTEND': { count: 1, amount: 10 },
});
expect(report.testPayout).toEqual({ 'grant:activity:AD_REWARD': { count: 1, amount: 10 } });
// 원장 행은 수정 · 삭제 0건.
expect((await db().doc(`users/${A}/popTransactions/tx-test`).get()).data())
.toMatchObject({ amount: 10, popType: 'activity', reasonCode: 'AD_REWARD' });
} finally {
await db().doc('popTransactionMarks/tx-test').delete();
}
});
});