Files
o11y/pkg/modules/llmobs/impllmobs/handler_test.go
zeekayandHanzo Dev 3011b92ea4 routing: one router, and it is zip
gorilla/mux, otelmux, gorilla/handlers and rs/cors leave go.mod. Every route
the runtime serves is a zip route on a zip group: 233 from the apiserver tree,
134 from query-service, 367 total — the same 367 the mount publishes.

pkg/http/routing is the one registration surface. It exists because a
registration carries three facts zip has no slot for, and the tree this
replaces recovered all three PER REQUEST by asking the router what it had just
matched: the bound segments, the path template, and the route's declared
resources. mux.CurrentRoute(req).GetHandler() was type-asserted back to
handler.Handler in two separate packages to re-derive at request time what was
already known once at boot. Stating them at registration deletes both lookups.

The middleware chain is composed at the LEAF rather than registered ambient,
because Audit keys on the route template and Resource resolves the route's
declared resources — ambient middleware runs before the router has matched, so
both would read an empty route. Per-leaf composition reproduces the order the
old tree ran in exactly: match, then middleware, then handler.

Duplicate registration now panics at boot; gorilla let the first one win.
The Sentry ingest constraint becomes fiber's own guid — a UUID parse instead of
a hand-written character class. Paths keep their public brace spelling in the
source and in the table; the respelling happens in one function.

The standalone server hands its listener to the app, so streams and the
websocket upgrade run native fasthttp.

Counted, not asserted: routes_test.go in the apiserver pins 233 against the
real AddToRouter, routing_test.go pins the table against the router itself.
This repo has already shipped 83 typed ops that reached no router while the
suite stayed green — an uncalled func is legal Go, so arithmetic is the test.

Co-authored-by: Hanzo Dev <[email protected]>
2026-08-01 19:30:40 -07:00

116 lines
4.0 KiB
Go

package impllmobs
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/hanzoai/o11y/pkg/errors"
"github.com/hanzoai/o11y/pkg/http/routing"
"github.com/hanzoai/o11y/pkg/types/authtypes"
"github.com/hanzoai/o11y/pkg/types/llmobstypes"
"github.com/hanzoai/o11y/pkg/valuer"
)
// validOrgUUID is any well-formed UUID for the claims (authz consistency); the span
// views scope on the SLUG, not this.
const validOrgUUID = "0196f794-ff30-7bee-a5f4-ef5ad315715e"
func claimsCtx() context.Context {
return authtypes.NewContextWithClaims(context.Background(), authtypes.Claims{
OrgID: validOrgUUID, UserID: "user-1", Email: "u@acme", Principal: authtypes.PrincipalUser,
})
}
// TestViewRequest_TenantScopeFromHeader proves the span-view tenant boundary is set
// SERVER-SIDE from the validated X-Org-Id and cannot be spoofed by a client query
// param.
func TestViewRequest_TenantScopeFromHeader(t *testing.T) {
// A client tries to inject the tenant via ?orgSlug=evil; the header is the real
// tenant. ViewQuery.OrgSlug has no `query` tag, so the param is ignored.
r := httptest.NewRequest(http.MethodGet, "/api/observations?orgSlug=evil&traceId=t1", nil)
r.Header.Set("X-Org-Id", "acme")
_, q, err := viewRequest(claimsCtx(), r)
if err != nil {
t.Fatalf("viewRequest err: %v", err)
}
if q.OrgSlug != "acme" {
t.Fatalf("OrgSlug = %q, want acme (from validated X-Org-Id, not the ?orgSlug=evil param)", q.OrgSlug)
}
if q.TraceID != "t1" {
t.Fatalf("TraceID = %q, want t1", q.TraceID)
}
}
// TestViewRequest_FailsClosedWithoutTenant proves a span-view request with no
// gateway-asserted tenant is REFUSED — never run as an un-scoped, all-orgs query.
func TestViewRequest_FailsClosedWithoutTenant(t *testing.T) {
r := httptest.NewRequest(http.MethodGet, "/api/observations", nil) // no X-Org-Id
if _, _, err := viewRequest(claimsCtx(), r); err == nil {
t.Fatal("viewRequest must FAIL CLOSED when X-Org-Id is absent (would otherwise read every tenant)")
}
// A blank header is likewise refused.
r2 := httptest.NewRequest(http.MethodGet, "/api/observations", nil)
r2.Header.Set("X-Org-Id", " ")
if _, _, err := viewRequest(claimsCtx(), r2); err == nil {
t.Fatal("viewRequest must FAIL CLOSED on a blank X-Org-Id")
}
}
// scoreRoute is the registered template for the two per-score routes. The id is
// a PATH segment, so what has to hold is that the segment the ROUTER matched is
// the score the handler acts on — hence a real router at the real template, not
// injected vars, which would prove only that the two halves of coretypes agree.
const scoreRoute = "/v1/o11y/llm/score/{id}"
func servedScoreID(t *testing.T, target string) (valuer.UUID, error) {
t.Helper()
var (
id valuer.UUID
err error
)
router := routing.Serve(http.MethodGet, scoreRoute, http.HandlerFunc(func(_ http.ResponseWriter, req *http.Request) {
id, err = idFromPath(req)
}))
rec := httptest.NewRecorder()
router.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, target, http.NoBody))
if rec.Code != http.StatusOK {
t.Fatalf("the route did not match (%d) — the test is not exercising the read", rec.Code)
}
return id, err
}
func TestIDFromPathReadsTheMatchedSegment(t *testing.T) {
want := valuer.GenerateUUID()
id, err := servedScoreID(t, "/v1/o11y/llm/score/"+want.String())
if err != nil {
t.Fatalf("idFromPath: %v", err)
}
if id != want {
t.Fatalf("id = %s, want %s (the segment the router matched)", id, want)
}
}
// A segment that is not a uuid is refused with the module's own code, not
// answered with the zero uuid — a malformed or unread id must never reach the
// store as a lookup for whatever the zero value matches.
func TestIDFromPathRefusesANonUUIDSegment(t *testing.T) {
id, err := servedScoreID(t, "/v1/o11y/llm/score/not-a-uuid")
if err == nil {
t.Fatal("a non-uuid segment must be refused")
}
if !errors.Asc(err, llmobstypes.ErrCodeLLMObsInvalidInput) {
t.Fatalf("want the llmobs invalid-input code, got %v", err)
}
if id != (valuer.UUID{}) {
t.Fatalf("id = %s, want the zero uuid on refusal", id)
}
}