Init Commit - beta release 0.99.12

This commit is contained in:
Fabrice Reix 2020-08-27 09:07:46 +02:00
commit d849e0d1b3
434 changed files with 33440 additions and 0 deletions

6
.gitignore vendored Normal file
View File

@ -0,0 +1,6 @@
*.swp
*.swo
*.pyc
target/
.idea/

47
.travis.yml Normal file
View File

@ -0,0 +1,47 @@
language: rust
sudo: true
rust:
- 1.42.0
jobs:
include:
- os: linux
dist: bionic
- os: osx
#cache: cargo
before_install:
- if [ "$TRAVIS_OS_NAME" = "osx" ]; then brew update && brew install libxml2 jq; fi
- if [ "$TRAVIS_OS_NAME" = "linux" ]; then sudo apt-get update && sudo apt install python3-pip; fi
before_script:
- rustup component add clippy
- cargo clippy --version
- cargo install cargo-deb
- which python3
- python3 -V
- pip3 install Flask
- cd integration && python3 server.py&
- sleep 2
- export VERSION=$(grep '^version' Cargo.toml | cut -f2 -d'"')
script:
- ./build.sh
- cargo build --release --verbose
- strip $PWD/target/release/hurl
- strip $PWD/target/release/hurlfmt
- export PATH="$PWD/target/release:$PATH"
- integration/integration.sh
- ci/create_tarballs target/upload "$TRAVIS_OS_NAME"
- if [ "$TRAVIS_OS_NAME" = "linux" ]; then cargo deb && cp target/debian/*.deb target/upload; fi
deploy:
provider: script
script: ci/deploy.sh
skip_cleanup: true
on:
all_branch: true
tags: true

1941
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

49
Cargo.toml Normal file
View File

@ -0,0 +1,49 @@
[package]
name = "hurl"
version = "0.99.12"
authors = ["Fabrice Reix <fabrice.reix@orange.com>"]
edition = "2018"
build = "build.rs"
[lib]
name = "hurl"
[features]
# Treat warnings as a build error.
strict = []
[dependencies]
clap = "2.33.0"
structopt = "0.2.10"
reqwest = "0.9.20"
libxml = "0.2.12"
regex = "1.1.0"
serde_json = "1.0.40"
xmlparser = "0.10.0"
roxmltree = "0.7.1"
serde-xml-rs = "0.3.1"
atty = "0.2.13"
url = "2.1.0"
sxd-document = "0.3.2"
serde = "1.0.104"
percent-encoding = "2.1.0"
cookie = "0.12.0"
base64 = "0.11.0"
float-cmp = "0.6.0"
encoding = "0.2"
chrono = "0.4.11"
curl = "0.4.33"
#[dev-dependencies]
proptest = "0.9.4"
[package.metadata.deb]
assets = [
["target/release/hurl", "usr/bin/", "755"],
["target/release/hurlfmt", "usr/bin/", "755"],
["target/man/hurl.1.gz", "usr/share/man/man1/hurl.1.gz", "644"],
["target/man/hurlfmt.1.gz", "usr/share/man/man1/hurlfmt.1.gz", "644"]
]

11
LICENSE.md Normal file
View File

@ -0,0 +1,11 @@
Hurl is licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

111
README.md Normal file
View File

@ -0,0 +1,111 @@
<a href="https://hurl.dev"><img src="https://raw.githubusercontent.com/Orange-OpenSource/hurl/master/docs/logo.svg?sanitize=true" align="center" width="264px"/></a>
<br/>
[![deploy status](https://travis-ci.org/Orange-OpenSource/hurl.svg?branch=master)](https://travis-ci.org/Orange-OpenSource/hurl/)
[![documentation](https://img.shields.io/badge/-documentation-informational)](https://hurl.dev)
# What's Hurl?
Hurl is a command line tool and a simple plain text format for describing an HTTP session.
Hurl is used in command lines or scripts to run HTTP sessions. Hurl can performs requests, capture values
and evaluate queries on headers and body response. Hurl is very versatile: it can be used to get HTTP data and
also to test HTTP sessions.
```hurl
# Get home:
GET https://example.net
HTTP/1.1 200
[Captures]
csrf_token: xpath "string(//meta[@name='_csrf_token']/@content)"
# Do login!
POST https://example.net/login?user=toto&password=1234
X-CSRF-TOKEN: {{csrf_token}}
HTTP/1.1 302
```
Chaining multiple requests is easy:
```hurl
GET https://api.example.net/health
GET https://api.example.net/health
GET https://api.example.net/health
GET https://api.example.net/health
```
# Also an HTTP Test Tool
Hurl can run HTTP requests but can also be used to test HTTP responses.
Different type of queries and predicates are supported, from [XPath](https://en.wikipedia.org/wiki/XPath)
and [JSONPath](https://goessner.net/articles/JsonPath/) on body response, to assert on status code and response headers.
```hurl
GET https://example.net
HTTP/1.1 200
[Asserts]
xpath "normalize-space(//head/title)" equals "Hello world!"
```
and is well adapted for REST/json apis
```hurl
POST https://api.example.net/tests
{
"id": "456",
"evaluate": true
}
HTTP/1.1 200
[Asserts]
jsonpath "$.status" equals "RUNNING" # Check the status code
jsonpath "$.tests" countEquals 25 # Check the number of items
```
and even SOAP apis
```hurl
POST https://example.net/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="http://www.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
```
# Documentation
Visit the [Hurl web site](https://hurl.dev) to find out how to install and use Hurl.
- [Installation](https://hurl.dev/docs/installation.html)
- [Samples](https://hurl.dev/docs/samples.html)
- [File Format](https://hurl.dev/docs/entry.html)
# Feedbacks
Hurl is still in beta, any 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
```

45
build.rs Normal file
View File

@ -0,0 +1,45 @@
/*
* hurl (https://hurl.dev)
* Copyright (C) 2020 Orange
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/
use std::process;
// objectives
// get version
// get git commit
// get build date
// => hard-code into your binaries
fn main() {
// Make the current git hash available to the build.
if let Some(rev) = git_revision_hash() {
println!("cargo:rustc-env=HURL_BUILD_GIT_HASH={}", rev);
}
}
fn git_revision_hash() -> Option<String> {
let result = process::Command::new("git")
.args(&["rev-parse", "--short=10", "HEAD"])
.output();
result.ok().and_then(|output| {
let v = String::from_utf8_lossy(&output.stdout).trim().to_string();
if v.is_empty() {
None
} else {
Some(v)
}
})
}

27
build.sh Executable file
View File

@ -0,0 +1,27 @@
#!/bin/bash
set -e
ROOT_DIR="$(dirname "$0")"
#cargo clean
cargo build --features "strict"
cargo test
cargo doc --document-private-items
touch src/lib.rs
cargo clippy -- -D warnings
DOCS_DIR="$ROOT_DIR/docs"
MAN_DIR="$ROOT_DIR/target/man"
mkdir -p "$MAN_DIR"
cp "$DOCS_DIR/hurl.1" "$MAN_DIR"
cp "$DOCS_DIR/hurlfmt.1" "$MAN_DIR"
gzip "$MAN_DIR"/hurl.1
gzip "$MAN_DIR"/hurlfmt.1
echo
echo "!!! Build successful !!!"

20
ci/check_tag Executable file
View File

@ -0,0 +1,20 @@
#!/bin/bash
set -e
set -u
VERSION=$1
echo check git tag
TAG=$(git tag --points-at HEAD |tr -d '\n')
if [ "$TAG" == "" ]; then
echo "Tag is not set"
exit 0
fi
if [ "$TAG" != "$VERSION" ]; then
echo "Tag '$TAG' does not match version '$VERSION'"
exit 1
fi
echo tag matches version

28
ci/create_tarballs Executable file
View File

@ -0,0 +1,28 @@
#!/bin/bash
set -e
set -u
OUTPUT_DIR="$1"
OS="$2"
RELEASE_DIR=$PWD/target/release
MAN_DIR=$PWD/target/man
VERSION=$(grep '^version' Cargo.toml | cut -f2 -d'"')
PACKAGE_DIR="hurl-$VERSION"
TARBALL_FILE="hurl-$VERSION-x86_64-$OS.tar.gz"
mkdir -p "$OUTPUT_DIR"
echo create tarballs into "$OUTPUT_DIR"
cd "$OUTPUT_DIR"
rm -rf "$PACKAGE_DIR"
mkdir "$PACKAGE_DIR"
cp "$RELEASE_DIR/hurl" "$PACKAGE_DIR"
cp "$RELEASE_DIR/hurlfmt" "$PACKAGE_DIR"
cp "$MAN_DIR"/* "$PACKAGE_DIR"
tar cvfz "$TARBALL_FILE" "$PACKAGE_DIR"
rm -rf "$PACKAGE_DIR"

26
ci/deploy.sh Executable file
View File

@ -0,0 +1,26 @@
#!/bin/bash
set -e
set -u
echo check git tag
TAG=$(git tag --points-at HEAD |tr -d '\n')
if [ "$TAG" == "" ]; then
echo "Tag is not set"
exit 0
fi
if [ "$TAG" != "$VERSION" ]; then
echo "Tag '$TAG' does not match version '$VERSION'"
exit 1
fi
echo tag matches version
ci/upload.sh "$VERSION" target/upload/*

45
ci/upload.sh Executable file
View File

@ -0,0 +1,45 @@
#!/bin/bash
set -e
set -u
tag=$1
shift
API_URL="https://api.github.com"
REPO_NAME=hurl
repo_url="$API_URL/repos/Orange-OpenSource/$REPO_NAME"
auth_header="Authorization: token $GITHUB_API_TOKEN"
echo "Uploading to $REPO_NAME for tag $tag"
release_id=$(curl -s -H "$auth_header" "$repo_url/releases/tags/$tag" | jq '.id')
upload_url=https://uploads.github.com/repos/Orange-OpenSource/$REPO_NAME/releases/$release_id/assets
echo "release from tag: $release_id"
asset_files=$*
for asset_file in $asset_files; do
echo "Uploading asset file $asset_file"
if [ ! -f "$asset_file" ]; then
echo "does not exist!"
exit 1
fi
asset_name=$(basename "$asset_file")
asset_url="https://github.com/Orange-OpenSource/$REPO_NAME/releases/download/$VERSION/$asset_name"
echo "asset url: $asset_url"
if curl --output /dev/null --silent --head --fail "$asset_url"; then
echo "File already uploaded"
exit 1
fi
curl -v -X POST \
-H "$auth_header" \
-H "Content-Type: application/octet-stream" \
--data-binary "@$asset_file" "$upload_url?name=$asset_name"
done

43
docs/gen_doc.py Executable file
View File

@ -0,0 +1,43 @@
#!/usr/bin/env python3
import sys
import re
def header():
return '''---
layout: doc
title: Man Page
---
# {{ page.title }}
'''
def escape(s):
return s.replace('<', '&lt;').replace('--', '\\-\\-')
def add_anchor_for_h2(s):
lines = []
p = re.compile('^## (.*)$')
for line in s.split('\n'):
m = p.match(line)
if m:
value = m.group(1)
anchor = value.lower().strip().replace(' ', '-')
lines.append('## ' + value + ' {#' + anchor + '}')
else:
lines.append(line)
return '\n'.join(lines)
def main():
input_file = sys.argv[1]
lines = open(input_file).readlines()
s = ''.join(lines)
s = escape(s)
s = add_anchor_for_h2(s)
print(header() + s)
if __name__ == '__main__':
main()

57
docs/gen_manpage.py Executable file
View File

@ -0,0 +1,57 @@
#!/usr/bin/env python3
import sys
import re
def header(version):
return '.TH hurl 1 "DATE" "hurl %s" " Hurl Manual"' % (version)
def version():
s = open('../Cargo.toml', 'r').read()
p = re.compile('version(.*)"')
p = re.compile('(.*)', re.MULTILINE)
m = p.match(s)
return '0.99'
def process_code_block(s):
p = re.compile("```(.*?)```" , re.DOTALL )
return p.sub('\\\\f[C]\\1\\\\f[R]', s)
def convert_md(s):
p = re.compile('^###\s+(.*)')
s = p.sub('.IP "\\1"', s)
p = re.compile('^##')
s = p.sub('.SH', s)
p = re.compile('\*\*(.*)\*\*\s+')
s = p.sub('.B \\1\n', s)
# Remove link Text
p = re.compile('\[(.*)\]\(.*\)')
s = p.sub('\\\\fI\\1\\\\fP', s)
# Remove local anchor
p = re.compile('{#.*}')
s = p.sub('', s)
return s
def main():
input_file = sys.argv[1]
data = open(input_file).readlines()
print(header(version()))
s = ''.join([convert_md(line) for line in data])
#s = process_code_block(s)
print(s)
if __name__ == '__main__':
main()

322
docs/hurl.1 Normal file
View File

@ -0,0 +1,322 @@
.TH hurl 1 "DATE" "hurl 0.99" " Hurl Manual"
.SH NAME
hurl - run and test HTTP requests.
.SH SYNOPSIS
.B hurl
[options] [FILE...]
.SH DESCRIPTION
.B 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-520814d64e2f9249ea44e0f0"
},
"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 the HTTP requests and output the response body of the last http call.
.SH HURL FILE FORMAT
The Hurl file format is fully documented in \fIhttps://hurl.dev/docs/hurl-file.html\fP
It consists of one or several HTTP requests
GET http:/example.net/endpoint1
GET http:/example.net/endpoint2
.IP "Capturing values"
A value from an HTTP response can be-reused for successive HTTP requests.
A typical example occurs with csrf tokens.
GET https://example.net
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.net/login?user=toto&password=1234
X-CSRF-TOKEN: {{csrf_token}}
.IP "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.
GET http:/google.com
HTTP/1.1 302
It can also include asserts on the response headers
GET http:/google.com
HTTP/1.1 302
Location: http://www.google.com
You can also include explicit asserts combining query and predicate
GET http:/google.com
HTTP/1.1 302
[Asserts]
xpath "//title" equals "301 Moved"
Thanks to asserts, Hurl can be used as a testing tool to run scenarii.
.SH OPTIONS
Options that exist in curl have exactly the same semantic.
.IP "--append "
This option can only be used with \fI--json\fP. It appends sessions to existing file instead of overwriting it.
This is typically used in a CI pipeline.
.IP "--color "
Colorize Output
.IP "-b, --cookie <filename> "
Read cookies from file (using the Netscape cookie file format).
Combined with \fI-c, --cookie-jar\fP, you can simulate a cookie storage between successive Hurl runs.
.IP "-c, --cookie-jar <filename> "
Write cookies to FILE after running the session (only for one session).
The file will be written using the Netscape cookie file format.
Combined with \fI-b, --cookie\fP,you can simulate a cookie storage between successive Hurl runs.
.IP "--fail-at-end "
Continue executing requests to the end of the Hurl file even when an assert error occurs.
By default, Hurl exits after an assert error in the HTTP response.
Note that this option does not affect the behavior with mutiple input Hurl files.
All the input files are executed independently. The result of one file does not affect the excecution of the other Hurl files.
.IP "--file-root <dir> "
Set root filesystem to import files in Hurl. This is used for both files in multipart form data and request body.
When this is not explicitly defined, the files are relative to the current directory in which Hurl is running.
.IP "-h, --help "
Usage help. This lists all current command line options with a short description.
.IP "--html <dir> "
Generate html report in dir.
If you want to combine results from different Hurl executions in a unique html report, you must also use the options \fI--json](#json) and [--append\fP.
.IP "-i, --include "
Include the HTTP headers in the output.
.IP "--json <file> "
Write full session(s) to a json file. The format is very closed to HAR format.
By default, this file is overwritten by the current run execution.
In order to append sessions to an existing json file, the option \fI--append\fP must be used.
This is typically used in a CI pipeline.
.IP "-k, --insecure "
This option explicitly allows Hurl to perform "insecure" SSL connections and transfers.
.IP "-L, --location "
Follow redirect. You can limit the amount of redirects to follow by using the \fI--max-redirs\fP option.
.IP "--max-redirs <num> "
Set maximum number of redirection-followings allowed
By default, the limit is set to 50 redirections. Set this option to -1 to make it unlimited.
.IP "--no-color "
Do not colorize Output
.IP "--noproxy <no-proxy-list> "
Comma-separated list of hosts which do not use a proxy.
Override value from Environment variable no_proxy.
.IP "--to-entry <entry-number> "
Execute Hurl file to ENTRY_NUMBER (starting at 1).
Ignore the remaining of the file. It is useful for debugging a session.
.IP "-o, --output <file> "
Write output to <file> instead of stdout.
.IP "-x, --proxy [protocol://]host[:port] "
Use the specified proxy.
.IP "--variable <name=value> "
Define variable (name/value) to be used in Hurl templates.
Only string values can be defined.
.IP "-v, --verbose "
Turn on verbose output on standard error stream
Useful for debugging.
A line starting with '>' means data sent by Hurl.
A line staring with '<' means data received by Hurl.
A line starting with '*' means additional info provided by Hurl.
If you only want HTTP headers in the output, -i, --include might be the option you're looking for.
.IP "-V, --version "
Prints version information
.SH ENVIRONMENT
Environment variables can only be specified in lowercase.
Using an environment variable to set the proxy has the same effect as using
the \fI-x, --proxy\fP option.
.IP "http_proxy [protocol://]<host>[:port]"
Sets the proxy server to use for HTTP.
.IP "https_proxy [protocol://]<host>[:port]"
Sets the proxy server to use for HTTPS.
.IP "all_proxy [protocol://]<host>[:port]"
Sets the proxy server to use if no protocol-specific proxy is set.
.IP "no_proxy <comma-separated list of hosts>"
list of host names that shouldn't go through any proxy.
.SH EXIT CODES
.IP "1"
Failed to parse command-line options.
.IP "2"
Input File Parsing Error.
.IP "3"
Runtime error (such as failure to connect to host).
.IP "4"
Assert Error.
.SH WWW
\fIhttps://hurl.dev\fP
.SH SEE ALSO
curl(1) hurlfmt(1)

318
docs/hurl.md Normal file
View File

@ -0,0 +1,318 @@
## 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-520814d64e2f9249ea44e0f0"
},
"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 the HTTP requests and output 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
GET http:/example.net/endpoint1
GET http:/example.net/endpoint2
### Capturing values
A value from an HTTP response can be-reused for successive HTTP requests.
A typical example occurs with csrf tokens.
GET https://example.net
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.net/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.
GET http:/google.com
HTTP/1.1 302
It can also include asserts on the response headers
GET http:/google.com
HTTP/1.1 302
Location: http://www.google.com
You can also include explicit asserts combining query and predicate
GET http:/google.com
HTTP/1.1 302
[Asserts]
xpath "//title" equals "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.
### --append {#append}
This option can only be used with [--json](#json). It appends sessions to existing file instead of overwriting it.
This is typically used in a CI pipeline.
### --color {#color}
Colorize Output
### -b, --cookie <filename> {#cookie}
Read cookies from file (using the Netscape cookie file format).
Combined with [-c, --cookie-jar](#cookie-jar), you can simulate a cookie storage between successive Hurl runs.
### -c, --cookie-jar <filename> {#cookie-jar}
Write cookies to FILE after running the session (only for one session).
The file will be written using the Netscape cookie file format.
Combined with [-b, --cookie](#cookie),you can simulate a cookie storage between successive Hurl runs.
### --fail-at-end {#fail-at-end}
Continue executing requests to the end of the Hurl file even when an assert error occurs.
By default, Hurl exits after an assert error in the HTTP response.
Note that this option does not affect the behavior with mutiple input Hurl files.
All the input files are executed independently. The result of one file does not affect the excecution of the other Hurl files.
### --file-root <dir> {#file-root}
Set root filesystem to import files in Hurl. This is used for both files in multipart form data and request body.
When this is not explicitly defined, the files are relative to the current directory in which Hurl is running.
### -h, --help {#help}
Usage help. This lists all current command line options with a short description.
### --html <dir> {#html}
Generate html report in dir.
If you want to combine results from different Hurl executions in a unique html report, you must also use the options [--json](#json) and [--append](#append).
### -i, --include {#include}
Include the HTTP headers in the output.
### --json <file> {#json}
Write full session(s) to a json file. The format is very closed to HAR format.
By default, this file is overwritten by the current run execution.
In order to append sessions to an existing json file, the option [--append](#append) must be used.
This is typically used in a CI pipeline.
### -k, --insecure {#insecure}
This option explicitly allows Hurl to perform "insecure" SSL connections and transfers.
### -L, --location {#location}
Follow redirect. You can limit the amount of redirects to follow by using the [--max-redirs](#max-redirs) option.
### --max-redirs <num> {#max-redirs}
Set maximum number of redirection-followings allowed
By default, the limit is set to 50 redirections. Set this option to -1 to make it unlimited.
### --no-color {#color}
Do not colorize Output
### --noproxy <no-proxy-list> {#noproxy}
Comma-separated list of hosts which do not use a proxy.
Override value from Environment variable no_proxy.
### --to-entry <entry-number> {#to-entry}
Execute Hurl file to ENTRY_NUMBER (starting at 1).
Ignore the remaining of the file. It is useful for debugging a session.
### -o, --output <file> {#output}
Write output to <file> instead of stdout.
### -x, --proxy [protocol://]host[:port] {#proxy}
Use the specified proxy.
### --variable <name=value> {#variables}
Define variable (name/value) to be used in Hurl templates.
Only string values can be defined.
### -v, --verbose {#verbose}
Turn on verbose output on standard error stream
Useful for debugging.
A line starting with '>' means data sent by Hurl.
A line staring with '<' means data received by Hurl.
A line starting with '*' means additional info provided by Hurl.
If you only want HTTP headers in the output, -i, --include might be the option you're looking for.
### -V, --version {#version}
Prints version information
## 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.
### http_proxy [protocol://]<host>[:port]
Sets the proxy server to use for HTTP.
### https_proxy [protocol://]<host>[:port]
Sets the proxy server to use for HTTPS.
### all_proxy [protocol://]<host>[:port]
Sets the proxy server to use if no protocol-specific proxy is set.
### no_proxy <comma-separated list of hosts>
list of host names that shouldn't go through any proxy.
## EXIT CODES
### 1
Failed to parse command-line options.
### 2
Input File Parsing Error.
### 3
Runtime error (such as failure to connect to host).
### 4
Assert Error.
## WWW
[https://hurl.dev](https://hurl.dev)
## SEE ALSO
curl(1) hurlfmt(1)

66
docs/hurlfmt.1 Normal file
View File

@ -0,0 +1,66 @@
.TH hurl 1 "DATE" "hurl 0.99" " Hurl Manual"
.SH NAME
hurlfmt - format Hurl files
.SH SYNOPSIS
.B hurlfmt
[options] [FILE...]
.SH DESCRIPTION
.B hurlfmt
formats Hurl files.
.SH OPTIONS
.IP "--color "
Colorize Output
.IP "-h, --help "
Usage help. This lists all current command line options with a short description.
.IP "--html "
Generate html output.
.IP "-V, --version "
Prints version information
.SH EXIT CODES
.IP "1"
Failed to parse command-line options.
.IP "2"
Input File Parsing Error.
.SH WWW
\fIhttps://hurl.dev\fP
.SH SEE ALSO
hurl(1)

62
docs/hurlfmt.md Normal file
View File

@ -0,0 +1,62 @@
## NAME
hurlfmt - format Hurl files
## SYNOPSIS
**hurlfmt** [options] [FILE...]
## DESCRIPTION
**hurlfmt** formats Hurl files.
## OPTIONS
### --color {#color}
Colorize Output
### -h, --help {#help}
Usage help. This lists all current command line options with a short description.
### --html {#html}
Generate html output.
### -V, --version {#version}
Prints version information
## EXIT CODES
### 1
Failed to parse command-line options.
### 2
Input File Parsing Error.
## WWW
[https://hurl.dev](https://hurl.dev)
## SEE ALSO
hurl(1)

5
docs/logo.svg Normal file
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 " style="fill: rgba(251, 0, 117, 1.000000); " fill="#ff0187" 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 " style="fill: rgba(251, 0, 117, 1.000000); " fill="#ff0187" 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.2 KiB

6
docs/update.sh Executable file
View File

@ -0,0 +1,6 @@
#!/bin/bash
./gen_manpage.py hurl.md > hurl.1
./gen_manpage.py hurlfmt.md > hurlfmt.1
./gen_doc.py hurl.md > ../../hurl-dev/sites/hurl.dev/_docs/man-page.md

13
integration/generate_html Executable file
View File

@ -0,0 +1,13 @@
#!/bin/bash
set -e
mkdir -p target
find tests -name "*.hurl" | while read -r hurl_file; do
echo "$hurl_file"
html_file=target/"${hurl_file%.*}".html
echo "Generating $html_file"
mkdir -p "$(dirname "$html_file")"
hurlfmt --html --standalone "$hurl_file" > "$html_file"
done

16
integration/hurl_echo Executable file
View File

@ -0,0 +1,16 @@
#!/bin/bash
set -e
for input_file in "$@"; do
echo "$input_file"
output_file="/tmp/$(basename "$input_file")"
hurlfmt --no-format "$input_file" >"$output_file"
expected=$(cat "$input_file")
actual=$(cat "$output_file")
if [ "$actual" != "$expected" ]; then
echo "=> Difference!"
diff "$output_file" "$input_file"
exit &
fi
done

14
integration/hurl_output.sh Executable file
View File

@ -0,0 +1,14 @@
#!/bin/bash
for command_file in "$@"; do
echo "$command_file"
command=$(cat $command_file)
expected=$(cat ${command_file%.*}.output)
output="$($command 2>&1)"
if [ "$output" != "$expected" ]; then
diff <(echo "$output" ) <(echo "$expected")
exit 1
fi
done

18
integration/integration.sh Executable file
View File

@ -0,0 +1,18 @@
#!/bin/bash
set -e
cd "$(dirname "$0")"
# Static Analysis
./hurl_echo tests/*.hurl tests_error_lint/*.hurl
./lint.sh tests_error_lint/*.hurl
./generate_html
# Dynamic
./run.sh tests/*.hurl tests_error_parser/*.hurl
#./hurl_output.sh output/*.command
set +e
rm -rf report/*
hurl --json report/tests.json --html report/html --output /dev/null tests/*.hurl
echo "test integration ok!"

46
integration/lint.sh Executable file
View File

@ -0,0 +1,46 @@
#!/bin/bash
set -u
EXITCODE_EXPECTED=1
for hurl_file in "$@"; do
echo "$hurl_file";
set +e
hurlfmt --color --check "$hurl_file" 2>/tmp/test.stderr
EXITCODE_ACTUAL=$?
set -e
if [ "$EXITCODE_ACTUAL" != "$EXITCODE_EXPECTED" ]; then
echo "ERROR Exit Code"
echo " Expected: $EXITCODE_EXPECTED"
echo " Actual: $EXITCODE_ACTUAL"
exit 1
fi
STDERR_ACTUAL=$(cat /tmp/test.stderr)
STDERR_EXPECTED=$(cat ${hurl_file%%.*}.err)
diff ${hurl_file%%.*}.err /tmp/test.stderr
if [ "$STDERR_ACTUAL" != "$STDERR_EXPECTED" ]; then
echo "ERROR stderr"
echo " expected:"
echo "$STDERR_EXPECTED" | perl -pe 'chomp;s/.*/ $_\n/'
echo " actual:"
echo "$STDERR_ACTUAL" | perl -pe 'chomp;s/.*/ $_\n/'
exit 1
fi
hurlfmt "$hurl_file" --no-color >/tmp/test.lint
LINT_ACTUAL=$(cat /tmp/test.lint)
LINT_EXPECTED=$(cat ${hurl_file%%.*}.hurl.lint)
if [ "$LINT_ACTUAL" != "$LINT_EXPECTED" ]; then
echo "ERROR linting"
echo " expected:"
echo "$LINT_EXPECTED" | perl -pe 'chomp;s/.*/ $_\n/'
echo " actual:"
echo "$LINT_ACTUAL" | perl -pe 'chomp;s/.*/ $_\n/'
exit 1
fi
done

