diff --git a/sites/hurl.dev/_data/docs.yml b/sites/hurl.dev/_data/docs.yml index d1d1aaa..aae4381 100644 --- a/sites/hurl.dev/_data/docs.yml +++ b/sites/hurl.dev/_data/docs.yml @@ -291,6 +291,7 @@ items: - title: Using --verbose and --very-verbose for all entries - title: Debugging a specific entry + - title: Use Error Format - title: Interactive Mode - title: Include Headers Like curl - title: Using a Proxy @@ -304,6 +305,9 @@ path: /docs/tutorial/security.html items: - title: Server Side Validation + items: + - title: Valid user creation + - title: Invalid user creation - title: Comments - title: Recap - title: CI/CD Integration diff --git a/sites/hurl.dev/_docs/response.md b/sites/hurl.dev/_docs/response.md index 089e48a..e3ec6fa 100644 --- a/sites/hurl.dev/_docs/response.md +++ b/sites/hurl.dev/_docs/response.md @@ -114,8 +114,8 @@ On each response, libcurl response timings are available: All timings are in microsecond.
- Response timings explanation - Response timings explanation + Response timings explanation + Response timings explanation Courtesy of CloudFlare
diff --git a/sites/hurl.dev/_docs/running-tests.md b/sites/hurl.dev/_docs/running-tests.md index 1f2f291..e507233 100644 --- a/sites/hurl.dev/_docs/running-tests.md +++ b/sites/hurl.dev/_docs/running-tests.md @@ -79,13 +79,13 @@ Hurl can generate an HTML report by using the [`--report-html HTML_DIR`] option. If the HTML report already exists, the test results will be appended to it.
- Hurl HTML Report + Hurl HTML Report
The input Hurl files (HTML version) are also included and are easily accessed from the main page.
- Hurl HTML file + Hurl HTML file
### JUnit Report diff --git a/sites/hurl.dev/_docs/tutorial/adding-asserts.md b/sites/hurl.dev/_docs/tutorial/adding-asserts.md index 4543730..6f4699d 100644 --- a/sites/hurl.dev/_docs/tutorial/adding-asserts.md +++ b/sites/hurl.dev/_docs/tutorial/adding-asserts.md @@ -42,7 +42,7 @@ An assert consists of a query and a predicate. As we want to test the value of t going to use the [XPath expression] `string(//head/title)`. {:start="1"} -1. Asserts are written in an Asserts section, so modify `basic.hurl` file: +1. Asserts are written in an `[Asserts]` section, so modify `basic.hurl` file: ```hurl # Our first Hurl file, just checking @@ -71,7 +71,7 @@ Duration: 19 ms There is no error so everything is good! {:start="3"} -3. Modify the predicate value to "Movies Bax" +3. Modify the test value to "Movies Bax" ```hurl # Our first Hurl file, just checking @@ -109,20 +109,26 @@ Hurl has failed now and provides information on which assert is not valid. ### Typed predicate -If we decompose our assert, `xpath "string(//head/title)"` is the XPath query and `== "Movies Box"` is our -predicate to test the query against. You can note that predicates values are typed: +Decompose our assert: -- `xpath "string(//head/title)" == "true"` +- __`xpath "string(//head/title)"`__ + is the XPath query +- __`== "Movies Box"`__ + is our predicate to test the query against + +You can note that tested values are typed: + +- `xpath "string(//head/title)" ==` __`"true"`__ tests that the XPath expression is returning a string, and this string is equal to the string `true` -- `xpath "boolean(//head/title)" == true` +- `xpath "boolean(//head/title)" ==` __`true`__ tests that the XPath expression is returning a boolean, and the boolean is `true` 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 h3 tag on our home page, -with `count`: +elements present in the [DOM]. We can use it to ensure that we have exactly two `

` tag on our home page, +with [`count`]: {:start="1"} -1. Add a new assert in `basic.hurl` to check the number of h3 tags: +1. Add a new assert in `basic.hurl` to check the number of `

` tags: ```hurl # Checking our home page: @@ -135,7 +141,7 @@ xpath "//h3" count == 2 ``` {:start="2"} -2. We can also check each button's title: +2. We can also check each `

`'s content: ```hurl # Checking our home page: @@ -216,14 +222,18 @@ xpath "string((//h3)[1])" contains "Popular" xpath "string((//h3)[2])" contains "Featured Today" ``` -The line `Content-Type: text/html; charset=utf-8` is testing that the header `Content-Type` is present in the response, +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]. +When creating a new session, our Express application returns a [`Set-Cookie` HTTP response header]. So to test it, we can modify our Hurl file with another header assert. {:start="3"} @@ -249,13 +259,13 @@ Not only we'll be able to easily tests [cookie attributes] (like `HttpOnly`, or 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 +> 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 with a `HttpOnly` session cookie, we can modify our file and add cookie asserts. {:start="4"} -4. Add two cookie asserts on the cookie `JESSIONID`: +4. Add two cookie asserts on the cookie `x-session-id`: ```hurl # Checking our home page: @@ -296,7 +306,7 @@ Duration: 20 ms 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 the basic structure of our page: a title, 2 `

