9
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

GoodpatchAdvent Calendar 2019

Day 22

Webサービスのe2eテスト  〜メール認証編〜

Last updated at Posted at 2019-12-25

はじめに

この記事は Goodpatch Advent Calendar 2019 の22日目です.

私が現在担当しているWebサービスの開発において、Puppeteerを用いたe2eテストを用いてQAの効率化を図っています。
この記事では Node.js と Gmail API を使い、アカウント作成時のメール認証を自動化する方法について共有したいと思います。

注:この記事ではPuppeteerには触れません!

環境準備

メールをNode.jsで取得するためには、Gmail APIの設定と、各種ファイルの取得・生成が必要です。
基本的にはNode.js Quickstartに従って作業します。

フォルダの準備

あらかじめ、各種ファイルを保存するフォルダの準備しておきます。

例として、以下のような構造にします。

image.png

Gmail Credentialの取得と配置

あらかじめ、利用するGmailのアカウントでログインしておきます。
image.png

ログイン後、以下のページを開きます
Node.js Quickstart | Gmail API

ボタンを押して、APIをEnableにします
image.png

モーダルが表示されるので、ボタンを押してcredensial.jsonをダウンロードし、
image.png

e2e/env フォルダに保存します
image.png

トークンの取得

Node.js Quickstart | Gmail API の Step2 に従って、以下をインストールします。

$ npm install googleapis@39 --save

Node.js Quickstart | Gmail API
中のStep 3 のコードをコピーし、 e2e/scripts/get-token.js と名付けて保存します。

フォルダ構造を合わせるため、credentials.jsのファイルパスと、トークンの出力先パス TOKEN_PATHを以下のように修正します。

const TOKEN_PATH = __dirname + '/../env/token.json';

