Cloud Firestore 보안 규칙 테스트

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

빠른 시작

단순한 규칙이 적용된 몇 가지 기본적인 테스트 사례를 확인하려면 자바스크립트 빠른 시작 또는 TypeScript 빠른 시작을 실행해 보세요.

Cloud Firestore 보안 규칙 이해

모바일 및 웹 클라이언트 라이브러리를 사용하면 서버리스 인증, 승인, 데이터 검증을 위한 Firebase 인증Cloud Firestore 보안 규칙을 적용하게 됩니다.

Cloud Firestore 보안 규칙에는 2가지가 있습니다.

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

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

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

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

에뮬레이터 설치

Cloud Firestore 에뮬레이터를 설치하려면 Firebase CLI를 사용하고 아래 단계를 따르세요.

  1. Cloud Firestore 에뮬레이터를 설치합니다.

     firebase setup:emulators:firestore
     

  2. 다음 명령어를 사용하여 에뮬레이터를 시작합니다. 에뮬레이터는 모든 테스트 도중 실행됩니다.

     firebase serve --only firestore
     

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

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

  • 현재 에뮬레이터를 지원하는 유일한 SDK는 Node.js SDK입니다. Google에서 제공되는 @firebase/testing 모듈을 사용하면 보다 쉽게 에뮬레이터를 활용할 수 있습니다.
  • 에뮬레이터는 선택적으로 사용할 수 있는 --rules CLI 플래그를 지원합니다. Cloud Firestore 보안 규칙이 포함된 로컬 파일 이름을 요청한 후 이 규칙을 모든 프로젝트에 적용합니다. 로컬 파일 경로를 제공하지 않거나 아래 설명에 따라 loadFirestoreRules 메소드를 사용하지 않는 경우 에뮬레이터는 공개 규칙에 따라 모든 프로젝트를 취급합니다.

로컬 테스트 실행

@firebase/testing 모듈을 사용하여 로컬로 실행되는 에뮬레이터를 활용하세요. 시간 초과나 ECONNREFUSED 오류가 발생하면 에뮬레이터가 실제로 실행 중인지 다시 확인하세요.

async/await 표기법을 사용하려면 최신 버전의 Node.js를 사용하는 것이 좋습니다. 테스트할 동작의 거의 대부분이 비동기 함수와 관련이 있으며 테스트 모듈은 프라미스 기반 코드로 작동하도록 설계되어 있습니다.

모듈이 다음 메소드를 노출합니다.

  • initializeTestApp({ projectId: <name>, auth: <auth> }) => FirebaseApp

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

     firebase.initializeTestApp({
       projectId: "my-test-project",
       auth: { uid: "alice", email: "alice@example.com" }
     });
    
  • initializeAdminApp({ projectId: <name> }) => FirebaseApp

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

    firebase.initializeAdminApp({ projectId: "my-test-project" });
    
  • apps() => [FirebaseApp] 이 메소드는 현재 초기화된 테스트 및 관리자 앱을 모두 반환합니다. 테스트 간에 또는 테스트 후에 앱을 정리하려면 이 메소드를 사용하세요.

     Promise.all(firebase.apps().map(app => app.delete()))
    
  • loadFirestoreRules({ projectId: <name>, rules: <rules> }) => 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: <name> }) => 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. Cloud Firestore 프로젝트를 명시적으로 만들 필요가 없습니다. 에뮬레이터는 액세스되는 모든 인스턴스를 자동으로 생성합니다.
  2. Cloud Firestore 에뮬레이터는 다른 Firebase 제품과 함께 작동하지 않습니다. 특히 일반적인 Firebase 인증 흐름이 작동하지 않습니다. Google은 이를 대신해 테스트 모듈에서 auth 필드를 취하는 initializeTestApp() 메소드를 제공합니다. 이 메소드로 만든 Firebase 핸들은 입력한 항목으로 인증된 것처럼 작동합니다. 단, null을 전달하면 인증되지 않은 사용자처럼 작동합니다. 예를 들어 auth != null 규칙은 실패합니다.
  3. Cloud Firestore 에뮬레이터는 데이터를 유지합니다. 이 사실이 결과에 영향을 미칠 수 있습니다. 테스트를 독립적으로 실행하려면 독립적인 각 테스트에 서로 다른 프로젝트 ID를 할당하세요. firebase.initializeAdminApp 또는 firebase.initializeTestApp을 호출하는 경우 고유 ID, 타임스탬프 또는 임의의 정수를 projectID에 추가하세요. 또한 자바스크립트의 async 또는 await 키워드를 통해 해결하는 프라미스를 대기하는 방법으로 경합 상태를 해결할 수도 있습니다.

알려진 문제 해결

Cloud Firestore 에뮬레이터를 사용할 때 다음과 같은 알려진 문제가 발생할 수도 있습니다. 불규칙한 동작이 발생할 경우 아래 안내에 따라 문제를 해결하세요.

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

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

특히 보안 규칙 설정(예: firebase.loadFirestoreRules 사용), 데이터 읽기 및 쓰기(예: db.collection("users").doc("alice").get() 사용), 작업 어설션(firebase.assertSucceedsfirebase.assertFails 포함) 등의 비동기 작업을 검토하세요.

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

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

  • 테스트마다 고유한 프로젝트 ID를 사용합니다.
  • 이전에 작성한 데이터와 상호작용하지 않도록 테스트를 다시 구성합니다. 예를 들어 테스트마다 다른 컬렉션을 사용합니다.
  • 테스트 중에 작성된 모든 데이터를 삭제합니다.

테스트 설정이 매우 복잡함

Cloud Firestore 보안 규칙이 실제로는 허용하지 않는 시나리오를 테스트하려는 경우가 있습니다. 예를 들어 인증되지 않은 사용자는 데이터를 수정할 수 없으므로 이러한 작업이 가능한지 테스트하는 것은 어렵습니다.

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

다음에 대한 의견 보내기...

도움이 필요하시나요? 지원 페이지를 방문하세요.