Files
zeekayandHanzo Dev 5f71d2496b The router is three nouns, and every one of them is concrete
App, Group, Operation. A group is a prefix and what holds under it, an operation
is one typed route declared there, and the verb methods are generic methods —
which Go 1.27 allows on a concrete type and still refuses on an interface. So In
and Out come from the handler and the caller writes no type arguments:

	users := app.Group("/v1/users")
	users.Post("/", api.Create)

	mint := users.Group("/mint").With(middleware.Mint(gate))
	mint.Post("/deposit", api.Deposit)

A Group is a CLOSED HANDLE: two unexported fields, and no method that hands back
the app, the parent or the fiber underneath. A subsystem given the group at its
own prefix cannot reach above it, so confinement is a property of the type
instead of a rule something has to police — which is what a host needs in order
to hand out a bounded surface and know what comes back.

That is bought by giving up the router interface, deliberately. Router and
OpTarget are gone, and with them the ability to implement a route table outside
this package: a decorator is no longer a router, a gate is middleware on a group,
and a bound is a path. The five package-level verb functions go the same way —
a registration is a method on the thing it registers into.

Raw carries what is not a typed operation: a stream, an upgrade, a probe, a
foreign subtree. It names its method, and it is the longer spelling because
opting out of the typed model is the exception and should read like one.



Co-authored-by: Hanzo Dev <[email protected]>
2026-09-17 01:39:17 -07:00

196 lines
5.6 KiB
Go

