85 lines
4.1 KiB
TypeScript
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' });
|
|
});
|