보안 규칙 테스트

앱을 빌드하는 동안 Firestore 데이터베이스 액세스를 차단해야 할 수 있습니다. 그러나 액세스를 차단하기 전에 더 자세한 Firestore 보안 규칙이 필요합니다. Firestore 에뮬레이터를 활용하면 Firestore 보안 규칙 동작을 확인하는 단위 테스트를 작성할 수 있습니다.

빠른 시작

단순한 규칙이 적용된 몇 가지 기본 테스트 사례는 자바스크립트 빠른 시작 또는 TypeScript 빠른 시작을 참조하세요.

Firestore 보안 규칙 이해

모바일 및 웹 클라이언트 라이브러리를 사용할 때 서버리스 인증, 승인, 데이터 유효성 검사를 위해 Firebase 인증Firestore 보안 규칙을 구현합니다.

Firestore 보안 규칙에는 다음 두 가지가 포함됩니다.

  1. match 문: 데이터베이스의 문서를 식별합니다.
  2. allow 표현식: 이러한 문서에 대한 액세스를 관리합니다.

Firebase 인증은 사용자 인증 정보를 확인하며 사용자 기반 및 역할 기반 액세스 시스템의 토대를 제공합니다.

Firestore 모바일/웹 클라이언트 라이브러리의 모든 데이터베이스 요청은 데이터를 읽거나 쓰기 전에 보안 규칙에 따라 평가됩니다. 규칙에서 지정된 문서 경로 중 일부라도 액세스를 거부하면 전체 요청이 실패합니다.

Firestore 보안 규칙 시작하기에서 Firestore 보안 규칙에 대해 자세히 알아보세요.

에뮬레이터 설치

Firestore 에뮬레이터를 설치하려면 Firebase CLI를 사용하여 다음 명령어를 실행합니다.

firebase setup:emulators:firestore

에뮬레이터 실행

다음 명령어를 사용하여 에뮬레이터를 시작합니다. 에뮬레이터는 프로세스가 중단될 때까지 실행됩니다.

firebase emulators:start --only firestore

대부분의 경우 에뮬레이터를 시작하려면 테스트 도구 모음을 실행한 다음 테스트가 실행된 후에 에뮬레이터를 중단합니다. emulators:exec 명령어를 사용하여 쉽게 수행할 수 있습니다.

firebase emulators:exec --only firestore "./my-test-script.sh"

시작 시 에뮬레이터는 기본 포트(8080)에서 실행을 시도합니다. firebase.json 파일의 "emulators" 섹션을 수정하여 에뮬레이터 포트를 변경할 수 있습니다.

{
  // ...
  "emulators": {
    "firestore": {
      "port": "YOUR_PORT"
    }
  }
}

에뮬레이터 실행 전 주의 사항

에뮬레이터 사용을 시작하려면 다음 사항에 주의하세요.

  • 에뮬레이터는 처음에 firebase.json 파일의 firestore.rules 필드에 지정된 규칙을 로드합니다. Firestore 보안 규칙이 포함된 로컬 파일의 이름을 요청한 후 이 규칙을 모든 프로젝트에 적용합니다. 로컬 파일 경로를 제공하지 않거나 아래 설명처럼 loadFirestoreRules 메서드를 사용하지 않으면 에뮬레이터는 공개 규칙에 따라 모든 프로젝트를 취급합니다.
  • 많은 SDK가 에뮬레이터와 작동하면 @firebase/testing Node.js 모듈만 보안 규칙에서 auth 모사를 지원하므로 단위 테스트가 훨씬 더 쉬워집니다. 또한 이 모듈은 아래 나열된 것처럼 모든 데이터 삭제 등 몇 가지 에뮬레이터 특정 기능을 지원합니다.
  • 에뮬레이터는 클라이언트 SDK를 통해 제공되는 프로덕션 Firebase 인증 토큰을 허용하고 그에 따라 규칙을 평가하므로 통합 및 수동 테스트 시 애플리케이션을 에뮬레이터에 직접 연결할 수 있습니다.

로컬 테스트 실행

initializeTestApp({ projectId: string, auth: Object }) => FirebaseApp

이 메소드는 옵션에 지정된 프로젝트 ID 및 인증 변수에 따라 초기화된 Firebase 앱을 반환합니다. 테스트에 사용할 특정 사용자로 인증된 앱을 만들려면 이 메소드를 사용합니다.

firebase.initializeTestApp({
  projectId: "my-test-project",
  auth: { uid: "alice", email: "alice@example.com" }
});

initializeAdminApp({ projectId: string }) => FirebaseApp

이 메서드는 초기화된 관리자 Firebase 앱을 반환합니다. 이 앱은 읽기 및 쓰기를 수행할 때 보안 규칙을 우회합니다. 테스트용 상태를 설정할 관리자로 인증된 앱을 만들려면 이 메서드를 사용하세요.

firebase.initializeAdminApp({ projectId: "my-test-project" });
    

apps() => [FirebaseApp] 이 메서드는 현재 초기화된 테스트 및 관리자 앱을 모두 반환합니다. 테스트 중에 또는 테스트 후에 앱을 정리하려면 이 메서드를 사용하세요.

Promise.all(firebase.apps().map(app => app.delete()))

loadFirestoreRules({ projectId: string, rules: Object }) => Promise

이 메소드는 로컬에서 실행 중인 데이터베이스에 규칙을 전송합니다. 이때 규칙을 문자열로 지정하는 객체를 선택해야 합니다. 데이터베이스 규칙을 설정하려면 이 메소드를 사용하세요.

firebase.loadFirestoreRules({
  projectId: "my-test-project",
  rules: fs.readFileSync("/path/to/firestore.rules", "utf8")
});
    

assertFails(pr: Promise) => Promise

