Caddy spec tests
End-to-end HTTP tests for Caddy handlers, written in
Hurl. Each test drives a real Caddy process via the
admin API on :2019, runs requests against :9080 / :9443, and asserts
on status, headers, and body. The Go integration suite under
caddytest/integration covers scenarios that need Go logic;
this directory covers everything that is just "HTTP in, HTTP out."
Layout
spec/
├── README.md
├── hurl_vars.properties # Shared variables ({{indexed_root}}, etc.)
└── http/
├── <directive>/
│ ├── spec.hurl # One spec per directive
│ └── assets/ # Optional fixtures (file_server, templates, …)
└── …
Each spec.hurl is self-contained: it POSTs its own Caddyfile to
/load, then issues the requests that validate the directive. Because
/load replaces the entire config, blocks within a file don't interfere
with each other.
Prerequisites
- Hurl 7.x —
brew install hurlon macOS, or the official install guide. - A Caddy binary running with the admin API on
:2019. For coverage, build with-cover:cd cmd/caddy && go build -cover -tags nobadger,nopgx,nomysql -o caddy-coverage
Running
Start Caddy in one terminal:
mkdir -p /tmp/caddy-coverage
GOCOVERDIR=/tmp/caddy-coverage ./cmd/caddy/caddy-coverage run
Run the suite from the repo root in another terminal:
hurl --test \
--variables-file caddytest/spec/hurl_vars.properties \
--retry 3 --retry-interval 500 \
caddytest/spec/http/**/spec.hurl
Run a single spec:
hurl --test \
--variables-file caddytest/spec/hurl_vars.properties \
caddytest/spec/http/file_server/spec.hurl
The CI workflow in .github/workflows/ci.yml runs the same command.
--retry 3 --retry-interval 500 is a safety net for the brief window after every POST /load when the TLS app is being re-provisioned and may not yet have a cert for localhost. It only fires on failure; passing requests pay nothing.
Coverage
After the suite finishes, stop the daemon and convert the binary coverage profile:
./cmd/caddy/caddy-coverage stop
go tool covdata textfmt -i=/tmp/caddy-coverage -o coverage.out
go tool cover -html=coverage.out # browser view
go tool cover -func=coverage.out | less # per-function summary
Filter to a specific package:
go tool cover -func=coverage.out | grep modules/caddyhttp/fileserver
Writing a spec
- Create
http/<directive>/spec.hurl. - For each scenario, write a
POST /loadwith a minimal Caddyfile, then the request(s) that exercise it. - Use real assertions on observable state — status, headers, body,
side-effect files. Avoid placeholder configs (e.g. routing logs to
discard); they prove only that the directive parses, not that it works. - If the directive has visible side effects you can't read from the
response, expose them with a sidecar site. For example, the
logspec writes to/tmp/caddy-log-spec/*.logand adds a:9081 file_serverblock so a follow-up GET can assert on the log contents. - Add reusable fixture paths to
hurl_vars.propertiesinstead of hard-coding them.
Conventions
- Ports —
9080(HTTP),9443(HTTPS),:9081for sidecar backends. Admin API is:2019. These are also what--retryassumes. - TLS — always include
[Options]\ninsecure: trueon HTTPS requests; the local CA is not trusted. - Templating — Hurl renders
{{name}}as variable substitution. If you need literal{{in a Caddyfile (e.g. atemplatesbody), escape it as\{\{ ... \}\}.