LoginSignup
7
4

More than 1 year has passed since last update.

firestore(v9)のセキュリティールール設定し、jestでテストする。(rules-unit-testing)

Last updated at Posted at 2022-02-13

前提

・firestore構築済み
・jestインストール済み
https://jestjs.io/ja/
・rules-unit-testingインストール済み
https://www.npmjs.com/package/@firebase/rules-unit-testing
・Emulator設定済み
https://firebase.google.com/docs/emulator-suite/install_and_configure?authuser=0

対象コレクションのセキュリティールール

userPrivateコレクション

データ構造.
email : stirng,
uid : string
get(読み取り時)

・認証済み
・ドキュメントIDがログインユーザーのUID同じ

create(作成時)

・認証済み
・ドキュメントIDがログインユーザーのUID同じ
・emailフィールドはstringで30文字以内
・uidフィールドはログインユーザーのUIDと同じ値で作成
・作成できるフィールドはemail, uidのみ

update(更新時)

・認証済み
・ログインユーザーのUIDと同じドキュメントIDのドキュメントのみ更新可能
・編集できるフィールドはemailのみ(※UIDは更新できない。)
・emailフィールドはstringで30文字以内

##topicコレクション

データ構造.
title : string,
content : string,
authorizedUIDs : Array,
uid : string
list(読み取り時)

・認証済み
・authorizedUIDsにログインユーザー中のUIDが含まれている場合

create(作成時)

・認証済み
・uidフィールドはログインユーザーのUIDと同じ値で作成
・createの際にauthorizedUIDsには必ずログインユーザーのUIDが含まれる必要がある。
・title, content, uid, authorizedUIDs フィールドのみ作成可能

update(更新時)

・認証済み
・uidフィールドがログインユーザーのUIDと同じ値のドキュメントのみ
・title, content, authorizedUIDsのみ更新可能

delete(削除時)

・認証済み
・uidフィールドがログインユーザーのUIDと同じ値のドキュメントのみ

※作成、更新可能フィールドの検証については記載省略
※各フィールドのバリデーションについては記載省略

##historyコレクション

historyコレクションは、topicコレクションのサブコレクション 

topic____
        |----history
        |----history
        |----history
データ構造.
url : string,
content : string,
uid : string
list(読み取り時)

・認証済み
・親topicコレクションのauthorizedUIDsにログインユーザーのUIDが含まれている

create(作成時)

・認証済み
・親topicコレクションのauthorizedUIDsにログインユーザーのUIDが含まれている場合可能
・uidフィールドはログインユーザーのUIDと異なる値とすることはできない。

update(更新時)

・認証済み
・親topicコレクションのauthorizedUIDsにログインユーザーのUIDが含まれている場合可能
・uidフィールドについては更新できない。

delete(削除時)

・認証済み
・ログインユーザーのUIDが、「親topicドキュメントのuidフィールド」, もしくは「historyドキュメントのuidフィールド」と同じである。
(※親topicドキュメントの作成者は,紐づくhiststoryコレクションのドキュメントを全て削除可能。hisitoryコレクションのドキュメント作成者はそのhistoryドキュメントは削除可能)

※各フィールドのバリデーションについては記載省略。

実装

プロジェクトのルートフォルダに下記のファイルを作成。

__tests__/firebase/firestore.test.js

package.jsonを修正

package.json
  "scripts": {
    "test": "jest", //追記
    "test-fs": "jest ./__tests__/firebase/firestore.test.js", //追記
  }

動作チェック
下記のように仮のテストを作成する。

firestore.test.js
const assert = require('assert');

describe("init test", () => {

  it("Understands basic addition", () => {
    assert.strictEqual(2 + 2, 4);
  })
})
ターミナル.
//エミュレーター立ち上げ
firebase emulators:start --only firestore

//テスト開始(ターミナル 別タブ)
npm run test-fs

下記のようになれば問題ありません。

image.png

テスト環境を作成するコードを実装

firestore.test.js
import * as fs from 'fs'
import { v4 } from "uuid"
import * as testing from '@firebase/rules-unit-testing'

const projectID = v4()
let testEnv
const uid = v4()
const otherUid = v4()

beforeAll(async () => {
  // テストプロジェクト環境の作成
  testEnv = await firebase.initializeTestEnvironment({
    projectId: projectID,
    firestore: {
      rules: fs.readFileSync('./firestore.rules', 'utf8'),
      port: 8080,
      host: "localhost"
    }
  })
})
// グローバルで定義されたbeforeAllはテストの開始前に一回実行されます。

beforeEach(async () => {
  // Firestore エミュレータ用に構成された projectId に属する Firestore データベースのデータをクリアします。
  await testEnv.clearFirestore()
})

// グローバルで定義されたbeforeEachは各テストの開始前に一回実行されます。

afterAll(async () => {
  //テスト終了後テスト環境で作成されたすべての RulesTestContexts を破棄します。
  await testEnv.cleanup()
})

// グローバルで定義されたafterAllはテストの終了後に一回実行されます。

userPrivateコレクションのルール作成とテストを実装していきます。

条件1:

認証していなければ読み取りできない。

firestore.rules
rules_version = '2';

service cloud.firestore {

 function isAuth(){
    return request.auth != null
    //reequest.authで認証情報を取得。認証していなければnullとなる。
  }

  match /databases/{database}/documents {
    match /userPrivate/{userID} {
      allow get: if isAuth()
    }
  }
}
firestore.test.js

//略

const getDB = () => {
  // ログイン情報つきのContextを作成し、そこから Firestore インスタンスを得る。
  // authenticatedContextは引数をUIDにもつ認証済みContextを返す。
  const authenticatedContext = testEnv.authenticatedContext(uid)
  const clientDB = authenticatedContext.firestore()

  // ゲストContextを作成し、そこから Firestore インスタンスを得る。
  // unauthenticatedContextは未認証Contextを返す。
  const unauthenticatedContext = testEnv.unauthenticatedContext()
  const guestClientDB = unauthenticatedContext.firestore()
  return { clientDB, guestClientDB }
}

describe('users collection', () => {
  describe('get', () => {
     it('get: 未認証では不可。', async () => {
      const { guestClientDB } = getDB();
      await testing.assertFails(
        getDoc(doc(guestClientDB, "userPrivate", uid))
      )
    })
  })
})

//assertFailは引数内の処理が失敗した時、テスト結果を正常と判断する。
ターミナル.
npm run test-fs

下記で成功
image.png

条件2:

・ドキュメントIDがログインユーザーのUID同じ

firestore.rules
 