View File

@ -0,0 +1 @@
hurl tests/output.hurl

View File

@ -0,0 +1 @@
Response endpoint2

View File

@ -0,0 +1 @@
hurl --include tests/output.hurl

View File

@ -0,0 +1,7 @@
HTTP/1.0 200
date: DATE2
content-type: text/html; charset=utf-8
content-length: 19
server: Werkzeug/0.16.1 Python/3.5.3
Response endpoint2

View File

@ -0,0 +1 @@
hurl --verbose tests/output.hurl

View File

@ -0,0 +1,33 @@
* Fail fast: true
* variables: {}
* ------------------------------------------------------------------------------
* executing entry 1
> POST http://localhost:8000/output/endpoint1
> Content-Type: application/json
> User-Agent: hurl/0.99.8
> Host: localhost
>
> { "user": "bob" }
< HTTP/1.0 200
< date: DATE1
< content-type: text/html; charset=utf-8
< content-length: 19
< server: Werkzeug/0.16.1 Python/3.5.3
<
< Response endpoint1
<
* ------------------------------------------------------------------------------
* executing entry 2
> GET http://localhost:8000/output/endpoint2
> User-Agent: hurl/0.99.8
> Host: localhost
>
< HTTP/1.0 200
< date: DATE2
< content-type: text/html; charset=utf-8
< content-length: 19
< server: Werkzeug/0.16.1 Python/3.5.3
<
< Response endpoint2
<
Response endpoint2

