Go httptest: a four-case table test for a Sume webhook handler

Sign a fixture body with sume-v1 HMAC, then assert 204 for a valid call and 401 for a stale timestamp, an empty server secret and a wrong secret.

4 min readSume
All posts

Build the signature in the test with the same recipe Sume uses, HMAC-SHA256 over {timestamp}.{raw_body} prefixed sume-v1=, then drive your handler through httptest for each case you must reject. Sume's webhook docs set the replay window at five minutes and say to refuse a delivery that fails the check, so four cases cover the contract: valid, stale, empty secret and wrong secret.

The four cases

The empty-secret case matters most. An unset environment variable gives a zero-length key, and HMAC with an empty key still returns a valid-looking digest that an attacker can compute, so the handler must refuse before it hashes.

Cases for a Sume webhook handler test (Sume webhook docs, read 2026-10-07)
CaseSigned withServer secretExpect
valids3crets3cret204
stale timestamp (600 s old)s3crets3cret401
empty secrets3cret(empty)401
wrong secretothers3cret401

The test

It assumes a package wh with Handler(secret string) http.HandlerFunc, the receiver from the linked Go verifier post. The test sets x-sume-webhook-timestamp and x-sume-webhook-signature exactly as a delivery does and passes the body unchanged, since a re-serialized body does not verify.

package wh

import ("crypto/hmac"; "crypto/sha256"; "encoding/hex"; "net/http/httptest"; "strconv"; "strings"; "testing"; "time")

func sign(secret, body string, ts int64) (string, string) {
	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write([]byte(strconv.FormatInt(ts, 10) + "." + body))
	return strconv.FormatInt(ts, 10), "sume-v1=" + hex.EncodeToString(mac.Sum(nil))
}

func TestHandler(t *testing.T) {
	body, now := `{"event":"job.completed","job_id":"job_1"}`, time.Now().Unix()
	for _, c := range []struct{ name, secret, signWith string; age int64; want int }{
		{"valid", "s3cret", "s3cret", 0, 204},
		{"stale timestamp", "s3cret", "s3cret", 600, 401},
		{"empty secret", "", "s3cret", 0, 401},
		{"wrong secret", "s3cret", "other", 0, 401},
	} {
		ts, sig := sign(c.signWith, body, now-c.age)
		r := httptest.NewRequest("POST", "/hook", strings.NewReader(body))
		r.Header.Set("x-sume-webhook-timestamp", ts)
		r.Header.Set("x-sume-webhook-signature", sig)
		w := httptest.NewRecorder()
		Handler(c.secret).ServeHTTP(w, r)
		if w.Code != c.want {
			t.Errorf("%s: got %d, want %d", c.name, w.Code, c.want)
		}
	}
}

What to add

During a secret rotation the signature header holds one entry per live secret, newest first, separated by commas, and a delivery is valid if any entry matches. Add a fifth case with sume-v1=00, in front of the good signature to prove your handler accepts it.

This test never contacts Sume. To check your public HTTPS URL end to end, the API has a test-delivery endpoint, POST /v1/webhooks/test-deliveries.

Sources

Related posts

More in Developers

All Developers posts

Written by Sume