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

# 前提
・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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/80b8a746-51ee-f13b-de18-9455db4a7494.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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/547556/23ce1d7d-010b-342d-febf-4256d4b8806f.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)
      )
    })
  })
})

```