Files

85 lines
4.1 KiB
TypeScript

import { afterAll, beforeAll, beforeEach, expect, it, vi } from 'vitest';
import { getFirestore } from 'firebase-admin/firestore';
/** C-45 이벤트별 개인 Push — 알림 설정 존중 · 단말 토큰 · 죽은 토큰 정리. FCM 은 가짜로 바꾼다. */
const sent: Array<{ tokens: string[]; notification: unknown; data: Record<string, string> }> = [];
vi.mock('firebase-admin/messaging', () => ({
getMessaging: () => ({
sendEachForMulticast: async (m: { tokens: string[]; notification: unknown; data: Record<string, string> }) => {
sent.push(m);
const responses = m.tokens.map((t) => t === 'dead'
? { success: false, error: { code: 'messaging/registration-token-not-registered' } }
: { success: true });
return {
responses,
successCount: responses.filter((r) => r.success).length,
failureCount: responses.filter((r) => !r.success).length,
};
},
}),
}));
const { pushAllowed } = await import('../src/notify/push.js');
const { pushInboxItem } = await import('../src/notify/inbox.js');
const { bootAdmin, shutdownAdmin } = await import('./support.js');
const U = 'test-push-user';
const quest = {
eventType: 'quest' as const, type: 'essential' as const,
title: '[만보기] 퀘스트를 완료했어요!', source: '퀘스트', targetPath: 'quests/q1',
};
const eclipse = {
eventType: 'eclipse' as const, type: 'fandomEvent' as const,
title: '내 행성의 이클립스가 발생했어요.', source: 'BTS',
};
beforeAll(() => { bootAdmin(); });
beforeEach(async () => {
sent.length = 0;
await getFirestore().recursiveDelete(getFirestore().doc(`users/${U}`));
});
afterAll(async () => {
await getFirestore().recursiveDelete(getFirestore().doc(`users/${U}`));
await shutdownAdmin();
});
it('기본값은 Marketing 만 OFF · 서비스 필수는 끌 수 없다 (B-77)', () => {
expect(pushAllowed(undefined, 'fandomEvent')).toBe(true);
expect(pushAllowed(undefined, 'marketing')).toBe(false);
expect(pushAllowed({ fandomEvent: false }, 'fandomEvent')).toBe(false);
expect(pushAllowed({ essential: false }, 'essential')).toBe(true);
expect(pushAllowed({ marketing: true }, 'marketing')).toBe(true);
});
it('알림함 항목 문구 그대로 그 사용자 단말 전부에 보내고 죽은 토큰은 걷어 낸다', async () => {
await getFirestore().doc(`users/${U}`).set({ fcmTokens: ['t1', 'dead', 't1'] });
expect(await pushInboxItem(U, quest)).toBe('sent');
expect(sent).toHaveLength(1);
expect(sent[0]!.tokens).toEqual(['t1', 'dead']);
expect(sent[0]!.notification).toEqual({ title: 'FANUZU', body: '[만보기] 퀘스트를 완료했어요!' });
expect(sent[0]!.data).toMatchObject({ category: 'essential', kind: 'quest', targetPath: 'quests/q1' });
expect((await getFirestore().doc(`users/${U}`).get()).get('fcmTokens')).toEqual(['t1', 't1']);
});
it('설정에서 끈 구분은 보내지 않는다 — OFF 발송 0건', async () => {
await getFirestore().doc(`users/${U}`).set({ fcmTokens: ['t1'], notificationPrefs: { fandomEvent: false } });
expect(await pushInboxItem(U, eclipse)).toBe('off');
expect(await pushInboxItem(U, quest)).toBe('sent');
expect(sent).toHaveLength(1);
});
it('토큰이 없으면 보내지 않는다 · 구분을 모르는 항목은 건너뛴다', async () => {
await getFirestore().doc(`users/${U}`).set({});
expect(await pushInboxItem(U, eclipse)).toBe('no-token');
expect(await pushInboxItem(U, { title: 'x' })).toBeNull();
expect(sent).toHaveLength(0);
});
it('탭 이동 · C-78 단계에 쓰는 값을 싣는다 — 팬덤 · 이클립스 단계 · 캠페인 (v1.5 8장 Deep Link)', async () => {
await getFirestore().doc(`users/${U}`).set({ fcmTokens: ['t1'] });
expect(await pushInboxItem(U, { ...eclipse, fandomId: 'bts', phase: 'defended' })).toBe('sent');
expect(await pushInboxItem(U, { eventType: 'campaign', type: 'essential', title: '정산', source: '', campaignId: 'c1' })).toBe('sent');
expect(sent[0]!.data).toEqual({ category: 'fandomEvent', fandomId: 'bts', kind: 'eclipse', phase: 'defended' });
expect(sent[1]!.data).toEqual({ category: 'essential', kind: 'campaign', campaignId: 'c1' });
});