` tags - 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 diff --git a/sites/hurl.dev/_docs/tutorial/captures.md b/sites/hurl.dev/_docs/tutorial/captures.md index c351dcf..79b7b88 100644 --- a/sites/hurl.dev/_docs/tutorial/captures.md +++ b/sites/hurl.dev/_docs/tutorial/captures.md @@ -7,61 +7,48 @@ section: Tutorial # Captures We have seen how to chain requests in a Hurl file. In some use cases, you want -to use data from one request and inject it in another one. That's what captures +to use data from one request and inject it in another one. That's what [captures] are all about. ## Capturing a CSRF Token -In our quiz application, a user can create a quiz at . +In our website, a user can login at . 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 +- a required username +- a required password If we look at the page HTML content, we can see an HTML form: ```html -
+ + ... - ... - ... - - + + ... + + ... + ...
``` -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. +When the user clicks on 'Login' button, a POST request is sent with form values: the username and a password. +Our server implements a [_Post / Redirect / Get pattern_]: if the POST submission is successful, the user is redirected +to his favorites movies page. Let's try to test it! -Form values can be sent using a [Form parameters section], with each key followed by its -corresponding value. +Form values can be sent using a [Form parameters section], with each key followed by its corresponding value. {:start="1"} -1. Create a new file named `create-quiz.hurl`: +1. Create a new file named `login.hurl`: ```hurl -POST http://localhost:8080/new-quiz +POST http://localhost:3000/login [FormParams] -name: Simpson -question0: 16f897ab -question1: dd894cca -question2: 4edc1fdb -question3: 37b9eff3 -question4: 0fec576c - +username: fab +password: 12345678 HTTP 302 ``` @@ -69,74 +56,70 @@ HTTP 302 > `Content-Type` HTTP header: Hurl infers that the content type of the request is `application/x-www-form-urlencoded`. {:start="2"} -2. Run `create-quiz.hurl`: +2. Run `login.hurl`: ```shell -$ hurl --test create-quiz.hurl -create-quiz.hurl: Running [1/1] +login.hurl: Running [1/1] error: Assert status code - --> create-quiz.hurl:6:10 + --> login.hurl:5:6 | -10 | HTTP 302 + 5 | HTTP 302 | ^^^ actual value is <403> | -create-quiz.hurl: Failure (1 request(s) in 5 ms) +login.hurl: Failure (1 request(s) in 9 ms) -------------------------------------------------------------------------------- Executed files: 1 Succeeded files: 0 (0.0%) Failed files: 1 (100.0%) -Duration: 5 ms +Duration: 10 ms ``` -This is unexpected! Our test is failing, we're not redirected to the new quiz detail page. +This is unexpected! Our test is failing, we're not redirected to the favorite movies page. The reason is quite simple, let's look more precisely at our HTML form: ```html -
+ + ... - -
``` -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, +The server login page is protected by a [CSRF token]. In a browser, when the user wants to log in by +sending a POST request, a token is sent along the username/password values. This token is generated server-side, +and embedded in the HTML. When the POST request is made, our server expects that the request includes a valid token, and will reject the request if the token is missing or invalid. In our Hurl file, we're not sending any token, so the server is rejecting our request with a [`403 Forbidden`] HTTP response. -Unfortunately, we can't hard code the value of a token in our -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. +Unfortunately, we can't hard code the value of a token in our `[FormParams]` section because the token is dynamically +generated on each request, and a certain fixed value would be valid only during a small period of time. We need to dynamically _capture_ the value of the CSRF token and pass it to our form. To do so, we are going to: -- perform a first GET request to 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 +- perform a first GET request to and capture the CSRF token +- chain with a POST request that contains our username/password value, and our captured CSRF token +- check that the POST response is a redirection, i.e. a [`302 Found`] to the favorites page So, let's go! ### How to capture values {:start="1"} -1. Modify `create-quiz.hurl`: +1. Modify `login.hurl`: ```hurl -# First, get the quiz creation page to capture +# First, display the login page to capture # the CSRF token (see https://en.wikipedia.org/wiki/Cross-site_request_forgery) -GET http://localhost:8080/new-quiz - +GET http://localhost:3000/login HTTP 200 [Captures] csrf_token: xpath "string(//input[@name='_csrf']/@value)" ``` -Captures are defined in a Captures section. Captures are composed of a variable name and a query. +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]. @@ -149,28 +132,23 @@ XPath query. Now that we have captured the CSRF token value, we can inject it in the POST request. {:start="2"} -2. Add a POST request using `csrf_token` variable in `create-quiz.hurl`: +2. Add a POST request using `csrf_token` variable in `login.hurl`: {% raw %} ```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 - +# First, display the login page to capture +# the CSRF token (see https://en.wikipedia.org/wiki/Cross-site_request_forgery) +GET http://localhost:3000/login HTTP 200 [Captures] csrf_token: xpath "string(//input[@name='_csrf']/@value)" -# Create a new quiz, using the captured CSRF token: -POST http://localhost:8080/new-quiz +# Log in user, using the captured CSRF token: +POST http://localhost:3000/login [FormParams] -name: Simpson -question0: 16f897ab -question1: dd894cca -question2: 4edc1fdb -question3: 37b9eff3 -question4: 0fec576c +username: fab +password: 12345678 _csrf: {{csrf_token}} HTTP 302 ``` @@ -178,17 +156,17 @@ HTTP 302 {:start="3"} -3. Run `create-quiz.hurl` and verify everything is ok: +3. Run `login.hurl` and verify everything is ok: ```shell -$ hurl --test create-quiz.hurl -create-quiz.hurl: Running [1/1] -create-quiz.hurl: Success (2 request(s) in 10 ms) +$ hurl --test login.hurl +login.hurl: Running [1/1] +login.hurl: Success (2 request(s) in 14 ms) -------------------------------------------------------------------------------- Executed files: 1 Succeeded files: 1 (100.0%) Failed files: 0 (0.0%) -Duration: 10 ms +Duration: 16 ms ``` ## Follow Redirections @@ -197,143 +175,103 @@ Like its HTTP engine [curl], Hurl doesn't follow redirection by default: if a re 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. +After having logged it, we would like to test the page where the user has been redirected. +This is really simple and can be achieved with a [header assert]: on the response to the POST creation request, we +are going to assert the [`Location`] header, which indicates the redirection URL target. {:start="1"} -1. Add a new header capture to capture the `Location` header in a variable named `detail_url`: +1. Add a new header assert to test the `Location` header: {% raw %} ```hurl -# First, get the quiz creation page to capture +# First, display the login page to capture # ... -# Create a new quiz, using the captured CSRF token: -POST http://localhost:8080/new-quiz +# Log in user, using the captured CSRF token: +POST http://localhost:3000/login [FormParams] -name: Simpson -question0: 16f897ab -question1: dd894cca -question2: 4edc1fdb -question3: 37b9eff3 -question4: 0fec576c +username: fab +password: 12345678 _csrf: {{csrf_token}} - HTTP 302 -[Captures] -detail_url: header "Location" -``` -{% endraw %} - - -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. - -{:start="2"} -2. Add a header assert on the POST response to check the redirection URL: - -{% raw %} -```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 302 -[Captures] -detail_url: header "Location" [Asserts] -header "Location" matches "/quiz/detail/[a-f0-9]{8}" +header "Location" == "/my-movies" ``` {% endraw %} -{:start="3"} -3. Add a request to get the detail page that the user has been redirected to: +{:start="2"} +2. Add a request to get the favorites page that the user has been redirected to: -{% raw %} ```hurl -# First, get the quiz creation page to capture +# First, display the login page to capture # ... -# Create a new quiz, using the captured CSRF token: +# Log in user, using the captured CSRF token: # ... -# Open the newly created quiz detail page: -GET {{detail_url}} +# Follow redirection and open favorites: +GET http://localhost:3000/my-movies HTTP 200 +[Asserts] +xpath "string(//title)" == "My Movies" ``` -{% endraw %} - -{:start="4"} -4. Run `create-quiz.hurl` and verify everything is ok: +{:start="3"} +3. Run `login.hurl` and verify everything is ok: ```shell -$ hurl --test create-quiz.hurl -create-quiz.hurl: Running [1/1] -create-quiz.hurl: Success (3 request(s) in 39 ms) +$ hurl --test login.hurl +login.hurl: Running [1/1] +login.hurl: Success (3 request(s) in 17 ms) -------------------------------------------------------------------------------- Executed files: 1 Succeeded files: 1 (100.0%) Failed files: 0 (0.0%) -Duration: 46 ms +Duration: 19 ms ``` - > You can force Hurl to follow redirection by using [`-L / --location` option] or using an [`[Options]` section][options]. > In this case, asserts and captures will be run against the last redirection step. +A login workflow is surprisingly hard to do well. You can try to add more test on our `login.hurl` test. With Hurl, try +now to test the following usecase: + +- when a user is not authenticated and goes to , he is redirected to the login page, +- what's happen if the user try to log in with a wrong password, +- after a user log out, he can open the login page again. + +You can see a more complete `login.hurl` on [the GitHub repo]. + ## Recap -So, our test file `create-quiz.hurl` is now: +So, our test file `login.hurl` is now: {% raw %} ```hurl -# First, get the quiz creation page to capture +# First, display the login page to capture # the CSRF token (see https://en.wikipedia.org/wiki/Cross-site_request_forgery) -GET http://localhost:8080/new-quiz - +GET http://localhost:3000/login HTTP 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 +# Log in user, using the captured CSRF token: +POST http://localhost:3000/login [FormParams] -name: Simpson -question0: 16f897ab -question1: dd894cca -question2: 4edc1fdb -question3: 37b9eff3 -question4: 0fec576c +username: fab +password: 12345678 _csrf: {{csrf_token}} - HTTP 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}} +# Follow redirection and open favorites: +GET http://localhost:3000/my-movies HTTP 200 +[Asserts] +xpath "string(//title)" == "My Movies" ``` {% endraw %} @@ -360,3 +298,6 @@ of a redirection. [`-L / --location` option]: {% link _docs/manual.md %}#location [capture response data]: {% link _docs/capturing-response.md %} [options]: {% link _docs/request.md %}#options +[captures]: {% link _docs/capturing-response.md %} +[header assert]: {% link _docs/asserting-response.md %}#header-assert +[the GitHub repo]: https://github.com/jcamiel/hurl-express-tutorial/tree/main/integration diff --git a/sites/hurl.dev/_docs/tutorial/chaining-requests.md b/sites/hurl.dev/_docs/tutorial/chaining-requests.md index 04952fd..df14d5a 100644 --- a/sites/hurl.dev/_docs/tutorial/chaining-requests.md +++ b/sites/hurl.dev/_docs/tutorial/chaining-requests.md @@ -27,10 +27,10 @@ cookie "x-session-id[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 write, the less fragile your tests suite will become. +many tests, the more asserts you write, the less fragile your tests suite is. 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: +request following our first request. Let's say we want to test that our server returns a [404 page] on a broken link: {:start="1"} 1. Modify `basic.hurl` to add a second request on a broken URL: @@ -88,14 +88,14 @@ Failed files: 0 (0.0%) Duration: 20 ms ``` -We can see that the test is still ok, now two requests are being run in sequence, and each response can be +We can see that the test is still ok. Now two requests are being run in sequence, and each response can be tested independently. ## Test REST API -So far, we have tested two HTML endpoints. We're going to see now how to test a 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 . +Our website exposes a health REST resource, available at . Let's use Hurl to check it. {:start="1"} @@ -106,22 +106,21 @@ $ echo 'GET http://localhost:3000/api/health' | hurl {"status":"RUNNING","healthy":true,"operationId":6212054377712155,"reportedDate":"2023-07-21T16:11:24.053Z"} ``` -> Being a classic CLI application, we can use the standard input with Hurl to provide requests -> to be executed, instead of a file. +Being a classic CLI application, we can use the standard input with Hurl instead of a file to provide requests +to be executed, and pipe the result to various tools like [`jq`]: -So, our health API returns this JSON resource: - -```json -{ - "status": "RUNNING", - "healthy": true, - "operationId": 6212054377712155 - "reportedDate": "2023-07-21T16:11:24.053Z", -} +```shell +$ echo 'GET http://localhost:3000/api/health' | hurl | jq +{ + "status": "RUNNING", + "healthy": true, + "operationId": 8629192252836205, + "reportedDate": "2023-08-04T11:04:52.516Z" +} ``` -We can test it with a [JSONPath assert]. JsonPath asserts have the same structure as XPath asserts: a query -followed by a predicate. A [JSONPath query] is a simple expression to inspect a JSON object. +We can test our health API it with a [JSONPath assert]. JSONPath asserts have the same structure as XPath asserts: a query +followed by a test. A [JSONPath query] is a simple expression to inspect a JSON object. {:start="2"} 2. Modify `basic.hurl` to add a third request that asserts our REST API: @@ -144,14 +143,30 @@ jsonpath "$.healthy" == true jsonpath "$.operationId" exists ``` -Like XPath assert, JSONPath predicate values are typed. String, boolean, number and -collections are supported. Let's practice writing JsonPath asserts 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 at . We can use it to add checks on getting questions -through the API endpoint. +Like XPath assert, JSONPath predicate values are typed. Strings, booleans, numbers, dates and +collections are supported. + +Let's practice writing JSONPath asserts by using another API. + +In our Movies Box website, user can search movies using different criteria like actor names, director names or +released date. The search page is exposed at . Go to the search page and type "1982": you +will see some movies that have been released in 1982. Our server exposed a REST API at + and the search page use a [XHR] to get the search results. You can see the XHR in +action by using the Developer Tools of your browser: + +
+ + + + + Firefox Developer Tool + +
+ +We can use this REST API to add checks on search results through the API endpoint. {:start="3"} -3. Add JSONPath asserts on the REST APIs: +3. Add JSONPath asserts on the REST APIs: ```hurl # Checking our home page: @@ -163,27 +178,28 @@ through the API endpoint. # Check our health API: # ... -# Check question API: -GET http://localhost:8080/api/questions?offset=0&size=20&sort=oldest +# Check search API: +GET http://localhost:3000/api/search?q=1982&sort=name HTTP 200 [Asserts] -header "Content-Type" == "application/json" -jsonpath "$" count == 20 -jsonpath "$[0].id" == "c0d80047" -jsonpath "$[0].title" == "What is a pennyroyal?" +header "Content-Type" == "application/json; charset=utf-8" +jsonpath "$" count == 5 +jsonpath "$[0].name" == "Blade Runner" +jsonpath "$[0].director" == "Ridley Scott" +jsonpath "$[0].release_date" == "1982-06-25" ``` -> 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 +> To keep things simple in this tutorial, we have mocked data +> in our "Movies Box" 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 > defined by environment 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 . We can set the query parameters +Note that the search API use query parameters `q` and `sort` that's why we have written the URL with +query parameters . We can set the query parameters in the URL, or use a [query parameter section]. {:start="4"} @@ -199,87 +215,165 @@ in the URL, or use a [query parameter section]. # Check our health API: # ... -# Check question API: -GET http://localhost:8080/api/questions +# Check search API: +GET http://localhost:3000/api/search [QueryStringParams] -offset: 0 -size: 20 -sort: oldest +q: 1982 +sort: name HTTP 200 [Asserts] -header "Content-Type" == "application/json" -jsonpath "$" count == 20 -jsonpath "$[0].id" == "c0d80047" -jsonpath "$[0].title" == "What is a pennyroyal?" +header "Content-Type" == "application/json; charset=utf-8" +jsonpath "$" count == 5 +jsonpath "$[0].name" == "Blade Runner" +jsonpath "$[0].director" == "Ridley Scott" +jsonpath "$[0].release_date" == "1982-06-25" ``` -Finally, our basic Hurl file, with four requests, looks like: +For the moment, we have just tested that values returned by the server are equals to expected values. You can also +use other type of assertions like [`startsWith`], [`endsWith`], [`contains`], [`matches`] etc... For instance, we could +test that the `release_date` of Blade Runner is 1982: + +{:start="5"} +5. Use `startsWith` to test the release date: ```hurl # Checking our home page: -GET http://localhost:8080 +# ... + +# Check that we have a 404 response for broken links: +# ... + +# Check our health API: +# ... + +# Check search API: +GET http://localhost:3000/api/search +[QueryStringParams] +q: 1982 +sort: name HTTP 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 +header "Content-Type" == "application/json; charset=utf-8" +jsonpath "$" count == 5 +jsonpath "$[0].name" == "Blade Runner" +jsonpath "$[0].director" == "Ridley Scott" +jsonpath "$[0].release_date" startsWith "1982" +``` +We could make our test stricter by validating the format of `release_date`. By using [filters], we can transform query +values. We're already using a filter, [`count`] that returns the number of elements in a collection. Now we are +going to use a `regex` filter to extract part of a string: + +{:start="5"} +5. Use a `regex` filter to test the release date: + +```hurl +# Checking our home page: +# ... # Check that we have a 404 response for broken links: -GET http://localhost:8080/not-found +# ... + +# Check our health API: +# ... + +# Check search API: +GET http://localhost:3000/api/search +[QueryStringParams] +q: 1982 +sort: name + +HTTP 200 +[Asserts] +header "Content-Type" == "application/json; charset=utf-8" +jsonpath "$" count == 5 +jsonpath "$[0].name" == "Blade Runner" +jsonpath "$[0].director" == "Ridley Scott" +jsonpath "$[0].release_date" regex /(\d{4})-\d{2}-\d{2}/ == "1982" +``` + +Let's decompose our final assert: + +- __`jsonpath "$[0].release_date"`__ + this is the JSONPath query that extracts some date from our response +- __`regex /(\d{4})-\d{2}-\d{2}/"`__ + this is a regex filter with a [regular expression] `/(\d{4})-\d{2}-\d{2}/`. Regular +expression can be written with `/.../` like in JavaScript for instance. Note that the regular expression has a +capture group `(\d{4})` that will extract the 4 digits year from the previous query +- __`== "1982"`__ + this is our test value + +As you can see, [filters] are very powerful; they can be combined to refine values for better tests. + + +Finally, our basic Hurl file with four HTTP requests looks like: + +```hurl +# Checking our home page: +GET http://localhost:3000 + +HTTP 200 +[Asserts] +xpath "string(//head/title)" == "Movies Box" +xpath "//h3" count == 2 +xpath "string((//h3)[1])" contains "Popular" +xpath "string((//h3)[2])" contains "Featured Today" +# Testing HTTP response headers: +header "Content-Type" == "text/html; charset=utf-8" +cookie "x-session-id" exists +cookie "x-session-id[HttpOnly]" exists + + +# Check that we have a 404 response for broken links: +GET http://localhost:3000/not-found HTTP 404 [Asserts] -header "Content-Type" == "text/html;charset=UTF-8" -xpath "string(//h1)" == "Error 404, Page not Found!" +header "Content-Type" == "text/html; charset=utf-8" +xpath "string(//h2)" == "Error" +xpath "string(//h3)" == "Not Found" # Check our health API: -GET http://localhost:8080/api/health +GET http://localhost:3000/api/health HTTP 200 [Asserts] -header "Content-Type" == "application/json" +header "Content-Type" == "application/json; charset=utf-8" jsonpath "$.status" == "RUNNING" jsonpath "$.healthy" == true jsonpath "$.operationId" exists -# Check question API: -GET http://localhost:8080/api/questions +# Check search API: +GET http://localhost:3000/api/search [QueryStringParams] -offset: 0 -size: 20 -sort: oldest +q: 1982 +sort: name HTTP 200 [Asserts] -header "Content-Type" == "application/json" -jsonpath "$" count == 20 -jsonpath "$[0].id" == "c0d80047" -jsonpath "$[0].title" == "What is a pennyroyal?" +header "Content-Type" == "application/json; charset=utf-8" +jsonpath "$" count == 5 +jsonpath "$[0].name" == "Blade Runner" +jsonpath "$[0].director" == "Ridley Scott" +jsonpath "$[0].release_date" regex /(\d{4})-\d{2}-\d{2}/ == "1982" ``` -{:start="5"} -5. Run `basic.hurl` and check that every assert of every request has been successful: +{:start="6"} +6. 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 (4 request(s) in 24 ms) +basic.hurl: Success (4 request(s) in 20 ms) -------------------------------------------------------------------------------- Executed files: 1 Succeeded files: 1 (100.0%) Failed files: 0 (0.0%) -Duration: 31 ms +Duration: 21 ms ``` ## Recap @@ -293,3 +387,12 @@ for your applications. [JSONPath query]: https://goessner.net/articles/JsonPath/ [query parameter section]: {% link _docs/request.md %}#query-parameters [`--test`]: {% link _docs/manual.md %}#test +[XHR]: https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest +[`startsWith`]: {% link _docs/asserting-response.md %}#predicates +[`endsWith`]: {% link _docs/asserting-response.md %}#predicates +[`contains`]: {% link _docs/asserting-response.md %}#predicates +[`matches`]: {% link _docs/asserting-response.md %}#predicates +[filters]: {% link _docs/filters.md %} +[`count`]: {% link _docs/filters.md %}#count +[regular expression]: https://en.wikipedia.org/wiki/Regular_expression +[`jq`]: https://github.com/jqlang/jq diff --git a/sites/hurl.dev/_docs/tutorial/ci-cd-integration.md b/sites/hurl.dev/_docs/tutorial/ci-cd-integration.md index 30259c5..fa495e1 100644 --- a/sites/hurl.dev/_docs/tutorial/ci-cd-integration.md +++ b/sites/hurl.dev/_docs/tutorial/ci-cd-integration.md @@ -22,7 +22,7 @@ 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 +> project with build, Docker upload/publish and integration tests, go to 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 @@ -31,19 +31,19 @@ script runs locally, we'll see how to run it automatically in a CI/CD pipeline. ## Integration Script {:start="1"} -1. First, create a directory name `quiz-project`, add [`integration/basic.hurl`] +1. First, create a directory name `movies-project`, add [`integration/basic.hurl`] and [`integration/create-quiz.hurl`] from the previous tutorial to the directory. -
$ mkdir quiz-project
-$ cd quiz-project
+
$ mkdir movies-project
+$ cd movies-project
 $ mkdir integration
 $ vi integration/basic.hurl
 