service cloud.firestore {

  function myUID(){
    return request.auth.uid
  } //追記

  match /databases/{database}/documents {
    match /userPrivate/{userID} {
      allow get: if isAuth() && 
                    userID == myUID() //追記
    }
  }
}
firestore.test.js
  //略

describe('users collection', () => {

  // 略

  it('get: 認証済みでもログインユーザーのUIDと異なるドキュメントIDのドキュメントは不可', async () => {
    const { clientDB } = getDB();
    await firebase.assertFails(
      getDoc(doc(clientDB, "userPrivate", otherUid))
    )
  }) //追記

  it('get: ログインユーザーのUIDと同じドキュメントIDのドキュメントは可能', async () => {
    const { clientDB } = getDB();
    await firebase.assertSucceeds(
      getDoc(doc(clientDB, "userPrivate", uid))
    )
  })
}) //追記

//assertSucceedsは引数内の処理が成功した時、テスト結果を正常と判断する。

条件3:

下記の条件を満たす場合、createできる。
・認証済み
・ドキュメントIDがログインユーザーのUID同じ
・emailフィールドはstringで30文字以内
・uidフィールドはログインユーザーのUIDと同じ値
・作成できるフィールドはemail, uidのみ

firestore.rules
rules_version = '2';

service cloud.firestore {

  function isAuth(){
    return request.auth != null
  }

  function myUID(){
    return request.auth.uid
  }

  function incomingData(){
    return request.resource.data
  } 
    //追記
    //request.resource.dataで変更しようとしている値をもつドキュメントデータを取得

  function isAvailableCreateFields(fieldList){
    return incomingData().keys().hasOnly(fieldList)
  } 
    //追記
    //keys() : ドキュメントのキーを取得
  //hasOnly() : 引数(配列)で与えられている要素が含まれていたらtrue 
  //結果、作成しようとするデータに許可されていないフィールドが含まれていたらNGとなる。

  match /databases/{database}/documents {
    match /userPrivate/{userID} {
      allow get:if isAuth() &&
                    userID == myUID()
      allow create:if isAuth() &&
                      userID == myUID() &&
                      incomingData().email is string &&
                      incomingData().email.size() < 31 &&
                      incomingData().uid == myUID() &&
                      isAvailableCreateFields(["email","uid"])
     //追記
    }
  }
}
firestore.test.js
describe('userPrivate collection', () => {
  describe('get', () => {
    // 略
  })
  describe('create', () => {
    it('create: 認証済みで条件を満たす場合は可能', async () => {
      const { clientDB } = getDB();
      await testing.assertSucceeds(
        setDoc(doc(clientDB, "userPrivate", uid), { email: "123456789012345678901234567890", uid })
      )
    })
    it('create: 未認証では不可。', async () => {
      const { guestClientDB } = getDB();
      await testing.assertFails(
        setDoc(doc(guestClientDB, "userPrivate", uid), { email: "otherEmail", uid })
      )
    })
    it('create: 認証済み。ドキュメントIDがUIDと異なる値では不可。', async () => {
      const { clientDB } = getDB();
      await testing.assertFails(
        setDoc(doc(clientDB, "userPrivate", otherUid), { email: "otherEmail", uid })
      )
    })
    it('create: 認証済み。uidフィールドがUIDと異なる値では不可。', async () => {
      const { clientDB } = getDB();
      await testing.assertFails(
        setDoc(doc(clientDB, "userPrivate", uid), { email: "otherEmail", uid: otherUid })
      )
    })
    it('create: 認証済み。emailが31文字以上不可。', async () => {
      const { clientDB } = getDB();
      await testing.assertFails(
        setDoc(doc(clientDB, "userPrivate", uid), { email: "1234567890123456789012345678901", uid: uid })
      )
    })
    it('create: 認証済み。許可されたフィールド以外は不可', async () => {
      const { clientDB } = getDB();
      await testing.assertFails(
        setDoc(doc(clientDB, "userPrivate", uid), { email: "1234567890123456789012345678901", uid: uid, age: 20 })
      )
    })
  })

条件4:

下記の条件を満たす場合、編集できる。
・認証済み
・ログインユーザーのUIDと同じドキュメントIDのドキュメントのみ更新可能
・編集できるフィールドはemailのみ(※uidは更新できない。)
・emailフィールドはstringで30文字以内

firestore.rules
rules_version = '2';

service cloud.firestore {

  function isAuth(){
    return request.auth != null
  }

  function myUID(){
    return request.auth.uid
  }

  function incomingData(){
    return request.resource.data
  }

  function existingData(){
    return resource.data
  }
  //追記
  //resource.dataで変更前のドキュメントデータを取得

  function isAvailableCreateFields(fieldList){
    return incomingData().keys().hasOnly(fieldList)
  }
   
  function isAvailableUpdateFields(FieldList){
    return incomingData().diff(existingData()).affectedKeys().hasOnly(FieldList)
  }
    //追記
    //incomingData().diff(existingData()) : 変更前との差分を取得
  //affectedKeys() : 変更があった値を持つキーを取得。
  //結果、作成しようとするデータに許可されていないフィールドが含まれていたらNGとなる。

  match /databases/{database}/documents {
    match /userPrivate/{userID} {
      allow get:if isAuth() &&
                    userID == myUID()
      allow create:if isAuth() &&
                      userID == myUID() &&
                      incomingData().email is string &&
                      incomingData().email.size() < 31 &&
                      incomingData().uid == myUID() &&
                      isAvailableCreateFields(["email","uid"])
      allow update:if isAuth() &&
                      userID == myUID() &&
                      incomingData().email is string &&
                      incomingData().email.size() < 31 &&
                      isAvailableUpdateFields(['email']);
    }
  }
}
firestore.test.js

 //略

describe('userPrivate collection', () => {
  // get
  describe('get', () => {
    //略
  })
  // create
  describe('create', () => {
    // 略
  })
  // update
  describe('update', () => {
    //各テスト前に保存済みモックデータを作成
    //なお、discribeでスコープがきられるため,下記のbeforeEachは「describe('update',() =>{})」内の各テストのみ対象となる。
    beforeEach(async () => {
      await testEnv.withSecurityRulesDisabled(async context => {
        const noRuleDB = context.firestore()
        await setDoc(doc(noRuleDB, "userPrivate", uid), { email: "authEmail", uid })
      })
    })
    //withSecurityRulesDisabledメソッドでセキュリティールールを回避してデータベースを操作できる。
   
    it('update: 認証済みで条件を満たす場合は可能', async () => {
      const { clientDB } = getDB();
      await firebase.assertSucceeds(
        updateDoc(doc(clientDB, "userPrivate", uid), { email: "changeEmail" })
      )
    })
    it('update: 未認証では不可。', async () => {
      const { guestClientDB } = getDB();
      await firebase.assertFails(
        updateDoc(doc(guestClientDB, "userPrivate", uid), { email: "changeEmail" })
      )
    })
    it('update: 認証済み。ドキュメントIDがUIDと異なる値では不可', async () => {
      const { clientDB } = getDB();
      await firebase.assertFails(
        updateDoc(doc(clientDB, "userPrivate", otherUid), { email: "changeEmail" })
      )
    })
    it('update: 認証済み。emailが文字列でない場合不可', async () => {
      const { clientDB } = getDB();
      await firebase.assertFails(
        updateDoc(doc(clientDB, "userPrivate", uid), { email: 0 })
      )
    })
    it('update: 認証済み。emailが31文字以上不可', async () => {
      const { clientDB } = getDB();
      await firebase.assertFails(
        updateDoc(doc(clientDB, "userPrivate", uid), { email: "1234567890123456789012345678901" })
      )
    })
    it('update: 認証済み。許可されたフィールド以外は不可', async () => {
      const { clientDB } = getDB();
      await firebase.assertFails(
        updateDoc(doc(clientDB, "userPrivate", uid), { email: "changeEmail", uid: "changeUID" })
      )
    })
  })
})

topicコレクションのテストを実装する。

条件1

下記の条件を満たす場合、listできる。
・認証済み(テスト記載割愛)
・authorizedUIDsにログインユーザー中のUIDが含まれている場合

firestore.rules
  
service cloud.firestore {
  match /databases/{database}/documents {
    match /userPrivate/{userID} {
      //略
    }
    match /topic/{documentID} {
      allow list:if isAuth() &&
                    myUID() in existingData().authorizedUIDs

    // in : 含まれていた場合trueを返す
    }
  }
}

※getとlistの違い

getについては、単体のドキュメントを取得の場合。
また、取得したドキュメントに対し権限確認を行う。
listについては、クエリによる複数ドキュメントを取得の場合。
listについてはクエリに対して権限確認を行う。
該当クエリを実行することにより、必ず条件にクリアするデータを取得するかをチェックする。(取得したデータに対して権限確認を行うわけではない。)

firestore.test.js
//略
describe('topic collection', () => {
  // list
  describe('list', () => {
    it('list:authorizedUIDsに自分のUIDが含まれれば可能 ', async () => {
      const { clientDB } = getDB();
      const q = query(collection(clientDB, "topic"), where("authorizedUIDs", "array-contains", uid));
      await firebase.assertSucceeds(
        getDocs(q)
      )
    })
    //このクエリを実行することにより、取得してきたデータは必ずログイン中ユーザーのUIDを
    //authorizedUIDsに含んでいることが担保されるのでルールを通過する。
    //クエリに対して権限チェックを行うので、事前にモックデータを作る必要はない。

    it('list:authorizedUIDsに自分のUIDが含まれていない可能性があるクエリは不可', async () => {
      const { clientDB } = getDB();
      const q = query(collection(clientDB, "topic"));
      await firebase.assertFails(
        getDocs(q)
      )
    })
    //このクエリでは、ログイン中ユーザーのUIDをauthorizedUIDsに含まないデータも
    //取得してくる可能性があるためルール通過しない。

条件2:

create(作成時)

・認証済み
・uidフィールドはログインユーザーのUIDと同じ値で作成
・createの際にauthorizedUIDsには必ずログインユーザーのUIDが含まれる必要がある。
・title, content, uid, authorizedUIDs フィールドのみ作成可能

update(更新時)

・認証済み
・uidフィールドがログインユーザーのUIDと同じ値のドキュメントのみ
・title, content, authorizedUIDsのみ更新可能(uidは更新できない。)

delete(削除時)

・認証済み
・uidフィールドがログインユーザーのUIDと同じ値のドキュメントのみ

※各フィールドのバリデーションについては記載省略。

firestore.rules
rules_version = '2';
  
service cloud.firestore {

  // 略

  match /databases/{database}/documents {
    match /userPrivate/{userID} {
          // 略
    }
    match /topic/{documentID} {
      allow list:if isAuth() &&
                    myUID() in existingData().authorizedUIDs
      allow create:if isAuth() &&
                      incomingData().uid == myUID() &&
                      myUID() in incomingData().authorizedUIDs &&
                      isAvailableCreateFields(["title","content","uid","authorizedUIDs"])
      allow update:if isAuth() &&
                      existingData().uid == myUID() &&
                      myUID() in incomingData().authorizedUIDs &&
                      isAvailableUpdateFields(["title","content","authorizedUIDs"])
      allow delete:if isAuth() &&
                      existingData().uid == myUID()
    }
  }
}
firestore.test.js
// topic
describe('topic collection', () => {
  // list
  // 略
  // create
  describe('create', () => {
    it('create: 認証済みで条件を満たす場合は可能', async () => {
      const { clientDB } = getDB();
      await firebase.assertSucceeds(
        setDoc(doc(clientDB, "topic", "topicID"), { title: "title", content: "content", uid, authorizedUIDs: [uid] })
      )
    })
    it('create: 未認証では不可。', async () => {
      const { guestClientDB } = getDB();
      await firebase.assertFails(
        setDoc(doc(guestClientDB, "topic", "topicID"), { title: "title", content: "content", uid, authorizedUIDs: [uid] })
      )
    })
    it('create: uidにログインユーザーのUIDと異なる値を与えるのは不可', async () => {
      const { clientDB } = getDB();
      await firebase.assertFails(
        setDoc(doc(clientDB, "topic", "topicID"), { title: "title", content: "content", uid: otherUid, authorizedUIDs: [uid] })
      )
    })
    it('create: authorizedUIDsにログインユーザーのUIDが含まれていない場合は不可', async () => {
      const { clientDB } = getDB();
      await firebase.assertFails(
        setDoc(doc(clientDB, "topic", "topicID"), { title: "title", content: "content", uid: otherUid, authorizedUIDs: [otherUid] })
      )
    })
  })
  // update
  describe('update', () => {
    // モック作成
    beforeEach(async () => {
      await testEnv.withSecurityRulesDisabled(async context => {
        const noRuleDB = context.firestore()
        //ログイン中ユーザーが作成したとするモック
        await setDoc(doc(noRuleDB, "topic", "topicID"), { title: "title", content: "content", uid, authorizedUIDs: [uid] })
        //他人(ログイン中ユーザー以外のユーザー)が作成したとするモック
        await setDoc(doc(noRuleDB, "topic", "otherUserTopicID"), { title: "title", content: "content", uid: otherUid, authorizedUIDs: [otherUid] })
      })
    })
    it('update: 認証済みで条件を満たす場合は可能', async () => {
      const { clientDB } = getDB();
      await firebase.assertSucceeds(
        updateDoc(doc(clientDB, "topic", "topicID"), { title: "title", content: "content", authorizedUIDs: [uid] })
      )
    })
    it('update: 未認証では不可。', async () => {
      const { guestClientDB } = getDB();
      await firebase.assertFails(
        updateDoc(doc(guestClientDB, "topic", "topicID"), { title: "title", content: "content", authorizedUIDs: [uid] })
      )
    })
    it('update: uidの変更は不可', async () => {
      const { clientDB } = getDB();
      await firebase.assertFails(
        updateDoc(doc(clientDB, "topic", "topicID"), { title: "title", content: "content", uid: otherUid, authorizedUIDs: [uid] })
      )
    })
    it('update: uidフィールドの値がログイン中のユーザーと異なるキュメントは不可', async () => {
      const { clientDB } = getDB();
      await firebase.assertFails(
        updateDoc(doc(clientDB, "topic", "otherUserTopicID"), { title: "title", content: "content", authorizedUIDs: [uid] })
      )
    })
  })
  // // delete
  describe('delete', () => {
    // モック作成
    beforeEach(async () => {
      await testEnv.withSecurityRulesDisabled(async context => {
        const noRuleDB = context.firestore()
        await setDoc(doc(noRuleDB, "topic", "topicID"), { title: "title", content: "content", uid, authorizedUIDs: [uid] })
        await setDoc(doc(noRuleDB, "topic", "otherUserTopicID"), { title: "title", content: "content", uid: otherUid, authorizedUIDs: [otherUid] })
      })
    })
    it('delete: 認証済みで条件を満たす場合は可能', async () => {
      const { clientDB } = getDB();
      await firebase.assertSucceeds(
        deleteDoc(doc(clientDB, "topic", "topicID"))
      )
    })
    it('delete: 未認証では不可。', async () => {
      const { guestClientDB } = getDB();
      await firebase.assertFails(
        deleteDoc(doc(guestClientDB, "topic", "topicID"))
      )
    })
    it('delete: uidフィールドの値がログイン中のユーザーと異なるキュメントは不可', async () => {
      const { clientDB } = getDB();
      await firebase.assertFails(
        deleteDoc(doc(clientDB, "topic", "otherUserTopicID"))
      )
    })
  })

historyコレクションのテストを実装する。

historyコレクションはtopicコレクションのサブコレクション。

条件1

下記の条件を満たす場合、listできる。
・認証済み
・親topicコレクションのauthorizedUIDsにログインユーザーのUIDが含まれている

firestore.rules
rules_version = '2';

service cloud.firestore {

   //略

  function getParentTopic(database,documentID){
    return get(/databases/$(database)/documents/topic/$(documentID))
    //get関数で対象のドキュメントを取得する。
  }

  match /databases/{database}/documents {
    
        // 略

    match /topic/{documentID} {

       //略

      match /history/{historyDocumentID}{
        allow list:if isAuth() &&
                      myUID() in getParentTopic(database,documentID).data.authorizedUIDs

//getParentTopic関数でtopicコレクションの親ドキュメントを取得する。
//取得した親ドキュメントのauthorizedUIDsにログインユーザーのUIDが含まれているかチェック
//サブコレクションはネスト構造で記述することができる。
      }
    }
  }
}
firestore.test.js
  // history
  describe('history collection', () => {
    // list
    describe('list', () => {
      // モック作成
      beforeEach(async () => {
        await testEnv.withSecurityRulesDisabled(async context => {
          const noRuleDB = context.firestore()
          //ログインユーザーがauthorizedUIDsに含まれている親topicドキュメント
          await setDoc(doc(noRuleDB, "topic", "topicID"), { title: "title", content: "content", uid, authorizedUIDs: [uid] })
          //ログインユーザーがauthorizedUIDsに含まれていない親topicドキュメント
          await setDoc(doc(noRuleDB, "topic", "outOfAuthTopicID"), { title: "title", content: "content", uid: otherUid, authorizedUIDs: [otherUid] })
        })
      })
      it('list:親topicドキュメントのauthorizedUIDsにログインユーザーのUIDが含まれていたら可能', async () => {
        const { clientDB } = getDB();
        const q = query(collection(clientDB, "topic", "topicID", "history"));
        await firebase.assertSucceeds(
          getDocs(q)
        )
      })
      //モックからtopicドキュメントを取得して、そのドキュメントの内容を含むクエリで評価する。
      //モックのtopicドキュメント(ドキュメントIDはtopicID)は、 authorizedUIDsにログインユーザーのUIDを含んでいるためこのクエリは許可される。

      it('list:親topicドキュメントのauthorizedUIDsにログインユーザーのUIDが含まれていなかったら不可', async () => {
        const { clientDB } = getDB();
        const q = query(collection(clientDB, "topic", "outOfAuthTopicID", "history",));
        await firebase.assertFails(
          getDocs(q)
        )
      })     
    })
  })

条件2

下記の条件を満たす場合、createできる。

・認証済み(記載省略)
・親topicコレクションのauthorizedUIDsにログインユーザーのUIDが含まれている場合可能
・uidフィールドはログインユーザーのUIDと異なる値とすることはできない。

firestore.rules
rules_version = '2';

service cloud.firestore {

   //略

  match /databases/{database}/documents {
    
        // 略

    match /topic/{documentID} {

       //略

      match /history/{historyDocumentID}{
        allow list:if isAuth() &&
                      myUID() in getParentTopic(database,documentID).data.authorizedUIDs

        allow create:if isAuth() &&
                        incomingData().uid == myUID() &&
                        myUID() in getParentTopic(database,documentID).data.authorizedUIDs &&
                        isAvailableCreateFields(["url","content","uid"])
      }
    }
  }
}
firestore.test.js
 // history
  describe('history collection', () => {    
    
    // 略
    
    // create
  describe('create', () => {
    // モック作成
    beforeEach(async () => {
      await testEnv.withSecurityRulesDisabled(async context => {
        const noRuleDB = context.firestore()
        await setDoc(doc(noRuleDB, "topic", "topicID"), { title: "title", content: "content", uid, authorizedUIDs: [uid] })
        await setDoc(doc(noRuleDB, "topic", "outOfAuthTopicID"), { title: "title", content: "content", uid: otherUid, authorizedUIDs: [otherUid] })
      })
    })
    it('create:親topicドキュメントのauthorizedUIDsにログインユーザーのUIDが含まれていたら、サブコレクション (history)ドキュメント作成可能', async () => {
      const { clientDB } = getDB();
      const docRef = doc(clientDB, "topic", "topicID", "history", "mmhistoryID");
      await testing.assertSucceeds(
        setDoc(docRef, { url: "url", content: "content", uid })
      )
    })
    it('create:親topicドキュメントのauthorizedUIDsにログインユーザーのUIDが含まれていなければ、サブコレクション (history)ドキュメント作成不可', async () => {
      const { clientDB } = getDB();
      const docRef = doc(clientDB, "topic", "outOfAuthTopicID", "history", "mmhistoryID");
      await testing.assertFails(
        setDoc(docRef, { url: "url", content: "content", uid })
      )
    })
    it('create:uidにログインユーザーのUIDと異なる値を与えるのは不可', async () => {
      const { clientDB } = getDB();
      const docRef = doc(clientDB, "topic", "topicID", "history", "mmhistoryID");
      await testing.assertFails(
        setDoc(docRef, { url: "url", content: "content", uid: otherUid })
      )
    })
  })
})

条件3

下記の条件を満たす場合、updateできる。

・認証済み(記載省略)
・親topicコレクションのauthorizedUIDsにログインユーザーのUIDが含まれている場合可能
・uidフィールドについては更新できない。(記載省略)

firestore.rules
rules_version = '2';

service cloud.firestore {

   //略

  match /databases/{database}/documents {
    
        // 略

    match /topic/{documentID} {

       //略

      match /history/{historyDocumentID}{

                // 略

        allow update:if isAuth() &&
                        myUID() in getParentTopic(database,documentID).data.authorizedUIDs &&
                        isAvailableUpdateFields(["url","content"])
      }
    }
  }
}
firestore.test.js
// history
describe('history collection', () => {    
    
    // 略
    
    // update
  describe('update', () => {
    // モック作成
    beforeEach(async () => {
      await testEnv.withSecurityRulesDisabled(async context => {
        const noRuleDB = context.firestore()
        // 親topicドキュメントのauthorizedUIDsにログインユーザーのUIDが含まれている場合
        await setDoc(doc(noRuleDB, "topic", "topicID"), { title: "title", content: "content", uid, authorizedUIDs: [uid] })
        await setDoc(doc(noRuleDB, "topic", "topicID", "history", "historyID"), { url: "url", content: "content", uid: otherUid })
        // 親topicドキュメントのauthorizedUIDsにログインユーザーのUIDが含まれていない場合
        await setDoc(doc(noRuleDB, "topic", "outOfAuthTopicID"), { title: "title", content: "content", uid, authorizedUIDs: [otherUid] })
        await setDoc(doc(noRuleDB, "topic", "outOfAuthTopicID", "history", "historyID"), { url: "url", content: "content", uid: otherUid })
      })
    })
    it('update:親topicドキュメントのauthorizedUIDsにログインユーザーのUIDが含まれていたら、サブコレクション (history)ドキュメントの更新可能', async () => {
      const { clientDB } = getDB();
      const docRef = doc(clientDB, "topic", "topicID", "history", "historyID");
      await testing.assertSucceeds(
        updateDoc(docRef, { url: "url", content: "content" })
      )
    })
    it('update:親topicドキュメントのauthorizedUIDsにログインユーザーのUIDが含まれていなければ、サブコレクション (history)ドキュメントの更新不可', async () => {
      const { clientDB } = getDB();
      const docRef = doc(clientDB, "topic", "outOfAuthTopicID", "history", "historyID");
      await testing.assertFails(
        updateDoc(docRef, { url: "url", content: "content" })
      )
    })
  })
})

条件4

下記の条件を満たす場合、deleteできる。
・認証済み(記載省略)
・ログインユーザーのUIDが、「親topicドキュメントのuidフィールド」, もしくは「historyドキュメントのuidフィールド」と同じである。
(※親topicドキュメントの作成者は,紐づくhiststoryコレクションのドキュメントを全て削除可能。hisitoryコレクションのドキュメント作成者はそのhistoryドキュメントは削除可能)

firestore.rules
rules_version = '2';

service cloud.firestore {

   //略

  match /databases/{database}/documents {
    
        // 略

    match /topic/{documentID} {

       //略

      match /history/{historyDocumentID}{

                // 略

        allow delete:if isAuth() &&
                        myUID() == getParentTopic(database,documentID).data.uid ||
                        myUID() == existingData().uid

||演算子でどちらか一方がtureならtrueを返す。
      }
    }
  }
}
firestore.test.js
// history
describe('history collection', () => {    
    
    // 略
    
  //delete
  describe('delete', () => {
    // モック作成
    beforeEach(async () => {
      await testEnv.withSecurityRulesDisabled(async context => {
        const noRuleDB = context.firestore()
        // ログインユーザーが作ったtopic/他人が作ったhistory
        await setDoc(doc(noRuleDB, "topic", "moTopicID"), { title: "title", content: "content", uid, authorizedUIDs: [uid] })
        await setDoc(doc(noRuleDB, "topic", "moTopicID", "history", "moHistoryID"), { url: "url", content: "content", uid: otherUid })
        //他人が作ったtopic/ログインユーザーが作ったhistory
        await setDoc(doc(noRuleDB, "topic", "omTopicID"), { title: "title", content: "content", uid: otherUid, authorizedUIDs: [otherUid] })
        await setDoc(doc(noRuleDB, "topic", "omTopicID", "history", "omHistoryID"), { url: "url", content: "content", uid })
        //他人が作ったtopic/他人が作ったhistory
        await setDoc(doc(noRuleDB, "topic", "ooTopicID"), { title: "title", content: "content", uid: otherUid, authorizedUIDs: [otherUid] })
        await setDoc(doc(noRuleDB, "topic", "ooTopicID", "history", "ooHistoryID"), { url: "url", content: "content", uid: otherUid })
      })
    })
    it('delete:親topicドキュメントのuidがログインユーザーのUIDであれば可能', async () => {
      const { clientDB } = getDB();
      const docRef = doc(clientDB, "topic", "moTopicID", "history", "moHistoryID");
      await testing.assertSucceeds(
        deleteDoc(docRef)
      )
    })
    it('delete:historyドキュメントのuidがログイン中のユーザーであれば可能', async () => {
      const { clientDB } = getDB();
      const docRef = doc(clientDB, "topic", "omTopicID", "history", "omHistoryID");
      await testing.assertSucceeds(
        deleteDoc(docRef)
      )
    })
    it('delete:親topic、history共に他人が作成したものであれば不可', async () => {
      const { clientDB } = getDB();
      const docRef = doc(clientDB, "topic", "ooTopicID", "history", "omHistoryID");
      await testing.assertFails(
        deleteDoc(docRef)
      )
    })
  })
})

以上です。

全文

firestore.rules
rules_version = '2';

service cloud.firestore {

  function isAuth(){
    return request.auth != null
  }
  function myUID(){
    return request.auth.uid
  }
  function incomingData(){
    return request.resource.data
  }
  function existingData(){
    return resource.data
  }
  function isAvailableCreateFields(fieldList){
    return incomingData().keys().hasOnly(fieldList)
  }
  function isAvailableUpdateFields(FieldList){
    return incomingData().diff(existingData()).affectedKeys().hasOnly(FieldList)
  }
  function getParentTopic(database,documentID){
    return get(/databases/$(database)/documents/topic/$(documentID))
  }

  match /databases/{database}/documents {   
    // userPrivate
    match /userPrivate/{userID} {
      allow get:if isAuth() &&
                    userID == myUID()
      allow create:if isAuth() &&
                      userID == myUID() &&
                      incomingData().email is string &&
                      incomingData().email.size() < 31 &&
                      incomingData().uid == myUID() &&
                      isAvailableCreateFields(["email","uid"])
      allow update:if isAuth() &&
                      userID == myUID() &&
                      incomingData().email is string &&
                      incomingData().email.size() < 31 &&
                      isAvailableUpdateFields(['email'])
    }
    // topic
    match /topic/{documentID} {
      allow get:if isAuth() &&
                    myUID() in existingData().authorizedUIDs
      allow list:if isAuth() &&
                    myUID() in existingData().authorizedUIDs
      allow create:if isAuth() &&
                      incomingData().uid == myUID() &&
                      myUID() in incomingData().authorizedUIDs &&
                      isAvailableCreateFields(["title","content","authorizedUIDs","uid"])
      allow update:if isAuth() &&
                      existingData().uid == myUID() &&
                      myUID() in incomingData().authorizedUIDs &&
                      isAvailableUpdateFields(["title","content","authorizedUIDs"])
      allow delete:if isAuth() &&
                      existingData().uid == myUID()
      // topic/history
      match /history/{historyDocumentID}{
        allow list:if isAuth() &&
                        myUID() in getParentTopic(database,documentID).data.authorizedUIDs
        allow create:if isAuth() &&
                        incomingData().uid == myUID() &&
                        myUID() in getParentTopic(database,documentID).data.authorizedUIDs &&
                        isAvailableCreateFields(["url","content","uid"])
        allow update:if isAuth() &&
                        myUID() in getParentTopic(database,documentID).data.authorizedUIDs &&
                        isAvailableUpdateFields(["url","content"])
        allow delete:if isAuth() &&
                        myUID() == getParentTopic(database,documentID).data.uid ||
                        myUID() == existingData().uid
      }
    }
  }
}
firestore.test.js
import * as fs from 'fs'
import { v4 } from "uuid"
import * as testing from '@firebase/rules-unit-testing'

import { doc, collection, setDoc, getDoc, updateDoc, query, where, getDocs, deleteDoc } from 'firebase/firestore'

const projectID = v4()
let testEnv
const uid = v4()
const otherUid = v4()

beforeAll(async () => {
  // テストプロジェクト環境の作成
  testEnv = await testing.initializeTestEnvironment({
    projectId: projectID,
    firestore: {
      rules: fs.readFileSync('./firestore.rules', 'utf8'),
      port: 8080,
      host: "localhost"
    }
  })
})

beforeEach(async () => {
  await testEnv.clearFirestore()
})

afterAll(async () => {
  await testEnv.cleanup()
})

const getDB = () => {
  // ログイン情報つきのContextを作成し、そこから Firestore インスタンスを得る
  const authenticatedContext = testEnv.authenticatedContext(uid)
  const clientDB = authenticatedContext.firestore()

  // ゲストContextを作成し、そこから Firestore インスタンスを得る
  const unauthenticatedContext = testEnv.unauthenticatedContext()
  const guestClientDB = unauthenticatedContext.firestore()
  return { clientDB, guestClientDB }
}

describe('userPrivate collection', () => {
  // get
  describe('get', () => {
    it('get: 未認証では不可。', async () => {
      const { guestClientDB } = getDB();
      await testing.assertFails(
        getDoc(doc(guestClientDB, "userPrivate", uid))
      )
    })
    it('get: ログインユーザーのUIDと異なるドキュメントIDのドキュメントは不可', async () => {
      const { clientDB } = getDB();
      await testing.assertFails(
        getDoc(doc(clientDB, "userPrivate", otherUid))
      )
    })
    it('get: ログインのUIDと同じドキュメントIDのドキュメントは可能', async () => {
      const { clientDB } = getDB();
      await testing.assertSucceeds(
        getDoc(doc(clientDB, "userPrivate", uid))
      )
    })
  })
  // create
  describe('create', () => {
    it('create: 認証済みで条件を満たす場合は可能', async () => {
      const { clientDB } = getDB();
      await testing.assertSucceeds(
        setDoc(doc(clientDB, "userPrivate", uid), { email: "123456789012345678901234567890", uid })
      )
    })
    it('create: 未認証では不可。', async () => {
      const { guestClientDB } = getDB();
      await testing.assertFails(
        setDoc(doc(guestClientDB, "userPrivate", uid), { email: "otherEmail", uid })
      )
    })
    it('create: 認証済み。ドキュメントIDがUIDと異なる値では不可。', async () => {
      const { clientDB } = getDB();
      await testing.assertFails(
        setDoc(doc(clientDB, "userPrivate", otherUid), { email: "otherEmail", uid })
      )
    })
    it('create: 認証済み。uidフィールドがUIDと異なる値では不可。', async () => {
      const { clientDB } = getDB();
      await testing.assertFails(
        setDoc(doc(clientDB, "userPrivate", uid), { email: "otherEmail", uid: otherUid })
      )
    })
    it('create: 認証済み。emailが31文字以上不可。', async () => {
      const { clientDB } = getDB();
      await testing.assertFails(
        setDoc(doc(clientDB, "userPrivate", uid), { email: "1234567890123456789012345678901", uid: uid })
      )
    })
    it('create: 認証済み。許可されたフィールド以外は不可', async () => {
      const { clientDB } = getDB();
      await testing.assertFails(
        setDoc(doc(clientDB, "userPrivate", uid), { email: "1234567890123456789012345678901", uid: uid, age: 20 })
      )
    })
  })
  // update
  describe('update', () => {
    beforeEach(async () => {
      await testEnv.withSecurityRulesDisabled(async context => {
        const noRuleDB = context.firestore()
        await setDoc(doc(noRuleDB, "userPrivate", uid), { email: "authEmail", uid })
      })
    })
    it('update: 認証済みで条件を満たす場合は可能', async () => {
      const { clientDB } = getDB();
      await testing.assertSucceeds(
        updateDoc(doc(clientDB, "userPrivate", uid), { email: "changeEmail" })
      )
    })
    it('update: 未認証では不可', async () => {
      const { guestClientDB } = getDB();
      await testing.assertFails(
        updateDoc(doc(guestClientDB, "userPrivate", uid), { email: "changeEmail" })
      )
    })
    it('update: 認証済み。ドキュメントIDがUIDと異なる値では不可。', async () => {
      const { clientDB } = getDB();
      await testing.assertFails(
        updateDoc(doc(clientDB, "userPrivate", otherUid), { email: "changeEmail" })
      )
    })
    it('update: 認証済み。emailが文字列でない場合不可。', async () => {
      const { clientDB } = getDB();
      await testing.assertFails(
        updateDoc(doc(clientDB, "userPrivate", uid), { email: 0 })
      )
    })
    it('update: 認証済み。emailが31文字以上不可。', async () => {
      const { clientDB } = getDB();
      await testing.assertFails(
        updateDoc(doc(clientDB, "userPrivate", uid), { email: "1234567890123456789012345678901" })
      )
    })
    it('update: 認証済み。許可されたフィールド以外は不可', async () => {
      const { clientDB } = getDB();
      await testing.assertFails(
        updateDoc(doc(clientDB, "userPrivate", uid), { email: "changeEmail", uid: "changeUID" })
      )
    })
  })
})
// topic
describe('topic collection', () => {
  // list
  describe('list', () => {
    it('list:authorizedUIDsにログインユーザーのUIDが含まれれば可能 ', async () => {
      const { clientDB } = getDB();
      const q = query(collection(clientDB, "topic"), where("authorizedUIDs", "array-contains", uid));
      await testing.assertSucceeds(
        getDocs(q)
      )
    })
    it('list:authorizedUIDsにログインユーザーのUIDが含まれていない可能性があるクエリは不可', async () => {
      const { clientDB } = getDB();
      const q = query(collection(clientDB, "topic"));
      await testing.assertFails(
        getDocs(q)
      )
    })
  })
  // create
  describe('create', () => {
    it('create: 認証済みで条件を満たす場合は可能', async () => {
      const { clientDB } = getDB();
      await testing.assertSucceeds(
        setDoc(doc(clientDB, "topic", "topicID"), { title: "title", content: "content", uid, authorizedUIDs: [uid] })
      )
    })
    it('create: 未認証では不可。', async () => {
      const { guestClientDB } = getDB();
      await testing.assertFails(
        setDoc(doc(guestClientDB, "topic", "topicID"), { title: "title", content: "content", uid, authorizedUIDs: [uid] })
      )
    })
    it('create: uidにログインユーザーのUIDと異なる値を与えるのは不可', async () => {
      const { clientDB } = getDB();
      await testing.assertFails(
        setDoc(doc(clientDB, "topic", "topicID"), { title: "title", content: "content", uid: otherUid, authorizedUIDs: [uid] })
      )
    })
    it('create: authorizedUIDsにログインユーザーのUIDが含まれていない場合は不可', async () => {
      const { clientDB } = getDB();
      await testing.assertFails(
        setDoc(doc(clientDB, "topic", "topicID"), { title: "title", content: "content", uid: otherUid, authorizedUIDs: [otherUid] })
      )
    })
  })
  // update
  describe('update', () => {
    // モック作成
    beforeEach(async () => {
      await testEnv.withSecurityRulesDisabled(async context => {
        const noRuleDB = context.firestore()
        await setDoc(doc(noRuleDB, "topic", "topicID"), { title: "title", content: "content", uid, authorizedUIDs: [uid] })
        await setDoc(doc(noRuleDB, "topic", "otherUserTopicID"), { title: "title", content: "content", uid: otherUid, authorizedUIDs: [otherUid] })
      })
    })
    it('update: 認証済みで条件を満たす場合は可能', async () => {
      const { clientDB } = getDB();
      await testing.assertSucceeds(
        updateDoc(doc(clientDB, "topic", "topicID"), { title: "title", content: "content", authorizedUIDs: [uid] })
      )
    })
    it('update: 未認証では不可。', async () => {
      const { guestClientDB } = getDB();
      await testing.assertFails(
        updateDoc(doc(guestClientDB, "topic", "topicID"), { title: "title", content: "content", authorizedUIDs: [uid] })
      )
    })
    it('update: uidの変更は不可', async () => {
      const { clientDB } = getDB();
      await testing.assertFails(
        updateDoc(doc(clientDB, "topic", "topicID"), { title: "title", content: "content", uid: otherUid, authorizedUIDs: [uid] })
      )
    })
    it('update: uidフィールドの値がログイン中のユーザーと異なるキュメントは不可', async () => {
      const { clientDB } = getDB();
      await testing.assertFails(
        updateDoc(doc(clientDB, "topic", "otherUserTopicID"), { title: "title", content: "content", authorizedUIDs: [uid] })
      )
    })
  })
  // // delete
  describe('delete', () => {
    // モック作成
    beforeEach(async () => {
      await testEnv.withSecurityRulesDisabled(async context => {
        const noRuleDB = context.firestore()
        await setDoc(doc(noRuleDB, "topic", "topicID"), { title: "title", content: "content", uid, authorizedUIDs: [uid] })
        await setDoc(doc(noRuleDB, "topic", "otherUserTopicID"), { title: "title", content: "content", uid: otherUid, authorizedUIDs: [otherUid] })
      })
    })
    it('delete: 認証済みで条件を満たす場合は可能', async () => {
      const { clientDB } = getDB();
      await testing.assertSucceeds(
        deleteDoc(doc(clientDB, "topic", "topicID"))
      )
    })
    it('delete: 未認証では不可。', async () => {
      const { guestClientDB } = getDB();
      await testing.assertFails(
        deleteDoc(doc(guestClientDB, "topic", "topicID"))
      )
    })
    it('delete: uidフィールドの値がログイン中のユーザーと異なるキュメントは不可', async () => {
      const { clientDB } = getDB();
      await testing.assertFails(
        deleteDoc(doc(clientDB, "topic", "otherUserTopicID"))
      )
    })
  })
})

// history
describe('history collection', () => {
  // list
  describe('list', () => {
    // モック作成
    beforeEach(async () => {
      await testEnv.withSecurityRulesDisabled(async context => {
        const noRuleDB = context.firestore()
        await setDoc(doc(noRuleDB, "topic", "topicID"), { title: "title", content: "content", uid, authorizedUIDs: [uid] })
        await setDoc(doc(noRuleDB, "topic", "outOfAuthTopicID"), { title: "title", content: "content", uid: otherUid, authorizedUIDs: [otherUid] })
      })
    })
    it('list:親topicドキュメントのauthorizedUIDsにログインユーザーのUIDが含まれていたら可能', async () => {
      const { clientDB } = getDB();
      const q = query(collection(clientDB, "topic", "topicID", "history"));
      await testing.assertSucceeds(
        getDocs(q)
      )
    })
    it('list:親topicドキュメントのauthorizedUIDsにログインユーザーのUIDが含まれていなかったら不可', async () => {
      const { clientDB } = getDB();
      const q = query(collection(clientDB, "topic", "outOfAuthTopicID", "history",));
      await testing.assertFails(
        getDocs(q)
      )
    })
    it('list:authorizedUIDsにログインユーザーのUIDが含まれていない可能性があるクエリは不可', async () => {
      const { clientDB } = getDB();
      const q = query(collection(clientDB, "topic"));
      await testing.assertFails(
        getDocs(q)
      )
    })
  })
  // create
  describe('create', () => {
    // モック作成
    beforeEach(async () => {
      await testEnv.withSecurityRulesDisabled(async context => {
        const noRuleDB = context.firestore()
        await setDoc(doc(noRuleDB, "topic", "topicID"), { title: "title", content: "content", uid, authorizedUIDs: [uid] })
        await setDoc(doc(noRuleDB, "topic", "outOfAuthTopicID"), { title: "title", content: "content", uid: otherUid, authorizedUIDs: [otherUid] })
      })
    })
    it('create:親topicドキュメントのauthorizedUIDsにログインユーザーのUIDが含まれていたら、サブコレクション (history)ドキュメント作成可能', async () => {
      const { clientDB } = getDB();
      const docRef = doc(clientDB, "topic", "topicID", "history", "mmhistoryID");
      await testing.assertSucceeds(
        setDoc(docRef, { url: "url", content: "content", uid })
      )
    })
    it('create:親topicドキュメントのauthorizedUIDsにログインユーザーのUIDが含まれていなければ、サブコレクション (history)ドキュメント作成不可', async () => {
      const { clientDB } = getDB();
      const docRef = doc(clientDB, "topic", "outOfAuthTopicID", "history", "mmhistoryID");
      await testing.assertFails(
        setDoc(docRef, { url: "url", content: "content", uid })
      )
    })
    it('create:uidにログインユーザーのUIDと異なる値を与えるのは不可', async () => {
      const { clientDB } = getDB();
      const docRef = doc(clientDB, "topic", "topicID", "history", "mmhistoryID");
      await testing.assertFails(
        setDoc(docRef, { url: "url", content: "content", uid: otherUid })
      )
    })
  })
  // update
  describe('update', () => {
    // モック作成
    beforeEach(async () => {
      await testEnv.withSecurityRulesDisabled(async context => {
        const noRuleDB = context.firestore()
        // 親topicドキュメントのauthorizedUIDsにログインユーザーのUIDが含まれている場合
        await setDoc(doc(noRuleDB, "topic", "topicID"), { title: "title", content: "content", uid, authorizedUIDs: [uid] })
        await setDoc(doc(noRuleDB, "topic", "topicID", "history", "historyID"), { url: "url", content: "content", uid: otherUid })
        // 親topicドキュメントのauthorizedUIDsにログインユーザーのUIDが含まれていない場合
        await setDoc(doc(noRuleDB, "topic", "outOfAuthTopicID"), { title: "title", content: "content", uid, authorizedUIDs: [otherUid] })
        await setDoc(doc(noRuleDB, "topic", "outOfAuthTopicID", "history", "historyID"), { url: "url", content: "content", uid: otherUid })
      })
    })
    it('update:親topicドキュメントのauthorizedUIDsにログインユーザーのUIDが含まれていたら、サブコレクション (history)ドキュメントの更新可能', async () => {
      const { clientDB } = getDB();
      const docRef = doc(clientDB, "topic", "topicID", "history", "historyID");
      await testing.assertSucceeds(
        updateDoc(docRef, { url: "url", content: "content" })
      )
    })
    it('update:親topicドキュメントのauthorizedUIDsにログインユーザーのUIDが含まれていなければ、サブコレクション (history)ドキュメントの更新不可', async () => {
      const { clientDB } = getDB();
      const docRef = doc(clientDB, "topic", "outOfAuthTopicID", "history", "historyID");
      await testing.assertFails(
        updateDoc(docRef, { url: "url", content: "content" })
      )
    })
  })
  //delete
  describe('delete', () => {
    // モック作成
    beforeEach(async () => {
      await testEnv.withSecurityRulesDisabled(async context => {
        const noRuleDB = context.firestore()
        // ログインユーザーが作ったtopic/他人が作ったhistory
        await setDoc(doc(noRuleDB, "topic", "moTopicID"), { title: "title", content: "content", uid, authorizedUIDs: [uid] })
        await setDoc(doc(noRuleDB, "topic", "moTopicID", "history", "moHistoryID"), { url: "url", content: "content", uid: otherUid })
        //他人が作ったtopic/ログインユーザーが作ったhistory
        await setDoc(doc(noRuleDB, "topic", "omTopicID"), { title: "title", content: "content", uid: otherUid, authorizedUIDs: [otherUid] })
        await setDoc(doc(noRuleDB, "topic", "omTopicID", "history", "omHistoryID"), { url: "url", content: "content", uid })
        //他人が作ったtopic/他人が作ったhistory
        await setDoc(doc(noRuleDB, "topic", "ooTopicID"), { title: "title", content: "content", uid: otherUid, authorizedUIDs: [otherUid] })
        await setDoc(doc(noRuleDB, "topic", "ooTopicID", "history", "ooHistoryID"), { url: "url", content: "content", uid: otherUid })
      })
    })
    it('delete:親topicドキュメントのuidがログインユーザーのUIDであれば可能', async () => {
      const { clientDB } = getDB();
      const docRef = doc(clientDB, "topic", "moTopicID", "history", "moHistoryID");
      await testing.assertSucceeds(
        deleteDoc(docRef)
      )
    })
    it('delete:historyドキュメントのuidがログイン中のユーザーであれば可能', async () => {
      const { clientDB } = getDB();
      const docRef = doc(clientDB, "topic", "omTopicID", "history", "omHistoryID");
      await testing.assertSucceeds(
        deleteDoc(docRef)
      )
    })
    it('delete:親topic、history共に他人が作成したものであれば不可', async () => {
      const { clientDB } = getDB();
      const docRef = doc(clientDB, "topic", "ooTopicID", "history", "ooHistoryID");
      await testing.assertFails(
        deleteDoc(docRef)
      )
    })
  })
})

7
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
4