Gmail OAuth 2.0 API Automation Example and example SubmitListener.beforeSubmit
Hi.
Recently I had a use case where I had to verify that our application sends an email in a particular case. In my case, it was an email whenever a certain status is reached, but it could also be for instance the sending of an invitation (user-creation) email or whatnot. In essence: I want to check at a give moment when the status condition is reached that the email is delivered to the proper email address.
In the past, there we some free email generators (like mailinator, 10minutemail,...) where you could get API calls to check the inbox, but I couldn't find any that offered that feature for free.
So I decided to setup a dedicated gmail email address for my tests and talk to the gmail api to read the inbox (all messages), verify the subject and the actual content of a mail and delete the messages. More info on the gmail api here: https://developers.google.com/gmail/api/reference/rest.
To get started, I created a gmail user and setup an OAuth2.0 google client Id (https://console.cloud.google.com/apis/credentials). These settings (clientId and secret) I used to setup an Authorization code grant as described here: https://support.smartbear.com/readyapi/docs/requests/auth/types/oauth2/grants/auth-code.html
So far so good, the readyAPI internal browers showed me the google popup and I could manually insert the authentication that was needed to generate successfully an access token from google.
BUT: When I tried to automate the flow in this popup using the example code provided in the documentation (= https://support.smartbear.com/readyapi/docs/requests/auth/types/oauth2/automate/sample.html) it did not work for me.
Therefore I wanted to share the changes I made to it in order to get it working.
Also the event handler SubmitListener.beforeSubmit (described on the same page) needed some rework for me to work properly.
As an extra, I also have a test case setup script that deletes all emails in the gmail inbox so I have a proper starting situation for my tests.
Hope this can help any other testers that would need this!
Automation Scripts tab of the Auth Manager
I have encrypted project properties that store my gmail username (gmailUser) and password (gmailPass).
Page 1:
// This function asks for permission to use OAuth. The user must be logged in to use it. Logging in is performed in the script below.
function consent() {
if (document.getElementById('submit_approve_access')){
document.getElementById('submit_approve_access').click();
}
}
// This function fills user password in when the user name is already known. It uses the project-level "pass" property.
function fillpwd() {
document.getElementsByName('password')[0].value = '${#Project#gmailPass}';
document.getElementById('passwordNext').click();
window.setInterval(consent, 1000);
}
// This script checks what page is displayed and provides the appropriate data. It uses the project-level "user" and "pass" properties.
if (document.getElementById('profileIdentifier')) {
document.getElementById('profileIdentifier').click();
window.setTimeout(fillpwd, 1000)
}else if (document.getElementById('identifierId') && document.getElementById('identifierNext')) {
document.getElementById('identifierId').value = '${#Project#gmailUser}';
document.getElementById('identifierNext').click();
window.setTimeout(fillpwd, 1000);
} else if (document.getElementByType('password')) {
fillpwd();
} else if(document.getElementById('submit_approve_access')){
window.setInterval(consent, 100);
}
Page 2:
function consent() {
if (document.getElementById('submit_approve_access')){
document.getElementById('submit_approve_access').click();
}
}
window.setInterval(consent, 100);
Event handler SubmitListener.beforeSubmit:
Note: There might be some redundant iteration of code in there, feel free to rewrite, main thing is: it works.
I also expected that I could use the "Target" column to filter on the requests steps that start with "gmail*" but that didn't do it. So I fixed that in another way, together with providing some smart checking whether a new token generation is needed or not (gmail token is valid for 60 minutes).
// Import the required classes
import com.eviware.soapui.impl.rest.actions.oauth.OltuOAuth2ClientFacade;
import com.eviware.soapui.support.editor.inspectors.auth.TokenType;
import com.eviware.soapui.model.support.ModelSupport;
import java.time.LocalDateTime
import java.time.format.DateTimeFormatter
def testStepName = context.getModelItem().getName()
def expiresOn = context.expand('${#Project#expiresOn}')
if (testStepName.toLowerCase().contains("gmail")) {
// IF expiresOn == "" OR dateNow is > expiresOn then we need to get a new token. Otherwise the old should still do....
TimeZone.setDefault(TimeZone.getTimeZone('UTC'))
TimeZone tz = TimeZone.getTimeZone("UTC")
LocalDateTime dateNow = LocalDateTime.now()
def patternUTC = "yyyy-MM-dd\'T\'HH:mm:ss.SSS\'Z\'"
DateTimeFormatter dateFormatUTC = DateTimeFormatter.ofPattern(patternUTC).withLocale(Locale.US)
String nowUtcFormat = dateNow.format(dateFormatUTC)
LocalDateTime dateExpiresOn = dateNow.plusMinutes(55)
String expiresOnUtcFormat = dateExpiresOn.format(dateFormatUTC)
if (expiresOn == "") {
/*
log.info("Expires on is empty, so we need to run submitListener and get new token. We also write the new expiresOn to the project properties!")
log.info("nowUtcFormat = " + nowUtcFormat)
log.info("dateExpiresOn = " + expiresOnUtcFormat)
log.info "let's run the submitListener.beforeSubmit to get a new accessToken. We know this one will be valid for 60 minutes, so we set a the expiresOn project property to now + 55 minutes"
*/
// Set up variables
def project = ModelSupport.getModelItemProject(context.getModelItem())
project.setPropertyValue("expiresOn", expiresOnUtcFormat)
def authProfile = project.getAuthRepository().getEntry("google")
def oldToken = authProfile.getAccessToken()
def tokenType = TokenType.ACCESS
// Create a facade object
def oAuthFacade = new OltuOAuth2ClientFacade(tokenType)
// Request an access token in headless mode
oAuthFacade.requestAccessToken(authProfile, true, true)
// Wait until the access token gets updated
int iteration = 0
while (oldToken == authProfile.getAccessToken() && iteration < 10) {
sleep(500)
iteration++
}
// Post the info to the log
log.info("Gmail authentication event handler: Project property \"expiresOn\" is empty! We get/set new token: " + authProfile.getAccessToken() + " with new expiresOn = " + expiresOnUtcFormat + ". The old token (with unknown expiresOn) was = " + oldToken)
} else {
def potentialNewExpiresOn = expiresOnUtcFormat
//expiresOn retrieved from project properties
dateExpiresOn = LocalDateTime.parse(expiresOn, dateFormatUTC)
expiresOnUtcFormat = dateExpiresOn.format(dateFormatUTC)
//log.info("dateExpiresOn = " + expiresOnUtcFormat)
if (dateNow > dateExpiresOn) {
// log.info "Expired! Let's get a new token..."
// Set up variables
def project = ModelSupport.getModelItemProject(context.getModelItem())
def authProfile = project.getAuthRepository().getEntry("google")
def oldToken = authProfile.getAccessToken()
def tokenType = TokenType.ACCESS
// Create a facade object
def oAuthFacade = new OltuOAuth2ClientFacade(tokenType)
// Request an access token in headless mode
oAuthFacade.requestAccessToken(authProfile, true, true)
// Wait until the access token gets updated
int iteration = 0
while (oldToken == authProfile.getAccessToken() && iteration < 10) {
sleep(500)
iteration++
}
// Post the info to the log
project.setPropertyValue("expiresOn", potentialNewExpiresOn)
log.info("Token was expired! We get/set new token: " + authProfile.getAccessToken() + " with new expiresOn = " + potentialNewExpiresOn + ". The old token (expiresOn = $expiresOn vs now " + nowUtcFormat + ") was = " + oldToken)
} else {
//log.info "Not yet expired. Let's keep using the same old token (expiresOn = $expiresOn vs now " + nowUtcFormat + ")"
}
}
}
Setup script to delete all emails in the gmail inbox for proper start situation:
I have a disabled test suite "WorkItem" with a test case named "GmailStartSituationCleanup"
This test case has 4 steps:
1° GET gmail messagesList
2° Script "IterateOverAllGmailMessageIds"
3° DELETE gmail messageId
4° GET gmail messagesList-EmptyListCheck
def testSuiteWorkItem = testRunner.testCase.testSuite.project.getTestSuiteByName("WorkItem")
def testCaseGmailStartSituationCleanup = testSuiteWorkItem.getTestCaseByName("GmailStartSituationCleanup")
testCaseGmailStartSituationCleanup.run(new com.eviware.soapui.support.types.StringToObjectMap(), false)
The groovy test step 2° IterateOverAllGmailMessageIds =
import com.eviware.soapui.support.JsonUtil
def testStepAllMessages = testRunner.testCase.getTestStepAt(context.getCurrentStepIndex()-1)
def teststepNameAllMessages = testStepAllMessages.getName()
def testStepDeleteMessage = testRunner.testCase.getTestStepAt(context.getCurrentStepIndex()+1)
testStepDeleteMessage.setDisabled(true)
def testStepVerifAllDeleted = testRunner.testCase.getTestStepAt(context.getCurrentStepIndex()+2)
testStepVerifAllDeleted.setDisabled(false)
def responseStatus = testStepAllMessages.testRequest.response.responseHeaders["#status#"][0]
if (responseStatus.contains("HTTP/1.1 2")){
def responseMessages = context.expand( '${'+teststepNameAllMessages+'#Response#$[\'messages\']}' )
if (responseMessages!= "" && responseMessages!= null){
def numberOfMessages = (JsonUtil.parseTrimmedText(responseMessages)).size()
def id
for (i=0;i<numberOfMessages;i++){
id = context.expand( '${'+teststepNameAllMessages+'#Response#$[\'messages\']['+i+'][\'id\']}' )
testStepDeleteMessage.setPropertyValue("messageId", id)
log.info "Cleanup of gmail messages : Message "+(i+1).toString()+"/"+numberOfMessages.toString()+" with messageId $id will be deleted so we can continue with a proper starting situation..."
testStepDeleteMessage.run(testRunner, context)
}
}else{
log.info "Cleanup of gmail messages : No messages found. We can continue with proper starting situation"
testStepVerifAllDeleted.setDisabled(true)
}
}