-# Import basic.hurl here!
+# Import basic.hurl here!
 
-$ vi integration/create-quiz.hurl
+$ vi integration/login.hurl
 
-# Import create-quiz.hurl here!
+# Import login.hurl here!
Next, we are going to write the first version of our integration script that will just pull the Quiz image and run it: @@ -55,8 +55,8 @@ just pull the Quiz image and run it: #!/bin/bash set -eu -echo "Starting Quiz container" -docker run --name quiz --rm --detach --publish 8080:8080 ghcr.io/jcamiel/quiz:latest +echo "Starting container" +docker run --name movies --rm --detach --publish 3000:3000 ghcr.io/jcamiel/hurl-express-tutorial:latest ``` {:start="3"} @@ -65,7 +65,7 @@ docker run --name quiz --rm --detach --publish 8080:8080 ghcr.io/jcamiel/quiz:la ```shell $ chmod u+x bin/integration.sh $ bin/integration.sh -Starting Quiz container +Starting container 5d311561828d6078e84eb4b8b87dfd5d67bde6d9614ad83860b60cf310438d2a ``` @@ -74,10 +74,10 @@ Starting Quiz container ```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 +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +4002ce42e507 ghcr.io/jcamiel/hurl-express-tutorial:latest "node dist/bin/www.js" 3 seconds ago Up 2 seconds 0.0.0.0:3000->3000/tcp, :::3000->3000/tcp movies +$ docker stop movies +movies ``` Now, we have a basic script that starts our container. Before adding our @@ -102,17 +102,17 @@ wait_for_url () { return 0 } -echo "Starting Quiz container" -docker run --rm --detach --publish 8080:8080 --name quiz ghcr.io/jcamiel/quiz:latest +echo "Starting container" +docker run --name movies --rm --detach --publish 3000:3000 ghcr.io/jcamiel/hurl-express-tutorial:latest -echo "Starting Quiz instance to be ready" -wait_for_url 'http://localhost:8080' 60 +echo "Waiting server to be ready" +wait_for_url 'http://localhost:3000' 60 -echo "Stopping Quiz instance" -docker stop quiz +echo "Stopping container" +docker stop movies ``` -We have now the simplest integration test script: it pulls a Quiz image, then starts +We have now the simplest integration test script: it pulls our Docker image, then starts the container and waits for a `200 OK` response. Next, we're going to add our Hurl tests to the script. @@ -126,16 +126,16 @@ set -eu # ... -echo "Starting Quiz container" +echo "Starting container" # ... -echo "Starting Quiz instance to be ready" +echo "Waiting server to be ready" # ... echo "Running Hurl tests" hurl --test integration/*.hurl -echo "Stopping Quiz instance" +echo "Stopping container" # ... ``` @@ -144,28 +144,23 @@ echo "Stopping Quiz instance" ```shell $ bin/integration.sh -Starting Quiz container +Starting container 48cf21d193a01651fc42b80648abdb51dc626f31c3f9c8917aea899c68eb4a12 -Starting Quiz instance to be ready -Testing http://localhost:8080 -Wait 0s -Wait 1s -Wait 2s -Wait 3s -Wait 4s -Wait 5s +Waiting server to be ready +Testing http://localhost:3000 Running Hurl tests integration/basic.hurl: Running [1/2] integration/basic.hurl: Success (4 request(s) in 18 ms) -integration/create-quiz.hurl: Running [2/2] -integration/create-quiz.hurl: Success (6 request(s) in 18 ms) +integration/login.hurl: Running [2/2] +integration/login.hurl: Success (6 request(s) in 18 ms) -------------------------------------------------------------------------------- Executed files: 2 Succeeded files: 2 (100.0%) Failed files: 0 (0.0%) Duration: 48 ms -Stopping Quiz instance -quiz + +Stopping container +movies ``` Locally, our test suite is now fully functional. As Hurl is very fast, we can use @@ -176,29 +171,28 @@ to create a [GitHub Action]. You can also see how to integrate your tests in [Gi ## Running Tests with GitHub Action {:start="1"} -1. Create a new empty repository in GitHub, named `quiz-project`: +1. Create a new empty repository in GitHub, named `movies-project`:
- Create new GitHub repository - Create new GitHub repository + Create new GitHub repository + Create new GitHub repository
{:start="2"} -2. On your computer, create a git repo in `quiz-project` directory and +2. On your computer, create a git repo in `movies-project` directory and commit the projects files: ```shell $ git init -Initialized empty Git repository in /Users/jc/Documents/Dev/quiz-project/.git/ +Initialized empty Git repository in /Users/jc/Documents/Dev/movies-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 remote add origin https://github.com/jcamiel/movies-project.git $ git push -u origin main Enumerating objects: 7, done. Counting objects: 100% (7/7), done. @@ -226,13 +220,13 @@ jobs: contents: read steps: - name: Checkout - uses: actions/checkout@v2 + uses: actions/checkout@v3 - name: Build run: echo "Building app..." - name: Integration test run: | - curl --location --remote-name https://github.com/Orange-OpenSource/hurl/releases/download/3.0.1/hurl_3.0.1_amd64.deb - sudo dpkg -i hurl_3.0.1_amd64.deb + curl --location --remote-name https://github.com/Orange-OpenSource/hurl/releases/download/4.0.0/hurl_4.0.0_amd64.deb + sudo dpkg -i hurl_4.0.0_amd64.deb bin/integration.sh ``` @@ -254,8 +248,8 @@ Counting objects: 100% (6/6), done. Finally, you can check on GitHub that our action is running:
- GitHub Action - GitHub Action + GitHub Action + GitHub Action
## Running Tests with GitLab CI/CD diff --git a/sites/hurl.dev/_docs/tutorial/debug-tips.md b/sites/hurl.dev/_docs/tutorial/debug-tips.md index 572ca8f..84d8e32 100644 --- a/sites/hurl.dev/_docs/tutorial/debug-tips.md +++ b/sites/hurl.dev/_docs/tutorial/debug-tips.md @@ -19,77 +19,64 @@ information like request HTTP headers, response HTTP headers, cookie storage, du $ hurl --verbose --no-output basic.hurl * Options: * fail fast: true -* insecure: false * follow redirect: false +* insecure: false * max redirect: 50 +* retry: 0 * ------------------------------------------------------------------------------ * Executing entry 1 * * Cookie store: * * Request: -* GET http://localhost:8080 +* GET http://localhost:3000 * * Request can be run with the following curl command: -* curl 'http://localhost:8080' +* curl 'http://localhost:3000' * > GET / HTTP/1.1 -> Host: localhost:8080 +> Host: localhost:3000 > Accept: */* -> User-Agent: hurl/1.7.0-snapshot +> User-Agent: hurl/4.0.0 > -* Response: -* -< HTTP/1.1 200 -< Set-Cookie: JSESSIONID=361948EF00AA04CB6659954A8D3EBC9D; 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-FR -< Transfer-Encoding: chunked -< Date: Wed, 17 Aug 2022 07:30:15 GMT +* Response: (received 9564 bytes in 11 ms) +* +< HTTP/1.1 200 OK +< Content-Type: text/html; charset=utf-8 +< Content-Length: 9564 +< Set-Cookie: x-session-id=s%3AEE3wsnrgUPSyAkgJZGa3jMWk7xmOtv4E.kXQpkmNBXnFOqmeSssqXnecF4qqv1D7bKu3rpbEJxmQ; Path=/; HttpOnly; SameSite=Strict +< Date: Wed, 26 Jul 2023 13:16:39 GMT +< Connection: keep-alive +< Keep-Alive: timeout=5 < * * ------------------------------------------------------------------------------ * Executing entry 2 * * Cookie store: -* #HttpOnly_localhost FALSE / FALSE 0 JSESSIONID 361948EF00AA04CB6659954A8D3EBC9D +* #HttpOnly_localhost FALSE / FALSE 0 x-session-id s%3AEE3wsnrgUPSyAkgJZGa3jMWk7xmOtv4E.kXQpkmNBXnFOqmeSssqXnecF4qqv1D7bKu3rpbEJxmQ * * Request: -* GET http://localhost:8080/not-found +* GET http://localhost:3000/not-found * * Request can be run with the following curl command: -* curl 'http://localhost:8080/not-found' --cookie 'JSESSIONID=361948EF00AA04CB6659954A8D3EBC9D' +* curl --cookie 'x-session-id=s%3AEE3wsnrgUPSyAkgJZGa3jMWk7xmOtv4E.kXQpkmNBXnFOqmeSssqXnecF4qqv1D7bKu3rpbEJxmQ' 'http://localhost:3000/not-found' * > GET /not-found HTTP/1.1 -> Host: localhost:8080 +> Host: localhost:3000 > Accept: */* -> Cookie: JSESSIONID=361948EF00AA04CB6659954A8D3EBC9D -> User-Agent: hurl/1.7.0-snapshot +> Cookie: x-session-id=s%3AEE3wsnrgUPSyAkgJZGa3jMWk7xmOtv4E.kXQpkmNBXnFOqmeSssqXnecF4qqv1D7bKu3rpbEJxmQ +> User-Agent: hurl/4.0.0 > -* Response: -* -< 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-FR -< Transfer-Encoding: chunked -< Date: Wed, 17 Aug 2022 07:30:15 GMT +* Response: (received 2217 bytes in 3 ms) +* +< HTTP/1.1 404 Not Found +< Content-Type: text/html; charset=utf-8 +< Content-Length: 2217 +< Date: Wed, 26 Jul 2023 13:16:39 GMT +< Connection: keep-alive +< Keep-Alive: timeout=5 < -* ... ``` @@ -103,122 +90,126 @@ request or response body, you can display more logs with [`--very-verbose`] opti $ hurl --very-verbose --no-output basic.hurl * Options: * fail fast: true -* insecure: false * follow redirect: false +* insecure: false * max redirect: 50 +* retry: 0 * ------------------------------------------------------------------------------ * Executing entry 1 * * Cookie store: * * Request: -* GET http://localhost:8080 +* GET http://localhost:3000 * * Request can be run with the following curl command: -* curl 'http://localhost:8080' +* curl 'http://localhost:3000' * +** WARNING: failed to open cookie file "" +**  Trying 127.0.0.1:3000... +** Connected to localhost (127.0.0.1) port 3000 (#0) > GET / HTTP/1.1 -> Host: localhost:8080 +> Host: localhost:3000 > Accept: */* -> User-Agent: hurl/1.7.0-snapshot +> User-Agent: hurl/4.0.0 > * Request body: * -* Response: -* -< HTTP/1.1 200 -< Set-Cookie: JSESSIONID=0B417BD5890C001B5B25A9B321FE4800; 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-FR -< Transfer-Encoding: chunked -< Date: Wed, 17 Aug 2022 07:42:46 GMT +** Added cookie x-session-id="s%3A_l88C6GKbPeC5YuDLraWARY32NB3bP-l.T%2BViEW%2BqMrmLZDqwzDxtEbdtW67lCKt0jGvvlfqls%2FI" for domain localhost, path /, expire 0 +** Connection #0 to host localhost left intact +* Response: (received 9564 bytes in 9 ms) +* +< HTTP/1.1 200 OK +< Content-Type: text/html; charset=utf-8 +< Content-Length: 9564 +< Set-Cookie: x-session-id=s%3A_l88C6GKbPeC5YuDLraWARY32NB3bP-l.T%2BViEW%2BqMrmLZDqwzDxtEbdtW67lCKt0jGvvlfqls%2FI; Path=/; HttpOnly; SameSite=Strict +< Date: Wed, 26 Jul 2023 13:19:45 GMT +< Connection: keep-alive +< Keep-Alive: timeout=5 < * Response body: * * -* -* -* -* Welcome to Quiz! -* -* -* -* -*
+* +* +* Movies Box +* +* +* +* ... -* +* * * +* Timings: +* begin: 2023-07-26 13:19:45.378037 UTC +* end: 2023-07-26 13:19:45.387332 UTC +* namelookup: 4182 µs +* connect: 4798 µs +* app_connect: 0 µs +* pre_transfer: 4912 µs +* start_transfer: 9126 µs +* total: 9171 µs * * ------------------------------------------------------------------------------ * Executing entry 2 * * Cookie store: -* #HttpOnly_localhost FALSE / FALSE 0 JSESSIONID 0B417BD5890C001B5B25A9B321FE4800 +* #HttpOnly_localhost FALSE / FALSE 0 x-session-id s%3A_l88C6GKbPeC5YuDLraWARY32NB3bP-l.T%2BViEW%2BqMrmLZDqwzDxtEbdtW67lCKt0jGvvlfqls%2FI * * Request: -* GET http://localhost:8080/not-found +* GET http://localhost:3000/not-found * * Request can be run with the following curl command: -* curl 'http://localhost:8080/not-found' --cookie 'JSESSIONID=0B417BD5890C001B5B25A9B321FE4800' +* curl --cookie 'x-session-id=s%3A_l88C6GKbPeC5YuDLraWARY32NB3bP-l.T%2BViEW%2BqMrmLZDqwzDxtEbdtW67lCKt0jGvvlfqls%2FI' 'http://localhost:3000/not-found' * +** Found bundle for host: 0x60000340c930 [serially] +** Can not multiplex, even if we wanted to +** Re-using existing connection #0 with host localhost > GET /not-found HTTP/1.1 -> Host: localhost:8080 +> Host: localhost:3000 > Accept: */* -> Cookie: JSESSIONID=0B417BD5890C001B5B25A9B321FE4800 -> User-Agent: hurl/1.7.0-snapshot +> Cookie: x-session-id=s%3A_l88C6GKbPeC5YuDLraWARY32NB3bP-l.T%2BViEW%2BqMrmLZDqwzDxtEbdtW67lCKt0jGvvlfqls%2FI +> User-Agent: hurl/4.0.0 > * Request body: * -* Response: -* -< 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-FR -< Transfer-Encoding: chunked -< Date: Wed, 17 Aug 2022 07:42:46 GMT +** Connection #0 to host localhost left intact +* Response: (received 2217 bytes in 5 ms) +* +< HTTP/1.1 404 Not Found +< Content-Type: text/html; charset=utf-8 +< Content-Length: 2217 +< Date: Wed, 26 Jul 2023 13:19:45 GMT +< Connection: keep-alive +< Keep-Alive: timeout=5 < * Response body: * * -* -* -* -* Error 404 - Quiz -* -* -* -* -*
-* Quiz logo -*
-*
-* -*

