광고 추가 보상 서버 시청 검증 · 사용자 퀘스트 승인 후 공개 · TIME ATTACK 모드 · 만보기 퀘스트 · 위치 퀘스트 지도 선택 · 운영 퀘스트 보상 재원
This commit is contained in:
@@ -81,8 +81,10 @@ beforeEach(async () => {
|
||||
const snap = await db().collection(`users/${UID}/${sub}`).get();
|
||||
await Promise.all(snap.docs.map((d) => d.ref.delete()));
|
||||
}
|
||||
const proofs = await db().collection('placeProofs').get();
|
||||
await Promise.all(proofs.docs.map((d) => d.ref.delete()));
|
||||
for (const c of ['placeProofs', 'adSsv', 'adRewardLogs']) {
|
||||
const docs = await db().collection(c).get();
|
||||
await Promise.all(docs.docs.map((d) => d.ref.delete()));
|
||||
}
|
||||
const events = await db().collection(`fandoms/${FANDOM}/fpiEvents`).get();
|
||||
await Promise.all(events.docs.map((d) => d.ref.delete()));
|
||||
await db().doc(`users/${UID}`).set({ uid: UID, fandomId: FANDOM, totalContributedPop: 0 });
|
||||
@@ -345,29 +347,52 @@ describe('클라이언트가 고른 ID 로 한도를 다시 열 수 없다 (리
|
||||
const adRows = async () => (await db().collection(`users/${UID}/popTransactions`)
|
||||
.where('reasonCode', '==', 'AD_REWARD').get()).docs;
|
||||
|
||||
it('광고: 노출 ID 를 바꿔 동시에 불러도 일일 한도를 넘지 않고, 같은 노출 재전송은 첫 결과를 돌려준다', async () => {
|
||||
/** Google 이 보낸 SSV 콜백(서명 검증 통과)을 흉내 낸다 — fn-ad-ssv 가 남기는 기록과 같은 모양. */
|
||||
async function ssv(baseTxId: string, transactionId = `ssv-${baseTxId}-${Math.random()}`) {
|
||||
await db().doc(`adSsv/${transactionId}`).set({ uid: UID, baseTxId, adNetwork: 'admob' });
|
||||
return transactionId;
|
||||
}
|
||||
const ad = (baseTxId: string) => call(fnAdReward, { baseTxId });
|
||||
|
||||
it('광고: SSV 기록 없이 함수만 부르면 증분을 주지 않고 횟수도 차감하지 않는다 (B-03 · D-26)', async () => {
|
||||
const baseTxId = await baseReward();
|
||||
await db().doc('config/policy').set({ adDailyLimit: 1 }, { merge: true });
|
||||
await Promise.allSettled([
|
||||
call(fnAdReward, { adImpressionId: `imp-a-${Date.now()}`, baseTxId }),
|
||||
call(fnAdReward, { adImpressionId: `imp-b-${Date.now()}`, baseTxId }),
|
||||
]);
|
||||
expect(await codeOf(() => ad(baseTxId))).toBe('E-AD-03');
|
||||
// 다른 사람의 SSV 기록은 증거가 아니다.
|
||||
await db().doc('adSsv/ssv-other').set({ uid: 'someone-else', baseTxId, adNetwork: 'admob' });
|
||||
expect(await codeOf(() => ad(baseTxId))).toBe('E-AD-03');
|
||||
expect((await adRows()).length).toBe(0);
|
||||
expect((await db().doc(`users/${UID}/questProgress/ad`).get()).exists).toBe(false);
|
||||
await ssv(baseTxId);
|
||||
expect((await ad(baseTxId)).rewarded).toBe(10);
|
||||
});
|
||||
|
||||
it('광고: 동시에 불러도 한 번만 지급하고, 재전송은 첫 결과를 돌려준다', async () => {
|
||||
const baseTxId = await baseReward();
|
||||
await ssv(baseTxId, 'ssv-a');
|
||||
await ssv(baseTxId, 'ssv-b');
|
||||
await Promise.allSettled([ad(baseTxId), ad(baseTxId)]);
|
||||
const ledger = await adRows();
|
||||
expect(ledger.length).toBe(1);
|
||||
await db().doc('config/policy').set({ adDailyLimit: 5 }, { merge: true });
|
||||
// 성공한 호출의 재전송(응답 유실) — 오류가 아니라 같은 txId 로 성공, 지급은 늘지 않는다.
|
||||
const used = ledger[0]!.get('refPath') as string;
|
||||
const again = await call(fnAdReward, { adImpressionId: used.split('/')[1], baseTxId });
|
||||
const again = await ad(baseTxId);
|
||||
expect(again.duplicate).toBe(true);
|
||||
expect(again.txId).toBe(ledger[0]!.id);
|
||||
// 앱은 이 값을 보상 화면에 띄운다 — 재전송에서도 원장 행의 실제 지급액이다.
|
||||
expect(again.rewarded).toBe(ledger[0]!.get('amount'));
|
||||
expect((await adRows()).length).toBe(1);
|
||||
});
|
||||
|
||||
it('광고: 일일 한도를 넘으면 E-AD-02', async () => {
|
||||
await db().doc('config/policy').set({ adDailyLimit: 0 }, { merge: true });
|
||||
const baseTxId = await baseReward();
|
||||
await ssv(baseTxId);
|
||||
expect(await codeOf(() => ad(baseTxId))).toBe('E-AD-02');
|
||||
await db().doc('config/policy').set({ adDailyLimit: 5 }, { merge: true });
|
||||
});
|
||||
|
||||
it('광고: 운영 Quest 는 기본 x2 증분만 기여 POP 으로 지급한다 (B-03)', async () => {
|
||||
const baseTxId = await baseReward(30);
|
||||
const reply = await call(fnAdReward, { adImpressionId: 'imp-x2', baseTxId });
|
||||
await ssv(baseTxId);
|
||||
const reply = await ad(baseTxId);
|
||||
expect(reply.rewarded).toBe(30);
|
||||
const row = only(await adRows());
|
||||
expect(row.get('popType')).toBe('support');
|
||||
@@ -381,27 +406,27 @@ describe('클라이언트가 고른 ID 로 한도를 다시 열 수 없다 (리
|
||||
it('광고: 지정 Quest 의 x3 은 기본 보상의 2배를 증분으로 준다 (B-03)', async () => {
|
||||
await db().doc(`quests/${ATTEND_QUEST}`).update({ adMultiplier: 3 });
|
||||
const baseTxId = await baseReward(30);
|
||||
expect((await call(fnAdReward, { adImpressionId: 'imp-x3', baseTxId })).rewarded).toBe(60);
|
||||
await ssv(baseTxId);
|
||||
expect((await ad(baseTxId)).rewarded).toBe(60);
|
||||
});
|
||||
|
||||
it('광고: 사용자 생성 Quest 는 기본 보상과 무관한 고정 10 기여 POP 이다 (C-02 · B-28)', async () => {
|
||||
await db().doc(`quests/${ATTEND_QUEST}`).update({ authorUid: 'creator-1' });
|
||||
const baseTxId = await baseReward(30);
|
||||
expect((await call(fnAdReward, { adImpressionId: 'imp-ugc', baseTxId })).rewarded).toBe(10);
|
||||
await ssv(baseTxId);
|
||||
expect((await ad(baseTxId)).rewarded).toBe(10);
|
||||
const row = only(await adRows());
|
||||
expect(row.get('popType')).toBe('support');
|
||||
});
|
||||
|
||||
it('광고: 기본 보상 한 건에 광고 보상은 한 번 · 퀘스트 보상이 아닌 행에는 붙지 않는다', async () => {
|
||||
it('광고: 퀘스트 보상이 아닌 행에는 붙지 않는다', async () => {
|
||||
expect(await codeOf(() => ad('no-such-tx'))).toBe('E-QST-01');
|
||||
const baseTxId = await baseReward();
|
||||
await call(fnAdReward, { adImpressionId: 'imp-once-1', baseTxId });
|
||||
expect(await codeOf(() => call(fnAdReward, { adImpressionId: 'imp-once-2', baseTxId })))
|
||||
.toBe('E-POP-03');
|
||||
expect(await codeOf(() => call(fnAdReward, { adImpressionId: 'imp-none', baseTxId: 'no-such-tx' })))
|
||||
.toBe('E-QST-01');
|
||||
await ssv(baseTxId);
|
||||
await ad(baseTxId);
|
||||
const adTx = only(await adRows()).id;
|
||||
expect(await codeOf(() => call(fnAdReward, { adImpressionId: 'imp-self', baseTxId: adTx })))
|
||||
.toBe('E-QST-01');
|
||||
await ssv(adTx);
|
||||
expect(await codeOf(() => ad(adTx))).toBe('E-QST-01');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user