Cypress + Gmail APIでメール+SMSの2FA認証をテスト自動化する(気合&パワー)

こんにちは!QA Engineerの@fukutomiです。
この記事は、Merpay Advent Calendar 2023 の11日目の記事です!
メルカリエンジニアリングブログに寄稿するのは初めてなので緊張しますが、よろしくお願いします。

はじめに(この記事はなんなのか)

今回のテーマは、弊社が運営しているパ・リーグ Exciting Moments β(略してPEM)におけるログイン処理をテスト自動化してみよう、です。

※パ・リーグ Exciting Moments βとは
「パ・リーグ Exciting Moments β」は、パ・リーグ6球団の記憶に残る名場面やメモリアルシーンを捉えた動画コンテンツを自分だけのコレクションとして保有できるパ・リーグ6球団公式のサービスです。

PEMはログインしないと大抵の機能が利用できず、テスト自動化をしたいならログイン処理の突破は必須。。。
後述する通りPEMのログイン処理は結構複雑なのですが、気合&パワーでなんとか実装したので、よかったら見てやってください。

PEMのログイン構造

最初にPEMのログイン処理について簡単に説明します。
PEMはE-mailとSMSの2要素認証(2FA)を採用しています。

PEMのログイン構造

お客さまが行う作業としては

  1. ログイン画面でE-mailアドレス入力
  2. サービスからメールが届くので、メール内のリンクを開く
  3. リンクを開くと登録されている電話番号にSMSが届く
  4. 同時にSMS認証番号入力画面を開くので、SMSに記載されている認証番号を入力
  5. (場合によってはここでreCAPTHA認証が入りますが、テスト環境では表示しない設定にしているので割愛)
  6. ログイン完了!

こんな形で結構複雑でして、今回はこれをCypress+Gmail APIですべて自動化しよう、という話です。

どんな仕組みで自動化するのか

今回はPEMを自動で動かすツールとしてCypressを、GmailにアクセスするためにGmail APIを、またテスト用電話番号の準備のためFirebaseを利用します。
上記ログイン構造のお客さまが行う作業をもとに、下記の感じで自動化してみます。

  1. CypressでPEMのログイン画面を開く
  2. ログイン画面でE-mailアドレスを入力して送信
  3. サービスからメールが届くので、GoogleにログインしGmail APIを利用してメールを検索
  4. メール本文からログイン用のリンクを抜き出す
  5. 抜き出したリンクをCypressで開く
  6. SMS認証番号入力画面に遷移するので、あらかじめFirebaseで設定しておいたテスト用電話番号の確認コードを入力
  7. ログイン完了!

それでは実際にやってみましょう!

下準備

Cypressのインストール

まずは下記を参照にCypressをインストールしましょう。

※Cypressとは
ウェブアプリケーションをフロントエンドで自動で動かすことができる、オープンソースソフトウェアのテストツールです。

(詳細は本題から逸れちゃうので割愛します)
Installing Cypress – Cypress.io
Opening the App – Cypress.io

GCPの準備

次にGCPのプロジェクトを作成します。
こちらも本題から逸れるので割愛!
プロジェクトの作成と管理 – Google Cloud

Gmail APIの準備とid,secretの確認

GCPのプロジェクトを作成したら、次はGmail APIを準備します。

  1. サイドメニューから「APIとサービス」を選択、画面遷移
  2. 「+APIとサービスの有効化」を押下し、ライブラリへ
  3. 「Gmail API」で検索し、APIの詳細画面へ
  4. 有効化

GCP上のGmail APIの画面

(出典:Google Cloud Platform)

  1. Gmail APIを有効化できたら認証情報を作成します。
  2. API管理画面を開き、認証情報タブを選択
  3. 「+認証情報を作成」を押下、OAuth クライアント IDを選択
  4. 作成画面に遷移するので、下記の情報を入力して作成

入力する内容はこんな感じ。

Gmail APIの画面

(出典:Google Cloud Platform)

作成完了後、詳細画面を開くとAdditional informationエリアが表示されます。
「クライアント ID」「クライアント シークレット」をあとで利用します。

認証情報の詳細

(出典:Google Cloud Platform)

Firebaseの準備(テスト用電話番号の準備)

PEMのログイン情報はFirebaseで管理しています。
FirebaseのAuthenticationでは、テストで使用できる電話番号ならびに確認コードをセットすることができるので、そちらを登録しておきます。
登録したテスト電話番号と確認コードはあとで利用するのでメモしておくとよいでしょう。

(出典:Firebase)

リフレッシュトークンの発行

