使用E2E UI测试中通过电子邮件发送的OTP。
#教程 #测试 #java #testautomation

背景

使用一次性密码(OTP)身份验证实施应用程序的端到端自动测试时,我们需要建立一种测试向用户的OTP交付的整个流量的方法。
在这里,我提供了两个可以在此类测试中使用的类,以通过后端发送的电子邮件和整合到您的框架中的分步指南接收OTP。

约束

要实现此方法,必须满足以下条件:

  • QA团队管理一个专用的电子邮件帐户。
  • 传递OTP的电子邮件具有已知的常数主题文本
  • 已知的常数短语先于电子邮件正文的OTP
  • OTP长度固定

算法

沿执行,一个自动:

  1. 根据配置设置(电子邮件提供商API凭据)创建并保留电子邮件提供商服务的实例
  2. 获取并保留一个指向最后一个接收的电子邮件,其中设置为OTP电子邮件设置的主题(或者如果没有人收到的话,请保留null)
  3. 触发OTP生成和交付
  4. 等待电子邮件发送给电子邮件提供商
  5. 获取电子邮件
  6. 从消息中解析OTP
  7. 使用OTP验证登录到测试中的应用程序

步骤1-2和4-6在提供的代码中实现。

EmailProviderHandler接口

为了使该方法灵活,已经声明了EmailProviderHandler接口。实施必须提供以下内容:

  • 一项服务启动,可以期待具有指定主题的电子邮件
  • 检查是否已经收到了带有主题的新电子邮件
  • 从接收电子邮件中获取消息

接口
interface EmailProviderHandler {
    void init(String emailSubject);
    boolean isEmailReceived();
    String getMessage();
}

我已经实现了Gmail的接口(请参见下文),但是您可以为您使用的另一个提供商实现它。

Emailotphandler课程

要使用OTP处理电子邮件,请使用EmailedOTPHandler类。
应该为电子邮件主体的特定组合,电子邮件主体中的OTP和OTP长度创建电子邮件。为了给处理程序一个访问特定电子邮件提供商电子邮件的工具,我们应该注入实施的EmailProviderHandler实例。

类变量列表
    private final String emailSubject; // Subject pattern of the emails containing the OTP
    private final String otpKeyPhrase; // Phrase that precedes the OTP in the email body
    private final int otpLength; // Length of the OTP
    private final EmailProviderHandler emailProvider; // Gmail service

在使用EmailedOTPHandler实例获取OTP之前,您需要使用init()方法启动它。
然后,您可以通过应用程序的UI或查询端点来触发OTP生成和交付。
要从电子邮件中获取OTP,请使用getOTPEmailSent()方法。该方法等待带有主题集的新电子邮件,然后尝试从中解析OTP。
如果时间段内没有新消息,则返回空。

显示完整的类代码below

GmailHandler类

GmailHandler实现了EmailProviderHandler,以通过API处理Gmail服务。
Google Gmail Java Quick Start指南中描述的方法用于启动Gmail服务并获得凭据。

在第一个Gmail API调用中,GmailHandler在项目中创建一个凭据文件,以验证对Gmail服务的所有未来呼叫(请参阅详细说明below)。

显示完整的类代码below

存储库

GitHub

注意

  • 要启用Java assert验证,请使用JVM参数-ea

Intellij Idea

IntelliJ IDEA interface for setting the JVM parameters

  • 在使用类之前,您必须启用并为Gmail帐户配置API,如图所示,如below

  • GmailHandler从电子邮件摘要中提取OTP。如果您的电子邮件主体中的OTP离开始太远,因此未包含在摘要中,请使用getPayload()而不是getSnippet()

如何设置Gmail帐户API

继续前进,您必须激活并配置您将使用的Gmail帐户的API接收OTP电子邮件。使用Google Cloud Console按以下步骤。

注册一个新项目

逐步
  • 单击创建项目。

Adding a new project in the Google Cloud Console

  • 然后给您的项目一个名字。

Setting the project name

启用API

逐步
  • 单击启用API和服务。

Enabling APIs

  • 在API库中搜索Gmail。

Searching for Gmail API

  • 启用gmail api

Enabling Gmail API

创建自动节目以访问您的Gmail帐户的凭据

逐步
  • 单击创建凭据。

Start the credentials creating

  • 选择Gmail API A User data类型。

Setting the data type

  • 自定义OAuth同意屏幕 - 输入该应用程序的任何名称,并添加您的联系电子邮件地址

