diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 6528db4..4f08560 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -85,7 +85,7 @@ jobs:
php-version: ${{ matrix.php }}
- run: composer install -d tests/integration/
- run: php tests/integration/public/index.php &
- - run: bash tests/await.sh
+ - run: bash tests/await.bash
- run: bash tests/integration.bash
Docker:
@@ -104,7 +104,7 @@ jobs:
- run: composer install -d tests/integration/
- run: docker build -f tests/integration/${{ matrix.dockerfile }} tests/integration/
- run: docker run -d -p 8080:8080 -v "$PWD/composer.json":/app/composer.json $(docker images -q | head -n1)
- - run: bash tests/await.sh
+ - run: bash tests/await.bash
- run: bash tests/integration.bash
- run: docker stop $(docker ps -qn1)
- run: docker logs $(docker ps -qn1)
@@ -129,8 +129,8 @@ jobs:
- run: docker build -f tests/integration/Dockerfile-basics tests/integration/
- run: docker run -d -p 8080:8080 -v "$PWD/composer.json":/app/composer.json $(docker images -q | head -n1)
- run: docker run -d --net=host -v "$PWD/tests/integration/":/home/framework-x/ -v "$PWD"/tests/integration/${{ matrix.config.path }}:/etc/nginx/conf.d/default.conf nginx:stable-alpine
- - run: bash tests/await.sh http://localhost
- - run: bash tests/integration.bash http://localhost
+ - run: bash tests/await.bash http://localhost/
+ - run: bash tests/integration.bash http://localhost/
- run: docker stop $(docker ps -qn2)
- run: docker logs $(docker ps -qn1)
if: ${{ always() }}
@@ -159,8 +159,8 @@ jobs:
- run: composer install -d tests/integration/
- run: docker run -d -v "$PWD/tests/integration/":/home/framework-x/ php:${{ matrix.php }}-fpm
- run: docker run -d -p 80:80 --link $(docker ps -qn1):php -v "$PWD/tests/integration/":/home/framework-x/ -v "$PWD"/tests/integration/nginx-fpm.conf:/etc/nginx/conf.d/default.conf nginx:stable-alpine
- - run: bash tests/await.sh http://localhost
- - run: bash tests/integration.bash http://localhost
+ - run: bash tests/await.bash http://localhost/
+ - run: bash tests/integration.bash http://localhost/
- run: docker logs $(docker ps -qn1)
if: ${{ always() }}
@@ -185,8 +185,8 @@ jobs:
php-version: ${{ matrix.php }}
- run: composer install -d tests/integration/
- run: docker run -d -p 80:80 -v "$PWD/tests/integration/":/home/framework-x/ php:${{ matrix.php }}-apache sh -c "rmdir /var/www/html;ln -s /home/framework-x/public /var/www/html;ln -s /etc/apache2/mods-available/rewrite.load /etc/apache2/mods-enabled; apache2-foreground"
- - run: bash tests/await.sh http://localhost
- - run: bash tests/integration.bash http://localhost
+ - run: bash tests/await.bash http://localhost/
+ - run: bash tests/integration.bash http://localhost/
- run: docker logs $(docker ps -qn1)
if: ${{ always() }}
@@ -211,5 +211,5 @@ jobs:
php-version: ${{ matrix.php }}
- run: composer install -d tests/integration/
- run: php -S localhost:8080 tests/integration/public/index.php &
- - run: bash tests/await.sh
+ - run: bash tests/await.bash
- run: bash tests/integration.bash
diff --git a/README.md b/README.md
index 78a2686..853285f 100644
--- a/README.md
+++ b/README.md
@@ -136,7 +136,7 @@ your installation like this:
```bash
$ php tests/integration/public/index.php
-$ tests/integration.bash http://localhost:8080
+$ tests/integration.bash http://localhost:8080/
```
## License
diff --git a/tests/await.sh b/tests/await.bash
similarity index 64%
rename from tests/await.sh
rename to tests/await.bash
index 442abf6..5e7812a 100755
--- a/tests/await.sh
+++ b/tests/await.bash
@@ -1,8 +1,9 @@
#!/bin/bash
-base=${1:-http://localhost:8080}
+base=${1:-http://localhost:8080/}
+base=${base%/}
-for i in {1..20}
+for i in {1..600}
do
out=$(curl -v -X PROBE $base/ 2>&1) && exit 0 || echo -n .
sleep 0.1
diff --git a/tests/integration.bash b/tests/integration.bash
index ca8b1d9..c2800c6 100755
--- a/tests/integration.bash
+++ b/tests/integration.bash
@@ -1,143 +1,492 @@
#!/bin/bash
-base=${1:-http://localhost:8080}
+base=${1:-http://localhost:8080/}
+base=${base%/}
baseWithPort=$(php -r 'echo parse_url($argv[1],PHP_URL_PORT) ? $argv[1] : $argv[1] . ":80";' "$base")
n=0
+skipping=false
+curl() {
+ skipping=false
+ out=$($(which curl) "$@" 2>&1);
+}
match() {
+ [[ $skipping == true ]] && return 0
n=$[$n+1]
echo "$out" | grep "$@" >/dev/null && echo -n . || \
(echo ""; echo "Error in test $n: Unable to \"grep $@\" this output:"; echo "$out"; exit 1) || exit 1
}
notmatch() {
+ [[ $skipping == true ]] && return 0
n=$[$n+1]
echo "$out" | grep "$@" >/dev/null && \
(echo ""; echo "Error in test $n: Expected to NOT \"grep $@\" this output:"; echo "$out") && exit 1 || echo -n .
}
skipif() {
- echo "$out" | grep "$@" >/dev/null && echo -n S && return 1 || return 0
+ echo "$out" | grep "$@" >/dev/null && echo -n S && skipping=true || return 0
}
skipifnot() {
- echo "$out" | grep "$@" >/dev/null && return 0 || echo -n S && return 1
+ echo "$out" | grep "$@" >/dev/null && return 0 || echo -n S && skipping=true
}
-out=$(curl -v $base/ 2>&1); match "HTTP/.* 200" && match -iP "Content-Type: text/plain; charset=utf-8[\r\n]"
-out=$(curl -v $base/ 2>&1 -X POST); match "HTTP/.* 405" && match -iP "Content-Type: text/html; charset=utf-8[\r\n]"
-
-out=$(curl -v $base/unknown 2>&1); match "HTTP/.* 404" && match -iP "Content-Type: text/html; charset=utf-8[\r\n]"
-out=$(curl -v $base/index.php 2>&1); match "HTTP/.* 404" && match -iP "Content-Type: text/html; charset=utf-8[\r\n]"
-out=$(curl -v $base/.htaccess 2>&1); match "HTTP/.* 404" && match -iP "Content-Type: text/html; charset=utf-8[\r\n]"
-out=$(curl -v $base// 2>&1); match "HTTP/.* 404" && match -iP "Content-Type: text/html; charset=utf-8[\r\n]"
-
-out=$(curl -v $base/error 2>&1); match "HTTP/.* 500" && match -iP "Content-Type: text/html; charset=utf-8[\r\n]" && match "Unable to load error"
-out=$(curl -v $base/error/null 2>&1); match "HTTP/.* 500" && match -iP "Content-Type: text/html; charset=utf-8[\r\n]"
-
-out=$(curl -v $base/sleep/fiber 2>&1); match "HTTP/.* 200" && match -iP "Content-Type: text/plain; charset=utf-8[\r\n]"
-out=$(curl -v $base/sleep/coroutine 2>&1); match "HTTP/.* 200" && match -iP "Content-Type: text/plain; charset=utf-8[\r\n]"
-out=$(curl -v $base/sleep/promise 2>&1); match "HTTP/.* 200" && match -iP "Content-Type: text/plain; charset=utf-8[\r\n]"
-
-out=$(curl -v $base/uri 2>&1); match "HTTP/.* 200" && match "$base/uri"
-out=$(curl -v $base/uri/ 2>&1); match "HTTP/.* 200" && match "$base/uri/"
-out=$(curl -v $base/uri/foo 2>&1); match "HTTP/.* 200" && match "$base/uri/foo"
-out=$(curl -v $base/uri/foo/bar 2>&1); match "HTTP/.* 200" && match "$base/uri/foo/bar"
-out=$(curl -v $base/uri/foo//bar 2>&1); match "HTTP/.* 200" && match "$base/uri/foo//bar"
-out=$(curl -v $base/uri/Wham! 2>&1); match "HTTP/.* 200" && match "$base/uri/Wham!"
-out=$(curl -v $base/uri/Wham%21 2>&1); match "HTTP/.* 200" && match "$base/uri/Wham%21"
-out=$(curl -v $base/uri/AC%2FDC 2>&1); skipif "HTTP/.* 404" && match "HTTP/.* 200" && match "$base/uri/AC%2FDC" # skip Apache (404 unless `AllowEncodedSlashes NoDecode`)
-out=$(curl -v $base/uri/bin%00ary 2>&1); skipif "HTTP/.* 40[04]" && match "HTTP/.* 200" && match "$base/uri/bin%00ary" # skip nginx (400) and Apache (404)
-out=$(curl -v $base/uri/AC/DC 2>&1); match "HTTP/.* 200" && match "$base/uri/AC/DC"
-out=$(curl -v $base/uri/http://example.com:8080/ 2>&1); match "HTTP/.* 200" && match "$base/uri/http://example.com:8080/"
-out=$(curl -v $base/uri? 2>&1); match "HTTP/.* 200" && match "$base/uri" # trailing "?" not reported for empty query string
-out=$(curl -v $base/uri?query 2>&1); match "HTTP/.* 200" && match "$base/uri?query"
-out=$(curl -v $base/uri?q=a 2>&1); match "HTTP/.* 200" && match "$base/uri?q=a"
-out=$(curl -v $base/uri?q=a! 2>&1); match "HTTP/.* 200" && match "$base/uri?q=a!"
-out=$(curl -v $base/uri?q=a%21 2>&1); match "HTTP/.* 200" && match "$base/uri?q=a%21"
-out=$(curl -v $base/uri?q=w%C3%B6rd 2>&1); match "HTTP/.* 200" && match "$base/uri?q=w%C3%B6rd"
-out=$(curl -v $base/uri?q=+ 2>&1); match "HTTP/.* 200" && match "$base/uri?q=+"
-out=$(curl -v $base/uri?q=%20 2>&1); match "HTTP/.* 200" && match "$base/uri?q=%20"
-out=$(curl -v $base/uri?q=a%2Fb 2>&1); match "HTTP/.* 200" && match "$base/uri?q=a%2Fb"
-out=$(curl -v $base/uri?q=a%00b 2>&1); match "HTTP/.* 200" && match "$base/uri?q=a%00b"
-out=$(curl -v $base/uri?q=a\&q=b 2>&1); match "HTTP/.* 200" && match "$base/uri?q=a&q=b"
-out=$(curl -v $base/uri?q%5B%5D=a\&q%5B%5D=b 2>&1); match "HTTP/.* 200" && match "$base/uri?q%5B%5D=a\&q%5B%5D=b"
-
-out=$(curl -v $base/query 2>&1); match "HTTP/.* 200" && match "{}"
-out=$(curl -v $base/query? 2>&1); match "HTTP/.* 200" && match "{}"
-out=$(curl -v $base/query?query 2>&1); match "HTTP/.* 200" && match "{\"query\":\"\"}"
-out=$(curl -v $base/query?q=a 2>&1); match "HTTP/.* 200" && match "{\"q\":\"a\"\}"
-out=$(curl -v $base/query?q=a! 2>&1); match "HTTP/.* 200" && match "{\"q\":\"a!\"\}"
-out=$(curl -v $base/query?q=a%21 2>&1); match "HTTP/.* 200" && match "{\"q\":\"a!\"\}"
-out=$(curl -v $base/query?q=w%C3%B6rd 2>&1); match "HTTP/.* 200" && match "{\"q\":\"wörd\"\}"
-out=$(curl -v $base/query?q=a+b 2>&1); match "HTTP/.* 200" && match "{\"q\":\"a b\"\}"
-out=$(curl -v $base/query?q=a%20b 2>&1); match "HTTP/.* 200" && match "{\"q\":\"a b\"\}"
-out=$(curl -v $base/query?q=a%2Fb 2>&1); match "HTTP/.* 200" && match "{\"q\":\"a/b\"\}"
-out=$(curl -v $base/query?q=a%00b 2>&1); match "HTTP/.* 200" && match "{\"q\":\"a\\\\u0000b\"\}"
-out=$(curl -v $base/query?q=a\&q=b 2>&1); match "HTTP/.* 200" && match "{\"q\":\"b\"}"
-out=$(curl -v $base/query?q%5B%5D=a\&q%5B%5D=b 2>&1); match "HTTP/.* 200" && match "{\"q\":[[]\"a\",\"b\"[]]}"
-
-out=$(curl -v $base/users/foo 2>&1); match "HTTP/.* 200" && match "Hello foo!" && match -iP "Content-Type: text/plain; charset=utf-8[\r\n]"
-out=$(curl -v $base/users/w%C3%B6rld 2>&1); match "HTTP/.* 200" && match "Hello wörld!"
-out=$(curl -v $base/users/w%F6rld 2>&1); match "HTTP/.* 200" && match "Hello w�rld!" # demo expects UTF-8 instead of ISO-8859-1
-out=$(curl -v $base/users/a+b 2>&1); match "HTTP/.* 200" && match "Hello a+b!"
-out=$(curl -v $base/users/Wham! 2>&1); match "HTTP/.* 200" && match "Hello Wham!!"
-out=$(curl -v $base/users/Wham%21 2>&1); match "HTTP/.* 200" && match "Hello Wham!!"
-out=$(curl -v $base/users/AC%2FDC 2>&1); skipif "HTTP/.* 404" && match "HTTP/.* 200" && match "Hello AC/DC!" # skip Apache (404 unless `AllowEncodedSlashes NoDecode`)
-out=$(curl -v $base/users/bi%00n 2>&1); skipif "HTTP/.* 40[04]" && match "HTTP/.* 200" && match "Hello bi�n!" # skip nginx (400) and Apache (404)
-
-out=$(curl -v $base/users 2>&1); match "HTTP/.* 404"
-out=$(curl -v $base/users/ 2>&1); match "HTTP/.* 404"
-out=$(curl -v $base/users/a/b 2>&1); match "HTTP/.* 404"
-
-out=$(curl -v $base/robots.txt 2>&1); match "HTTP/.* 200" && match -iP "Content-Type: text/plain[\r\n]"
-out=$(curl -v $base/source 2>&1); match -i "Location: /source/" && match -iP "Content-Type: text/html; charset=utf-8[\r\n]"
-out=$(curl -v $base/source/ 2>&1); match "HTTP/.* 200"
-out=$(curl -v $base/source/composer.json 2>&1); match "HTTP/.* 200" && match -iP "Content-Type: application/json[\r\n]"
-out=$(curl -v $base/source/public/robots.txt 2>&1); match "HTTP/.* 200" && match -iP "Content-Type: text/plain[\r\n]"
-out=$(curl -v $base/source/public/robots.txt/ 2>&1); match -i "Location: ../robots.txt" && match -iP "Content-Type: text/html; charset=utf-8[\r\n]"
-out=$(curl -v $base/source/public/robots.txt// 2>&1); match "HTTP/.* 404"
-out=$(curl -v $base/source//public/robots.txt 2>&1); match "HTTP/.* 404"
-out=$(curl -v $base/source/public 2>&1); match -i "Location: public/" && match -iP "Content-Type: text/html; charset=utf-8[\r\n]"
-out=$(curl -v $base/source/invalid 2>&1); match "HTTP/.* 404"
-out=$(curl -v $base/source/bin%00ary 2>&1); match "HTTP/.* 40[40]" # expects 404, but not processed with nginx (400) and Apache (404)
-
-out=$(curl -v $base/method 2>&1); match "HTTP/.* 200" && match "GET"
-out=$(curl -v $base/method -I 2>&1); match "HTTP/.* 200" && match -iP "Content-Length: 5[\r\n]" # HEAD has no response body
-out=$(curl -v $base/method -X POST 2>&1); match "HTTP/.* 200" && match "POST"
-out=$(curl -v $base/method -X PUT 2>&1); match "HTTP/.* 200" && match "PUT"
-out=$(curl -v $base/method -X PATCH 2>&1); match "HTTP/.* 200" && match "PATCH"
-out=$(curl -v $base/method -X DELETE 2>&1); match "HTTP/.* 200" && match "DELETE"
-out=$(curl -v $base/method -X OPTIONS 2>&1); match "HTTP/.* 200" && match "OPTIONS"
-out=$(curl -v $base -X OPTIONS --request-target "*" 2>&1); skipif "Server: nginx" && match "HTTP/.* 200" # skip nginx (400)
-
-out=$(curl -v $base/method/get 2>&1); match "HTTP/.* 200" && match -iP "Content-Length: 4[\r\n]" && match -iP "Content-Type: text/plain; charset=utf-8[\r\n]" && match -iP "X-Is-Head: false[\r\n]" && match -P "GET$"
-out=$(curl -v $base/method/get -I 2>&1); match "HTTP/.* 200" && match -iP "Content-Length: 4[\r\n]" && match -iP "Content-Type: text/plain; charset=utf-8[\r\n]" && match -iP "X-Is-Head: true[\r\n]"
-out=$(curl -v $base/method/head 2>&1); match "HTTP/.* 200" && match -iP "Content-Length: 5[\r\n]" && match -iP "Content-Type: text/plain; charset=utf-8[\r\n]" && match -iP "X-Is-Head: false[\r\n]" && match -P "HEAD$"
-out=$(curl -v $base/method/head -I 2>&1); match "HTTP/.* 200" && match -iP "Content-Length: 5[\r\n]" && match -iP "Content-Type: text/plain; charset=utf-8[\r\n]" && match -iP "X-Is-Head: true[\r\n]"
-
-out=$(curl -v $base/etag/ 2>&1); match "HTTP/.* 200" && match -iP "Content-Length: 0[\r\n]" && match -iP "Etag: \"_\""
-out=$(curl -v $base/etag/ -H 'If-None-Match: "_"' 2>&1); match "HTTP/.* 304" && notmatch -i "Content-Length" && match -iP "Etag: \"_\""
-out=$(curl -v $base/etag/a 2>&1); match "HTTP/.* 200" && match -iP "Content-Length: 2[\r\n]" && match -iP "Etag: \"a\""
-out=$(curl -v $base/etag/a -H 'If-None-Match: "a"' 2>&1); skipif "Server: Apache" && match "HTTP/.* 304" && match -iP "Content-Length: 2[\r\n]" && match -iP "Etag: \"a\"" # skip Apache (no Content-Length)
-
-out=$(curl -v $base/headers -H 'Accept: text/html' 2>&1); match "HTTP/.* 200" && match "\"Accept\": \"text/html\""
-out=$(curl -v $base/headers -d 'name=Alice' 2>&1); match "HTTP/.* 200" && match "\"Content-Type\": \"application/x-www-form-urlencoded\"" && match "\"Content-Length\": \"10\""
-out=$(curl -v $base/headers -u user:pass 2>&1); match "HTTP/.* 200" && match "\"Authorization\": \"Basic dXNlcjpwYXNz\""
-out=$(curl -v $base/headers 2>&1); match "HTTP/.* 200" && notmatch -i "\"Content-Type\"" && notmatch -i "\"Content-Length\""
-out=$(curl -v $base/headers -H User-Agent: -H Accept: -H Host: -10 2>&1); match "HTTP/.* 200" && match "{}"
-out=$(curl -v $base/headers -H 'Content-Length: 0' 2>&1); match "HTTP/.* 200" && match "\"Content-Length\": \"0\""
-out=$(curl -v $base/headers -H 'Empty;' 2>&1); match "HTTP/.* 200" && match "\"Empty\": \"\""
-out=$(curl -v $base/headers -H 'Content-Type;' 2>&1); skipif "Server: Apache" && match "HTTP/.* 200" && match "\"Content-Type\": \"\"" # skip Apache (discards empty Content-Type)
-out=$(curl -v $base/headers -H 'DNT: 1' 2>&1); skipif "Server: nginx" && match "HTTP/.* 200" && match "\"DNT\"" && notmatch "\"Dnt\"" # skip nginx which doesn't report original case (DNT->Dnt)
-out=$(curl -v $base/headers -H 'V: a' -H 'V: b' 2>&1); skipif "Server: nginx" && skipifnot "Server:" && match "HTTP/.* 200" && match "\"V\": \"a, b\"" # skip nginx (last only) and PHP webserver (first only)
-
-out=$(curl -v $base/set-cookie 2>&1); match "HTTP/.* 200" && match "Set-Cookie: 1=1" && match "Set-Cookie: 2=2"
-
-out=$(curl -v --proxy $baseWithPort $base/debug 2>&1); skipif "Server: nginx" && match "HTTP/.* 400" # skip nginx (continues like direct request)
-out=$(curl -v --proxy $baseWithPort -p $base/debug 2>&1); skipif "CONNECT aborted" && match "HTTP/.* 400" # skip PHP development server (rejects as "Malformed HTTP request")
-
-out=$(curl -v $base/location/201 2>&1); match "HTTP/.* 201" && match "Location: /foobar"
-out=$(curl -v $base/location/202 2>&1); match "HTTP/.* 202" && match "Location: /foobar"
-out=$(curl -v $base/location/301 2>&1); match "HTTP/.* 301" && match "Location: /foobar"
-out=$(curl -v $base/location/302 2>&1); match "HTTP/.* 302" && match "Location: /foobar"
+# check index endpoint
+
+curl -v $base/
+match "HTTP/.* 200"
+match -iP "Content-Type: text/plain; charset=utf-8[\r\n]"
+
+curl -v $base/ -X POST
+match "HTTP/.* 405"
+match -iP "Content-Type: text/html; charset=utf-8[\r\n]"
+
+# check unknown endpoints return `404 Not Found`
+
+curl -v $base/unknown
+match "HTTP/.* 404"
+match -iP "Content-Type: text/html; charset=utf-8[\r\n]"
+
+curl -v $base/index.php
+match "HTTP/.* 404"
+match -iP "Content-Type: text/html; charset=utf-8[\r\n]"
+
+curl -v $base/.htaccess
+match "HTTP/.* 404"
+match -iP "Content-Type: text/html; charset=utf-8[\r\n]"
+
+curl -v $base//
+match "HTTP/.* 404"
+match -iP "Content-Type: text/html; charset=utf-8[\r\n]"
+
+# check endpoints that intentionally return an error
+
+curl -v $base/error
+match "HTTP/.* 500"
+match -iP "Content-Type: text/html; charset=utf-8[\r\n]"
+match "Unable to load error"
+
+curl -v $base/error/null
+match "HTTP/.* 500"
+match -iP "Content-Type: text/html; charset=utf-8[\r\n]"
+
+# check async fibers + coroutines + promises
+
+curl -v $base/sleep/fiber
+match "HTTP/.* 200"
+match -iP "Content-Type: text/plain; charset=utf-8[\r\n]"
+
+curl -v $base/sleep/coroutine
+match "HTTP/.* 200"
+match -iP "Content-Type: text/plain; charset=utf-8[\r\n]"
+
+curl -v $base/sleep/promise
+match "HTTP/.* 200"
+match -iP "Content-Type: text/plain; charset=utf-8[\r\n]"
+
+# check URIs with special characters and encoding
+
+curl -v $base/uri
+match "HTTP/.* 200"
+match "$base/uri"
+
+curl -v $base/uri/
+match "HTTP/.* 200"
+match "$base/uri/"
+
+curl -v $base/uri/foo
+match "HTTP/.* 200"
+match "$base/uri/foo"
+
+curl -v $base/uri/foo/bar
+match "HTTP/.* 200"
+match "$base/uri/foo/bar"
+
+curl -v $base/uri/foo//bar
+match "HTTP/.* 200"
+match "$base/uri/foo//bar"
+
+curl -v $base/uri/Wham!
+match "HTTP/.* 200"
+match "$base/uri/Wham!"
+
+curl -v $base/uri/Wham%21
+match "HTTP/.* 200"
+match "$base/uri/Wham%21"
+
+curl -v $base/uri/AC%2FDC
+skipif "HTTP/.* 404" # skip Apache (404 unless `AllowEncodedSlashes NoDecode`)
+match "HTTP/.* 200"
+match "$base/uri/AC%2FDC"
+
+curl -v $base/uri/bin%00ary
+skipif "HTTP/.* 40[04]" # skip nginx (400) and Apache (404)
+match "HTTP/.* 200"
+match "$base/uri/bin%00ary"
+
+curl -v $base/uri/AC/DC
+match "HTTP/.* 200"
+match "$base/uri/AC/DC"
+
+curl -v $base/uri/http://example.com:8080/
+match "HTTP/.* 200"
+match "$base/uri/http://example.com:8080/"
+
+curl -v $base/uri?
+match "HTTP/.* 200"
+match "$base/uri" # trailing "?" not reported for empty query string
+
+curl -v $base/uri?query
+match "HTTP/.* 200"
+match "$base/uri?query"
+
+curl -v $base/uri?q=a
+match "HTTP/.* 200"
+match "$base/uri?q=a"
+
+curl -v $base/uri?q=a!
+match "HTTP/.* 200"
+match "$base/uri?q=a!"
+
+curl -v $base/uri?q=a%21
+match "HTTP/.* 200"
+match "$base/uri?q=a%21"
+
+curl -v $base/uri?q=w%C3%B6rd
+match "HTTP/.* 200"
+match "$base/uri?q=w%C3%B6rd"
+
+curl -v $base/uri?q=+
+match "HTTP/.* 200"
+match "$base/uri?q=+"
+
+curl -v $base/uri?q=%20
+match "HTTP/.* 200"
+match "$base/uri?q=%20"
+
+curl -v $base/uri?q=a%2Fb
+match "HTTP/.* 200"
+match "$base/uri?q=a%2Fb"
+
+curl -v $base/uri?q=a%00b
+match "HTTP/.* 200"
+match "$base/uri?q=a%00b"
+
+curl -v $base/uri?q=a\&q=b
+match "HTTP/.* 200"
+match "$base/uri?q=a&q=b"
+
+curl -v $base/uri?q%5B%5D=a\&q%5B%5D=b
+match "HTTP/.* 200"
+match "$base/uri?q%5B%5D=a\&q%5B%5D=b"
+
+# check query strings with special characters and encoding
+
+curl -v $base/query
+match "HTTP/.* 200"
+match "{}"
+
+curl -v $base/query?
+match "HTTP/.* 200"
+match "{}"
+
+curl -v $base/query?query
+match "HTTP/.* 200"
+match "{\"query\":\"\"}"
+
+curl -v $base/query?q=a
+match "HTTP/.* 200"
+match "{\"q\":\"a\"\}"
+
+curl -v $base/query?q=a!
+match "HTTP/.* 200"
+match "{\"q\":\"a!\"\}"
+
+curl -v $base/query?q=a%21
+match "HTTP/.* 200"
+match "{\"q\":\"a!\"\}"
+
+curl -v $base/query?q=w%C3%B6rd
+match "HTTP/.* 200"
+match "{\"q\":\"wörd\"\}"
+
+curl -v $base/query?q=a+b
+match "HTTP/.* 200"
+match "{\"q\":\"a b\"\}"
+
+curl -v $base/query?q=a%20b
+match "HTTP/.* 200"
+match "{\"q\":\"a b\"\}"
+
+curl -v $base/query?q=a%2Fb
+match "HTTP/.* 200"
+match "{\"q\":\"a/b\"\}"
+
+curl -v $base/query?q=a%00b
+match "HTTP/.* 200"
+match "{\"q\":\"a\\\\u0000b\"\}"
+
+curl -v $base/query?q=a\&q=b
+match "HTTP/.* 200"
+match "{\"q\":\"b\"}"
+
+curl -v $base/query?q%5B%5D=a\&q%5B%5D=b
+match "HTTP/.* 200"
+match "{\"q\":[[]\"a\",\"b\"[]]}"
+
+# check endpoint accepting single placeholder with special characters and encoding
+
+curl -v $base/users/foo
+match "HTTP/.* 200"
+match "Hello foo!"
+match -iP "Content-Type: text/plain; charset=utf-8[\r\n]"
+
+curl -v $base/users/w%C3%B6rld
+match "HTTP/.* 200"
+match "Hello wörld!"
+
+curl -v $base/users/w%F6rld
+match "HTTP/.* 200"
+match "Hello w�rld!" # demo expects UTF-8 instead of ISO-8859-1
+
+curl -v $base/users/a+b
+match "HTTP/.* 200"
+match "Hello a+b!"
+
+curl -v $base/users/Wham!
+match "HTTP/.* 200"
+match "Hello Wham!!"
+
+curl -v $base/users/Wham%21
+match "HTTP/.* 200"
+match "Hello Wham!!"
+
+curl -v $base/users/AC%2FDC
+skipif "HTTP/.* 404" # skip Apache (404 unless `AllowEncodedSlashes NoDecode`)
+match "HTTP/.* 200"
+match "Hello AC/DC!"
+
+curl -v $base/users/bi%00n
+skipif "HTTP/.* 40[04]" # skip nginx (400) and Apache (404)
+match "HTTP/.* 200"
+match "Hello bi�n!"
+
+curl -v $base/users
+match "HTTP/.* 404"
+
+curl -v $base/users/
+match "HTTP/.* 404"
+
+curl -v $base/users/a/b
+match "HTTP/.* 404"
+
+# check filesystem access
+
+curl -v $base/robots.txt
+match "HTTP/.* 200"
+match -iP "Content-Type: text/plain[\r\n]"
+
+curl -v $base/source
+match -i "Location: /source/"
+match -iP "Content-Type: text/html; charset=utf-8[\r\n]"
+
+curl -v $base/source/
+match "HTTP/.* 200"
+
+curl -v $base/source/composer.json
+match "HTTP/.* 200"
+match -iP "Content-Type: application/json[\r\n]"
+
+curl -v $base/source/public/robots.txt
+match "HTTP/.* 200"
+match -iP "Content-Type: text/plain[\r\n]"
+
+curl -v $base/source/public/robots.txt/
+match -i "Location: ../robots.txt"
+match -iP "Content-Type: text/html; charset=utf-8[\r\n]"
+
+curl -v $base/source/public/robots.txt//
+match "HTTP/.* 404"
+
+curl -v $base/source//public/robots.txt
+match "HTTP/.* 404"
+
+curl -v $base/source/public
+match -i "Location: public/"
+match -iP "Content-Type: text/html; charset=utf-8[\r\n]"
+
+curl -v $base/source/invalid
+match "HTTP/.* 404"
+
+curl -v $base/source/bin%00ary
+match "HTTP/.* 40[40]" # expects 404, but not processed with nginx (400) and Apache (404)
+
+# check different request methods
+
+curl -v $base/method
+match "HTTP/.* 200"
+match "GET"
+
+curl -v $base/method -I
+match "HTTP/.* 200"
+match -iP "Content-Length: 5[\r\n]" # HEAD has no response body
+
+curl -v $base/method -X POST
+match "HTTP/.* 200"
+match "POST"
+
+curl -v $base/method -X PUT
+match "HTTP/.* 200"
+match "PUT"
+
+curl -v $base/method -X PATCH
+match "HTTP/.* 200"
+match "PATCH"
+
+curl -v $base/method -X DELETE
+match "HTTP/.* 200"
+match "DELETE"
+
+curl -v $base/method -X OPTIONS
+match "HTTP/.* 200"
+match "OPTIONS"
+
+curl -v $base -X OPTIONS --request-target "*" # OPTIONS * HTTP/1.1
+skipif "Server: nginx" # skip nginx (400)
+match "HTTP/.* 200"
+
+curl -v $base/method/get
+match "HTTP/.* 200"
+match -iP "Content-Length: 4[\r\n]"
+match -iP "Content-Type: text/plain; charset=utf-8[\r\n]"
+match -iP "X-Is-Head: false[\r\n]"
+match -P "GET$"
+
+curl -v $base/method/get -I
+match "HTTP/.* 200"
+match -iP "Content-Length: 4[\r\n]"
+match -iP "Content-Type: text/plain; charset=utf-8[\r\n]"
+match -iP "X-Is-Head: true[\r\n]"
+
+curl -v $base/method/head
+match "HTTP/.* 200"
+match -iP "Content-Length: 5[\r\n]"
+match -iP "Content-Type: text/plain; charset=utf-8[\r\n]"
+match -iP "X-Is-Head: false[\r\n]"
+match -P "HEAD$"
+
+curl -v $base/method/head -I
+match "HTTP/.* 200"
+match -iP "Content-Length: 5[\r\n]"
+match -iP "Content-Type: text/plain; charset=utf-8[\r\n]"
+match -iP "X-Is-Head: true[\r\n]"
+
+# check ETag caching headers
+
+curl -v $base/etag/
+match "HTTP/.* 200"
+match -iP "Content-Length: 0[\r\n]"
+match -iP "Etag: \"_\""
+
+curl -v $base/etag/ -H 'If-None-Match: "_"'
+match "HTTP/.* 304"
+notmatch -i "Content-Length"
+match -iP "Etag: \"_\""
+
+curl -v $base/etag/a
+match "HTTP/.* 200"
+match -iP "Content-Length: 2[\r\n]"
+match -iP "Etag: \"a\""
+
+curl -v $base/etag/a -H 'If-None-Match: "a"'
+skipif "Server: Apache" # skip Apache (no Content-Length)
+match "HTTP/.* 304"
+match -iP "Content-Length: 2[\r\n]"
+match -iP "Etag: \"a\""
+
+# check HTTP request headers
+
+curl -v $base/headers -H 'Accept: text/html'
+match "HTTP/.* 200"
+match "\"Accept\": \"text/html\""
+
+curl -v $base/headers -d 'name=Alice'
+match "HTTP/.* 200"
+match "\"Content-Type\": \"application/x-www-form-urlencoded\""
+match "\"Content-Length\": \"10\""
+
+curl -v $base/headers -u user:pass
+match "HTTP/.* 200"
+match "\"Authorization\": \"Basic dXNlcjpwYXNz\""
+
+curl -v $base/headers
+match "HTTP/.* 200"
+notmatch -i "\"Content-Type\""
+notmatch -i "\"Content-Length\""
+
+curl -v $base/headers -H User-Agent: -H Accept: -H Host: -10
+match "HTTP/.* 200"
+match "{}"
+
+curl -v $base/headers -H 'Content-Length: 0'
+match "HTTP/.* 200"
+match "\"Content-Length\": \"0\""
+
+curl -v $base/headers -H 'Empty;'
+match "HTTP/.* 200"
+match "\"Empty\": \"\""
+
+curl -v $base/headers -H 'Content-Type;'
+skipif "Server: Apache" # skip Apache (discards empty Content-Type)
+match "HTTP/.* 200"
+match "\"Content-Type\": \"\""
+
+curl -v $base/headers -H 'DNT: 1'
+skipif "Server: nginx" # skip nginx which doesn't report original case (DNT->Dnt)
+match "HTTP/.* 200"
+match "\"DNT\""
+notmatch "\"Dnt\""
+
+curl -v $base/headers -H 'V: a' -H 'V: b'
+skipif "Server: nginx" # skip nginx (last only) and PHP webserver (first only)
+skipifnot "Server:"
+match "HTTP/.* 200"
+match "\"V\": \"a, b\""
+
+# check HTTP response with multiple cookie headers
+
+curl -v $base/set-cookie
+match "HTTP/.* 200"
+match "Set-Cookie: 1=1"
+match "Set-Cookie: 2=2"
+
+# check rejecting HTTP proxy requests
+
+curl -v --proxy $baseWithPort $base/debug
+skipif "Server: nginx" # skip nginx (continues like direct request)
+match "HTTP/.* 400"
+
+curl -v --proxy $baseWithPort -p $base/debug
+skipif "CONNECT aborted" # skip PHP development server (rejects as "Malformed HTTP request")
+match "HTTP/.* 400"
+
+# check HTTP redirects
+
+curl -v $base/location/201
+match "HTTP/.* 201"
+match "Location: /foobar"
+
+curl -v $base/location/202
+match "HTTP/.* 202"
+match "Location: /foobar"
+
+curl -v $base/location/301
+match "HTTP/.* 301"
+match "Location: /foobar"
+
+curl -v $base/location/302
+match "HTTP/.* 302"
+match "Location: /foobar"
+
+# end
echo "OK ($n)"