From f1218f2c84c40d0eb025729330336024de64c9f1 Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Mon, 21 Sep 2026 19:47:16 +0200 Subject: [PATCH] test(06-14): regression coverage for surf and bouncer gap closure --- bouncer/jwt_test.go | 40 ++++++++++++++++++ bouncer/registry_test.go | 44 ++++++++++++++++++++ surf/bodylimit_test.go | 88 ++++++++++++++++++++++++++++++++++++++++ surf/limiter_test.go | 68 +++++++++++++++++++++++++++++++ surf/router_test.go | 52 ++++++++++++++++++++++++ 5 files changed, 292 insertions(+) diff --git a/bouncer/jwt_test.go b/bouncer/jwt_test.go index c293ce2..149fd25 100644 --- a/bouncer/jwt_test.go +++ b/bouncer/jwt_test.go @@ -252,3 +252,43 @@ func TestVerifyRejectsFractionalSubject(t *testing.T) { t.Fatalf("whole sub: %q %v", sub, err) } } + +func TestVerifySubjectMatrix(t *testing.T) { + exp := time.Now().Add(time.Hour).Unix() + cases := []struct { + name string + sub any + want string + }{ + {"fractional", 12.5, ""}, + {"huge float", 1e300, ""}, + {"2^60 float", float64(1 << 60), ""}, + {"negative", -1, ""}, + {"zero", 0, ""}, + {"whole number", 12, "12"}, + {"string", "12", "12"}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + tok := sign(t, jwt.SigningMethodHS256, jwt.MapClaims{"sub": tc.sub, "exp": exp}, []byte(secret)) + sub, err := Verify(tok, secret) + if tc.want == "" { + if err == nil || sub != "" { + t.Fatalf("sub %v accepted: %q", tc.sub, sub) + } + return + } + if err != nil || sub != tc.want { + t.Fatalf("sub %v: %q %v", tc.sub, sub, err) + } + }) + } +} + +func TestSubjectJSONNumber(t *testing.T) { + for in, want := range map[string]string{"1.5": "", "-3": "", "0": "", "12": "12"} { + if got := subject(jwt.MapClaims{"sub": json.Number(in)}); got != want { + t.Errorf("json.Number(%q) = %q, want %q", in, got, want) + } + } +} diff --git a/bouncer/registry_test.go b/bouncer/registry_test.go index 2e5494e..4f78790 100644 --- a/bouncer/registry_test.go +++ b/bouncer/registry_test.go @@ -245,3 +245,47 @@ func TestRegisterRejectsTypedNilGuard(t *testing.T) { type credPtrGuard struct{} func (*credPtrGuard) Authenticate(*http.Request) (*Principal, error) { return nil, nil } + +type nilFuncGuard func(*http.Request) (*Principal, error) + +func (f nilFuncGuard) Authenticate(r *http.Request) (*Principal, error) { return f(r) } + +type nilMapGuard map[string]string + +func (nilMapGuard) Authenticate(*http.Request) (*Principal, error) { return nil, nil } + +func TestRegisterRejectsTypedNilPointerFuncMapGuards(t *testing.T) { + var ( + ptr *credPtrGuard + fn nilFuncGuard + mp nilMapGuard + ) + for name, g := range map[string]any{"pointer": ptr, "func": fn, "map": mp} { + t.Run(name, func(t *testing.T) { + var reg Registry + err := reg.Register("golem15.p", "g", g) + if err == nil || !strings.Contains(err.Error(), "golem15.p") || !strings.Contains(err.Error(), `"g"`) { + t.Fatalf("typed-nil %s guard: err = %v", name, err) + } + if _, err := reg.Middleware("g"); err == nil { + t.Fatal("rejected guard must not be resolvable") + } + }) + } +} + +func TestRegisterAcceptsValidGuards(t *testing.T) { + var reg Registry + if err := reg.Register("p", "ptr", &credPtrGuard{}); err != nil { + t.Fatal(err) + } + if err := reg.Register("p", "fn", nilFuncGuard(func(*http.Request) (*Principal, error) { return nil, nil })); err != nil { + t.Fatal(err) + } + if err := reg.Register("p", "map", nilMapGuard{}); err != nil { + t.Fatal(err) + } + if err := reg.Register("p", "w", writerGuard{}); err != nil { + t.Fatal(err) + } +} diff --git a/surf/bodylimit_test.go b/surf/bodylimit_test.go index 54186ee..e822137 100644 --- a/surf/bodylimit_test.go +++ b/surf/bodylimit_test.go @@ -179,3 +179,91 @@ func TestCompileRouteConflictReturnsError(t *testing.T) { t.Fatal("expected conflict error") } } + +type bodyProbePlugin struct { + id string + raw bool + use []string + mw map[string]pact.Middleware + routes func(pact.Router) +} + +func (p bodyProbePlugin) ID() string { return p.id } +func (p bodyProbePlugin) Requires() []string { return nil } +func (p bodyProbePlugin) Register(*backpack.App) error { return nil } +func (p bodyProbePlugin) Boot(*backpack.App) error { return nil } +func (p bodyProbePlugin) Middlewares() map[string]pact.Middleware { return p.mw } +func (p bodyProbePlugin) Routes(r pact.Router) error { + open := r.Group + if p.raw { + open = r.GroupRaw + } + open("/", Use(p.use...), func(g pact.Router) { + g.Post("/x", func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusOK) }) + }) + return nil +} + +func TestBodyLimitBoundsBodyConsumingMiddleware(t *testing.T) { + type result struct { + n int + err error + } + run := func(t *testing.T, raw bool, use []string, bodyLen int, after func()) (result, *httptest.ResponseRecorder) { + t.Helper() + var res result + reader := func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + b, err := io.ReadAll(r.Body) + res = result{n: len(b), err: err} + if after != nil { + after() + } + next.ServeHTTP(w, r) + }) + } + cfg := writeHTTPConfig(t, "body_limits:\n default_bytes: 4\n upload_bytes: 8\n") + h, err := Assemble(backpack.New(cfg), []party.Plugin{bodyProbePlugin{ + id: "golem15.probe", raw: raw, use: use, + mw: map[string]pact.Middleware{"reader": reader}, + }}) + if err != nil { + t.Fatal(err) + } + rec := httptest.NewRecorder() + h.ServeHTTP(rec, httptest.NewRequest(http.MethodPost, "/x", strings.NewReader(strings.Repeat("a", bodyLen)))) + return res, rec + } + var maxErr *http.MaxBytesError + + t.Run("default limit", func(t *testing.T) { + res, _ := run(t, false, []string{"reader"}, 10, nil) + if !errors.As(res.err, &maxErr) || res.n > 4 { + t.Fatalf("read %d bytes, err = %v", res.n, res.err) + } + }) + t.Run("body.limit override raises cap", func(t *testing.T) { + res, _ := run(t, false, []string{"reader", "body.limit:6"}, 5, nil) + if res.err != nil || res.n != 5 { + t.Fatalf("read %d bytes, err = %v", res.n, res.err) + } + }) + t.Run("body.limit override still bounds", func(t *testing.T) { + res, _ := run(t, false, []string{"reader", "body.limit:6"}, 10, nil) + if !errors.As(res.err, &maxErr) || res.n > 6 { + t.Fatalf("read %d bytes, err = %v", res.n, res.err) + } + }) + t.Run("raw route unaffected", func(t *testing.T) { + res, rec := run(t, true, []string{"reader"}, 10, nil) + if res.err != nil || res.n != 10 || rec.Code != http.StatusOK { + t.Fatalf("read %d bytes, err = %v, code %d", res.n, res.err, rec.Code) + } + }) + t.Run("panic after read still clean 500", func(t *testing.T) { + _, rec := run(t, false, []string{"reader"}, 10, func() { panic("boom") }) + if rec.Code != http.StatusInternalServerError || strings.Contains(rec.Body.String(), "boom") { + t.Fatalf("code %d body %q", rec.Code, rec.Body.String()) + } + }) +} diff --git a/surf/limiter_test.go b/surf/limiter_test.go index 7990855..2ad6e01 100644 --- a/surf/limiter_test.go +++ b/surf/limiter_test.go @@ -403,3 +403,71 @@ func TestRegisterBucketRejectsInvalidDefinitions(t *testing.T) { t.Fatal("overflowing minutes accepted") } } + +func TestRegisterBucketRejectsInvalid(t *testing.T) { + key := func(*http.Request) string { return "k" } + cases := []struct { + name string + store Store + b Bucket + }{ + {"nil key", NewMemoryStore(time.Minute), Bucket{Max: 1, Decay: time.Minute}}, + {"max zero", NewMemoryStore(time.Minute), Bucket{Max: 0, Decay: time.Minute, Key: key}}, + {"max negative", NewMemoryStore(time.Minute), Bucket{Max: -1, Decay: time.Minute, Key: key}}, + {"decay zero", NewMemoryStore(time.Minute), Bucket{Max: 1, Key: key}}, + {"decay negative", NewMemoryStore(time.Minute), Bucket{Max: 1, Decay: -time.Second, Key: key}}, + {"nil store", nil, Bucket{Max: 1, Decay: time.Minute, Key: key}}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + l := NewFixedWindowLimiter(tc.store, nil) + err := l.RegisterBucket("golem15.p", "bkt", tc.b) + if err == nil || !strings.Contains(err.Error(), "golem15.p") || !strings.Contains(err.Error(), "bkt") { + t.Fatalf("err = %v", err) + } + }) + } +} + +func TestValidateThrottleRejectsOverflowAndNilStore(t *testing.T) { + l := NewFixedWindowLimiter(NewMemoryStore(time.Minute), nil) + for _, p := range []string{"1,9223372036854775807", "0,1", "1,0", "-1,1", "x,y", "nope"} { + if err := l.ValidateThrottle(p); err == nil { + t.Errorf("%q accepted", p) + } + } + if err := l.ValidateThrottle("5,1"); err != nil { + t.Fatal(err) + } + if err := NewFixedWindowLimiter(nil, nil).ValidateThrottle("5,1"); err == nil { + t.Fatal("nil store accepted") + } + var nilLim *FixedWindowLimiter + if err := nilLim.ValidateThrottle("5,1"); err == nil { + t.Fatal("nil limiter accepted") + } +} + +func TestMiddlewareFailsClosed(t *testing.T) { + var nilLim *FixedWindowLimiter + cases := map[string]*FixedWindowLimiter{ + "unknown bucket": NewFixedWindowLimiter(NewMemoryStore(time.Minute), nil), + "nil store": NewFixedWindowLimiter(nil, nil), + "nil limiter": nilLim, + } + params := map[string]string{"unknown bucket": "missing", "nil store": "5,1", "nil limiter": "5,1"} + for name, l := range cases { + t.Run(name, func(t *testing.T) { + called := false + h := l.Middleware(params[name])(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + called = true + w.WriteHeader(http.StatusNoContent) + })) + rec := httptest.NewRecorder() + h.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/", nil)) + if called || rec.Code != http.StatusInternalServerError { + t.Fatalf("called=%v code=%d", called, rec.Code) + } + }) + } +} diff --git a/surf/router_test.go b/surf/router_test.go index 7a7e6ef..8edd078 100644 --- a/surf/router_test.go +++ b/surf/router_test.go @@ -480,3 +480,55 @@ func TestDuplicateMiddlewareFactoryNamesPluginAndName(t *testing.T) { t.Fatalf("want plugin and factory name in error, got %v", err) } } + +func TestBuildRouterFailsOnMissingBodyConfig(t *testing.T) { + bad := map[string]string{ + "missing default": "body_limits:\n upload_bytes: 8\n", + "missing upload": "body_limits:\n default_bytes: 8\n", + "zero": "body_limits:\n default_bytes: 0\n upload_bytes: 8\n", + "negative": "body_limits:\n default_bytes: 4\n upload_bytes: -1\n", + "non-numeric": "body_limits:\n default_bytes: lots\n upload_bytes: 8\n", + } + for name, y := range bad { + t.Run(name, func(t *testing.T) { + if _, err := BuildRouter(backpack.New(writeHTTPConfig(t, y)), nil); err == nil { + t.Fatal("expected error") + } + }) + } + if _, err := BuildRouter(backpack.New(writeHTTPConfig(t, "body_limits:\n default_bytes: 4\n upload_bytes: 8\n")), nil); err != nil { + t.Fatal(err) + } +} + +type factoryCountPlugin struct{ calls *map[string]int } + +func (p factoryCountPlugin) ID() string { return "golem15.count" } +func (p factoryCountPlugin) Requires() []string { return nil } +func (p factoryCountPlugin) Register(*backpack.App) error { return nil } +func (p factoryCountPlugin) Boot(*backpack.App) error { return nil } +func (p factoryCountPlugin) MiddlewareFactories() map[string]func(string) pact.Middleware { + return map[string]func(string) pact.Middleware{"count": func(param string) pact.Middleware { + (*p.calls)[param]++ + return func(next http.Handler) http.Handler { return next } + }} +} +func (p factoryCountPlugin) Routes(r pact.Router) error { + r.Group("/", Use("count:a"), func(g pact.Router) { + g.Get("/one", func(http.ResponseWriter, *http.Request) {}) + g.Get("/two", func(http.ResponseWriter, *http.Request) {}) + g.Get("/three", func(http.ResponseWriter, *http.Request) {}, "count:b") + }) + return nil +} + +func TestFactoriesBuiltOncePerName(t *testing.T) { + calls := map[string]int{} + cfg := writeHTTPConfig(t, "body_limits:\n default_bytes: 4\n upload_bytes: 8\n") + if _, err := Assemble(backpack.New(cfg), []party.Plugin{factoryCountPlugin{calls: &calls}}); err != nil { + t.Fatal(err) + } + if calls["a"] != 1 || calls["b"] != 1 { + t.Fatalf("factory invocations = %v, want each name:param built once", calls) + } +}