View File

@ -0,0 +1,2 @@
<!DOCTYPE html>
<html><head><title>Hurl Report</title><link rel="stylesheet" type="text/css" href="report.css"></head><body><h2>Hurl Report</h2><div class="date">Wed, 26 Aug 2020 11:50:20 +0200</div><table><thead><tr><td>filename</td><td>duration</td></tr></thead><tbody><tr><td class="success">tests/assert_base64.hurl</td><td>0.019s</td></tr><tr><td class="success">tests/assert_header.hurl</td><td>0.023s</td></tr><tr><td class="success">tests/assert_json.hurl</td><td>0.073s</td></tr><tr><td class="success">tests/assert_match.hurl</td><td>0.049s</td></tr><tr><td class="success">tests/assert_regex.hurl</td><td>0.025s</td></tr><tr><td class="success">tests/assert_xpath.hurl</td><td>0.023s</td></tr><tr><td class="success">tests/bytes.hurl</td><td>0.02s</td></tr><tr><td class="success">tests/capture_and_assert.hurl</td><td>0.022s</td></tr><tr><td class="success">tests/captures.hurl</td><td>0.078s</td></tr><tr><td class="success">tests/cookies.hurl</td><td>0.256s</td></tr><tr><td class="success">tests/delete.hurl</td><td>0.033s</td></tr><tr><td class="success">tests/empty.hurl</td><td>0s</td></tr><tr><td class="success">tests/encoding.hurl</td><td>0.059s</td></tr><tr><td class="failure">tests/error_assert_base64.hurl</td><td>0.052s</td></tr><tr><td class="failure">tests/error_assert_file.hurl</td><td>0.025s</td></tr><tr><td class="failure">tests/error_assert_header_not_found.hurl</td><td>0.028s</td></tr><tr><td class="failure">tests/error_assert_header_value.hurl</td><td>0.025s</td></tr><tr><td class="failure">tests/error_assert_http_version.hurl</td><td>0.025s</td></tr><tr><td class="failure">tests/error_assert_invalid_predicate_type.hurl</td><td>0.02s</td></tr><tr><td class="failure">tests/error_assert_match_utf8.hurl</td><td>0.022s</td></tr><tr><td class="failure">tests/error_assert_query_invalid_regex.hurl</td><td>0.018s</td></tr><tr><td class="failure">tests/error_assert_query_invalid_xpath.hurl</td><td>0.021s</td></tr><tr><td class="failure">tests/error_assert_status.hurl</td><td>0.019s</td></tr><tr><td class="failure">tests/error_assert_template_variable_not_found.hurl</td><td>0.027s</td></tr><tr><td class="failure">tests/error_assert_value_error.hurl</td><td>0.03s</td></tr><tr><td class="failure">tests/error_assert_variable.hurl</td><td>0.025s</td></tr><tr><td class="failure">tests/error_file_read_access.hurl</td><td>0s</td></tr><tr><td class="failure">tests/error_invalid_jsonpath.hurl</td><td>0.043s</td></tr><tr><td class="failure">tests/error_invalid_url.hurl</td><td>0s</td></tr><tr><td class="failure">tests/error_invalid_xml.hurl</td><td>0.036s</td></tr><tr><td class="failure">tests/error_multipart_form_data.hurl</td><td>0s</td></tr><tr><td class="failure">tests/error_predicate.hurl</td><td>0.06s</td></tr><tr><td class="failure">tests/error_query_header_not_found.hurl</td><td>0.033s</td></tr><tr><td class="failure">tests/error_query_invalid_json.hurl</td><td>0.022s</td></tr><tr><td class="failure">tests/error_query_invalid_utf8.hurl</td><td>0.024s</td></tr><tr><td class="failure">tests/error_template_variable_not_found.hurl</td><td>0s</td></tr><tr><td class="failure">tests/error_template_variable_not_renderable.hurl</td><td>0.021s</td></tr><tr><td class="success">tests/form_params.hurl</td><td>0.045s</td></tr><tr><td class="success">tests/headers.hurl</td><td>0.14s</td></tr><tr><td class="success">tests/hello.hurl</td><td>0.046s</td></tr><tr><td class="success">tests/multipart_form_data.hurl</td><td>0.024s</td></tr><tr><td class="success">tests/no_entry.hurl</td><td>0s</td></tr><tr><td class="success">tests/output.hurl</td><td>0.052s</td></tr><tr><td class="success">tests/patch.hurl</td><td>0.023s</td></tr><tr><td class="success">tests/post_base64.hurl</td><td>0.027s</td></tr><tr><td class="success">tests/post_file.hurl</td><td>0.02s</td></tr><tr><td class="success">tests/post_json.hurl</td><td>0.294s</td></tr><tr><td class="success">tests/post_multilines.hurl</td><td>0.078s</td></tr><tr><td class="success">tests/post_xml.hurl</td><td>0.068s</td></tr><tr><td class="success">tests/predicates-string.hurl</td><td>0.045s</td></tr><tr><td class="success">tests/put.hurl</td><td>0.024s</td></tr><tr><td class="success">tests/querystring_params.hurl</td><td>0.12s</td></tr><tr><td class="success">tests/redirect.hurl</td><td>0.062s</td></tr><tr><td class="success">tests/utf8.hurl</td><td>0.035s</td></tr></tbody></table></body></html>

