diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 2c7a67c5bff..faff8b4f125 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -7,6 +7,7 @@ on:
branches:
- master
- 2.*
+ - hurl-tests
pull_request:
branches:
- master
@@ -14,6 +15,9 @@ on:
jobs:
test:
+ permissions:
+ checks: write
+ pull-requests: write
strategy:
# Default is true, cancels jobs for other platforms in the matrix if one fails
fail-fast: false
@@ -140,6 +144,93 @@ jobs:
# echo "step_test ${{ steps.step_test.outputs.status }}\n"
# exit 1
+ spec-test:
+ permissions:
+ checks: write
+ pull-requests: write
+ strategy:
+ matrix:
+ os:
+ - linux
+ go:
+ - '1.23'
+
+ include:
+ # Set the minimum Go patch version for the given Go minor
+ # Usable via ${{ matrix.GO_SEMVER }}
+ - go: '1.23'
+ GO_SEMVER: '~1.23.0'
+
+ # Set some variables per OS, usable via ${{ matrix.VAR }}
+ # OS_LABEL: the VM label from GitHub Actions (see https://docs.github.com/en/actions/using-github-hosted-runners/about-github-hosted-runners/about-github-hosted-runners#standard-github-hosted-runners-for-public-repositories)
+ # CADDY_BIN_PATH: the path to the compiled Caddy binary, for artifact publishing
+ # SUCCESS: the typical value for $? per OS (Windows/pwsh returns 'True')
+ - os: linux
+ OS_LABEL: ubuntu-latest
+ CADDY_BIN_PATH: ./cmd/caddy/caddy
+ SUCCESS: 0
+
+ runs-on: ${{ matrix.OS_LABEL }}
+
+ steps:
+ - name: Checkout code
+ uses: actions/checkout@v4
+
+ - name: Install Go
+ uses: actions/setup-go@v5
+ with:
+ go-version: ${{ matrix.GO_SEMVER }}
+ check-latest: true
+
+ - name: Print Go version and environment
+ id: vars
+ shell: bash
+ run: |
+ printf "curl version: $(curl --version)\n"
+ printf "Using go at: $(which go)\n"
+ printf "Go version: $(go version)\n"
+ printf "\n\nGo environment:\n\n"
+ go env
+ printf "\n\nSystem environment:\n\n"
+ env
+ printf "Git version: $(git version)\n\n"
+ # Calculate the short SHA1 hash of the git commit
+ echo "short_sha=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT
+
+ - name: Get dependencies
+ run: |
+ go get -v -t -d ./...
+ # mkdir test-results
+ - name: Build Caddy
+ working-directory: ./cmd/caddy
+ env:
+ CGO_ENABLED: 0
+ run: |
+ go build -tags nobadger -trimpath -ldflags="-w -s" -v
+
+ - name: Install Hurl
+ env:
+ HURL_VERSION: "5.0.1"
+ run: |
+ curl --location --remote-name https://github.com/Orange-OpenSource/hurl/releases/download/${HURL_VERSION}/hurl_${HURL_VERSION}_amd64.deb
+ sudo dpkg -i hurl_${HURL_VERSION}_amd64.deb
+ hurl --version
+
+ - name: Run Caddy
+ run: |
+ ./cmd/caddy/caddy start
+
+ - name: Run tests with Hurl
+ run: |
+ mkdir hurl-report
+ find . -name *.hurl -exec hurl --variables-file caddytest/spec/hurl_vars.properties --very-verbose --verbose --test --report-junit hurl-report/junit.xml --color {} \;
+
+ - name: Publish Test Results
+ uses: EnricoMi/publish-unit-test-result-action@v2
+ with:
+ files: |
+ hurl-report/junit.xml
+
s390x-test:
name: test (s390x on IBM Z)
runs-on: ubuntu-latest
diff --git a/caddytest/spec/http/basicauth/spec.hurl b/caddytest/spec/http/basicauth/spec.hurl
new file mode 100644
index 00000000000..0362d3dbdff
--- /dev/null
+++ b/caddytest/spec/http/basicauth/spec.hurl
@@ -0,0 +1,38 @@
+# Configure Caddy
+POST http://localhost:2019/load
+Content-Type: text/caddyfile
+```
+{
+ skip_install_trust
+ http_port 9080
+ https_port 9443
+ local_certs
+ debug
+}
+localhost {
+ log
+ basic_auth {
+ john $2a$14$x4HlYwA9Zeer4RkMEYbUzug9XxWmncneR.dcMs.UjalR95URnHg5.
+ }
+ respond "Hello, World!"
+}
+```
+
+# requests without `Authorization` header are rejected with 401
+GET https://localhost:9443
+[Options]
+insecure: true
+HTTP 401
+[Asserts]
+header "WWW-Authenticate" == "Basic realm=\"restricted\""
+
+
+# requests with `Authorization` header are accepted with 200
+GET https://localhost:9443
+[BasicAuth]
+john:password
+[Options]
+insecure: true
+HTTP 200
+[Asserts]
+`Hello, World!`
diff --git a/caddytest/spec/http/file_server/assets/indexed/index.html b/caddytest/spec/http/file_server/assets/indexed/index.html
new file mode 100644
index 00000000000..da5de44a090
--- /dev/null
+++ b/caddytest/spec/http/file_server/assets/indexed/index.html
@@ -0,0 +1,9 @@
+
+
+
+ Index.html Title
+
+
+ Index.html
+
+
\ No newline at end of file
diff --git a/caddytest/spec/http/file_server/assets/indexed/index.txt b/caddytest/spec/http/file_server/assets/indexed/index.txt
new file mode 100644
index 00000000000..21fc0c67e88
--- /dev/null
+++ b/caddytest/spec/http/file_server/assets/indexed/index.txt
@@ -0,0 +1 @@
+index.txt
\ No newline at end of file
diff --git a/caddytest/spec/http/file_server/assets/unindexed/.gitkeep b/caddytest/spec/http/file_server/assets/unindexed/.gitkeep
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/caddytest/spec/http/file_server/spec.hurl b/caddytest/spec/http/file_server/spec.hurl
new file mode 100644
index 00000000000..be9504e1513
--- /dev/null
+++ b/caddytest/spec/http/file_server/spec.hurl
@@ -0,0 +1,119 @@
+# Configure Caddy with default configuration
+POST http://localhost:2019/load
+Content-Type: text/caddyfile
+```
+{
+ skip_install_trust
+ http_port 9080
+ https_port 9443
+ local_certs
+ debug
+}
+localhost {
+ root {{indexed_root}}
+ file_server
+}
+```
+
+# requests without specific file receive index file per
+# the default index list: index.html, index.txt
+GET https://localhost:9443
+[Options]
+insecure: true
+HTTP 200
+[Asserts]
+```
+
+
+
+ Index.html Title
+
+
+ Index.html
+
+```
+
+
+# if index.txt is specifically requested, we expect index.txt
+GET https://localhost:9443/index.txt
+[Options]
+insecure: true
+HTTP 200
+[Asserts]
+body == "index.txt"
+
+# requests for sub-folder followed by .. result in sanitized path
+GET https://localhost:9443/non-existent/../index.txt
+[Options]
+insecure: true
+HTTP 200
+[Asserts]
+body == "index.txt"
+
+# results out of root folder are sanitized,
+# and conform to default index list sequence.
+GET https://localhost:9443/../
+[Options]
+insecure: true
+HTTP 200
+[Asserts]
+```
+
+
+
+ Index.html Title
+
+
+ Index.html
+
+```
+
+
+# Configure Caddy with custsom index "index.txt"
+POST http://localhost:2019/load
+Content-Type: text/caddyfile
+```
+{
+ skip_install_trust
+ http_port 9080
+ https_port 9443
+ local_certs
+ debug
+}
+localhost {
+ root {{indexed_root}}
+ file_server {
+ index index.txt
+ }
+}
+```
+
+GET https://localhost:9443
+[Options]
+insecure: true
+HTTP 200
+[Asserts]
+body == "index.txt"
+
+
+# Configure with a root not containing index files
+POST http://localhost:2019/load
+Content-Type: text/caddyfile
+```
+{
+ skip_install_trust
+ http_port 9080
+ https_port 9443
+ local_certs
+ debug
+}
+localhost {
+ root {{unindexed_root}}
+ file_server
+}
+```
+
+GET https://localhost:9443
+[Options]
+insecure: true
+HTTP 404
\ No newline at end of file
diff --git a/caddytest/spec/http/headers/spec.hurl b/caddytest/spec/http/headers/spec.hurl
new file mode 100644
index 00000000000..909402b6fa2
--- /dev/null
+++ b/caddytest/spec/http/headers/spec.hurl
@@ -0,0 +1,22 @@
+# Configure Caddy
+POST http://localhost:2019/load
+Content-Type: text/caddyfile
+```
+{
+ skip_install_trust
+ http_port 9080
+ https_port 9443
+ local_certs
+ debug
+}
+localhost {
+ header "X-Custom-Header" "Custom-Value"
+}
+```
+
+GET https://localhost:9443
+[Options]
+insecure: true
+HTTP 200
+[Asserts]
+header "X-Custom-Header" == "Custom-Value"
diff --git a/caddytest/spec/http/rewrite/spec.hurl b/caddytest/spec/http/rewrite/spec.hurl
new file mode 100644
index 00000000000..dd1de7bdc06
--- /dev/null
+++ b/caddytest/spec/http/rewrite/spec.hurl
@@ -0,0 +1,66 @@
+# Configure Caddy
+POST http://localhost:2019/load
+User-Agent: hurl/ci
+Content-Type: text/caddyfile
+```
+{
+ skip_install_trust
+ http_port 9080
+ https_port 9443
+ local_certs
+}
+localhost {
+ rewrite /from /to
+ respond {uri}
+}
+```
+
+# simple scenario: rewriting /from to /to produces expected result of seeing /to
+GET https://localhost:9443/from
+[Options]
+insecure: true
+HTTP 200
+[Asserts]
+body == "/to"
+
+# unmatched path is passed through unchanged
+GET https://localhost:9443
+[Options]
+insecure: true
+HTTP 200
+[Asserts]
+body == "/"
+
+# having a query parameter does not trip the rewrite and retains the query
+GET https://localhost:9443/from?query_param=value
+[Options]
+insecure: true
+HTTP 200
+[Asserts]
+body == "/to?query_param=value"
+
+
+# Configure Caddy
+POST http://localhost:2019/load
+User-Agent: hurl/ci
+Content-Type: text/caddyfile
+```
+{
+ skip_install_trust
+ http_port 9080
+ https_port 9443
+ local_certs
+}
+localhost {
+ rewrite /from /to?a=b
+ respond {uri}
+}
+```
+
+# a rewrite with query parameters affects the parameters
+GET https://localhost:9443/from?query_param=value
+[Options]
+insecure: true
+HTTP 200
+[Asserts]
+body == "/to?a=b"
diff --git a/caddytest/spec/http/static_response/spec.hurl b/caddytest/spec/http/static_response/spec.hurl
new file mode 100644
index 00000000000..e3d0c0697a1
--- /dev/null
+++ b/caddytest/spec/http/static_response/spec.hurl
@@ -0,0 +1,105 @@
+# Configure Caddy
+POST http://localhost:2019/load
+User-Agent: hurl/ci
+Content-Type: text/caddyfile
+```
+{
+ skip_install_trust
+ http_port 9080
+ https_port 9443
+ local_certs
+}
+localhost {
+ log
+ respond "Hello, World!"
+}
+```
+
+GET https://localhost:9443
+[Options]
+insecure: true
+HTTP 200
+[Asserts]
+`Hello, World!`
+
+
+GET https://localhost:9443/foo
+[Options]
+insecure: true
+HTTP 200
+[Asserts]
+`Hello, World!`
+
+# Configure Caddy
+POST http://localhost:2019/load
+User-Agent: hurl/ci
+Content-Type: text/caddyfile
+```
+{
+ skip_install_trust
+ http_port 9080
+ https_port 9443
+ local_certs
+}
+localhost {
+ respond "New text!"
+}
+```
+
+GET https://localhost:9443
+[Options]
+insecure: true
+HTTP/2 200
+[Asserts]
+`New text!`
+
+
+GET https://localhost:9443/foo
+[Options]
+insecure: true
+HTTP/2 200
+[Asserts]
+`New text!`
+
+GET https://localhost:9443/foo
+[Options]
+insecure: true
+HTTP/2 200
+[Asserts]
+body != "Hello, World!"
+
+# Configure Caddy
+# The body is a placeholder
+POST http://localhost:2019/load
+User-Agent: hurl/ci
+Content-Type: text/caddyfile
+```
+{
+ skip_install_trust
+ http_port 9080
+ https_port 9443
+ local_certs
+}
+localhost {
+ log
+ respond {http.request.body}
+}
+```
+
+# handler responds with the "application/json" if the response body is valid JSON
+POST https://localhost:9443
+[Options]
+insecure: true
+```json
+{
+ "greeting": "Hello, world!"
+}
+```
+HTTP/2 200
+[Asserts]
+header "Content-Type" == "application/json"
+```json
+{
+ "greeting": "Hello, world!"
+}
+```
diff --git a/caddytest/spec/hurl_vars.properties b/caddytest/spec/hurl_vars.properties
new file mode 100644
index 00000000000..a3832eaead4
--- /dev/null
+++ b/caddytest/spec/hurl_vars.properties
@@ -0,0 +1,2 @@
+indexed_root=caddytest/spec/http/file_server/assets/indexed
+unindexed_root=caddytest/spec/http/file_server/assets/unindexed
\ No newline at end of file