Firebase AuthとFunctionsを使ったbusboyベースのメールパーサを作る

以前作った[SendGrid経由で受信したメールをFirebase Functionsを使ってパースする記事](https://qiita.com/festa78/items/ac926c921a50055263e8)の続きです。

今回はメールを受信する事で起動するFirebase Functionsによるパーサへ、Firebase Authによるユーザー情報取得とそれに合わせたFirestoreへの保存を行います。またそれに対する簡単なテストを書いてみます。

## Functionsによるメールパーサ

前回記事から少し修正しましたが、基本は変わらずで`onRequest`経由でSendGridからemail情報を`multipart/form-data`として受け取ります。
受け取ったデータは[busboy](https://github.com/mscdex/busboy)を使ってパースします。

```js
exports.addMessage = functions.https.onRequest(async (req, res) => {
  try {
    await new Promise((resolve, reject) => {
      console.log('Email recieved')

      let email_data = {};

      const busboy_parser = new busboy({ headers: req.headers })

      busboy_parser.on("field", (field, val) => {
        console.log(`Processed field ${field}: ${val}.`);
        if (field === 'envelope') {
          console.log('find envelope')
          email_data['email'] = JSON.parse(val).from;
        } else if (field === 'subject') {
          console.log('find subject')
          email_data['title'] = val;
        } else if (field === 'html') {
          console.log('find html')
          email_data[field] = val;
        } else if (field === 'text') {
          console.log('find text')
          email_data[field] = val;
        }
      })

      busboy_parser.on("error", () => {
        console.log('busboy error');
        reject('busboy error');
      });

      busboy_parser.on("finish", () => {
        email_data['tags'] = ['__all__'];
        email_data['timestamp'] = admin.firestore.FieldValue.serverTimestamp();
        console.log('busboy finished', email_data);

        firebaseGetUserByEmail(email_data['email'])
          .then((userId) => {
            console.log('get uid', userId);
            return fireStore.collection('user_data/' + userId + '/snippets')
              .doc()
              .set(email_data);
          })
          .then(() => {
            console.log('added tags');
            resolve();
          })
          .catch((error) => {
            console.log('error tags:', error);
            reject(error);
          });
        });

      busboy_parser.end(req.rawBody);
    });
  } catch (error) {
    console.log(error);
    res.send(400);
  } finally {
    res.send(200);
  }
});
```

主な追加点としては２点あります：

1. `email_data`へまずパース結果を`.on("field",)`経由で保存していく。最後に`.on("finish",)`で取得した`email_data`をユーザー毎に分けてFirestoreへ保存する。
2. `Promise`で全体を覆う。

特に２点目はこの後テストを書く際に、全処理が終わった後に結果をテストするために必要です。

コード中の`firebaseGetUserByEmali`以下のようになっています

```js
function firebaseGetUserByEmail(email) {
  return admin.auth().getUserByEmail(email)
    .then((userRecord) => {
      // See the UserRecord reference doc for the contents of userRecord.
      console.log('Successfully fetched user data:', userRecord.toJSON());
      return userRecord.uid;
    })
    .catch((error) => {
      console.log('Error fetching user data:', error);
      throw error;
    });
}
```

## テスト

続いて簡単なテストを書いていきます。[公式ドキュメント](https://firebase.google.com/docs/functions/unit-testing?hl=ja)を参考にしつつ、テスト用プロジェクトを作成し、それを用いてテストしていきます。

まずは、ダミーの`multipart/form-data`を作成します

```js
const boundary = '---------------------------paZqsnEHRufoShdX6fh0lUhXBP4k'
const req = {
    headers: {
        'content-type': 'multipart/form-data; boundary=' + boundary
    },
    rawBody:
        ['-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
        'Content-Disposition: form-data; name="envelope"',
        '',
        '{ "from": "example@example.com" }',
        '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k',
        'Content-Disposition: form-data; name="subject"',
        '',
        'this is an addMessage test',
        '-----------------------------paZqsnEHRufoShdX6fh0lUhXBP4k--'
        ].join('\r\n')
};
```

このダミーデータでは、"example@example.com"というユーザーから"this is an addMessage test"というタイトルのemailが送られてきた想定となっています。
そのため、テスト用プロジェクトに、今回利用するダミーアカウントをFirebase Authentification UI等から登録しておきます。

続いて、テスト用プロジェクトのFirestoreに対して、テスト後にクリーンアップを行うように後処理を定義します。

```js
describe("addMessage", () => {
    afterEach(() => {
        return admin.auth().getUserByEmail('example@example.com')
            .then(userRecord => {
                return admin.firestore().collection('user_data/' + userRecord.uid + '/snippets')
                    .where("title", '==', 'this is an addMessage test')
                    .get();
            })
            .then((querySnapshot) => {
                querySnapshot.forEach(doc => {
                    console.log('delete doc', doc.id);
                    doc.ref.delete();
                })
            })
        });

    ...
});
```

最後に、実際に`addMessage` functionsを叩いて、Firestoreへ当該データが登録されたかどうかを確認します。

```js
describe("addMessage", () => {
    ...

    it("Properly parse and add email message", function () {
        return new Promise((resolve, reject) => {
            const res = {
                send: (code) => {
                    console.log(code);
                    admin.auth().getUserByEmail('example@example.com')
                        .then((userRecord) => {
                            return admin.firestore()
                                .collection('user_data/' + userRecord.uid + '/snippets')
                                .where("title", '==', 'this is an addMessage test')
                                .get();
                        })
                        .then((querySnapshot) => {
                            let idList = [];
                            querySnapshot.forEach((doc) => {
                                console.log(doc.id, " => ", doc.data());
                                idList.push(doc.id);
                            })
                            console.log('num id list', idList.length);
                            assert.equal(idList.length, 1);
                            resolve();
                        })
                        .catch((error) => {
                            console.log('Error at addMessage test assertions:', error);
                            reject(error);
                        });

                }
            };
            myFunctions.addMessage(req, res);
        });
    });
});
```

ここで`addMessage`内で`Promise`を使って`busboy`をラップしておいたおかげで、確実に`busboy`によるemailのパース処理及びFireStoreへの書き込みが完了するのを待ってから、`send`によって定義されたcallback関数による結果のチェックが確実に行えるようになっています。