이 메소드는 입력이 성공하면 거부된 프라미스를 반환하고, 입력이 거부되면 성공한 프라미스를 반환합니다. 데이터베이스 읽기 또는 쓰기 실패를 알리는 어설션을 만들려면 이 메소드를 사용하세요.

firebase.assertFails(app.firestore().collection("private").doc("super-secret-document").get());
    

assertSucceeds(pr: Promise) => Promise

이 메소드는 입력이 성공하면 성공한 프라미스를 반환하고, 입력이 거부되면 거부된 프라미스를 반환합니다. 데이터베이스 읽기 또는 쓰기 성공을 알리는 어설션을 만들려면 이 메소드를 사용하세요.

firebase.assertSucceeds(app.firestore().collection("public").doc("test-document").get());
    

clearFirestoreData({ projectId: string }) => Promise

이 메소드는 로컬에서 실행되는 Firestore 인스턴스의 특정 프로젝트와 연결된 모든 데이터를 지웁니다. 테스트 후 삭제하려면 이 메소드를 사용하세요.

firebase.clearFirestoreData({
  projectId: "my-test-project"
});
   

테스트 보고서 생성

일련의 테스트를 실행한 후 각 보안 규칙이 판정된 방식을 보여주는 테스트 범위 보고서에 액세스할 수 있습니다.

이 보고서를 가져오려면 실행 중 에뮬레이터에서 노출된 엔드포인트를 쿼리하세요. 브라우저 버전에서는 다음 URL을 사용하세요.

http://localhost:8080/emulator/v1/projects/<project_id>:ruleCoverage.html

이렇게 하면 규칙이 표현식과 하위 표현식으로 구분되며, 마우스 오버하면 평가 횟수 및 반환된 값을 비롯한 자세한 정보를 얻을 수 있습니다. 이 데이터의 원시 JSON 버전을 가져오려면 쿼리에 다음 URL을 포함하세요.

http://localhost:8080/emulator/v1/projects/<project_id>:ruleCoverage

에뮬레이터와 프로덕션의 차이점

  1. Firestore 프로젝트를 명시적으로 만들 필요가 없습니다. 에뮬레이터는 액세스되는 모든 인스턴스를 자동으로 만듭니다.
  2. Firestore 에뮬레이터는 일반적인 Firebase 인증 흐름에서 작동하지 않습니다. 대신 Firebase 테스트 SDK에서는 테스트 모듈에 auth 필드를 사용하는 initializeTestApp() 메서드를 제공합니다. 이 메서드로 만든 Firebase 핸들은 입력한 항목으로 인증된 것처럼 작동합니다. null을 전달하면 인증되지 않은 사용자처럼 동작합니다. 예를 들어 auth != null 규칙은 실패합니다.

알려진 문제 해결

Firestore 에뮬레이터를 사용할 때 다음과 같은 알려진 문제가 발생할 수 있습니다. 불규칙한 동작이 발생할 경우 아래 안내에 따라 문제를 해결하세요. 이 메모는 Firebase Test SDK를 염두에 두고 작성되었지만 일반적인 접근 방법은 모든 Firebase SDK에 적용됩니다.

테스트 동작에 일관성이 없음

테스트를 변경하지 않은 경우에도 테스트가 가끔씩 통과하거나 실패한다면 올바르게 시퀀싱되었는지 확인해야 할 수 있습니다. 대부분의 에뮬레이터 상호작용은 비동기적이므로 모든 비동기 코드가 올바르게 시퀀싱되었는지 다시 확인하세요. 프라미스를 체이닝하거나 await 표기법을 자유롭게 사용하여 시퀀싱을 수정할 수 있습니다.

특히 다음 비동기 작업을 살펴보세요.

  • 보안 규칙 설정(예: firebase.loadFirestoreRules 사용)
  • 데이터 읽기 및 쓰기(예: db.collection("users").doc("alice").get() 사용)
  • 작업 어설션(firebase.assertSucceedsfirebase.assertFails 포함)

에뮬레이터를 처음 로드할 때만 테스트 통과

에뮬레이터는 상태 저장 방식입니다. 작성된 모든 데이터를 메모리에 저장하므로 에뮬레이터가 종료될 때마다 데이터가 손실됩니다. 동일한 프로젝트 ID로 여러 테스트를 실행하는 경우 각 테스트에서 후속 테스트에 영향을 미칠 수 있는 데이터를 생성할 수 있습니다. 다음 방법을 사용하여 이러한 동작을 우회할 수 있습니다.

  • 테스트마다 고유한 프로젝트 ID를 사용합니다. 이렇게 하려면 각 테스트의 일부로 loadFirestoreRules를 호출해야 합니다. 규칙은 기본 프로젝트 ID에 대해서만 자동으로 로드됩니다.
  • 이전에 작성한 데이터와 상호작용하지 않도록 테스트를 다시 구성합니다. 예를 들어 테스트마다 다른 컬렉션을 사용합니다.
  • 테스트 중에 작성된 모든 데이터를 삭제합니다.

테스트 설정이 매우 복잡함

Firestore 보안 규칙에서 실제로는 허용하지 않는 시나리오를 테스트하려 할 수 있습니다. 예를 들어 인증되지 않은 사용자가 데이터를 수정할 수 있는지 여부를 테스트하는 것은 어렵습니다. 이러한 사용자가 데이터를 편집할 수 없기 때문입니다.

규칙으로 인해 테스트 설정이 복잡해질 경우 관리자로 승인된 클라이언트를 사용하여 규칙을 우회해 보세요. firebase.initializeAdminApp으로 이를 수행할 수 있습니다. 관리자로 승인된 클라이언트의 읽기 및 쓰기는 규칙을 우회하며 PERMISSION_DENIED 오류가 발생하지 않습니다.