How to use a single OAuth2.0 token for Multiple Virtual Users in a Gatling load test
Asked Answered
R

3

5

I need to load test an API that requires an OAuth2.0 token via Gatling (of which I'm a complete novice!) but would like each virtual user to use the same token. I'm retrieving the token ok (I think) and putting it in a variable called 'access' but I keep getting 'no attribute named 'access' is defined' when the test itself starts.

My token retrieval looks like the following(along with httpConf, used below):

 class MySimulation extends Simulation {

 val httpConf = http        
    .baseUrl("https://MyBaseUrl.Com/")
    .acceptHeader("application/json") 
    .doNotTrackHeader("1")
    .acceptLanguageHeader("en-UK,en;q=0.5")
    .acceptEncodingHeader("gzip, deflate")
    .userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:16.0) Gecko/20100101 Firefox/16.0")
    .shareConnections

 val header = Map("Content-Type" -> """application/x-www-form-urlencoded""")

 al auth = scenario("Retrieve Token")
 .exec(http("POST OAuth Req")
 .post("https://SomeTokenUrl")
 .formParam("resource", "someresource")
 .formParam("grant_type", "somegranttype")
 .formParam("client_secret", "someclientsecret")
 .formParam("client_id", "someclientid")
 .headers(header).check(status.is(200)).check(jsonPath("$.access_token").find.saveAs("access"))) 

I then tried setting up the load test as (Note: I did initially put 'Map', rather than the mutable variant, but read somewhere the default was immutable, and wondered if this was why the header couldn't update):

 val headers_10 = scala.collection.mutable.Map("Content-Type" -> "application/json; charset=ISO-8859-1", "Authorization" -> "Bearer ${access}")

 val scn = scenario("MyService Gatling test run")       
           .exec(http("")               
           .post("Myservice/api")
           .headers(headers_10.toMap)                
           .body(StringBody("""{"SomeProperty": "Some Value"}"""))
           .asJson
           .check(status.is(200)))

 setUp(
    auth.inject(constantUsersPerSec(1) during (2 seconds)),
    scn.inject(nothingFor(2 seconds),
    constantUsersPerSec(10) during (10 seconds) 
    ).protocols(httpConf))
    .assertions(global.responseTime.max.lt(500)) 
    .assertions(forAll.failedRequests.percent.lte(1)) 
    .assertions(global.responseTime.mean.lte(100)) 

The idea was that the token retrieval would complete prior to the load test kicking in and the 'access' variable would then be used by the load test scenario, but it gives the following result:

  ERROR : Failed to build request: No attribute named 'access' is defined 

I've reached the end of my tether with it. I'm guessing it might be something to do with scopes, and perhaps the variable doesn't carry over to the load test scenario, but I've seen examples elsewhere that seem to recommend exactly that set up, so I don't know whether these other examples are partially complete or what.

Rode answered 4/4, 2019 at 17:24 Comment(0)
B
3

Sessions are per user and no session data is shared between users. So while you have 1 user running your 'auth' scenario and saving the token, it is two different users that run 'scn' and they don't have access to the session values of the auth user.

It's not recommended practice, but you can solve this by pushing the auth token into a regular scala var and the setting this in the auth scenario and reading it in the main scenario - you just need to be sure that auth always completes before you inject any other users.

var token: String = ""

then in the auth scenario, have a step at the end such as

.exec(session => {
    token = session("access").as[String]
    session
})

then at the start of the scn scenario have a step to set the session variable

.exec(session.set("access", token))

I've used this pattern in the past and it works, but I'm sure there are nicer ways to do it

Basidiospore answered 4/4, 2019 at 23:30 Comment(2)
Thanks for your answer James, but I still can't quite get it to work. My problem is the 'token = session.get("access").as[String]' bit. It doesn't recognise 'get'. I then tried '.exec(session => { token = session("access")})', which understandably gave a type mismatch as token was string, and it was returning a SessionAttribute type. I then readded the '.as[String]' to the end again, and I now get a different type mismatch - it's now saying that it found the 'unit' type, but was expecting 'Validation' ?!?!Rode
Hi James - found the problem. 'get' is no longer part of the session API in later versions of Gatling, which is the version that I am using (plus, I think it needed to be curly brackets rather than normal ones around the session!). If you want to edit your answer to mention that you might need to have .exec{session => { token = session("access").as[String] session}} if you're on a later version of Gatling, I'll mark it as the correct answer. Thanks for your help!Rode
P
4

Today I implemented this scenario for my project. Please see the code below and it will work for you as well.

Note: I have written this code with my API's required params. You can modify this code as per your requirement. For now, this code is written in a single class. I have implemented this code in a proper format as well with the use of different classes and property files. I will post that one as well.

For a single class, the code goes as follows:

`

package aapi

    import io.gatling.core.Predef._
    import io.gatling.http.Predef._

     class manifestSimulation extends Simulation {    

    private var token = ""

    object authAvi{
     // This is the request(API) which we are going to use for generating the auth token 1 time and then will feed this token into subsequent request.
     var postBody = "{\"username\":\"devusername\”,\”password\”:\”devpassword”}”

    val auth = scenario("Retrieve our auth Token which will be used in the subsequent request“)
        .exec(
            http("POST OAuth Req")
            .post(“User Post URL“)
            .body(StringBody(postBody))
            .header("ClientId", “test”)
    .header("DSN", “devDB”)
    .header("accept", "application/json")
    .header("Accept-Language", "en-us")
    .header("Content-Type", "application/json")
            .check(status.is(200))
    .check(jsonPath("$.token")
    .saveAs("token")))
            .exitHereIfFailed
            .exec{session => { token = session("token").as[String]
                             session}}       
    }
    object manifest {
     // This is the request(API) which we are going to hit multiple times using the token which we generated from the previous auth API

        var manifestHeaders = Map("ClientId" -> “test”, "DSN" -> "devDB", "Token" -> "${token}")

        val manifestMethod = exec(session => session.set("token", token))            
            .exec(http("Manifest Details")              
                .get(“Your get URL“)
                .headers(manifestHeaders)
                .check(status.is(200))
                 )
    }   

    val scn = scenario(“**********This is your actual load test*******************”)
        .exec(manifest.manifestMethod)
       setUp(
      authAvi.auth.inject(constantUsersPerSec(1) during (1 seconds)), // fire 1 requests per second for 1 second to retrieve token
    scn.inject(nothingFor(4 seconds), // waits 4 seconds as a margin to process token and this time varies for every user
        constantUsersPerSec(5) during (5 seconds))) // fire 5 requests per second for 5 seconds which will result in 25 (5*5) requests and overall 26 requests when the report gets generated (because we have 1 request for auth token and 25 requests of our intended API (25+1 = 26)

     }`
Pabulum answered 30/7, 2019 at 12:55 Comment(0)
B
3

Sessions are per user and no session data is shared between users. So while you have 1 user running your 'auth' scenario and saving the token, it is two different users that run 'scn' and they don't have access to the session values of the auth user.

It's not recommended practice, but you can solve this by pushing the auth token into a regular scala var and the setting this in the auth scenario and reading it in the main scenario - you just need to be sure that auth always completes before you inject any other users.

var token: String = ""

then in the auth scenario, have a step at the end such as

.exec(session => {
    token = session("access").as[String]
    session
})

then at the start of the scn scenario have a step to set the session variable

.exec(session.set("access", token))

I've used this pattern in the past and it works, but I'm sure there are nicer ways to do it

Basidiospore answered 4/4, 2019 at 23:30 Comment(2)
Thanks for your answer James, but I still can't quite get it to work. My problem is the 'token = session.get("access").as[String]' bit. It doesn't recognise 'get'. I then tried '.exec(session => { token = session("access")})', which understandably gave a type mismatch as token was string, and it was returning a SessionAttribute type. I then readded the '.as[String]' to the end again, and I now get a different type mismatch - it's now saying that it found the 'unit' type, but was expecting 'Validation' ?!?!Rode
Hi James - found the problem. 'get' is no longer part of the session API in later versions of Gatling, which is the version that I am using (plus, I think it needed to be curly brackets rather than normal ones around the session!). If you want to edit your answer to mention that you might need to have .exec{session => { token = session("access").as[String] session}} if you're on a later version of Gatling, I'll mark it as the correct answer. Thanks for your help!Rode
R
1

@Tarun,

When I did it, I had the 'exec' in my scenario, rather than the set up, and used the following syntax:

 val dataToUse = feed(testData)
 .exec(session => session.set("access", token))            
 .exec(http("")             
 .post("*the_URL_to_send_to)*")
 .headers(headers_10.toMap)
 .body(RawFileBody("${filePath}")).asJson
 .check(status.is(200))
             )

As mentioned in the comments in the previous discussion, this was because I was using a later version of gatling and the 'get' method was no longer part of the session api.

My full solution was as follows - note that are a number of things to look out for that might not apply to your solution. I used an object, as it just made things clearer in my mind for what I was trying to do! Also, some of the imports are probably redundant, as I included them as part of scattergun approach to finding something that worked!

Finally, I basically list the contents of a directory, and cycle through the files listed in it, using each one as a feeder. You look as if you're using a literal template of json, so probably don't need that, but I thought I would include it for completeness, as it's quite handy - if you change the format of your json, you don't need to mess around changing the template in the simulation, you just clear the directory and drop examples of the new format in there and away you go! :

 package myTest

 import io.gatling.core.Predef._
 import io.gatling.http.Predef._
 import scala.concurrent.duration._
 import scala.collection.JavaConversions._
 import java.io.File
 import java.io.FileNotFoundException


 class myTestSimulation extends Simulation {    


 val httpConf = http
    .baseUrl("*your_base_URL*")
.acceptHeader("application/json") // Here are the common headers
.doNotTrackHeader("1")
.acceptLanguageHeader("en-UK,en;q=0.5")
.acceptEncodingHeader("gzip, deflate")
.userAgentHeader("Mozilla/5.0 (Macintosh; Intel Mac OS X 10.8; rv:16.0) Gecko/20100101 Firefox/16.0")
.shareConnections

val header = Map("Content-Type" -> """application/x-www-form-urlencoded""");
private var token = ""

val auth = scenario("Retrieve Token")
    .exec(
        http("POST OAuth Req")
        .post("*URL_for_Token*")
        .formParam("resource", "*your_resource_value*")
        .formParam("grant_type", "*your_grant_type*")
        .formParam("client_secret", "*your_client_secret_value*")
        .formParam("client_id", "*your_client_id_value*")
        .headers(header)
        .check(status.is(200)).check(jsonPath("$.access_token").find.saveAs("access")))  
        .exec{session => { token = session("access").as[String]
                         session}}       

object myTestObject {

    var headers_10 = scala.collection.mutable.Map("Content-Type" -> "application/json; charset=ISO-8859-1", "Authorization" -> "Bearer ${access}")     
    val testData = Iterator.continually(
    new File("*pathway_to_file*") match {
      case d if d.isDirectory => d.listFiles.map(f => Map("filePath" -> f.getPath))
      case _ => throw new FileNotFoundException("Samples path must point to directory")
    }).flatten

    val myTestObjectMethod = feed(testData)
        .exec(session => session.set("access", token))            
        .exec(http("")              
            .post("*the_URL_to_send_to(don't_forget_that_base_URL_above_is_automatically_stuck_to_the_front_of_this!)*")
            .headers(headers_10.toMap)
            .body(RawFileBody("${filePath}")).asJson
            .check(status.is(200))
             )
}   

val scn = scenario("my_actual_load_test")
    .exec(myTestSimulation.myTestObject)
   setUp(
  auth.inject(constantUsersPerSec(1) during (1 seconds)), // fire 1 requests per second for 1 second to retrieve token
scn.inject(nothingFor(2 seconds), // waits 2 seconds as a margin to process token
    constantUsersPerSec(50) during (300 seconds) // fire 50 requests per second for 300 seconds
   ).protocols(httpConf))                             
   .assertions(global.responseTime.max.lt(500)) // set max acceptable response time
   .assertions(forAll.failedRequests.percent.lte(1)) // less than 1% of tests should fail
   .assertions(global.responseTime.mean.lte(100)) // set average response time
 }

I mean, I've probably made a typo somewhere along the line as I removed the sensitive stuff from it, but hopefully that will do what you need.

Rode answered 12/6, 2019 at 10:11 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.