9.7 KiB
Captures
We have seen how to chain requests in a Hurl file. In some use cases, you want to use data from one request and inject it in another one. That's what captures are all about.
Capturing a CSRF Token
In our website, a user can login at http://localhost:3000/login. The HTML page is a form where the user can input:
- a required username
- a required password
If we look at the page HTML content, we can see an HTML form:
<form class="login-form" method="post" action="/login">
<input type="hidden" name="_csrf" value="0fSk7gRA-UTkS25Fbsyal0dgLPBjVy1YIoNg">
...
<input type="text" name="username" id="username" autocomplete="off" minlength="3" maxlength="32" pattern="[a-zA-Z0-9_-]{3,32}" title="Username must use a-z, A-Z and 0-9" required="">
...
<input type="password" name="password" id="password" autocomplete="off" minlength="6" maxlength="32" required="">
...
<input type="submit" value="Login">
...
</form>
When the user clicks on 'Login' button, a POST request is sent with form values: the username and a password. Our server implements a Post / Redirect / Get pattern: if the POST submission is successful, the user is redirected to his favorites movies page.
Let's try to test it!
Form values can be sent using a Form parameters section, with each key followed by its corresponding value.
- Create a new file named
login.hurl
:
POST http://localhost:3000/login
[FormParams]
username: fab
password: 12345678
HTTP 302
When sending form data with a Form parameters section, you don't need to set the
Content-Type
HTTP header: Hurl infers that the content type of the request isapplication/x-www-form-urlencoded
.
- Run
login.hurl
:
[1mlogin.hurl[0m: [1;36mRunning[0m [1/1]
[1;31merror[0m: [1mAssert status code[0m
[1;34m-->[0m login.hurl:5:6
[1;34m|[0m
[1;34m 5[0m [1;34m|[0m HTTP 302
[1;34m|[0m [1;31m^^^[0m [1;31mactual value is <403>[0m
[1;34m|[0m
[1mlogin.hurl[0m: [1;31mFailure[0m (1 request(s) in 9 ms)
--------------------------------------------------------------------------------
Executed files: 1
Succeeded files: 0 (0.0%)
Failed files: 1 (100.0%)
Duration: 10 ms
This is unexpected! Our test is failing, we're not redirected to the favorite movies page.
The reason is quite simple, let's look more precisely at our HTML form:
<form class="login-form" method="post" action="/login">
<input type="hidden" name="_csrf" value="0fSk7gRA-UTkS25Fbsyal0dgLPBjVy1YIoNg">
...
</form>
The server login page is protected by a CSRF token. In a browser, when the user wants to log in by sending a POST request, a token is sent along the username/password values. This token is generated server-side, and embedded in the HTML. When the POST request is made, our server expects that the request includes a valid token, and will reject the request if the token is missing or invalid.
In our Hurl file, we're not sending any token, so the server is rejecting our request with a 403 Forbidden
HTTP response.
Unfortunately, we can't hard code the value of a token in our [FormParams]
section because the token is dynamically
generated on each request, and a certain fixed value would be valid only during a small period of time.
We need to dynamically capture the value of the CSRF token and pass it to our form. To do so, we are going to:
- perform a first GET request to http://localhost:3000/login and capture the CSRF token
- chain with a POST request that contains our username/password value, and our captured CSRF token
- check that the POST response is a redirection, i.e. a
302 Found
to the favorites page
So, let's go!
How to capture values
- Modify
login.hurl
:
# First, display the login page to capture
# the CSRF token (see https://en.wikipedia.org/wiki/Cross-site_request_forgery)
GET http://localhost:3000/login
HTTP 200
[Captures]
csrf_token: xpath "string(//input[@name='_csrf']/@value)"
Captures are defined in a [Captures]
section. Captures are composed of a variable name and a query.
We have already seen queries in Adding asserts tutorial part. Since we want to capture value from an HTML
document, we can use a XPath capture.
Every query can be used in assert or in capture. You can capture value from JSON response with a JSONPath capture, or capture cookie value with the same queries that you use in asserts.
In this capture, csrf_token
is a variable and xpath "string(//input[@name='_csrf']/@value)"
is the
XPath query.
Now that we have captured the CSRF token value, we can inject it in the POST request.
- Add a POST request using
csrf_token
variable inlogin.hurl
:
# First, display the login page to capture
# the CSRF token (see https://en.wikipedia.org/wiki/Cross-site_request_forgery)
GET http://localhost:3000/login
HTTP 200
[Captures]
csrf_token: xpath "string(//input[@name='_csrf']/@value)"
# Log in user, using the captured CSRF token:
POST http://localhost:3000/login
[FormParams]
username: fab
password: 12345678
_csrf: {{csrf_token}}
HTTP 302
- Run
login.hurl
and verify everything is ok:
$ hurl --test login.hurl
[1mlogin.hurl[0m: [1;36mRunning[0m [1/1]
[1mlogin.hurl[0m: [1;32mSuccess[0m (2 request(s) in 14 ms)
--------------------------------------------------------------------------------
Executed files: 1
Succeeded files: 1 (100.0%)
Failed files: 0 (0.0%)
Duration: 16 ms
Follow Redirections
Like its HTTP engine curl, Hurl doesn't follow redirection by default: if a response has a 302 Found
status code, Hurl doesn't implicitly run requests until a 200 OK
is reached. This can be useful if you want
to validate each redirection step.
After having logged it, we would like to test the page where the user has been redirected.
This is really simple and can be achieved with a header assert: on the response to the POST creation request, we
are going to assert the Location
header, which indicates the redirection URL target.
- Add a new header assert to test the
Location
header:
# First, display the login page to capture
# ...
# Log in user, using the captured CSRF token:
POST http://localhost:3000/login
[FormParams]
username: fab
password: 12345678
_csrf: {{csrf_token}}
HTTP 302
[Asserts]
header "Location" == "/my-movies"
- Add a request to get the favorites page that the user has been redirected to:
# First, display the login page to capture
# ...
# Log in user, using the captured CSRF token:
# ...
# Follow redirection and open favorites:
GET http://localhost:3000/my-movies
HTTP 200
[Asserts]
xpath "string(//title)" == "My Movies"
- Run
login.hurl
and verify everything is ok:
$ hurl --test login.hurl
[1mlogin.hurl[0m: [1;36mRunning[0m [1/1]
[1mlogin.hurl[0m: [1;32mSuccess[0m (3 request(s) in 17 ms)
--------------------------------------------------------------------------------
Executed files: 1
Succeeded files: 1 (100.0%)
Failed files: 0 (0.0%)
Duration: 19 ms
You can force Hurl to follow redirection by using
-L / --location
option or using an[Options]
section. In this case, asserts and captures will be run against the last redirection step.
A login workflow is surprisingly hard to do well. You can try to add more test on our login.hurl
test. With Hurl, try
now to test the following usecase:
- when a user is not authenticated and goes to http://localhost:3000/my-movies, he is redirected to the login page,
- what's happen if the user try to log in with a wrong password,
- after a user log out, he can open the login page again.
You can see a more complete login.hurl
on the GitHub repo.
Recap
So, our test file login.hurl
is now:
# First, display the login page to capture
# the CSRF token (see https://en.wikipedia.org/wiki/Cross-site_request_forgery)
GET http://localhost:3000/login
HTTP 200
[Captures]
csrf_token: xpath "string(//input[@name='_csrf']/@value)"
# Log in user, using the captured CSRF token:
POST http://localhost:3000/login
[FormParams]
username: fab
password: 12345678
_csrf: {{csrf_token}}
HTTP 302
# Follow redirection and open favorites:
GET http://localhost:3000/my-movies
HTTP 200
[Asserts]
xpath "string(//title)" == "My Movies"
We have seen how to capture response data in a variable and use it in others request. Captures and asserts share the same queries, and can be inter-mixed in the same response. Finally, Hurl doesn't follow redirect by default, but captures can be used to run each step of a redirection.