import { assertFails, assertSucceeds } from '@firebase/rules-unit-testing'; import { deleteObject, ref, uploadBytes } from 'firebase/storage'; import { afterAll, beforeAll, describe, test } from 'vitest'; import { createStorageTestEnvironment } from '../support/environment.mjs'; /// 프로필 사진 업로드 — 정책 원장 B-44 의 *Storage 규칙 서버 검증* 이 실제로 /// 거르는지 본다. 클라이언트 1차 검증(`checkProfilePhoto`)은 앱 쪽 테스트가 잡고, /// 여기서는 **앱을 고쳐 우회해도 막히는가**를 확인한다. let testEnvironment; const storageFor = (uid) => testEnvironment.authenticatedContext(uid).storage(); const anonymousStorage = () => testEnvironment.unauthenticatedContext().storage(); /// 규칙은 내용을 보지 않고 크기와 선언된 종류만 본다. const bytesOf = (size) => new Uint8Array(size); const image = { contentType: 'image/png' }; beforeAll(async () => { testEnvironment = await createStorageTestEnvironment(); }); afterAll(async () => { await testEnvironment.cleanup(); }); describe('프로필 사진 — users/{uid}/profile', () => { test('본인 경로에 이미지를 올린다', async () => { await assertSucceeds( uploadBytes(ref(storageFor('alice'), 'users/alice/profile/photo'), bytesOf(1024), image), ); }); test('10MB 를 넘으면 거부한다', async () => { await assertFails( uploadBytes( ref(storageFor('alice'), 'users/alice/profile/photo'), bytesOf(10 * 1024 * 1024 + 1), image, ), ); }); test('이미지가 아닌 종류는 거부한다', async () => { await assertFails( uploadBytes( ref(storageFor('alice'), 'users/alice/profile/photo'), bytesOf(1024), { contentType: 'application/pdf' }, ), ); }); test('남의 경로에는 올리지 못한다', async () => { await assertFails( uploadBytes(ref(storageFor('bob'), 'users/alice/profile/photo'), bytesOf(1024), image), ); }); test('로그인하지 않으면 올리지 못한다', async () => { await assertFails( uploadBytes(ref(anonymousStorage(), 'users/alice/profile/photo'), bytesOf(1024), image), ); }); test('라운지 사진은 본인 경로에 10MB 이하 이미지만, 지우기도 본인만', async () => { const id = 'AbCdEfGhIjKlMnOpQrSt'; // Firestore 자동 ID 모양(20자) const path = `users/alice/lounge/${id}-0`; await assertSucceeds(uploadBytes(ref(storageFor('alice'), path), bytesOf(1024), image)); // 이미 올린 파일 덮어쓰기는 닫는다 — 글의 URL 이 가리키는 내용이 바뀐다. await assertFails(uploadBytes(ref(storageFor('alice'), path), bytesOf(1024), image)); await assertFails( uploadBytes( ref(storageFor('alice'), `users/alice/lounge/${id}-1`), bytesOf(10 * 1024 * 1024 + 1), image, ), ); await assertFails( uploadBytes(ref(storageFor('alice'), `users/alice/lounge/${id}-2`), bytesOf(1024), { contentType: 'application/pdf', }), ); await assertFails(uploadBytes(ref(storageFor('bob'), `users/alice/lounge/${id}-3`), bytesOf(1024), image)); // 파일 이름 규칙 밖 — 칸 번호 5 · 임의 이름. await assertFails(uploadBytes(ref(storageFor('alice'), `users/alice/lounge/${id}-5`), bytesOf(1024), image)); await assertFails(uploadBytes(ref(storageFor('alice'), 'users/alice/lounge/anything'), bytesOf(1024), image)); await assertFails(deleteObject(ref(storageFor('bob'), path))); await assertSucceeds(deleteObject(ref(storageFor('alice'), path))); }); test('방문 인증 사진은 본인 경로에 10MB 이하 이미지 한 번만, 지우지 못한다 (C-03)', async () => { const path = 'questProofs/alice/quest-1/1.jpg'; const jpeg = { contentType: 'image/jpeg' }; await assertSucceeds(uploadBytes(ref(storageFor('alice'), path), bytesOf(1024), jpeg)); await assertFails(uploadBytes(ref(storageFor('alice'), path), bytesOf(1024), jpeg)); await assertFails(uploadBytes(ref(storageFor('bob'), 'questProofs/alice/quest-1/2.jpg'), bytesOf(1024), jpeg)); await assertFails( uploadBytes(ref(storageFor('alice'), 'questProofs/alice/quest-1/3.jpg'), bytesOf(10 * 1024 * 1024 + 1), jpeg), ); await assertFails(deleteObject(ref(storageFor('alice'), path))); }); test('열지 않은 경로는 기본 거부다', async () => { await assertFails( uploadBytes(ref(storageFor('alice'), 'posts/post-1/media/1.png'), bytesOf(1024), image), ); }); }); describe('캠페인 결과보고 사진·영수증 — campaignReports/{campaignId} (B-35 운영 전용)', () => { const path = 'campaignReports/campaign-1/r1.png'; const ops = () => testEnvironment.authenticatedContext('ops-user', { role: 'ops' }).storage(); test('운영이 이미지를 올린다', async () => { await assertSucceeds(uploadBytes(ref(ops(), path), bytesOf(1024), image)); }); test('운영이 아니면(개설자 포함) · 10MB 초과 · 이미지 아닌 종류는 거부한다', async () => { await assertFails(uploadBytes(ref(storageFor('alice'), path), bytesOf(1024), image)); await assertFails(uploadBytes(ref(ops(), path), bytesOf(10 * 1024 * 1024 + 1), image)); await assertFails(uploadBytes(ref(ops(), path), bytesOf(1024), { contentType: 'text/html' })); }); });