View File

@ -0,0 +1,20 @@
.date {
margin-bottom: 10px;
}
thead {
font-weight: bolder;
}
.success {
color: green;
}
.failure {
color: red;
}

File diff suppressed because it is too large Load Diff

51
integration/run.sh Executable file
View File

@ -0,0 +1,51 @@
#!/bin/bash
set -u
set -e
for hurl_file in "$@"; do
echo "$hurl_file";
set +e
hurl "$hurl_file" --color 2>/tmp/test.stderr >/tmp/test.stdout
EXITCODE_ACTUAL=$?
set -e
EXITCODE_EXPECTED=$(cat "${hurl_file%.*}.exit")
if [ "$EXITCODE_ACTUAL" != "$EXITCODE_EXPECTED" ]; then
echo "ERROR Exit Code"
echo " Expected: $EXITCODE_EXPECTED"
echo " Actual: $EXITCODE_ACTUAL"
# log unexpected error
if [ "$EXITCODE_ACTUAL" != 0 ]; then
cat /tmp/test.stderr
fi
exit 1
fi
if [ "$EXITCODE_ACTUAL" == 0 ]; then
expected=$(cat "${hurl_file%.*}.out")
actual=$(cat /tmp/test.stdout)
if [ "$actual" != "$expected" ]; then
diff <(echo "$actual" ) <(echo "$expected")
exit 1
fi
else
STDERR_EXPECTED=$(cat "${hurl_file%.*}.err")
STDERR_ACTUAL=$(cat /tmp/test.stderr)
if [ "$STDERR_ACTUAL" != "$STDERR_EXPECTED" ]; then
echo "ERROR StandardError"
echo " Expected: $STDERR_EXPECTED"
echo " Actual: $STDERR_ACTUAL"
exit 1
fi
fi
done

