Files
fanuzu/app/firebase/test/tests/storage.rules.test.mjs
T

117 lines
4.6 KiB
JavaScript

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('열지 않은 경로는 기본 거부다', 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' }));
});
});