{{announcement.body}}
{{announcement.title}}

Automate Testing With OAuth 2.0: a Step-By-Step Tutorial

DZone 's Guide to

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.

· Security Zone ·
Free Resource

Nowadays, 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
  • response _ type: code
  • client _ id: {{client _ id}}
  • redirect _ uri: https://localhost
  • state: BOX

Here is how the request looks in Restbird:

Box Get authorization code

  • Click “Run test,” and then copy the URL into the web browser:

Run Box get authorization code API

  • Enter user credential and click “Authorize:”

Box User login

  • Click “Grant access to Box:”

Box user grant access

  • 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.

Box code url

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)
  • grant _ type: authorization _ code
  • client _ id: {{client _ id}}
  • rclient _ secret: {{client _ secret}}
  • code: Copy Code from last API

Here is how the request looks in Restbird:

Box Get access token

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.

Box response with token

Click "Globals” to verify that the variables have been saved into the global environment.

view global env

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)
  • grant _ type: refresh _ token
  • client _ id: {{client _ id}}
  • rclient _ secret: {{client _ secret}}
  • code: {{box _ refresh _ token}}

{{}} is the syntax for using both local and global environment variables.

Here is how the request looks in Restbird:

Box req referesh token

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_tokenneed 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 the  restbird/[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:

Box Get folder info

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:

Box create folder

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

TestScript

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.

TestScript-console

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

Topics:
oauth 2.0 ,jenkins ,devops ,rest api ,automated testing ,golang ,oauth ,continious integration ,integration ,security

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}