Customizing the OAuth Consent scr

  • 设置范围 选择read only范围
  • 是有意义的

Scope setting

Scope setting 2

  • 选择Desktop app应用程序类型并给它一个名称

Choosing the app type

  • 您的凭据已被创建;您需要以JSON格式下载客户ID文件。

Credentials created scr

  • 您还可以在“凭据”选项卡中随时自定义凭据,然后下载更新的JSON文件。

Customize credentials

Customize credentials 2

注册值得信赖的测试用户

逐步
  • 导航到OAuth同意屏幕选项卡,然后单击ADD USERS

Add test user step 1 scr

  • 添加您的任何真实的Gmail帐户电子邮件地址。您需要以后根据此帐户行事以验证客户ID
  • 的访问权限

Add test user step 2 scr

如何向项目添加Gmail帐户凭据

以JSON格式接收客户端ID文件(如上图),您必须在第一次致电Gmail API时将其交换为StoredCredential文件。

添加JSON客户端ID文件

逐步
  • 将文件放入src/main/resources/credentials

JSON Client ID file in the project structure

验证在Gmail上对帐户的自动访问

逐步
第一次运行您的项目。在第一次致电Gmail API时,浏览器将由Google打开。您应该关注Google对话框。

Access verification - choose account alert

  • 单击Continue验证应用程序

Access verification - continue scr

  • 单击Continue授予访问

Access verification - granting access scr

  • 检查确认

Gmail confirmation scr

  • 停止第一个测试项目执行。

检查存储文件

逐步
  • 在您的第一个Gmail API调用期间,应在src/main/resources/credentials中自动创建StoredCredential文件;如果不是,请再次重复this section

StoredCredential file in the project structure

如果将来更改控制台中的Gmail API配置,则应删除StoredCredential文件并重复以下步骤以添加新的步骤。

完整代码

EmailEdotphandler

<摘要>代码
package gmail;

import static java.lang.Thread.sleep;

interface EmailProviderHandler {
    void init(String emailSubject);
    boolean isEmailReceived();
    String getMessage();
}

public class EmailedOTPHandler {
    private final String emailSubject; // Subject pattern of the emails containing the OTP
    private final String otpKeyPhrase; // Phrase that precedes the OTP in the email body
    private final int otpLength; // Length of the OTP
    private final EmailProviderHandler emailProvider; // Gmail service

    public EmailedOTPHandler(String emailSubject, String otpKeyPhrase, int otpLength, EmailProviderHandler emailProvider) {
        this.emailSubject = emailSubject;
        this.otpKeyPhrase = otpKeyPhrase;
        this.otpLength = otpLength;
        this.emailProvider = emailProvider;
    }

    public void init() {
        emailProvider.init(emailSubject);
    }

    /**
     * Parse and return OTP from the email with id = this.emailID
     */
    private String parseOTP() {
        String mailText = emailProvider.getMessage();
        // Parse OTP
        int pos = mailText.indexOf(otpKeyPhrase);   // Find the OTP key phrase
        assert pos != -1 : "OTP key phrase not found in the email";
        pos = pos + otpKeyPhrase.length();         // Move to the OTP start position
        return mailText.substring(pos, pos + otpLength);
    }

