Import of the canonical docs (raw Markdown, without ant Jekyll specificities.)
643
docs/asserting-response.md
Normal file
@ -0,0 +1,643 @@
|
||||
# Asserting Response
|
||||
|
||||
## Version - Status
|
||||
|
||||
Expected protocol version and status code of the HTTP response.
|
||||
|
||||
Protocol version is one of `HTTP/1.0`, `HTTP/1.1`, `HTTP/2` or
|
||||
`HTTP/*`; `HTTP/*` describes any version. Note that there are no status text following the status code.
|
||||
|
||||
```hurl
|
||||
GET https://example.org/404.html
|
||||
|
||||
HTTP/1.1 404
|
||||
```
|
||||
|
||||
Wildcard keywords (`HTTP/*`, `*`) can be used to disable tests on protocol version and status:
|
||||
|
||||
```hurl
|
||||
GET https://example.org/api/pets
|
||||
|
||||
HTTP/1.0 *
|
||||
# Check that response status code is > 400 and <= 500
|
||||
[Asserts]
|
||||
status > 400
|
||||
status <= 500
|
||||
```
|
||||
|
||||
|
||||
## Headers
|
||||
|
||||
Optional list of the expected HTTP response headers that must be in the received response.
|
||||
|
||||
A header consists of a name, followed by a `:` and a value.
|
||||
|
||||
For each expected header, the received response headers are checked. If the received header is not equal to the expected,
|
||||
or not present, an error is raised. Note that the expected headers list is not fully descriptive: headers present in the response
|
||||
and not in the expected list doesn't raise error.
|
||||
|
||||
```hurl
|
||||
# Check that user toto is redirected to home after login.
|
||||
POST https://example.org/login
|
||||
[FormParams]
|
||||
user: toto
|
||||
password: 12345678
|
||||
|
||||
HTTP/1.1 302
|
||||
Location: https://example.org/home
|
||||
```
|
||||
|
||||
> Quotes in the header value are part of the value itself.
|
||||
>
|
||||
> This is used by the [ETag](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag) Header
|
||||
> ```
|
||||
> ETag: W/"<etag_value>"
|
||||
> ETag: "<etag_value>"
|
||||
> ```
|
||||
|
||||
|
||||
Testing duplicated headers is also possible.
|
||||
|
||||
For example with the `Set-Cookie` header:
|
||||
|
||||
```
|
||||
Set-Cookie: theme=light
|
||||
Set-Cookie: sessionToken=abc123; Expires=Wed, 09 Jun 2021 10:18:14 GMT
|
||||
```
|
||||
|
||||
You can either test the two header values:
|
||||
|
||||
```hurl
|
||||
GET https://example.org/index.html
|
||||
Host: example.net
|
||||
|
||||
HTTP/1.0 200
|
||||
Set-Cookie: theme=light
|
||||
Set-Cookie: sessionToken=abc123; Expires=Wed, 09 Jun 2021 10:18:14 GMT
|
||||
```
|
||||
|
||||
Or only one:
|
||||
|
||||
```hurl
|
||||
GET https://example.org/index.html
|
||||
Host: example.net
|
||||
|
||||
HTTP/1.0 200
|
||||
Set-Cookie: theme=light
|
||||
```
|
||||
|
||||
If you want to test specifically the number of headers returned for a given header name, or
|
||||
if you want to test header value with [predicates] (like `startsWith`, `contains`, `exists`)
|
||||
you can use the explicit [header assert].
|
||||
|
||||
|
||||
## Asserts
|
||||
|
||||
Optional list of assertions on the HTTP response. Assertions can describe checks
|
||||
on status code, on the received body (or part of it) and on response headers.
|
||||
|
||||
Structure of an assert:
|
||||
|
||||
<div class="schema-container schema-container u-font-size-1 u-font-size-2-sm u-font-size-3-md">
|
||||
<div class="schema">
|
||||
<span class="schema-token schema-color-2">jsonpath "$.book"<span class="schema-label">query</span></span>
|
||||
<span class="schema-token schema-color-1">contains<span class="schema-label">predicate type</span></span>
|
||||
<span class="schema-token schema-color-3">"Dune"<span class="schema-label">predicate value</span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="schema-container schema-container u-font-size-1 u-font-size-2-sm u-font-size-3-md">
|
||||
<div class="schema">
|
||||
<span class="schema-token schema-color-2">body<span class="schema-label">query</span></span>
|
||||
<span class="schema-token schema-color-1">matches<span class="schema-label">predicate type</span></span>
|
||||
<span class="schema-token schema-color-3">/\d{4}-\d{2}-\d{2}/<span class="schema-label">predicate value</span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
An assert consists of a query followed by a predicate. The format of the query
|
||||
is shared with [captures], and can be one of :
|
||||
|
||||
- [`status`](#status-assert)
|
||||
- [`header`](#header-assert)
|
||||
- [`cookie`](#cookie-assert)
|
||||
- [`body`](#body-assert)
|
||||
- [`bytes`](#bytes-assert)
|
||||
- [`xpath`](#xpath-assert)
|
||||
- [`jsonpath`](#jsonpath-assert)
|
||||
- [`regex`](#regex-assert)
|
||||
- [`sha256`](#sha-256-assert)
|
||||
- [`md5`](#md5-assert)
|
||||
- [`variable`](#variable-assert)
|
||||
- [`duration`](#duration-assert)
|
||||
|
||||
Queries, as in captures, can be refined with subqueries. [`count`] subquery can be used
|
||||
with various predicates to add tests on collections sizes.
|
||||
|
||||
|
||||
### Predicates
|
||||
|
||||
Predicates consist of a predicate function, and a predicate value. Predicate functions are:
|
||||
|
||||
- `==` (`equals`): check equality of query and predicate value
|
||||
- `!=`: check that query and predicate value are different
|
||||
- `>` (`greaterThan`): check that query number is greater than predicate value
|
||||
- `>=` (`greaterThanOrEquals`): check that query number is greater than or equal to the predicate value
|
||||
- `<` (`lessThan`): check that query number is less than that predicate value
|
||||
- `<=` (`lessThanOrEquals`): check that query number is less than or equal to the predicate value
|
||||
- `startsWith`: check that query starts with the predicate value (query can return a string or a binary content)
|
||||
- `endsWith`: check that query ends with the predicate value (query can return a string or a binary content)
|
||||
- `contains`: check that query contains the predicate value (query can return a string or a binary content)
|
||||
- `includes`: check that query collections includes the predicate value
|
||||
- `matches`: check that query string matches the regex pattern described by the predicate value
|
||||
- `exists`: check that query returns a value
|
||||
- `isInteger`: check that query returns an integer
|
||||
- `isFloat`: check that query returns a float
|
||||
- `isBoolean`: check that query returns a boolean
|
||||
- `isString`: check that query returns a string
|
||||
- `isCollection`: check that query returns a collection
|
||||
|
||||
|
||||
Each predicate can be negated by prefixing it with `not` (for instance, `not contains` or `not exists`)
|
||||
|
||||
<div class="schema-container schema-container u-font-size-1 u-font-size-2-sm u-font-size-3-md">
|
||||
<div class="schema">
|
||||
<span class="schema-token schema-color-2">jsonpath "$.book"<span class="schema-label">query</span></span>
|
||||
<span class="schema-token schema-color-1">not contains<span class="schema-label">predicate type</span></span>
|
||||
<span class="schema-token schema-color-3">"Dune"<span class="schema-label">predicate value</span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
A predicate values is typed, and can be a string, a boolean, a number, a bytestream, `null` or a collection. Note that
|
||||
`"true"` is a string, whereas `true` is a boolean.
|
||||
|
||||
For instance, to test the presence of a h1 node in an HTML response, the following assert can be used:
|
||||
|
||||
```hurl
|
||||
GET https://example.org/home
|
||||
|
||||
HTTP/1.1 200
|
||||
[Asserts]
|
||||
xpath "boolean(count(//h1))" == true
|
||||
xpath "//h1" exists # Equivalent but simpler
|
||||
```
|
||||
|
||||
As the XPath query `boolean(count(//h1))` returns a boolean, the predicate value in the assert must be either
|
||||
`true` or `false` without double quotes. On the other side, say you have an article node and you want to check the value of some
|
||||
[data attributes]:
|
||||
|
||||
```xml
|
||||
<article
|
||||
id="electric-cars"
|
||||
data-visible="true"
|
||||
...
|
||||
</article>
|
||||
```
|
||||
|
||||
The following assert will check the value of the `data-visible` attribute:
|
||||
|
||||
```hurl
|
||||
GET https://example.org/home
|
||||
|
||||
HTTP/1.1 200
|
||||
[Asserts]
|
||||
xpath "string(//article/@data-visible)" == "true"
|
||||
```
|
||||
|
||||
In this case, the XPath query `string(//article/@data-visible)` returns a string, so the predicate value must be a
|
||||
string.
|
||||
|
||||
The predicate function `equals` can work with string, number or boolean while `matches`, `startWith` and `contains` work
|
||||
only on string. If a query returns a number, a `contains` predicate will raise a runner error.
|
||||
|
||||
```hurl
|
||||
# A really well tested web page...
|
||||
GET https://example.org/home
|
||||
|
||||
HTTP/1.1 200
|
||||
[Asserts]
|
||||
header "Content-Type" contains "text/html"
|
||||
header "Last-Modified" == "Wed, 21 Oct 2015 07:28:00 GMT"
|
||||
xpath "//h1" exists # Check we've at least one h1
|
||||
xpath "normalize-space(//h1)" contains "Welcome"
|
||||
xpath "//h2" count == 13
|
||||
xpath "string(//article/@data-id)" startsWith "electric"
|
||||
```
|
||||
|
||||
### Status assert
|
||||
|
||||
Check the received HTTP response status code. Status assert consists of the keyword `status` followed by a predicate
|
||||
function and value.
|
||||
|
||||
```hurl
|
||||
GET https://example.org
|
||||
|
||||
HTTP/1.1 *
|
||||
[Asserts]
|
||||
status < 300
|
||||
```
|
||||
|
||||
### Header assert
|
||||
|
||||
Check the value of a received HTTP response header. Header assert consists of the keyword `header` followed by a predicate
|
||||
function and value.
|
||||
|
||||
```hurl
|
||||
GET https://example.org
|
||||
|
||||
HTTP/1.1 302
|
||||
[Asserts]
|
||||
header "Location" contains "www.example.net"
|
||||
```
|
||||
|
||||
### Cookie assert
|
||||
|
||||
Check value or attributes of a [`Set-Cookie`] response header. Cookie assert
|
||||
consists of the keyword `cookie`, followed by the cookie name (and optionally a
|
||||
cookie attribute), a predicate function and value.
|
||||
|
||||
Cookie attributes value can be checked by using the following format:
|
||||
`<cookie-name>[cookie-attribute]`. The following attributes are supported: `Value`,
|
||||
`Expires`, `Max-Age`, `Domain`, `Path`, `Secure`, `HttpOnly` and `SameSite`.
|
||||
|
||||
```hurl
|
||||
GET http://localhost:8000/cookies/set
|
||||
|
||||
HTTP/1.0 200
|
||||
|
||||
# Explicit check of Set-Cookie header value. If the attributes are
|
||||
# not in this excat order, this assert will fail.
|
||||
Set-Cookie: LSID=DQAAAKEaem_vYg; Expires=Wed, 13 Jan 2021 22:23:01 GMT; Secure; HttpOnly; Path=/accounts; SameSite=Lax;
|
||||
Set-Cookie: HSID=AYQEVnDKrdst; Domain=.localhost; Expires=Wed, 13 Jan 2021 22:23:01 GMT; HttpOnly; Path=/
|
||||
Set-Cookie: SSID=Ap4PGTEq; Domain=.localhost; Expires=Wed, 13 Jan 2021 22:23:01 GMT; Secure; HttpOnly; Path=/
|
||||
|
||||
# Using cookie assert, one can check cookie value and various attributes.
|
||||
[Asserts]
|
||||
cookie "LSID" == "DQAAAKEaem_vYg"
|
||||
cookie "LSID[Value]" == "DQAAAKEaem_vYg"
|
||||
cookie "LSID[Expires]" exists
|
||||
cookie "LSID[Expires]" contains "Wed, 13 Jan 2021"
|
||||
cookie "LSID[Max-Age]" not exists
|
||||
cookie "LSID[Domain]" not exists
|
||||
cookie "LSID[Path]" == "/accounts"
|
||||
cookie "LSID[Secure]" exists
|
||||
cookie "LSID[HttpOnly]" exists
|
||||
cookie "LSID[SameSite]" equals "Lax"
|
||||
```
|
||||
|
||||
> `Secure` and `HttpOnly` attributes can only be tested with `exists` or `not exists` predicates
|
||||
> to reflect the [Set-Cookie header] semantic (in other words, queries `<cookie-name>[HttpOnly]`
|
||||
> and `<cookie-name>[Secure]` don't return boolean).
|
||||
|
||||
### Body assert
|
||||
|
||||
Check the value of the received HTTP response body when decoded as a string.
|
||||
Body assert consists of the keyword `body` followed by a predicate function and
|
||||
value. The encoding used to decode the body is based on the `charset` value in the
|
||||
`Content-Type` header response.
|
||||
|
||||
```hurl
|
||||
GET https://example.org
|
||||
|
||||
HTTP/1.1 200
|
||||
[Asserts]
|
||||
body contains "<h1>Welcome!</h1>"
|
||||
```
|
||||
|
||||
> Precise the encoding used to decode the text body.
|
||||
|
||||
### Bytes assert
|
||||
|
||||
Check the value of the received HTTP response body as a bytestream. Body assert
|
||||
consists of the keyword `bytes` followed by a predicate function and value.
|
||||
|
||||
```hurl
|
||||
GET https://example.org/data.bin
|
||||
|
||||
HTTP/* 200
|
||||
[Asserts]
|
||||
bytes startsWith hex,efbbbf;
|
||||
```
|
||||
|
||||
### XPath assert
|
||||
|
||||
Check the value of a [XPath] query on the received HTTP body decoded as a string.
|
||||
Currently, only XPath 1.0 expression can be used. Body assert consists of the
|
||||
keyword `xpath` followed by a predicate function and value. Values can be string,
|
||||
boolean or number depending on the XPath query.
|
||||
|
||||
Let's say we want to check this HTML response:
|
||||
|
||||
```plain
|
||||
$ curl -v https://example.org
|
||||
|
||||
< HTTP/1.1 200 OK
|
||||
< Content-Type: text/html; charset=UTF-8
|
||||
...
|
||||
<!doctype html>
|
||||
<html>
|
||||
<head>
|
||||
<title>Example Domain</title>
|
||||
...
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div>
|
||||
<h1>Example</h1>
|
||||
<p>This domain is for use in illustrative examples in documents. You may use this domain in literature without prior coordination or asking for permission.</p>
|
||||
<p><a href="https://www.iana.org/domains/example">More information...</a></p>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
With Hurl, we can write multiple XPath asserts describing the DOM content:
|
||||
|
||||
```hurl
|
||||
GET https://example.org
|
||||
|
||||
HTTP/1.1 200
|
||||
Content-Type: text/html; charset=UTF-8
|
||||
|
||||
[Asserts]
|
||||
xpath "string(/html/head/title)" contains "Example" # Check title
|
||||
xpath "count(//p)" == 2 # Check the number of <p>
|
||||
xpath "//p" count == 2 # Similar assert for <p>
|
||||
xpath "boolean(count(//h2))" == false # Check there is no <h2>
|
||||
xpath "//h2" not exists # Similar assert for <h2>
|
||||
```
|
||||
|
||||
### JSONPath assert
|
||||
|
||||
Check the value of a [JSONPath] query on the received HTTP body decoded as a JSON
|
||||
document. Body assert consists of the keyword `jsonpath` followed by a predicate
|
||||
function and value.
|
||||
|
||||
Let's say we want to check this JSON response:
|
||||
|
||||
```plain
|
||||
curl -v http://httpbin.org/json
|
||||
|
||||
< HTTP/1.1 200 OK
|
||||
< Content-Type: application/json
|
||||
...
|
||||
|
||||
{
|
||||
"slideshow": {
|
||||
"author": "Yours Truly",
|
||||
"date": "date of publication",
|
||||
"slides": [
|
||||
{
|
||||
"title": "Wake up to WonderWidgets!",
|
||||
"type": "all"
|
||||
},
|
||||
...
|
||||
],
|
||||
"title": "Sample Slide Show"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
With Hurl, we can write multiple JSONPath asserts describing the DOM content:
|
||||
|
||||
|
||||
```hurl
|
||||
GET http://httpbin.org/json
|
||||
|
||||
HTTP/1.1 200
|
||||
[Asserts]
|
||||
jsonpath "$.slideshow.author" == "Yours Truly"
|
||||
jsonpath "$.slideshow.slides[0].title" contains "Wonder"
|
||||
jsonpath "$.slideshow.slides" count == 2
|
||||
jsonpath "$.slideshow.date" != null
|
||||
jsonpath "$.slideshow.slides[*].title" includes "Mind Blowing!"
|
||||
```
|
||||
|
||||
> Explain that the value selected by the JSONPath is coerced to a string when only
|
||||
> one node is selected.
|
||||
|
||||
In `matches` predicates, metacharacters beginning with a backslash (like `\d`, `\s`) must be escaped.
|
||||
Alternatively, `matches` predicate support [Javascript-like Regular expression syntax] to enhance
|
||||
the readability:
|
||||
|
||||
```hurl
|
||||
GET https://sample.org/hello
|
||||
|
||||
HTTP/1.0 200
|
||||
[Asserts]
|
||||
# Predicate value with matches predicate:
|
||||
jsonpath "$.date" matches "^\\d{4}-\\d{2}-\\d{2}$"
|
||||
jsonpath "$.name" matches "Hello [a-zA-Z]+!"
|
||||
# Equivalent syntax:
|
||||
jsonpath "$.date" matches /^\d{4}-\d{2}-\d{2}$/
|
||||
jsonpath "$.name" matches /Hello [a-zA-Z]+!/
|
||||
```
|
||||
|
||||
### Regex assert
|
||||
|
||||
Check that the HTTP received body, decoded as text, matches a regex pattern.
|
||||
|
||||
```hurl
|
||||
GET https://sample.org/hello
|
||||
|
||||
HTTP/1.0 200
|
||||
[Asserts]
|
||||
regex "^\\d{4}-\\d{2}-\\d{2}$" == "2018-12-31"
|
||||
```
|
||||
|
||||
### SHA-256 assert
|
||||
|
||||
Check response body [SHA-256] hash.
|
||||
|
||||
```hurl
|
||||
GET https://example.org/data.tar.gz
|
||||
|
||||
HTTP/* *
|
||||
[Asserts]
|
||||
sha256 == hex,039058c6f2c0cb492c533b0a4d14ef77cc0f78abccced5287d84a1a2011cfb81;
|
||||
```
|
||||
|
||||
### MD5 assert
|
||||
|
||||
Check response body [MD5] hash.
|
||||
|
||||
```hurl
|
||||
GET https://example.org/data.tar.gz
|
||||
|
||||
HTTP/* *
|
||||
[Asserts]
|
||||
md5 == hex,ed076287532e86365e841e92bfc50d8c;
|
||||
```
|
||||
|
||||
|
||||
### Variable assert
|
||||
|
||||
```hurl
|
||||
# Test that the XML endpoint return 200 pets
|
||||
GET https://example.org/api/pets
|
||||
HTTP/* 200
|
||||
[Captures]
|
||||
pets: xpath "//pets"
|
||||
[Asserts]
|
||||
variable "pets" count == 200
|
||||
```
|
||||
|
||||
### Duration assert
|
||||
|
||||
Check the total duration (sending plus receiving time) of the HTTP transaction.
|
||||
|
||||
```hurl
|
||||
GET https://sample.org/helloworld
|
||||
|
||||
HTTP/1.0 200
|
||||
[Asserts]
|
||||
duration < 1000 # Check that response time is less than one second
|
||||
```
|
||||
|
||||
## Body
|
||||
|
||||
Optional assertion on the received HTTP response body. Body section can be seen
|
||||
as syntactic sugar over [body asserts] (with `equals` predicate function). If the
|
||||
body of the response is a [JSON] string or a [XML] string, the body assertion can
|
||||
be directly inserted without any modification. For a text based body that is not JSON nor XML,
|
||||
one can use multiline string that starts with <code>```</code> and ends
|
||||
with <code>```</code>. For a precise byte control of the response body,
|
||||
a [Base64] encoded string or an input file can be used to describe exactly
|
||||
the body byte content to check.
|
||||
|
||||
### JSON body
|
||||
|
||||
```hurl
|
||||
# Get a doggy thing:
|
||||
GET https://example.org/api/dogs/{{dog-id}}
|
||||
|
||||
HTTP/1.1 200
|
||||
{
|
||||
"id": 0,
|
||||
"name": "Frieda",
|
||||
"picture": "images/scottish-terrier.jpeg",
|
||||
"age": 3,
|
||||
"breed": "Scottish Terrier",
|
||||
"location": "Lisco, Alabama"
|
||||
}
|
||||
```
|
||||
|
||||
### XML body
|
||||
|
||||
~~~hurl
|
||||
GET https://example.org/api/catalog
|
||||
|
||||
HTTP/1.1 200
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<catalog>
|
||||
<book id="bk101">
|
||||
<author>Gambardella, Matthew</author>
|
||||
<title>XML Developer's Guide</title>
|
||||
<genre>Computer</genre>
|
||||
<price>44.95</price>
|
||||
<publish_date>2000-10-01</publish_date>
|
||||
<description>An in-depth look at creating applications with XML.</description>
|
||||
</book>
|
||||
</catalog>
|
||||
~~~
|
||||
|
||||
### Raw string body
|
||||
|
||||
~~~hurl
|
||||
GET https://example.org/models
|
||||
|
||||
HTTP/1.1 200
|
||||
```
|
||||
Year,Make,Model,Description,Price
|
||||
1997,Ford,E350,"ac, abs, moon",3000.00
|
||||
1999,Chevy,"Venture ""Extended Edition""","",4900.00
|
||||
1999,Chevy,"Venture ""Extended Edition, Very Large""",,5000.00
|
||||
1996,Jeep,Grand Cherokee,"MUST SELL! air, moon roof, loaded",4799.00
|
||||
```
|
||||
~~~
|
||||
|
||||
The standard usage of a raw string is :
|
||||
|
||||
~~~
|
||||
```
|
||||
line1
|
||||
line2
|
||||
line3
|
||||
```
|
||||
~~~
|
||||
|
||||
is evaluated as "line1\nline2\nline3\n".
|
||||
|
||||
|
||||
To construct an empty string :
|
||||
|
||||
~~~
|
||||
```
|
||||
```
|
||||
~~~
|
||||
|
||||
or
|
||||
|
||||
~~~
|
||||
``````
|
||||
~~~
|
||||
|
||||
|
||||
Finaly, raw string can be used without any newline:
|
||||
|
||||
~~~
|
||||
```line```
|
||||
~~~
|
||||
|
||||
is evaluated as "line".
|
||||
|
||||
### Base64 body
|
||||
|
||||
Base64 body assert starts with `base64,` and end with `;`. MIME's Base64 encoding
|
||||
is supported (newlines and white spaces may be present anywhere but are to be
|
||||
ignored on decoding), and `=` padding characters might be added.
|
||||
|
||||
```hurl
|
||||
GET https://example.org
|
||||
|
||||
HTTP/1.1 200
|
||||
base64,TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIG
|
||||
FkaXBpc2NpbmcgZWxpdC4gSW4gbWFsZXN1YWRhLCBuaXNsIHZlbCBkaWN0dW0g
|
||||
aGVuZHJlcml0LCBlc3QganVzdG8gYmliZW5kdW0gbWV0dXMsIG5lYyBydXRydW
|
||||
0gdG9ydG9yIG1hc3NhIGlkIG1ldHVzLiA=;
|
||||
```
|
||||
|
||||
### File body
|
||||
|
||||
To use the binary content of a local file as the body response assert, file body
|
||||
can be used. File body starts with `file,` and ends with `;``
|
||||
|
||||
```hurl
|
||||
GET https://example.org
|
||||
|
||||
HTTP/1.1 200
|
||||
file,data.bin;
|
||||
```
|
||||
|
||||
File are relative to the input Hurl file, and cannot contain implicit parent
|
||||
directory (`..`). You can use [`--file-root` option] to specify the root directory
|
||||
of all file nodes.
|
||||
|
||||
[predicates]: #predicates
|
||||
[header assert]: #header-assert
|
||||
[captures]: /docs/capturing-response.md#query
|
||||
[data attributes]: https://developer.mozilla.org/en-US/docs/Learn/HTML/Howto/Use_data_attributes
|
||||
[`Set-Cookie`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
|
||||
[Set-Cookie header]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
|
||||
[XPath]: https://en.wikipedia.org/wiki/XPath
|
||||
[JSONPath]: https://goessner.net/articles/JsonPath/
|
||||
[body asserts]: #body-assert
|
||||
[JSON]: https://www.json.org
|
||||
[XML]: https://en.wikipedia.org/wiki/XML
|
||||
[Base64]: https://en.wikipedia.org/wiki/Base64
|
||||
[`--file-root` option]: /docs/man-page.md#file-root
|
||||
[`count`]: /docs/capturing-response.md#count-subquery
|
||||
[Javascript-like Regular expression syntax]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
|
||||
[MD5]: https://en.wikipedia.org/wiki/MD5
|
||||
[SHA-256]: https://en.wikipedia.org/wiki/SHA-2
|
BIN
docs/assets/img/github-action-dark.png
Normal file
After Width: | Height: | Size: 52 KiB |
BIN
docs/assets/img/github-action-light.png
Normal file
After Width: | Height: | Size: 54 KiB |
BIN
docs/assets/img/github-new-repository-dark.png
Normal file
After Width: | Height: | Size: 66 KiB |
BIN
docs/assets/img/github-new-repository-light.png
Normal file
After Width: | Height: | Size: 66 KiB |
BIN
docs/assets/img/hurl-html-file.png
Normal file
After Width: | Height: | Size: 54 KiB |
BIN
docs/assets/img/hurl-html-report.png
Normal file
After Width: | Height: | Size: 33 KiB |
5
docs/assets/img/logo-dark.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg width="704px" height="183px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g style=""> <path d="M498.000000,33.000000 L652.046555,33.000000 C652.051235,14.993509,652.042435,0.117931,652.000665,0.000622 L703.499993,48.000000 L651.999993,96.000000 C651.999993,96.000000,652.015443,82.165430,652.028753,65.000000 L497.999990,65.000000 Z M652.000000,0.000000 C652.000220,-0.000210,652.000440,0.000000,652.000670,0.000620 Z " fill="#ff0288" stroke="none" />
|
||||
</g><g style=""> <path d="M695.000000,115.000000 L540.953445,115.000000 C540.948765,96.993509,540.957565,82.117931,540.999335,82.000622 L489.500007,130.000000 L541.000007,178.000000 C541.000007,178.000000,540.984557,164.165430,540.971247,147.000000 L695.000010,147.000000 Z M541.000000,82.000000 C540.999780,81.999790,540.999560,82.000000,540.999330,82.000620 Z " fill="#ff0288" stroke="none" />
|
||||
</g><g style=""> <path d="M0.250000,0.500000 L0.250000,179.000000 L39.500000,179.000000 L39.500000,102.000000 L111.750000,102.000000 L111.750000,179.000000 L151.000000,179.000000 L151.000000,0.500000 L111.750000,0.500000 L111.750000,69.000000 L39.500000,69.000000 L39.500000,0.500000 Z M303.000000,179.000000 L303.000000,49.750000 L267.500000,49.750000 L267.500000,117.500000 C267.500000,130.666733,265.333355,140.124971,261.000000,145.875000 C256.666645,151.625029,249.666715,154.500000,240.000000,154.500000 C231.499957,154.500000,225.583350,151.875026,222.250000,146.625000 C218.916650,141.374974,217.250000,133.416720,217.250000,122.750000 L217.250000,49.750000 L181.750000,49.750000 L181.750000,129.250000 C181.750000,137.250040,182.458326,144.541634,183.875000,151.125000 C185.291674,157.708366,187.749982,163.291644,191.250000,167.875000 C194.750018,172.458356,199.541636,175.999988,205.625000,178.500000 C211.708364,181.000013,219.499953,182.250000,229.000000,182.250000 C236.500037,182.250000,243.833298,180.583350,251.000000,177.250000 C258.166702,173.916650,263.999977,168.500038,268.500000,161.000000 L269.250000,161.000000 L269.250000,179.000000 Z M330.000000,49.750000 L330.000000,179.000000 L365.500000,179.000000 L365.500000,120.750000 C365.500000,114.916637,366.083327,109.500025,367.250000,104.500000 C368.416673,99.499975,370.374986,95.125019,373.125000,91.375000 C375.875014,87.624981,379.499977,84.666677,384.000000,82.500000 C388.500023,80.333323,393.999968,79.250000,400.500000,79.250000 C402.666677,79.250000,404.916655,79.374999,407.250000,79.625000 C409.583345,79.875001,411.583325,80.166665,413.250000,80.500000 L413.250000,47.500000 C410.416652,46.666662,407.833345,46.250000,405.500000,46.250000 C400.999977,46.250000,396.666687,46.916660,392.500000,48.250000 C388.333313,49.583340,384.416685,51.458321,380.750000,53.875000 C377.083315,56.291679,373.833348,59.208316,371.000000,62.625000 C368.166652,66.041684,365.916675,69.749980,364.250000,73.750000 L363.750000,73.750000 L363.750000,49.750000 Z M428.250000,0.500000 L428.250000,179.000000 L463.750000,179.000000 L463.750000,0.500000 Z " style="fill: rgba(222, 222, 222, 1.000000); stroke-width: 0.000000px; stroke: rgba(0, 0, 0, 1.000000); " fill="#dedede" stroke="#000000" stroke-width="0.000000" />
|
||||
</g></svg>
|
After Width: | Height: | Size: 3.1 KiB |
5
docs/assets/img/logo-light.svg
Normal file
@ -0,0 +1,5 @@
|
||||
<svg width="704px" height="183px" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
|
||||
<g style=""> <path d="M498.000000,33.000000 L652.046555,33.000000 C652.051235,14.993509,652.042435,0.117931,652.000665,0.000622 L703.499993,48.000000 L651.999993,96.000000 C651.999993,96.000000,652.015443,82.165430,652.028753,65.000000 L497.999990,65.000000 Z M652.000000,0.000000 C652.000220,-0.000210,652.000440,0.000000,652.000670,0.000620 Z " fill="#ff0288" stroke="none" />
|
||||
</g><g style=""> <path d="M695.000000,115.000000 L540.953445,115.000000 C540.948765,96.993509,540.957565,82.117931,540.999335,82.000622 L489.500007,130.000000 L541.000007,178.000000 C541.000007,178.000000,540.984557,164.165430,540.971247,147.000000 L695.000010,147.000000 Z M541.000000,82.000000 C540.999780,81.999790,540.999560,82.000000,540.999330,82.000620 Z " fill="#ff0288" stroke="none" />
|
||||
</g><g style=""> <path d="M0.250000,0.500000 L0.250000,179.000000 L39.500000,179.000000 L39.500000,102.000000 L111.750000,102.000000 L111.750000,179.000000 L151.000000,179.000000 L151.000000,0.500000 L111.750000,0.500000 L111.750000,69.000000 L39.500000,69.000000 L39.500000,0.500000 Z M303.000000,179.000000 L303.000000,49.750000 L267.500000,49.750000 L267.500000,117.500000 C267.500000,130.666733,265.333355,140.124971,261.000000,145.875000 C256.666645,151.625029,249.666715,154.500000,240.000000,154.500000 C231.499957,154.500000,225.583350,151.875026,222.250000,146.625000 C218.916650,141.374974,217.250000,133.416720,217.250000,122.750000 L217.250000,49.750000 L181.750000,49.750000 L181.750000,129.250000 C181.750000,137.250040,182.458326,144.541634,183.875000,151.125000 C185.291674,157.708366,187.749982,163.291644,191.250000,167.875000 C194.750018,172.458356,199.541636,175.999988,205.625000,178.500000 C211.708364,181.000013,219.499953,182.250000,229.000000,182.250000 C236.500037,182.250000,243.833298,180.583350,251.000000,177.250000 C258.166702,173.916650,263.999977,168.500038,268.500000,161.000000 L269.250000,161.000000 L269.250000,179.000000 Z M330.000000,49.750000 L330.000000,179.000000 L365.500000,179.000000 L365.500000,120.750000 C365.500000,114.916637,366.083327,109.500025,367.250000,104.500000 C368.416673,99.499975,370.374986,95.125019,373.125000,91.375000 C375.875014,87.624981,379.499977,84.666677,384.000000,82.500000 C388.500023,80.333323,393.999968,79.250000,400.500000,79.250000 C402.666677,79.250000,404.916655,79.374999,407.250000,79.625000 C409.583345,79.875001,411.583325,80.166665,413.250000,80.500000 L413.250000,47.500000 C410.416652,46.666662,407.833345,46.250000,405.500000,46.250000 C400.999977,46.250000,396.666687,46.916660,392.500000,48.250000 C388.333313,49.583340,384.416685,51.458321,380.750000,53.875000 C377.083315,56.291679,373.833348,59.208316,371.000000,62.625000 C368.166652,66.041684,365.916675,69.749980,364.250000,73.750000 L363.750000,73.750000 L363.750000,49.750000 Z M428.250000,0.500000 L428.250000,179.000000 L463.750000,179.000000 L463.750000,0.500000 Z " style="fill: rgba(38, 38, 38, 1.000000); stroke-width: 0.000000px; stroke: rgba(0, 0, 0, 1.000000); " fill="#333333" stroke="#000000" stroke-width="0.000000" />
|
||||
</g></svg>
|
After Width: | Height: | Size: 3.1 KiB |
BIN
docs/assets/img/quiz-dark.png
Normal file
After Width: | Height: | Size: 79 KiB |
BIN
docs/assets/img/quiz-light.png
Normal file
After Width: | Height: | Size: 78 KiB |
336
docs/capturing-response.md
Normal file
@ -0,0 +1,336 @@
|
||||
# Capturing Response
|
||||
|
||||
## Captures
|
||||
|
||||
Captures are optional values captured from the HTTP response, in a named variable. Captures can be the
|
||||
response status code, part or the entire of the body, and response headers.
|
||||
|
||||
Captured variables are available through a run session; each new value of a given variable overrides the last value.
|
||||
|
||||
Captures allow using data from one request to another request, when working with
|
||||
[CSRF tokens] for instance. Variables can also be initialized at the start of the
|
||||
session, by passing [variable values], or can be used in [templates].
|
||||
|
||||
```hurl
|
||||
# An example to show how to pass a CSRF token from one request
|
||||
# to another:
|
||||
|
||||
# First GET request to get CSRF token value:
|
||||
GET https://example.org
|
||||
|
||||
HTTP/1.1 200
|
||||
# Capture the CSRF token value from html body.
|
||||
[Captures]
|
||||
csrf_token: xpath "normalize-space(//meta[@name='_csrf_token']/@content)"
|
||||
|
||||
# Do the login !
|
||||
POST https://acmecorp.net/login?user=toto&password=1234
|
||||
X-CSRF-TOKEN: {{csrf_token}}
|
||||
|
||||
HTTP/1.1 302
|
||||
```
|
||||
|
||||
Structure of a capture:
|
||||
|
||||
<div class="schema-container schema-container u-font-size-2 u-font-size-3-sm">
|
||||
<div class="schema">
|
||||
<span class="schema-token schema-color-1">my_var<span class="schema-label">variable</span></span>
|
||||
<span> : </span>
|
||||
<span class="schema-token schema-color-2">xpath "string(//h1)"<span class="schema-label">query</span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
A capture consists of a variable name, followed by `:` and a query. The captures
|
||||
section starts with `[Captures]`.
|
||||
|
||||
|
||||
### Query
|
||||
|
||||
Query can be of the following type:
|
||||
|
||||
- [`status`](#status-capture)
|
||||
- [`header`](#header-capture)
|
||||
- [`cookie`](#cookie-capture)
|
||||
- [`body`](#body-capture)
|
||||
- [`bytes`](#bytes-capture)
|
||||
- [`xpath`](#xpath-capture)
|
||||
- [`jsonpath`](#jsonpath-capture)
|
||||
- [`regex`](#regex-capture)
|
||||
- [`variable`](#variable-capture)
|
||||
- [`duration`](#duration-capture)
|
||||
|
||||
|
||||
### Status capture
|
||||
|
||||
Capture the received HTTP response status code. Status capture consists of a variable name, followed by a `:`, and the
|
||||
keyword `status`.
|
||||
|
||||
```hurl
|
||||
GET https://example.org
|
||||
|
||||
HTTP/1.1 200
|
||||
[Captures]
|
||||
my_status: status
|
||||
```
|
||||
|
||||
### Header capture
|
||||
|
||||
Capture a header from the received HTTP response headers. Header capture consists of a variable name, followed by a `:`,
|
||||
then the keyword `header` and a header name.
|
||||
|
||||
```hurl
|
||||
POST https://example.org/login
|
||||
[FormParams]
|
||||
user: toto
|
||||
password: 12345678
|
||||
|
||||
HTTP/1.1 302
|
||||
[Captures]
|
||||
next_url: header "Location"
|
||||
```
|
||||
|
||||
### Cookie capture
|
||||
|
||||
Capture a [`Set-Cookie`] header from the received HTTP response headers. Cookie
|
||||
capture consists of a variable name, followed by a `:`, then the keyword `cookie`
|
||||
and a cookie name.
|
||||
|
||||
```hurl
|
||||
GET https://example.org/cookies/set
|
||||
|
||||
HTTP/1.0 200
|
||||
[Captures]
|
||||
session-id: cookie "LSID"
|
||||
```
|
||||
|
||||
Cookie attributes value can also be captured by using the following format:
|
||||
`<cookie-name>[cookie-attribute]`. The following attributes are supported:
|
||||
`Value`, `Expires`, `Max-Age`, `Domain`, `Path`, `Secure`, `HttpOnly` and `SameSite`.
|
||||
|
||||
```hurl
|
||||
GET https://example.org/cookies/set
|
||||
|
||||
HTTP/1.0 200
|
||||
[Captures]
|
||||
value1: cookie "LSID"
|
||||
value2: cookie "LSID[Value]" # Equivalent to the previous capture
|
||||
expires: cookie "LSID[Expires]"
|
||||
max-age: cookie "LSID[Max-Age]"
|
||||
domain: cookie "LSID[Domain]"
|
||||
path: cookie "LSID[Path]"
|
||||
secure: cookie "LSID[Secure]"
|
||||
http-only: cookie "LSID[HttpOnly]"
|
||||
same-site: cookie "LSID[SameSite]"
|
||||
```
|
||||
|
||||
|
||||
### Body capture
|
||||
|
||||
Capture the entire body (decoded as text) from the received HTTP response
|
||||
|
||||
```hurl
|
||||
GET https://example.org/home
|
||||
|
||||
HTTP/1.1 200
|
||||
[Captures]
|
||||
my_body: body
|
||||
```
|
||||
|
||||
### Bytes capture
|
||||
|
||||
Capture the entire body (as a raw bytestream) from the received HTTP response
|
||||
|
||||
```hurl
|
||||
GET https://example.org/data.bin
|
||||
|
||||
HTTP/1.1 200
|
||||
[Captures]
|
||||
my_data: bytes
|
||||
```
|
||||
|
||||
|
||||
### XPath capture
|
||||
|
||||
Capture a [XPath] query from the received HTTP body decoded as a string.
|
||||
Currently, only XPath 1.0 expression can be used.
|
||||
|
||||
```hurl
|
||||
GET https://example.org/home
|
||||
|
||||
# Capture the identifier from the dom node <div id="pet0">5646eaf23</div
|
||||
HTTP/1.1 200
|
||||
[Captures]
|
||||
ped-id: xpath "normalize-space(//div[@id='pet0'])"
|
||||
|
||||
# Open the captured page.
|
||||
GET https://example.org/home/pets/{{pet-id}}
|
||||
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
XPath captures are not limited to node values (like string, or boolean); any
|
||||
valid XPath can be captured and assert with variable asserts.
|
||||
|
||||
```hurl
|
||||
# Test that the XML endpoint return 200 pets
|
||||
GET https://example.org/api/pets
|
||||
HTTP/* 200
|
||||
[Captures]
|
||||
pets: xpath "//pets"
|
||||
[Asserts]
|
||||
variable "pets" count == 200
|
||||
```
|
||||
|
||||
|
||||
### JSONPath capture
|
||||
|
||||
Capture a [JSONPath] query from the received HTTP body.
|
||||
|
||||
```hurl
|
||||
POST https://example.org/api/contact
|
||||
[FormParams]
|
||||
token: {{token}}
|
||||
email: toto@rookie.net
|
||||
|
||||
HTTP/1.1 200
|
||||
[Captures]
|
||||
contact-id: jsonpath "$['id']"
|
||||
```
|
||||
|
||||
> Explain that the value selected by the JSONPath is coerced to a string when only one node is selected.
|
||||
|
||||
As with [XPath captures], JSONPath captures can be anything from string, number, to object and collections.
|
||||
For instance, if we have a JSON endpoint that returns the following JSON:
|
||||
|
||||
```
|
||||
{
|
||||
"a_null": null,
|
||||
"an_object": {
|
||||
"id": "123"
|
||||
},
|
||||
"a_list": [
|
||||
1,
|
||||
2,
|
||||
3
|
||||
],
|
||||
"an_integer": 1,
|
||||
"a float": 1.1,
|
||||
"a_bool": true,
|
||||
"a_string": "hello"
|
||||
}
|
||||
```
|
||||
|
||||
We can capture the following paths:
|
||||
|
||||
```hurl
|
||||
GET https://example.org/captures-json
|
||||
|
||||
HTTP/1.0 200
|
||||
[Captures]
|
||||
an_object: jsonpath "$['an_object']"
|
||||
a_list: jsonpath "$['a_list']"
|
||||
a_null: jsonpath "$['a_null']"
|
||||
an_integer: jsonpath "$['an_integer']"
|
||||
a_float: jsonpath "$['a_float']"
|
||||
a_bool: jsonpath "$['a_bool']"
|
||||
a_string: jsonpath "$['a_string']"
|
||||
all: jsonpath "$"
|
||||
```
|
||||
|
||||
|
||||
### Regex capture
|
||||
|
||||
Capture a regex pattern from the HTTP received body, decoded as text.
|
||||
|
||||
```hurl
|
||||
GET https://example.org/helloworld
|
||||
|
||||
HTTP/1.0 200
|
||||
[Captures]
|
||||
id_a: regex "id_a:([0-9]+)!"
|
||||
id_b: regex "id_b:(\\d+)!"
|
||||
name: regex "Hello ([a-zA-Z]+)!"
|
||||
```
|
||||
|
||||
Pattern of the regex query must have at least one capture group, otherwise the
|
||||
capture will fail. Metacharacters beginning with a backslash in the pattern
|
||||
(like `\d`, `\s`) must be escaped: `regex "(\\d+)!"` will capture one or more digit.
|
||||
|
||||
|
||||
### Variable capture
|
||||
|
||||
Capture the value of a variable into another.
|
||||
|
||||
```hurl
|
||||
GET https://example.org/helloworld
|
||||
|
||||
HTTP/1.0 200
|
||||
[Captures]
|
||||
in: body
|
||||
name: variable "in" regex "Hello ([a-zA-Z]+)!"
|
||||
```
|
||||
|
||||
### Duration capture
|
||||
|
||||
Capture the response time of the request in ms.
|
||||
|
||||
```hurl
|
||||
GET https://example.org/helloworld
|
||||
|
||||
HTTP/1.0 200
|
||||
[Captures]
|
||||
duration_in_ms: duration
|
||||
|
||||
```
|
||||
|
||||
### Subquery
|
||||
|
||||
Optionally, query can be refined using subqueries `regex` and `count`.
|
||||
|
||||
<div class="schema-container u-font-size-0 u-font-size-1-sm u-font-size-3-md">
|
||||
<div class="schema">
|
||||
<span class="schema-token schema-color-1">my_var<span class="schema-label">variable</span></span>
|
||||
<span> : </span>
|
||||
<span class="schema-token schema-color-2">xpath "string(//h1)"<span class="schema-label">query</span></span>
|
||||
<span class="schema-token">regex "(\\d+)"<span class="schema-label">subquery (optional)</span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
#### Regex subquery
|
||||
|
||||
```hurl
|
||||
GET https://pets.org/cats/cutest
|
||||
|
||||
HTTP/1.0 200
|
||||
# Cat name are structured like this `meow + id`: for instance `meow123456`
|
||||
[Captures]
|
||||
id: jsonpath "$.cats[0].name" regex "meow(\\d+)"
|
||||
id: jsonpath "$.cats[0].name" regex "meow(\\d+)"
|
||||
```
|
||||
|
||||
Pattern of the regex subquery must have at least one capture group, otherwise the
|
||||
capture will fail. Metacharacters beginning with a backslash in the pattern
|
||||
(like `\d`, `\s`) must be escaped: `regex "(\\d+)!"` will capture one or more digit.
|
||||
|
||||
#### Count subquery
|
||||
|
||||
Returns the count of a collection.
|
||||
|
||||
```hurl
|
||||
GET https://pets.org/cats/cutest
|
||||
|
||||
HTTP/1.0 200
|
||||
[Captures]
|
||||
cats_size: jsonpath "$.cats" count
|
||||
```
|
||||
|
||||
|
||||
|
||||
[CSRF tokens]: https://en.wikipedia.org/wiki/Cross-site_request_forgery
|
||||
[variable values]: /docs/man-page.md#variable
|
||||
[templates]: /docs/templates.md
|
||||
[`Set-Cookie`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
|
||||
[XPath]: https://en.wikipedia.org/wiki/XPath
|
||||
[JSONPath]: https://goessner.net/articles/JsonPath/
|
||||
[XPath captures]: #xpath-capture
|
||||
[Javascript-like Regular expression syntax]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
|
74
docs/entry.md
Normal file
@ -0,0 +1,74 @@
|
||||
# Entry
|
||||
|
||||
## Definition
|
||||
|
||||
A Hurl file is a list of entry, each entry being a mandatory [request], optionally followed by a [response].
|
||||
|
||||
Responses are not mandatory, a Hurl file consisting only of requests is perfectly valid. To sum up, responses can be used
|
||||
to [capture values] to perform subsequent requests, or [add asserts to HTTP responses].
|
||||
|
||||
## Example
|
||||
|
||||
```hurl
|
||||
# First, test home title.
|
||||
GET https://acmecorp.net
|
||||
|
||||
HTTP/1.1 200
|
||||
[Asserts]
|
||||
xpath "normalize-space(//head/title)" == "Hello world!"
|
||||
|
||||
# Get some news, response description is optional
|
||||
GET https://acmecorp.net/news
|
||||
|
||||
# Do a POST request without csrf token and check
|
||||
# that status code is Forbidden 403
|
||||
POST https://acmecorp.net/contact
|
||||
[FormParams]
|
||||
default: false
|
||||
email: john.doe@rookie.org
|
||||
number: 33611223344
|
||||
|
||||
HTTP/1.1 403
|
||||
```
|
||||
|
||||
## Description
|
||||
|
||||
### Cookie storage
|
||||
|
||||
Requests in the same Hurl file share the cookie storage, enabling, for example, session based scenario.
|
||||
|
||||
### Redirects
|
||||
|
||||
By default, Hurl doesn't follow redirection. To effectively run a redirection, entries should describe each step
|
||||
of the redirection, allowing insertion of asserts in each response.
|
||||
|
||||
```hurl
|
||||
# First entry, test the redirection (status code and
|
||||
# Location header)
|
||||
GET http://google.fr
|
||||
|
||||
HTTP/1.1 301
|
||||
Location: http://www.google.fr/
|
||||
|
||||
# Second entry, the 200 OK response
|
||||
GET http://www.google.fr
|
||||
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
Alternatively, one can use [`--location`] option to force redirection
|
||||
to be followed. In this case, asserts are executed on the last received response. Optionally, the number of
|
||||
redirections can be limited with [`--max-redirs`].
|
||||
|
||||
```hurl
|
||||
# Running hurl --location google.hurl
|
||||
GET http://google.fr
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
[request]: /docs/request.md
|
||||
[response]: /docs/response.md
|
||||
[capture values]: /docs/capturing-response.md
|
||||
[add asserts to HTTP responses]: /docs/asserting-response.md
|
||||
[`--location`]: /docs/man-page.md#location
|
||||
[`--max-redirs`]: /docs/man-page.md#max-redirs
|
264
docs/frequently-asked-questions.md
Normal file
@ -0,0 +1,264 @@
|
||||
# Frequently Asked Questions
|
||||
|
||||
- [General](#general)
|
||||
- [Why "Hurl"?](#why-hurl)
|
||||
- [Yet Another Tool, I already use X](#yet-another-tool-i-already-use-x)
|
||||
- [Hurl is build on top of libcurl, but what is added?](#hurl-is-build-on-top-of-libcurl-but-what-is-added)
|
||||
- [Why shouldn't I use Hurl?](#why-shouldnt-i-use-hurl)
|
||||
- [I have a large numbers of tests, how to run just specific tests?](#i-have-a-large-numbers-of-tests-how-to-run-just-specific-tests)
|
||||
- [How can I use my Hurl files outside Hurl?](#how-can-i-use-my-hurl-files-outside-hurl)
|
||||
- [Can I do calculation within a Hurl file?](#can-i-do-calculation-within-a-hurl-file)
|
||||
- [macOS](#macos)
|
||||
- [How can I use a custom libcurl (from Homebrew by instance)?](#how-can-i-use-a-custom-libcurl-from-homebrew-by-instance)
|
||||
- [Hurl error: SSL certificate has expired](#hurl-error-ssl-certificate-has-expired)
|
||||
- [Hurl warning on Big Sur: Closing connection 0](#hurl-warning-on-big-sur-closing-connection-0)
|
||||
|
||||
## General
|
||||
|
||||
### Why "Hurl"?
|
||||
|
||||
The name Hurl is a tribute to the awesome [curl], with a focus on the HTTP protocol.
|
||||
While it may have an informal meaning not particularly elegant, [other eminent tools] have set a precedent in naming.
|
||||
|
||||
### Yet Another Tool, I already use X
|
||||
|
||||
We think that Hurl has some advantages compared to similar tools.
|
||||
|
||||
Hurl is foremost a command line tool and should be easy to use on a local computer, or in a CI/CD pipeline. Some
|
||||
tools in the same space as Hurl ([Postman] for instance), are GUI oriented, and we find it
|
||||
less attractive than CLI. As a command line tool, Hurl can be used to get HTTP datas (like [curl]),
|
||||
but also as a test tool for HTTP sessions, or even as documentation.
|
||||
|
||||
Having a text based [file format] is another advantage. The Hurl format is simple,
|
||||
focused on the HTTP domain, can serve as documentation and can be read or written by non-technical people.
|
||||
|
||||
For instance posting JSON data with Hurl can be done with this simple file:
|
||||
|
||||
```
|
||||
POST http://localhost:3000/api/login
|
||||
{
|
||||
"username": "xyz",
|
||||
"password": "xyz"
|
||||
}
|
||||
```
|
||||
|
||||
With [curl]:
|
||||
|
||||
```
|
||||
curl --header "Content-Type: application/json" \
|
||||
--request POST \
|
||||
--data '{"username": "xyz","password": "xyz"}' \
|
||||
http://localhost:3000/api/login
|
||||
```
|
||||
|
||||
|
||||
[Karate], a tool combining API test automation, mocking, performance-testing, has
|
||||
similar features but offers also much more at a cost of an increased complexity.
|
||||
|
||||
Comparing Karate file format:
|
||||
|
||||
```
|
||||
Scenario: create and retrieve a cat
|
||||
|
||||
Given url 'http://myhost.com/v1/cats'
|
||||
And request { name: 'Billie' }
|
||||
When method post
|
||||
Then status 201
|
||||
And match response == { id: '#notnull', name: 'Billie }
|
||||
|
||||
Given path response.id
|
||||
When method get
|
||||
Then status 200
|
||||
```
|
||||
|
||||
And Hurl:
|
||||
|
||||
```
|
||||
# Scenario: create and retrieve a cat
|
||||
|
||||
POST http://myhost.com/v1/cats
|
||||
{ "name": "Billie" }
|
||||
HTTP/* 201
|
||||
[Captures]
|
||||
cat_id: jsonpath "$.id"
|
||||
[Asserts]
|
||||
jsonpath "$.name" == "Billie"
|
||||
|
||||
GET http://myshost.com/v1/cats/{{cat_id}}
|
||||
HTTP/* 200
|
||||
```
|
||||
|
||||
A key point of Hurl is to work on the HTTP domain. In particular, there is no Javascript runtime, Hurl works on the
|
||||
raw HTTP requests/responses, and not on a DOM managed by a HTML engine. For security, this can be seen as a feature:
|
||||
let's say you want to test backend validation, you want to be able to bypass the browser or javascript validations and
|
||||
directly test a backend endpoint.
|
||||
|
||||
Finally, with no headless browser and working on the raw HTTP data, Hurl is also
|
||||
really reliable with a very small probability of false positives. Integration tests with tools like
|
||||
[Selenium] can, in this regard, be challenging to maintain.
|
||||
|
||||
Just use what is convenient for you. In our case, it's Hurl!
|
||||
|
||||
### Hurl is build on top of libcurl, but what is added?
|
||||
|
||||
Hurl has two main functionalities on top of [curl]:
|
||||
|
||||
1. Chain several requests:
|
||||
|
||||
With its [captures], it enables to inject data received from a response into
|
||||
following requests. [CSRF tokens]
|
||||
are typical examples in a standard web session.
|
||||
|
||||
2. Test HTTP responses:
|
||||
|
||||
With its [asserts], responses can be easily tested.
|
||||
|
||||
### Why shouldn't I use Hurl?
|
||||
|
||||
If you need a GUI. Currently, Hurl does not offer a GUI version (like [Postman]). While we
|
||||
think that it can be useful, we prefer to focus for the time-being on the core, keeping something simple and fast.
|
||||
Contributions to build a GUI are welcome.
|
||||
|
||||
|
||||
### I have a large numbers of tests, how to run just specific tests?
|
||||
|
||||
By convention, you can organize Hurl files into different folders or prefix them.
|
||||
|
||||
For example, you can split your tests into two folders critical and additional.
|
||||
|
||||
```
|
||||
critical/test1.hurl
|
||||
critical/test2.hurl
|
||||
additional/test1.hurl
|
||||
additional/test2.hurl
|
||||
```
|
||||
|
||||
You can simply run your critical tests with
|
||||
|
||||
```
|
||||
hurl critical/*.hurl
|
||||
```
|
||||
|
||||
### How can I use my Hurl files outside Hurl?
|
||||
|
||||
Hurl file can be exported to a json file with `hurlfmt`.
|
||||
This json file can then be easily parsed for converting a different format, getting ad-hoc information,...
|
||||
|
||||
For example, the Hurl file
|
||||
|
||||
```hurl
|
||||
GET https://example.org/api/users/1
|
||||
User-Agent: Custom
|
||||
|
||||
HTTP/1.1 200
|
||||
[Asserts]
|
||||
jsonpath "$.name" equals "Bob"
|
||||
|
||||
```
|
||||
|
||||
will be converted to json with the following command:
|
||||
|
||||
```
|
||||
hurlfmt test.hurl --format json | jq
|
||||
{
|
||||
"entries": [
|
||||
{
|
||||
"request": {
|
||||
"method": "GET",
|
||||
"url": "https://example.org/api/users/1",
|
||||
"headers": [
|
||||
{
|
||||
"name": "User-Agent",
|
||||
"value": "Custom"
|
||||
}
|
||||
]
|
||||
},
|
||||
"response": {
|
||||
"version": "HTTP/1.1",
|
||||
"status": 200,
|
||||
"asserts": [
|
||||
{
|
||||
"query": {
|
||||
"type": "jsonpath",
|
||||
"expr": "$.name"
|
||||
},
|
||||
"predicate": {
|
||||
"type": "equal",
|
||||
"value": "Bob"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
### Can I do calculation within a Hurl file?
|
||||
|
||||
Currently, the templating is very simple, only accessing variables.
|
||||
Calculations can be done beforehand, before running the Hurl File.
|
||||
|
||||
For example, with date calculations, variables `now` and `tomorrow` can be used as param or expected value.
|
||||
|
||||
```
|
||||
TODAY=$(date '+%y%m%d')
|
||||
TOMORROW=$(date '+%y%m%d' -d"+1days")
|
||||
hurl --variable "today=$TODAY" --variable "tomorrow=$TOMORROW" test.hurl
|
||||
```
|
||||
|
||||
## macOS
|
||||
|
||||
### How can I use a custom libcurl (from Homebrew by instance)?
|
||||
|
||||
No matter how you've installed Hurl (using the precompiled binary for macOS or with [Homebrew])
|
||||
Hurl is linked against the built-in system libcurl. If you want to use another libcurl (for instance,
|
||||
if you've installed curl with Homebrew and want Hurl to use Homebrew's libcurl), you can patch Hurl with
|
||||
the following command:
|
||||
|
||||
```shell
|
||||
sudo install_name_tool -change /usr/lib/libcurl.4.dylib PATH_TO_CUSTOM_LIBCURL PATH_TO_HURL_BIN
|
||||
```
|
||||
|
||||
For instance:
|
||||
|
||||
```shell
|
||||
# /usr/local/opt/curl/lib/libcurl.4.dylib is installed by `brew install curl`
|
||||
sudo install_name_tool -change /usr/lib/libcurl.4.dylib /usr/local/opt/curl/lib/libcurl.4.dylib /usr/local/bin/hurl
|
||||
```
|
||||
|
||||
### Hurl error: SSL certificate has expired
|
||||
|
||||
If you have a `SSL certificate has expired` error on valid certificates with Hurl, it can be due to the macOS libcurl certificates
|
||||
not updated. On Mojave, the built-in curl (`/usr/bin/curl`) relies on the `/etc/ssl/cert.pem` file for root CA verification,
|
||||
and some certificates has expired. To solve this problem:
|
||||
|
||||
1. Edit `/etc/ssl/cert.pem` and remove the expired certificate (for instance, the `DST Root CA X3` has expired)
|
||||
2. Use a recent curl (installed with Homebrew) and [configure Hurl to use it].
|
||||
|
||||
### Hurl warning on Big Sur: Closing connection 0
|
||||
|
||||
In Big Sur, the system version of libcurl (7.64.1), has a bug that [erroneously
|
||||
displays `* Closing connection 0` on `stderr`]. To fix Hurl not to output this
|
||||
warning, one can link Hurl to a newer version of libcurl.
|
||||
|
||||
For instance, to use the latest libcurl with Homebrew:
|
||||
|
||||
```shell
|
||||
$ brew install curl
|
||||
$ sudo install_name_tool -change /usr/lib/libcurl.4.dylib /usr/local/opt/curl/lib/libcurl.4.dylib /usr/local/bin/hurl
|
||||
```
|
||||
|
||||
[curl]: https://curl.haxx.se
|
||||
[other eminent tools]: https://git.wiki.kernel.org/index.php/GitFaq#Why_the_.27Git.27_name.3F
|
||||
[Postman]: https://www.postman.com
|
||||
[file format]: /docs/hurl-file.md
|
||||
[Karate]: https://github.com/intuit/karate
|
||||
[Selenium]: https://www.selenium.dev
|
||||
[captures]: /docs/capturing-response.md
|
||||
[CSRF tokens]: https://en.wikipedia.org/wiki/Cross-site_request_forgery
|
||||
[asserts]: /docs/asserting-response.md
|
||||
[configure Hurl to use it]: #how-can-i-use-a-custom-libcurl-from-homebrew-by-instance
|
||||
[Homebrew]: https://brew.sh
|
||||
[erroneously displays `* Closing connection 0` on `stderr`]: https://github.com/curl/curl/issues/3891
|
339
docs/grammar.md
Normal file
@ -0,0 +1,339 @@
|
||||
# Grammar
|
||||
|
||||
## Definitions
|
||||
|
||||
Short description:
|
||||
|
||||
- operator | denotes alternative,
|
||||
- operator * denotes iteration (zero or more),
|
||||
- operator + denotes iteration (one or more),
|
||||
|
||||
## Syntax Grammar
|
||||
|
||||
<div class="grammar">
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="hurl-file">hurl-file </div>
|
||||
<div class="tokens">= <a href="#entry">entry</a>*<br>
|
||||
<a href="#lt">lt</a>*</div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="entry">entry </div>
|
||||
<div class="tokens">= <a href="#request">request</a><br>
|
||||
<a href="#response">response</a>?</div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="request">request </div>
|
||||
<div class="tokens">= <a href="#lt">lt</a>*<br>
|
||||
<a href="#sp">sp</a>* <a href="#method">method</a> <a href="#sp">sp</a> <a href="#sp">sp</a>* <a href="#url">url</a> <a href="#lt">lt</a><br>
|
||||
<a href="#header">header</a>*<br>
|
||||
<a href="#request-section">request-section</a>*<br>
|
||||
<a href="#body">body</a>?</div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="response">response </div>
|
||||
<div class="tokens">= <a href="#lt">lt</a>*<br>
|
||||
<a href="#sp">sp</a>* <a href="#version">version</a> <a href="#sp">sp</a> <a href="#sp">sp</a>* <a href="#status">status</a> <a href="#lt">lt</a><br>
|
||||
<a href="#header">header</a>*<br>
|
||||
<a href="#response-section">response-section</a>*<br>
|
||||
<a href="#body">body</a>?</div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="method">method </div>
|
||||
<div class="tokens">= <span class="terminal">"GET"</span><br>
|
||||
| <span class="terminal">"HEAD"</span><br>
|
||||
| <span class="terminal">"POST"</span><br>
|
||||
| <span class="terminal">"PUT"</span><br>
|
||||
| <span class="terminal">"DELETE"</span><br>
|
||||
| <span class="terminal">"CONNECT"</span><br>
|
||||
| <span class="terminal">"OPTIONS"</span><br>
|
||||
| <span class="terminal">"TRACE"</span><br>
|
||||
| <span class="terminal">"PATCH"</span></div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="url">url </div>
|
||||
<div class="tokens">= <span class="definition"><(alphanum | ":" | "/" | "{" | "}" | "*" | "," | "@" | "]")+></span></div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="version">version </div>
|
||||
<div class="tokens">= <span class="terminal">"HTTP/1.0"</span> | <span class="terminal">"HTTP/1.1"</span> | <span class="terminal">"HTTP/2"</span> | <span class="terminal">"HTTP/*"</span></div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="status">status </div>
|
||||
<div class="tokens">= <span class="definition"><[0-9]+></span></div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="header">header </div>
|
||||
<div class="tokens">= <a href="#lt">lt</a>*<br>
|
||||
<a href="#sp">sp</a>* <a href="#key-value">key-value</a> <a href="#lt">lt</a></div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="body">body </div>
|
||||
<div class="tokens">= <a href="#lt">lt</a>*<br>
|
||||
<a href="#sp">sp</a>* <a href="#bytes">bytes</a> <a href="#lt">lt</a></div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="request-section">request-section </div>
|
||||
<div class="tokens">= <a href="#query-string-params-section">query-string-params-section</a><br>
|
||||
| <a href="#form-params-section">form-params-section</a><br>
|
||||
| <a href="#multipart-form-data-section">multipart-form-data-section</a><br>
|
||||
| <a href="#cookies-section">cookies-section</a></div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="response-section">response-section </div>
|
||||
<div class="tokens">= <a href="#captures-section">captures-section</a> | <a href="#asserts-section">asserts-section</a></div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="query-string-params-section">query-string-params-section </div>
|
||||
<div class="tokens">= <a href="#lt">lt</a>*<br>
|
||||
<a href="#sp">sp</a>* <span class="terminal">"[QueryStringParams]"</span> <a href="#lt">lt</a><br>
|
||||
<a href="#param">param</a>*</div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="form-params-section">form-params-section </div>
|
||||
<div class="tokens">= <a href="#lt">lt</a>*<br>
|
||||
<a href="#sp">sp</a>* <span class="terminal">"[FormParams]"</span> <a href="#lt">lt</a><br>
|
||||
<a href="#param">param</a>*</div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="multipart-form-data-section">multipart-form-data-section </div>
|
||||
<div class="tokens">= <a href="#lt">lt</a>*<br>
|
||||
<a href="#sp">sp</a>* <span class="terminal">"[MultipartFormData]"</span> <a href="#lt">lt</a><br>
|
||||
<a href="#multipart-form-data-param">multipart-form-data-param</a>*</div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="cookies-section">cookies-section </div>
|
||||
<div class="tokens">= <a href="#lt">lt</a>*<br>
|
||||
<a href="#sp">sp</a>* <span class="terminal">"[Cookies]"</span> <a href="#lt">lt</a><br>
|
||||
<a href="#cookie">cookie</a>*</div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="captures-section">captures-section </div>
|
||||
<div class="tokens">= <a href="#lt">lt</a>*<br>
|
||||
<a href="#sp">sp</a>* <span class="terminal">"[Captures]"</span> <a href="#lt">lt</a><br>
|
||||
<a href="#capture">capture</a>*</div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="asserts-section">asserts-section </div>
|
||||
<div class="tokens">= <a href="#lt">lt</a>*<br>
|
||||
<a href="#sp">sp</a>* <span class="terminal">"[Asserts]"</span> <a href="#lt">lt</a><br>
|
||||
<a href="#assert">assert</a>*</div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="param">param </div>
|
||||
<div class="tokens">= <a href="#lt">lt</a>*<br>
|
||||
<a href="#sp">sp</a>* <a href="#key-value">key-value</a> <a href="#lt">lt</a></div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="multipart-form-data-param">multipart-form-data-param </div>
|
||||
<div class="tokens">= <a href="#file-param">file-param</a> | <a href="#param">param</a></div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="file-param">file-param </div>
|
||||
<div class="tokens">= <a href="#lt">lt</a>*<br>
|
||||
<a href="#sp">sp</a>* <a href="#key-string">key-string</a> <a href="#sp">sp</a>* <span class="terminal">":"</span> <a href="#sp">sp</a>* <a href="#file-value">file-value</a> <a href="#lt">lt</a></div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="file-value">file-value </div>
|
||||
<div class="tokens">= <span class="terminal">"file,"</span> <a href="#sp">sp</a>* <a href="#filename">filename</a> <a href="#sp">sp</a>* <span class="terminal">";"</span> (<a href="#sp">sp</a>* <a href="#file-contenttype">file-contenttype</a>)?</div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="file-contenttype">file-contenttype </div>
|
||||
<div class="tokens">= <span class="definition"><(alphanum | "/" | ";" | "=" | " ")+ without leading/trailing space></span></div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="cookie">cookie </div>
|
||||
<div class="tokens">= <a href="#lt">lt</a>*<br>
|
||||
<a href="#sp">sp</a>* <a href="#key-string">key-string</a> <a href="#sp">sp</a>* <span class="terminal">":"</span> <a href="#sp">sp</a>* <a href="#cookie-value">cookie-value</a> <a href="#lt">lt</a></div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="cookie-value">cookie-value </div>
|
||||
<div class="tokens">= <span class="definition"><(alphanum | "!" | "#" | "$" | "%" | "&" | "'" | "(" | ")" | "*" | "+"
|
||||
| "-" | "." | "/" | ":" | "<" | "=" | ">" | "?" | "@" | "["
|
||||
| "]" | "^" | "_" | "`" | "~" )* ></span></div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="capture">capture </div>
|
||||
<div class="tokens">= <a href="#lt">lt</a>*<br>
|
||||
<a href="#sp">sp</a>* <a href="#key-string">key-string</a> <a href="#sp">sp</a>* <span class="terminal">":"</span> <a href="#sp">sp</a>* <a href="#query">query</a> <a href="#lt">lt</a></div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="query">query </div>
|
||||
<div class="tokens">= <a href="#main-query">main-query</a> (<a href="#sp">sp</a> <a href="#sp">sp</a>* <a href="#subquery">subquery</a>)?</div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="main-query">main-query </div>
|
||||
<div class="tokens">= <a href="#status-query">status-query</a><br>
|
||||
| <a href="#header-query">header-query</a><br>
|
||||
| <a href="#cookie-query">cookie-query</a><br>
|
||||
| <a href="#body-query">body-query</a><br>
|
||||
| <a href="#xpath-query">xpath-query</a><br>
|
||||
| <a href="#jsonpath-query">jsonpath-query</a><br>
|
||||
| <a href="#regex-query">regex-query</a><br>
|
||||
| <a href="#variable-query">variable-query</a><br>
|
||||
| <a href="#duration-query">duration-query</a><br>
|
||||
| <a href="#bytes-query">bytes-query</a><br>
|
||||
| <a href="#sha256-query">sha256-query</a></div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="status-query">status-query </div>
|
||||
<div class="tokens">= <span class="terminal">"status"</span></div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="header-query">header-query </div>
|
||||
<div class="tokens">= <span class="terminal">"header"</span> <a href="#sp">sp</a> <a href="#sp">sp</a>* <a href="#quoted-string">quoted-string</a></div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="cookie-query">cookie-query </div>
|
||||
<div class="tokens">= <span class="terminal">"cookie"</span> <a href="#sp">sp</a> <a href="#sp">sp</a>* <a href="#quoted-string">quoted-string</a></div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="body-query">body-query </div>
|
||||
<div class="tokens">= <span class="terminal">"body"</span></div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="xpath-query">xpath-query </div>
|
||||
<div class="tokens">= <span class="terminal">"xpath"</span> <a href="#sp">sp</a> <a href="#sp">sp</a>* <a href="#quoted-string">quoted-string</a></div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="jsonpath-query">jsonpath-query </div>
|
||||
<div class="tokens">= <span class="terminal">"jsonpath"</span> <a href="#sp">sp</a> <a href="#sp">sp</a>* <a href="#quoted-string">quoted-string</a></div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="regex-query">regex-query </div>
|
||||
<div class="tokens">= <span class="terminal">"regex"</span> <a href="#sp">sp</a> <a href="#sp">sp</a>* <a href="#quoted-string">quoted-string</a></div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="variable-query">variable-query </div>
|
||||
<div class="tokens">= <span class="terminal">"variable"</span> <a href="#sp">sp</a> <a href="#sp">sp</a>* <a href="#quoted-string">quoted-string</a></div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="duration">duration </div>
|
||||
<div class="tokens">= <span class="terminal">"duration"</span></div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="sha256-query">sha256-query </div>
|
||||
<div class="tokens">= <span class="terminal">"sha256"</span></div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="bytes-query">bytes-query </div>
|
||||
<div class="tokens">= <span class="terminal">"bytes"</span></div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="subquery">subquery </div>
|
||||
<div class="tokens">= <a href="#regex-subquery">regex-subquery</a> | <a href="#count-subquery">count-subquery</a></div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="regex-subquery">regex-subquery </div>
|
||||
<div class="tokens">= <span class="terminal">"regex"</span> <a href="#sp">sp</a> <a href="#sp">sp</a>* <a href="#quoted-string">quoted-string</a></div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="count-subquery">count-subquery </div>
|
||||
<div class="tokens">= <span class="terminal">"count"</span></div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="assert">assert </div>
|
||||
<div class="tokens">= <a href="#lt">lt</a>*<br>
|
||||
<a href="#sp">sp</a>* <a href="#query">query</a> <a href="#sp">sp</a> <a href="#sp">sp</a>* <a href="#predicate">predicate</a> <a href="#lt">lt</a></div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="predicate">predicate </div>
|
||||
<div class="tokens">= (<span class="terminal">"not"</span> <a href="#sp">sp</a> <a href="#sp">sp</a>*)? <a href="#predicate-func">predicate-func</a></div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="predicate-func">predicate-func </div>
|
||||
<div class="tokens">= <a href="#equal-predicate">equal-predicate</a><br>
|
||||
| <a href="#not-equal-predicate">not-equal-predicate</a><br>
|
||||
| <a href="#greater-predicate">greater-predicate</a><br>
|
||||
| <a href="#greater-or-equal-predicate">greater-or-equal-predicate</a><br>
|
||||
| <a href="#less-predicate">less-predicate</a><br>
|
||||
| <a href="#less-or-equal-predicate">less-or-equal-predicate</a><br>
|
||||
| <a href="#start-with-predicate">start-with-predicate</a><br>
|
||||
| <a href="#end-with-predicate">end-with-predicate</a><br>
|
||||
| <a href="#contain-predicate">contain-predicate</a><br>
|
||||
| <a href="#match-predicate">match-predicate</a><br>
|
||||
| <a href="#exist-predicate">exist-predicate</a><br>
|
||||
| <a href="#include-predicate">include-predicate</a><br>
|
||||
| <a href="#integer-predicate">integer-predicate</a><br>
|
||||
| <a href="#float-predicate">float-predicate</a><br>
|
||||
| <a href="#boolean-predicate">boolean-predicate</a><br>
|
||||
| <a href="#string-predicate">string-predicate</a><br>
|
||||
| <a href="#collection-predicate">collection-predicate</a></div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="equal-predicate">equal-predicate </div>
|
||||
<div class="tokens">= (<span class="terminal">"equals"</span> | <span class="terminal">"=="</span>) <a href="#sp">sp</a> <a href="#sp">sp</a>* (<a href="#null">null</a> | <a href="#float">float</a> | <a href="#integer">integer</a> | <a href="#boolean">boolean</a> | <a href="#quoted-string">quoted-string</a> | <a href="#raw-string">raw-string</a> | <a href="#hex">hex</a> | <a href="#base64">base64</a> | <a href="#expr">expr</a>)</div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="not-equal-predicate">not-equal-predicate </div>
|
||||
<div class="tokens">= (<span class="terminal">"notEquals"</span> | <span class="terminal">"!="</span>) <a href="#sp">sp</a> <a href="#sp">sp</a>* (<a href="#null">null</a> | <a href="#float">float</a> | <a href="#integer">integer</a> | <a href="#boolean">boolean</a> | <a href="#quoted-string">quoted-string</a> | <a href="#raw-string">raw-string</a> | <a href="#hex">hex</a> | <a href="#base64">base64</a> | <a href="#expr">expr</a>)</div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="greater-predicate">greater-predicate </div>
|
||||
<div class="tokens">= (<span class="terminal">"greaterThan"</span> | <span class="terminal">">"</span>) <a href="#sp">sp</a> <a href="#sp">sp</a>* (<a href="#integer">integer</a> | <a href="#float">float</a>)</div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="greater-or-equal-predicate">greater-or-equal-predicate </div>
|
||||
<div class="tokens">= (<span class="terminal">"greaterThanOrEquals"</span> | <span class="terminal">">="</span>) <a href="#sp">sp</a> <a href="#sp">sp</a>* (<a href="#integer">integer</a> | <a href="#float">float</a>)</div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="less-predicate">less-predicate </div>
|
||||
<div class="tokens">= (<span class="terminal">"lessThan"</span> | <span class="terminal">"<"</span>) <a href="#sp">sp</a> <a href="#sp">sp</a>* (<a href="#integer">integer</a> | <a href="#float">float</a>)</div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="less-or-equal-predicate">less-or-equal-predicate </div>
|
||||
<div class="tokens">= (<span class="terminal">"lessThanOrEquals"</span> | <span class="terminal">"<="</span>) <a href="#sp">sp</a> <a href="#sp">sp</a>* (<a href="#integer">integer</a> | <a href="#float">float</a>)</div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="start-with-predicate">start-with-predicate </div>
|
||||
<div class="tokens">= <span class="terminal">"startsWith"</span> <a href="#sp">sp</a> <a href="#sp">sp</a>* (<a href="#quoted-string">quoted-string</a> | <a href="#hex">hex</a> | <a href="#base64">base64</a>)</div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="end-with-predicate">end-with-predicate </div>
|
||||
<div class="tokens">= <span class="terminal">"endsWith"</span> <a href="#sp">sp</a> <a href="#sp">sp</a>* (<a href="#quoted-string">quoted-string</a> | <a href="#hex">hex</a> | <a href="#base64">base64</a>)</div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="contain-predicate">contain-predicate </div>
|
||||
<div class="tokens">= <span class="terminal">"contains"</span> <a href="#sp">sp</a> <a href="#sp">sp</a>* <a href="#quoted-string">quoted-string</a></div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="match-predicate">match-predicate </div>
|
||||
<div class="tokens">= <span class="terminal">"matches"</span> <a href="#sp">sp</a> <a href="#sp">sp</a>* <a href="#quoted-string">quoted-string</a></div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="exist-predicate">exist-predicate </div>
|
||||
<div class="tokens">= <span class="terminal">"exists"</span></div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="include-predicate">include-predicate </div>
|
||||
<div class="tokens">= <span class="terminal">"includes"</span> <a href="#sp">sp</a> <a href="#sp">sp</a>* (<a href="#null">null</a> |<a href="#float">float</a> | <a href="#integer">integer</a> | <a href="#boolean">boolean</a> | <a href="#quoted-string">quoted-string</a> | <a href="#expr">expr</a>)</div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="integer-predicate">integer-predicate </div>
|
||||
<div class="tokens">= <span class="terminal">"isInteger"</span></div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="float-predicate">float-predicate </div>
|
||||
<div class="tokens">= <span class="terminal">"isFloat"</span></div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="boolean-predicate">boolean-predicate </div>
|
||||
<div class="tokens">= <span class="terminal">"isBoolean"</span></div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="string-predicate">string-predicate </div>
|
||||
<div class="tokens">= <span class="terminal">"isString"</span></div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="collection-predicate">collection-predicate </div>
|
||||
<div class="tokens">= <span class="terminal">"isCollection"</span></div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="key-value">key-value </div>
|
||||
<div class="tokens">= <a href="#key-string">key-string</a> <a href="#sp">sp</a>* <span class="terminal">":"</span> <a href="#sp">sp</a>* <a href="#value-string">value-string</a></div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="key-string">key-string </div>
|
||||
<div class="tokens">= <span class="definition"><(alphanum | "_" | "-" | "." | escape-char)+ ></span></div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="value-string">value-string </div>
|
||||
<div class="tokens">= <span class="definition"><(anychar except escaped char and #| escape-char)* without leading/trailing space></span></div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="quoted-string">quoted-string </div>
|
||||
<div class="tokens">= <span class="terminal">"""</span> <span class="definition"><(anychar except escaped char | escape-char)*></span> <span class="terminal">"""</span></div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="expr">expr </div>
|
||||
<div class="tokens">= <span class="terminal">"{{"</span> <a href="#sp">sp</a>* <a href="#variable-name">variable-name</a> <a href="#sp">sp</a>* <span class="terminal">"}}"</span></div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="variable-name">variable-name </div>
|
||||
<div class="tokens">= <span class="definition"><(alphanum | "_" )+></span></div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="escaped-char">escaped-char </div>
|
||||
<div class="tokens">= <span class="terminal">"\""</span><br>
|
||||
| <span class="terminal">"\\"</span><br>
|
||||
| <span class="terminal">"\b"</span><br>
|
||||
| <span class="terminal">"\f"</span><br>
|
||||
| <span class="terminal">"\n"</span><br>
|
||||
| <span class="terminal">"\r"</span><br>
|
||||
| <span class="terminal">"\t"</span><br>
|
||||
| <span class="terminal">"\u"</span> <a href="#unicode-char">unicode-char</a></div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="unicode-char">unicode-char </div>
|
||||
<div class="tokens">= <span class="terminal">"{"</span> <a href="#hexdigit">hexdigit</a>+ <span class="terminal">"}"</span></div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="bytes">bytes </div>
|
||||
<div class="tokens">= <a href="#json">json</a><br>
|
||||
| <a href="#xml">xml</a><br>
|
||||
| <a href="#raw-string">raw-string</a><br>
|
||||
| <a href="#base64">base64</a><br>
|
||||
| <a href="#file">file</a><br>
|
||||
| <a href="#hex">hex</a></div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="raw-string">raw-string </div>
|
||||
<div class="tokens">= <span class="terminal">"```"</span> (<a href="#sp">sp</a>* <a href="#newline">newline</a>)? (<a href="#any">any</a> <a href="#characters">characters</a>) <span class="terminal">"```"</span></div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="base64">base64 </div>
|
||||
<div class="tokens">= <span class="terminal">"base64,"</span> <span class="definition"><base64 encoding with optional whitesp/padding></span> <span class="terminal">";"</span></div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="file">file </div>
|
||||
<div class="tokens">= <span class="terminal">"file,"</span> <a href="#sp">sp</a>* <a href="#filename">filename</a> <a href="#sp">sp</a>* <span class="terminal">";"</span></div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="hex">hex </div>
|
||||
<div class="tokens">= <span class="terminal">"hex,"</span> <a href="#sp">sp</a>* <a href="#hexdigit">hexdigit</a>* <a href="#sp">sp</a>* <span class="terminal">";"</span></div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="lt">lt </div>
|
||||
<div class="tokens">= <a href="#sp">sp</a>* <a href="#comment">comment</a>? (<a href="#newline">newline</a> | <a href="#eof">eof</a>)</div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="comment">comment </div>
|
||||
<div class="tokens">= <span class="terminal">"#"</span> <span class="definition"><any characters except newline - does not end with sp></span></div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="newline">newline </div>
|
||||
<div class="tokens">= <span class="terminal">"\n"</span> | <span class="terminal">"\r\n"</span></div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="sp">sp </div>
|
||||
<div class="tokens">= <span class="terminal">" "</span> | <span class="terminal">"\t"</span></div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="filename">filename </div>
|
||||
<div class="tokens">= <span class="definition"><(alphanum | ".")+></span></div></div>
|
||||
<div class="rule">
|
||||
<div class="non-terminal" id="integer">integer </div>
|
||||
<div class="tokens">= <span class="definition"><-?[1-9][0-9]*></span></div></div>
|
||||
|
||||
</div>
|
||||
|
259
docs/grammar/hurl.grammar
Normal file
@ -0,0 +1,259 @@
|
||||
hurl-file = entry*
|
||||
lt*
|
||||
|
||||
entry = request
|
||||
response?
|
||||
|
||||
request = lt*
|
||||
sp* method sp sp* url lt
|
||||
header*
|
||||
request-section*
|
||||
body?
|
||||
|
||||
response = lt*
|
||||
sp* version sp sp* status lt
|
||||
header*
|
||||
response-section*
|
||||
body?
|
||||
|
||||
method = "GET"
|
||||
| "HEAD"
|
||||
| "POST"
|
||||
| "PUT"
|
||||
| "DELETE"
|
||||
| "CONNECT"
|
||||
| "OPTIONS"
|
||||
| "TRACE"
|
||||
| "PATCH"
|
||||
|
||||
url = <(alphanum | ":" | "/" | "{" | "}" | "*" | "," | "@" | "]")+>
|
||||
|
||||
version = "HTTP/1.0" | "HTTP/1.1" | "HTTP/2" | "HTTP/*"
|
||||
|
||||
status = <[0-9]+>
|
||||
|
||||
header = lt*
|
||||
sp* key-value lt
|
||||
|
||||
body = lt*
|
||||
sp* bytes lt
|
||||
|
||||
|
||||
# Sections
|
||||
|
||||
# a section can not be repeated
|
||||
|
||||
request-section = query-string-params-section
|
||||
| form-params-section
|
||||
| multipart-form-data-section
|
||||
| cookies-section
|
||||
|
||||
# a section can not be duplicated
|
||||
|
||||
response-section = captures-section | asserts-section
|
||||
|
||||
query-string-params-section = lt*
|
||||
sp* "[QueryStringParams]" lt
|
||||
param*
|
||||
|
||||
form-params-section = lt*
|
||||
sp* "[FormParams]" lt
|
||||
param*
|
||||
|
||||
multipart-form-data-section = lt*
|
||||
sp* "[MultipartFormData]" lt
|
||||
multipart-form-data-param*
|
||||
|
||||
cookies-section = lt*
|
||||
sp* "[Cookies]" lt
|
||||
cookie*
|
||||
|
||||
captures-section = lt*
|
||||
sp* "[Captures]" lt
|
||||
capture*
|
||||
|
||||
asserts-section = lt*
|
||||
sp* "[Asserts]" lt
|
||||
assert*
|
||||
|
||||
param = lt*
|
||||
sp* key-value lt
|
||||
|
||||
multipart-form-data-param = file-param | param
|
||||
|
||||
file-param = lt*
|
||||
sp* key-string sp* ":" sp* file-value lt
|
||||
|
||||
file-value = "file," sp* filename sp* ";" (sp* file-contenttype)?
|
||||
|
||||
file-contenttype = <(alphanum | "/" | ";" | "=" | " ")+ without leading/trailing space>
|
||||
|
||||
cookie = lt*
|
||||
sp* key-string sp* ":" sp* cookie-value lt
|
||||
|
||||
cookie-value = <(alphanum | "!" | "#" | "$" | "%" | "&" | "'" | "(" | ")" | "*" | "+"
|
||||
| "-" | "." | "/" | ":" | "<" | "=" | ">" | "?" | "@" | "["
|
||||
| "]" | "^" | "_" | "`" | "~" )* >
|
||||
|
||||
|
||||
capture = lt*
|
||||
sp* key-string sp* ":" sp* query lt
|
||||
|
||||
query = main-query (sp sp* subquery)?
|
||||
|
||||
main-query = status-query
|
||||
| header-query
|
||||
| cookie-query
|
||||
| body-query
|
||||
| xpath-query
|
||||
| jsonpath-query
|
||||
| regex-query
|
||||
| variable-query
|
||||
| duration-query
|
||||
| bytes-query
|
||||
| sha256-query
|
||||
|
||||
status-query = "status"
|
||||
|
||||
header-query = "header" sp sp* quoted-string
|
||||
|
||||
cookie-query = "cookie" sp sp* quoted-string
|
||||
|
||||
body-query = "body"
|
||||
|
||||
xpath-query = "xpath" sp sp* quoted-string
|
||||
|
||||
jsonpath-query = "jsonpath" sp sp* quoted-string
|
||||
|
||||
regex-query = "regex" sp sp* quoted-string
|
||||
|
||||
variable-query = "variable" sp sp* quoted-string
|
||||
|
||||
duration = "duration"
|
||||
|
||||
sha256-query = "sha256"
|
||||
|
||||
bytes-query = "bytes"
|
||||
|
||||
subquery = regex-subquery | count-subquery
|
||||
|
||||
regex-subquery = "regex" sp sp* quoted-string
|
||||
|
||||
count-subquery = "count"
|
||||
|
||||
|
||||
assert = lt*
|
||||
sp* query sp sp* predicate lt
|
||||
|
||||
|
||||
predicate = ("not" sp sp*)? predicate-func
|
||||
|
||||
predicate-func = equal-predicate
|
||||
| not-equal-predicate
|
||||
| greater-predicate
|
||||
| greater-or-equal-predicate
|
||||
| less-predicate
|
||||
| less-or-equal-predicate
|
||||
| start-with-predicate
|
||||
| end-with-predicate
|
||||
| contain-predicate
|
||||
| match-predicate
|
||||
| exist-predicate
|
||||
| include-predicate
|
||||
| integer-predicate
|
||||
| float-predicate
|
||||
| boolean-predicate
|
||||
| string-predicate
|
||||
| collection-predicate
|
||||
|
||||
|
||||
equal-predicate = ("equals" | "==") sp sp* (null | float | integer | boolean | quoted-string | raw-string | hex | base64 | expr)
|
||||
|
||||
not-equal-predicate = ("notEquals" | "!=") sp sp* (null | float | integer | boolean | quoted-string | raw-string | hex | base64 | expr)
|
||||
|
||||
greater-predicate = ("greaterThan" | ">") sp sp* (integer | float)
|
||||
|
||||
greater-or-equal-predicate = ("greaterThanOrEquals" | ">=") sp sp* (integer | float)
|
||||
|
||||
less-predicate = ("lessThan" | "<") sp sp* (integer | float)
|
||||
|
||||
less-or-equal-predicate = ("lessThanOrEquals" | "<=") sp sp* (integer | float)
|
||||
|
||||
start-with-predicate = "startsWith" sp sp* (quoted-string | hex | base64)
|
||||
|
||||
end-with-predicate = "endsWith" sp sp* (quoted-string | hex | base64)
|
||||
|
||||
contain-predicate = "contains" sp sp* quoted-string
|
||||
|
||||
match-predicate = "matches" sp sp* quoted-string
|
||||
|
||||
exist-predicate = "exists"
|
||||
|
||||
include-predicate = "includes" sp sp* (null |float | integer | boolean | quoted-string | expr)
|
||||
|
||||
integer-predicate = "isInteger"
|
||||
|
||||
float-predicate = "isFloat"
|
||||
|
||||
boolean-predicate = "isBoolean"
|
||||
|
||||
string-predicate = "isString"
|
||||
|
||||
collection-predicate = "isCollection"
|
||||
|
||||
|
||||
# Primitives
|
||||
|
||||
key-value = key-string sp* ":" sp* value-string
|
||||
|
||||
key-string = <(alphanum | "_" | "-" | "." | escape-char)+ >
|
||||
|
||||
value-string = <(anychar except escaped char and #| escape-char)* without leading/trailing space>
|
||||
|
||||
quoted-string = """ <(anychar except escaped char | escape-char)*> """
|
||||
|
||||
expr = "{{" sp* variable-name sp* "}}"
|
||||
|
||||
variable-name = <(alphanum | "_" )+>
|
||||
|
||||
escaped-char = "\""
|
||||
| "\\"
|
||||
| "\b"
|
||||
| "\f"
|
||||
| "\n"
|
||||
| "\r"
|
||||
| "\t"
|
||||
| "\u" unicode-char
|
||||
|
||||
unicode-char = "{" hexdigit+ "}"
|
||||
|
||||
|
||||
bytes = json
|
||||
| xml
|
||||
| raw-string
|
||||
| base64
|
||||
| file
|
||||
| hex
|
||||
|
||||
raw-string = "```" (sp* newline)? (any characters) "```"
|
||||
|
||||
base64 = "base64," <base64 encoding with optional whitesp/padding> ";"
|
||||
|
||||
file = "file," sp* filename sp* ";"
|
||||
|
||||
hex = "hex," sp* hexdigit* sp* ";"
|
||||
|
||||
lt = sp* comment? (newline | eof)
|
||||
|
||||
comment = "#" <any characters except newline - does not end with sp>
|
||||
|
||||
newline = "\n" | "\r\n"
|
||||
|
||||
sp = " " | "\t"
|
||||
|
||||
filename = <(alphanum | ".")+>
|
||||
|
||||
integer = <-?[1-9][0-9]*>
|
||||
|
||||
|
||||
|
160
docs/home.md
Normal file
@ -0,0 +1,160 @@
|
||||
<div class="home-logo">
|
||||
<img class="light-img" src="/docs/assets/img/logo-light.svg" width="277px" height="72px" alt="Hurl logo"/>
|
||||
<img class="dark-img" src="/docs/assets/img/logo-dark.svg" width="277px" height="72px" alt="Hurl logo"/>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
# What's Hurl?
|
||||
|
||||
Hurl is a command line tool that runs <b>HTTP requests</b> defined in a simple <b>plain text format</b>.
|
||||
|
||||
It can perform requests, capture values and evaluate queries on headers and body response. Hurl is very
|
||||
versatile: it can be used for both <b>fetching data</b> and <b>testing HTTP</b> sessions.
|
||||
|
||||
```hurl
|
||||
# Get home:
|
||||
GET https://example.org
|
||||
|
||||
HTTP/1.1 200
|
||||
[Captures]
|
||||
csrf_token: xpath "string(//meta[@name='_csrf_token']/@content)"
|
||||
|
||||
# Do login!
|
||||
POST https://example.org/login?user=toto&password=1234
|
||||
X-CSRF-TOKEN: {{csrf_token}}
|
||||
|
||||
HTTP/1.1 302
|
||||
```
|
||||
|
||||
Chaining multiple requests is easy:
|
||||
|
||||
```hurl
|
||||
GET https://example.org/api/health
|
||||
GET https://example.org/api/step1
|
||||
GET https://example.org/api/step2
|
||||
GET https://example.org/api/step3
|
||||
```
|
||||
|
||||
# Also an HTTP Test Tool
|
||||
|
||||
Hurl can run HTTP requests but can also be used to <b>test HTTP responses</b>.
|
||||
Different types of queries and predicates are supported, from [XPath] and [JSONPath] on body response,
|
||||
to assert on status code and response headers.
|
||||
|
||||
It is well adapted for <b>REST / JSON apis</b>
|
||||
|
||||
```hurl
|
||||
POST https://example.org/api/tests
|
||||
{
|
||||
"id": "4568",
|
||||
"evaluate": true
|
||||
}
|
||||
|
||||
HTTP/1.1 200
|
||||
[Asserts]
|
||||
header "X-Frame-Options" == "SAMEORIGIN"
|
||||
jsonpath "$.status" == "RUNNING" # Check the status code
|
||||
jsonpath "$.tests" count == 25 # Check the number of items
|
||||
jsonpath "$.id" matches /\d{4}/ # Check the format of the id
|
||||
```
|
||||
|
||||
<b>HTML content</b>
|
||||
|
||||
```hurl
|
||||
GET https://example.org
|
||||
|
||||
HTTP/1.1 200
|
||||
[Asserts]
|
||||
xpath "normalize-space(//head/title)" == "Hello world!"
|
||||
```
|
||||
|
||||
and even SOAP apis
|
||||
|
||||
```hurl
|
||||
POST https://example.org/InStock
|
||||
Content-Type: application/soap+xml; charset=utf-8
|
||||
SOAPAction: "http://www.w3.org/2003/05/soap-envelope"
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:m="https://example.org">
|
||||
<soap:Header></soap:Header>
|
||||
<soap:Body>
|
||||
<m:GetStockPrice>
|
||||
<m:StockName>GOOG</m:StockName>
|
||||
</m:GetStockPrice>
|
||||
</soap:Body>
|
||||
</soap:Envelope>
|
||||
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
Hurl can also be used to test HTTP endpoints performances:
|
||||
|
||||
```hurl
|
||||
GET https://example.org/api/v1/pets
|
||||
|
||||
HTTP/1.0 200
|
||||
[Asserts]
|
||||
duration < 1000 # Duration in ms
|
||||
```
|
||||
|
||||
And responses bytes content
|
||||
|
||||
```hurl
|
||||
GET https://example.org/data.tar.gz
|
||||
|
||||
HTTP/1.0 200
|
||||
[Asserts]
|
||||
sha256 == hex,039058c6f2c0cb492c533b0a4d14ef77cc0f78abccced5287d84a1a2011cfb81;
|
||||
```
|
||||
|
||||
|
||||
# Why Hurl?
|
||||
|
||||
<ul class="showcase-container">
|
||||
<li class="showcase-item"><h2 class="showcase-item-title">Text Format</h2>For both devops and developers</li>
|
||||
<li class="showcase-item"><h2 class="showcase-item-title">Fast CLI</h2>A command line for local dev and continuous integration</li>
|
||||
<li class="showcase-item"><h2 class="showcase-item-title">Single Binary</h2>Easy to install, with no runtime required</li>
|
||||
</ul>
|
||||
|
||||
# Powered by curl
|
||||
|
||||
Hurl is a lightweight binary written in [Rust]. Under the hood, Hurl HTTP engine is
|
||||
powered by [libcurl], one of the most powerful and reliable file transfer library.
|
||||
With its text file format, Hurl adds syntactic sugar to run and tests HTTP requests,
|
||||
but it's still the [curl] that we love.
|
||||
|
||||
# Feedbacks
|
||||
|
||||
[Feedback, suggestion, bugs or improvements] are welcome!
|
||||
|
||||
```hurl
|
||||
POST https://hurl.dev/api/feedback
|
||||
{
|
||||
"name": "John Doe",
|
||||
"feedback": "Hurl is awesome !"
|
||||
}
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
# Resources
|
||||
|
||||
[License]
|
||||
|
||||
[Blog]
|
||||
|
||||
[Documentation]
|
||||
|
||||
[GitHub]
|
||||
|
||||
[XPath]: https://en.wikipedia.org/wiki/XPath
|
||||
[JSONPath]: https://goessner.net/articles/JsonPath/
|
||||
[Rust]: https://www.rust-lang.org
|
||||
[curl]: https://curl.se
|
||||
[the installation section]: /docs/installation.md
|
||||
[Feedback, suggestion, bugs or improvements]: https://github.com/Orange-OpenSource/hurl/issues
|
||||
[License]: /docs/license.md
|
||||
[Documentation]: /docs/installation.md
|
||||
[Blog]: blog.md
|
||||
[GitHub]: https://github.com/Orange-OpenSource/hurl
|
||||
[libcurl]: https://curl.se/libcurl/
|
59
docs/hurl-file.md
Normal file
@ -0,0 +1,59 @@
|
||||
# Hurl File
|
||||
|
||||
## Character Encoding
|
||||
|
||||
Hurl file should be encoded in UTF-8, without byte order mark to the beginning
|
||||
(while Hurl ignores the presence of a byte order mark
|
||||
rather than treating it as an error)
|
||||
|
||||
## File Extension
|
||||
|
||||
Hurl file extension is `.hurl`
|
||||
|
||||
## Comments
|
||||
|
||||
Comments begin with `#` and continue until the end of line. Hurl file can serve as
|
||||
a documentation for HTTP based workflows so it can be useful to be very descriptive.
|
||||
|
||||
```hurl
|
||||
# A very simple Hurl file
|
||||
# with tasty comments...
|
||||
GET https://www.sample.net
|
||||
x-app: MY_APP # Add a dummy header
|
||||
|
||||
HTTP/1.1 302 # Check that we have a redirection
|
||||
[Asserts]
|
||||
header "Location" exists
|
||||
header "Location" contains "login" # Check that we are redirected to the login page
|
||||
```
|
||||
|
||||
## Special Characters in Strings
|
||||
|
||||
String can include the following special characters:
|
||||
|
||||
- The escaped special characters \" (double quotation mark), \\ (backslash), \b (backspace), \f (form feed),
|
||||
\n (line feed), \r (carriage return), and \t (horizontal tab)
|
||||
- An arbitrary Unicode scalar value, written as \u{n}, where n is a 1–8 digit hexadecimal number
|
||||
|
||||
```hurl
|
||||
GET https://example.org/api
|
||||
|
||||
HTTP/1.1 200
|
||||
|
||||
# The following assert are equivalent:
|
||||
[Asserts]
|
||||
jsonpath "$.slideshow.title" == "A beautiful ✈!"
|
||||
jsonpath "$.slideshow.title" == "A beautiful \u{2708}!"
|
||||
```
|
||||
|
||||
In some case, (in headers value, etc..), you will also need to escape # to distinguish from a comment.
|
||||
In the following example:
|
||||
|
||||
```hurl
|
||||
GET https://example.org/api
|
||||
x-token: BEEF \#STEACK # Some somment
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
We're sending a header `x-token` with value `BEEF #STEACK`
|
||||
|
20
docs/index.md
Normal file
@ -0,0 +1,20 @@
|
||||
# Documentation
|
||||
|
||||
## [Getting Started]
|
||||
|
||||
## [File Format]
|
||||
|
||||
## [Tutorial]
|
||||
|
||||
## [Resources]
|
||||
|
||||
|
||||
[Getting Started]: /docs/man-page.md
|
||||
[File Format]: /docs/hurl-file.md
|
||||
[Tutorial]: /docs/tutorial/your-first-hurl-file.md
|
||||
[Resources]: /docs/license.md
|
||||
|
||||
|
||||
|
||||
|
||||
|
160
docs/installation.md
Normal file
@ -0,0 +1,160 @@
|
||||
# Installation
|
||||
|
||||
## Binaries Installation
|
||||
|
||||
### Linux
|
||||
|
||||
Precompiled binary is available at [hurl-1.6.1-x86_64-linux.tar.gz]:
|
||||
|
||||
```shell
|
||||
$ INSTALL_DIR=/tmp
|
||||
$ curl -sL https://github.com/Orange-OpenSource/hurl/releases/download/1.6.1/hurl-1.6.1-x86_64-linux.tar.gz | tar xvz -C $INSTALL_DIR
|
||||
$ export PATH=$INSTALL_DIR/hurl-1.6.1:$PATH
|
||||
```
|
||||
|
||||
#### Debian / Ubuntu
|
||||
|
||||
For Debian / Ubuntu, Hurl can be installed using a binary .deb file provided in each Hurl release.
|
||||
|
||||
```shell
|
||||
$ curl -LO https://github.com/Orange-OpenSource/hurl/releases/download/1.6.1/hurl_1.6.1_amd64.deb
|
||||
$ sudo dpkg -i hurl_1.6.1_amd64.deb
|
||||
```
|
||||
|
||||
#### Arch Linux / Manjaro
|
||||
|
||||
[`hurl-bin` package] for Arch Linux and derived distros is available via [AUR].
|
||||
|
||||
#### NixOS / Nix
|
||||
|
||||
[NixOS / Nix package] is available on stable channel.
|
||||
|
||||
### macOS
|
||||
|
||||
Precompiled binary is available at [hurl-1.6.1-x86_64-osx.tar.gz].
|
||||
|
||||
Hurl can also be installed with [Homebrew]:
|
||||
|
||||
```shell
|
||||
$ brew install hurl
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
||||
#### Zip File
|
||||
|
||||
Hurl can be installed from a standalone zip file [hurl-1.6.1-win64.zip]. You will need to update your `PATH` variable.
|
||||
|
||||
#### Installer
|
||||
|
||||
An installer [hurl-1.6.1-win64-installer.exe] is also available.
|
||||
|
||||
#### Chocolatey
|
||||
|
||||
```shell
|
||||
$ choco install hurl
|
||||
```
|
||||
|
||||
#### Scoop
|
||||
|
||||
```shell
|
||||
$ scoop install hurl
|
||||
```
|
||||
|
||||
#### Windows Package Manager
|
||||
|
||||
```shell
|
||||
$ winget install hurl
|
||||
```
|
||||
|
||||
### Cargo
|
||||
|
||||
If you're a Rust programmer, Hurl can be installed with cargo.
|
||||
|
||||
```shell
|
||||
$ cargo install hurl
|
||||
```
|
||||
|
||||
### Docker
|
||||
|
||||
```shell
|
||||
$ docker pull orangeopensource/hurl
|
||||
```
|
||||
|
||||
### npm
|
||||
|
||||
```shell
|
||||
$ npm install --save-dev @orangeopensource/hurl
|
||||
```
|
||||
|
||||
## Building From Sources
|
||||
|
||||
Hurl sources are available in [GitHub].
|
||||
|
||||
### Build on Linux, macOS
|
||||
|
||||
Hurl depends on libssl, libcurl and libxml2 native libraries. You will need their development files in your platform.
|
||||
|
||||
|
||||
#### Debian based distributions
|
||||
|
||||
```shell
|
||||
$ apt install -y build-essential pkg-config libssl-dev libcurl4-openssl-dev libxml2-dev
|
||||
```
|
||||
|
||||
#### Red Hat based distributions
|
||||
|
||||
```shell
|
||||
$ yum install -y pkg-config gcc openssl-devel libxml2-devel
|
||||
```
|
||||
|
||||
#### Arch based distributions
|
||||
|
||||
```shell
|
||||
$ pacman -Sy --noconfirm pkgconf gcc openssl libxml2
|
||||
```
|
||||
|
||||
#### macOS
|
||||
|
||||
```shell
|
||||
$ xcode-select --install
|
||||
$ brew install pkg-config
|
||||
```
|
||||
|
||||
Hurl is written in [Rust]. You should [install] the latest stable release.
|
||||
|
||||
```shell
|
||||
$ curl https://sh.rustup.rs -sSf | sh -s -- -y
|
||||
$ source $HOME/.cargo/env
|
||||
$ rustc --version
|
||||
$ cargo --version
|
||||
```
|
||||
|
||||
Then build hurl:
|
||||
|
||||
```shell
|
||||
$ git clone https://github.com/Orange-OpenSource/hurl
|
||||
$ cd hurl
|
||||
$ cargo build --release
|
||||
$ ./target/release/hurl --version
|
||||
```
|
||||
|
||||
### Build on Windows
|
||||
|
||||
Please follow the [contrib on Windows section].
|
||||
|
||||
[GitHub]: https://github.com/Orange-OpenSource/hurl
|
||||
[hurl-1.6.1-win64.zip]: https://github.com/Orange-OpenSource/hurl/releases/download/1.6.1/hurl-1.6.1-win64.zip
|
||||
[hurl-1.6.1-win64-installer.exe]: https://github.com/Orange-OpenSource/hurl/releases/download/1.6.1/hurl-1.6.1-win64-installer.exe
|
||||
[hurl-1.6.1-x86_64-osx.tar.gz]: https://github.com/Orange-OpenSource/hurl/releases/download/1.6.1/hurl-1.6.1-x86_64-osx.tar.gz
|
||||
[hurl-1.6.1-x86_64-linux.tar.gz]: https://github.com/Orange-OpenSource/hurl/releases/download/1.6.1/hurl-1.6.1-x86_64-linux.tar.gz
|
||||
[Homebrew]: https://brew.sh
|
||||
[AUR]: https://wiki.archlinux.org/index.php/Arch_User_Repository
|
||||
[`hurl-bin` package]: https://aur.archlinux.org/packages/hurl-bin/
|
||||
[install]: https://www.rust-lang.org/tools/install
|
||||
[Rust]: https://www.rust-lang.org
|
||||
[contrib on Windows section]: https://github.com/Orange-OpenSource/hurl/blob/master/contrib/windows/README.md
|
||||
[NixOS / Nix package]: https://search.nixos.org/packages?channel=21.11&from=0&size=1&sort=relevance&type=packages&query=hurl
|
||||
|
||||
|
||||
|
191
docs/man-page.md
Normal file
@ -0,0 +1,191 @@
|
||||
# Man Page
|
||||
|
||||
## Name
|
||||
|
||||
hurl - run and test HTTP requests.
|
||||
|
||||
|
||||
## Synopsis
|
||||
|
||||
**hurl** [options] [FILE...]
|
||||
|
||||
|
||||
## Description
|
||||
|
||||
**Hurl** is an HTTP client that performs HTTP requests defined in a simple plain text format.
|
||||
|
||||
Hurl is very versatile, it enables to chain HTTP requests, capture values from HTTP responses and make asserts.
|
||||
|
||||
```
|
||||
$ hurl session.hurl
|
||||
```
|
||||
|
||||
If no input-files are specified, input is read from stdin.
|
||||
|
||||
```
|
||||
$ echo GET http://httpbin.org/get | hurl
|
||||
{
|
||||
"args": {},
|
||||
"headers": {
|
||||
"Accept": "*/*",
|
||||
"Accept-Encoding": "gzip",
|
||||
"Content-Length": "0",
|
||||
"Host": "httpbin.org",
|
||||
"User-Agent": "hurl/0.99.10",
|
||||
"X-Amzn-Trace-Id": "Root=1-5eedf4c7-520814d64e2f9249ea44e0"
|
||||
},
|
||||
"origin": "1.2.3.4",
|
||||
"url": "http://httpbin.org/get"
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
Output goes to stdout by default. For output to a file, use the -o option:
|
||||
|
||||
```
|
||||
$ hurl -o output input.hurl
|
||||
```
|
||||
|
||||
|
||||
|
||||
By default, Hurl executes all HTTP requests and outputs the response body of the last HTTP call.
|
||||
|
||||
|
||||
|
||||
## Hurl File Format
|
||||
|
||||
The Hurl file format is fully documented in [https://hurl.dev/docs/hurl-file.html](https://hurl.dev/docs/hurl-file.html)
|
||||
|
||||
It consists of one or several HTTP requests
|
||||
|
||||
```hurl
|
||||
GET http:/example.org/endpoint1
|
||||
GET http:/example.org/endpoint2
|
||||
```
|
||||
|
||||
|
||||
### Capturing values
|
||||
|
||||
A value from an HTTP response can be-reused for successive HTTP requests.
|
||||
|
||||
A typical example occurs with csrf tokens.
|
||||
|
||||
```hurl
|
||||
GET https://example.org
|
||||
HTTP/1.1 200
|
||||
# Capture the CSRF token value from html body.
|
||||
[Captures]
|
||||
csrf_token: xpath "normalize-space(//meta[@name='_csrf_token']/@content)"
|
||||
|
||||
# Do the login !
|
||||
POST https://example.org/login?user=toto&password=1234
|
||||
X-CSRF-TOKEN: {{csrf_token}}
|
||||
```
|
||||
|
||||
### Asserts
|
||||
|
||||
The HTTP response defined in the Hurl session are used to make asserts.
|
||||
|
||||
At the minimum, the response includes the asserts on the HTTP version and status code.
|
||||
|
||||
```hurl
|
||||
GET http:/google.com
|
||||
HTTP/1.1 301
|
||||
```
|
||||
|
||||
It can also include asserts on the response headers
|
||||
|
||||
```hurl
|
||||
GET http:/google.com
|
||||
HTTP/1.1 301
|
||||
Location: http://www.google.com
|
||||
```
|
||||
|
||||
You can also include explicit asserts combining query and predicate
|
||||
|
||||
```hurl
|
||||
GET http:/google.com
|
||||
HTTP/1.1 301
|
||||
[Asserts]
|
||||
xpath "string(//title)" == "301 Moved"
|
||||
```
|
||||
|
||||
Thanks to asserts, Hurl can be used as a testing tool to run scenarii.
|
||||
|
||||
|
||||
|
||||
|
||||
## Options
|
||||
|
||||
Options that exist in curl have exactly the same semantic.
|
||||
|
||||
Option | Description
|
||||
--- | ---
|
||||
<a href="#cacert" id="cacert"><code>--cacert</code></a> | Tells curl to use the specified certificate file to verify the peer.<br/>The file may contain multiple CA certificates.<br/>The certificate(s) must be in PEM format.<br/>Normally curl is built to use a default file for this, so this option is typically used to alter that default file.<br/>
|
||||
<a href="#color" id="color"><code>--color</code></a> | Colorize Output<br/>
|
||||
<a href="#compressed" id="compressed"><code>--compressed</code></a> | Request a compressed response using one of the algorithms br, gzip, deflate and automatically decompress the content.<br/>
|
||||
<a href="#connect-timeout" id="connect-timeout"><code>--connect-timeout <seconds></code></a> | Maximum time in seconds that you allow Hurl's connection to take.<br/><br/>See also [-m, --max-time](#max-time) option.<br/>
|
||||
<a href="#cookie" id="cookie"><code>-b, --cookie <file></code></a> | Read cookies from file (using the Netscape cookie file format).<br/><br/>Combined with [-c, --cookie-jar](#cookie-jar), you can simulate a cookie storage between successive Hurl runs.<br/>
|
||||
<a href="#cookie-jar" id="cookie-jar"><code>-c, --cookie-jar <file></code></a> | Write cookies to FILE after running the session (only for one session).<br/>The file will be written using the Netscape cookie file format.<br/><br/>Combined with [-b, --cookie](#cookie), you can simulate a cookie storage between successive Hurl runs.<br/>
|
||||
<a href="#fail-at-end" id="fail-at-end"><code>--fail-at-end</code></a> | Continue executing requests to the end of the Hurl file even when an assert error occurs.<br/>By default, Hurl exits after an assert error in the HTTP response.<br/><br/>Note that this option does not affect the behavior with multiple input Hurl files.<br/><br/>All the input files are executed independently. The result of one file does not affect the execution of the other Hurl files.<br/>
|
||||
<a href="#file-root" id="file-root"><code>--file-root <dir></code></a> | Set root filesystem to import files in Hurl. This is used for both files in multipart form data and request body.<br/>When this is not explicitly defined, the files are relative to the current directory in which Hurl is running.<br/>
|
||||
<a href="#location" id="location"><code>-L, --location</code></a> | Follow redirect. You can limit the amount of redirects to follow by using the [--max-redirs](#max-redirs) option.<br/>
|
||||
<a href="#glob" id="glob"><code>--glob <glob></code></a> | Specify input files that match the given glob pattern.<br/><br/>Multiple glob flags may be used. This flag supports common Unix glob patterns like *, ? and []. <br/>However, to avoid your shell accidentally expanding glob patterns before Hurl handles them, you must use single quotes or double quotes around each pattern.<br/>
|
||||
<a href="#include" id="include"><code>-i, --include</code></a> | Include the HTTP headers in the output (last entry).<br/>
|
||||
<a href="#ignore-asserts" id="ignore-asserts"><code>--ignore-asserts</code></a> | Ignore all asserts defined in the Hurl file.<br/>
|
||||
<a href="#insecure" id="insecure"><code>-k, --insecure</code></a> | This option explicitly allows Hurl to perform "insecure" SSL connections and transfers.<br/>
|
||||
<a href="#interactive" id="interactive"><code>--interactive</code></a> | Stop between requests.<br/>This is similar to a break point, You can then continue (Press C) or quit (Press Q).<br/>
|
||||
<a href="#json" id="json"><code>--json</code></a> | Output each hurl file result to JSON. The format is very closed to HAR format. <br/>
|
||||
<a href="#max-redirs" id="max-redirs"><code>--max-redirs <num></code></a> | Set maximum number of redirection-followings allowed<br/>By default, the limit is set to 50 redirections. Set this option to -1 to make it unlimited.<br/>
|
||||
<a href="#max-time" id="max-time"><code>-m, --max-time <seconds></code></a> | Maximum time in seconds that you allow a request/response to take. This is the standard timeout.<br/><br/>See also [--connect-timeout](#connect-timeout) option.<br/>
|
||||
<a href="#no-color" id="no-color"><code>--no-color</code></a> | Do not colorize Output<br/>
|
||||
<a href="#no-output" id="no-output"><code>--no-output</code></a> | Suppress output. By default, Hurl outputs the body of the last response.<br/>
|
||||
<a href="#noproxy" id="noproxy"><code>--noproxy <no-proxy-list></code></a> | Comma-separated list of hosts which do not use a proxy.<br/>Override value from Environment variable no_proxy.<br/>
|
||||
<a href="#output" id="output"><code>-o, --output <file></code></a> | Write output to <file> instead of stdout.<br/>
|
||||
<a href="#progress" id="progress"><code>--progress</code></a> | Print filename and status for each test (on stderr)<br/>
|
||||
<a href="#proxy" id="proxy"><code>-x, --proxy [protocol://]host[:port]</code></a> | Use the specified proxy.<br/>
|
||||
<a href="#report-junit" id="report-junit"><code>--report-junit <file></code></a> | Generate JUNIT <file>.<br/><br/>If the <file> report already exists, it will be updated with the new test results.<br/>
|
||||
<a href="#report-html" id="report-html"><code>--report-html <dir></code></a> | Generate HTML report in dir.<br/><br/>If the HTML report already exists, it will be updated with the new test results.<br/>
|
||||
<a href="#summary" id="summary"><code>--summary</code></a> | Print test metrics at the end of the run (on stderr)<br/>
|
||||
<a href="#test" id="test"><code>--test</code></a> | Activate test mode; equals [--no-output](#no-output) [--progress](#progress) [--summary](#summary)<br/>
|
||||
<a href="#to-entry" id="to-entry"><code>--to-entry <entry-number></code></a> | Execute Hurl file to ENTRY_NUMBER (starting at 1).<br/>Ignore the remaining of the file. It is useful for debugging a session.<br/>
|
||||
<a href="#user" id="user"><code>-u, --user <user:password></code></a> | Add basic Authentication header to each request.<br/>
|
||||
<a href="#user-agent" id="user-agent"><code>-A, --user-agent <name></code></a> | Specify the User-Agent string to send to the HTTP server.<br/>
|
||||
<a href="#variable" id="variable"><code>--variable <name=value></code></a> | Define variable (name/value) to be used in Hurl templates.<br/>
|
||||
<a href="#variables-file" id="variables-file"><code>--variables-file <file></code></a> | Set properties file in which your define your variables.<br/><br/>Each variable is defined as name=value exactly as with [--variable](#variable) option.<br/><br/>Note that defining a variable twice produces an error.<br/>
|
||||
<a href="#verbose" id="verbose"><code>-v, --verbose</code></a> | Turn on verbose output on standard error stream<br/>Useful for debugging.<br/><br/>A line starting with '>' means data sent by Hurl.<br/>A line staring with '<' means data received by Hurl.<br/>A line starting with '*' means additional info provided by Hurl.<br/><br/>If you only want HTTP headers in the output, -i, --include might be the option you're looking for.<br/>
|
||||
<a href="#help" id="help"><code>-h, --help</code></a> | Usage help. This lists all current command line options with a short description.<br/>
|
||||
<a href="#version" id="version"><code>-V, --version</code></a> | Prints version information<br/>
|
||||
|
||||
## Environment
|
||||
|
||||
Environment variables can only be specified in lowercase.
|
||||
|
||||
Using an environment variable to set the proxy has the same effect as using the [-x, --proxy](#proxy) option.
|
||||
|
||||
Variable | Description
|
||||
--- | ---
|
||||
`http_proxy [protocol://]<host>[:port]` | Sets the proxy server to use for HTTP.<br/>
|
||||
`https_proxy [protocol://]<host>[:port]` | Sets the proxy server to use for HTTPS.<br/>
|
||||
`all_proxy [protocol://]<host>[:port]` | Sets the proxy server to use if no protocol-specific proxy is set.<br/>
|
||||
`no_proxy <comma-separated list of hosts>` | list of host names that shouldn't go through any proxy.<br/>
|
||||
`HURL_name value` | Define variable (name/value) to be used in Hurl templates. This is similar than [--variable](#variable) and [--variables-file](#variables-file) options.<br/>
|
||||
|
||||
## Exit Codes
|
||||
|
||||
Value | Description
|
||||
--- | ---
|
||||
`1` | Failed to parse command-line options.<br/>
|
||||
`2` | Input File Parsing Error.<br/>
|
||||
`3` | Runtime error (such as failure to connect to host).<br/>
|
||||
`4` | Assert Error.<br/>
|
||||
|
||||
## WWW
|
||||
|
||||
[https://hurl.dev](https://hurl.dev)
|
||||
|
||||
|
||||
## See Also
|
||||
|
||||
curl(1) hurlfmt(1)
|
||||
|
515
docs/request.md
Normal file
@ -0,0 +1,515 @@
|
||||
# Request
|
||||
|
||||
## Definition
|
||||
|
||||
Request describes an HTTP request: a mandatory [method] and [url], followed by optional [headers].
|
||||
|
||||
Then, [query parameters], [form parameters], [multipart form datas], [cookies] and
|
||||
[basic authentication] can be used to configure the HTTP request.
|
||||
|
||||
Finally, an optional [body] can be used to configure the HTTP request body.
|
||||
|
||||
## Example
|
||||
|
||||
```hurl
|
||||
GET https://example.org/api/dogs?id=4567
|
||||
User-Agent: My User Agent
|
||||
Content-Type: application/json
|
||||
[BasicAuth]
|
||||
alice: secret
|
||||
```
|
||||
|
||||
## Structure
|
||||
|
||||
<div class="hurl-structure-schema">
|
||||
<div class="hurl-structure">
|
||||
<div class="hurl-structure-col-0">
|
||||
<div class="hurl-part-0">
|
||||
PUT https://sample.net
|
||||
</div>
|
||||
<div class="hurl-part-1">
|
||||
accept: */*<br>x-powered-by: Express<br>user-agent: Test
|
||||
</div>
|
||||
<div class="hurl-part-2">
|
||||
[QueryStringParams]<br>...
|
||||
</div>
|
||||
<div class="hurl-part-2">
|
||||
[FormParams]<br>...
|
||||
</div>
|
||||
<div class="hurl-part-2">
|
||||
[BasicAuth]<br>...
|
||||
</div>
|
||||
<div class="hurl-part-2">
|
||||
[Cookies]<br>...
|
||||
</div>
|
||||
<div class="hurl-part-2">
|
||||
...
|
||||
</div>
|
||||
<div class="hurl-part-2">
|
||||
...
|
||||
</div>
|
||||
<div class="hurl-part-3">
|
||||
{<br>
|
||||
"type": "FOO",<br>
|
||||
"value": 356789,<br>
|
||||
"ordered": true,<br>
|
||||
"index": 10<br>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="hurl-structure-col-1">
|
||||
<div class="hurl-request-explanation-part-0">
|
||||
<a href="#method">Method</a> and <a href="#url">URL</a> (mandatory)
|
||||
</div>
|
||||
<div class="hurl-request-explanation-part-1">
|
||||
<br><a href="#headers">HTTP request headers</a> (optional)
|
||||
</div>
|
||||
<div class="hurl-request-explanation-part-2">
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
</div>
|
||||
<div class="hurl-request-explanation-part-2">
|
||||
<a href="#query-parameters">Query strings</a>, <a href="#form-parameters">form params</a>, <a href="#cookies">cookies</a>, <a href="#basic-authentification">authentification</a> ...<br>(optional sections, unordered)
|
||||
</div>
|
||||
<div class="hurl-request-explanation-part-2">
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
</div>
|
||||
<div class="hurl-request-explanation-part-3">
|
||||
<br>
|
||||
</div>
|
||||
<div class="hurl-request-explanation-part-3">
|
||||
<a href="#body">HTTP request body</a> (optional)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
[Headers], if present, follow directly after the [method] and [url]. This allows Hurl format to 'look like' the real HTTP format.
|
||||
Contrary to HTTP headers, other parameters are defined in sections (`[Cookies]`, `[QueryStringParams]`, `[FormParams]` etc...)
|
||||
These sections are not ordered and can be mixed in any way:
|
||||
|
||||
```hurl
|
||||
GET https://example.org/api/dogs
|
||||
User-Agent: My User Agent
|
||||
[QueryStringParams]
|
||||
id: 4567
|
||||
order: newest
|
||||
[BasicAuth]
|
||||
alice: secret
|
||||
```
|
||||
|
||||
```hurl
|
||||
GET https://example.org/api/dogs
|
||||
User-Agent: My User Agent
|
||||
[BasicAuth]
|
||||
alice: secret
|
||||
[QueryStringParams]
|
||||
id: 4567
|
||||
order: newest
|
||||
```
|
||||
|
||||
The last optional part of a request configuration is the request [body]. Request body must be the last paremeter of a request
|
||||
(after [headers] and request sections). Like headers, [body] have no explicit marker:
|
||||
|
||||
```hurl
|
||||
POST https://example.org/api/dogs?id=4567
|
||||
User-Agent: My User Agent
|
||||
{
|
||||
"name": "Ralphy"
|
||||
}
|
||||
```
|
||||
|
||||
## Description
|
||||
|
||||
### Method
|
||||
|
||||
Mandatory HTTP request method, one of `GET`, `HEAD`, `POST`, `PUT`, `DELETE`, `CONNECT`, `OPTIONS`,
|
||||
`TRACE`, `PATCH`.
|
||||
|
||||
### URL
|
||||
|
||||
Mandatory HTTP request url.
|
||||
|
||||
Url can contain query parameters, even if using a [query parameters section] is preferred.
|
||||
|
||||
```hurl
|
||||
# A request with url containing query parameters.
|
||||
GET https://example.org/forum/questions/?search=Install%20Linux&order=newest
|
||||
|
||||
# A request with query parameters section, equivalent to the first request.
|
||||
GET https://example.org/forum/questions/
|
||||
[QueryStringParams]
|
||||
search: Install Linux
|
||||
order: newest
|
||||
```
|
||||
|
||||
> Query parameters in query parameter section are not url encoded.
|
||||
|
||||
When query parameters are present in the url and in a query parameters section, the resulting request will
|
||||
have both parameters.
|
||||
|
||||
### Headers
|
||||
|
||||
Optional list of HTTP request headers.
|
||||
|
||||
A header consists of a name, followed by a `:` and a value.
|
||||
|
||||
```hurl
|
||||
GET https://example.org/news
|
||||
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:70.0) Gecko/20100101 Firefox/70.0
|
||||
Accept: */*
|
||||
Accept-Language: en-US,en;q=0.5
|
||||
Accept-Encoding: gzip, deflate, br
|
||||
Connection: keep-alive
|
||||
```
|
||||
|
||||
> Headers directly follow url, without any section name, contrary to query parameters, form parameters
|
||||
> or cookies
|
||||
|
||||
Note that header usually don't start with double quotes. If the header value starts with double quotes, the double
|
||||
quotes will be part of the header value:
|
||||
|
||||
```hurl
|
||||
PATCH https://example.org/file.txt
|
||||
If-Match: "e0023aa4e"
|
||||
```
|
||||
|
||||
`If-Match` request header will be sent will the following value `"e0023aa4e"` (started and ended with double quotes).
|
||||
|
||||
Headers must follow directly after the [method] and [url].
|
||||
|
||||
### Query parameters
|
||||
|
||||
Optional list of query parameters.
|
||||
|
||||
A query parameter consists of a field, followed by a `:` and a value. The query parameters section starts with
|
||||
`[QueryStringParams]`. Contrary to query parameters in the url, each value in the query parameters section is not
|
||||
url encoded.
|
||||
|
||||
```hurl
|
||||
GET https://example.org/news
|
||||
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.14; rv:70.0) Gecko/20100101 Firefox/70.0
|
||||
[QueryStringParams]
|
||||
order: newest
|
||||
search: {{custom-search}}
|
||||
count: 100
|
||||
```
|
||||
|
||||
If there are any parameters in the url, the resulted request will have both parameters.
|
||||
|
||||
### Form parameters
|
||||
|
||||
A form parameters section can be used to send data, like [HTML form].
|
||||
|
||||
This section contains an optional list of key values, each key followed by a `:` and a value. Key values will be
|
||||
encoded in key-value tuple separated by '&', with a '=' between the key and the value, and sent in the body request.
|
||||
The content type of the request is `application/x-www-form-urlencoded`. The form parameters section starts
|
||||
with `[FormParams]`.
|
||||
|
||||
```hurl
|
||||
POST https://example.org/contact
|
||||
[FormParams]
|
||||
default: false
|
||||
token: {{token}}
|
||||
email: john.doe@rookie.org
|
||||
number: 33611223344
|
||||
```
|
||||
|
||||
Form parameters section can be seen as syntactic sugar over body section (values in form parameters section
|
||||
are not url encoded.). A [multiline string body] could be used instead of a forms parameters section.
|
||||
|
||||
~~~hurl
|
||||
# Run a POST request with form parameters section:
|
||||
POST https://example.org/test
|
||||
[FormParams]
|
||||
name: John Doe
|
||||
key1: value1
|
||||
|
||||
# Run the same POST request with a body section:
|
||||
POST https://example.org/test
|
||||
Content-Type: application/x-www-form-urlencoded
|
||||
```
|
||||
name=John%20Doe&key1=value1
|
||||
```
|
||||
~~~
|
||||
|
||||
When both [body section] and form parameters section are present, only the body section is taken into account.
|
||||
|
||||
### Multipart Form Data
|
||||
|
||||
A multipart form data section can be used to send data, with key / value and file content
|
||||
(see [multipart/form-data on MDN]).
|
||||
|
||||
The form parameters section starts with `[MultipartFormData]`.
|
||||
|
||||
```hurl
|
||||
POST https://example.org/upload
|
||||
[MultipartFormData]
|
||||
field1: value1
|
||||
field2: file,example.txt;
|
||||
# One can specify the file content type:
|
||||
field3: file,example.zip; application/zip
|
||||
```
|
||||
|
||||
Files are relative to the input Hurl file, and cannot contain implicit parent directory (`..`). You can use
|
||||
[`--file-root` option] to specify the root directory of all file nodes.
|
||||
|
||||
Content type can be specified or inferred based on the filename extension:
|
||||
|
||||
- `.gif`: `image/gif`,
|
||||
- `.jpg`: `image/jpeg`,
|
||||
- `.jpeg`: `image/jpeg`,
|
||||
- `.png`: `image/png`,
|
||||
- `.svg`: `image/svg+xml`,
|
||||
- `.txt`: `text/plain`,
|
||||
- `.htm`: `text/html`,
|
||||
- `.html`: `text/html`,
|
||||
- `.pdf`: `application/pdf`,
|
||||
- `.xml`: `application/xml`
|
||||
|
||||
By default, content type is `application/octet-stream`.
|
||||
|
||||
|
||||
### Cookies
|
||||
|
||||
Optional list of session cookies for this request.
|
||||
|
||||
A cookie consists of a name, followed by a `:` and a value. Cookies are sent per request, and are not added to
|
||||
the cookie storage session, contrary to a cookie set in a header response. (for instance `Set-Cookie: theme=light`). The
|
||||
cookies section starts with `[Cookies]`.
|
||||
|
||||
```hurl
|
||||
GET https://example.org/index.html
|
||||
[Cookies]
|
||||
theme: light
|
||||
sessionToken: abc123
|
||||
```
|
||||
|
||||
Cookies section can be seen as syntactic sugar over corresponding request header.
|
||||
|
||||
```hurl
|
||||
# Run a GET request with cookies section:
|
||||
GET https://example.org/index.html
|
||||
[Cookies]
|
||||
theme: light
|
||||
sessionToken: abc123
|
||||
|
||||
# Run the same GET request with a header:
|
||||
GET https://example.org/index.html
|
||||
Cookie: theme=light; sessionToken=abc123
|
||||
```
|
||||
|
||||
### Basic Authentication
|
||||
|
||||
A basic authentication section can be used to perform [basic authentication].
|
||||
|
||||
Username is followed by a `:` and a password. The basic authentication section starts with
|
||||
`[BasicAuth]`. Username and password are _not_ base64 encoded.
|
||||
|
||||
|
||||
```hurl
|
||||
# Perform basic authentification with login `bob` and password `secret`.
|
||||
GET https://example.org/protected
|
||||
[BasicAuth]
|
||||
bob: secret
|
||||
```
|
||||
|
||||
> Spaces surrounded username and password are trimmed. If you
|
||||
> really want a space in your password (!!), you could use [Hurl unicode literals \u{20}].
|
||||
|
||||
This is equivalent (but simpler) to construct the request with a [Authorization] header:
|
||||
|
||||
```hurl
|
||||
# Authorization header value can be computed with `echo -n 'bob:secret' | base64`
|
||||
GET https://example.org/protected
|
||||
Authorization: Basic Ym9iOnNlY3JldA==
|
||||
```
|
||||
|
||||
Basic authentication allows per request authentication.
|
||||
If you want to add basic authentication to all the request of a Hurl file
|
||||
you can use [`-u/--user` option].
|
||||
|
||||
### Body
|
||||
|
||||
Optional HTTP body request.
|
||||
|
||||
If the body of the request is a [JSON] string or a [XML] string, the value can be
|
||||
directly inserted without any modification. For a text based body that is not JSON nor XML,
|
||||
one can use multiline string that starts with <code>```</code> and ends
|
||||
with <code>```</code>.
|
||||
|
||||
For a precise byte control of the request body, [Base64] encoded string, [hexadecimal string]
|
||||
or [included file] can be used to describe exactly the body byte content.
|
||||
|
||||
> You can set a body request even with a `GET` body, even if this is not a common practice.
|
||||
|
||||
The body section must be the last section of the request configuration.
|
||||
|
||||
#### JSON body
|
||||
|
||||
JSON body is used to set a literal JSON as the request body.
|
||||
|
||||
```hurl
|
||||
# Create a new doggy thing with JSON body:
|
||||
POST https://example.org/api/dogs
|
||||
{
|
||||
"id": 0,
|
||||
"name": "Frieda",
|
||||
"picture": "images/scottish-terrier.jpeg",
|
||||
"age": 3,
|
||||
"breed": "Scottish Terrier",
|
||||
"location": "Lisco, Alabama"
|
||||
}
|
||||
```
|
||||
|
||||
When using JSON body, the content type `application/json` is automatically set.
|
||||
|
||||
#### XML body
|
||||
|
||||
XML body is used to set a literal XML as the request body.
|
||||
|
||||
~~~hurl
|
||||
# Create a new soapy thing XML body:
|
||||
POST https://example.org/InStock
|
||||
Content-Type: application/soap+xml; charset=utf-8
|
||||
Content-Length: 299
|
||||
SOAPAction: "http://www.w3.org/2003/05/soap-envelope"
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:m="http://example.net">
|
||||
<soap:Header></soap:Header>
|
||||
<soap:Body>
|
||||
<m:GetStockPrice>
|
||||
<m:StockName>GOOG</m:StockName>
|
||||
</m:GetStockPrice>
|
||||
</soap:Body>
|
||||
</soap:Envelope>
|
||||
~~~
|
||||
|
||||
#### Raw string body
|
||||
|
||||
For text based body that are not JSON nor XML, one can used multiline string, started and ending with
|
||||
<code>```</code>.
|
||||
|
||||
~~~hurl
|
||||
POST https://example.org/models
|
||||
```
|
||||
Year,Make,Model,Description,Price
|
||||
1997,Ford,E350,"ac, abs, moon",3000.00
|
||||
1999,Chevy,"Venture ""Extended Edition""","",4900.00
|
||||
1999,Chevy,"Venture ""Extended Edition, Very Large""",,5000.00
|
||||
1996,Jeep,Grand Cherokee,"MUST SELL! air, moon roof, loaded",4799.00
|
||||
```
|
||||
~~~
|
||||
|
||||
The standard usage of a raw string is:
|
||||
|
||||
~~~
|
||||
```
|
||||
line1
|
||||
line2
|
||||
line3
|
||||
```
|
||||
~~~
|
||||
|
||||
is evaluated as "line1\nline2\nline3\n".
|
||||
|
||||
|
||||
To construct an empty string:
|
||||
|
||||
~~~
|
||||
```
|
||||
```
|
||||
~~~
|
||||
|
||||
or
|
||||
|
||||
~~~
|
||||
``````
|
||||
~~~
|
||||
|
||||
|
||||
Finaly, raw string can be used without any newline:
|
||||
|
||||
~~~
|
||||
```line```
|
||||
~~~
|
||||
|
||||
is evaluated as "line".
|
||||
|
||||
|
||||
#### Base64 body
|
||||
|
||||
Base64 body is used to set binary data as the request body.
|
||||
|
||||
Base64 body starts with `base64,` and end with `;`. MIME's Base64 encoding is supported (newlines and white spaces may be
|
||||
present anywhere but are to be ignored on decoding), and `=` padding characters might be added.
|
||||
|
||||
```hurl
|
||||
POST https://example.org
|
||||
# Some random comments before body
|
||||
base64,TG9yZW0gaXBzdW0gZG9sb3Igc2l0IGFtZXQsIGNvbnNlY3RldHVyIG
|
||||
FkaXBpc2NpbmcgZWxpdC4gSW4gbWFsZXN1YWRhLCBuaXNsIHZlbCBkaWN0dW0g
|
||||
aGVuZHJlcml0LCBlc3QganVzdG8gYmliZW5kdW0gbWV0dXMsIG5lYyBydXRydW
|
||||
0gdG9ydG9yIG1hc3NhIGlkIG1ldHVzLiA=;
|
||||
```
|
||||
|
||||
#### Hex body
|
||||
|
||||
Hex body is used to set binary data as the request body.
|
||||
|
||||
Hex body starts with `hex,` and end with `;`.
|
||||
|
||||
```hurl
|
||||
PUT https://example.org
|
||||
# Send a café, encoded in UTF-8
|
||||
hex,636166c3a90a;
|
||||
```
|
||||
|
||||
|
||||
#### File body
|
||||
|
||||
To use the binary content of a local file as the body request, file body can be used. File body starts with
|
||||
`file,` and ends with `;``
|
||||
|
||||
```hurl
|
||||
POST https://example.org
|
||||
# Some random comments before body
|
||||
file,data.bin;
|
||||
```
|
||||
|
||||
File are relative to the input Hurl file, and cannot contain implicit parent directory (`..`). You can use
|
||||
[`--file-root` option] to specify the root directory of all file nodes.
|
||||
|
||||
|
||||
[method]: #method
|
||||
[url]: #url
|
||||
[headers]: #headers
|
||||
[Headers]: #headers
|
||||
[query parameters]: #query-parameters
|
||||
[form parameters]: #form-parameters
|
||||
[multipart form datas]: #multipart-form-data
|
||||
[cookies]: #cookies
|
||||
[basic authentication]: #basic-authentication
|
||||
[body]: #body
|
||||
[query parameters section]: #query-parameters
|
||||
[HTML form]: https://developer.mozilla.org/en-US/docs/Learn/Forms
|
||||
[multiline string body]: #multiline-string-body
|
||||
[body section]: #body
|
||||
[multipart/form-data on MDN]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods/POST
|
||||
[`--file-root` option]: /docs/man-page.md#file-root
|
||||
[JSON]: https://www.json.org
|
||||
[XML]: https://en.wikipedia.org/wiki/XML
|
||||
[Base64]: https://en.wikipedia.org/wiki/Base64
|
||||
[hexadecimal string]: #hex-body
|
||||
[included file]: #file-body
|
||||
[`--file-root` option]: /docs/man-page.md#file-root
|
||||
[`-u/--user` option]: /docs/man-page.md#user
|
||||
[Hurl unicode literals \u{20}]: /docs/hurl-file.md#special-character-in-strings
|
||||
[Authorization]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization
|
104
docs/response.md
Normal file
@ -0,0 +1,104 @@
|
||||
# Response
|
||||
|
||||
## Definition
|
||||
|
||||
Responses can be used to capture values to perform subsequent requests, or add asserts to HTTP responses. Response on
|
||||
requests are optional, a Hurl file can only be a sequence of [requests].
|
||||
|
||||
A response describes the expected HTTP response, with mandatory [version and status], followed by optional [headers],
|
||||
[captures], [asserts] and [body]. Assertions in the expected HTTP response describe values of the received HTTP response.
|
||||
Captures capture values from the received HTTP response and populate a set of named variables that can be used
|
||||
in the following entries.
|
||||
|
||||
## Example
|
||||
|
||||
```hurl
|
||||
GET https://example.org
|
||||
|
||||
HTTP/1.1 200
|
||||
Last-Modified: Wed, 21 Oct 2015 07:28:00 GMT
|
||||
[Asserts]
|
||||
xpath "normalize-space(//head/title)" startsWith "Welcome"
|
||||
xpath "//li" count == 18
|
||||
```
|
||||
|
||||
## Structure
|
||||
|
||||
<div class="hurl-structure-schema">
|
||||
<div class="hurl-structure">
|
||||
<div class="hurl-structure-col-0">
|
||||
<div class="hurl-part-0">
|
||||
HTTP/1.1 *
|
||||
</div>
|
||||
<div class=" hurl-part-1">
|
||||
content-length: 206<br>accept-ranges: bytes<br>user-agent: Test
|
||||
</div>
|
||||
<div class="hurl-part-2">
|
||||
[Captures]<br>...
|
||||
</div>
|
||||
<div class="hurl-part-2">
|
||||
[Asserts]<br>...
|
||||
</div>
|
||||
<div class="hurl-part-3">
|
||||
{<br>
|
||||
"type": "FOO",<br>
|
||||
"value": 356789,<br>
|
||||
"ordered": true,<br>
|
||||
"index": 10<br>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div class="hurl-structure-col-1">
|
||||
<div class="hurl-request-explanation-part-0">
|
||||
<a href="/docs/asserting-response.html#version-status">Version and status (mandatory if response present)</a>
|
||||
</div>
|
||||
<div class="hurl-request-explanation-part-1">
|
||||
<br><a href="/docs/asserting-response.html#headers">HTTP response headers</a> (optional)
|
||||
</div>
|
||||
<div class="hurl-request-explanation-part-2">
|
||||
<br>
|
||||
<br>
|
||||
</div>
|
||||
<div class="hurl-request-explanation-part-2">
|
||||
<a href="/docs/capturing-response.html">Captures</a> and <a href="/docs/asserting-response.html#asserts">asserts</a> (optional sections, unordered)
|
||||
</div>
|
||||
<div class="hurl-request-explanation-part-2">
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
<br>
|
||||
</div>
|
||||
<div class="hurl-request-explanation-part-3">
|
||||
<a href="/docs/asserting-response.html#body">HTTP response body</a> (optional)
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
## Capture and Assertion
|
||||
|
||||
With the response section, one can optionally [capture value from headers, body],
|
||||
or [add assert on status code, body or headers].
|
||||
|
||||
### Body compression
|
||||
|
||||
Hurl outputs the raw HTTP body to stdout by default. If response body is compressed (using [br, gzip, deflate]),
|
||||
the binary stream is output, without any modification. One can use [`--compressed` option]
|
||||
to request a compressed response and automatically get the decompressed body.
|
||||
|
||||
Captures and asserts work automatically on the decompressed body, so you can request compressed data (using [`Accept-Encoding`]
|
||||
header by example) and add assert and captures on the decoded body as if there weren't any compression.
|
||||
|
||||
|
||||
[requests]: /docs/request.md
|
||||
[version and status]: /docs/asserting-response.md#version-status
|
||||
[headers]: /docs/asserting-response.md#headers
|
||||
[captures]: /docs/capturing-response.md#captures
|
||||
[asserts]: /docs/asserting-response.md#asserts
|
||||
[body]: /docs/asserting-response.md#body
|
||||
[capture value from headers, body]: /docs/capturing-response.md
|
||||
[add assert on status code, body or headers]: /docs/asserting-response.md
|
||||
[br, gzip, deflate]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding
|
||||
[`--compressed` option]: /docs/man-page.md#compressed
|
||||
[`Accept-Encoding`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding
|
99
docs/running-tests.md
Normal file
@ -0,0 +1,99 @@
|
||||
# Running Tests
|
||||
|
||||
Hurl is run by default as an HTTP client, returning the body of the last HTTP response.
|
||||
|
||||
```shell
|
||||
$ hurl hello.hurl
|
||||
Hello World!
|
||||
```
|
||||
|
||||
When multiple input files are provided, Hurl outputs the body of the last HTTP response of each file.
|
||||
|
||||
```shell
|
||||
$ hurl hello.hurl assert_json.hurl
|
||||
Hello World![
|
||||
{ "id": 1, "name": "Bob"},
|
||||
{ "id": 2, "name": "Bill"}
|
||||
]
|
||||
```
|
||||
|
||||
|
||||
For testing, we are only interested in the asserts results, we don't need body response.
|
||||
Several options relating to testing can be used:
|
||||
|
||||
- do not output response body ([`--output /dev/null`])
|
||||
|
||||
```shell
|
||||
$ hurl --output /dev/null hello.hurl assert_json.hurl
|
||||
```
|
||||
|
||||
- show progress ([`--progress`])
|
||||
|
||||
```shell
|
||||
$ hurl --progress /dev/null hello.hurl assert_json.hurl
|
||||
hello.hurl: RUNNING [1/2]
|
||||
hello.hurl: SUCCESS
|
||||
assert_json.hurl: RUNNING [2/2]
|
||||
assert_json.hurl: SUCCESS
|
||||
Hello World![
|
||||
{ "id": 1, "name": "Bob"},
|
||||
{ "id": 2, "name": "Bill"}
|
||||
]
|
||||
```
|
||||
|
||||
- print summary ([`--summary`])
|
||||
|
||||
```shell
|
||||
$ hurl --summary hello.hurl assert_json.hurl
|
||||
Hello World![
|
||||
{ "id": 1, "name": "Bob"},
|
||||
{ "id": 2, "name": "Bill"}
|
||||
]
|
||||
--------------------------------------------------------------------------------
|
||||
Executed: 2
|
||||
Succeeded: 2 (100.0%)
|
||||
Failed: 0 (0.0%)
|
||||
Duration: 134ms
|
||||
```
|
||||
|
||||
For convenience, all these options can also be set with the unique option [`--test`].
|
||||
|
||||
```shell
|
||||
$ hurl --test hello.hurl error_assert_status.hurl
|
||||
hello.hurl: RUNNING [1/2]
|
||||
hello.hurl: SUCCESS
|
||||
error_assert_status.hurl: RUNNING [2/2]
|
||||
error: Assert Status
|
||||
--> error_assert_status.hurl:2:10
|
||||
|
|
||||
2 | HTTP/1.0 200
|
||||
| ^^^ actual value is <404>
|
||||
|
|
||||
|
||||
error_assert_status.hurl: FAILURE
|
||||
-------------------------------------------------------------
|
||||
Executed: 2
|
||||
Succeeded: 1 (50.0%)
|
||||
Failed: 1 (50.0%)
|
||||
Duration: 52ms
|
||||
```
|
||||
|
||||
|
||||
## Generating an HTML report
|
||||
|
||||
Hurl can also generates an HTML by using the [`--report-html HTML_DIR`] option.
|
||||
|
||||
If the HTML report already exists, the test results will be appended to it.
|
||||
|
||||
<img src="/docs/assets/img/hurl-html-report.png" width="320" height="258" alt="Hurl HTML Report">
|
||||
|
||||
The input Hurl files (HTML version) are also included and are easily accessed from the main page.
|
||||
|
||||
<img src="/docs/assets/img/hurl-html-file.png" width="380" height="206" alt="Hurl HTML file">
|
||||
|
||||
|
||||
[`--output /dev/null`]: /docs/man-page.md#output
|
||||
[`--progress`]: /docs/man-page.md#progress
|
||||
[`--summary`]: /docs/man-page.md#summary
|
||||
[`--test`]: /docs/man-page.md#test
|
||||
[`--report-html HTML_DIR`]: /docs/man-page.md#report-html
|
375
docs/samples.md
Normal file
@ -0,0 +1,375 @@
|
||||
# Samples
|
||||
|
||||
To run a sample, edit a file with the sample content, and run Hurl:
|
||||
|
||||
```shell
|
||||
$ vi sample.hurl
|
||||
|
||||
GET https://example.org
|
||||
|
||||
$ hurl sample.hurl
|
||||
```
|
||||
|
||||
You can check [Hurl tests suite] for more samples.
|
||||
|
||||
## Getting Data
|
||||
|
||||
A simple GET:
|
||||
|
||||
```hurl
|
||||
GET https://example.org
|
||||
```
|
||||
|
||||
[Doc](/docs/request.md#method)
|
||||
|
||||
### HTTP Headers
|
||||
|
||||
A simple GET with headers:
|
||||
|
||||
```hurl
|
||||
GET https://example.org/news
|
||||
User-Agent: Mozilla/5.0
|
||||
Accept: */*
|
||||
Accept-Language: en-US,en;q=0.5
|
||||
Accept-Encoding: gzip, deflate, br
|
||||
Connection: keep-alive
|
||||
```
|
||||
|
||||
[Doc](/docs/request.md#headers)
|
||||
|
||||
### Query Params
|
||||
|
||||
```hurl
|
||||
GET https://example.org/news
|
||||
[QueryStringParams]
|
||||
order: newest
|
||||
search: something to search
|
||||
count: 100
|
||||
```
|
||||
|
||||
Or:
|
||||
|
||||
```hurl
|
||||
GET https://example.org/news?order=newest&search=something%20to%20search&count=100
|
||||
```
|
||||
|
||||
[Doc](/docs/request.md#query-parameters)
|
||||
|
||||
### Basic Authentification
|
||||
|
||||
```hurl
|
||||
GET https://example.org/protected
|
||||
[BasicAuth]
|
||||
bob: secret
|
||||
```
|
||||
|
||||
[Doc](/docs/request.md#basic-authentification)
|
||||
|
||||
This is equivalent to construct the request with a [Authorization] header:
|
||||
|
||||
```hurl
|
||||
# Authorization header value can be computed with `echo -n 'bob:secret' | base64`
|
||||
GET https://example.org/protected
|
||||
Authorization: Basic Ym9iOnNlY3JldA==
|
||||
```
|
||||
|
||||
Basic authentification allows per request authentification.
|
||||
If you want to add basic authentification to all the request of a Hurl file
|
||||
you could use [`-u/--user` option].
|
||||
|
||||
## Sending Data
|
||||
|
||||
### Sending HTML Form Datas
|
||||
|
||||
```hurl
|
||||
POST https://example.org/contact
|
||||
[FormParams]
|
||||
default: false
|
||||
token: {{token}}
|
||||
email: john.doe@rookie.org
|
||||
number: 33611223344
|
||||
```
|
||||
|
||||
[Doc](/docs/request.md#form-parameters)
|
||||
|
||||
### Sending Multipart Form Datas
|
||||
|
||||
```hurl
|
||||
POST https://example.org/upload
|
||||
[MultipartFormData]
|
||||
field1: value1
|
||||
field2: file,example.txt;
|
||||
# On can specify the file content type:
|
||||
field3: file,example.zip; application/zip
|
||||
```
|
||||
|
||||
[Doc](/docs/request.md#multipart-form-data)
|
||||
|
||||
### Posting a JSON Body
|
||||
|
||||
With an inline JSON:
|
||||
|
||||
```hurl
|
||||
POST https://example.org/api/tests
|
||||
{
|
||||
"id": "456",
|
||||
"evaluate": true
|
||||
}
|
||||
```
|
||||
|
||||
[Doc](/docs/request.md#json-body)
|
||||
|
||||
With a local file:
|
||||
|
||||
```hurl
|
||||
POST https://example.org/api/tests
|
||||
Content-Type: application/json
|
||||
file,data.json;
|
||||
```
|
||||
|
||||
[Doc](/docs/request.md#file-body)
|
||||
|
||||
### Templating a JSON / XML Body
|
||||
|
||||
Using templates with [JSON body] or [XML body] is not currently supported in Hurl.
|
||||
Besides, you can use templates in [raw string body] with variables to send a JSON or XML body:
|
||||
|
||||
~~~hurl
|
||||
PUT https://example.org/api/hits
|
||||
Content-Type: application/json
|
||||
```
|
||||
{
|
||||
"key0": "{{a_string}}",
|
||||
"key1": {{a_bool}},
|
||||
"key2": {{a_null}},
|
||||
"key3": {{a_number}}
|
||||
}
|
||||
```
|
||||
~~~
|
||||
|
||||
Variables can be initialized via command line:
|
||||
|
||||
```shell
|
||||
$ hurl --variable a_string=apple \
|
||||
--variable a_bool=true \
|
||||
--variable a_null=null \
|
||||
--variable a_number=42 \
|
||||
test.hurl
|
||||
```
|
||||
|
||||
Resulting in a PUT request with the following JSON body:
|
||||
|
||||
```
|
||||
{
|
||||
"key0": "apple",
|
||||
"key1": true,
|
||||
"key2": null,
|
||||
"key3": 42
|
||||
}
|
||||
```
|
||||
|
||||
[Doc](/docs/request.md#raw-string-body)
|
||||
|
||||
## Testing Response
|
||||
|
||||
### Testing Response Headers
|
||||
|
||||
Use implicit response asserts to test header values:
|
||||
|
||||
```hurl
|
||||
GET https://example.org/index.html
|
||||
|
||||
HTTP/1.0 200
|
||||
Set-Cookie: theme=light
|
||||
Set-Cookie: sessionToken=abc123; Expires=Wed, 09 Jun 2021 10:18:14 GMT
|
||||
```
|
||||
|
||||
[Doc](/docs/asserting-response.md#headers)
|
||||
|
||||
|
||||
Or use explicit response asserts with [predicates]:
|
||||
|
||||
```hurl
|
||||
GET https://example.org
|
||||
|
||||
HTTP/1.1 302
|
||||
[Asserts]
|
||||
header "Location" contains "www.example.net"
|
||||
```
|
||||
|
||||
[Doc](/docs/asserting-response.md#header-assert)
|
||||
|
||||
|
||||
### Testing REST Apis
|
||||
|
||||
Asserting JSON body response (node values, collection count etc...) with [JSONPath]:
|
||||
|
||||
```hurl
|
||||
GET https://example.org/order
|
||||
screencapability: low
|
||||
|
||||
HTTP/1.1 200
|
||||
[Asserts]
|
||||
jsonpath "$.validated" == true
|
||||
jsonpath "$.userInfo.firstName" == "Franck"
|
||||
jsonpath "$.userInfo.lastName" == "Herbert"
|
||||
jsonpath "$.hasDevice" == false
|
||||
jsonpath "$.links" count == 12
|
||||
jsonpath "$.state" != null
|
||||
jsonpath "$.order" matches "^order-\\d{8}$"
|
||||
jsonpath "$.order" matches /^order-\d{8}$/ # Alternative syntax with regex litteral
|
||||
```
|
||||
|
||||
[Doc](/docs/asserting-response.md#jsonpath-assert)
|
||||
|
||||
|
||||
Testing status code:
|
||||
|
||||
```hurl
|
||||
GET https://example.org/order/435
|
||||
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
[Doc](/docs/asserting-response.md#version-status)
|
||||
|
||||
```hurl
|
||||
GET https://example.org/order/435
|
||||
|
||||
# Testing status code is in a 200-300 range
|
||||
HTTP/1.1 *
|
||||
[Asserts]
|
||||
status >= 200
|
||||
status < 300
|
||||
```
|
||||
|
||||
[Doc](/docs/asserting-response.md#status-assert)
|
||||
|
||||
|
||||
### Testing HTML Response
|
||||
|
||||
```hurl
|
||||
GET https://example.org
|
||||
|
||||
HTTP/1.1 200
|
||||
Content-Type: text/html; charset=UTF-8
|
||||
|
||||
[Asserts]
|
||||
xpath "string(/html/head/title)" contains "Example" # Check title
|
||||
xpath "count(//p)" == 2 # Check the number of p
|
||||
xpath "//p" count == 2 # Similar assert for p
|
||||
xpath "boolean(count(//h2))" == false # Check there is no h2
|
||||
xpath "//h2" not exists # Similar assert for h2
|
||||
xpath "string(//div[1])" matches /Hello.*/
|
||||
```
|
||||
|
||||
[Doc](/docs/asserting-response.md#xpath-assert)
|
||||
|
||||
### Testing Set-Cookie Attributes
|
||||
|
||||
```hurl
|
||||
GET http://myserver.com/home
|
||||
|
||||
HTTP/1.0 200
|
||||
[Asserts]
|
||||
cookie "JSESSIONID" == "8400BAFE2F66443613DC38AE3D9D6239"
|
||||
cookie "JSESSIONID[Value]" == "8400BAFE2F66443613DC38AE3D9D6239"
|
||||
cookie "JSESSIONID[Expires]" contains "Wed, 13 Jan 2021"
|
||||
cookie "JSESSIONID[Secure]" exists
|
||||
cookie "JSESSIONID[HttpOnly]" exists
|
||||
cookie "JSESSIONID[SameSite]" == "Lax"
|
||||
```
|
||||
|
||||
[Doc](/docs/asserting-response.md#cookie-assert)
|
||||
|
||||
### Testing Bytes Content
|
||||
|
||||
|
||||
Check the SHA-256 response body hash:
|
||||
|
||||
```hurl
|
||||
GET https://example.org/data.tar.gz
|
||||
|
||||
HTTP/* *
|
||||
[Asserts]
|
||||
sha256 == hex,039058c6f2c0cb492c533b0a4d14ef77cc0f78abccced5287d84a1a2011cfb81;
|
||||
```
|
||||
|
||||
[Doc](/docs/asserting-response.md#sha-256-assert)
|
||||
|
||||
|
||||
## Others
|
||||
|
||||
### Testing Endpoint Performance
|
||||
|
||||
```hurl
|
||||
GET https://sample.org/helloworld
|
||||
|
||||
HTTP/* *
|
||||
[Asserts]
|
||||
duration < 1000 # Check that response time is less than one second
|
||||
```
|
||||
|
||||
[Doc](/docs/asserting-response.md#duration-assert)
|
||||
|
||||
### Using SOAP Apis
|
||||
|
||||
```hurl
|
||||
POST https://example.org/InStock
|
||||
Content-Type: application/soap+xml; charset=utf-8
|
||||
SOAPAction: "http://www.w3.org/2003/05/soap-envelope"
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<soap:Envelope xmlns:soap="http://www.w3.org/2003/05/soap-envelope" xmlns:m="https://example.org">
|
||||
<soap:Header></soap:Header>
|
||||
<soap:Body>
|
||||
<m:GetStockPrice>
|
||||
<m:StockName>GOOG</m:StockName>
|
||||
</m:GetStockPrice>
|
||||
</soap:Body>
|
||||
</soap:Envelope>
|
||||
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
[Doc](/docs/request.md#xml-body)
|
||||
|
||||
### Capturing and Using a CSRF Token
|
||||
|
||||
```hurl
|
||||
GET https://example.org
|
||||
|
||||
HTTP/* 200
|
||||
[Captures]
|
||||
csrf_token: xpath "string(//meta[@name='_csrf_token']/@content)"
|
||||
|
||||
POST https://example.org/login?user=toto&password=1234
|
||||
X-CSRF-TOKEN: {{csrf_token}}
|
||||
|
||||
HTTP/* 302
|
||||
```
|
||||
|
||||
[Doc](/docs/capturing-response.md#xpath-capture)
|
||||
|
||||
### Checking Byte Order Mark (BOM) in Response Body
|
||||
|
||||
```hurl
|
||||
GET https://example.org/data.bin
|
||||
|
||||
HTTP/* 200
|
||||
[Asserts]
|
||||
bytes startsWith hex,efbbbf;
|
||||
```
|
||||
|
||||
[Doc](/docs/asserting-response.md#bytes-assert)
|
||||
|
||||
|
||||
[JSON body]: /docs/request.md#json-body
|
||||
[XML body]: /docs/request.md#xml-body
|
||||
[raw string body]: /docs/request.md#raw-string-body
|
||||
[predicates]: /docs/asserting-response.md#predicates
|
||||
[JSONPath]: https://goessner.net/articles/JsonPath/
|
||||
[Basic authentication]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Authentication#basic_authentication_scheme
|
||||
[`Authorization` header]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization
|
||||
[Hurl tests suite]: https://github.com/Orange-OpenSource/hurl/tree/master/integration/tests_ok
|
||||
[Authorization]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization
|
||||
[`-u/--user` option]: /docs/man-page.md#user
|
196
docs/templates.md
Normal file
@ -0,0 +1,196 @@
|
||||
# Templates
|
||||
|
||||
## Variables
|
||||
|
||||
In Hurl file, you can generate value using two curly braces, i.e `{{my_variable}}`. For instance, if you want to reuse a
|
||||
value from an HTTP response in the next entries, you can capture this value in a variable and reuse it in a template.
|
||||
|
||||
```hurl
|
||||
GET https://example.org
|
||||
|
||||
HTTP/1.1 200
|
||||
[Captures]
|
||||
csrf_token: xpath "string(//meta[@name='_csrf_token']/@content)"
|
||||
|
||||
# Do the login !
|
||||
POST https://acmecorp.net/login?user=toto&password=1234
|
||||
X-CSRF-TOKEN: {{csrf_token}}
|
||||
|
||||
HTTP/1.1 302
|
||||
```
|
||||
|
||||
In this example, we capture the value of the [CSRF token] from the body a first response, and inject it
|
||||
as a header in the next POST request.
|
||||
|
||||
```hurl
|
||||
GET https://example.org/api/index
|
||||
HTTP/* 200
|
||||
[Captures]
|
||||
index: body
|
||||
|
||||
GET https://example.org/api/status
|
||||
HTTP/* 200
|
||||
[Asserts]
|
||||
jsonpath "$.errors[{{index}}].id" == "error"
|
||||
```
|
||||
|
||||
In this second example, we capture the body in a variable `index`, and reuse this value in the query
|
||||
`jsonpath "$.errors[{{index}}].id"`.
|
||||
|
||||
## Types
|
||||
|
||||
Variable are typed, and can be either string, bool, number, `null` or collections. Depending on the variable type,
|
||||
templates can be rendered differently. Let's say we have captured an integer value into a variable named
|
||||
`count`:
|
||||
|
||||
```hurl
|
||||
GET https://sample/counter
|
||||
HTTP/* 200
|
||||
[Captures]
|
||||
count: jsonpath "$.results[0]"
|
||||
```
|
||||
|
||||
The following entry:
|
||||
|
||||
```hurl
|
||||
GET https://sample/counter/{{count}}
|
||||
HTTP/* 200
|
||||
[Asserts]
|
||||
jsonpath "$.id" == "{{count}}"
|
||||
```
|
||||
|
||||
will be rendered at runtime to:
|
||||
|
||||
```hurl
|
||||
GET https://sample/counter/458
|
||||
HTTP/* 200
|
||||
[Asserts]
|
||||
jsonpath "$.id" == "458"
|
||||
```
|
||||
|
||||
resulting in a comparison between the [JSONPath] expression and a string value.
|
||||
|
||||
On the other hand, the following assert:
|
||||
|
||||
```hurl
|
||||
GET https://sample/counter/{{count}}
|
||||
HTTP/* 200
|
||||
[Asserts]
|
||||
jsonpath "$.index" == {{count}}
|
||||
```
|
||||
|
||||
will be rendered at runtime to:
|
||||
|
||||
```hurl
|
||||
GET https://sample/counter/458
|
||||
HTTP/* 200
|
||||
[Asserts]
|
||||
jsonpath "$.index" == 458
|
||||
```
|
||||
|
||||
resulting in a comparison between the [JSONPath] expression and an integer value.
|
||||
|
||||
So if you want to use typed values (in asserts for instances), you can use `{{my_var}}`.
|
||||
If you're interested in the string representation of a variable, you can surround the variable with double quotes
|
||||
, as in `"{{my_var}}"`.
|
||||
|
||||
> When there is no possible ambiguities, like using a variable in an url, or
|
||||
> in a header, you can omit the double quotes. The value will always be rendered
|
||||
> as a string.
|
||||
|
||||
## Injecting Variables
|
||||
|
||||
Variables can also be injected in a Hurl file:
|
||||
|
||||
- by using [`--variable` option]
|
||||
- by using [`--variables-file` option]
|
||||
- by defining environment variables, for instance `HURL_foo=bar`
|
||||
|
||||
Lets' see how to inject variables, given this `test.hurl`:
|
||||
|
||||
```hurl
|
||||
GET https://{{host}}/{{id}}/status
|
||||
HTTP/1.1 304
|
||||
|
||||
GET https://{{host}}/health
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
### `variable` option
|
||||
|
||||
Variable can be defined with command line option:
|
||||
|
||||
```shell
|
||||
$ hurl --variable host=example.net --variable id=1234 test.hurl
|
||||
```
|
||||
|
||||
|
||||
### `variables-file` option
|
||||
|
||||
We can also define all injected variables in a file:
|
||||
|
||||
```shell
|
||||
$ hurl --variables-files vars.env test.hurl
|
||||
```
|
||||
|
||||
where `vars.env` is
|
||||
|
||||
```
|
||||
host=example.net
|
||||
id=1234
|
||||
```
|
||||
|
||||
### Environment variable
|
||||
|
||||
Finally, we can use environment variables in the form of `HURL_name=value`:
|
||||
|
||||
```shell
|
||||
$ export HURL_host=example.net
|
||||
$ export HURL_id=1234
|
||||
$ hurl test.hurl
|
||||
```
|
||||
|
||||
|
||||
|
||||
## Templating Body
|
||||
|
||||
Using templates with [JSON body] or [XML body] is not currently supported in Hurl.
|
||||
Besides, you can use templates in [raw string body] with variables to send a JSON or XML body:
|
||||
|
||||
~~~hurl
|
||||
PUT https://example.org/api/hits
|
||||
Content-Type: application/json
|
||||
```
|
||||
{
|
||||
"key0": "{{a_string}}",
|
||||
"key1": {{a_bool}},
|
||||
"key2": {{a_null}},
|
||||
"key3": {{a_number}}
|
||||
}
|
||||
```
|
||||
~~~
|
||||
|
||||
Variables can be initialized via command line:
|
||||
|
||||
```shell
|
||||
$ hurl --variable a_string=apple --variable a_bool=true --variable a_null=null --variable a_number=42 test.hurl
|
||||
```
|
||||
|
||||
Resulting in a PUT request with the following JSON body:
|
||||
|
||||
```
|
||||
{
|
||||
"key0": "apple",
|
||||
"key1": true,
|
||||
"key2": null,
|
||||
"key3": 42
|
||||
}
|
||||
```
|
||||
|
||||
[`--variable` option]: /docs/man-page.md#variable
|
||||
[`--variables-file` option]: /docs/man-page.md#variables-file
|
||||
[CSRF token]: https://en.wikipedia.org/wiki/Cross-site_request_forgery
|
||||
[JSONPath]: /docs/asserting-response.md#jsonpath-assert
|
||||
[JSON body]: /docs/request.md#json-body
|
||||
[XML body]: /docs/request.md#xml-body
|
||||
[raw string body]: /docs/request.md#raw-string-body
|
310
docs/tutorial/adding-asserts.md
Normal file
@ -0,0 +1,310 @@
|
||||
# Adding Asserts
|
||||
|
||||
Our basic Hurl file is now:
|
||||
|
||||
```hurl
|
||||
# Our first Hurl file, just checking
|
||||
# that our server is up and running.
|
||||
GET http://localhost:8080
|
||||
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
Currently, we're just checking that our home page is responding with a `200 OK` HTTP status code.
|
||||
But we also want to check the _content_ of our home page, to ensure that everything is ok. To check the response
|
||||
of an HTTP request with Hurl, we have to _describe_ tests that the response content must pass.
|
||||
|
||||
> We're already implicitly asserting the response with the line\
|
||||
> `HTTP/1.1 200`\
|
||||
> On one hand, we are checking that the HTTP protocol version is 1.1; on the other hand, we are checking
|
||||
> that the HTTP status response code is 200.
|
||||
|
||||
To do so, we're going to use [asserts].
|
||||
|
||||
As our endpoint <http://localhost:8080> is serving HTML content, it makes sense to use [XPath asserts].
|
||||
If we want to test a REST api or any sort of api that serves JSON content,
|
||||
we could use [JSONPath asserts] instead. There are other type of asserts but every one shares
|
||||
the same structure. So, let's look how to write a [XPath asserts].
|
||||
|
||||
## HTML Body Test
|
||||
|
||||
### Structure of an assert
|
||||
|
||||
<div class="schema-container schema-container u-font-size-1 u-font-size-2-sm u-font-size-3-md">
|
||||
<div class="schema">
|
||||
<span class="schema-token schema-color-2">xpath "string(//h1)"<span class="schema-label">query</span></span>
|
||||
<span class="schema-token schema-color-1">contains<span class="schema-label">predicate type</span></span>
|
||||
<span class="schema-token schema-color-3">"Hello"<span class="schema-label">predicate value</span></span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
An assert consists of a query and a predicate. As we want to test the value of the HTML title tag, we're
|
||||
going to use the [XPath expression] `string(//head/title)`.
|
||||
|
||||
1. Asserts are written in an Asserts section, so modify `basic.hurl` file:
|
||||
|
||||
```hurl
|
||||
# Our first Hurl file, just checking
|
||||
# that our server is up and running.
|
||||
GET http://localhost:8080
|
||||
|
||||
HTTP/1.1 200
|
||||
[Asserts]
|
||||
xpath "string(//head/title)" == "Welcome to Quiz!"
|
||||
```
|
||||
|
||||
2. Run `basic.hurl`:
|
||||
|
||||
```shell
|
||||
$ hurl basic.hurl
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Welcome to Quiz!</title>
|
||||
<!-- <link rel="stylesheet" href="style.css">
|
||||
<script src="script.js"></script>-->
|
||||
</head>
|
||||
....
|
||||
</html>
|
||||
```
|
||||
|
||||
We get the content of the page and there is no error so everything is good!
|
||||
|
||||
3. Modify the predicate value to "Welcome to Quaz!"
|
||||
|
||||
```hurl
|
||||
# Our first Hurl file, just checking
|
||||
# that our server is up and running.
|
||||
GET http://localhost:8080
|
||||
|
||||
HTTP/1.1 200
|
||||
[Asserts]
|
||||
xpath "string(//head/title)" == "Welcome to Quaz!"
|
||||
```
|
||||
|
||||
4. Run `basic.hurl`:
|
||||
|
||||
```shell
|
||||
$ hurl basic.hurl
|
||||
error: Assert Failure
|
||||
--> integration/basic.hurl:6:0
|
||||
|
|
||||
6 | xpath "string(//head/title)" == "Welcome to Quaz!"
|
||||
| actual: string <Welcome to Quiz!>
|
||||
| expected: string <Welcome to Quaz!>
|
||||
|
|
||||
```
|
||||
|
||||
Hurl has failed now and provides informations on which assert is not valid.
|
||||
|
||||
### Typed predicate
|
||||
|
||||
If we decompose our assert, `xpath "string(//head/title)"` is the XPath query and `== "Welcome to Quiz!"` is our
|
||||
predicate to test the query against. You can note that predicates values are typed:
|
||||
|
||||
- `xpath "string(//head/title)" == "true"`
|
||||
tests that the XPath expression is returning a string, and
|
||||
- `xpath "boolean(//head/title)" == true`
|
||||
tests that the XPath expression is returning a boolean
|
||||
|
||||
Some queries can also return collections. For instance, the XPath expression `//button` is returning all the button
|
||||
elements present in the [DOM]. We can use it to ensure that we have exactly two buttons on our home page,
|
||||
with `count`:
|
||||
|
||||
1. Add a new assert in `basic.hurl` to check the number of buttons:
|
||||
|
||||
```hurl
|
||||
# Checking our home page:
|
||||
GET http://localhost:8080
|
||||
|
||||
HTTP/1.1 200
|
||||
[Asserts]
|
||||
xpath "string(//head/title)" == "Welcome to Quiz!"
|
||||
xpath "//button" count == 2
|
||||
```
|
||||
|
||||
2. We can also check each button's title:
|
||||
|
||||
```hurl
|
||||
# Checking our home page:
|
||||
GET http://localhost:8080
|
||||
|
||||
HTTP/1.1 200
|
||||
[Asserts]
|
||||
xpath "string(//head/title)" == "Welcome to Quiz!"
|
||||
xpath "//button" count == 2
|
||||
xpath "string((//button)[1])" contains "Play"
|
||||
xpath "string((//button)[2])" contains "Create"
|
||||
```
|
||||
|
||||
> XPath queries can sometimes be a little tricky to write but modern browsers can help writing these expressions.
|
||||
> Try open the Javascript console of your browser (Firefox, Safari or Chrome) and type `$x("string(//head/title)")`
|
||||
> then press Return. You should see the result of your XPath query.
|
||||
|
||||
3. Run `basic.hurl` and check that every assert has been successful:
|
||||
|
||||
```shell
|
||||
$ hurl basic.hurl
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Welcome to Quiz!</title>
|
||||
<!-- <link rel="stylesheet" href="style.css">
|
||||
<script src="script.js"></script>-->
|
||||
</head>
|
||||
....
|
||||
</html>
|
||||
```
|
||||
|
||||
|
||||
## HTTP Headers Test
|
||||
|
||||
We are also going to add tests on the HTTP response headers with explicit [`header` asserts].
|
||||
As our endpoint is serving UTF-8 encoded HTML content, we can check the value of the [`Content-Type` response header].
|
||||
|
||||
1. Add a new assert at the end of `basic.hurl` to test the value of the `Content-Type` HTTP header:
|
||||
|
||||
```hurl
|
||||
# Checking our home page:
|
||||
GET http://localhost:8080
|
||||
|
||||
HTTP/1.1 200
|
||||
[Asserts]
|
||||
xpath "string(//head/title)" == "Welcome to Quiz!"
|
||||
xpath "//button" count == 2
|
||||
xpath "string((//button)[1])" contains "Play"
|
||||
xpath "string((//button)[2])" contains "Create"
|
||||
# Testing HTTP response headers:
|
||||
header "Content-Type" == "text/html;charset=UTF-8"
|
||||
```
|
||||
|
||||
> Our HTTP response has only one `Content-Type` header, so we're testing this header value as string.
|
||||
> The same header could be present multiple times in an HTTP response, with different values.
|
||||
> In this case, the `header` query will return collections and could be tested with
|
||||
> `countEqual` or `include` predicates.
|
||||
|
||||
For HTTP headers, we can also use an [implicit header assert]. You can use indifferently implicit or
|
||||
explicit header assert: the implicit one allows you to only check the exact value of the header,
|
||||
while the explicit one allows you to use other [predicates] (like `contains`, `startsWith`, `matches` etc...).
|
||||
|
||||
2. Replace the explicit assert with [implicit header assert]:
|
||||
|
||||
```hurl
|
||||
# Checking our home page:
|
||||
GET http://localhost:8080
|
||||
|
||||
HTTP/1.1 200
|
||||
# Implicitely testing response headers:
|
||||
Content-Type: text/html;charset=UTF-8
|
||||
[Asserts]
|
||||
xpath "string(//head/title)" == "Welcome to Quiz!"
|
||||
xpath "//button" count == 2
|
||||
xpath "string((//button)[1])" contains "Play"
|
||||
xpath "string((//button)[2])" contains "Create"
|
||||
```
|
||||
|
||||
The line `Content-Type: text/html;charset=UTF-8` is testing that the header `Content-Type` is present in the response,
|
||||
and its value must be exactly `text/html;charset=UTF-8`.
|
||||
|
||||
> In the implicit assert, quotes in the header value are part of the value itself.
|
||||
|
||||
Finally, we want to check that our server is creating a new session.
|
||||
|
||||
When creating a new session, our Spring Boot application should return a [`Set-Cookie` HTTP response header].
|
||||
So to test it, we can modify our Hurl file with another header assert.
|
||||
|
||||
3. Add a header assert on `Set-Cookie` header:
|
||||
|
||||
```hurl
|
||||
# Checking our home page:
|
||||
GET http://localhost:8080
|
||||
|
||||
HTTP/1.1 200
|
||||
[Asserts]
|
||||
xpath "string(//head/title)" == "Welcome to Quiz!"
|
||||
xpath "//button" count == 2
|
||||
xpath "string((//button)[1])" contains "Play"
|
||||
xpath "string((//button)[2])" contains "Create"
|
||||
# Testing HTTP response headers:
|
||||
header "Content-Type" == "text/html;charset=UTF-8"
|
||||
header "Set-Cookie" startsWith "JSESSIONID="
|
||||
```
|
||||
|
||||
For `Set-Cookie` header, we can use the specialized [Cookie assert].
|
||||
Not only we'll be able to easily tests [cookie attributes] (like `HttpOnly`, or `SameSite`), but also
|
||||
it simplifies tests on cookies, particularly when there are multiple `Set-Cookie` header in the HTTP response.
|
||||
|
||||
> Hurl is not a browser, one can see it as syntactic sugar over [curl]. Hurl
|
||||
> has no Javascript runtime and stays close to the HTTP layer. With others tools relying on headless browser, it can be
|
||||
> difficult to access some HTTP requests attributes, like `Set-Cookie` header.
|
||||
|
||||
So to test that our server is responding a `HttpOnly` session cookie, we can modify our file and add cookie asserts.
|
||||
|
||||
4. Add two cookie asserts on the cookie `JESSIONID`:
|
||||
|
||||
```hurl
|
||||
# Checking our home page:
|
||||
GET http://localhost:8080
|
||||
|
||||
HTTP/1.1 200
|
||||
[Asserts]
|
||||
xpath "string(//head/title)" == "Welcome to Quiz!"
|
||||
xpath "//button" count == 2
|
||||
xpath "string((//button)[1])" contains "Play"
|
||||
xpath "string((//button)[2])" contains "Create"
|
||||
# Testing content type:
|
||||
header "Content-Type" == "text/html;charset=UTF-8"
|
||||
# Testing session cookie:
|
||||
cookie "JSESSIONID" exists
|
||||
cookie "JSESSIONID[HttpOnly]" exists
|
||||
```
|
||||
|
||||
5. Run `basic.hurl` and check that every assert has been successful:
|
||||
|
||||
```shell
|
||||
$ hurl basic.hurl
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Welcome to Quiz!</title>
|
||||
<!-- <link rel="stylesheet" href="style.css">
|
||||
<script src="script.js"></script>-->
|
||||
</head>
|
||||
....
|
||||
</html>
|
||||
```
|
||||
|
||||
## Performance Test
|
||||
|
||||
> TODO: add duration assert
|
||||
|
||||
## Recap
|
||||
|
||||
Our Hurl file is now around 10 lines long, but we're already testing a lot on our home page:
|
||||
|
||||
- we are testing that our home page is responding with a `200 OK`
|
||||
- we are checking the basic structure of our page: a title, 2 buttons
|
||||
- we are checking that the content type is UTF-8 HTML
|
||||
- we are checking that our server has created a session, and that the cookie session has the `HttpOnly` attribute
|
||||
|
||||
You can see now that launching and running requests with Hurl is fast, _really_ fast.
|
||||
|
||||
In the next session, we're going to see how we chain request tests, and how we add basic check on a REST api.
|
||||
|
||||
[asserts]: /docs/asserting-response.md
|
||||
[XPath asserts]: /docs/asserting-response.md#xpath-assert
|
||||
[JSONPath asserts]: /docs/asserting-response.md#jsonpath-assert
|
||||
[XPath expression]: https://en.wikipedia.org/wiki/XPath
|
||||
[DOM]: https://en.wikipedia.org/wiki/Document_Object_Model
|
||||
[`header` asserts]: /docs/asserting-response.md#header-assert
|
||||
[`Content-Type` response header]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Type
|
||||
[implicit header assert]: /docs/asserting-response.md#headers
|
||||
[predicates]: /docs/asserting-response.md#predicates
|
||||
[`Set-Cookie` HTTP response header]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies#creating_cookies
|
||||
[Cookie assert]: /docs/asserting-response.md#cookie-assert
|
||||
[cookie attributes]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#attributes
|
||||
[curl]: https://curl.se
|
||||
|
328
docs/tutorial/captures.md
Normal file
@ -0,0 +1,328 @@
|
||||
# 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 what captures
|
||||
are all about.
|
||||
|
||||
## Capturing a CSRF Token
|
||||
|
||||
In our quiz application, user can create a quiz at <http://localhost:8080/new-quiz>.
|
||||
The HTML page is a [form] where the user can input:
|
||||
|
||||
- a required name
|
||||
- an optional email
|
||||
- the 5 questions that will form the new quiz
|
||||
|
||||
If we look at the page HTML content, we can see an HTML form:
|
||||
|
||||
```html
|
||||
<form action="/new-quiz" method="POST">
|
||||
...
|
||||
<input id="name" type="text" name="name" minlength="4" maxlength="32" value="" required>...
|
||||
<input id="email" type="email" name="email" value="">...
|
||||
<select name="question0" id="question0" required="">...
|
||||
<option value="">--Please choose a question--</option>
|
||||
<option value="0fec576c">Which astronaut did NOT walk on the moon?</option>
|
||||
<option value="dd894cca">If you multiply the width of a rectangle by the height, what do you get?</option>
|
||||
<option value="16f897ab">How far does the Moon move away from Earth each year?</option>
|
||||
...
|
||||
</select>
|
||||
<select name="question1" id="question1" required="">...
|
||||
</select>
|
||||
...
|
||||
</form>
|
||||
```
|
||||
|
||||
When the user clicks on 'Create' button, a POST request is sent with form values for the newly
|
||||
created quiz: the author's name, an optional email and the list of 5 question ids. Our server implements a
|
||||
[_Post / Redirect / Get pattern_]: if the POST submission is successful, the user is redirected to a detail
|
||||
page of the new quiz, indicating creation success.
|
||||
|
||||
Let's try to test it!
|
||||
|
||||
Form values can be sent using a [Form parameters section], with each key followed by it
|
||||
corresponding value.
|
||||
|
||||
1. Create a new file named `create-quiz.hurl`:
|
||||
|
||||
```hurl
|
||||
POST http://localhost:8080/new-quiz
|
||||
[FormParams]
|
||||
name: Simpson
|
||||
question0: 16f897ab
|
||||
question1: dd894cca
|
||||
question2: 4edc1fdb
|
||||
question3: 37b9eff3
|
||||
question4: 0fec576c
|
||||
|
||||
HTTP/1.1 302
|
||||
```
|
||||
|
||||
> When sending form datas with a Form parameters section, you don't need to set the
|
||||
> `Content-Type` HTTP header: Hurl enfers that the content type of the request is `application/x-www-form-urlencoded`.
|
||||
|
||||
2. Run `create-quiz.hurl`:
|
||||
|
||||
```shell
|
||||
$ hurl --test create-quiz.hurl
|
||||
create-quiz.hurl: RUNNING [1/1]
|
||||
error: Assert Status
|
||||
--> integration/create-quiz.hurl:9:10
|
||||
|
|
||||
9 | HTTP/1.1 302
|
||||
| ^^^ actual value is <403>
|
||||
|
|
||||
create-quiz.hurl: FAILURE
|
||||
--------------------------------------------------------------------------------
|
||||
Executed: 1
|
||||
Succeeded: 0 (0.0%)
|
||||
Failed: 1 (100.0%)
|
||||
Duration: 13ms
|
||||
```
|
||||
|
||||
This is unexpected! Our test is failing, we're not redirected to the new quiz detail page.
|
||||
|
||||
The reason is quite simple, let's look more precisely at our HTML form:
|
||||
|
||||
```html
|
||||
<form action="/new-quiz" method="POST">
|
||||
...
|
||||
<button type="submit">Create</button>
|
||||
<input type="hidden" name="_csrf" value="7d4da7d7-2970-442a-adc3-55e5e6ba038a">
|
||||
</form>
|
||||
```
|
||||
|
||||
The server quiz creation endpoint is protected by a [CSRF token]. In a browser, when the user is creating a new quiz by
|
||||
sending a POST request, a token is sent along the new quiz values. This token is generated server-side, and embedded
|
||||
in the HTML. When the POST request is made, our quiz application 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
|
||||
Form parameters 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:8080/new-quiz> and capture the CSRF token
|
||||
- chain with a POST request that contains our quiz value, and our captured CSRF token
|
||||
- check that the POST response is a redirection, i.e. a [`302 Found`] to the quiz detail page
|
||||
|
||||
So, let's go!
|
||||
|
||||
### How to capture values
|
||||
|
||||
1. Modify `create-quiz.hurl`:
|
||||
|
||||
```hurl
|
||||
# First, get the quiz creation page to capture
|
||||
# the CSRF token (see https://en.wikipedia.org/wiki/Cross-site_request_forgery)
|
||||
GET http://localhost:8080/new-quiz
|
||||
|
||||
HTTP/1.1 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.
|
||||
|
||||
2. Add a POST request using `csrf_token` variable in `create-quiz.hurl`:
|
||||
|
||||
```hurl
|
||||
# First, get the quiz creation page to capture
|
||||
# the CSRF token (see https://en.wikipedia.org/wiki/Cross-site_request_forgery):
|
||||
GET http://localhost:8080/new-quiz
|
||||
|
||||
HTTP/1.1 200
|
||||
[Captures]
|
||||
csrf_token: xpath "string(//input[@name='_csrf']/@value)"
|
||||
|
||||
# Create a new quiz, using the captured CSRF token:
|
||||
POST http://localhost:8080/new-quiz
|
||||
[FormParams]
|
||||
name: Simpson
|
||||
question0: 16f897ab
|
||||
question1: dd894cca
|
||||
question2: 4edc1fdb
|
||||
question3: 37b9eff3
|
||||
question4: 0fec576c
|
||||
_csrf: {{csrf_token}}
|
||||
|
||||
HTTP/1.1 302
|
||||
```
|
||||
|
||||
3. Run `create-quiz.hurl` and verify everything is ok:
|
||||
|
||||
```shell
|
||||
$ hurl --test create-quiz.hurl
|
||||
create-quiz.hurl: RUNNING [1/1]
|
||||
create-quiz.hurl: SUCCESS
|
||||
--------------------------------------------------------------------------------
|
||||
Executed: 1
|
||||
Succeeded: 1 (100.0%)
|
||||
Failed: 0 (0.0%)
|
||||
Duration: 33ms
|
||||
```
|
||||
|
||||
## 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.
|
||||
|
||||
What if we want to follow redirections? We can simply use captures!
|
||||
|
||||
After having created a new quiz, we would like to test the page where the user has been redirected.
|
||||
This is really simple and can be achieved with a [header capture]: on the response to the POST creation request, we
|
||||
are going to capture the [`Location`] header, which indicates the redirection url target, and use it to
|
||||
go to the next page.
|
||||
|
||||
1. Add a new header capture to capture the `Location` header in a variable named `detail_url`:
|
||||
|
||||
```hurl
|
||||
# First, get the quiz creation page to capture
|
||||
# ...
|
||||
|
||||
# Create a new quiz, using the captured CSRF token:
|
||||
POST http://localhost:8080/new-quiz
|
||||
[FormParams]
|
||||
name: Simpson
|
||||
question0: 16f897ab
|
||||
question1: dd894cca
|
||||
question2: 4edc1fdb
|
||||
question3: 37b9eff3
|
||||
question4: 0fec576c
|
||||
_csrf: {{csrf_token}}
|
||||
|
||||
HTTP/1.1 302
|
||||
[Captures]
|
||||
detail_url: header "Location"
|
||||
```
|
||||
|
||||
Captures and asserts can be mixed in the same response spec. For example, we can check that the redirection after
|
||||
the quiz creation matches a certain url, and add a header assert with a matches predicate.
|
||||
|
||||
2. Add a header assert on the POST response to check the redirection url:
|
||||
|
||||
```hurl
|
||||
# First, get the quiz creation page to capture
|
||||
# ...
|
||||
|
||||
# Create a new quiz, using the captured CSRF token:
|
||||
POST http://localhost:8080/new-quiz
|
||||
[FormParams]
|
||||
name: Simpson
|
||||
question0: 16f897ab
|
||||
question1: dd894cca
|
||||
question2: 4edc1fdb
|
||||
question3: 37b9eff3
|
||||
question4: 0fec576c
|
||||
_csrf: {{csrf_token}}
|
||||
|
||||
HTTP/1.1 302
|
||||
[Captures]
|
||||
detail_url: header "Location"
|
||||
[Asserts]
|
||||
header "Location" matches "/quiz/detail/[a-f0-9]{8}"
|
||||
```
|
||||
|
||||
3. Add a request to get the detail page where the user has been redirected:
|
||||
|
||||
```hurl
|
||||
# First, get the quiz creation page to capture
|
||||
# ...
|
||||
|
||||
# Create a new quiz, using the captured CSRF token:
|
||||
# ...
|
||||
|
||||
# Open the newly created quiz detail page:
|
||||
GET {{detail_url}}
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
4. Run `create-quiz.hurl` and verify everything is ok:
|
||||
|
||||
```shell
|
||||
$ hurl --test create-quiz.hurl
|
||||
create-quiz.hurl: RUNNING [1/1]
|
||||
create-quiz.hurl: SUCCESS
|
||||
--------------------------------------------------------------------------------
|
||||
Executed: 1
|
||||
Succeeded: 1 (100.0%)
|
||||
Failed: 0 (0.0%)
|
||||
Duration: 41ms
|
||||
```
|
||||
|
||||
|
||||
> You can force Hurl to follow redirection by using [`-L / --location` option].
|
||||
> In this case, asserts and captures will be run against the last redirection step.
|
||||
|
||||
|
||||
## Recap
|
||||
|
||||
So, our test file `create-quiz.hurl` is now:
|
||||
|
||||
```hurl
|
||||
# First, get the quiz creation page to capture
|
||||
# the CSRF token (see https://en.wikipedia.org/wiki/Cross-site_request_forgery)
|
||||
GET http://localhost:8080/new-quiz
|
||||
|
||||
HTTP/1.1 200
|
||||
[Captures]
|
||||
csrf_token: xpath "string(//input[@name='_csrf']/@value)"
|
||||
|
||||
# Create a new quiz, using the captured CSRF token.
|
||||
POST http://localhost:8080/new-quiz
|
||||
[FormParams]
|
||||
name: Simpson
|
||||
question0: 16f897ab
|
||||
question1: dd894cca
|
||||
question2: 4edc1fdb
|
||||
question3: 37b9eff3
|
||||
question4: 0fec576c
|
||||
_csrf: {{csrf_token}}
|
||||
|
||||
HTTP/1.1 302
|
||||
[Captures]
|
||||
detail_url: header "Location"
|
||||
[Asserts]
|
||||
header "Location" matches "/quiz/detail/[a-f0-9]{8}"
|
||||
|
||||
# Open the newly created quiz detail page:
|
||||
GET {{detail_url}}
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
We have seen how to [capture response data] in a variable and use it in others request.
|
||||
Captures and asserts share the sames 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
|
||||
[`-L / --location` option]: /docs/man-page.md#location
|
||||
[capture response data]: /docs/capturing-response.md
|
337
docs/tutorial/chaining-requests.md
Normal file
@ -0,0 +1,337 @@
|
||||
# Chaining Requests
|
||||
|
||||
## Adding Another Request
|
||||
|
||||
Our basic Hurl file is for the moment:
|
||||
|
||||
```hurl
|
||||
# Checking our home page:
|
||||
GET http://localhost:8080
|
||||
|
||||
HTTP/1.1 200
|
||||
[Asserts]
|
||||
xpath "string(//head/title)" == "Welcome to Quiz!"
|
||||
xpath "//button" count == 2
|
||||
xpath "string((//button)[1])" contains "Play"
|
||||
xpath "string((//button)[2])" contains "Create"
|
||||
# Testing content type:
|
||||
header "Content-Type" == "text/html;charset=UTF-8"
|
||||
# Testing session cookie:
|
||||
cookie "JSESSIONID" exists
|
||||
cookie "JSESSIONID[HttpOnly]" exists
|
||||
```
|
||||
|
||||
We're only running one HTTP request and have already added lots of tests on the response. Don't hesitate to add
|
||||
many tests, the more asserts you will write, the less fragile will be your tests suite.
|
||||
|
||||
Now, we want to perform other HTTP requests and keep adding tests. In the same file, we can simply write another
|
||||
request following our first request. Let's say we want to test that we have a [404 page] on a broken link:
|
||||
|
||||
1. Modify `basic.hurl` to add a second request on a broken url:
|
||||
|
||||
```hurl
|
||||
# Checking our home page:
|
||||
GET http://localhost:8080
|
||||
|
||||
HTTP/1.1 200
|
||||
[Asserts]
|
||||
xpath "string(//head/title)" == "Welcome to Quiz!"
|
||||
xpath "//button" count == 2
|
||||
xpath "string((//button)[1])" contains "Play"
|
||||
xpath "string((//button)[2])" contains "Create"
|
||||
# Testing content type:
|
||||
header "Content-Type" == "text/html;charset=UTF-8"
|
||||
# Testing session cookie:
|
||||
cookie "JSESSIONID" exists
|
||||
cookie "JSESSIONID[HttpOnly]" exists
|
||||
|
||||
# Check that we have a 404 response for broken links:
|
||||
GET http://localhost:8080/not-found
|
||||
|
||||
HTTP/1.1 404
|
||||
[Asserts]
|
||||
header "Content-Type" == "text/html;charset=UTF-8"
|
||||
xpath "string(//h1)" == "Error 404, Page not Found!"
|
||||
```
|
||||
|
||||
Now, we have two entries in our Hurl file: each entry is composed of one request and one expected response
|
||||
description.
|
||||
|
||||
> In a Hurl file, response description are optional. We could also have written
|
||||
> our file with only requests:
|
||||
>
|
||||
> ```hurl
|
||||
> GET http://localhost:8080
|
||||
> GET http://localhost:8080/not-found
|
||||
> ```
|
||||
> But it would have performed nearly zero test. This type of Hurl file can be useful
|
||||
> if you use Hurl to get data for instance.
|
||||
|
||||
2. Run `basic.hurl`:
|
||||
|
||||
```shell
|
||||
$ hurl basic.hurl
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title></title>
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
<!--<script src="script.js"></script>-->
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<img class="logo" src="/quiz.svg" alt="Quiz logo">
|
||||
</div>
|
||||
<h1>Error 404, Page not Found!</h1>
|
||||
|
||||
<a href="/">Quiz Home</a>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
We can see that the test is still ok, but now, Hurl outputs the response of the last HTTP request (i.e.
|
||||
the content of our 404 page). This is useful when you want to get data from a server, and you need to
|
||||
perform additional steps (like login, confirmation etc...) before being able to call your last request.
|
||||
|
||||
In our tutorial, we're simply interested to verify the success or failure of our integration tests.
|
||||
So, first, we'll remove the standard output (if a test is broken, we'll still have the error output).
|
||||
|
||||
3. Run `basic.hurl` while redirecting the standard ouput to `/dev/null`:
|
||||
|
||||
```shell
|
||||
$ hurl basic.hurl > /dev/null
|
||||
```
|
||||
|
||||
Then, we can also use [`--progress`] and [`--summary`] option to give us some feedback on
|
||||
our tests progression and a simple summary:
|
||||
|
||||
4. Run `basic.hurl` with `--progress` and `--summary` options:
|
||||
|
||||
```shell
|
||||
$ hurl --progress --summary basic.hurl > /dev/null
|
||||
basic.hurl: RUNNING [1/1]
|
||||
basic.hurl: SUCCESS
|
||||
--------------------------------------------------------------------------------
|
||||
Executed: 1
|
||||
Succeeded: 1 (100.0%)
|
||||
Failed: 0 (0.0%)
|
||||
Duration: 40ms
|
||||
```
|
||||
|
||||
Finally, we can use the [`--test`] option that is a shortcut for no output,
|
||||
using [`--progress`] and [`--summary`] options:
|
||||
|
||||
5. Run `basic.hurl` with `--test` option:
|
||||
|
||||
```shell
|
||||
$ hurl --test basic.hurl
|
||||
basic.hurl: RUNNING [1/1]
|
||||
basic.hurl: SUCCESS
|
||||
--------------------------------------------------------------------------------
|
||||
Executed: 1
|
||||
Succeeded: 1 (100.0%)
|
||||
Failed: 0 (0.0%)
|
||||
Duration: 40ms
|
||||
```
|
||||
|
||||
From now on, we will always use `--test` to run our tests files.
|
||||
|
||||
## Test REST Api
|
||||
|
||||
So far, we have tested two HTML endpoints. We're going to see now how to test a REST api.
|
||||
|
||||
Our quiz application exposes a health REST resource, available at <http://localhost:8080/api/health>.
|
||||
Let's use Hurl to check it.
|
||||
|
||||
1. In a shell, use Hurl to test the </api/health> endpoint:
|
||||
|
||||
```shell
|
||||
$ echo 'GET http://localhost:8080/api/health' | hurl
|
||||
{"status":"RUNNING","reportedDate":"2021-06-06T14:08:27Z","healthy":true,"operationId":425276758}
|
||||
```
|
||||
|
||||
> Being a classic CLI application, we can use the standard input with Hurl to provide requests
|
||||
> to be executed, instead of a file.
|
||||
|
||||
So, our health api returns this JSON resource:
|
||||
|
||||
```
|
||||
{
|
||||
"status": "RUNNING",
|
||||
"reportedDate": "2021-06-06T14:08:27Z",
|
||||
"healthy": true,
|
||||
"operationId": 425276758
|
||||
}
|
||||
```
|
||||
|
||||
We can test it with a [JsonPath assert]. JsonPath asserts have the same structure as XPath asserts: a query
|
||||
followed by a predicate. [JsonPath query] are simple expressions to inspect a JSON object.
|
||||
|
||||
2. Modify `basic.hurl` to add a third request that asserts our </api/health> REST api:
|
||||
|
||||
```hurl
|
||||
# Checking our home page:
|
||||
# ...
|
||||
|
||||
# Check that we have a 404 response for broken links:
|
||||
# ...
|
||||
|
||||
# Check our health api:
|
||||
GET http://localhost:8080/api/health
|
||||
|
||||
HTTP/1.1 200
|
||||
[Asserts]
|
||||
header "Content-Type" == "application/json"
|
||||
jsonpath "$.status" == "RUNNING"
|
||||
jsonpath "$.healthy" == true
|
||||
jsonpath "$.operationId" exists
|
||||
```
|
||||
|
||||
Like XPath assert, JsonPath predicates values are typed. String, boolean, number and
|
||||
collections are supported. Let's practice it by using another api. In our Quiz model, a
|
||||
quiz is a set of questions, and a question resource is exposed through a
|
||||
REST api exposed et <http://localhost:8080/api/questions>. We can use it to add checks on getting questions
|
||||
through the api endpoint.
|
||||
|
||||
3. Add JSONPath asserts on the </api/questions> REST apis:
|
||||
|
||||
```hurl
|
||||
# Checking our home page:
|
||||
# ...
|
||||
|
||||
# Check that we have a 404 response for broken links:
|
||||
# ...
|
||||
|
||||
# Check our health api:
|
||||
# ...
|
||||
|
||||
# Check question api:
|
||||
GET http://localhost:8080/api/questions?offset=0&size=20&sort=oldest
|
||||
|
||||
HTTP/1.1 200
|
||||
[Asserts]
|
||||
header "Content-Type" == "application/json"
|
||||
jsonpath "$" count == 20
|
||||
jsonpath "$[0].id" == "c0d80047"
|
||||
jsonpath "$[0].title" == "What is a pennyroyal?"
|
||||
```
|
||||
|
||||
> To keep things simple in this tutorial, we have hardcoded mocked data
|
||||
> in our Quiz application. That's something you don't want to do when building
|
||||
> your application, you want to build an app production ready. A better way to
|
||||
> do this should have been to expose a "debug" or "integration" mode on our app
|
||||
> positioned by environnement variables. If our app is launched in "integration" mode,
|
||||
> mocked data is used and asserts can be tested on known values. Our app could also use
|
||||
> a mocked database, configured in our tests suits.
|
||||
|
||||
Note that the question api use query parameters `offset`, `size` and `sort`, that's why we have written the url with
|
||||
query parameters <http://localhost:8080/api/questions?offset=0&size=20&sort=oldest>. We can set the query parameters
|
||||
in the url, or use a [query parameter section].
|
||||
|
||||
4. Use a query parameter section in `basic.hurl`:
|
||||
|
||||
```hurl
|
||||
# Checking our home page:
|
||||
# ...
|
||||
|
||||
# Check that we have a 404 response for broken links:
|
||||
# ...
|
||||
|
||||
# Check our health api:
|
||||
# ...
|
||||
|
||||
# Check question api:
|
||||
GET http://localhost:8080/api/questions
|
||||
[QueryStringParams]
|
||||
offset: 0
|
||||
size: 20
|
||||
sort: oldest
|
||||
|
||||
HTTP/1.1 200
|
||||
[Asserts]
|
||||
header "Content-Type" == "application/json"
|
||||
jsonpath "$" count == 20
|
||||
jsonpath "$[0].id" == "c0d80047"
|
||||
jsonpath "$[0].title" == "What is a pennyroyal?"
|
||||
```
|
||||
|
||||
Finally, our basic Hurl file, with four requests, looks like:
|
||||
|
||||
```hurl
|
||||
# Checking our home page:
|
||||
GET http://localhost:8080
|
||||
|
||||
HTTP/1.1 200
|
||||
[Asserts]
|
||||
xpath "string(//head/title)" == "Welcome to Quiz!"
|
||||
xpath "//button" count == 2
|
||||
xpath "string((//button)[1])" contains "Play"
|
||||
xpath "string((//button)[2])" contains "Create"
|
||||
# Testing content type:
|
||||
header "Content-Type" == "text/html;charset=UTF-8"
|
||||
# Testing session cookie:
|
||||
cookie "JSESSIONID" exists
|
||||
cookie "JSESSIONID[HttpOnly]" exists
|
||||
|
||||
# Check that we have a 404 response for broken links:
|
||||
GET http://localhost:8080/not-found
|
||||
|
||||
HTTP/1.1 404
|
||||
[Asserts]
|
||||
header "Content-Type" == "text/html;charset=UTF-8"
|
||||
xpath "string(//h1)" == "Error 404, Page not Found!"
|
||||
|
||||
# Check our health api:
|
||||
GET http://localhost:8080/api/health
|
||||
|
||||
HTTP/1.1 200
|
||||
[Asserts]
|
||||
header "Content-Type" == "application/json"
|
||||
jsonpath "$.status" == "RUNNING"
|
||||
jsonpath "$.healthy" == true
|
||||
jsonpath "$.operationId" exists
|
||||
|
||||
# Check question api:
|
||||
GET http://localhost:8080/api/questions
|
||||
[QueryStringParams]
|
||||
offset: 0
|
||||
size: 20
|
||||
sort: oldest
|
||||
|
||||
HTTP/1.1 200
|
||||
[Asserts]
|
||||
header "Content-Type" == "application/json"
|
||||
jsonpath "$" count == 20
|
||||
jsonpath "$[0].id" == "c0d80047"
|
||||
jsonpath "$[0].title" == "What is a pennyroyal?"
|
||||
```
|
||||
|
||||
5. Run `basic.hurl` and check that every assert of every request has been successful:
|
||||
|
||||
```shell
|
||||
$ hurl --test basic.hurl
|
||||
basic.hurl: RUNNING [1/1]
|
||||
basic.hurl: SUCCESS
|
||||
--------------------------------------------------------------------------------
|
||||
Executed: 1
|
||||
Succeeded: 1 (100.0%)
|
||||
Failed: 0 (0.0%)
|
||||
Duration: 33ms
|
||||
```
|
||||
|
||||
## Recap
|
||||
|
||||
We can simply chain requests with Hurl, adding asserts on every response. As your Hurl file will grow,
|
||||
don't hesitate to add many comments: your Hurl file will be a valuable and testable documentation
|
||||
for your applications.
|
||||
|
||||
[404 page]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/404
|
||||
[JsonPath assert]: /docs/asserting-response.md#jsonpath-assert
|
||||
[JsonPath query]: https://goessner.net/articles/JsonPath/
|
||||
[query parameter section]: /docs/request.md#query-parameters
|
||||
[`--progress`]: /docs/man-page.md#progress
|
||||
[`--summary`]: /docs/man-page.md#summary
|
||||
[`--test`]: /docs/man-page.md#test
|
270
docs/tutorial/ci-cd-integration.md
Normal file
@ -0,0 +1,270 @@
|
||||
# CI/CD Integration
|
||||
|
||||
Up until now, we have run our tests files locally. Now, we want to integrate
|
||||
them in a CI/CD pipeline (like [GitHub Actions] or [GitLab CI/CD pipelines]). As
|
||||
Hurl is very fast, we're going to run our tests on each commit of our project,
|
||||
drastically improving the project quality.
|
||||
|
||||
A typical web project pipeline is:
|
||||
|
||||
- build the application, run units tests and static code analysis,
|
||||
- publish the application image to a Docker registry,
|
||||
- pull the application image and run integration tests.
|
||||
|
||||
In this workflow, we're testing the same image that will be used and deployed in
|
||||
production.
|
||||
|
||||
> For the tutorial, we are skipping build and publication phases and
|
||||
> only run integration tests on a prebuilt Docker image. To check a complete
|
||||
> project with build, Docker upload/publish and integration tests, go to <https://github.com/jcamiel/quiz>
|
||||
|
||||
In a first step, we're going to write a bash script that will pull our Docker
|
||||
image, launch it and run Hurl tests against it. Once we have checked that this
|
||||
script runs locally, we'll see how to run it automatically in a CI/CD pipeline.
|
||||
|
||||
## Integration Script
|
||||
|
||||
1. First, create a directory name `quiz-project`, add [`integration/basic.hurl`]
|
||||
and [`integration/create-quiz.hurl`] from the previous tutorial to the directory.
|
||||
|
||||
<pre><code class="language-shell">$ mkdir quiz-project
|
||||
$ cd quiz-project
|
||||
$ mkdir integration
|
||||
$ vi integration/basic.hurl
|
||||
|
||||
# Import <a href="https://raw.githubusercontent.com/jcamiel/quiz/master/integration/basic.hurl">basic.hurl</a> here!
|
||||
|
||||
$ vi integration/create-quiz.hurl
|
||||
|
||||
# Import <a href="https://raw.githubusercontent.com/jcamiel/quiz/master/integration/create-quiz.hurl">create-quiz.hurl</a> here!</code></pre>
|
||||
|
||||
Next, we are going to write a first version of our integration script that will
|
||||
just pull the Quiz image and run it:
|
||||
|
||||
2. Create a script named `bin/integration.sh` with the following content:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
set -eu
|
||||
|
||||
echo "Starting Quiz container"
|
||||
docker run --name quiz --rm --detach --publish 8080:8080 ghcr.io/jcamiel/quiz:latest
|
||||
```
|
||||
|
||||
3. Make the script executable and run it:
|
||||
|
||||
```shell
|
||||
$ chmod u+x bin/integration.sh
|
||||
$ bin/integration.sh
|
||||
Starting Quiz container
|
||||
5d311561828d6078e84eb4b8b87dfd5d67bde6d9614ad83860b60cf310438d2a
|
||||
```
|
||||
|
||||
4. Verify that our container is up and running, and stop it.
|
||||
|
||||
```shell
|
||||
$ docker ps
|
||||
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
|
||||
c685f3887cc1 ghcr.io/jcamiel/quiz:latest "java -jar app/quiz.…" 3 seconds ago Up 3 seconds 0.0.0.0:8080->8080/tcp, :::8080->8080/tcp quiz
|
||||
$ docker stop quiz
|
||||
quiz
|
||||
```
|
||||
|
||||
Now, we have a basic script that starts our container. Before adding our
|
||||
integration tests, we need to ensure that our application server is ready: the
|
||||
container have started, but the application server can take a few seconds to be
|
||||
really ready to accept incoming HTTP requests.
|
||||
|
||||
To do so, we can test our health api. With a function `wait_for_url`,
|
||||
we use Hurl to check a given url to return a `200 OK`. We loop on this function
|
||||
until the check succeed. Once the test has succeeded, we stop the container.
|
||||
|
||||
5. Modify `bin/integration.sh` to wait for the application to be ready:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
set -eu
|
||||
|
||||
wait_for_url () {
|
||||
echo "Testing $1"
|
||||
max_in_s=$2
|
||||
delay_in_s=1
|
||||
total_in_s=0
|
||||
while [ $total_in_s -le "$max_in_s" ]
|
||||
do
|
||||
echo "Wait ${total_in_s}s"
|
||||
if (echo -e "GET $1\nHTTP/* 200" | hurl > /dev/null 2>&1;) then
|
||||
return 0
|
||||
fi
|
||||
total_in_s=$(( total_in_s + delay_in_s))
|
||||
sleep $delay_in_s
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
echo "Starting Quiz container"
|
||||
docker run --name quiz --rm --detach --publish 8080:8080 ghcr.io/jcamiel/quiz:latest
|
||||
|
||||
echo "Starting Quiz instance to be ready"
|
||||
wait_for_url 'http://localhost:8080' 60
|
||||
|
||||
echo "Stopping Quiz instance"
|
||||
docker stop quiz
|
||||
```
|
||||
|
||||
We have now the simplest integration test script: it pulls a Quiz image, then starts
|
||||
the container and waits for a `200 OK` response.
|
||||
|
||||
Next, we're going to add our Hurl tests to the script.
|
||||
|
||||
6. Modify `bin/integration.sh` to add integraion tests:
|
||||
|
||||
```bash
|
||||
#!/bin/bash
|
||||
set -eu
|
||||
|
||||
# ...
|
||||
|
||||
echo "Starting Quiz container"
|
||||
# ...
|
||||
|
||||
echo "Starting Quiz instance to be ready"
|
||||
# ...
|
||||
|
||||
echo "Running Hurl tests"
|
||||
hurl integration/*.hurl --test
|
||||
|
||||
echo "Stopping Quiz instance"
|
||||
# ...
|
||||
```
|
||||
|
||||
7. Run [`bin/integration.sh`] to check that our application passes all tests:
|
||||
|
||||
```shell
|
||||
$ bin/integration.sh
|
||||
Starting Quiz container
|
||||
48cf21d193a01651fc42b80648abdb51dc626f31c3f9c8917aea899c68eb4a12
|
||||
Starting Quiz instance to be ready
|
||||
Testing http://localhost:8080
|
||||
Wait 0s
|
||||
Wait 1s
|
||||
Wait 2s
|
||||
Wait 3s
|
||||
Wait 4s
|
||||
Wait 5s
|
||||
Running Hurl tests
|
||||
integration/basic.hurl: RUNNING [1/2]
|
||||
integration/basic.hurl: SUCCESS
|
||||
integration/create-quiz.hurl: RUNNING [2/2]
|
||||
integration/create-quiz.hurl: SUCCESS
|
||||
--------------------------------------------------------------------------------
|
||||
Executed: 2
|
||||
Succeeded: 2 (100.0%)
|
||||
Failed: 0 (0.0%)
|
||||
Duration: 1026ms
|
||||
Stopping Quiz instance
|
||||
quiz
|
||||
```
|
||||
|
||||
Locally, our test suite is now fully functional. As Hurl is very fast, we can use
|
||||
it to ensure that new developments don't have regression. Our next step is to run
|
||||
the integration tests automatically in a CI/CD pipeline. As an example, we're going
|
||||
to create a [GitHub Action].
|
||||
|
||||
## Running Tests with GitHub Action
|
||||
|
||||
1. Create a new empty repository in GitHub, named `quiz-project`:
|
||||
|
||||
<p>
|
||||
<img class="light-img u-drop-shadow u-border" src="/docs/assets/img/github-new-repository-light.png" width="100%" alt="Create new GitHub repository"/>
|
||||
<img class="dark-img u-drop-shadow u-border" src="/docs/assets/img/github-new-repository-dark.png" width="100%" alt="Create new GitHub repository"/>
|
||||
</p>
|
||||
|
||||
|
||||
2. On your computer, create a git repo in `quiz-project` directory and
|
||||
commit the projects files:
|
||||
|
||||
```shell
|
||||
$ git init
|
||||
Initialized empty Git repository in /Users/jc/Documents/Dev/quiz-project/.git/
|
||||
$ git add .
|
||||
$ git commit -m "Add integration tests."
|
||||
[master (root-commit) ea3e5cd] Add integration tests.
|
||||
3 files changed, 146 insertions(+)
|
||||
create mode 100755 bin/integration.sh
|
||||
...
|
||||
$ git branch -M main
|
||||
$ git remote add origin https://github.com/jcamiel/quiz-project.git
|
||||
$ git push -u origin main
|
||||
Enumerating objects: 7, done.
|
||||
Counting objects: 100% (7/7), done.
|
||||
...
|
||||
```
|
||||
|
||||
Next, we are going to add a GitHub Action to our repo. The purpose of this action
|
||||
will be to launch our integration script on each commit.
|
||||
|
||||
3. Create a file in `.github/workflows/ci.yml`:
|
||||
|
||||
```yaml
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Build
|
||||
run: echo "Building app..."
|
||||
- name: Integration test
|
||||
run: |
|
||||
curl -LO https://github.com/Orange-OpenSource/hurl/releases/download/1.4.0/hurl_1.4.0_amd64.deb
|
||||
sudo dpkg -i hurl_1.4.0_amd64.deb
|
||||
bin/integration.sh
|
||||
```
|
||||
|
||||
4. Commit and push the new action:
|
||||
|
||||
```shell
|
||||
$ git add .github/workflows/ci.yml
|
||||
$ git commit -m "Add GitHub action."
|
||||
[main 077d754] Add GitHub action.
|
||||
1 file changed, 19 insertions(+)
|
||||
...
|
||||
$ git push
|
||||
Enumerating objects: 6, done.
|
||||
Counting objects: 100% (6/6), done.
|
||||
...
|
||||
```
|
||||
|
||||
Finally, you can check on GitHub that our action is running:
|
||||
|
||||
<p>
|
||||
<img class="light-img u-drop-shadow u-border" src="/docs/assets/img/github-action-light.png" width="100%" alt="GitHub Action"/>
|
||||
<img class="dark-img u-drop-shadow u-border" src="/docs/assets/img/github-action-dark.png" width="100%" alt="GitHub Action"/>
|
||||
</p>
|
||||
|
||||
## Tests Report
|
||||
|
||||
TBD
|
||||
|
||||
## Recap
|
||||
|
||||
In less than half an hour, we have added a fully CI/CD pipeline to our project.
|
||||
Now, we can add more Hurl tests and start developing new features with confidence!
|
||||
|
||||
|
||||
[`integration/basic.hurl`]: https://raw.githubusercontent.com/jcamiel/quiz/master/integration/basic.hurl
|
||||
[`integration/create-quiz.hurl`]: https://raw.githubusercontent.com/jcamiel/quiz/master/integration/create-quiz.hurl
|
||||
[GitHub Actions]: https://github.com/features/actions
|
||||
[GitHub Action]: https://github.com/features/actions
|
||||
[GitLab CI/CD pipelines]: https://docs.gitlab.com/ee/ci/pipelines/
|
||||
[`bin/integration.sh`]: https://github.com/jcamiel/quiz/blob/master/bin/integration.sh
|
220
docs/tutorial/debug-tips.md
Normal file
@ -0,0 +1,220 @@
|
||||
# Debug Tips
|
||||
|
||||
Now that we have many requests in our test file, let's review some tips to debug the executed HTTP exchanges.
|
||||
|
||||
## Verbose Mode
|
||||
|
||||
We can run our test with [`-v/--verbose` option]. In this mode, each entry is displayed, with debugging
|
||||
information like request HTTP headers, response HTTP headers, cookie storage, duration etc...
|
||||
|
||||
```shell
|
||||
$ hurl -v basic.hurl > /dev/null
|
||||
* fail fast: true
|
||||
* insecure: false
|
||||
* follow redirect: false
|
||||
* max redirect: 50
|
||||
* ------------------------------------------------------------------------------
|
||||
* executing entry 1
|
||||
*
|
||||
* Cookie store:
|
||||
*
|
||||
* Request
|
||||
* GET http://localhost:8080
|
||||
*
|
||||
> GET / HTTP/1.1
|
||||
> Host: localhost:8080
|
||||
> Accept: */*
|
||||
> User-Agent: hurl/1.2.0
|
||||
>
|
||||
< HTTP/1.1 200
|
||||
< Set-Cookie: JSESSIONID=02A8B2F4F604BAE9F016034C13C31282; Path=/; HttpOnly
|
||||
< X-Content-Type-Options: nosniff
|
||||
< X-XSS-Protection: 1; mode=block
|
||||
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
|
||||
< Pragma: no-cache
|
||||
< Expires: 0
|
||||
< X-Frame-Options: DENY
|
||||
< Content-Type: text/html;charset=UTF-8
|
||||
< Content-Language: en-US
|
||||
< Transfer-Encoding: chunked
|
||||
< Date: Fri, 04 Jun 2021 12:24:15 GMT
|
||||
<
|
||||
* Response Time: 16ms
|
||||
*
|
||||
* ------------------------------------------------------------------------------
|
||||
* executing entry 2
|
||||
*
|
||||
* Cookie store:
|
||||
* #HttpOnly_localhost FALSE / FALSE 0 JSESSIONID 02A8B2F4F604BAE9F016034C13C31282
|
||||
*
|
||||
* Request
|
||||
* GET http://localhost:8080/not-found
|
||||
*
|
||||
> GET /not-found HTTP/1.1
|
||||
> Host: localhost:8080
|
||||
> Accept: */*
|
||||
> Cookie: JSESSIONID=02A8B2F4F604BAE9F016034C13C31282
|
||||
> User-Agent: hurl/1.2.0
|
||||
>
|
||||
< HTTP/1.1 404
|
||||
< Vary: Origin
|
||||
< Vary: Access-Control-Request-Method
|
||||
< Vary: Access-Control-Request-Headers
|
||||
< X-Content-Type-Options: nosniff
|
||||
< X-XSS-Protection: 1; mode=block
|
||||
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
|
||||
< Pragma: no-cache
|
||||
< Expires: 0
|
||||
< X-Frame-Options: DENY
|
||||
< Content-Type: text/html;charset=UTF-8
|
||||
< Content-Language: en-US
|
||||
< Transfer-Encoding: chunked
|
||||
< Date: Fri, 04 Jun 2021 12:24:15 GMT
|
||||
<
|
||||
* Response Time: 8ms
|
||||
*
|
||||
...
|
||||
```
|
||||
|
||||
Line beginning by `*` are debug info, lines that begin by `>` are HTTP request headers and lines that begin with
|
||||
`<` are HTTP response headers.
|
||||
|
||||
## Interactive Mode
|
||||
|
||||
We can run the whole Hurl file request by request, with the [`--interactive` option]:
|
||||
|
||||
```shell
|
||||
$ hurl --interactive basic.hurl
|
||||
* fail fast: true
|
||||
* insecure: false
|
||||
* follow redirect: false
|
||||
* max redirect: 50
|
||||
|
||||
interactive mode:
|
||||
Press Q (Quit) or C (Continue)
|
||||
|
||||
* ------------------------------------------------------------------------------
|
||||
* executing entry 1
|
||||
*
|
||||
* Cookie store:
|
||||
*
|
||||
* Request
|
||||
* GET http://localhost:8080
|
||||
*
|
||||
> GET / HTTP/1.1
|
||||
> Host: localhost:8080
|
||||
> Accept: */*
|
||||
> User-Agent: hurl/1.2.0
|
||||
>
|
||||
< HTTP/1.1 200
|
||||
< Set-Cookie: JSESSIONID=829EF66D8B441D9B57B2498CF9989E54; Path=/; HttpOnly
|
||||
< X-Content-Type-Options: nosniff
|
||||
< X-XSS-Protection: 1; mode=block
|
||||
< Cache-Control: no-cache, no-store, max-age=0, must-revalidate
|
||||
< Pragma: no-cache
|
||||
< Expires: 0
|
||||
< X-Frame-Options: DENY
|
||||
< Content-Type: text/html;charset=UTF-8
|
||||
< Content-Language: en-US
|
||||
< Transfer-Encoding: chunked
|
||||
< Date: Fri, 04 Jun 2021 12:35:04 GMT
|
||||
<
|
||||
* Response Time: 11ms
|
||||
*
|
||||
|
||||
interactive mode:
|
||||
Press Q (Quit) or C (Continue)
|
||||
```
|
||||
|
||||
## Include Headers Like curl
|
||||
|
||||
We can also run our file to only output HTTP headers, with [`-i/--include` option].
|
||||
In this mode, headers of the last entry are displayed:
|
||||
|
||||
```shell
|
||||
$ hurl -i basic.hurl
|
||||
HTTP/1.1 200
|
||||
X-Content-Type-Options: nosniff
|
||||
X-XSS-Protection: 1; mode=block
|
||||
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
|
||||
Pragma: no-cache
|
||||
Expires: 0
|
||||
X-Frame-Options: DENY
|
||||
Content-Type: application/json
|
||||
Transfer-Encoding: chunked
|
||||
Date: Sun, 06 Jun 2021 15:11:31 GMT
|
||||
|
||||
[{"id":"c0d80047-4fbe-4d45-a005-91b5c7018b34","created":"1995-12-17T03:24:00Z"....
|
||||
```
|
||||
|
||||
If you want to inspect any entry other than the last entry, you can run your test to a
|
||||
given entry with the [`--to-entry` option], starting at index 1:
|
||||
|
||||
```shell
|
||||
$ hurl -i --to-entry 2 basic.hurl
|
||||
HTTP/1.1 404
|
||||
Vary: Origin
|
||||
Vary: Access-Control-Request-Method
|
||||
Vary: Access-Control-Request-Headers
|
||||
X-Content-Type-Options: nosniff
|
||||
X-XSS-Protection: 1; mode=block
|
||||
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
|
||||
Pragma: no-cache
|
||||
Expires: 0
|
||||
X-Frame-Options: DENY
|
||||
Content-Type: text/html;charset=UTF-8
|
||||
Content-Language: en-US
|
||||
Transfer-Encoding: chunked
|
||||
Date: Sun, 06 Jun 2021 15:14:20 GMT
|
||||
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title></title>
|
||||
<link rel="stylesheet" href="/style.css">
|
||||
<!--<script src="script.js"></script>-->
|
||||
</head>
|
||||
<body>
|
||||
<div>
|
||||
<img class="logo" src="/quiz.svg" alt="Quiz logo">
|
||||
</div>
|
||||
<h1>Error 404, Page not Found!</h1>
|
||||
|
||||
<a href="/">Quiz Home</a>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
## Using a Proxy
|
||||
|
||||
Finally, you can use a proxy between Hurl and your server to inspect requests and responses.
|
||||
|
||||
For instance, with [mitmproxy]:
|
||||
|
||||
1. First, launch mitmproxy, it will listen to connections on 8888 port
|
||||
|
||||
```shell
|
||||
$ mitmweb -p 8888 --web-port 8889 --web-open-browser
|
||||
Web server listening at http://127.0.0.1:8889/
|
||||
Proxy server listening at http://*:8888
|
||||
```
|
||||
|
||||
2. Then, run Hurl with [`-x/--proxy` option]
|
||||
|
||||
```shell
|
||||
$ hurl --proxy localhost:8888 basic.hurl
|
||||
```
|
||||
|
||||
The web interface of mitmproxy allows you to inspect, intercept any requests run by Hurl, and see
|
||||
the returned response to Hurl.
|
||||
|
||||
|
||||
[`-v/--verbose` option]: /docs/man-page.md#verbose
|
||||
[`--interactive` option]: /docs/man-page.md#interactive
|
||||
[`-i/--include` option]: /docs/man-page.md#include
|
||||
[`--to-entry` option]: /docs/man-page.md#to-entry
|
||||
[mitmproxy]: https://mitmproxy.org
|
||||
[`-x/--proxy` option]: /docs/man-page.md#proxy
|
13
docs/tutorial/index.md
Normal file
@ -0,0 +1,13 @@
|
||||
# Tutorial
|
||||
|
||||
1. [Your First Hurl File](/docs/tutorial/your-first-hurl-file.md)
|
||||
2. [Adding Asserts](/docs/tutorial/adding-asserts.md)
|
||||
3. [Chaining Requests](/docs/tutorial/chaining-requests.md)
|
||||
4. [Debug Tips](/docs/tutorial/debug-tips.md)
|
||||
5. [Captures](/docs/tutorial/captures.md)
|
||||
6. [Security](/docs/tutorial/security.md)
|
||||
7. [CI/CD Integration](/docs/tutorial/ci-cd-integration.md)
|
||||
|
||||
|
||||
|
||||
|
321
docs/tutorial/security.md
Normal file
@ -0,0 +1,321 @@
|
||||
# Security
|
||||
|
||||
In the previous part, we have tested the basic creation of a quiz, through the <http://localhost:8080/new-quiz>
|
||||
endpoint. Our test file `create-quiz.hurl` is now:
|
||||
|
||||
```hurl
|
||||
# First, get the quiz creation page to capture
|
||||
# the CSRF token (see https://en.wikipedia.org/wiki/Cross-site_request_forgery)
|
||||
GET http://localhost:8080/new-quiz
|
||||
|
||||
HTTP/1.1 200
|
||||
[Captures]
|
||||
csrf_token: xpath "string(//input[@name='_csrf']/@value)"
|
||||
|
||||
# Create a new quiz, using the captured CSRF token.
|
||||
POST http://localhost:8080/new-quiz
|
||||
[FormParams]
|
||||
name: Simpson
|
||||
question0: 16f897ab
|
||||
question1: dd894cca
|
||||
question2: 4edc1fdb
|
||||
question3: 37b9eff3
|
||||
question4: 0fec576c
|
||||
_csrf: {{csrf_token}}
|
||||
|
||||
HTTP/1.1 302
|
||||
[Captures]
|
||||
detail_url: header "Location"
|
||||
[Asserts]
|
||||
header "Location" matches "/quiz/detail/[a-f0-9]{8}"
|
||||
|
||||
# Open the newly created quiz detail page:
|
||||
GET {{detail_url}}
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
So far, we have tested a "simple" form creation: every value of the form is valid and sanitized, but what if the user
|
||||
put an invalid email?
|
||||
|
||||
## Server Side Validation
|
||||
|
||||
In the browser, there is client-side validation helping users enter data and avoid unnecessary server load.
|
||||
|
||||
Our HTML form is:
|
||||
|
||||
```html
|
||||
<form action="/new-quiz" method="POST">
|
||||
...
|
||||
<input id="name" type="text" name="name" minlength="4" maxlength="32" value="" required>...
|
||||
<input id="email" type="email" name="email" value="">...
|
||||
...
|
||||
</form>
|
||||
```
|
||||
|
||||
The first input, name, has validation HTML attributes: `minlenght="4"`, `maxlenght="32"` and `required`.
|
||||
In a browser, these attributes will prevent user to fill invalid data like a missing value or a name too long. If your
|
||||
tests rely on a "headless" browser, this type of validation can block you to test your server-side
|
||||
validation. Client-side validation can also use JavaScript, and it can be a challenge to send invalid data to your server.
|
||||
|
||||
But server-side validation is critical to secure your app. You must always validate and sanitize data on your backend,
|
||||
and try to test it.
|
||||
|
||||
As Hurl is not a browser, but merely an HTTP runner on top of [curl], sending and testing invalid data is easy.
|
||||
|
||||
1. Add a POST request to create a new quiz in `create-quiz.hurl`, with an invalid name. We check that the status code is 200 (user is
|
||||
not redirected to the quiz detail page), and that the label for "name" field has an `invalid` class:
|
||||
|
||||
```hurl
|
||||
# First, get the quiz creation page to capture
|
||||
# ...
|
||||
|
||||
# Create a new quiz, using the captured CSRF token.
|
||||
# ...
|
||||
|
||||
# Open the newly created quiz detail page:
|
||||
# ...
|
||||
|
||||
# Test various server-side validations:
|
||||
|
||||
# Invalid form name value: too short
|
||||
POST http://localhost:8080/new-quiz
|
||||
[FormParams]
|
||||
name: x
|
||||
question0: 16f897ab
|
||||
question1: dd894cca
|
||||
question2: 4edc1fdb
|
||||
question3: 37b9eff3
|
||||
question4: 0fec576c
|
||||
_csrf: {{csrf_token}}
|
||||
|
||||
HTTP/1.1 200
|
||||
[Asserts]
|
||||
xpath "//label[@for='name'][@class='invalid']" exists
|
||||
```
|
||||
|
||||
2. Add a POST request to create a new quiz with an email name. We check that the status
|
||||
code is 200 (user is not redirected to the quiz detail page), and that the label for "email" field has an
|
||||
`invalid` class:
|
||||
|
||||
```hurl
|
||||
# First, get the quiz creation page to capture
|
||||
# ...
|
||||
|
||||
# Create a new quiz, using the captured CSRF token.
|
||||
# ...
|
||||
|
||||
# Open the newly created quiz detail page:
|
||||
# ...
|
||||
|
||||
# Test various server-side validations:
|
||||
|
||||
# Invalid form name value: too short
|
||||
# ...
|
||||
# Invalid email parameter
|
||||
POST http://localhost:8080/new-quiz
|
||||
[FormParams]
|
||||
name: Barth
|
||||
email: barthsimpson
|
||||
question0: 16f897ab
|
||||
question1: dd894cca
|
||||
question2: 4edc1fdb
|
||||
question3: 37b9eff3
|
||||
question4: 0fec576c
|
||||
_csrf: {{csrf_token}}
|
||||
|
||||
HTTP/1.1 200
|
||||
[Asserts]
|
||||
xpath "//label[@for='email'][@class='invalid']" exists
|
||||
```
|
||||
|
||||
3. Finally, add a POST request with no CSRF token, to test that our endpoint has CRSF protection:
|
||||
|
||||
```hurl
|
||||
# First, get the quiz creation page to capture
|
||||
# ...
|
||||
|
||||
# Create a new quiz, using the captured CSRF token.
|
||||
# ...
|
||||
|
||||
# Open the newly created quiz detail page:
|
||||
# ...
|
||||
|
||||
# Test various server-side validations:
|
||||
|
||||
# Invalid form name value: too short
|
||||
# ...
|
||||
# Invalid email parameter
|
||||
# ...
|
||||
# No CSRF token:
|
||||
POST http://localhost:8080/new-quiz
|
||||
[FormParams]
|
||||
name: Barth
|
||||
email: barth.simpson@provider.net
|
||||
question0: 16f897ab
|
||||
question1: dd894cca
|
||||
question2: 4edc1fdb
|
||||
question3: 37b9eff3
|
||||
question4: 0fec576c
|
||||
|
||||
HTTP/1.1 403
|
||||
```
|
||||
|
||||
> We're using [the exist predicate] to check labels in the DOM
|
||||
|
||||
4. Run `create-quiz.hurl` and verify everything is ok:
|
||||
|
||||
```shell
|
||||
$ hurl --test create-quiz.hurl
|
||||
create-quiz.hurl: RUNNING [1/1]
|
||||
create-quiz.hurl: SUCCESS
|
||||
--------------------------------------------------------------------------------
|
||||
Executed: 1
|
||||
Succeeded: 1 (100.0%)
|
||||
Failed: 0 (0.0%)
|
||||
Duration: 33ms
|
||||
```
|
||||
|
||||
## Comments
|
||||
|
||||
So Hurl, being close to the HTTP layer, has no "browser protection" / client-side validation: it facilitates
|
||||
the testing of your app's security with no preconception.
|
||||
|
||||
Another use case is checking if there are no comment in your served HTML. These leaks can reveal sensitive information
|
||||
and [is it recommended] to trim HTML comments in your production files.
|
||||
|
||||
Popular front-end construction technologies use client-side JavaScript like [ReactJS] or [Vue.js].
|
||||
If you use one of this framework, and you inspect the DOM with the browser developer tools, you won't see any comment
|
||||
because the framework is managing the DOM and removing them.
|
||||
|
||||
But, if you look at the HTML page sent on the network, i.e. is the real HTML document sent by the
|
||||
server (and not _the document dynamically created by the framework_), you can still see those HTML comments.
|
||||
|
||||
With Hurl, you will be able to check the content of the _real_ network data.
|
||||
|
||||
1. In the first entry of `create-quiz.hurl`, add a [XPath assert] when getting the quiz creation page:
|
||||
|
||||
```hurl
|
||||
# First, get the quiz creation page to capture
|
||||
# the CSRF token (see https://en.wikipedia.org/wiki/Cross-site_request_forgery)
|
||||
GET http://localhost:8080/new-quiz
|
||||
|
||||
HTTP/1.1 200
|
||||
[Captures]
|
||||
csrf_token: xpath "string(//input[@name='_csrf']/@value)"
|
||||
[Asserts]
|
||||
xpath "//comment" count == 0 # Check that we don't leak comments
|
||||
|
||||
# ...
|
||||
```
|
||||
|
||||
|
||||
2. Run `create-quiz.hurl` and verify everything is ok:
|
||||
|
||||
```shell
|
||||
$ hurl --test create-quiz.hurl
|
||||
create-quiz.hurl: RUNNING [1/1]
|
||||
create-quiz.hurl: SUCCESS
|
||||
--------------------------------------------------------------------------------
|
||||
Executed: 1
|
||||
Succeeded: 1 (100.0%)
|
||||
Failed: 0 (0.0%)
|
||||
Duration: 33ms
|
||||
```
|
||||
|
||||
## Recap
|
||||
|
||||
So, our test file `create-quiz.hurl` is now:
|
||||
|
||||
```hurl
|
||||
# First, get the quiz creation page to capture
|
||||
# the CSRF token (see https://en.wikipedia.org/wiki/Cross-site_request_forgery)
|
||||
GET http://localhost:8080/new-quiz
|
||||
|
||||
HTTP/1.1 200
|
||||
[Captures]
|
||||
csrf_token: xpath "string(//input[@name='_csrf']/@value)"
|
||||
[Asserts]
|
||||
xpath "//comment" count == 0 # Check that we don't leak comments
|
||||
|
||||
# Create a new quiz, using the captured CSRF token.
|
||||
POST http://localhost:8080/new-quiz
|
||||
[FormParams]
|
||||
name: Simpson
|
||||
question0: 16f897ab
|
||||
question1: dd894cca
|
||||
question2: 4edc1fdb
|
||||
question3: 37b9eff3
|
||||
question4: 0fec576c
|
||||
_csrf: {{csrf_token}}
|
||||
|
||||
HTTP/1.1 302
|
||||
[Captures]
|
||||
detail_url: header "Location"
|
||||
[Asserts]
|
||||
header "Location" matches "/quiz/detail/[a-f0-9]{8}"
|
||||
|
||||
# Open the newly created quiz detail page:
|
||||
GET {{detail_url}}
|
||||
HTTP/1.1 200
|
||||
|
||||
# Test various server-side validations:
|
||||
|
||||
# Invalid form name value: too short
|
||||
POST http://localhost:8080/new-quiz
|
||||
[FormParams]
|
||||
name: x
|
||||
question0: 16f897ab
|
||||
question1: dd894cca
|
||||
question2: 4edc1fdb
|
||||
question3: 37b9eff3
|
||||
question4: 0fec576c
|
||||
_csrf: {{csrf_token}}
|
||||
|
||||
HTTP/1.1 200
|
||||
[Asserts]
|
||||
xpath "//label[@for='name'][@class='invalid']" exists
|
||||
|
||||
# Invalid email parameter:
|
||||
POST http://localhost:8080/new-quiz
|
||||
[FormParams]
|
||||
name: Barth
|
||||
email: barthsimpson
|
||||
question0: 16f897ab
|
||||
question1: dd894cca
|
||||
question2: 4edc1fdb
|
||||
question3: 37b9eff3
|
||||
question4: 0fec576c
|
||||
_csrf: {{csrf_token}}
|
||||
|
||||
HTTP/1.1 200
|
||||
[Asserts]
|
||||
xpath "//label[@for='email'][@class='invalid']" exists
|
||||
|
||||
# No CSRF token:
|
||||
POST http://localhost:8080/new-quiz
|
||||
[FormParams]
|
||||
name: Barth
|
||||
email: barth.simpson@provider.net
|
||||
question0: 16f897ab
|
||||
question1: dd894cca
|
||||
question2: 4edc1fdb
|
||||
question3: 37b9eff3
|
||||
question4: 0fec576c
|
||||
|
||||
HTTP/1.1 403
|
||||
```
|
||||
|
||||
We have seen that Hurl can be used as a security tool, to check you server-side validation.
|
||||
Until now, we have done all our tests locally, and in the next session we are going to see how simple
|
||||
it is to integrate Hurl in a CI/CD pipeline like [GitHub Action] or [GitLab CI/CD].
|
||||
|
||||
|
||||
[curl]: https://curl.se
|
||||
[the exist predicate]: /docs/asserting-response.md#predicates
|
||||
[is it recommended]: https://owasp.org/www-project-web-security-testing-guide/latest/4-Web_Application_Security_Testing/01-Information_Gathering/05-Review_Webpage_Content_for_Information_Leakage
|
||||
[DOM]: https://en.wikipedia.org/wiki/Document_Object_Model
|
||||
[ReactJS]: https://reactjs.org
|
||||
[Vue.js]: https://vuejs.org
|
||||
[XPath assert]: /docs/asserting-response.md#xpath-assert
|
||||
[GitHub Action]: https://github.com/features/actions
|
||||
[GitLab CI/CD]: https://docs.gitlab.com/ee/ci/
|
199
docs/tutorial/your-first-hurl-file.md
Normal file
@ -0,0 +1,199 @@
|
||||
# Your First Hurl File
|
||||
|
||||
Throughout this tutorial, we'll walk through the creation of multiple
|
||||
Hurl files to test a basic quiz application. We'll show how to test
|
||||
this site locally, and how to automate these integration tests in a CI/CD
|
||||
chain like [GitHub Action] and [GitLab CI/CD].
|
||||
|
||||
The quiz application consists of:
|
||||
|
||||
- a website that lets people create or play a series of quizzes
|
||||
- a set of REST apis to list, create and delete question and quiz
|
||||
|
||||
With Hurl, we're going to add tests for the website and the apis.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
We’ll assume you have Hurl installed already. You can test it by running the
|
||||
following command in a shell prompt (indicated by the $ prefix):
|
||||
|
||||
```shell
|
||||
$ hurl --version
|
||||
```
|
||||
|
||||
If Hurl is already installed, you should see the version of Hurl. If it isn't, you
|
||||
can check [Installation] to see how to install Hurl.
|
||||
|
||||
Next, we’re going to install our quiz application locally, in order to test it. We are
|
||||
not going to build our application from scratch, in order to focus on how to test it.
|
||||
|
||||
> Hurl being really language agnostic, you can use it to validate any type of application: in
|
||||
> this tutorial, our quiz application is built with [Spring Boot],
|
||||
> but this could as well be a [Node.js] or a [Flask] app.
|
||||
|
||||
Our quiz application can be launched locally either:
|
||||
|
||||
- using a Docker image
|
||||
- directly using the jar of the application
|
||||
|
||||
If you want to use the Docker image, you must have Docker installed locally. If it is the case,
|
||||
just run in a shell:
|
||||
|
||||
```shell
|
||||
$ docker pull ghcr.io/jcamiel/quiz:latest
|
||||
$ docker run --name quiz --rm --detach --publish 8080:8080 ghcr.io/jcamiel/quiz:latest
|
||||
```
|
||||
|
||||
And check that the container is running with:
|
||||
|
||||
```shell
|
||||
$ docker ps
|
||||
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
|
||||
922d387923ec ghcr.io/jcamiel/quiz:latest "java -jar app/quiz.…" 8 seconds ago Up 6 seconds 0.0.0.0:8080->8080/tcp, :::8080->8080/tcp quiz
|
||||
```
|
||||
|
||||
If you want to use the jar application, you must have Java installed locally. If it is the case, download
|
||||
the jar application from <https://github.com/jcamiel/quiz/releases/latest> and run in a shell:
|
||||
|
||||
```shell
|
||||
$ java -jar quiz-0.0.2.jar
|
||||
```
|
||||
|
||||
Either you're using the Docker images ot the jar app, you can open a browser and test the quiz application by
|
||||
typing the url <http://localhost:8080>:
|
||||
|
||||
<div>
|
||||
<img class="light-img" src="/docs/assets/img/quiz-light.png" width="400px" alt="Quiz home page"/>
|
||||
<img class="dark-img" src="/docs/assets/img/quiz-dark.png" width="400px" alt="Quiz home page"/>
|
||||
</div>
|
||||
|
||||
<small>Our quiz app: we've only secured a budget for integration tests and nothing for the site design...</small>
|
||||
|
||||
## A Basic Test
|
||||
|
||||
Next, we’re going to write our first test.
|
||||
|
||||
1. Open a text editor and create a file named `basic.hurl`. In this file, just type the following text and save:</li>
|
||||
|
||||
```hurl
|
||||
GET http://localhost:8080
|
||||
```
|
||||
|
||||
This is your first Hurl file, and probably one of the simplest. It consists of only one [entry].
|
||||
|
||||
> An entry has a mandatory [request specification]: in this case, we want to perform a
|
||||
> `GET` HTTP request on the endpoint <http://localhost:8080>. A request can be optionally followed by a [response
|
||||
> description], to add asserts on the HTTP response. For the moment, we don't have any response description.
|
||||
|
||||
2. In a shell, execute `hurl` with `basic.hurl` as argument:</li>
|
||||
|
||||
```shell
|
||||
$ hurl basic.hurl
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Welcome to Quiz!</title>
|
||||
<!-- <link rel="stylesheet" href="style.css">
|
||||
<script src="script.js"></script>-->
|
||||
</head>
|
||||
....
|
||||
</html>
|
||||
```
|
||||
|
||||
If the quiz app is running, you should see the content of the html file at <http://localhost:8080>. If the quiz app
|
||||
is not running, you'll see an error:
|
||||
|
||||
```shell
|
||||
$ hurl basic.hurl
|
||||
error: Http Connection
|
||||
--> basic.hurl:1:5
|
||||
|
|
||||
1 | GET http://localhost:8080
|
||||
| ^^^^^^^^^^^^^^^^^^^^^ Fail to connect
|
||||
|
|
||||
```
|
||||
|
||||
|
||||
As there are no response description, this basic test only checks that an HTTP server is running at
|
||||
<http://localhost:8080> and responds _something_. If the server had a problem on this endpoint, and had responded
|
||||
with a [`500 Internal Server Error`], Hurl would have just executed successfully the HTTP request,
|
||||
without checking the actual HTTP response.
|
||||
|
||||
As this test is not sufficient to ensure that our server is alive and running, we're going to add some asserts on
|
||||
the response and, at least, check that the HTTP response status code is [`200 OK`].
|
||||
|
||||
3.Open `basic.hurl` and modify it to test the status code response:</li>
|
||||
|
||||
```hurl
|
||||
GET http://localhost:8080
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
4. Execute `basic.hurl`:
|
||||
|
||||
```shell
|
||||
$ hurl basic.hurl
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Welcome to Quiz!</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<script src="script.js"></script>
|
||||
</head>
|
||||
....
|
||||
</html>
|
||||
```
|
||||
|
||||
There is no modification to the output of Hurl, the content of the HTTP request is outputted to the terminal. But, now,
|
||||
we check that our server is responding with a `200 OK`.
|
||||
|
||||
5. Modify `basic.hurl` to test a different HTTP response status code:
|
||||
|
||||
```hurl
|
||||
GET http://localhost:8080
|
||||
HTTP/1.1 500
|
||||
```
|
||||
|
||||
6. Save and execute it:
|
||||
|
||||
|
||||
```shell
|
||||
$ hurl basic.hurl
|
||||
error: Assert Status
|
||||
--> basic.hurl:2:10
|
||||
|
|
||||
2 | HTTP/1.1 500
|
||||
| ^^^ actual value is <200>
|
||||
|
|
||||
```
|
||||
|
||||
7. Revert your changes and finally add a comment at the beginning of the file:
|
||||
|
||||
|
||||
```hurl
|
||||
# Our first Hurl file, just checking
|
||||
# that our server is up and running.
|
||||
GET http://localhost:8080
|
||||
HTTP/1.1 200
|
||||
```
|
||||
|
||||
## Recap
|
||||
|
||||
That's it, this is your first Hurl file!
|
||||
|
||||
This is really a basic test, but Hurl's file format strength is its simplicity.
|
||||
We're going to see in the next section how to improve our tests while keeping it really simple.
|
||||
|
||||
[GitHub Action]: https://github.com/features/actions
|
||||
[GitLab CI/CD]: https://docs.gitlab.com/ee/ci/
|
||||
[Installation]: /docs/installation.md
|
||||
[Spring Boot]: https://spring.io/projects/spring-boot
|
||||
[Node.js]: https://nodejs.org/en/
|
||||
[Flask]: https://flask.palletsprojects.com
|
||||
[entry]: /docs/entry.md
|
||||
[request specification]: /docs/request.md
|
||||
[response description]: /docs/response.md
|
||||
[`500 Internal Server Error`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/500
|
||||
[`200 OK`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/200
|