hurl/docs/tutorial/captures.md

281 lines
9.7 KiB
Markdown
Raw Normal View History

# Captures
We have seen how to chain requests in a Hurl file. In some use cases, you want
2023-07-21 14:28:26 +03:00
to use data from one request and inject it in another one. That's what [captures]
are all about.
## Capturing a CSRF Token
2023-07-21 14:28:26 +03:00
In our website, a user can login at <http://localhost:3000/login>.
The HTML page is a [form] where the user can input:
2023-07-21 14:28:26 +03:00
- a required username
- a required password
If we look at the page HTML content, we can see an HTML form:
```html
2023-07-21 14:28:26 +03:00
<form class="login-form" method="post" action="/login">
<input type="hidden" name="_csrf" value="0fSk7gRA-UTkS25Fbsyal0dgLPBjVy1YIoNg">
...
2023-07-21 14:28:26 +03:00
<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>
```
2023-07-21 14:28:26 +03:00
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!
2023-07-21 14:28:26 +03:00
Form values can be sent using a [Form parameters section], with each key followed by its corresponding value.
2023-07-21 14:28:26 +03:00
1. Create a new file named `login.hurl`:
```hurl
2023-07-21 14:28:26 +03:00
POST http://localhost:3000/login
[FormParams]
2023-07-21 14:28:26 +03:00
username: fab
password: 12345678
2022-12-19 23:30:08 +03:00
HTTP 302
```
2022-09-25 13:52:24 +03:00
> When sending form data with a Form parameters section, you don't need to set the
2022-09-28 11:24:24 +03:00
> `Content-Type` HTTP header: Hurl infers that the content type of the request is `application/x-www-form-urlencoded`.
2023-07-21 14:28:26 +03:00
2. Run `login.hurl`:
```shell
2023-07-21 14:28:26 +03:00
login.hurl: Running [1/1]
error: Assert status code
2023-07-21 14:28:26 +03:00
--> login.hurl:5:6
|
2023-07-21 14:28:26 +03:00
 5 | HTTP 302
2022-12-19 23:30:08 +03:00
| ^^^ actual value is <403>
|
2023-07-21 14:28:26 +03:00
login.hurl: Failure (1 request(s) in 9 ms)
--------------------------------------------------------------------------------
2022-08-23 19:36:47 +03:00
Executed files: 1
Succeeded files: 0 (0.0%)
Failed files: 1 (100.0%)
2023-07-21 14:28:26 +03:00
Duration: 10 ms
```
2023-07-21 14:28:26 +03:00
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:
```html
2023-07-21 14:28:26 +03:00
<form class="login-form" method="post" action="/login">
<input type="hidden" name="_csrf" value="0fSk7gRA-UTkS25Fbsyal0dgLPBjVy1YIoNg">
...
</form>
```
2023-07-21 14:28:26 +03:00
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.
2023-07-21 14:28:26 +03:00
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:
2023-07-21 14:28:26 +03:00
- 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
2023-07-21 14:28:26 +03:00
1. Modify `login.hurl`:
```hurl
2023-07-21 14:28:26 +03:00
# First, display the login page to capture
# the CSRF token (see https://en.wikipedia.org/wiki/Cross-site_request_forgery)
2023-07-21 14:28:26 +03:00
GET http://localhost:3000/login
2022-12-19 23:30:08 +03:00
HTTP 200
[Captures]
csrf_token: xpath "string(//input[@name='_csrf']/@value)"
```
2023-07-21 14:28:26 +03:00
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.
2023-07-21 14:28:26 +03:00
2. Add a POST request using `csrf_token` variable in `login.hurl`:
```hurl
2023-07-21 14:28:26 +03:00
# 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
2022-12-19 23:30:08 +03:00
HTTP 200
[Captures]
csrf_token: xpath "string(//input[@name='_csrf']/@value)"
2022-12-19 23:30:08 +03:00
2023-07-21 14:28:26 +03:00
# Log in user, using the captured CSRF token:
POST http://localhost:3000/login
[FormParams]
2023-07-21 14:28:26 +03:00
username: fab
password: 12345678
_csrf: {{csrf_token}}
2022-12-19 23:30:08 +03:00
HTTP 302
```
2023-07-21 14:28:26 +03:00
3. Run `login.hurl` and verify everything is ok:
```shell
2023-07-21 14:28:26 +03:00
$ hurl --test login.hurl
login.hurl: Running [1/1]
login.hurl: Success (2 request(s) in 14 ms)
--------------------------------------------------------------------------------
2022-08-23 19:36:47 +03:00
Executed files: 1
Succeeded files: 1 (100.0%)
Failed files: 0 (0.0%)
2023-07-21 14:28:26 +03:00
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.
2023-07-21 14:28:26 +03:00
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.
2023-07-21 14:28:26 +03:00
1. Add a new header assert to test the `Location` header:
```hurl
2023-07-21 14:28:26 +03:00
# First, display the login page to capture
# ...
2023-07-21 14:28:26 +03:00
# Log in user, using the captured CSRF token:
POST http://localhost:3000/login
[FormParams]
2023-07-21 14:28:26 +03:00
username: fab
password: 12345678
_csrf: {{csrf_token}}
2022-12-19 23:30:08 +03:00
HTTP 302
[Asserts]
2023-07-21 14:28:26 +03:00
header "Location" == "/my-movies"
```
2023-07-21 14:28:26 +03:00
2. Add a request to get the favorites page that the user has been redirected to:
```hurl
2023-07-21 14:28:26 +03:00
# First, display the login page to capture
# ...
2023-07-21 14:28:26 +03:00
# Log in user, using the captured CSRF token:
# ...
2023-07-21 14:28:26 +03:00
# Follow redirection and open favorites:
GET http://localhost:3000/my-movies
2022-12-19 23:30:08 +03:00
HTTP 200
2023-07-21 14:28:26 +03:00
[Asserts]
xpath "string(//title)" == "My Movies"
```
2023-07-21 14:28:26 +03:00
3. Run `login.hurl` and verify everything is ok:
```shell
2023-07-21 14:28:26 +03:00
$ hurl --test login.hurl
login.hurl: Running [1/1]
login.hurl: Success (3 request(s) in 17 ms)
--------------------------------------------------------------------------------
2022-08-23 19:36:47 +03:00
Executed files: 1
Succeeded files: 1 (100.0%)
Failed files: 0 (0.0%)
2023-07-21 14:28:26 +03:00
Duration: 19 ms
```
2022-12-19 23:30:08 +03:00
> You can force Hurl to follow redirection by using [`-L / --location` option] or using an [`[Options]` section][options].
> In this case, asserts and captures will be run against the last redirection step.
2023-07-21 14:28:26 +03:00
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
2023-07-21 14:28:26 +03:00
So, our test file `login.hurl` is now:
```hurl
2023-07-21 14:28:26 +03:00
# First, display the login page to capture
# the CSRF token (see https://en.wikipedia.org/wiki/Cross-site_request_forgery)
2023-07-21 14:28:26 +03:00
GET http://localhost:3000/login
2022-12-19 23:30:08 +03:00
HTTP 200
[Captures]
csrf_token: xpath "string(//input[@name='_csrf']/@value)"
2022-12-19 23:30:08 +03:00
2023-07-21 14:28:26 +03:00
# Log in user, using the captured CSRF token:
POST http://localhost:3000/login
[FormParams]
2023-07-21 14:28:26 +03:00
username: fab
password: 12345678
_csrf: {{csrf_token}}
2022-12-19 23:30:08 +03:00
HTTP 302
2022-12-19 23:30:08 +03:00
2023-07-21 14:28:26 +03:00
# Follow redirection and open favorites:
GET http://localhost:3000/my-movies
2022-12-19 23:30:08 +03:00
HTTP 200
2023-07-21 14:28:26 +03:00
[Asserts]
xpath "string(//title)" == "My Movies"
```
We have seen how to [capture response data] in a variable and use it in others request.
2022-09-28 11:24:24 +03:00
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.
[form]: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/form
[_Post / Redirect / Get pattern_]: https://en.wikipedia.org/wiki/Post/Redirect/Get
[Form parameters section]: /docs/request.md#form-parameters
[CSRF token]: https://en.wikipedia.org/wiki/Cross-site_request_forgery
[`403 Forbidden`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/403
[`302 Found`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/302
[Adding asserts tutorial part]: /docs/tutorial/adding-asserts.md#structure-of-an-assert
[XPath capture]: /docs/capturing-response.md#xpath-capture
[JSONPath capture]: /docs/capturing-response.md#jsonpath-capture
[capture cookie value]: /docs/capturing-response.md#cookie-capture
[curl]: https://curl.se
[header capture]: /docs/capturing-response.md#header-capture
[`Location`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Location
2022-09-02 15:45:54 +03:00
[`-L / --location` option]: /docs/manual.md#location
2023-07-21 14:28:26 +03:00
[capture response data]: /docs/capturing-response.md
[options]: /docs/request.md#options
[captures]: /docs/capturing-response.md
[header assert]: /docs/asserting-response.md#header-assert
[the GitHub repo]: https://github.com/jcamiel/hurl-express-tutorial/tree/main/integration