    /**
     * Trying to get a new email, checking for a new message every 5 sec for 6 times.
     * If gotten a new message, return the OTP from it.
     * If there is no new message during the time period, return NULL
     */
    public String getOTP() {
        String otp = null;
        for (int attempt = 0; attempt < 6; attempt++) {
            if (emailProvider.isEmailReceived()) {
                otp = parseOTP();
                break;
            }
            try {
                sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        return otp;
    }
}

GmailHandler

<摘要>代码
package gmail;

import com.google.api.client.auth.oauth2.Credential;
import com.google.api.client.extensions.java6.auth.oauth2.AuthorizationCodeInstalledApp;
import com.google.api.client.extensions.jetty.auth.oauth2.LocalServerReceiver;
import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow;
import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets;
import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport;
import com.google.api.client.http.javanet.NetHttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.client.util.store.FileDataStoreFactory;
import com.google.api.services.gmail.Gmail;
import com.google.api.services.gmail.GmailScopes;
import com.google.api.services.gmail.model.ListMessagesResponse;
import com.google.api.services.gmail.model.Message;

import java.io.*;
import java.nio.file.Files;
import java.security.GeneralSecurityException;
import java.util.*;

public class GmailHandler implements EmailProviderHandler{
    private static final String GMAIL_AUTHENTICATED_USER = "me";
    private final JsonFactory JSON_FACTORY = JacksonFactory.getDefaultInstance();
    private static final String APPLICATION_NAME = "Gmail handler";
    private static final String PATH_TOKEN_DIRECTORY = System.getProperty("user.dir") + "/src/main/resources/credentials";
    private static final String PATH_CREDENTIALS_FILE = PATH_TOKEN_DIRECTORY + "/gmail_credentials.json";
    private Gmail gmailService;
    private final List<String> SCOPES = Arrays.asList(GmailScopes.MAIL_GOOGLE_COM);
    private String emailSubject;
    private String emailID; // ID of the last email with the provided Subject


    public void init(String emailSubject) {
        this.emailSubject = emailSubject;
        startService();
        emailID = getEmailID();
    }

    /**
     * Start a new Gmail service
     */
    public void startService () {
        final NetHttpTransport HTTP_TRANSPORT;
        try {
            HTTP_TRANSPORT = GoogleNetHttpTransport.newTrustedTransport();
            gmailService =  new Gmail.Builder(HTTP_TRANSPORT, JSON_FACTORY, getCredentials(HTTP_TRANSPORT))
                    .setApplicationName(APPLICATION_NAME)
                    .build();
        } catch (IOException | GeneralSecurityException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * Creates an authorized Credential object.
     * This is the code from the Google Developer Console documentation.
     *
     * @param HTTP_TRANSPORT The network HTTP Transport.
     * @return An authorized Credential object.
     * @throws IOException If the credentials.json file cannot be found.
     */
    private Credential getCredentials(final NetHttpTransport HTTP_TRANSPORT) throws IOException {

        InputStream in = Files.newInputStream(new File(PATH_CREDENTIALS_FILE).toPath());
        if (in == null) {
            throw new FileNotFoundException("Resource not found: " + PATH_CREDENTIALS_FILE);
        }
        GoogleClientSecrets clientSecrets = GoogleClientSecrets.load(JSON_FACTORY, new InputStreamReader(in));

        // Build flow and trigger user authorization request.
        GoogleAuthorizationCodeFlow flow = new GoogleAuthorizationCodeFlow.Builder(
                HTTP_TRANSPORT, JSON_FACTORY, clientSecrets, SCOPES)
                .setDataStoreFactory(new FileDataStoreFactory(new File(PATH_TOKEN_DIRECTORY)))
                .setAccessType("offline")
                .build();
        LocalServerReceiver receiver = new LocalServerReceiver.Builder().setPort(8888).build();
        return new AuthorizationCodeInstalledApp(flow, receiver).authorize("user");
    }

    /**
     * Return ID of the last email with subject = emailSubject
     * @return - last email id
     */
    public String getEmailID() {
        List<Message> listOfMessages;
        try {
            ListMessagesResponse response = gmailService.users().messages()
                    .list(GMAIL_AUTHENTICATED_USER)
                    .setQ("subject:" + emailSubject)
                    .execute();
            listOfMessages = response.getMessages();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return (listOfMessages == null || listOfMessages.isEmpty()) ? null :  listOfMessages.get(0).getId();
    }

    /**
     * Return the email snippet text
     * @return - email snippet text as a String
     */
    public String getMessage() {
        Message message;
        try {
            message = gmailService.users().messages()
                    .get(GMAIL_AUTHENTICATED_USER, emailID)
                    .execute();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return message.getSnippet(); // Use getPayload() instead if you want to get the full email body
    }

    /**
     * Check if there is a new email with subject = emailSubject
     * @return
     */
    public boolean isEmailReceived() {
        String newEmailID = getEmailID();
        if (newEmailID == null) {
            return false;
        }
        if (newEmailID.equals(emailID)) {
            return false;
        }
        emailID = newEmailID;
        return true;
    }
}

测试示例

<摘要>代码
import gmail.EmailedOTPHandler;
import gmail.GmailHandler;

public class EmailedOTPTest {

    static String subject = "OTP test";
    static String keyPhrase = "Your OTP is: ";
    static int otpLength = 6;

    public static void main(String[] args) {

        GmailHandler gmail = new GmailHandler();
        EmailedOTPHandler otpHandler = new EmailedOTPHandler(subject, keyPhrase, otpLength, gmail);

        otpHandler.init();
        // -> Here trigger the OTP email sending
        String otp = otpHandler.getOTP();

        assert otp != null : "No new email with subject = '" + subject + "' was received during the time period";
        System.out.println("OTP: " + otp);
    }

}