알림함 항목 생성 · 알림 이동 · 브리핑 항목 승인·상세 · 푸시 토픽을 가입 팬덤 전체로

This commit is contained in:
Sn-Kinos
2026-10-05 22:48:10 +09:00
parent fbf9a2c3d7
commit cf608a1d99
27 changed files with 714 additions and 69 deletions
+46 -1
View File
@@ -4,7 +4,7 @@ import type { CallableRequest } from 'firebase-functions/v2/https';
import { bootAdmin, shutdownAdmin } from './support.js';
import {
candidateDocId, fnBriefingIngest, fnBriefingPublish, validateCandidate,
candidateDocId, fnBriefingIngest, fnBriefingPublish, fnBriefingReject, validateCandidate,
} from '../src/briefing/briefing.js';
import { dayKey } from '../src/core/time.js';
import { fandomTopic, notifyEclipseWarning } from '../src/notify/push.js';
@@ -276,6 +276,51 @@ describe('승인 — fn-briefing-publish (export 직접 호출)', () => {
});
});
describe('항목별 검수 — 승인 · 반려(상세정의 §12)', () => {
const day = () => dayKey(new Date());
async function ids(): Promise<string[]> {
await fnBriefingIngest.run(req('ops', {
items: [
candidate({ sourceUrl: 'https://example.com/i/1' }),
candidate({ sourceUrl: 'https://example.com/i/2' }),
candidate({ section: 'SNS', sourceUrl: 'https://example.com/i/3', sourceKind: 'fandom' }),
],
}));
const docs = await getFirestore().collection('briefings').where('fandomId', '==', FANDOM).get();
return docs.docs.map((d) => d.id).sort();
}
it('출처 구분은 선택 필드 — 받으면 저장하고, 정해진 값이 아니면 거부한다', async () => {
expect(validateCandidate(candidate({ sourceKind: 'blog' })).ok).toBe(false);
const [, , third] = await ids();
const kinds = (await getFirestore().collection('briefings').where('fandomId', '==', FANDOM).get())
.docs.map((d) => d.get('sourceKind'));
expect(kinds.filter((k) => k === 'fandom')).toHaveLength(1);
expect(third).toBeTruthy();
});
it('고른 항목만 게시하고, 반려는 사유와 함께 닫고 다시 게시되지 않는다', async () => {
const [a, b, c] = await ids();
await fnBriefingReject.run(req('ops', { itemIds: [b], reason: '출처 불명확' }));
const result = await fnBriefingPublish.run(req('ops', { fandomId: FANDOM, dayKey: day(), itemIds: [a] }));
expect(result.published).toBe(1);
const db = getFirestore();
expect((await db.doc(`briefings/${a}`).get()).get('status')).toBe('published');
const rejected = await db.doc(`briefings/${b}`).get();
expect(rejected.get('status')).toBe('rejected');
expect(rejected.get('rejectReason')).toBe('출처 불명확');
expect((await db.doc(`briefings/${c}`).get()).get('status')).toBe('draft');
// 반려 · 게시된 항목은 다시 고를 수 없다.
await expect(fnBriefingPublish.run(req('ops', { fandomId: FANDOM, dayKey: day(), itemIds: [b] })))
.rejects.toThrowError('초안이 아닌 항목');
await expect(fnBriefingReject.run(req('ops', { itemIds: [a], reason: 'x' })))
.rejects.toThrowError('초안만 반려');
await expect(fnBriefingReject.run(req('viewer', { itemIds: [c], reason: 'x' })))
.rejects.toThrowError('권한이 없습니다.');
});
});
describe('당일 필터 — 앱이 읽는 질의', () => {
it('게시된 당일분만 고르고 초안과 전일분은 제외한다', async () => {
const db = getFirestore();