Import of the canonical docs (raw Markdown, without ant Jekyll specificities.)

This commit is contained in:
jcamiel 2022-05-31 14:36:54 +02:00 committed by Fabrice Reix
parent 0e0b6697e7
commit 00e0c5cc58
34 changed files with 5802 additions and 0 deletions

643
docs/asserting-response.md Normal file
View 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>&#96;&#96;&#96;</code> and ends
with <code>&#96;&#96;&#96;</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

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

View 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

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

336
docs/capturing-response.md Normal file
View 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
View 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

View 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
View File

@ -0,0 +1,339 @@
# Grammar
## Definitions
Short description:
- operator &#124; 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&nbsp;</div>
<div class="tokens">=&nbsp;<a href="#entry">entry</a>*<br>&nbsp;
<a href="#lt">lt</a>*</div></div>
<div class="rule">
<div class="non-terminal" id="entry">entry&nbsp;</div>
<div class="tokens">=&nbsp;<a href="#request">request</a><br>&nbsp;
<a href="#response">response</a>?</div></div>
<div class="rule">
<div class="non-terminal" id="request">request&nbsp;</div>
<div class="tokens">=&nbsp;<a href="#lt">lt</a>*<br>&nbsp;
<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>&nbsp;
<a href="#header">header</a>*<br>&nbsp;
<a href="#request-section">request-section</a>*<br>&nbsp;
<a href="#body">body</a>?</div></div>
<div class="rule">
<div class="non-terminal" id="response">response&nbsp;</div>
<div class="tokens">=&nbsp;<a href="#lt">lt</a>*<br>&nbsp;
<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>&nbsp;
<a href="#header">header</a>*<br>&nbsp;
<a href="#response-section">response-section</a>*<br>&nbsp;
<a href="#body">body</a>?</div></div>
<div class="rule">
<div class="non-terminal" id="method">method&nbsp;</div>
<div class="tokens">=&nbsp;<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&nbsp;</div>
<div class="tokens">=&nbsp;<span class="definition">&lt;(alphanum | ":" | "/" | "{" | "}" | "*" | "," | "@" | "]")+&gt;</span></div></div>
<div class="rule">
<div class="non-terminal" id="version">version&nbsp;</div>
<div class="tokens">=&nbsp;<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&nbsp;</div>
<div class="tokens">=&nbsp;<span class="definition">&lt;[0-9]+&gt;</span></div></div>
<div class="rule">
<div class="non-terminal" id="header">header&nbsp;</div>
<div class="tokens">=&nbsp;<a href="#lt">lt</a>*<br>&nbsp;
<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&nbsp;</div>
<div class="tokens">=&nbsp;<a href="#lt">lt</a>*<br>&nbsp;
<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&nbsp;</div>
<div class="tokens">=&nbsp;<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&nbsp;</div>
<div class="tokens">=&nbsp;<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&nbsp;</div>
<div class="tokens">=&nbsp;<a href="#lt">lt</a>*<br>&nbsp;
<a href="#sp">sp</a>* <span class="terminal">"[QueryStringParams]"</span> <a href="#lt">lt</a><br>&nbsp;
<a href="#param">param</a>*</div></div>
<div class="rule">
<div class="non-terminal" id="form-params-section">form-params-section&nbsp;</div>
<div class="tokens">=&nbsp;<a href="#lt">lt</a>*<br>&nbsp;
<a href="#sp">sp</a>* <span class="terminal">"[FormParams]"</span> <a href="#lt">lt</a><br>&nbsp;
<a href="#param">param</a>*</div></div>
<div class="rule">
<div class="non-terminal" id="multipart-form-data-section">multipart-form-data-section&nbsp;</div>
<div class="tokens">=&nbsp;<a href="#lt">lt</a>*<br>&nbsp;
<a href="#sp">sp</a>* <span class="terminal">"[MultipartFormData]"</span> <a href="#lt">lt</a><br>&nbsp;
<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&nbsp;</div>
<div class="tokens">=&nbsp;<a href="#lt">lt</a>*<br>&nbsp;
<a href="#sp">sp</a>* <span class="terminal">"[Cookies]"</span> <a href="#lt">lt</a><br>&nbsp;
<a href="#cookie">cookie</a>*</div></div>
<div class="rule">
<div class="non-terminal" id="captures-section">captures-section&nbsp;</div>
<div class="tokens">=&nbsp;<a href="#lt">lt</a>*<br>&nbsp;
<a href="#sp">sp</a>* <span class="terminal">"[Captures]"</span> <a href="#lt">lt</a><br>&nbsp;
<a href="#capture">capture</a>*</div></div>
<div class="rule">
<div class="non-terminal" id="asserts-section">asserts-section&nbsp;</div>
<div class="tokens">=&nbsp;<a href="#lt">lt</a>*<br>&nbsp;
<a href="#sp">sp</a>* <span class="terminal">"[Asserts]"</span> <a href="#lt">lt</a><br>&nbsp;
<a href="#assert">assert</a>*</div></div>
<div class="rule">
<div class="non-terminal" id="param">param&nbsp;</div>
<div class="tokens">=&nbsp;<a href="#lt">lt</a>*<br>&nbsp;
<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&nbsp;</div>
<div class="tokens">=&nbsp;<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&nbsp;</div>
<div class="tokens">=&nbsp;<a href="#lt">lt</a>*<br>&nbsp;
<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&nbsp;</div>
<div class="tokens">=&nbsp;<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&nbsp;</div>
<div class="tokens">=&nbsp;<span class="definition">&lt;(alphanum | "/" | ";" | "=" | " ")+ without leading/trailing space&gt;</span></div></div>
<div class="rule">
<div class="non-terminal" id="cookie">cookie&nbsp;</div>
<div class="tokens">=&nbsp;<a href="#lt">lt</a>*<br>&nbsp;
<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&nbsp;</div>
<div class="tokens">=&nbsp;<span class="definition">&lt;(alphanum | "!" | "#" | "$" | "%" | "&" | "'" | "(" | ")" | "*" | "+"
| "-" | "." | "/" | ":" | "<" | "=" | ">" | "?" | "@" | "["
| "]" | "^" | "_" | "`" | "~" )* &gt;</span></div></div>
<div class="rule">
<div class="non-terminal" id="capture">capture&nbsp;</div>
<div class="tokens">=&nbsp;<a href="#lt">lt</a>*<br>&nbsp;
<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&nbsp;</div>
<div class="tokens">=&nbsp;<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&nbsp;</div>
<div class="tokens">=&nbsp;<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&nbsp;</div>
<div class="tokens">=&nbsp;<span class="terminal">"status"</span></div></div>
<div class="rule">
<div class="non-terminal" id="header-query">header-query&nbsp;</div>
<div class="tokens">=&nbsp;<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&nbsp;</div>
<div class="tokens">=&nbsp;<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&nbsp;</div>
<div class="tokens">=&nbsp;<span class="terminal">"body"</span></div></div>
<div class="rule">
<div class="non-terminal" id="xpath-query">xpath-query&nbsp;</div>
<div class="tokens">=&nbsp;<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&nbsp;</div>
<div class="tokens">=&nbsp;<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&nbsp;</div>
<div class="tokens">=&nbsp;<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&nbsp;</div>
<div class="tokens">=&nbsp;<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&nbsp;</div>
<div class="tokens">=&nbsp;<span class="terminal">"duration"</span></div></div>
<div class="rule">
<div class="non-terminal" id="sha256-query">sha256-query&nbsp;</div>
<div class="tokens">=&nbsp;<span class="terminal">"sha256"</span></div></div>
<div class="rule">
<div class="non-terminal" id="bytes-query">bytes-query&nbsp;</div>
<div class="tokens">=&nbsp;<span class="terminal">"bytes"</span></div></div>
<div class="rule">
<div class="non-terminal" id="subquery">subquery&nbsp;</div>
<div class="tokens">=&nbsp;<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&nbsp;</div>
<div class="tokens">=&nbsp;<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&nbsp;</div>
<div class="tokens">=&nbsp;<span class="terminal">"count"</span></div></div>
<div class="rule">
<div class="non-terminal" id="assert">assert&nbsp;</div>
<div class="tokens">=&nbsp;<a href="#lt">lt</a>*<br>&nbsp;
<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&nbsp;</div>
<div class="tokens">=&nbsp;(<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&nbsp;</div>
<div class="tokens">=&nbsp;<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&nbsp;</div>
<div class="tokens">=&nbsp;(<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&nbsp;</div>
<div class="tokens">=&nbsp;(<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&nbsp;</div>
<div class="tokens">=&nbsp;(<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&nbsp;</div>
<div class="tokens">=&nbsp;(<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&nbsp;</div>
<div class="tokens">=&nbsp;(<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&nbsp;</div>
<div class="tokens">=&nbsp;(<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&nbsp;</div>
<div class="tokens">=&nbsp;<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&nbsp;</div>
<div class="tokens">=&nbsp;<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&nbsp;</div>
<div class="tokens">=&nbsp;<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&nbsp;</div>
<div class="tokens">=&nbsp;<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&nbsp;</div>
<div class="tokens">=&nbsp;<span class="terminal">"exists"</span></div></div>
<div class="rule">
<div class="non-terminal" id="include-predicate">include-predicate&nbsp;</div>
<div class="tokens">=&nbsp;<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&nbsp;</div>
<div class="tokens">=&nbsp;<span class="terminal">"isInteger"</span></div></div>
<div class="rule">
<div class="non-terminal" id="float-predicate">float-predicate&nbsp;</div>
<div class="tokens">=&nbsp;<span class="terminal">"isFloat"</span></div></div>
<div class="rule">
<div class="non-terminal" id="boolean-predicate">boolean-predicate&nbsp;</div>
<div class="tokens">=&nbsp;<span class="terminal">"isBoolean"</span></div></div>
<div class="rule">
<div class="non-terminal" id="string-predicate">string-predicate&nbsp;</div>
<div class="tokens">=&nbsp;<span class="terminal">"isString"</span></div></div>
<div class="rule">
<div class="non-terminal" id="collection-predicate">collection-predicate&nbsp;</div>
<div class="tokens">=&nbsp;<span class="terminal">"isCollection"</span></div></div>
<div class="rule">
<div class="non-terminal" id="key-value">key-value&nbsp;</div>
<div class="tokens">=&nbsp;<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&nbsp;</div>
<div class="tokens">=&nbsp;<span class="definition">&lt;(alphanum | "_" | "-" | "." | escape-char)+ &gt;</span></div></div>
<div class="rule">
<div class="non-terminal" id="value-string">value-string&nbsp;</div>
<div class="tokens">=&nbsp;<span class="definition">&lt;(anychar except escaped char and #| escape-char)* without leading/trailing space&gt;</span></div></div>
<div class="rule">
<div class="non-terminal" id="quoted-string">quoted-string&nbsp;</div>
<div class="tokens">=&nbsp;<span class="terminal">"""</span> <span class="definition">&lt;(anychar except escaped char | escape-char)*&gt;</span> <span class="terminal">"""</span></div></div>
<div class="rule">
<div class="non-terminal" id="expr">expr&nbsp;</div>
<div class="tokens">=&nbsp;<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&nbsp;</div>
<div class="tokens">=&nbsp;<span class="definition">&lt;(alphanum | "_" )+&gt;</span></div></div>
<div class="rule">
<div class="non-terminal" id="escaped-char">escaped-char&nbsp;</div>
<div class="tokens">=&nbsp;<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&nbsp;</div>
<div class="tokens">=&nbsp;<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&nbsp;</div>
<div class="tokens">=&nbsp;<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&nbsp;</div>
<div class="tokens">=&nbsp;<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&nbsp;</div>
<div class="tokens">=&nbsp;<span class="terminal">"base64,"</span> <span class="definition">&lt;base64 encoding with optional whitesp/padding&gt;</span> <span class="terminal">";"</span></div></div>
<div class="rule">
<div class="non-terminal" id="file">file&nbsp;</div>
<div class="tokens">=&nbsp;<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&nbsp;</div>
<div class="tokens">=&nbsp;<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&nbsp;</div>
<div class="tokens">=&nbsp;<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&nbsp;</div>
<div class="tokens">=&nbsp;<span class="terminal">"#"</span> <span class="definition">&lt;any characters except newline - does not end with sp&gt;</span></div></div>
<div class="rule">
<div class="non-terminal" id="newline">newline&nbsp;</div>
<div class="tokens">=&nbsp;<span class="terminal">"\n"</span> | <span class="terminal">"\r\n"</span></div></div>
<div class="rule">
<div class="non-terminal" id="sp">sp&nbsp;</div>
<div class="tokens">=&nbsp;<span class="terminal">" "</span> | <span class="terminal">"\t"</span></div></div>
<div class="rule">
<div class="non-terminal" id="filename">filename&nbsp;</div>
<div class="tokens">=&nbsp;<span class="definition">&lt;(alphanum | ".")+&gt;</span></div></div>
<div class="rule">
<div class="non-terminal" id="integer">integer&nbsp;</div>
<div class="tokens">=&nbsp;<span class="definition">&lt;-?[1-9][0-9]*&gt;</span></div></div>
</div>

259
docs/grammar/hurl.grammar Normal file
View 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
View 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
View 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 18 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
View 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
View 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
View 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 &lt;seconds&gt;</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 &lt;file&gt;</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 &lt;file&gt;</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 &lt;dir&gt;</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 &lt;glob&gt;</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 &lt;num&gt;</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 &lt;seconds&gt;</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 &lt;no-proxy-list&gt;</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 &lt;file&gt;</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 &lt;file&gt;</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 &lt;dir&gt;</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 &lt;entry-number&gt;</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 &lt;user:password&gt;</code></a> | Add basic Authentication header to each request.<br/>
<a href="#user-agent" id="user-agent"><code>-A, --user-agent &lt;name&gt;</code></a> | Specify the User-Agent string to send to the HTTP server.<br/>
<a href="#variable" id="variable"><code>--variable &lt;name=value&gt;</code></a> | Define variable (name/value) to be used in Hurl templates.<br/>
<a href="#variables-file" id="variables-file"><code>--variables-file &lt;file&gt;</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
View 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>
&nbsp;&nbsp;"type": "FOO",<br>
&nbsp;&nbsp;"value": 356789,<br>
&nbsp;&nbsp;"ordered": true,<br>
&nbsp;&nbsp;"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>&#96;&#96;&#96;</code> and ends
with <code>&#96;&#96;&#96;</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>&#96;&#96;&#96;</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
View 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>
&nbsp;&nbsp;"type": "FOO",<br>
&nbsp;&nbsp;"value": 356789,<br>
&nbsp;&nbsp;"ordered": true,<br>
&nbsp;&nbsp;"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
View 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
View 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
View 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

View 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
View 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

View 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

View 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
View 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
View 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
View 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/

View 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
Well 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, were 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, were 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