package o11y_test import ( "net/http" "net/http/httptest" "os" "sort" "strings" "testing" "github.com/hanzoai/o11y" "github.com/zap-proto/zip" ) // THE CENSUS. This file is the arithmetic that the wildcard used to hide. // // While Mount ended in app.All("/v1/o11y/*", …) there was no way to tell a // converted route from an unconverted one from a MISSING one: the catch-all // answered all three the same. That is exactly how 83 typed ops shipped in // files nothing called — the package built, the suite passed, and every one of // those routes still fell through the wildcard. These tests are the replacement // for that catch-all's silence. // registered is every non-HEAD, non-OPTIONS route the mount actually installed. func registered(t *testing.T, app *zip.App) map[string]bool { t.Helper() got := map[string]bool{} for _, r := range app.Fiber().GetRoutes(true) { if r.Method == http.MethodHead || r.Method == http.MethodOptions { continue } got[r.Method+" "+r.Path] = true } return got } // assertRoutes proves a slice registered EXACTLY its own routes under prefix — // no more, no fewer, at the same methods. Each slice counts only its own routes, // so a slice that stops being mounted fails here rather than being quietly // answered by something else. func assertRoutes(t *testing.T, want map[string]bool, prefix string) { t.Helper() got := map[string]bool{} for route := range registered(t, mounted(t)) { _, path, _ := strings.Cut(route, " ") if path == prefix || strings.HasPrefix(path, prefix+"/") { got[route] = true } } for route := range want { if !got[route] { t.Errorf("%s is not registered", route) } } for route := range got { if !want[route] { t.Errorf("%s is registered but not in the census", route) } } } // THE WHOLE SURFACE, counted. 324 is the number of method+path pairs the o11y // runtime registers on its gorilla/mux tree (pkg/query-service/app/routes_*.go // and pkg/apiserver/o11yapiserver/*.go). Mount registers the same 324 — 318 as // typed ops that carry a named In, a named Out and their prose into the // document, 3 as named escape hatches, 3 as native service probes. // // If this number moves, one of two things happened and both need a human: a // route was added to the runtime and not named here (it would 404 in the // composed binary), or a route was named here that the runtime does not serve // (it would 404 at the runtime). Neither is visible without the count. func TestEveryRouteIsNamedAndCounted(t *testing.T) { app := mounted(t) all := registered(t, app) const ( wantTyped = 318 // typed ops: in the OpenAPI document and the MCP tool list wantHatches = 3 // mount.go mountHatches, each with its reason wantProbes = 3 // health.go livez/healthz/readyz, native with fall-through ) if len(all) != wantTyped+wantHatches+wantProbes { t.Fatalf("mount registered %d routes, want %d", len(all), wantTyped+wantHatches+wantProbes) } // The typed ops are exactly the operations zip published. Counting them off // the document rather than off the router is what makes this a payoff // measurement and not a tautology: an op that reaches the router but not the // document is the defect this whole conversion exists to remove. spec := app.OpenAPISpec() if got := len(app.MCPTools()); got != wantTyped { t.Fatalf("MCP tools = %d, want %d", got, wantTyped) } paths, ok := spec["paths"].(map[string]map[string]any) if !ok { t.Fatalf("OpenAPI paths has type %T", spec["paths"]) } ops := 0 for path, item := range paths { if !strings.HasPrefix(path, "/v1/o11y") && !strings.HasPrefix(path, "/v1/o11y/sentinel") { t.Errorf("OpenAPI published %s, which is not o11y's surface", path) } for method := range item { switch method { case "get", "post", "put", "patch", "delete": ops++ } } } if ops != wantTyped { t.Fatalf("OpenAPI operations = %d, want %d", ops, wantTyped) } } // The table, address by address. The count above says how many routes there // are; testdata/routes.txt says which, so a route that moves, disappears or // changes its spelling reads as a diff instead of as a count that still adds up. func TestRouteTableIsTheCensus(t *testing.T) { census, err := os.ReadFile("testdata/routes.txt") if err != nil { t.Fatalf("read the census: %v", err) } var got []string for route := range registered(t, mounted(t)) { got = append(got, route) } sort.Strings(got) if moved := drift(strings.Split(strings.TrimSpace(string(census)), "\n"), got); moved != "" { t.Errorf("the route table is not the one in testdata/routes.txt:\n%s", moved) } } // drift names what the census holds and the mount does not, and the reverse. func drift(want, got []string) string { held, have := map[string]bool{}, map[string]bool{} for _, w := range want { held[w] = true } for _, g := range got { have[g] = true } var out []string for _, w := range want { if !have[w] { out = append(out, " gone: "+w) } } for _, g := range got { if !held[g] { out = append(out, " new: "+g) } } return strings.Join(out, "\n") } // No route may end in a wildcard. This is the invariant the whole change buys: // a NEW un-typed route is a 404 someone has to come and justify, not a silent // fall-through into the runtime. func TestNoCatchAllRemains(t *testing.T) { for route := range registered(t, mounted(t)) { if strings.HasSuffix(route, "*") || strings.HasSuffix(route, "/+") { t.Errorf("%s is a catch-all", route) } } } // A path under /v1/o11y that nothing named is a 404 — proof the catch-all is // gone. Under the wildcard this same request reached the runtime, which is why // an unmounted slice could go unnoticed. func TestUnnamedPathIsNotServed(t *testing.T) { app := mounted(t) served := false setRuntime(t, func(w http.ResponseWriter, r *http.Request) { served = true w.WriteHeader(http.StatusOK) }) status, _ := call(t, app, member(http.MethodGet, "/v1/o11y/no-such-thing", nil)) if status != http.StatusNotFound { t.Fatalf("status=%d want 404", status) } if served { t.Fatal("an unnamed path still reached the runtime") } } // setRuntime installs a stand-in runtime for the duration of the test. func setRuntime(t *testing.T, fn http.HandlerFunc) { t.Helper() o11y.SetRuntime(o11y.Whole(fn)) t.Cleanup(func() { o11y.SetRuntime(nil) }) } // THE THREE. Each hatch is registered at its exact method and path, and each // delegates with the path UNTOUCHED — a rewrite at this seam can only ever move // a request off its own route, and a mangler is invisible to the compiler, so // this is the only thing that catches one coming back. func TestHatchesDelegateVerbatim(t *testing.T) { hatches := []struct{ method, path string }{ {http.MethodGet, "/v1/o11y/logs/livetail"}, {http.MethodGet, "/v1/o11y/query_progress"}, {http.MethodPost, "/v1/o11y/export_raw_data"}, } if len(hatches) != 3 { t.Fatalf("the census itself is wrong: %d", len(hatches)) } app := mounted(t) var saw string setRuntime(t, func(w http.ResponseWriter, r *http.Request) { saw = r.URL.Path w.WriteHeader(http.StatusOK) }) for _, h := range hatches { t.Run(h.method+" "+h.path, func(t *testing.T) { saw = "" status, body := call(t, app, member(h.method, h.path, strings.NewReader("{}"))) if status != http.StatusOK { t.Fatalf("status=%d body=%s", status, body) } if saw != h.path { t.Fatalf("runtime received %q, want %q verbatim", saw, h.path) } }) } } // A hatch answers 503 rather than 404 while no runtime is registered — the same // honest "not initialized yet" the wildcard gave, kept now that each hatch is // its own route. func TestHatchWithoutRuntimeIs503(t *testing.T) { app := mounted(t) o11y.SetRuntime(nil) resp, err := app.Test(httptest.NewRequest(http.MethodGet, "/v1/o11y/logs/livetail", nil)) if err != nil { t.Fatalf("Test: %v", err) } if resp.StatusCode != http.StatusServiceUnavailable { t.Fatalf("status=%d want 503", resp.StatusCode) } } // TestEveryOperationDeclaresItsOwnId holds what made this package's op target // unnecessary. // // An op with no declared id takes zip's shape-derived one, and that one is // qualified by the prefix of the occurrence it answers under — so registering // through a group used to publish every undeclared op as "v1.o11y.", // a rename that once cost 353 ids. A struct answering with a fixed prefix was // the way around it. Once every op states its id, a declared id survives // composition verbatim and the group is just a group; the test that measured // that gap reached zero and said so, which is why the struct is gone. // // This is the same property from the other side: the number that has to stay at // zero is the UNDECLARED one. func TestEveryOperationDeclaresItsOwnId(t *testing.T) { app := mounted(t) if err := app.Build(); err != nil { t.Fatalf("Build: %v", err) } var undeclared []string for _, op := range app.Registry() { if op.OperationID == "" { undeclared = append(undeclared, op.Method+" "+op.Path) } } if len(undeclared) != 0 { t.Errorf("%d of %d ops declare no id, so composition would rename them: %v", len(undeclared), len(app.Registry()), undeclared) } if len(app.Registry()) != 318 { t.Errorf("registry holds %d ops, want the 318 of TestEveryRouteIsNamedAndCounted", len(app.Registry())) } // The property itself, on the real mount: nothing this service publishes // carries the occurrence qualification. for _, tool := range app.MCPTools() { name, _ := tool["name"].(string) if strings.HasPrefix(name, "v1.") { t.Errorf("published name %q is prefix-qualified", name) } } }