package bearer import ( "context" "errors" "net/http" "net/http/httptest" "testing" ) // The grant strings these benchmarks present. The stateless one is what a short // token carries; the registered one adds the id: member that turns step 4 from // a no-op into a cache lookup. const ( benchGrants = "bench:upload cov:upload artifacts:upload" benchGrantsRegistered = benchGrants + " id:4711" ) // The sinks keep a validated token from being discarded as an unread result. var ( sinkToken *Token sinkErr error ) // liveDaemon stands in for tokens.sr.ht's revocation endpoint and answers 204 // — live — to everything. // // It is only ever asked once per benchmark: the warm-up call below populates // the cache, and every measured iteration is then the cached path, which is the // one a running service spends its time in. A benchmark that hit the server on // every iteration would be measuring httptest's loopback, not this package. func liveDaemon(b *testing.B) *httptest.Server { b.Helper() s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNoContent) })) b.Cleanup(s.Close) return s } // benchValidator builds a validator pointed at a daemon that says "live". func benchValidator(b *testing.B) *Validator { b.Helper() s := liveDaemon(b) v, err := New(Options{ Origin: s.URL, ClientID: callerClientID, NodeID: callerNodeID, HTTPClient: s.Client(), }) if err != nil { b.Fatalf("building the validator: %v", err) } return v } // BenchmarkValidate is the whole of SPEC ch. 6 on one presented token, which is // what a service pays per authenticated request. // // The four cases are the four outcomes that happen in production: // // - stateless: an HMAC, a grant parse and a map lookup, and no network at all. // This is the common case under the default configuration and the number // that matters. // - registered_cached: the same, plus step 4 answered from the cache under the // validator's mutex. The difference between it and stateless is the price of // revocability. // - forbidden: a good credential that does not carry the action. It costs more // than a success, because the refusal renders the grant set into its message // — worth knowing, since a misconfigured client retries this in a loop. // - invalid: junk. Step 1 refuses it without allocating a token, and this is // the path an instance under a flood of forged credentials runs; it must // stay the cheapest thing here. func BenchmarkValidate(b *testing.B) { ctx := context.Background() v := benchValidator(b) stateless := ourToken(benchGrants) registered := ourToken(benchGrantsRegistered) // Warm the revocation cache, and check that the fixture validates at all — // a token that fails here would turn every measured iteration into an early // refusal and report it as a fast success. if _, err := v.Validate(ctx, registered, "bench:upload"); err != nil { b.Fatalf("the registered fixture must validate: %v", err) } b.Run("stateless", func(b *testing.B) { b.ReportAllocs() for b.Loop() { sinkToken, sinkErr = v.Validate(ctx, stateless, "bench:upload") } if sinkErr != nil { b.Fatalf("unexpected refusal: %v", sinkErr) } }) b.Run("registered_cached", func(b *testing.B) { b.ReportAllocs() for b.Loop() { sinkToken, sinkErr = v.Validate(ctx, registered, "bench:upload") } if sinkErr != nil { b.Fatalf("unexpected refusal: %v", sinkErr) } }) b.Run("forbidden", func(b *testing.B) { b.ReportAllocs() for b.Loop() { sinkToken, sinkErr = v.Validate(ctx, stateless, "dolt:push") } if !errors.Is(sinkErr, ErrForbidden) { b.Fatalf("this case must be refused with ErrForbidden, got %v", sinkErr) } }) b.Run("invalid", func(b *testing.B) { // Not a token: the shape a scanner presents. const junk = "not-a-token-at-all-just-a-string-somebody-tried" b.ReportAllocs() for b.Loop() { sinkToken, sinkErr = v.Validate(ctx, junk, "bench:upload") } if !errors.Is(sinkErr, ErrInvalid) { b.Fatalf("this case must be refused with ErrInvalid, got %v", sinkErr) } }) } // BenchmarkInspect is the path a service's identity middleware runs: the same // four steps without step 3, once per request, upstream of the router. Read it // beside BenchmarkValidate/stateless — the gap is the grant check that Inspect // leaves to the handler. func BenchmarkInspect(b *testing.B) { ctx := context.Background() v := benchValidator(b) presented := ourToken(benchGrants) b.ReportAllocs() for b.Loop() { sinkToken, sinkErr = v.Inspect(ctx, presented) } if sinkErr != nil { b.Fatalf("unexpected refusal: %v", sinkErr) } } // BenchmarkValidateParallel is the same registered token validated from many // goroutines at once, which is how a service actually holds a Validator: one of // them, behind every request handler. // // The revocation cache is a map behind a single mutex, so this is where that // choice shows up. A per-request HMAC is embarrassingly parallel; a shared lock // is not, and the point of measuring it is to know which of the two dominates // before somebody proposes sharding the cache. func BenchmarkValidateParallel(b *testing.B) { ctx := context.Background() v := benchValidator(b) registered := ourToken(benchGrantsRegistered) if _, err := v.Validate(ctx, registered, "bench:upload"); err != nil { b.Fatalf("the registered fixture must validate: %v", err) } b.ReportAllocs() b.RunParallel(func(pb *testing.PB) { for pb.Next() { if _, err := v.Validate(ctx, registered, "bench:upload"); err != nil { b.Errorf("unexpected refusal: %v", err) return } } }) }