걸음 수 Health Connect·HealthKit · 위치 퀘스트 사진·QR 인증 · 행성 형태 확정

This commit is contained in:
Sn-Kinos
2026-10-05 23:57:01 +09:00
parent dc9861df38
commit dce2f64362
44 changed files with 930 additions and 66 deletions
+47 -13
View File
@@ -7,7 +7,7 @@ import { grantPop } from '../src/ledger/ledger.js';
import { verifiedQuestCountOf } from '../src/ledger/fpi.js';
import { fnAdminCompletionVoid } from '../src/admin/admin.js';
import { fnAdReward, fnQuestAttend, fnQuestVerifySteps } from '../src/quest/quest.js';
import { fnQuestVerifyMusicShake, fnQuestVerifyPlace } from '../src/quest/verify.js';
import { fnQuestVerifyMusicShake, fnQuestVerifyPlace, placeProofStorage } from '../src/quest/verify.js';
import { fnCampaignFund } from '../src/fandom/fandom.js';
/**
@@ -81,7 +81,7 @@ beforeEach(async () => {
const snap = await db().collection(`users/${UID}/${sub}`).get();
await Promise.all(snap.docs.map((d) => d.ref.delete()));
}
for (const c of ['placeProofs', 'adSsv', 'adRewardLogs']) {
for (const c of ['placeProofs', 'questSecrets', 'adSsv', 'adRewardLogs']) {
const docs = await db().collection(c).get();
await Promise.all(docs.docs.map((d) => d.ref.delete()));
}
@@ -503,20 +503,54 @@ describe('방문 — S-H-PLACE (fn-quest-verify-place)', () => {
expect((await db().collection(`users/${UID}/popTransactions`).get()).size).toBe(1);
});
it('반경 안 + QR 이면 장소 보상값으로 지급하고 인증 재사용을 막는다', async () => {
const result = await call(
fnQuestVerifyPlace, { ...inRange, proofKind: 'qr', proof: 'tok-ok' },
);
it('QR 을 고른 Quest 는 발급 코드와 같은 QR 이 GPS 와 함께 와야 지급하고 두 판정을 모두 남긴다(C-03)', async () => {
await db().doc('quests/place-quest-qr').set({ type: 'location', placeId: PLACE, verifyMethods: ['gps', 'qr'] });
await db().doc('questSecrets/place-quest-qr').set({ authorUid: 'someone', qrCode: 'code-ok' });
const q = { ...inRange, questId: 'place-quest-qr' };
// GPS 만 · 다른 코드 — 미완료.
expect(await codeOf(() => call(fnQuestVerifyPlace, q))).toBe('E-QST-01');
expect(await codeOf(() => call(fnQuestVerifyPlace, { ...q, qr: 'code-bad' }))).toBe('E-QST-01');
const result = await call(fnQuestVerifyPlace, { ...q, qr: 'code-ok' });
expect(result.rewarded).toBe(100);
expect(result.distanceM).toBeLessThanOrEqual(100);
const ok = (await db().collection(`users/${UID}/activityLogs`).where('verified', '==', true).get()).docs;
const evidence = only(ok).get('evidence') as { place: { gps: unknown; qr: { value: string; ok: boolean } } };
expect(evidence.place.gps).toBeDefined();
expect(evidence.place.qr).toEqual({ value: 'code-ok', ok: true });
// GPS 없이(반경 밖) QR 만으로는 0건.
await db().doc('quests/place-quest-qr2').set({ type: 'location', placeId: PLACE, verifyMethods: ['gps', 'qr'] });
await db().doc('questSecrets/place-quest-qr2').set({ authorUid: 'someone', qrCode: 'code-ok' });
expect(await codeOf(() => call(fnQuestVerifyPlace, { ...farAway, questId: 'place-quest-qr2', qr: 'code-ok' })))
.toBe('E-QST-03');
});
it('사진을 고른 Quest 는 본인이 이 Quest 로 올린 사진이어야 하고 같은 사진은 다시 못 쓴다', async () => {
const real = placeProofStorage.exists;
placeProofStorage.exists = async (path) => path.endsWith('/a.jpg');
try {
for (const id of ['place-quest-photo', 'place-quest-photo2']) {
await db().doc(`quests/${id}`).set({ type: 'location', placeId: PLACE, verifyMethods: ['gps', 'photo'] });
}
const q = { ...inRange, questId: 'place-quest-photo' };
expect(await codeOf(() => call(fnQuestVerifyPlace, q))).toBe('E-QST-01');
expect(await codeOf(() => call(fnQuestVerifyPlace, { ...q, photo: `questProofs/other/place-quest-photo/a.jpg` })))
.toBe('E-QST-01');
expect(await codeOf(() => call(fnQuestVerifyPlace, { ...q, photo: `questProofs/${UID}/place-quest-photo/none.jpg` })))
.toBe('E-QST-01');
const photo = `questProofs/${UID}/place-quest-photo/a.jpg`;
expect((await call(fnQuestVerifyPlace, { ...q, photo })).rewarded).toBe(100);
// 같은 파일 경로는 이 Quest 접두라 다른 Quest 에는 애초에 맞지 않는다.
expect(await codeOf(() => call(fnQuestVerifyPlace, { ...inRange, questId: 'place-quest-photo2', photo })))
.toBe('E-QST-01');
} finally {
placeProofStorage.exists = real;
}
});
it('추가 수단을 고르지 않은 Quest 에 온 QR 은 기록만 하고 GPS 로 판정한다', async () => {
const result = await call(fnQuestVerifyPlace, { ...inRange, proofKind: 'qr', proof: 'tok-ok' });
expect(result.rewarded).toBe(100);
const logs = await db().collection(`users/${UID}/activityLogs`).get();
expect(only(logs.docs).get('verified')).toBe(true);
// 같은 QR 토큰을 다른 Quest 에 다시 쓰는 시도.
expect(await codeOf(
() => call(fnQuestVerifyPlace, { ...inRange, questId: 'place-quest-reuse', proofKind: 'qr', proof: 'tok-ok' }),
)).toBe('E-POP-03');
expect(only(logs.docs).get('type')).toBe('placeQr');
});
});
@@ -541,6 +541,18 @@ describe('위치 선택 지도 핀 (Figma 371:356705)', () => {
expect((ok as Record<string, unknown>).rewarded).toBe(5);
});
it('추가 인증 수단(사진 · QR)을 고르면 GPS 에 덧붙고, QR 이면 생성자만 읽는 코드를 발급한다(C-03)', async () => {
const { questId } = await create({
activityKey: 'location', url: undefined, manualTitle: undefined,
pin: { lat: 37.4979, lng: 127.0276, address: '서울특별시 강남구 역삼동' },
extraVerify: ['qr', 'nfc', 'photo'],
});
expect((await quest(questId)).verifyMethods).toEqual(['gps', 'photo', 'qr']);
const secret = (await db().doc(`questSecrets/${questId}`).get()).data()!;
expect(secret.authorUid).toBe(CREATOR);
expect(secret.qrCode).toMatch(/^[0-9a-f]{24}$/);
});
it('좌표가 틀리면 거부한다', async () => {
expect(await codeOf(() => create({
activityKey: 'location', url: undefined, manualTitle: undefined,