hurl

Install

官网: https://hurl.dev

sudo pacman -S hurl

Use

movies.hurl

# 以下内容测试用例来自于官方教程
# 测试服务来自于官方提供的 docker 服务
# docker run --name movies -d --detach --publish 3000:3000 ghcr.io/jcamiel/hurl-express-tutorial:latest
#
# 测试方法:
#     hurl --test movies.hurl                      # 直接测试全部,输出测试结果,不要细节
#     hurl --verbose movies.hurl                   # 输出包含调试信息,请求细节以及响应头信息
#     hurl --very-verbose movies.hurl              # 输出包含调试信息,以及非常详细的请求和响应,甚至 libcurl 日志,响应时间等
#     hurl --error-format long --test movies.hurl  # 可以在有断言错误时输出错误详情,在CI/CD 时尤其有用
#     hurl --verbose --interactive movies.hurl     # 可以以互动的模式运行
#     hurl -i movies.hurl                          # 仅出HTTP头
#
# 使用变量:
#     hurl --variable host=http://localhost:3000 --test basic.hurl # 可以在内部使用 `GET {{host}}` 来指代要请求的地址,不必硬编码
#     hurl --variable host=http://localhost:3000 --test *.hurl     # 测试多个脚本
#
# 代理方法:
# 搭建代理:
#     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
# 使用代理:
#     hurl --proxy localhost:8888 basic.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"
header "Set-Cookie" startsWith "x-session-id="
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(//h2)" == "Error"
xpath "string(//h3)" == "Not Found"



# Check our health API:
GET http://localhost:3000/api/health
[Options]
very-verbose: true
# use - to output on standard output, foo.bin to save on disk 
output: -
HTTP 200
[Asserts]
header "Content-Type" == "application/json; charset=utf-8"
jsonpath "$.status" == "RUNNING"
jsonpath "$.healthy" == true
jsonpath "$.operationId" exists



# Check search API:
GET http://localhost:3000/api/search?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" == "1982-06-25"




# Check search API:
GET http://localhost:3000/api/search
[Options]
verbose: true
[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" == "1982-06-25"




# 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" startsWith "1982"




# 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"

csrf.hurl

# https://hurl.dev/docs/tutorial/captures.html

# First, display the login page to capture
# the CSRF token (see https://en.wikipedia.org/wiki/Cross-site_request_forgery)
GET http://localhost:3000/login
HTTP 200
[Captures]
csrf_token: xpath "string(//input[@name='_csrf']/@value)"


# Log in user, using the captured CSRF token:
POST http://localhost:3000/login
[FormParams]
username: fab
password: 12345678
_csrf: {{csrf_token}}
HTTP 302
[Asserts]
header "Location" == "/my-movies"


# Follow redirection and open favorites:
GET http://localhost:3000/my-movies
HTTP 200
[Asserts]
xpath "string(//title)" == "My Movies"

signup_ok.hurl

# https://hurl.dev/docs/tutorial/security.html
# 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)"


POST http://localhost:3000/signup
[Options]
location: true
[FormParams]
_csrf: {{csrf_token}}
username: {{username}}
name: Bob
email: {{username}}@example.net
password: 12345678
HTTP 200
[Asserts]
url endsWith "/my-movies"

signup_fail.hurl

# https://hurl.dev/docs/tutorial/security.html
#

# 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)"
[Asserts]
xpath "//comment" count == 0     # Check that we don't leak comments


POST http://localhost:3000/signup
[Options]
location: true
[FormParams]
_csrf: {{csrf_token}}
username: {{username}}
name: Bob
email: {{username}}@example.net
password: 12345678
HTTP 200
[Asserts]
url endsWith "/my-movies"


# Play some checks on signup form: username too short
# email already taken, invalid pattern for username
GET http://localhost:3000/signup
HTTP 200
[Captures]
csrf_token: xpath "string(//input[@name='_csrf']/@value)"


# Create a new user, username too short
POST http://localhost:3000/signup
[Options]
location: true
[FormParams]
_csrf: {{csrf_token}}
username: bo
name: Bob
email: bob78@example.net
password: 12345678
HTTP 200
[Asserts]
url endsWith "/signup"
xpath "string(//div[@class='form-errors'])" contains "Username must be 3 to 32 chars long"


# Test CSRF is mandatory:
POST http://localhost:3000/signup
[FormParams]
username: bob
name: Bob
email: bob78@example.net
password: 12345678
HTTP 403

integration.sh

#!/bin/sh

set -eu

wait_for_url() {
  echo "Testing $1..."
  printf 'GET %s\nHTTP 200' "$1" | hurl --retry "$2" >/dev/null
  return 0
}

echo "Starting container"
docker run --name movies --rm --detach --publish 3000:3000 ghcr.io/jcamiel/hurl-express-tutorial:latest

echo "Waiting server to be ready"
wait_for_url "$1" 60

echo "Running Hurl tests"
hurl --variable host="$1" --test integration/*.hurl

echo "Stopping container"
docker stop movies

test.hurl

GET http://httpbin.org/get
HTTP 200                                           # 隐式断言
Content-Type: application/json                     # 隐式断言
[Asserts]                                          # 显示断言
jsonpath "$.url" contains "http://httpbin.org/get"
# header "User-Agent" contains "hurl"

POST http://httpbin.org/post
HTTP 200                                           # 隐式断言
Content-Type: application/json                     # 隐式断言
[Asserts]                                          # 显示断言
jsonpath "$.url" contains "http://httpbin.org/post"
# header "User-Agent" contains "hurl"