4
integration/server.py Normal file
View File

@ -0,0 +1,4 @@
from tests import app
if __name__ == '__main__':
app.run(debug=True, host='0.0.0.0', port=8000)

View File

@ -0,0 +1,18 @@
from flask import Flask
import glob
import importlib
import os
app = Flask(__name__)
current_dir = os.path.basename(os.path.dirname(__file__))
for python_file in glob.glob(current_dir + '/*.py'):
if python_file.endswith('__init__.py'):
continue
module_name = python_file.split('.')[0].replace('/', '.')
print('loading %s' % module_name)
try:
importlib.import_module(module_name)
except ImportError as err:
print('Error:', err)

View File

@ -0,0 +1 @@
0

View File

@ -0,0 +1,11 @@
# Test body response with line ending LF and CRLF.
# We receive the text body "line1\nline2\r\nline3\n"
#
# $ printf "line1\nline2\r\nline3\n" | base64
# bGluZTEKbGluZTINCmxpbmUzCg==
GET http://localhost:8000/assert-base64
HTTP/1.0 200
base64,bGluZTEKbGluZTINCmxpbmUzCg==;

View File

@ -0,0 +1,3 @@
line1
line2
line3

View File

@ -0,0 +1,5 @@
from tests import app
@app.route("/assert-base64")
def assert_base64():
return 'line1\nline2\r\nline3\n'