(参照:Google Authentication – Cypress.io
Googleにログインするためにリフレッシュトークンを発行します。
Google Developpers OAuth 2.0 Playgroundにアクセスして、リフレッシュトークンを発行しましょう。

まず事前設定として、上記のGmail API認証情報をセットします。
右上の歯車マークから設定可能です。
「Use your own OAuth credentials」にチェックを入れると認証情報の入力欄が表示されます。

(出典:Google Developpers OAuth 2.0 Playground)

それが終わったらScopeを選択してAuthorizeします。
自分はこんな感じで設定しました。

AuthorizeするとAuthorization codeが表示されます。
今回用があるのはリフレッシュトークンなので、「Exchange authorization code for tokens」を押下してリフレッシュトークンを生成してください。

(出典:Google Developpers OAuth 2.0 Playground)

さて、これで事前準備が整いました。
ここからは実際に自動テストのコーディングに入っていきます。

コーディング

まずは環境変数をCypress.env.jsonに定義しておきましょう。
セキュリティ的な観点でも、上記のトークンとかはベタ書きするわけにはいかないですからね!

{
  "google_client_id": "xxxxxxxxxx",
  "google_client_secret": "yyyyyyyyyy",
  "google_refresh_token": "zzzzzzzzzzzzzzz",
  "sign_in_email": "hogehoge@mercari.com",
  "test_phone": "07000000000",
  "test_phone_sms": "123456",
  "from_email": "hogehoge"
}

次はほんとにログイン処理を書いていきましょう。
まずはログイン画面に遷移して、メールアドレスを入力します。

  it("ログインページに遷移、メールアドレスを入力して送信", () => {
    // ログインページに遷移
    cy.visit("/signin/");

    // メールアドレスで登録画面に遷移
    cy.contains("メールアドレスでログイン").click();

    // ログインページにいることを確認
    cy.contains("h1", "ログイン").should("be.visible");

    // メールアドレス入力
    cy.get("input[name=email]").type(Cypress.env("sign_in_email"));

    // フォームを送信
    cy.get("form").submit();

    // メッセージ確認
    cy.contains("メールをチェックしてください").should("be.visible");

    // メールが来るまでちょっと待つ(ほんとはメールが来るのをキャッチしたい
    cy.wait(15000);
  });

メールアドレス送信後、メールが届くまでちょっと待って、メール内からリンクを引っ張ってアクセスする作業に入ります。


  it("受け取ったメールからリンクを読み取ってアクセス", () => {
    //Googleへのアクセストークンを生成する
    cy.request({
      method: "POST",
      url: "https://www.googleapis.com/oauth2/v4/token",
      body: {
        grant_type: "refresh_token",
        client_id: Cypress.env("google_client_id"),
        client_secret: Cypress.env("google_client_secret"),
        refresh_token: Cypress.env("google_refresh_token"),
      },
    }).then(({ body }) => {
      const access_token = body.access_token;
      // 件名にサインインを含む、未読、Toがログインメールアドレスになっているメールを1件だけ抽出
      cy.request({
        method: "GET",
        url: "https://content-gmail.googleapis.com/gmail/v1/users/me/messages",
        headers: {
          Authorization: `Bearer ${access_token}`,
        },
        qs: {
          q: `from:${Cypress.env("from_email")} subject:サインイン is:unread to:${Cypress.env("sign_in_email")}`,
          maxResults: 1,
        },
      }).then(({ body }) => {
        const mailID = body.messages[0].id;

        // 取得したメールIDをもとにメールの詳細を取得する
        cy.request({
          method: "GET",
          url: `https://content-gmail.googleapis.com/gmail/v1/users/me/messages/${mailID}`,
          headers: {
            Authorization: `Bearer ${access_token}`,
          },
        }).then(({ body }) => {

          // 取得したメール詳細をデコードしつつ本文を抜きだす
          var mailBody = decodeURIComponent(
            escape(
              atob(
                body.payload.parts[1].body.data
                  .replace(/-/g, "+")
                  .replace(/_/g, "/")
              )
            )
          );
          // URLを囲むコーテーションがシングルだったりダブルだったりするので、ダブルに統一
          mailBody = mailBody.replace(/'/g, '"');

          // 文中最初のURLだけを抽出する
          const accessUrl = mailBody
            .substring(mailBody.indexOf("http"), mailBody.indexOf('">'))
            .trim();

          // 抽出したURLにvisit
          cy.visit(accessUrl);
        });
      });
    });
  });

リンクにアクセスすると電話番号入力画面に遷移するので、予め設定しておいたテスト電話番号と確認コードを入力し、ログイン完了!というわけですね。

  it("電話番号を入力してログイン完了", () => {
    // 描画が完了し、画面がSMS認証番号入力に切り替わるまで待つ
    cy.wait(5000);

    // SMS認証番号入力画面に切り替わったことを確認
    cy.contains("電話番号に届いた6桁の確認コードを入力してください").should(
      "be.visible"
    );

    // SMS暗証番号入力
    cy.contains("電話番号に届いた6桁の確認コードを入力してください")
      .parent("form")
      .within(($form) => {
        cy.get('input[name="verificationCode"]').type(Cypress.env("test_phone_sms"));

        // 続行する!
        cy.contains("送信する").click();
      });

  });

実際の動作

(出典:Cypress(左)、パ・リーグ Exciting Moments β(右))

あとがき

いかがだったでしょうか。
自分で言うのもなんですが、すんごい力業だったと思います。
まあでも、ログイン処理が自動化できたことでその後のMoment購入処理やマイページのテストを自動化することができました。
可読性や保守性ももちろん大事なんですが、目的を果たすことが第一ということで。

ちなみに今回はCypressを利用しましたが、別の他のツールでもできると思うのでよかったら試してみてください。

さて、パ・リーグ Exciting Moments βは2024年3月31日をもってサービス終了することになりました。
あと少しではありますが、パ・リーグ Exciting Moments βのことをよろしくお願いします。

以上です!

明日はLiuさんが担当します。お楽しみに!

  • X
  • Facebook
  • linkedin
  • このエントリーをはてなブックマークに追加