import { assertFails, assertSucceeds, } from '@firebase/rules-unit-testing'; import { collectionGroup, deleteDoc, doc, getDoc, getDocs, setDoc, updateDoc, } from 'firebase/firestore'; import { afterAll, beforeAll, beforeEach, describe, expect, test, } from 'vitest'; import { createTestEnvironment, seedDocuments, userDocument, } from '../support/environment.mjs'; let testEnvironment; const dbFor = (uid, claims = {}) => testEnvironment.authenticatedContext(uid, claims).firestore(); const anonymousDb = () => testEnvironment.unauthenticatedContext().firestore(); beforeAll(async () => { testEnvironment = await createTestEnvironment(); }); beforeEach(async () => { await testEnvironment.clearFirestore(); }); afterAll(async () => { await testEnvironment.cleanup(); }); describe('원칙 1 — POP·FPI·원장은 클라이언트 쓰기 불가', () => { test('본인 지갑 잔액 직접 증액을 거부한다', async () => { await seedDocuments(testEnvironment, { 'users/alice/wallet/main': { balance: 100 }, }); await assertFails(updateDoc(doc(dbFor('alice'), 'users/alice/wallet/main'), { balance: 999 })); }); test('POP 원장 문서 생성을 거부한다', async () => { await assertFails( setDoc(doc(dbFor('alice'), 'users/alice/popTransactions/forged'), { amount: 1000, reason: 'QUEST_REWARD', }), ); }); test('POP Lot 잔량 조작을 거부한다', async () => { await seedDocuments(testEnvironment, { 'users/alice/popLots/lot-1': { remaining: 100 }, }); await assertFails( updateDoc(doc(dbFor('alice'), 'users/alice/popLots/lot-1'), { remaining: 999_999, }), ); }); test('FPI 이벤트 위조를 거부한다', async () => { await assertFails( setDoc(doc(dbFor('alice'), 'fandoms/fandom-a/fpiEvents/forged'), { uid: 'alice', score: 999_999, }), ); }); test('누적 기여 통계 직접 조작을 거부한다', async () => { await seedDocuments(testEnvironment, { 'users/alice': userDocument('alice'), }); await assertFails( updateDoc(doc(dbFor('alice'), 'users/alice'), { totalContributedPop: 999_999 }), ); }); test('다른 사용자의 지갑 쓰기를 거부한다', async () => { await seedDocuments(testEnvironment, { 'users/bob/wallet/main': { balance: 100 }, }); await assertFails(updateDoc(doc(dbFor('alice'), 'users/bob/wallet/main'), { balance: 0 })); }); test('행성 성장 단계 조작을 거부한다', async () => { await seedDocuments(testEnvironment, { 'fandoms/fandom-a/planets/alice': { ownerUid: 'alice', stage: 1, colorKey: 'blue' }, }); await assertFails( updateDoc(doc(dbFor('alice'), 'fandoms/fandom-a/planets/alice'), { stage: 7 }), ); }); }); describe('단결형 아이템 접근 경계', () => { beforeEach(async () => { await seedDocuments(testEnvironment, { 'fandoms/fandom-a/solidarityBoosts/boost-1': { triggerUid: 'alice', multiplier: 2, startsAt: '2026-08-14T00:00:00Z', endsAt: '2026-08-14T01:00:00Z', scope: 'fandom', status: 'active', }, }); }); test('로그인 사용자는 단결형 활성 상태를 읽을 수 있다', async () => { await assertSucceeds( getDoc(doc(dbFor('alice'), 'fandoms/fandom-a/solidarityBoosts/boost-1')), ); }); test('미인증 사용자는 단결형 활성 상태를 읽을 수 없다', async () => { await assertFails( getDoc(doc(anonymousDb(), 'fandoms/fandom-a/solidarityBoosts/boost-1')), ); }); test('일반 사용자는 단결형 상태를 생성할 수 없다', async () => { await assertFails( setDoc(doc(dbFor('alice'), 'fandoms/fandom-a/solidarityBoosts/forged'), { triggerUid: 'alice', multiplier: 999, status: 'active', }), ); }); test('master도 단결형 상태를 수정할 수 없다', async () => { await assertFails( updateDoc( doc(dbFor('master-user', { role: 'master' }), 'fandoms/fandom-a/solidarityBoosts/boost-1'), { multiplier: 999 }, ), ); }); test('일반 사용자는 단결형 상태를 삭제할 수 없다', async () => { await assertFails( deleteDoc(doc(dbFor('alice'), 'fandoms/fandom-a/solidarityBoosts/boost-1')), ); }); }); describe('원칙 2 — 개인 리소스와 쓰기 필드 제한', () => { beforeEach(async () => { await seedDocuments(testEnvironment, { 'users/alice': userDocument('alice', { displayName: 'Alice' }), 'users/bob': userDocument('bob', { displayName: 'Bob' }), }); }); test('다른 사용자의 프로필 읽기를 거부한다', async () => { await assertFails(getDoc(doc(dbFor('alice'), 'users/bob'))); }); test('다른 사용자의 프로필 쓰기를 거부한다', async () => { await assertFails(updateDoc(doc(dbFor('alice'), 'users/bob'), { displayName: 'Forged' })); }); test('허용 필드와 서버 통계를 섞은 본인 업데이트를 전부 거부한다', async () => { await assertFails( updateDoc(doc(dbFor('alice'), 'users/alice'), { displayName: 'New Alice', totalContributedPop: 999_999, }), ); }); test('미인증 프로필 접근을 거부한다', async () => { await assertFails(getDoc(doc(anonymousDb(), 'users/alice'))); }); test('허용된 본인 프로필 필드만 바꾸면 허용한다', async () => { await assertSucceeds( updateDoc(doc(dbFor('alice'), 'users/alice'), { displayName: 'New Alice', locale: 'ko', updatedAt: '2026-08-08T01:00:00Z', }), ); }); }); describe('G-02 — 약관 동의 이력 무결성 회귀', () => { beforeEach(async () => { await seedDocuments(testEnvironment, { 'users/alice/consents/terms-v1': { termsVersion: '1.0', agreedAt: '2026-08-08T00:00:00Z', }, 'users/bob/consents/terms-v1': { termsVersion: '1.0', agreedAt: '2026-08-08T00:00:00Z', }, }); }); test.each([ [ '생성을', () => setDoc(doc(dbFor('alice'), 'users/alice/consents/terms-v2'), { termsVersion: '2.0', agreedAt: '2026-08-08T01:00:00Z', }), ], [ '수정을', () => updateDoc(doc(dbFor('alice'), 'users/alice/consents/terms-v1'), { agreedAt: '2099-12-31T00:00:00Z', }), ], [ '삭제를', () => deleteDoc(doc(dbFor('alice'), 'users/alice/consents/terms-v1')), ], ])('본인의 consents 문서 %s 거부한다', async (_operation, attempt) => { await assertFails(attempt()); }); test('다른 사용자의 consents 문서 읽기를 거부한다', async () => { await assertFails(getDoc(doc(dbFor('alice'), 'users/bob/consents/terms-v1'))); }); test('본인의 consents 문서 읽기를 허용한다', async () => { await assertSucceeds(getDoc(doc(dbFor('alice'), 'users/alice/consents/terms-v1'))); }); }); describe('F-01 — 컬렉션 그룹 쿼리 회귀', () => { test('로그인 사용자의 donations 컬렉션 그룹 조회를 허용한다', async () => { await seedDocuments(testEnvironment, { 'campaigns/campaign-a/donations/donation-1': { donorUid: 'alice', amount: 10 }, }); await assertSucceeds(getDocs(collectionGroup(dbFor('alice'), 'donations'))); }); test('미인증 donations 컬렉션 그룹 조회를 거부한다', async () => { await seedDocuments(testEnvironment, { 'campaigns/campaign-a/donations/donation-1': { donorUid: 'alice', amount: 10 }, }); await assertFails(getDocs(collectionGroup(anonymousDb(), 'donations'))); }); test('로그인 사용자의 answers 컬렉션 그룹 조회를 허용한다', async () => { await seedDocuments(testEnvironment, { 'questions/question-1/answers/answer-1': { authorUid: 'alice', body: 'answer' }, }); await assertSucceeds(getDocs(collectionGroup(dbFor('alice'), 'answers'))); }); test.each([ ['popTransactions', 'users/alice/popTransactions/tx-1'], ['activityLogs', 'users/alice/activityLogs/log-1'], ['questProgress', 'users/alice/questProgress/quest-1'], ['comments', 'posts/post-1/comments/comment-1'], ])('viewer의 %s 컬렉션 그룹 조회를 허용한다', async (group, path) => { await seedDocuments(testEnvironment, { [path]: { ownerUid: 'alice' } }); await assertSucceeds(getDocs(collectionGroup(dbFor('viewer', { role: 'viewer' }), group))); }); test.each([ ['popTransactions', 'users/alice/popTransactions/tx-1'], ['activityLogs', 'users/alice/activityLogs/log-1'], ['questProgress', 'users/alice/questProgress/quest-1'], ['comments', 'posts/post-1/comments/comment-1'], ])('일반 사용자의 %s 컬렉션 그룹 조회를 계속 거부한다', async (group, path) => { await seedDocuments(testEnvironment, { [path]: { ownerUid: 'alice' } }); await assertFails(getDocs(collectionGroup(dbFor('ordinary-user'), group))); }); }); describe('F-02 — 캠페인 집계 필드 위조 회귀', () => { const validCampaign = { fandomId: 'fandom-a', creatorUid: 'alice', creatorName: 'Alice', title: 'Campaign', summary: 'Summary', body: 'Body', coverImageUrl: 'https://example.test/cover.png', campaignType: 'other', targetPop: 1000, endsAt: '2026-09-01T00:00:00Z', status: 'draft', raisedPop: 0, achievedRate: 0, donorCount: 0, refundState: 'none', createdAt: '2026-08-08T00:00:00Z', updatedAt: '2026-08-08T00:00:00Z', }; beforeEach(async () => { await seedDocuments(testEnvironment, { 'users/alice': userDocument('alice', { displayName: 'Alice' }), }); }); test('정상 초기값의 캠페인 생성을 허용한다', async () => { await assertSucceeds(setDoc(doc(dbFor('alice'), 'campaigns/valid'), validCampaign)); }); test('집계 필드 위조를 거부한다', async () => { await assertFails( setDoc(doc(dbFor('alice'), 'campaigns/forged'), { ...validCampaign, achievedRate: 1, donorCount: 99_999, }), ); }); test('화이트리스트 밖 집계 필드 추가를 거부한다', async () => { await assertFails( setDoc(doc(dbFor('alice'), 'campaigns/extra-field'), { ...validCampaign, serverAggregate: 999_999, }), ); }); }); describe('F-05 — 작성자 표시명 사칭 회귀', () => { beforeEach(async () => { await seedDocuments(testEnvironment, { 'users/alice': userDocument('alice', { displayName: 'Alice' }), }); }); test('정상 게시물 생성을 허용한다', async () => { await assertSucceeds( setDoc(doc(dbFor('alice'), 'posts/valid'), { fandomId: 'fandom-a', authorUid: 'alice', authorName: 'Alice', body: 'hello', imageUrls: [], likeCount: 0, commentCount: 0, visibility: 'visible', createdAt: '2026-08-08T00:00:00Z', updatedAt: '2026-08-08T00:00:00Z', }), ); }); test('게시물 authorName 사칭을 거부한다', async () => { await assertFails( setDoc(doc(dbFor('alice'), 'posts/forged'), { fandomId: 'fandom-a', authorUid: 'alice', authorName: 'FANUZU 운영팀', body: 'forged', imageUrls: [], likeCount: 0, commentCount: 0, visibility: 'visible', createdAt: '2026-08-08T00:00:00Z', updatedAt: '2026-08-08T00:00:00Z', }), ); }); test('댓글 authorName 사칭을 거부한다', async () => { await assertFails( setDoc(doc(dbFor('alice'), 'posts/post-1/comments/forged'), { authorUid: 'alice', authorName: 'FANUZU 운영팀', body: 'forged', visibility: 'visible', createdAt: '2026-08-08T00:00:00Z', updatedAt: '2026-08-08T00:00:00Z', }), ); }); test('답변 authorName 사칭을 거부한다', async () => { await assertFails( setDoc(doc(dbFor('alice'), 'questions/question-1/answers/forged'), { authorUid: 'alice', authorName: 'FANUZU 운영팀', body: 'forged', sourceUrls: [], accepted: false, visibility: 'visible', createdAt: '2026-08-08T00:00:00Z', updatedAt: '2026-08-08T00:00:00Z', }), ); }); }); describe('F-14 — posts 허용 필드 회귀', () => { test('작성자도 스키마에 없는 title 필드를 추가할 수 없다', async () => { await seedDocuments(testEnvironment, { 'posts/post-1': { authorUid: 'alice', body: 'before', imageUrls: [], updatedAt: '2026-08-08T00:00:00Z', }, }); await assertFails( updateDoc(doc(dbFor('alice'), 'posts/post-1'), { title: '스키마 밖 필드', }), ); }); }); describe('F-06 — users 클라이언트 생성 경로 회귀', () => { test('일반 사용자의 users 문서 생성을 거부한다', async () => { await assertFails( setDoc( doc(dbFor('alice'), 'users/alice'), userDocument('alice', { totalContributedPop: 999_999 }), ), ); }); test('master 클레임도 users 문서를 직접 생성할 수 없다', async () => { await assertFails( setDoc( doc(dbFor('master-user', { role: 'master' }), 'users/new-user'), userDocument('new-user'), ), ); }); }); describe('F-07 — 결과보고 대상 상태 회귀', () => { test('실패 캠페인 결과보고 생성을 거부한다', async () => { await seedDocuments(testEnvironment, { 'campaigns/failed': { creatorUid: 'alice', status: 'fail' }, }); await assertFails( setDoc(doc(dbFor('alice'), 'campaigns/failed/reports/report-1'), { body: 'report' }), ); }); test('성공 캠페인 개설자의 결과보고 생성을 허용한다', async () => { await seedDocuments(testEnvironment, { 'campaigns/success': { creatorUid: 'alice', status: 'success' }, }); await assertSucceeds( setDoc(doc(dbFor('alice'), 'campaigns/success/reports/report-1'), { body: 'report' }), ); }); }); describe('F-08 — 미승인 캠페인 열람 회귀', () => { beforeEach(async () => { await seedDocuments(testEnvironment, { 'campaigns/review': { creatorUid: 'alice', status: 'review' }, 'campaigns/funding': { creatorUid: 'alice', status: 'funding' }, }); }); test('미인증 사용자의 review 캠페인 열람을 거부한다', async () => { await assertFails(getDoc(doc(anonymousDb(), 'campaigns/review'))); }); test('일반 타 사용자의 review 캠페인 열람을 거부한다', async () => { await assertFails(getDoc(doc(dbFor('bob'), 'campaigns/review'))); }); test('개설자와 ops의 review 캠페인 열람을 허용한다', async () => { await assertSucceeds(getDoc(doc(dbFor('alice'), 'campaigns/review'))); await assertSucceeds(getDoc(doc(dbFor('ops-user', { role: 'ops' }), 'campaigns/review'))); }); test('미인증 사용자의 funding 캠페인 열람은 계속 허용한다', async () => { await assertSucceeds(getDoc(doc(anonymousDb(), 'campaigns/funding'))); }); }); describe('원칙 3 — 권한은 커스텀 클레임 role로만 판정', () => { test('users 문서에 role=master를 심어도 관리자 권한을 얻지 못한다', async () => { await seedDocuments(testEnvironment, { 'users/forged-admin': userDocument('forged-admin', { role: 'master' }), 'adminActionLogs/log-1': { action: 'seed' }, 'admins/real-admin': { role: 'master' }, }); const forgedDb = dbFor('forged-admin'); await assertFails(getDoc(doc(forgedDb, 'adminActionLogs/log-1'))); await assertFails(getDoc(doc(forgedDb, 'admins/real-admin'))); await assertFails(setDoc(doc(forgedDb, 'config/pop'), { reward: 999_999 })); }); test('viewer는 관리자 조회 리소스를 읽을 수 있다', async () => { await seedDocuments(testEnvironment, { 'users/alice': userDocument('alice'), 'adminActionLogs/log-1': { action: 'seed' }, 'admins/admin-1': { role: 'master' }, 'adRewardLogs/reward-1': { uid: 'alice' }, }); const viewerDb = dbFor('viewer-user', { role: 'viewer' }); await assertSucceeds(getDoc(doc(viewerDb, 'users/alice'))); await assertSucceeds(getDoc(doc(viewerDb, 'adminActionLogs/log-1'))); await assertSucceeds(getDoc(doc(viewerDb, 'admins/admin-1'))); await assertSucceeds(getDoc(doc(viewerDb, 'adRewardLogs/reward-1'))); }); test('viewer의 관리자 쓰기를 거부한다', async () => { const viewerDb = dbFor('viewer-user', { role: 'viewer' }); await assertFails(setDoc(doc(viewerDb, 'notices/notice-1'), { title: 'forged' })); await assertFails(setDoc(doc(viewerDb, 'config/pop'), { reward: 999_999 })); }); const directAdminOperations = [ { name: '사용자 제재', minimumRole: 'ops', seed: { 'users/alice': userDocument('alice') }, run: (db) => updateDoc(doc(db, 'users/alice'), { status: 'suspended', banReason: 'test', updatedAt: '2026-08-08T01:00:00Z', }), }, { name: '아티스트 관리', minimumRole: 'ops', run: (db) => setDoc(doc(db, 'artists/artist-1'), { name: 'Artist' }), }, { name: '장소 관리', minimumRole: 'ops', run: (db) => setDoc(doc(db, 'places/place-1'), { name: 'Place' }), }, { name: '브리핑 수동 편집', minimumRole: 'ops', run: (db) => setDoc(doc(db, 'briefings/briefing-1'), { body: 'Briefing' }), }, { name: '캠페인 승인', minimumRole: 'ops', seed: { 'campaigns/campaign-1': { creatorUid: 'alice', status: 'review' } }, run: (db) => updateDoc(doc(db, 'campaigns/campaign-1'), { status: 'funding', reviewNote: 'approved', reviewedBy: 'admin', updatedAt: '2026-08-08T01:00:00Z', }), }, { name: '콘텐츠 숨김', minimumRole: 'ops', seed: { 'posts/post-1': { authorUid: 'alice', visibility: 'visible' } }, run: (db) => updateDoc(doc(db, 'posts/post-1'), { visibility: 'hidden', moderationNote: 'test', updatedAt: '2026-08-08T01:00:00Z', }), }, { name: '신고 처리', minimumRole: 'ops', seed: { 'reports/report-1': { reporterUid: 'alice', status: 'pending' } }, run: (db) => updateDoc(doc(db, 'reports/report-1'), { status: 'resolved', handledBy: 'admin', handleNote: 'test', updatedAt: '2026-08-08T01:00:00Z', }), }, { name: '문의 처리', minimumRole: 'ops', seed: { 'inquiries/inquiry-1': { authorUid: 'alice', status: 'pending' } }, run: (db) => updateDoc(doc(db, 'inquiries/inquiry-1'), { status: 'answered', answerBody: 'answer', handledBy: 'admin', updatedAt: '2026-08-08T01:00:00Z', }), }, { name: '스테이션 링크 관리', minimumRole: 'ops', run: (db) => setDoc(doc(db, 'stationLinks/link-1'), { url: 'https://example.test' }), }, { name: '제안 링크 심사', minimumRole: 'ops', seed: { 'stationSuggestions/suggestion-1': { authorUid: 'alice', status: 'pending' } }, run: (db) => updateDoc(doc(db, 'stationSuggestions/suggestion-1'), { status: 'approved', reviewNote: 'approved', updatedAt: '2026-08-08T01:00:00Z', }), }, { name: '공지 관리', minimumRole: 'ops', run: (db) => setDoc(doc(db, 'notices/notice-1'), { title: 'Notice' }), }, { name: '재화·FPI 설정', minimumRole: 'master', run: (db) => setDoc(doc(db, 'config/pop'), { reward: 10 }), }, { name: '점검·버전 설정', minimumRole: 'master', run: (db) => setDoc(doc(db, 'appConfig/current'), { maintenance: false }), }, { name: '다국어 문자열 관리', minimumRole: 'master', run: (db) => setDoc(doc(db, 'i18n/ko'), { hello: '안녕' }), }, { name: '행성 성장 단계 관리', minimumRole: 'master', run: (db) => setDoc(doc(db, 'planetStages/1'), { threshold: 100 }), }, { name: '퀘스트 파라미터 관리', minimumRole: 'master', run: (db) => setDoc(doc(db, 'quests/quest-1'), { reward: 10 }), }, { name: '배지 마스터 관리', minimumRole: 'master', run: (db) => setDoc(doc(db, 'badges/badge-1'), { name: 'Badge' }), }, ]; for (const operation of directAdminOperations) { test(`RBAC: viewer는 ${operation.name}을 수행할 수 없다`, async () => { if (operation.seed) await seedDocuments(testEnvironment, operation.seed); await assertFails(operation.run(dbFor('viewer-user', { role: 'viewer' }))); }); test(`RBAC: ops의 ${operation.name} 권한이 매트릭스와 일치한다`, async () => { if (operation.seed) await seedDocuments(testEnvironment, operation.seed); const attempt = operation.run(dbFor('ops-user', { role: 'ops' })); if (operation.minimumRole === 'ops') { await assertSucceeds(attempt); } else { await assertFails(attempt); } }); test(`RBAC: master는 ${operation.name}을 수행할 수 있다`, async () => { if (operation.seed) await seedDocuments(testEnvironment, operation.seed); await assertSucceeds(operation.run(dbFor('master-user', { role: 'master' }))); }); } test('운영 이상만 사전예약·행성개설 신청을 읽을 수 있다', async () => { await seedDocuments(testEnvironment, { 'preRegistrations/pre-1': { socialUid: 'alice' }, 'planetApplications/application-1': { applicantUid: 'alice' }, }); await assertFails(getDoc(doc(dbFor('viewer-user', { role: 'viewer' }), 'preRegistrations/pre-1'))); await assertFails(getDoc(doc(dbFor('viewer-user', { role: 'viewer' }), 'planetApplications/application-1'))); await assertSucceeds(getDoc(doc(dbFor('ops-user', { role: 'ops' }), 'preRegistrations/pre-1'))); await assertSucceeds(getDoc(doc(dbFor('ops-user', { role: 'ops' }), 'planetApplications/application-1'))); }); test.each([ ['POP 지갑', 'users/alice/wallet/main', { balance: 1 }], ['POP 원장', 'users/alice/popTransactions/tx-1', { amount: 1 }], ['FPI 이벤트', 'fandoms/fandom-a/fpiEvents/event-1', { score: 1 }], ['캠페인 후원·환불', 'campaigns/campaign-1/donations/donation-1', { amount: 1 }], ['사전예약', 'preRegistrations/pre-1', { socialUid: 'alice' }], ['행성 개설 신청', 'planetApplications/application-1', { applicantUid: 'alice' }], ['번역 검수 원천', 'translationEntries/entry-1', { body: 'translation' }], ['관리자 계정', 'admins/admin-1', { role: 'master' }], ['관리자 작업 로그', 'adminActionLogs/log-1', { action: 'forged' }], ])('Functions 전용 경계: master도 %s 직접 쓰기가 거부된다', async (_name, path, data) => { await assertFails(setDoc(doc(dbFor('master-user', { role: 'master' }), path), data)); }); }); describe('캠페인 후원·환불과 기본 거부', () => { test('일반 사용자의 캠페인 후원 문서 생성·수정·삭제를 모두 거부한다', async () => { await seedDocuments(testEnvironment, { 'campaigns/campaign-1/donations/donation-1': { donorUid: 'alice', amount: 10 }, }); const donation = doc(dbFor('alice'), 'campaigns/campaign-1/donations/donation-1'); await assertFails(setDoc(doc(dbFor('alice'), 'campaigns/campaign-1/donations/new'), { amount: 10 })); await assertFails(updateDoc(donation, { refundState: 'complete' })); await assertFails(deleteDoc(donation)); }); test.each([ ['ordinary', {}], ['master', { role: 'master' }], ])('규칙 밖 경로는 %s 사용자에게도 읽기·쓰기를 기본 거부한다', async (uid, claims) => { await seedDocuments(testEnvironment, { 'unmatchedCollection/secret': { value: 'secret' }, }); const firestore = dbFor(uid, claims); await assertFails(getDoc(doc(firestore, 'unmatchedCollection/secret'))); await assertFails(setDoc(doc(firestore, 'unmatchedCollection/new'), { value: 'forged' })); }); test('미인증 사용자의 규칙 밖 경로 읽기·쓰기를 기본 거부한다', async () => { await seedDocuments(testEnvironment, { 'unmatchedCollection/secret': { value: 'secret' }, }); await assertFails(getDoc(doc(anonymousDb(), 'unmatchedCollection/secret'))); await assertFails(setDoc(doc(anonymousDb(), 'unmatchedCollection/new'), { value: 'forged' })); }); }); describe('테스트 자가 점검', () => { test('규칙이 적재된 프로젝트 ID가 demo- 접두사를 사용한다', () => { expect(testEnvironment.projectId).toMatch(/^demo-/); }); });