Error 404, Page not Found!

-* ... -* +*

Not Found

+*

404

+... * * +* Timings: +* begin: 2023-07-26 13:19:45.390823 UTC +* end: 2023-07-26 13:19:45.395983 UTC +* namelookup: 44 µs +* connect: 0 µs +* app_connect: 0 µs +* pre_transfer: 126 µs +* start_transfer: 5100 µs +* total: 5124 µs * ... ``` +[`--very-verbose`] output is much more verbose; with body request and response, [`libcurl`] logs and [response timings] +are displayed. + + ### Debugging a specific entry If you have a lot of entries (request / response pairs) in your Hurl file, using [`--verbose`] or [`--very-verbose`] @@ -235,15 +226,13 @@ use an `[Options]` section that will activate logs only for the specified entry: # Check our health API: # ... -# Check question API: -GET http://localhost:8080/api/questions -# You can pass options to this entry only +# Check search API: +GET http://localhost:3000/api/search [Options] verbose: true [QueryStringParams] -offset: 0 -size: 20 -sort: oldest +q: 1982 +sort: name HTTP 200 # ... @@ -260,46 +249,96 @@ $ hurl --no-output basic.hurl * verbose: true * * Cookie store: -* #HttpOnly_localhost FALSE / FALSE 0 JSESSIONID 31818147FB20A7085AC54C372318BAF1 +* #HttpOnly_localhost FALSE / FALSE 0 x-session-id s%3Aq_5wf1l2wBQ_96y6kpLeR0J4zLJF34EZ.n%2Bu1UJPqK0Ih2tz3Dd6w2kXAuufueT6HQDekBPtHhbc * * Request: -* GET http://localhost:8080/api/questions +* GET http://localhost:3000/api/search * [QueryStringParams] -* offset: 0 -* size: 20 -* sort: oldest +* q: 1982 +* sort: name * * Request can be run with the following curl command: -* curl 'http://localhost:8080/api/questions?offset=0&size=20&sort=oldest' --cookie 'JSESSIONID=31818147FB20A7085AC54C372318BAF1' +* curl --cookie 'x-session-id=s%3Aq_5wf1l2wBQ_96y6kpLeR0J4zLJF34EZ.n%2Bu1UJPqK0Ih2tz3Dd6w2kXAuufueT6HQDekBPtHhbc' 'http://localhost:3000/api/search?q=1982&sort=name' * -> GET /api/questions?offset=0&size=20&sort=oldest HTTP/1.1 -> Host: localhost:8080 +> GET /api/search?q=1982&sort=name HTTP/1.1 +> Host: localhost:3000 > Accept: */* -> Cookie: JSESSIONID=31818147FB20A7085AC54C372318BAF1 -> User-Agent: hurl/1.7.0-snapshot +> Cookie: x-session-id=s%3Aq_5wf1l2wBQ_96y6kpLeR0J4zLJF34EZ.n%2Bu1UJPqK0Ih2tz3Dd6w2kXAuufueT6HQDekBPtHhbc +> User-Agent: hurl/4.0.0 > -* Response: -* -< 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: Wed, 17 Aug 2022 08:11:50 GMT +* Response: (received 1447 bytes in 0 ms) +* +< HTTP/1.1 200 OK +< Cache-control: no-store +< Content-Type: application/json; charset=utf-8 +< Content-Length: 1447 +< Date: Wed, 26 Jul 2023 13:29:39 GMT +< Connection: keep-alive +< Keep-Alive: timeout=5 < * ``` +## Use Error Format + +When you’ve asserts errors, the analysis can be difficult because you don’t have a lot of information apart +of the expected values: + +```shell +$ hurl --test basic.hurl +basic.hurl: Running [1/1] +error: Assert failure + --> basic.hurl:47:0 + | +47 | jsonpath "$[0].name" == "Robocop" + | actual: string  + | expected: string  + | + +basic.hurl: Failure (4 request(s) in 16 ms) +-------------------------------------------------------------------------------- +Executed files: 1 +Succeeded files: 0 (0.0%) +Failed files: 1 (100.0%) +Duration: 17 ms +``` + +With [`--error-format`] option, you can opt in for a longer error description in case of error assert. On any error, +we'll get the response headers and body. This is useful to see the expected values, especially in CI/CD +context when you've to analyse past executed tests. + +```shell +$ hurl --error-format long --test basic.hurl +basic.hurl: Running [1/1] +HTTP/1.1 200 +Cache-control: no-store +Content-Type: application/json; charset=utf-8 +Content-Length: 1447 +Date: Wed, 26 Jul 2023 14:14:00 GMT +Connection: keep-alive +Keep-Alive: timeout=5 + +[{"name":"Blade Runner","url":"/movies/blade-runner","director":"Ridley Scott","release_date":"1982-06-25","actors":["Harrison Ford","Rutger Hauer","Sean Young","Edward James Olmos"],"artwork":"/img/blade-runner-800x1200.webp","artwork_128":"/img/blade-runner-128x192.webp"},{"name":"Conan the Barbarian","url":"/movies/conan-the-barbarian","director":"John Milius","release_date":"1982-05-14","actors":["Arnold Schwarzenegger","James Earl Jones","Sandahl Bergman","Ben Davidson","Cassandra Gaviola","Gerry Lopez","Mako","Valerie Quennessen","William Smith","Max von Sydow"],"artwork":"/img/conan-the-barbarian-800x1200.webp","artwork_128":"/img/conan-the-barbarian-128x192.webp"},{"name":"The Dark Crystal","url":"/movies/the-dark-crystal","director":"Jim Henson","release_date":"1982-12-17","actors":["Stephen Garlick","Lisa Maxwell","Billie Whitelaw","Percy Edwards"],"artwork":"/img/the-dark-crystal-800x1200.webp","artwork_128":"/img/the-dark-crystal-128x192.webp"},{"name":"The Thing","url":"/movies/the-thing","director":"John Carpenter","release_date":"1982-06-25","actors":["Kurt Russell"],"artwork":"/img/the-thing-800x1200.webp","artwork_128":"/img/the-thing-128x192.webp"},{"name":"Tron","url":"/movies/tron","director":"Steven Lisberger","release_date":"1982-07-09","actors":["Jeff Bridges","Bruce Boxleitner","David Warner","Cindy Morgan","Barnard Hughes"],"artwork":"/img/tron-800x1200.webp","artwork_128":"/img/tron-128x192.webp"}] + +error: Assert failure + --> basic.hurl:47:0 + | +47 | jsonpath "$[0].name" == "Robocop" + | actual: string  + | expected: string  + | + +basic.hurl: Failure (4 request(s) in 23 ms) +``` + + ## Interactive Mode We can run the whole Hurl file request by request, with the [`--interactive` option]: ```shell +$ hurl --verbose --interactive basic.hurl * Options: * fail fast: true * insecure: false @@ -320,30 +359,25 @@ Press Q (Quit) or C (Continue) * Cookie store: * * Request: -* GET http://localhost:8080 +* GET http://localhost:3000 * * Request can be run with the following curl command: -* curl 'http://localhost:8080' +* curl 'http://localhost:3000' * > GET / HTTP/1.1 -> Host: localhost:8080 +> Host: localhost:3000 > Accept: */* -> User-Agent: hurl/1.7.0-snapshot +> User-Agent: hurl/4.0.0 > -* Response: -* -< HTTP/1.1 200 -< Set-Cookie: JSESSIONID=B08BF0F6F83E91750A76E97713A5C144; 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-FR -< Transfer-Encoding: chunked -< Date: Wed, 17 Aug 2022 08:18:36 GMT +* Response: (received 9564 bytes in 11 ms) +* +< HTTP/1.1 200 OK +< Content-Type: text/html; charset=utf-8 +< Content-Length: 9564 +< Set-Cookie: x-session-id=s%3AEE3wsnrgUPSyAkgJZGa3jMWk7xmOtv4E.kXQpkmNBXnFOqmeSssqXnecF4qqv1D7bKu3rpbEJxmQ; Path=/; HttpOnly; SameSite=Strict +< Date: Wed, 26 Jul 2023 13:16:39 GMT +< Connection: keep-alive +< Keep-Alive: timeout=5 < * @@ -364,18 +398,14 @@ In this mode, headers of the last entry are displayed: ```shell $ hurl -i basic.hurl HTTP/1.1 200 -Set-Cookie: JSESSIONID=76984131F0D0821C4A8D5CB3FC27CD3B; 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: application/json -Transfer-Encoding: chunked -Date: Fri, 13 Jan 2023 12:49:47 GMT - -{"status":"RUNNING","reportedDate":"2023-01-13T13:49:47+01:00","healthy":true,"operationId":3183000623} +Cache-control: no-store +Content-Type: application/json; charset=utf-8 +Content-Length: 1447 +Date: Wed, 26 Jul 2023 14:58:27 GMT +Connection: keep-alive +Keep-Alive: timeout=5 + +[{"name":"Blade Runner","url":"/movies/blade-runner","director":"Ridley Scott","release_date":"1982-06-25",... ``` If you want to inspect any entry other than the last one, you can run your test to a @@ -384,49 +414,20 @@ 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-FR -Transfer-Encoding: chunked -Date: Fri, 13 Jan 2023 12:50:52 GMT +Content-Type: text/html; charset=utf-8 +Content-Length: 2217 +Date: Wed, 26 Jul 2023 14:59:57 GMT +Connection: keep-alive +Keep-Alive: timeout=5 - - - - Error 404 - Quiz - - - - -
- Quiz logo -
-
- -