// Load client secrets from a local file.
fs.readFile(__dirname + '/../env/credentials.json', (err, content) => {

保存後、以下のコマンドを実行します。

$ node e2e/scripts/get-token

すると、以下ようにURLがあらわれるので、言われた通りこのページを開きます。
image.png

ログインするアカウントを選択すると、以下の画面がでて来るので、詳細を表示Quickstartに移動します
image.png

権限の付与を許可します
image.png
image.png

コードが表示されるので、コピーし、
image.png

ターミナルに戻って Enter the code from that page here: のあとに貼り付け、Enterします。
image.png

すると、Gmailで利用しているラベルの一覧が表示され、e2e/env フォルダに token.jsファイルが生成されます。

これで、Node.jsからGmailを利用する準備は完了です。

Gmailから目的のメールを取得する

準備が長かった気がしますが、ここからが本番です。

数多くのメールの中から、認証リンクを含んだメールを探し、本文からリンク取得します。

取得すべきメール

取得すべきメールは以下のようなものです。
APIのフィルタ機能と、本文への正規表現を用いた検索を使ってこのメールを探します。

  • 未読状態である。
  • Webサービスのメール送信用アドレスから送信されている。
    (ここではhoge@piyo.jpとします)
  • 登録したメールアドレスに送信されている。
    (登録に利用したメールアドレスを引数として渡す)
  • 本文に認証リンクを含む。
    (ここでは https://hoge.piyo.jp/mail/XXXXXX のフォーマットであるとします)

環境準備

追加で以下のpackageをインストールします。

$ npm i google-auth-library -s

最終的なコード

先に最終的なコードを貼っておきます。
以下で説明していきます。

const fs = require('fs')
const { promisify } = require('util')
const { google } = require('googleapis')
const { OAuth2Client } = require('google-auth-library')
const gmail = google.gmail('v1')
const TOKEN_PATH = __dirname + '/../env/token.json'
const SECRET_PATH = __dirname + '/../env/credentials.json'
const sleep = msec => new Promise(resolve => setTimeout(resolve, msec))
const MAX_RETRY = 10

//Promise 化
const readFileAsync = promisify(fs.readFile)

const getMessageList = params => {
  return new Promise((resolve, reject) => {
    gmail.users.messages.list(params, (error, response) => {
      if (error) {
        reject(error)
        return
      }
      resolve(response.data)
    })
  })
}
const getMessage = params => {
  return new Promise((resolve, reject) => {
    gmail.users.messages.get(params, (error, response) => {
      if (error) {
        reject(error)
        return
      }
      resolve(response.data)
    })
  })
}

const getRegisterToken = async ({ email }) => {
  //クレデンシャル情報の取得
  const content = await readFileAsync(SECRET_PATH) //クライアントシークレットのファイルを指定
  const credentials = JSON.parse(content) //クレデンシャル

  //認証
  const clientSecret = credentials.installed.client_secret
  const clientId = credentials.installed.client_id
  const redirectUrl = credentials.installed.redirect_uris[0]
  const oauth2Client = new OAuth2Client(clientId, clientSecret, redirectUrl)
  const token = await readFileAsync(TOKEN_PATH)
  oauth2Client.credentials = JSON.parse(token)

  //API経由でシートにアクセス
  try {
    const getToken = async () => {
      // メッセージリスト取得
      const data = await getMessageList({
        auth: oauth2Client,
        userId: 'me',
        q: `is:unread from:piyo@hoge.jp to:${email}`,
      })
      if (!data.messages || data.messages.length === 0) {
        console.log('no message')
        return
      }
      const message = await getMessage({
        auth: oauth2Client,
        userId: 'me',
        id: data.messages[0].id,
      })
      const text = Buffer.from(
        message.payload.parts[1].body.data,
        'base64'
      ).toString('utf8')
      const regex = new RegExp(
        /https:\/\/hoge\.piyo\.jp\/mail\/([A-Za-z0-9]+)/
      )
      const result = text.match(regex)
      if (!result) {
        console.log('not matched')
        return
      }
      console.log('matched', result[1])
      return result[1]
    }

    let retry = MAX_RETRY
    let token = null
    while (retry > 0) {
      token = await getToken()
      if (token) {
        break
      }
      retry--
      await sleep(10000)
    }
    console.log('token', token)
    return token
  } catch (err) {
    return ''
  }
}

module.exports = { getRegisterToken }

Promise化

async/awaitで書きたいので、各種関数をPromiseでラップします。
promisifyが使えるものについては、promisifyを使い、そうでないものはベタに書いていきます。

//promisifyでプロミス化
const readFileAsync = promisify(fs.readFile)

const getMessageList = params => {
  return new Promise((resolve, reject) => {
    gmail.users.messages.list(params, (error, response) => {
      if (error) {
        reject(error)
        return
      }
      resolve(response.data)
    })
  })
}
const getMessage = params => {
  return new Promise((resolve, reject) => {
    gmail.users.messages.get(params, (error, response) => {
      if (error) {
        reject(error)
        return
      }
      resolve(response.data)
    })
  })
}

認証データの準備

認証データをファイルから読み出し、OAuth2の認証クライアントを生成します。

  //クレデンシャル情報の取得
  const content = await readFileAsync(SECRET_PATH) //クライアントシークレットのファイルを指定
  const credentials = JSON.parse(content) //クレデンシャル

  //認証
  const clientSecret = credentials.installed.client_secret
  const clientId = credentials.installed.client_id
  const redirectUrl = credentials.installed.redirect_uris[0]
  const oauth2Client = new OAuth2Client(clientId, clientSecret, redirectUrl)
  const token = await readFileAsync(TOKEN_PATH)
  oauth2Client.credentials = JSON.parse(token)

メッセージリストの取得

メッセージリストを検索クエリを付与してフィルタリングし、取得します。

指定 意味
is:unread 未読
from:hoge@piyo.jp hoge@piyo.jp から送信されている
to:${email} ${email} へ送信されている
      // メッセージリスト取得
      const data = await getMessageList({
        auth: oauth2Client,
        userId: 'me',
        q: `is:unread from:piyo@hoge.jp to:${email}`,
      })

メール本文の取得

メール本文を取得します。
ここでは、メッセージリストで複数候補があっても1つ目のメールのみを取得しています。

      const message = await getMessage({
        auth: oauth2Client,
        userId: 'me',
        id: data.messages[0].id, //1つ目のメールを指定
      })

デコード

Gmailでは本文は、UTF-8の文字列バイトデータがBase64エンコードされたものになっています。
そのため、Base64からバイト配列に変換し、UTF-8に変換、といったデコードが必要です。

      const text = Buffer.from(
        message.payload.parts[1].body.data,
        'base64'
      ).toString('utf8')

文字列の抽出

ここまできたら正規表現で文字列を検索・抽出してあげるだけです!

      const regex = new RegExp(
        /https:\/\/hoge\.piyo\.jp\/mail\/([A-Za-z0-9]+)/
      )
      const result = text.match(regex)

リトライ

メールが直に送信されるとは限らないので、リトライの仕組みを入れています。
ここでは、10秒毎に最大10回リトライするようにしています。

    let retry = MAX_RETRY
    let token = null
    while (retry > 0) {
      token = await getToken()
      if (token) {
        break
      }
      retry--
      await sleep(10000)
    }

最後に

まだ追記するべき点がありますので、後ほど更新します!

時間切れにてここまで。何かのお役に立ちますように〜〜〜!

9
2
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
9
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?