Conformance suite#
tests/conformance holds the spec cases as an importable package
rather than as test files, so anything can replay them.
- The core module runs them with no router in between.
- Each adapter module runs the same cases through its own wrapper.
- An adapter this repo does not ship can import the package and do the same.
Running it#
go test ./tests -run TestConformance -vThrough a specific adapter:
cd adapter/fiber && go test ./... -race -vHow it is put together#
Cases are values, not functions scattered across files:
var hubCases = []testCase{
{name: "accepts a subscription request with 202", run: hubAccepts},
{name: "refuses a perpetual lease", modes: []websub.Mode{websub.ModeWebSub}, run: hubRefusesPerpetual},
{name: "grants a perpetual lease", modes: []websub.Mode{websub.ModePuSH04}, run: hubGrantsPerpetual},
}A case with no modes runs against both specifications. A case that names
one runs only there, which keeps behavior the two specs share from being
tested twice for nothing, and makes the places they disagree obvious in the
source.
Every fixture on the other side of an exchange is hand written and does not
use this library: fakeHub speaks the hub side of the protocol without
being a websub.Hub, and fakeSubscriber answers challenges without being
a websub.Subscriber. That matters. If both ends shared an implementation,
a case could pass because both ends make the same mistake.
Configuring a run#
type Config struct {
Wrap func(http.Handler) http.Handler
Serve func(t *testing.T, h http.Handler) string
Modes []websub.Mode
}Wrap mounts a handler behind a router that is itself an http.Handler,
which covers most of them. Serve is for frameworks that are not built on
net/http and have to run their own server. Set at most one.
The handler must be mounted so every path below the root reaches it, since a subscriber’s callbacks live under a prefix rather than at one exact path.
The passthrough suite#
RunPassthrough is a smaller, separate set of cases checking that a router
preserves what the protocol depends on:
- Request bodies arrive byte for byte, because a signature is computed over exactly those bytes
- Repeated
Linkheaders stay separate values, because discovery reads several from one message X-Hub-Signaturesurvives, and still verifies against the delivered body- Query parameters survive encoding, because a challenge can contain anything
- Response status codes are preserved, because 404 and 202 are protocol signals
- Repeated response headers are preserved, because that is how a publisher advertises
Run covers these indirectly, but a failure there is hard to read. These
fail with the exact byte, header, or status that went missing.
What is covered#
See the conformance checklist in the README for the current list, which is kept in step with the cases.
Adding a case#
- Write the case function in
subscriber_cases.go,publisher_cases.go, orhub_cases.go, taking(t *testing.T, cfg Config, mode websub.Mode). - Add it to the slice at the top of that file. Set
modesonly if the two specifications genuinely differ here. - Link the spec section in a comment next to the assertion.
- Run it through an adapter as well as through core. If it passes in core and fails in fiber, you have found something real.
- Add a line to the README checklist.
A case belongs here if it tests protocol behavior over HTTP. A case testing
this library’s own API, functional options, error values, retry policy,
belongs in the module’s tests/ package instead.
Fuzzing#
Signature verification is fuzzed, because a forged X-Hub-Signature
accepted through a parsing bug is the worst thing that can happen here. The
target asserts two properties: it never panics, and it never accepts a
header that was not produced from the same secret and body.
go test ./tests -run '^$' -fuzz FuzzVerify -fuzztime 60sCI runs it on every pull request. If it finds a failure, commit the corpus
entry it writes under tests/testdata/fuzz/.