mirror of
https://github.com/hanzoai/o11y.git
synced 2026-10-15 12:58:33 +00:00
One word stood for several: the MCP server, one address that accepts requests, the single way in, and the boundary a credential is validated at. Doc comments here are lifted into OpenAPI descriptions and republished on docs.hanzo.ai, so the ambiguity was reaching customers. Each site now names its own sense — endpoint, entry point, the MCP server, the edge, address — or takes the word the surrounding code already uses. Comments and prose strings only. Identifiers keep their names, including the doc comments that must open with the identifier they document. go build ./... passes.
275 lines
10 KiB
Go
275 lines
10 KiB
Go
package routing_test
|
|
|
|
import (
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/hanzoai/o11y/pkg/http/handler"
|
|
"github.com/hanzoai/o11y/pkg/http/routing"
|
|
"github.com/hanzoai/o11y/pkg/types/coretypes"
|
|
"github.com/zap-proto/fiber/v3/middleware/adaptor"
|
|
"github.com/zap-proto/zip"
|
|
)
|
|
|
|
// THE CENSUS. Every registration must reach the router, and every route on the
|
|
// router must have come from a registration. This repo has already shipped the
|
|
// other outcome once: three slices of typed ops landed with their files present
|
|
// and nothing calling them, so 83 routes existed in the source and reached no
|
|
// router at all — the package built and the whole suite passed while every one of
|
|
// them stayed dark. An uncalled func is legal Go, so arithmetic is the only thing
|
|
// that catches it. That is what the Table is for and this is the test of it.
|
|
func TestTableAndRouterAgree(t *testing.T) {
|
|
app := zip.New(zip.Config{DisableStartupMessage: true})
|
|
r := routing.New(app.Group(""), nil)
|
|
|
|
r.Get("/v1/o11y/rules", nothing())
|
|
r.Post("/v1/o11y/rules", nothing())
|
|
r.Get("/v1/o11y/rules/{id}", nothing())
|
|
r.Put("/v1/o11y/rules/{id}", nothing())
|
|
r.Patch("/v1/o11y/rules/{id}", nothing())
|
|
r.Delete("/v1/o11y/rules/{id}", nothing())
|
|
pods := r.Group("/v1/o11y/pods")
|
|
pods.Get("/attribute_keys", nothing())
|
|
pods.Post("/list", nothing())
|
|
|
|
want := map[string]bool{
|
|
"GET /v1/o11y/rules": true,
|
|
"POST /v1/o11y/rules": true,
|
|
"GET /v1/o11y/rules/{id}": true,
|
|
"PUT /v1/o11y/rules/{id}": true,
|
|
"PATCH /v1/o11y/rules/{id}": true,
|
|
"DELETE /v1/o11y/rules/{id}": true,
|
|
"GET /v1/o11y/pods/attribute_keys": true,
|
|
"POST /v1/o11y/pods/list": true,
|
|
}
|
|
|
|
got := map[string]bool{}
|
|
for _, route := range r.Table().Routes() {
|
|
got[route.Method+" "+route.Path] = true
|
|
}
|
|
for route := range want {
|
|
if !got[route] {
|
|
t.Errorf("%s is not in the table", route)
|
|
}
|
|
}
|
|
for route := range got {
|
|
if !want[route] {
|
|
t.Errorf("%s is in the table but not in the census", route)
|
|
}
|
|
}
|
|
|
|
// And the router itself holds exactly those, in the router's own spelling.
|
|
onRouter := map[string]bool{}
|
|
for _, route := range app.Fiber().GetRoutes(true) {
|
|
if route.Method == http.MethodHead || route.Method == http.MethodOptions {
|
|
continue
|
|
}
|
|
onRouter[route.Method+" "+route.Path] = true
|
|
}
|
|
if len(onRouter) != len(want) {
|
|
t.Fatalf("router holds %d routes, the table holds %d", len(onRouter), len(want))
|
|
}
|
|
for route := range want {
|
|
method, path, _ := strings.Cut(route, " ")
|
|
spelled := method + " " + strings.ReplaceAll(strings.ReplaceAll(path, "{", ":"), "}", "")
|
|
if !onRouter[spelled] {
|
|
t.Errorf("%s is in the table but %s is not on the router", route, spelled)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A route registered twice is a mistake in the source, and the tree this
|
|
// replaces answered it by silently letting the first one win.
|
|
func TestDuplicateRegistrationPanics(t *testing.T) {
|
|
defer func() {
|
|
if recover() == nil {
|
|
t.Fatal("a second registration at the same address did not panic")
|
|
}
|
|
}()
|
|
app := zip.New(zip.Config{DisableStartupMessage: true})
|
|
r := routing.New(app.Group(""), nil)
|
|
r.Get("/v1/o11y/rules", nothing())
|
|
r.Get("/v1/o11y/rules", nothing())
|
|
}
|
|
|
|
// The handler reads the segment the ROUTER matched, through the one seam, and
|
|
// the template it reads back is the registered literal rather than the request.
|
|
func TestRouteValuesReachTheHandler(t *testing.T) {
|
|
var id, template string
|
|
served := routing.Serve(http.MethodGet, "/v1/o11y/rules/{id}", http.HandlerFunc(func(_ http.ResponseWriter, req *http.Request) {
|
|
id, template = coretypes.Param(req, "id"), coretypes.RoutePath(req)
|
|
}))
|
|
|
|
rec := httptest.NewRecorder()
|
|
served.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/v1/o11y/rules/7?id=99", http.NoBody))
|
|
|
|
if rec.Code != http.StatusOK {
|
|
t.Fatalf("status=%d — the route did not match, so nothing was proved", rec.Code)
|
|
}
|
|
if id != "7" {
|
|
t.Errorf("Param(id)=%q want 7 (a query value of the same name must not answer)", id)
|
|
}
|
|
if template != "/v1/o11y/rules/{id}" {
|
|
t.Errorf("RoutePath=%q want the registered template", template)
|
|
}
|
|
}
|
|
|
|
// BY NAME AND BY MATCHING ARE THE SAME ANSWER, and that is the property the
|
|
// seam rests on.
|
|
//
|
|
// A handler reached by matching is handed its route by the router that matched
|
|
// it. A handler reached through [Table.Handler] has no router in front of it —
|
|
// the caller named the address instead of asking one to find it — so the two
|
|
// chain members that need the route (Audit keys its record on the template,
|
|
// Resource resolves the route's declared resources) would read an empty one, and
|
|
// would do it SILENTLY: an audit record keyed on "" is still a record, and
|
|
// Param("id") returning "" is still a string. Nothing would go red.
|
|
//
|
|
// So both paths are driven here, against one registration, and compared to each
|
|
// other. Comparing them to a written-down expectation would pin what someone
|
|
// believed; comparing them to each other pins what the seam promises.
|
|
func TestByNameCarriesTheSameRouteAsMatching(t *testing.T) {
|
|
type reading struct {
|
|
id, template string
|
|
wrapped bool
|
|
}
|
|
var got reading
|
|
read := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
|
got.id, got.template = coretypes.Param(req, "id"), coretypes.RoutePath(req)
|
|
w.WriteHeader(http.StatusNoContent)
|
|
})
|
|
chain := func([]handler.ResourceDef) func(http.Handler) http.Handler {
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
|
got.wrapped = true
|
|
next.ServeHTTP(w, req)
|
|
})
|
|
}
|
|
}
|
|
|
|
app := zip.New(zip.Config{DisableStartupMessage: true})
|
|
r := routing.New(app.Group(""), chain)
|
|
r.Get("/v1/o11y/rules/{id}", read)
|
|
|
|
got = reading{}
|
|
adaptor.FiberApp(app.Fiber()).ServeHTTP(
|
|
httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/v1/o11y/rules/7", http.NoBody))
|
|
byMatching := got
|
|
|
|
byName := r.Table().Handler(http.MethodGet, "/v1/o11y/rules/{id}")
|
|
if byName == nil {
|
|
t.Fatal("the registration does not resolve by name, so the seam has nothing to reach")
|
|
}
|
|
got = reading{}
|
|
byName.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/v1/o11y/rules/7", http.NoBody))
|
|
|
|
if byMatching.id == "" || byMatching.template == "" || !byMatching.wrapped {
|
|
t.Fatalf("matching itself read %+v — the control did not run, so nothing is being compared", byMatching)
|
|
}
|
|
if got != byMatching {
|
|
t.Errorf("by name the route reads %+v, by matching %+v", got, byMatching)
|
|
}
|
|
}
|
|
|
|
// An address nothing registered resolves to nothing, which is what lets the seam
|
|
// say "this declaration has drifted" instead of handing the request to a router
|
|
// that answers 404 for either reason.
|
|
func TestUnregisteredAddressResolvesToNothing(t *testing.T) {
|
|
app := zip.New(zip.Config{DisableStartupMessage: true})
|
|
r := routing.New(app.Group(""), nil)
|
|
r.Get("/v1/o11y/rules/{id}", nothing())
|
|
|
|
for _, address := range []struct{ method, path string }{
|
|
{http.MethodPost, "/v1/o11y/rules/{id}"}, // registered, other method
|
|
{http.MethodGet, "/v1/o11y/rules/{ruleId}"}, // same shape, other segment NAME
|
|
{http.MethodGet, "/v1/o11y/rules/:id"}, // the router's spelling, not the public one
|
|
} {
|
|
if r.Table().Handler(address.method, address.path) != nil {
|
|
t.Errorf("%s %s resolves, and nothing registered it", address.method, address.path)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A constrained segment only matches values it accepts: the two Sentry ingest
|
|
// routes take a UUID project and nothing else, so a segment that cannot be a
|
|
// project id is a 404 from the router rather than a body handed onward.
|
|
func TestConstrainedSegmentRefusesAValueItDoesNotAccept(t *testing.T) {
|
|
served := routing.Serve(http.MethodPost, "/v1/event/{project:guid}/envelope/", nothing())
|
|
|
|
for path, want := range map[string]int{
|
|
"/v1/event/6ba7b810-9dad-11d1-80b4-00c04fd430c8/envelope/": http.StatusNoContent,
|
|
"/v1/event/anything/envelope/": http.StatusNotFound,
|
|
} {
|
|
rec := httptest.NewRecorder()
|
|
served.ServeHTTP(rec, httptest.NewRequest(http.MethodPost, path, http.NoBody))
|
|
if rec.Code != want {
|
|
t.Errorf("POST %s = %d, want %d", path, rec.Code, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The registered path keeps its public spelling in the table even when the route
|
|
// carries a constraint the router needs — the document and the census name what a
|
|
// client writes, not what the matcher reads.
|
|
func TestTableRecordsThePublicSpelling(t *testing.T) {
|
|
app := zip.New(zip.Config{DisableStartupMessage: true})
|
|
r := routing.New(app.Group(""), nil)
|
|
r.Post("/v1/event/{project:guid}/envelope/", nothing())
|
|
|
|
if got := r.Table().Routes()[0].Path; got != "/v1/event/{project}/envelope/" {
|
|
t.Fatalf("table records %q, want the constraint dropped", got)
|
|
}
|
|
}
|
|
|
|
// The chain wraps every leaf, and it is handed the route's OWN declared
|
|
// resources — the fact the router knows and the middleware used to re-derive by
|
|
// asking the router what it had just matched.
|
|
func TestChainWrapsEachLeafWithItsOwnDefs(t *testing.T) {
|
|
var seen []int
|
|
chain := func(defs []handler.ResourceDef) func(http.Handler) http.Handler {
|
|
return func(next http.Handler) http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
|
seen = append(seen, len(defs))
|
|
next.ServeHTTP(w, req)
|
|
})
|
|
}
|
|
}
|
|
|
|
app := zip.New(zip.Config{DisableStartupMessage: true})
|
|
r := routing.New(app.Group(""), chain)
|
|
r.Get("/v1/o11y/bare", nothing())
|
|
r.Get("/v1/o11y/declared", handler.New(func(w http.ResponseWriter, _ *http.Request) {
|
|
w.WriteHeader(http.StatusNoContent)
|
|
}, handler.OpenAPIDef{ID: "Declared"}, handler.WithResourceDefs(handler.BasicResourceDef{})))
|
|
|
|
served := adaptor.FiberApp(app.Fiber())
|
|
for _, path := range []string{"/v1/o11y/bare", "/v1/o11y/declared"} {
|
|
served.ServeHTTP(httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, path, http.NoBody))
|
|
}
|
|
|
|
if len(seen) != 2 || seen[0] != 0 || seen[1] != 1 {
|
|
t.Fatalf("chain saw defs %v, want [0 1]", seen)
|
|
}
|
|
}
|
|
|
|
func nothing() http.Handler {
|
|
return http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.WriteHeader(http.StatusNoContent) })
|
|
}
|
|
|
|
// A constraint the router does not know is DROPPED by it, and a dropped
|
|
// constraint matches everything — so the segment it was guarding starts
|
|
// accepting any word at all. The router is silent about this; the registrar is
|
|
// not.
|
|
func TestUnknownConstraintPanics(t *testing.T) {
|
|
defer func() {
|
|
if recover() == nil {
|
|
t.Fatal("a constraint the router would drop was accepted")
|
|
}
|
|
}()
|
|
app := zip.New(zip.Config{DisableStartupMessage: true})
|
|
routing.New(app.Group(""), nil).
|
|
Post("/v1/event/{project:[0-9a-fA-F]{8}}/envelope/", nothing())
|
|
}
|