背景
使用一次性密码(OTP)身份验证实施应用程序的端到端自动测试时,我们需要建立一种测试向用户的OTP交付的整个流量的方法。
在这里,我提供了两个可以在此类测试中使用的类,以通过后端发送的电子邮件和整合到您的框架中的分步指南接收OTP。
约束
要实现此方法,必须满足以下条件:
- QA团队管理一个专用的电子邮件帐户。
- 传递OTP的电子邮件具有已知的常数主题文本
- 已知的常数短语先于电子邮件正文的OTP
- OTP长度固定
算法
沿执行,一个自动:
- 根据配置设置(电子邮件提供商API凭据)创建并保留电子邮件提供商服务的实例
- 获取并保留一个指向最后一个接收的电子邮件,其中设置为OTP电子邮件设置的主题(或者如果没有人收到的话,请保留null)
- 触发OTP生成和交付
- 等待电子邮件发送给电子邮件提供商
- 获取电子邮件
- 从消息中解析OTP
- 使用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。
存储库
注意
- 要启用Java
assert
验证,请使用JVM参数-ea
-
在使用类之前,您必须启用并为Gmail帐户配置API,如图所示,如below。
-
GmailHandler
从电子邮件摘要中提取OTP。如果您的电子邮件主体中的OTP离开始太远,因此未包含在摘要中,请使用getPayload()
而不是getSnippet()
。
如何设置Gmail帐户API
继续前进,您必须激活并配置您将使用的Gmail帐户的API接收OTP电子邮件。使用Google Cloud Console按以下步骤。
注册一个新项目
启用API
创建自动节目以访问您的Gmail帐户的凭据
注册值得信赖的测试用户
如何向项目添加Gmail帐户凭据
以JSON格式接收客户端ID文件(如上图),您必须在第一次致电Gmail API时将其交换为StoredCredential
文件。
添加JSON客户端ID文件
验证在Gmail上对帐户的自动访问
逐步
第一次运行您的项目。在第一次致电Gmail API时,浏览器将由Google打开。您应该关注Google对话框。
Continue
验证应用程序
Continue
授予访问
检查存储文件
如果将来更改控制台中的Gmail API配置,则应删除逐步
src/main/resources/credentials
中自动创建StoredCredential
文件;如果不是,请再次重复this section。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);
}
}