View File

@ -0,0 +1,2 @@
0

View File

@ -0,0 +1,13 @@
GET http://localhost:8000/assert-header
HTTP/1.0 200
Content-Type: text/html; charset=utf-8
Set-Cookie: cookie1=value1; Path=/
Set-Cookie: cookie2=value2; Path=/
[Asserts]
header "Custom" not exists
header "Content-Type" exists
header "Header1" equals "value1"
header "Set-Cookie" exists
header "Set-Cookie" countEquals 3
header "Set-Cookie" includes "cookie1=value1; Path=/"
header "Set-Cookie" not includes "cookie4=value4; Path=/"

View File

View File

@ -0,0 +1,13 @@
from tests import app
from flask import make_response
@app.route("/assert-header")
def assert_header():
resp = make_response()
resp.headers['Header1'] = 'value1'
resp.set_cookie('cookie1', 'value1')
resp.set_cookie('cookie2', 'value2')
resp.set_cookie('cookie3', 'value3')
return resp

View File

@ -0,0 +1 @@
0

View File

@ -0,0 +1,38 @@
GET http://localhost:8000/assert-json
HTTP/1.0 200
[Asserts]
jsonpath "$.success" equals false
jsonpath "$.success" equals false
jsonpath "$.errors" countEquals 2
jsonpath "$.warnings" countEquals 0
jsonpath "$.toto" not exists
jsonpath "$.warnings" exists
jsonpath "$.warnings" exists
jsonpath "$.errors[0]" exists
jsonpath "$.errors[0].id" equals "error1"
jsonpath "$.errors[0]['id']" equals "error1"
jsonpath "$.duration" equals 1.5
jsonpath "$.nullable" equals null
{
"success": false,
"errors": [{"id":"error1"},{"id":"error2"}],
"warnings": [],
"duration": 1.5,
"tags": ["test"],
"nullable": null
}
GET http://localhost:8000/assert-json/index
HTTP/* 200
[Captures]
index: body
GET http://localhost:8000/assert-json
HTTP/1.0 200
[Asserts]
jsonpath "$.errors[{{index}}].id" equals "error2"
jsonpath "$.tags" includes "test"
jsonpath "$.tags" not includes "prod"
jsonpath "$.tags" not includes null

View File

@ -0,0 +1,8 @@
{
"success": false,
"errors": [{"id":"error1"},{"id":"error2"}],
"warnings": [],
"duration": 1.5,
"tags": ["test"],
"nullable": null
}

View File

@ -0,0 +1,18 @@
from tests import app
from flask import Response
@app.route("/assert-json")
def assert_json():
return Response('''{
"success": false,
"errors": [{"id":"error1"},{"id":"error2"}],
"warnings": [],
"duration": 1.5,
"tags": ["test"],
"nullable": null
}''', mimetype='application/json')
@app.route("/assert-json/index")
def assert_json_index():
return "1"

View File

@ -0,0 +1 @@
0

View File

@ -0,0 +1,7 @@
GET http://localhost:8000/assert-match
HTTP/1.0 200
[Asserts]
jsonpath "$.date1" matches "\\d{4}-\\d{2}-\\d{2}"
jsonpath "$.date2" matches "\\d{4}-\\d{2}-\\d{2}"
jsonpath "$.date1" matches "^\\d{4}-\\d{2}-\\d{2}$"
jsonpath "$.date2" not matches "^\\d{4}-\\d{2}-\\d{2}$"

View File

@ -0,0 +1,4 @@
{
"date1": "2014-01-01",
"date2": "x2014-01-01"
}

View File

@ -0,0 +1,11 @@
from tests import app
from flask import Response
@app.route("/assert-match")
def assert_match():
return Response('''{
"date1": "2014-01-01",
"date2": "x2014-01-01"
}''', mimetype='application/json')

View File

@ -0,0 +1 @@
0

View File

@ -0,0 +1,6 @@
GET http://localhost:8000/assert-regex
HTTP/1.0 200
[Asserts]
regex "Hello ([0-9]+)!" not exists
regex "Hello ([a-zA-Z]+)!" equals "World"

View File

@ -0,0 +1 @@
Hello World!

View File

@ -0,0 +1,8 @@
# coding=utf-8
from tests import app
@app.route("/assert-regex")
def assert_regex():
return 'Hello World!'

View File

@ -0,0 +1 @@
0

View File

@ -0,0 +1,9 @@
GET http://localhost:8000/assert-xpath
HTTP/1.0 200
[Asserts]
xpath "normalize-space(//data)" equals "café"
xpath "normalize-space(//data)" equals "caf\u{00e9}"
xpath "//toto" not exists
<data>café</data>

View File

@ -0,0 +1 @@
<data>café</data>

View File

@ -0,0 +1,8 @@
# coding=utf-8
from tests import app
@app.route("/assert-xpath")
def assert_xpath():
return '<data>café</data>'

View File

@ -0,0 +1,4 @@
0

View File

@ -0,0 +1,9 @@
GET http://localhost:8000/bytes
HTTP/1.0 200
Content-Type: application/octet-stream
[Asserts]
#TODO create a bytes query to get e byte array
#body countEquals 1

View File

@ -0,0 +1 @@
<EFBFBD>

View File

@ -0,0 +1,16 @@
from tests import app
from flask import make_response, request
from io import BytesIO
@app.route('/bytes')
def bytes():
result = BytesIO()
result.write(b'\xff')
data = result.getvalue()
resp = make_response(data)
resp.content_type = 'application/octet-stream'
return resp

View File

@ -0,0 +1 @@
0

View File

@ -0,0 +1,8 @@
GET http://localhost:8000/capture-and-assert
HTTP/1.0 200
[Captures]
content_type: header "content-type"
[Asserts]
header "content-type" equals "{{content_type}}"

View File

@ -0,0 +1 @@
Hello World!

View File

@ -0,0 +1,7 @@
from tests import app
@app.route("/capture-and-assert")
def capture_and_assert():
return 'Hello World!'

View File

@ -0,0 +1 @@
0

View File

@ -0,0 +1,40 @@
GET http://localhost:8000/captures
HTTP/1.0 200
[Captures]
param1: header "header1"
param2: header "header2" regex "Hello (.*)!"
[Asserts]
variable "param1" equals "value1"
variable "param2" equals "Bob"
GET http://localhost:8000/captures-check
[QueryStringParams]
param1: {{param1}}
param2: {{param2}}
HTTP/1.0 200
GET http://localhost:8000/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 "$"
[Asserts]
variable "a_null" exists
variable "undefined" not exists
variable "a_null" equals {{a_null}}
variable "an_integer" equals {{an_integer}}
variable "a_float" equals {{a_float}}
variable "a_bool" equals {{a_bool}}
variable "a_string" equals {{a_string}}
variable "a_list" equals {{a_list}}

View File

@ -0,0 +1 @@
{ "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" }

View File

@ -0,0 +1,23 @@
from tests import app
from flask import make_response, request
@app.route('/captures')
def captures():
resp = make_response()
resp.headers['Header1'] = 'value1'
resp.headers['Header2'] = 'Hello Bob!'
return resp
@app.route('/captures-check')
def captures_check():
assert request.args.get('param1') == 'value1'
assert request.args.get('param2') == 'Bob'
return ''
@app.route('/captures-json')
def captures_json():
return '{ "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" }'

View File

@ -0,0 +1 @@
0

View File

@ -0,0 +1,74 @@
# Request Cookie
GET http://localhost:8000/cookies/set-request-cookie1-valueA
[Cookies]
cookie1: valueA
HTTP/1.0 200
# In the future we want a dedicated directive to set the session cookie on the client
# but for the time-being, sending a request cookie will update the cookiejar
#GET http://localhost:8000/cookies/assert-that-cookie1-is-not-in-session
#HTTP/1.0 200
# Session Cookie
GET http://localhost:8000/cookies/set-session-cookie2-valueA
HTTP/1.0 200
[Asserts]
cookie "cookie2" equals "valueA"
GET http://localhost:8000/cookies/assert-that-cookie2-is-valueA
HTTP/1.0 200
GET http://127.0.0.1:8000/cookies/assert-that-cookie2-is-not-in-session
HTTP/1.0 200
GET http://localhost:8000/cookies/set-request-cookie2-valueB
[Cookies]
cookie2: valueB
HTTP/1.0 200
# In the future we want a dedicated directive to set the session cookie on the client
# but for the time-being, sending a request cookie will update the cookiejar
#GET http://localhost:8000/cookies/assert-that-cookie2-is-valueA
GET http://localhost:8000/cookies/assert-that-cookie2-is-valueB
HTTP/1.0 200
GET http://localhost:8000/cookies/delete-cookie2
HTTP/1.0 200
[Asserts]
header "Set-Cookie" contains "Max-Age=0" # TODO replace by cookie query
#GET http://localhost:8000/cookies/assert-that-cookie2-is-not-in-session
#HTTP/1.0 200
GET http://localhost:8000/cookies/set
HTTP/1.0 200
Set-Cookie: LSID=DQAAAKEaem_vYg; Expires=Wed, 13 Jan 2021 22:23:01 GMT; Secure; HttpOnly; Path=/accounts
Set-Cookie: HSID=AYQEVnDKrdst; Domain=.foo.com; Expires=Wed, 13 Jan 2021 22:23:01 GMT; HttpOnly; Path=/
Set-Cookie: SSID=Ap4PGTEq; Domain=.foo.com; Expires=Wed, 13 Jan 2021 22:23:01 GMT; Secure; HttpOnly; Path=/
[Asserts]
header "Set-Cookie" countEquals 3
cookie "LSID" equals "DQAAAKEaem_vYg"
cookie "LSID[Value]" equals "DQAAAKEaem_vYg"
cookie "LSID[Expires]" exists
cookie "LSID[Expires]" equals "Wed, 13 Jan 2021 22:23:01 GMT"
cookie "LSID[Max-Age]" not exists
cookie "LSID[Domain]" not exists
cookie "LSID[Path]" equals "/accounts"
cookie "LSID[Secure]" equals true
cookie "LSID[HttpOnly]" equals true
cookie "LSID[SameSite]" not exists

View File

View File

@ -0,0 +1,92 @@
from flask import request, make_response
from tests import app
@app.route("/cookies/set-request-cookie1-valueA")
def set_request_cookie1_value1():
assert request.cookies['cookie1'] == 'valueA'
return ''
@app.route("/cookies/set-session-cookie2-valueA")
def set_session_cookie2_valuea():
resp = make_response()
resp.set_cookie('cookie2', 'valueA')
return resp
@app.route("/cookies/set-request-cookie2-valueB")
def set_request_cookie2_valueb():
assert request.cookies['cookie2'] == 'valueB'
return ''
@app.route("/cookies/send-cookie2-value1")
def send_cookie2_value1():
assert'cookie1' not in request.cookies
assert request.cookies['cookie2'] == 'value1'
return ''
@app.route("/cookies/send-cookie2-value2")
def send_cookie2_value2():
assert request.cookies['cookie2'] == 'value2'
return ''
@app.route("/cookies/delete-cookie2")
def delete_cookie2():
resp = make_response()
resp.set_cookie('cookie2', '', max_age=0)
return resp
@app.route("/cookies/assert-that-cookie1-is-not-in-session")
def assert_that_cookie1_is_not_in_session():
assert'cookie1' not in request.cookies
return ''
@app.route("/cookies/assert-that-cookie2-is-not-in-session")
def assert_that_cookie2_is_not_in_session():
assert'cookie2' not in request.cookies
return ''
@app.route("/cookies/assert-that-cookie2-is-valueA")
def assert_that_cookie2_is_valuea():
assert request.cookies['cookie2'] == 'valueA'
return ''
@app.route("/cookies/assert-that-cookie2-is-valueB")
def assert_that_cookie2_is_valueb():
assert request.cookies['cookie2'] == 'valueB'
return ''
@app.route("/cookies/set-session-cookie2-valueA-subdomain")
def set_session_cookie2_valuea_subdomain():
resp = make_response()
resp.set_cookie('cookie2', 'valueA', domain='myshop.orange.localhost')
return resp
@app.route("/cookies/set-session-cookie2-valueA-subdomain2")
def set_session_cookie2_valuea_subdomain2():
resp = make_response()
resp.set_cookie('cookie2', 'valueA', domain='orange.localhost')
return resp
# Set-Cookie: LSID=; Path=/accounts; Expires=Wed, 13 Jan 2021 22:23:01 GMT; Secure; HttpOnly
# Set-Cookie: HSID=AYQEVn…DKrdst; Domain=.foo.com; Path=/; Expires=Wed, 13 Jan 2021 22:23:01 GMT; HttpOnly
# Set-Cookie: SSID=Ap4P…GTEq; Domain=foo.com; Path=/; Expires=Wed, 13 Jan 2021 22:23:01 GMT; Secure; HttpOnly
@app.route("/cookies/set")
def set_cookies():
resp = make_response()
resp.set_cookie('LSID', 'DQAAAKEaem_vYg', path='/accounts', secure=True, httponly=True, expires='Wed, 13 Jan 2021 22:23:01 GMT')
resp.set_cookie('HSID', 'AYQEVnDKrdst', domain='.foo.com', path='/', expires='Wed, 13 Jan 2021 22:23:01 GMT', httponly=True)
resp.set_cookie('SSID', 'Ap4PGTEq',domain='.foo.com', path='/', expires='Wed, 13 Jan 2021 22:23:01 GMT', secure=True, httponly=True)
return resp

View File

@ -0,0 +1 @@
Hello World!

View File

@ -0,0 +1 @@
0

View File

@ -0,0 +1,3 @@
DELETE http://localhost:8000/delete
HTTP/1.0 200

View File

View File

@ -0,0 +1,7 @@
from tests import app
@app.route('/delete', methods=['DELETE'])
def delete():
return ''

View File

@ -0,0 +1 @@
0

View File

View File

View File

@ -0,0 +1 @@
0

View File

@ -0,0 +1,14 @@
GET http://localhost:8000/encoding/utf8
HTTP/1.0 200
Content-Type: text/html; charset=utf-8
[Asserts]
body equals "caf\u{e9}"
GET http://localhost:8000/encoding/latin1
HTTP/1.0 200
Content-Type: text/html; charset=ISO-8859-1
[Asserts]
body equals "caf\u{e9}"

View File

@ -0,0 +1 @@
caf<EFBFBD>

View File

@ -0,0 +1,22 @@
from flask import request, make_response
from tests import app
from io import BytesIO
@app.route("/encoding/utf8")
def encoding_utf8():
return 'café'
@app.route("/encoding/latin1")
def encoding_latin1():
result = BytesIO()
result.write(b'\x63\x61\x66\xe9')
data = result.getvalue()
resp = make_response(data)
resp.content_type = 'text/html; charset=ISO-8859-1'
return resp

View File

@ -0,0 +1,7 @@
error: Assert Body Value
--> tests/error_assert_base64.hurl:12:8
|
12 | base64,bGluZTEKbGluZTIKbGluZTMK;
| ^^^^^^^^^^^^^^^^^^^^^^^^ actual value is <Bytes([6c, 69, 6e, 65, 31, a, 6c, 69, 6e, 65, 32, d, a, 6c, 69, 6e, 65, 33, a])>
|

View File

@ -0,0 +1 @@
4

View File

@ -0,0 +1,13 @@
# Test body response with line ending LF and CRLF.
# We receive the text body "line1\nline2\r\nline3\n"
# and not "line1\nline2\nline3\n"
#
# $ printf "line1\nline2\nline3\n" | base64
# bGluZTEKbGluZTIKbGluZTMK
GET http://localhost:8000/assert-base64
HTTP/1.0 200
base64,bGluZTEKbGluZTIKbGluZTMK;

View File

@ -0,0 +1,5 @@
from tests import app
@app.route("/error-assert-base64")
def error_assert_base64():
return 'line1\nline2\r\nline3\n'

View File

@ -0,0 +1,7 @@
error: Assert Body Value
--> tests/error_assert_file.hurl:8:1
|
8 | file,hello.txt;
| ^ actual value is <Bytes([48, 65, 6c, 6c, 6f])>
|

View File

@ -0,0 +1 @@
4

View File

@ -0,0 +1,9 @@
# Test body response with file assertion.
# We receive the body "Hello" and not "Hello World!"
GET http://localhost:8000/error-assert-file
HTTP/1.0 200
file,hello.txt;

View File

@ -0,0 +1,5 @@
from tests import app
@app.route("/error-assert-file")
def error_assert_file():
return 'Hello'

View File

@ -0,0 +1,7 @@
error: Header not Found
--> tests/error_assert_header_not_found.hurl:3:1
|
3 | Custom: ???
| ^^^^^^ This header has not been found in the response
|

View File

@ -0,0 +1 @@
4

View File

@ -0,0 +1,3 @@
GET http://localhost:8000/error-assert-header-not-found
HTTP/1.0 200
Custom: ???

Some files were not shown because too many files have changed in this diff Show More