라이브 티커 · 갤럭시 검색 색인 · 주간 랭킹 스냅샷 · 연속 ZAP 상한 · 가입 행성 Zap · 성운 목표 · 랭킹 갱신 안내 · 행성 터치 반응

This commit is contained in:
Sn-Kinos
2026-10-05 22:38:42 +09:00
parent 000c058097
commit 4bb92c2ee7
44 changed files with 727 additions and 78 deletions
+20 -1
View File
@@ -160,7 +160,7 @@ it('fn-pop-spend 는 Zap 만 받고 횟수를 서버가 세며 Zap 한 가입
const { fnPopSpend } = await import('../src/ledger/pop.js');
const run = (data: Record<string, unknown>) => fnPopSpend.run(
{ data, auth: { uid, token: {} } } as Parameters<typeof fnPopSpend.run>[0]);
await db.doc('config/policy').set({ zapPop: 10 }, { merge: true });
await db.doc('config/policy').set({ zapPop: 10, zapMinIntervalMs: 0 }, { merge: true });
await grantPop({ uid, popType: 'activity', amount: 100, reasonCode: 'ADMIN_GRANT', idempotencyKey: 'zap:seed' });
await expect(run({ popType: 'activity', amount: 10, reasonCode: 'CAMPAIGN_FUND', idempotencyKey: 'zap:h1' }))
@@ -192,3 +192,22 @@ it('fn-pop-spend 는 Zap 만 받고 횟수를 서버가 세며 Zap 한 가입
await db.recursiveDelete(db.collection(`fandoms/${a}/fpiEvents`));
await db.recursiveDelete(db.collection(`fandoms/${b}/fpiEvents`));
});
it('연속 ZAP Rate Limit — 한 호출 상한 · 호출 간격(운영값, FX §3)', async () => {
const db = getFirestore();
const { fnPopSpend } = await import('../src/ledger/pop.js');
const run = (data: Record<string, unknown>) => fnPopSpend.run(
{ data, auth: { uid, token: {} } } as Parameters<typeof fnPopSpend.run>[0]);
await db.doc('config/policy').set({ zapPop: 10, zapMaxPerCall: 3, zapMinIntervalMs: 60_000 }, { merge: true });
await grantPop({ uid, popType: 'activity', amount: 100, reasonCode: 'ADMIN_GRANT', idempotencyKey: 'rl:seed' });
await expect(run({ popType: 'activity', amount: 40, reasonCode: 'ZAP_CONTRIBUTION', idempotencyKey: 'rl:4' }))
.rejects.toMatchObject({ details: { code: 'E-QST-02' } });
await run({ popType: 'activity', amount: 30, reasonCode: 'ZAP_CONTRIBUTION', idempotencyKey: 'rl:1' });
// 같은 요청의 재시도는 간격에 걸리지 않는다(멱등).
await run({ popType: 'activity', amount: 30, reasonCode: 'ZAP_CONTRIBUTION', idempotencyKey: 'rl:1' });
await expect(run({ popType: 'activity', amount: 10, reasonCode: 'ZAP_CONTRIBUTION', idempotencyKey: 'rl:2' }))
.rejects.toMatchObject({ details: { code: 'E-QST-02' } });
expect((await db.doc(`users/${uid}/wallet/main`).get()).get('activityBalance')).toBe(70);
await db.doc('config/policy').set({ zapMaxPerCall: 50, zapMinIntervalMs: 400 }, { merge: true });
await db.recursiveDelete(db.collection(`fandoms/${a}/fpiEvents`));
});