package middleware_test
import (
"errors"
"net/http"
"sync"
"testing"
"time"
"github.com/zap-proto/zip"
"github.com/zap-proto/zip/middleware"
)
// TestBreaker_OpenAfterThreshold verifies the breaker opens after N
// consecutive failures and serves 503 thereafter.
func TestBreaker_OpenAfterThreshold(t *testing.T) {
b := middleware.NewBreaker(middleware.BreakerConfig{
FailureThreshold: 3,
OpenWindow: 50 * time.Millisecond,
})
for i := 0; i < 3; i++ {
if !b.Allow() {
t.Fatalf("iter %d: Allow returned false while closed", i)
}
b.Report(errors.New("boom"), 500)
}
if b.State() != middleware.BreakerOpen {
t.Fatalf("state=%d, want Open", b.State())
}
if b.Allow() {
t.Fatalf("Allow returned true while open")
}
}
// TestBreaker_HalfOpenAfterWindow verifies that after OpenWindow the
// breaker transitions to half-open and admits exactly one request.
func TestBreaker_HalfOpenAfterWindow(t *testing.T) {
now := time.Unix(0, 0)
nowFn := func() time.Time { return now }
b := middleware.NewBreaker(middleware.BreakerConfig{
FailureThreshold: 1,
OpenWindow: 100 * time.Millisecond,
Now: nowFn,
})
// Trip open.
if !b.Allow() {
t.Fatal("Allow false on first call")
}
b.Report(errors.New("boom"), 500)
if s := b.State(); s != middleware.BreakerOpen {
t.Fatalf("state=%d, want Open", s)
}
// Advance past OpenWindow.
now = now.Add(200 * time.Millisecond)
// First Allow in half-open should succeed.
if !b.Allow() {
t.Fatalf("first half-open Allow returned false")
}
// Second concurrent Allow should be denied.
if b.Allow() {
t.Fatalf("second concurrent half-open Allow returned true")
}
// A success closes the breaker.
b.Report(nil, 200)
if s := b.State(); s != middleware.BreakerClosed {
t.Fatalf("state after half-open success = %d, want Closed", s)
}
}
// TestBreaker_HalfOpenFailureReopens verifies a half-open failure
// re-opens the breaker.
func TestBreaker_HalfOpenFailureReopens(t *testing.T) {
now := time.Unix(0, 0)
nowFn := func() time.Time { return now }
b := middleware.NewBreaker(middleware.BreakerConfig{
FailureThreshold: 1,
OpenWindow: 100 * time.Millisecond,
Now: nowFn,
})
_ = b.Allow()
b.Report(errors.New("boom"), 500)
now = now.Add(200 * time.Millisecond)
if !b.Allow() {
t.Fatalf("half-open Allow false")
}
b.Report(errors.New("still broken"), 500)
if s := b.State(); s != middleware.BreakerOpen {
t.Fatalf("state after half-open failure = %d, want Open", s)
}
}
// TestBreaker_Middleware_HappyPath confirms the zip middleware passes
// successful requests through.
func TestBreaker_Middleware_HappyPath(t *testing.T) {
app := zip.New(zip.Config{DisableStartupMessage: true})
b := middleware.NewBreaker(middleware.BreakerConfig{FailureThreshold: 3})
app.Use(b.Middleware())
app.Raw("GET", "/ok", func(c *zip.Ctx) error {
return c.JSON(200, map[string]bool{"ok": true})
})
req, _ := http.NewRequest("GET", "/ok", nil)
resp, err := app.Fiber().Test(req)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != 200 {
t.Fatalf("status=%d, want 200", resp.StatusCode)
}
}
// TestBreaker_Middleware_ShortCircuits confirms the breaker returns
// 503 once tripped.
func TestBreaker_Middleware_ShortCircuits(t *testing.T) {
app := zip.New(zip.Config{DisableStartupMessage: true})
b := middleware.NewBreaker(middleware.BreakerConfig{
FailureThreshold: 2,
OpenWindow: time.Hour, // effectively never re-opens for the test
})
app.Use(b.Middleware())
app.Raw("GET", "/fail", func(c *zip.Ctx) error {
return zip.Errorf(500, "broken")
})
for i := 0; i < 2; i++ {
req, _ := http.NewRequest("GET", "/fail", nil)
resp, _ := app.Fiber().Test(req)
if resp.StatusCode != 500 {
t.Fatalf("iter %d: status=%d, want 500", i, resp.StatusCode)
}
}
// Next call should short-circuit with 503.
req, _ := http.NewRequest("GET", "/fail", nil)
resp, err := app.Fiber().Test(req)
if err != nil {
t.Fatal(err)
}
if resp.StatusCode != 503 {
t.Fatalf("short-circuit status=%d, want 503", resp.StatusCode)
}
}
// TestBreaker_StateChangeCallback verifies OnStateChange fires.
func TestBreaker_StateChangeCallback(t *testing.T) {
var mu sync.Mutex
var transitions []middleware.BreakerState
b := middleware.NewBreaker(middleware.BreakerConfig{
FailureThreshold: 1,
OpenWindow: 10 * time.Millisecond,
OnStateChange: func(_, next middleware.BreakerState) {
mu.Lock()
defer mu.Unlock()
transitions = append(transitions, next)
},
})
_ = b.Allow()
b.Report(errors.New("boom"), 500)
// Wait long enough for OpenWindow to elapse, then call State() to
// trigger the half-open transition.
time.Sleep(20 * time.Millisecond)
_ = b.State()
mu.Lock()
defer mu.Unlock()
if len(transitions) < 2 {
t.Fatalf("transitions=%v, want at least 2 (Open, HalfOpen)", transitions)
}
if transitions[0] != middleware.BreakerOpen || transitions[1] != middleware.BreakerHalfOpen {
t.Fatalf("transitions=%v, want [Open, HalfOpen, ...]", transitions)
}
}
// TestBreaker_Stats exposes counter semantics.
func TestBreaker_Stats(t *testing.T) {
b := middleware.NewBreaker(middleware.BreakerConfig{
FailureThreshold: 2,
OpenWindow: time.Hour,
})
_ = b.Allow()
b.Report(nil, 200)
_ = b.Allow()
b.Report(errors.New("boom"), 500)
_ = b.Allow()
b.Report(errors.New("boom"), 500)
// Now open. Two more Allow calls — both short-circuit.
_ = b.Allow()
_ = b.Allow()
s := b.Stats()
if s.Successes != 1 {
t.Fatalf("successes=%d, want 1", s.Successes)
}
if s.Failures != 2 {
t.Fatalf("failures=%d, want 2", s.Failures)
}
if s.ShortCircuited != 2 {
t.Fatalf("short_circuited=%d, want 2", s.ShortCircuited)
}
}