Automate Testing With OAuth 2.0: a Step-By-Step Tutorial
Want to learn how to automate testing with OAuth 2.0? Check out this tutorial where we learn how to automate testing with OAuth 2.0 and integrate with Jenkins.
Join the DZone community and get the full member experience.
Join For FreeNowadays, OAuth 2.0 has become the most commonly used authentication framework for RESTful API services. Among the different grant types, the Authorization Code grant type is probably the most common of the OAuth 2.0 grant types that you’ll encounter. It is used by both web apps and native apps to get an access token after a user authorizes an app.
Useful links:
OAuth 2.0 Framework - RFC 6749
A complete guide to Oauth 2.0 grant
What is the OAuth 2.0 Authorization Code Grant Type
How to Automate Testing This Kind of Service, Especially for CI/CD?
Candidate solutions, like Selenium or another scripting way, simulate user interactions with the web page and call the automation scripts when they are needed. Selenium can solve some of these situations, however, the complexity of Selenium coding for various login pages is a bit tricky, because the UI tends to change often. All of these can, therefore, become a real headache.
Here, we’re going to introduce how Restbird solves this particular case using its global environment and task. Then, we'll create a test case and integrate it with Jenkins.
All the code used in this arttical can be doanload from GitHub: restbird/example-Box-Oauth2
Case Study: Box API Testing Automation
Take the Box API for example — Box uses the standard OAuth 2 three-legged authentication process, which proceeds as follows:
- The application asks an authentication service to present a login request to a user.
- After a successful login, the authentication service returns an authorization code to the application.
- The application exchanges the authorization code for an access token.
Create a New Rest API Case for Box
First, you will need to create a new REST API case for Box. Click here to learn how to create test cases for a RESTful API.
Add Authorize API for Authorization Code
- First, you will have created a REST API to get the authorization code
Method | GET |
Url | https://account.box.com/api/oauth2/authorize |
Url parameter |
|
Here is how the request looks in Restbird:
- Click “Run test,” and then copy the URL into the web browser:
- Enter user credential and click “Authorize:”
- Click “Grant access to Box:”
- The browser will then redirect to “redirect_ur” as the request specified, in this case, it’s https://localhost, with the authorization code as the “code” parameter.
Add the Get Access Token API
- Create a REST API to get the access token
Method | POST |
Url | https://api.box.com/oauth2/token |
Req Body (application/x-www-form-urlencoded) |
|
Here is how the request looks in Restbird:
- Create the Response Check Scripts to save the access token and refresh the token to a global environment.
Here, Go is used as a sample language; access taken and refresh token will be extracted from the response JSON data and saved into the global environment.
callapi is a Restbird-defined Go language library that has a variety of APIs for core scripting functionality.
package api
import "callapi"
import "net/http"
import "io/ioutil"
import"encoding/json"
import "fmt"
type MyDATA struct {
Access_token string `json:"access_token,omitempty"`
Refresh_token string `json:"refresh_token,omitempty"`
}
func (c CallBack) ResponseValidate(resp *http.Response, ctx *callapi.RestBirdCtx) bool {
var body []byte
var err error
var data MyDATA = MyDATA{}
if body, err = ioutil.ReadAll(resp.Body); err != nil {
fmt.Println("read body failed.")
return false
}
if err = json.Unmarshal(body, &data); err != nil {
fmt.Println("conver body to json failed")
return false
}
callapi.SetGlobalString("box_access_token", data.Access_token)
callapi.SetGlobalString("box_refresh_token", data.Refresh_token)
_, box_access_token := callapi.GetGlobalString("box_access_token")
_, box_refresh_token := callapi.GetGlobalString("box_refresh_token")
fmt.Println("box_access_token: " + box_access_token)
fmt.Println("box_refresh_token: " + box_refresh_token)
return true
}
- After clicking “run test,” you can see the
refresh_token
in the response body.
Click "Globals” to verify that the variables have been saved into the global environment.
Add the Refresh Access Token API to Refresh the Access Token
- Create a REST API to refresh the access token
Method | POST |
Url | https://api.box.com/oauth2/token |
Req Body (application/x-www-form-urlencoded) |
|
{{}} is the syntax for using both local and global environment variables.
Here is how the request looks in Restbird:
- Create the Response Check Scripts to save the access token and the refresh token to the global environment
Similar as the Get access token API, after the refresh token API has been called, the two global variables, box_access_token
and box_referesh_token
need to be updated accordingly.
package api
import "callapi"
import "net/http"
import "io/ioutil"
import"encoding/json"
import "fmt"
type MyDATA struct {
Access_token string `json:"access_token,omitempty"`
Refresh_token string `json:"refresh_token,omitempty"`
}
func (c CallBack) ResponseValidate(resp *http.Response, ctx *callapi.RestBirdCtx) bool {
var body []byte
var err error
var data MyDATA = MyDATA{}
if body, err = ioutil.ReadAll(resp.Body); err != nil {
fmt.Println("read body failed.")
return false
}
if err = json.Unmarshal(body, &data); err != nil {
fmt.Println("conver body to json failed")
return false
}
callapi.SetGlobalString("box_access_token", data.Access_token)
callapi.SetGlobalString("box_refresh_token", data.Refresh_token)
_, box_access_token := callapi.GetGlobalString("box_access_token")
_, box_refresh_token := callapi.GetGlobalString("box_refresh_token")
fmt.Println("box_access_token: " + box_access_token)
fmt.Println("box_refresh_token: " + box_refresh_token)
return true
}
Create Task to automate token retrival
Up to now, we have already done creating all the pieces, now there need a glue to hold everything together. Here comes task to complete the job, task is pure script that can do everything. In this example, we use task to implement a timer, inside the timer function, we call the refresh token API to periodically update the two gloabl variables box_access_token and box_referesh_token, so that we can have valid token as long as the Restbird server is up.
package main
import "callapi"
import "fmt"
import "time"
import api2 "restbird/Box/Token/api2"
func main() {
for {
select {
case <-time.Tick(time.Millisecond * 60000 * 60 ):
mytime := time.Now()
callapi.SetGlobalVars("starttime", mytime)
callapi.DoHttpRequestWithEnv("Box/Token", "api2", api2.CallBack{}, "Box")
callapi.GetGlobalVars("starttime", &mytime)
fmt.Println("starttime: ", mytime, "current Time:", time.Now())
_, box_access_token:= callapi.GetGlobalString("box_access_token")
fmt.Println("++box_access_token: " + box_access_token, "\n")
_, box_refresh_token:= callapi.GetGlobalString("box_refresh_token")
fmt.Println("--box_refresh_token: " + box_refresh_token, "\n\n")
}
}
}
- import api2 “
restbird/Box/Token/api2
,” the syntax of reference REST API is therestbird/[path to rest case]/[rest case]/api[index]
, where the index starts from 0.callapi.DoHttpRequestWithEnv(“Box/Token”, “api2”, api2.CallBack{}, “Box”)
, if local environment has been used in the API,DoHttpRequestWithEnv
needs to be called with the name of the environment passed as the last argument.
Until now, we have introduced a way to automatically retrieve the access token for testing OAuth 2.0 service where the user only needs to log in once to get the authorization code, after that, the Restbird test server will keep refreshing the token in the given time interval to maintain a valid token.
DevOps Integration
Now, we’re going to step forward to discuss how to integrate Restbird test cases into a Continuous Integration (CI) system. And, we will show you how Restbird can take an important part in DevOps.
Let’s continue with the Box test project since we already have a valid access token in hand and a task to refresh it periodically. We can now freely play with the APIs.
In the following example, we’ll create a simple test case in Restbird to implement the logic of “Get the ID of the specific folder name under root directory; if the folder doesn’t exist, then create a new one.” The test case will be marked as a success if the logic completed without error — otherwise, the case failed.
We will then integrate this test case into Jenkins, the most popular CI tool being used by DevOps teams.
Create Test Case for API Endpoints
Two BOX File-related APIs will be used in this example. We will create a new REST case in the Box project and include these two APIs.
Create an API to Retrieve the ID of the Specific Folder
Here is the API definition of Box to get the information of a folder.
Method | GET |
Url | https://api.box.com/2.0/folders/{folder_id} |
The root folder of a Box account is always represented by the ID “0."
This is how the request looks in Restbird:
Then, we will need to implement the logic to retrieve the folder id of the folder name and save the folder_id
in the environment variable in Response Check Scripts. Here, we use the Go language again as an example:
package api
import "callapi"
import "net/http"
import "io/ioutil"
import"encoding/json"
import "fmt"
type MyEntry struct {
Id string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Type string `json:"type,omitempty"`
}
type MyCollection struct{
Entries []MyEntry`json:"entries,omitempty"`
}
type MyDATA struct {
Item_collection MyCollection `json:"item_collection,omitempty"`
}
func (c CallBack) ResponseValidate(resp *http.Response, ctx *callapi.RestBirdCtx) bool {
var body []byte
var err error
var data MyDATA
if resp.StatusCode == 200 {
if body, err = ioutil.ReadAll(resp.Body); err != nil {
fmt.Println("read body failed.")
return false
}
if err = json.Unmarshal(body, &data); err != nil {
fmt.Println("conver body to json failed")
return false
}
for i, v := range data.Item_collection.Entries {
if v.Name == ctx.GetVars("folder_name") {
ctx.SetVars("folder_id", v.Id)
fmt.Println("Found folder: ")
fmt.Println("i, folder_id, folder_name: " , i, ",", ctx.GetVars("folder_id"), ",", ctx.GetVars("folder_name"))
return true
}
}
fmt.Println("Can't find folder: ", ctx.GetVars("folder_name"))
return true
}
return false
}
Create an API to Create a Specific Folder
Here is the API definition of a Box to create a folder.
Method | POST |
Url | https://api.box.com/2.0/folders |
Req Body (raw) |
{“name”:“{{folder_name}}”, “parent”: {“id”: “0”}} |
Here is how the request looks in Restbird:
In Response Check Scripts, we’ll check the response code and retrieve the folder_id
of the new folder and save it into an environment variable.
package api
import "callapi"
import "net/http"
import "fmt"
import "io/ioutil"
import"encoding/json"
type MyDATA struct {
Id string `json:"id, omitempty"`
}
func (c CallBack) ResponseValidate(resp *http.Response, ctx *callapi.RestBirdCtx) bool {
if resp.StatusCode == 201 {
var body []byte
var err error
var data MyDATA
if body, err = ioutil.ReadAll(resp.Body); err != nil {
fmt.Println("read body failed.")
return false
}
if err = json.Unmarshal(body, &data); err != nil {
fmt.Println("conver body to json failed:", err.Error())
return false
}
ctx.SetVars("folder_id", data.Id)
fmt.Println("folder_id", ctx.GetVars("folder_id"))
return true
}
return false
}
All REST APIs need to add an OAuth authorization header with access token authorization:
Bearer {{box_access_token}}
Create Test Script to Implement Logic
Beside HTTPS requests, Restbird also supports creating a pure script in the REST project to organize multiple single APIs into complex test cases. Here, we’re going to create a script for our first test case:
- Case 0: Get an ID of the
folder_name
. If the folder doesn’t exist, create a new one
In the script, we call the two APIs that we just created in the previous steps.
package api
import "callapi"
import "fmt"
import api0 "restbird/Box/File/api0"
import api1 "restbird/Box/File/api1"
type CallBack struct {}
func (c CallBack) GoScripts(ctx *callapi.RestBirdCtx) bool{
var folder_id = ""
ctx.SetVars("folder_name", "Demo6")
ctx.SetVars("folder_id", "")
reterr, retbool,retMsg := callapi.DoHttpRequest("Box/File", "api0", api0.CallBack{}, ctx)
fmt.Println(reterr, retbool,retMsg)
if !retbool {
fmt.Println("callapi failed", retMsg)
return false
}
folder_id = ctx.GetVars("folder_id")
if folder_id == ""{
fmt.Println("Folder doesn't exist, create a new one: ", ctx.GetVars("folder_name"))
reterr, retbool,retMsg := callapi.DoHttpRequest("Box/File", "api1", api1.CallBack{}, ctx)
fmt.Println(reterr, retbool,retMsg)
if(!retbool){
fmt.Println("callapi failed", retMsg)
return false
}
folder_id = ctx.GetVars("folder_id")
}
fmt.Println(folder_id)
return true
}
We can always run the API or script directly in Restbird to check the logic — after you click “Run Test,” we can check using “console log” to verify that everything runs properly.
Integrate With Jenkins
Now, we have everything ready on the Restbird-side; the final step is to integrate Restbird into Jenkins to complete our CI system.
Well, this is truly a breeze with the HTTP Request plugin — Jenkins is able to call the Restbird API.
Install the HTTP Request Plugin in Jenkins
Manage Jenkins —> Manage Plugin —> Search “Http Request,” then install
Install the Pipeline Utility Steps Plugin in Jenkins
Manage Jenkins —> Manage Plugin —> Search “Pipeline Utility Steps,” then install
This plugin provides the library for the JSON parser
Add a New Pipeline Task
Here, we create a typical CI workflow, including three steps: build, deploy, and run autotest. Restbird is used in this autotest step. This code can be used to define the pipeline script:
node {
def payload
stage('Build') {
//...
}
stage('Deploy') {
//..
}
stage('Run test') {
def host = 'http://192.168.1.178:10000'
def basicAuth = 'Basic YWRtaW46YWRtaW4='
println('Call Restbird API to run test:')
def reqBody = '{"casepath":"Box/TestScripts","apis":["api0"]}'
def response = httpRequest httpMode:'POST', customHeaders: [[name: 'Authorization', value: basicAuth]], requestBody: reqBody, url:host+"/v1/rest/run"
println('Status: '+response.status)
println('Response: '+response.content)
payload = readJSON text: response.content
def historyId = payload.his.id
println('History_id: '+ historyId)
def historyReqBody = '{"project":"Box/TestScripts","id":"' + payload.his.id + '", "immediatereturn": true}'
for(int i=0;i<10;i++){
println('Call Restbird API to get result: ' + i)
def historyResponse = httpRequest httpMode:'POST', customHeaders: [[name: 'Authorization', value: basicAuth]], requestBody: historyReqBody, url:host+"/v1/rest/runresult"
println('Status: '+historyResponse.status)
println('Response: '+historyResponse.content)
if(historyResponse.status == 200){
printConsoleLog(host, basicAuth, historyId)
payload = readJSON text: historyResponse.content
if(payload.code == 0){
if(payload.his.responseval.result == true){
currentBuild.result = 'SUCCESS'
}else{
currentBuild.result = 'FAILURE'
}
return
}else if(payload.code == -1){
println("Test unfinshed, check back in 10 seconds")
sleep 10
}else{
println("Test error" + payload.code + ", " + payload.info)
currentBuild.result = 'FAILURE'
return
}
}else{
currentBuild.result = 'FAILURE'
return
}
}
//return timeout
currentBuild.result = 'FAILURE'
return
}
}
def printConsoleLog(host, basicAuth, historyId){
println('--Call Restbird API to get console log:')
def consoleResponse = httpRequest httpMode:'GET', customHeaders: [[name: 'Authorization', value: basicAuth]], url:host+"/v1/rest/his/console?project=Box/TestScripts&id=" + historyId
println('--Console Status: '+consoleResponse.status)
println('--Console Response: '+consoleResponse.content)
}
Hope you enjoyed this tutorial! You can download the example project from GitHub
Opinions expressed by DZone contributors are their own.
Comments