WesternGun
WesternGun

Reputation: 12748

scala - process string parsed from gatling EL expression in place with anonymous function

I am using Gatling EL expression in a http test, and it seems that wrapping the EL expression of any kind will invalidate the parsing, i.e.,

This works:

def run(env: Parameters): ChainBuilder =
  feed(env.feeder)
  .exec(
    http(Seq(env.env, "verify").mkString(":"))
      .get(env.base + "/verify")
      .queryParam("email", "${username}")
      .check(
        status.is(200),
        jsonPath("$..exist").is("true"),
      )
  )
  exitHereIfFailed

But this does not work:

def run(env: Parameters): ChainBuilder =
  feed(env.feeder).exec(
    http(Seq(env.base, "authorize").mkString(":"))
      .post(env.base + "/authorize")
      .asJSON // "Accept" and "ContentType" set to JSON
      .header(HttpHeaderNames.AcceptCharset, "UTF-8")
      .header(HttpHeaderNames.Authorization,
        "AppBasic " + Base64.getEncoder().encodeToString(("${username}" + ":" + "password").getBytes())))
      .body(StringBody(env.authorizeBody))
      .check(
        status.is(200),
        header(HttpHeaderNames.ContentType).is(HttpHeaderValues.ApplicationJson),
        header(HttpHeaderNames.AcceptCharset).is("UTF-8"),
        jsonPath("$..id_token") exists
    )
  )

I want to encode with Base64 "email:password" and use it as Authorization header, with "AppBasic" as prefix. So it is not Basic authorization, which has Basic as prefix.

Now I do something like:

...
.header(HttpHeaderNames.Authorization,
      "AppBasic " + "${username}".map(username => Base64.getEncoder().encodeToString((username + ":" + "password").getBytes())))
...

But, the log shows a strange Vector. I want a String.

HTTP request:
POST <some url>
headers=
Accept: application/json
Content-Type: application/json
Accept-Charset: UTF-8
Authorization: AppBasic Vector(JDoxMTEx, ezoxMTEx, dToxMTEx, czoxMTEx, ZToxMTEx, cjoxMTEx, bjoxMTEx, YToxMTEx, bToxMTEx, ZToxMTEx, fToxMTEx)
User-Agent: curl/7.54.0
Content-Length: 143

So, it is a in-place string conversion, not a collection mapping. What can I do?

Upvotes: 2

Views: 2264

Answers (2)

WesternGun
WesternGun

Reputation: 12748

Found answer from another question:

Using a feeder to pass in header values (Gatling)

The solution is to write a function here, in my case:

.header(HttpHeaderNames.Authorization, session =>
    for {
        username <- session("username").validate[String]
    } yield "AppBasic " + Base64.getEncoder.encodeToString((username + ":" + "1111").getBytes())
)

Thanks for that.

Before found this I have done a workaround: the team leader replace the 10k lines test csv with a 20 lines one, because Gatling will load beforehand the content into memory in testing, and have a big csv will consume memory, as per documentation.

So with 20 lines I can hardcode the encoded email:password in the csv file and read them with column name "${col_name}".

But it is best to read from session. I didn't know that. Thanks @tilois.

Upvotes: 0

tilois
tilois

Reputation: 682

This is more of a Gatling problem, than a Scala problem, but let's try to find out what's going on here.

Scala String Interpolation

To include values into a string one can use String Interpolation using the ${…} syntax. Example:

s"Hello, $name" where name = "World" would result in "Hello World. However, that is not what you are using.

Now the easiest thing to do would be to actually use this instead. However it would require the value of username to be available in the scala program.

If I see it correctly, you want the value to come from the Gatling Session and thus are using the Gatling Expression Language, as you already mentioned.

If that is not the case, the easiest thing to do would be to use Scala String interpolation:

def asBase64(input: String): String = java.util.Base64.getEncoder().encodeToString(input.getBytes())
val username = "johnDoe"
val password = "1111" // Some more or less random password
val headerValue = asBase64(s"${username}:$password")
val header = s"Basic $headerValue" // … should be "Basic am9obkRvZToxMTEx" here

What's happening here?

Gatling parses Strings parameter values and turn them into functions that will compute a result based on the data stored into the Session when they will be evaluated.

(taken from the documentation with the highlight from me) This requires that the string is passed to Gatling. However you are using plain Scala here to transform the String. Gatling is an embedded DSL, meaning it is embedded in the host language (Scala) and this just bit you.

You are doing this

"${username}".map(username =>
    Base64.getEncoder().encodeToString((username + ":" + "password").getBytes()

Let's go through this step by step: ${username} is not String interpolation like above, because it is missing the "string interpolation methods" at the beginning (e.g. the s in s"") which is correct, if you want to pass it literally for it to be parsed by Gatling. However you don't pass it into Gatling, instead you are map over it. This is plain Scala and Scala has no idea of something like a Session where values are within.

Instead String.map passes each character and uses the function on it. What you thought would be the username is actually a single letter.

So it goes on and uses username + ":" + "password" on each letter. By the way: Your original code didn't had "password" there… and if that is a real hidden password, it isn't hidden anymore (Base64 is reversable).

So this is where the

Vector(JDoxMTEx, ezoxMTEx, dToxMTEx, czoxMTEx, ZToxMTEx, cjoxMTEx, bjoxMTEx, YToxMTEx, bToxMTEx, ZToxMTEx, fToxMTEx)

comes from. It has 11 entries, one for each letter in: $, {, u, s, e, r, n, a, m, e, } combined with a bit more.

What can be done instead to use a password from the session

The easiest thing would be to use what Gatling already offers. These don't know about your AppBasic and will use Basic instead… so no option.

You could use the Session API to access the value (with session("name") accessing the value from the session).

.header(HttpHeaderNames.Authorization, s"AppBasic ${asBase64(s"${session("username").as[String]}:$password")}")

which assumes, that the function asBase64 is defined like above (otherwise it's a bit longer) and that there is a variable password in scope. If not just replace it (along with the dollar) with the value you want there.

So hopefully this helps you with your problem. I would say that you are not fully aware of what is Gatling specific and what is Scala (which is totally okay, but sometimes it will bite you, like here). I went to a bit of length in the hope that this separates it a bit.

Your basic problem was that you were used to access values from a session with the dollar notation. Which is fine as long as you are passing values to Gatling. In this case Gatling does this for you. However it won't work in plain Scala, there you have to access them using the Session API. You can see if you can access a value with the dollar notation, if the API talks about an Expression[…] which means that Gatling takes an extra turn to parse it (which is exactly why it knows that there is a reference in it)

Upvotes: 2

Related Questions