Error 404, Page not Found!

- -Quiz Home - - -
- - + + + +... + - ``` ## Using a Proxy @@ -463,3 +464,6 @@ the returned response to Hurl. [`--to-entry` option]: {% link _docs/manual.md %}#to-entry [mitmproxy]: https://mitmproxy.org [`-x/--proxy` option]: {% link _docs/manual.md %}#proxy +[`--error-format`]: {% link _docs/manual.md %}#error-format +[`libcurl`]: https://curl.se/libcurl/ +[response timings]: {% link _docs/response.md %}#timings diff --git a/sites/hurl.dev/_docs/tutorial/security.md b/sites/hurl.dev/_docs/tutorial/security.md index 0593d57..4afcbf5 100644 --- a/sites/hurl.dev/_docs/tutorial/security.md +++ b/sites/hurl.dev/_docs/tutorial/security.md @@ -6,205 +6,238 @@ section: Tutorial # Security -In the previous part, we have tested the basic creation of a quiz, through the -endpoint. Our test file `create-quiz.hurl` now looks like: +In the [previous part], we have tested our login workflow. So far, we have tested a "simple" form creation: each value of +the form is valid and sanitized, but what if the user put invalid data? We're going to test a user acount creation and see +how we can check that our signup workflow is secure. + + +## Server Side Validation + +In the browser, client-side validation is helping users to enter data and avoid unnecessary server load. + +On the signup page, , we have an HTML form: + + +```html + +``` + +The first input, username, has [validation HTML attributes]: `minlength="3"`, `maxlength="32"`, a pattern and `required`. +In a browser, these attributes will prevent the user from entering invalid data like a missing value or a name that is +too long. If your tests rely on a "headless" browser, it can stop you from testing 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. +To do so, we're going to test the _nominal_ user account creation case, then we'll see how to test with invalid datas. + +### Valid user creation + +{:start="1"} +1. Create a new file named `signup.hurl`. We're going to use a new REST API to give us an available username: + +```hurl +# First we obtain an available username: +GET http://localhost:3000/api/usernames/available +HTTP 200 +[Captures] +username: jsonpath "$.username" +``` + +Now, we can create a new user. As we have seen in the [previous part], first we have to get a +CSRF token from the signup part, then POST the form to create a user and finally + +{:start="2"} +2. Go to the signup page, and create a new user: {% raw %} ```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 +# First we obtain an available username: +# ... +# Create a new valid user: get the CSRF token the signup: +GET http://localhost:3000/signup HTTP 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 +POST http://localhost:3000/signup [FormParams] -name: Simpson -question0: 16f897ab -question1: dd894cca -question2: 4edc1fdb -question3: 37b9eff3 -question4: 0fec576c _csrf: {{csrf_token}} - +username: {{username}} +name: Bob +email: {{username}}@example.net +password: 12345678 HTTP 302 -[Captures] -detail_url: header "Location" [Asserts] -header "Location" matches "/quiz/detail/[a-f0-9]{8}" +header "Location" == "/my-movies" -# Open the newly created quiz detail page: -GET {{detail_url}} +# Go to my movies +GET http://localhost:3000/my-movies HTTP 200 ``` {% endraw %} -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 +Writing each step of a redirection can be a little tedious so we can ask Hurl to automatically follow redirection +after the POST login. An [`[Options]` section][options] can be used to modify how a request is played: -In the browser, there is client-side validation helping users enter data and avoid unnecessary server load. - -Our HTML form is: - -```html -
- ... - ... - ... - ... -
-``` - -The first input, name, has validation HTML attributes: `minlength="4"`, `maxlength="32"` and `required`. -In a browser, these attributes will prevent the user from entering invalid data like a missing value or a name that is too long. If your -tests rely on a "headless" browser, it can stop you from testing 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. - -{:start="1"} -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: +{:start="3"} +3. Use an `[Options]` section to follow redirection on the user account creation: {% raw %} ```hurl -# First, get the quiz creation page to capture -# ... - -# Create a new quiz, using the captured CSRF token. +# First we obtain an available username: # ... -# Open the newly created quiz detail page: -# ... +# Create a new valid user: get the CSRF token the signup: +GET http://localhost:3000/signup +HTTP 200 +[Captures] +csrf_token: xpath "string(//input[@name='_csrf']/@value)" -# Test various server-side validations: -# Invalid form name value: too short -POST http://localhost:8080/new-quiz +POST http://localhost:3000/signup +[Options] +location: true [FormParams] -name: x -question0: 16f897ab -question1: dd894cca -question2: 4edc1fdb -question3: 37b9eff3 -question4: 0fec576c _csrf: {{csrf_token}} - +username: {{username}} +name: Bob +email: {{username}}@example.net +password: 12345678 HTTP 200 [Asserts] -xpath "//label[@for='name'][@class='invalid']" exists +url endsWith "/my-movies" ``` {% endraw %} -{:start="2"} -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: +Note that, when following redirection, asserts are run against the final HTTP response. That's why we must have a `200 OK` +instead of a `302 Found`. We can also use an [`url` assert] to check what's the final redirected URL. + +{:start="4"} +4. Run `signup.hurl` and verify that everything is ok: + +```shell +$ hurl --test signup.hurl +signup.hurl: Running [1/1] +signup.hurl: Success (4 request(s) in 16 ms) +-------------------------------------------------------------------------------- +Executed files: 1 +Succeeded files: 1 (100.0%) +Failed files: 0 (0.0%) +Duration: 18 ms +``` + +### Invalid user creation + +Now that we have tested a user creation, let's try to create a user with an invalid username. We can try to create a +two letters long username for instance. In that case, we should be redirected to the signup page, with an error message +displayed. + +{:start="5"} +5. Add a POST user signup with `bo` as username: {% raw %} ```hurl -# First, get the quiz creation page to capture -# ... - -# Create a new quiz, using the captured CSRF token. +# First we obtain an available username: # ... -# Open the newly created quiz detail page: +# Create a new valid user: get the CSRF token the signup: # ... -# Test various server-side validations: +# Try an invalid username: too short. We should stay on signup +GET http://localhost:3000/signup +HTTP 200 +[Captures] +csrf_token: xpath "string(//input[@name='_csrf']/@value)" -# Invalid form name value: too short -# ... -# Invalid email parameter -POST http://localhost:8080/new-quiz +POST http://localhost:3000/signup +[Options] +location: true [FormParams] -name: Barth -email: barthsimpson -question0: 16f897ab -question1: dd894cca -question2: 4edc1fdb -question3: 37b9eff3 -question4: 0fec576c _csrf: {{csrf_token}} - +username: bo +name: Bob +email: bob78@example.net +password: 12345678 HTTP 200 [Asserts] -xpath "//label[@for='email'][@class='invalid']" exists +url endsWith "/signup" +xpath "string(//div[@class='form-errors'])" contains "Username must be 3 to 32 chars long" ``` {% endraw %} -{:start="3"} -3. Finally, add a POST request with no CSRF token, to test that our endpoint has CRSF protection: +{:start="6"} +6. Finally, add a POST request with no CSRF token to test that our endpoint has CSRF protection: ```hurl -# First, get the quiz creation page to capture +# First we obtain an available username: # ... -# Create a new quiz, using the captured CSRF token. +# Create a new valid user: get the CSRF token the signup: # ... -# Open the newly created quiz detail page: +# Try an invalid username: too short. We should stay on signup # ... -# Test various server-side validations: - -# Invalid form name value: too short -# ... -# Invalid email parameter -# ... -# No CSRF token: -POST http://localhost:8080/new-quiz +# Test CSRF token is mandatory: +POST http://localhost:3000/signup [FormParams] -name: Barth -email: barth.simpson@provider.net -question0: 16f897ab -question1: dd894cca -question2: 4edc1fdb -question3: 37b9eff3 -question4: 0fec576c +username: bob +name: Bob +email: bob78@example.net +password: 12345678 HTTP 403 ``` -> We're using [the exist predicate] to check labels in the DOM +This final test is also interesting because if you're testing your page with a headless browser, the CRSF token is always +created and sent and you don't test that your backend has CSRF protection. -{:start="4"} -4. Run `create-quiz.hurl` and verify that everything is ok: + + +{:start="7"} +7.Run `signup.hurl` and verify that everything is ok: ```shell -create-quiz.hurl: Running [1/1] -create-quiz.hurl: Success (6 request(s) in 33 ms) +$ hurl --test signup.hurl +signup.hurl: Running [1/1] +signup.hurl: Success (8 request(s) in 28 ms) -------------------------------------------------------------------------------- Executed files: 1 Succeeded files: 1 (100.0%) Failed files: 0 (0.0%) -Duration: 41 ms +Duration: 35 ms ``` ## Comments -So Hurl, being close to the HTTP layer, has no "browser protection" / client-side validation: it facilitates +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 comments in your served HTML. Comments can reveal sensitive information +Another security use case is checking that your served HTML isn't leaking comments. Comments can reveal sensitive information and [is it recommended] to trim HTML comments in your production files. -Popular front-end frameworks like [ReactJS] or [Vue.js] use client-side JavaScript. +Popular front-end frameworks like [ReactJS] or [Vue.js] use client-side JavaScript rendering. If you use one of these frameworks, and you inspect the DOM with the browser developer tools, you won't see any comments -because the framework is managing the DOM and is removing them. +because the framework managing the DOM is removing them. But, if you look at the HTML page sent on the network, i.e. the real HTML document sent by the server (and not _the document dynamically created by the framework_), you can still see those HTML comments. @@ -212,47 +245,52 @@ server (and not _the document dynamically created by the framework_), you can st With Hurl, you will be able to check the content of the _real_ network data. {:start="1"} -1. In the first entry of `create-quiz.hurl`, add a [XPath assert] when getting the quiz creation page: +1. In the second entry of `signup.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 +# First we obtain an available username: +# ... +# Create a new valid user: get the CSRF token the signup: +GET http://localhost:3000/signup HTTP 200 [Captures] csrf_token: xpath "string(//input[@name='_csrf']/@value)" [Asserts] xpath "//comment" count == 0 # Check that we don't leak comments - # ... ``` {:start="2"} -2. Run `create-quiz.hurl` and verify that everything is ok: +2. Run `signup.hurl` and verify that everything is ok: ```shell -$ hurl --test create-quiz.hurl -create-quiz.hurl: Running [1/1] -create-quiz.hurl: Success (6 request(s) in 33 ms) +$ hurl --test signup.hurl +signup.hurl: Running [1/1] +signup.hurl: Success (8 request(s) in 28 ms) -------------------------------------------------------------------------------- Executed files: 1 Succeeded files: 1 (100.0%) Failed files: 0 (0.0%) -Duration: 41 ms +Duration: 31 ms ``` ## Recap -So, our test file `create-quiz.hurl` is now: +So, our test file `signup.hurl` is now: {% raw %} ```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 +# First we obtain an available username: +GET http://localhost:3000/api/usernames/available +HTTP 200 +[Captures] +username: jsonpath "$.username" + +# Create a new valid user: get the CSRF token the signup: +GET http://localhost:3000/signup HTTP 200 [Captures] csrf_token: xpath "string(//input[@name='_csrf']/@value)" @@ -260,89 +298,71 @@ csrf_token: xpath "string(//input[@name='_csrf']/@value)" 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 +POST http://localhost:3000/signup +[Options] +location: true [FormParams] -name: Simpson -question0: 16f897ab -question1: dd894cca -question2: 4edc1fdb -question3: 37b9eff3 -question4: 0fec576c _csrf: {{csrf_token}} - -HTTP 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}} +username: {{username}} +name: Bob +email: {{username}}@example.net +password: 12345678 HTTP 200 +[Asserts] +url endsWith "/my-movies" -# 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}} +# Play some checks on signup form: username too short +# email already taken, invalid pattern for username +GET http://localhost:3000/signup HTTP 200 -[Asserts] -xpath "//label[@for='name'][@class='invalid']" exists +[Captures] +csrf_token: xpath "string(//input[@name='_csrf']/@value)" -# Invalid email parameter: -POST http://localhost:8080/new-quiz +# Create a new user, username too short +POST http://localhost:3000/signup +[Options] +location: true [FormParams] -name: Barth -email: barthsimpson -question0: 16f897ab -question1: dd894cca -question2: 4edc1fdb -question3: 37b9eff3 -question4: 0fec576c _csrf: {{csrf_token}} - +username: bo +name: Bob +email: bob78@example.net +password: 12345678 HTTP 200 [Asserts] -xpath "//label[@for='email'][@class='invalid']" exists +url endsWith "/signup" +xpath "string(//div[@class='form-errors'])" contains "Username must be 3 to 32 chars long" -# No CSRF token: -POST http://localhost:8080/new-quiz +# Test CSRF is mandatory: +POST http://localhost:3000/signup [FormParams] -name: Barth -email: barth.simpson@provider.net -question0: 16f897ab -question1: dd894cca -question2: 4edc1fdb -question3: 37b9eff3 -question4: 0fec576c +username: bob +name: Bob +email: bob78@example.net +password: 12345678 HTTP 403 ``` {% endraw %} -We have seen that Hurl can be used as a security tool, to check your server-side validation. +We have seen that Hurl can be used as a security tool to check your 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]: {% link _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 +[is it recommended]: https://owasp.org/www-project-web-security-testing-guide/v41/4-Web_Application_Security_Testing/01-Information_Gathering/05-Review_Webpage_Comments_and_Metadata_for_Information_Leakage [DOM]: https://en.wikipedia.org/wiki/Document_Object_Model [ReactJS]: https://reactjs.org [Vue.js]: https://vuejs.org [XPath assert]: {% link _docs/asserting-response.md %}#xpath-assert [GitHub Action]: https://github.com/features/actions [GitLab CI/CD]: https://docs.gitlab.com/ee/ci/ +[previous part]: {% link _docs/tutorial/captures.md %} +[options]: {% link _docs/request.md %}#options +[`url` assert]: {% link _docs/asserting-response.md %}#url-assert +[validation HTML attributes]: https://developer.mozilla.org/en-US/docs/Learn/Forms/Form_validation diff --git a/sites/hurl.dev/_docs/tutorial/your-first-hurl-file.md b/sites/hurl.dev/_docs/tutorial/your-first-hurl-file.md index 7da81d6..70f23a7 100644 --- a/sites/hurl.dev/_docs/tutorial/your-first-hurl-file.md +++ b/sites/hurl.dev/_docs/tutorial/your-first-hurl-file.md @@ -46,33 +46,42 @@ If you want to use the Docker image, you must have Docker installed locally. If 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 +$ docker pull ghcr.io/jcamiel/hurl-express-tutorial:latest +$ docker run --name movies --rm --detach --publish 3000:3000 ghcr.io/jcamiel/hurl-express-tutorial: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 +CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES +4002ce42e507 ghcr.io/jcamiel/hurl-express-tutorial:latest "node dist/bin/www.js" 3 seconds ago Up 2 seconds 0.0.0.0:3000->3000/tcp, :::3000->3000/tcp movies ``` If you want to launch the Node application, you must have Node installed locally. ```shell -$ git clone -$ cd movies-box -$ npm run build -$ node dist/www/bin +$ git clone https://github.com/jcamiel/hurl-express-tutorial.git && cd hurl-express-tutorial +$ npm install +$ npm start ``` Either you're using the Docker images or the Node app, you can open a browser and test the website by typing the URL : -
- Quiz home page - Quiz home page +
+ + + + + Movies Box home page + + + + + + Movies Box home page +
Play a little with the site. You can see details of each movie, search for movies (try "1982"), login to add favorites @@ -114,8 +123,9 @@ $ hurl basic.hurl ``` -If the Movies Box website is running, you should see the content of the HTML file at . If the website -is not running, you'll see an error: +If the Movies Box website is running, you should see the content of the HTML file at . + +If the website is not running, you'll see an error: ```shell $ hurl basic.hurl @@ -140,7 +150,7 @@ 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: ```hurl -GET http://localhost:8080 +GET http://localhost:3000 HTTP 200 ``` @@ -191,7 +201,7 @@ Duration: 27 ms 6. Modify `basic.hurl` to test a different HTTP response status code: ```hurl -GET http://localhost:8080 +GET http://localhost:3000 HTTP 500 ``` diff --git a/sites/hurl.dev/_sass/_code.scss b/sites/hurl.dev/_sass/_code.scss index c62756b..8851aac 100644 --- a/sites/hurl.dev/_sass/_code.scss +++ b/sites/hurl.dev/_sass/_code.scss @@ -237,4 +237,5 @@ pre, code { color: $rust-name-builtin-light; color: var(--rust-name-builtin); } -} \ No newline at end of file +} + diff --git a/sites/hurl.dev/_sass/_utilities.scss b/sites/hurl.dev/_sass/_utilities.scss index fe160f5..e6b492b 100644 --- a/sites/hurl.dev/_sass/_utilities.scss +++ b/sites/hurl.dev/_sass/_utilities.scss @@ -117,4 +117,8 @@ .u-greyed { color: $dark-grey; +} + +.u-max-width-100 { + max-width: 100%; } \ No newline at end of file diff --git a/sites/hurl.dev/_sass/main.scss b/sites/hurl.dev/_sass/main.scss index 13ade0f..b094399 100644 --- a/sites/hurl.dev/_sass/main.scss +++ b/sites/hurl.dev/_sass/main.scss @@ -355,15 +355,6 @@ footer { margin-top: $m2; } -.home-html-report { - max-width: 480px; - margin-left: 0; -} - -.home-html-report img { - width: 100%; -} - .doc { display: flex; padding: 0; diff --git a/sites/hurl.dev/assets/img/developer-tools.avif b/sites/hurl.dev/assets/img/developer-tools.avif new file mode 100644 index 0000000..283b2f3 Binary files /dev/null and b/sites/hurl.dev/assets/img/developer-tools.avif differ diff --git a/sites/hurl.dev/assets/img/developer-tools.png b/sites/hurl.dev/assets/img/developer-tools.png new file mode 100644 index 0000000..18b78b1 Binary files /dev/null and b/sites/hurl.dev/assets/img/developer-tools.png differ diff --git a/sites/hurl.dev/assets/img/developer-tools.webp b/sites/hurl.dev/assets/img/developer-tools.webp new file mode 100644 index 0000000..039f47b Binary files /dev/null and b/sites/hurl.dev/assets/img/developer-tools.webp differ diff --git a/sites/hurl.dev/assets/img/github-action-dark.png b/sites/hurl.dev/assets/img/github-action-dark.png index 4004a1b..821b0af 100644 Binary files a/sites/hurl.dev/assets/img/github-action-dark.png and b/sites/hurl.dev/assets/img/github-action-dark.png differ diff --git a/sites/hurl.dev/assets/img/github-action-light.png b/sites/hurl.dev/assets/img/github-action-light.png index e5019a4..42ea31e 100644 Binary files a/sites/hurl.dev/assets/img/github-action-light.png and b/sites/hurl.dev/assets/img/github-action-light.png differ diff --git a/sites/hurl.dev/assets/img/github-new-repository-dark.png b/sites/hurl.dev/assets/img/github-new-repository-dark.png index c5f854a..9f44bb7 100644 Binary files a/sites/hurl.dev/assets/img/github-new-repository-dark.png and b/sites/hurl.dev/assets/img/github-new-repository-dark.png differ diff --git a/sites/hurl.dev/assets/img/github-new-repository-light.png b/sites/hurl.dev/assets/img/github-new-repository-light.png index e7dfaba..311177d 100644 Binary files a/sites/hurl.dev/assets/img/github-new-repository-light.png and b/sites/hurl.dev/assets/img/github-new-repository-light.png differ diff --git a/sites/hurl.dev/assets/img/movies-box-dark.avif b/sites/hurl.dev/assets/img/movies-box-dark.avif new file mode 100644 index 0000000..628b62c Binary files /dev/null and b/sites/hurl.dev/assets/img/movies-box-dark.avif differ diff --git a/sites/hurl.dev/assets/img/movies-box-dark.webp b/sites/hurl.dev/assets/img/movies-box-dark.webp new file mode 100644 index 0000000..442c8c4 Binary files /dev/null and b/sites/hurl.dev/assets/img/movies-box-dark.webp differ diff --git a/sites/hurl.dev/assets/img/movies-box-light.avif b/sites/hurl.dev/assets/img/movies-box-light.avif new file mode 100644 index 0000000..1c9a725 Binary files /dev/null and b/sites/hurl.dev/assets/img/movies-box-light.avif differ diff --git a/sites/hurl.dev/assets/img/movies-box-light.webp b/sites/hurl.dev/assets/img/movies-box-light.webp new file mode 100644 index 0000000..56cdb49 Binary files /dev/null and b/sites/hurl.dev/assets/img/movies-box-light.webp differ diff --git a/sites/hurl.dev/index.md b/sites/hurl.dev/index.md index 966b69c..b7a6dd2 100644 --- a/sites/hurl.dev/index.md +++ b/sites/hurl.dev/index.md @@ -138,18 +138,18 @@ sha256 == hex,039058c6f2c0cb492c533b0a4d14ef77cc0f78abccced5287d84a1a2011cfb81; Finally, Hurl is easy to integrate in CI/CD, with text, JUnit and HTML reports -
+
- HTML report + HTML report - HTML report + HTML report