Files
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

118 lines
5.0 KiB
Go

package implserviceaccount
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/hanzoai/o11y/pkg/http/routing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/hanzoai/o11y/pkg/modules/serviceaccount"
"github.com/hanzoai/o11y/pkg/types/authtypes"
"github.com/hanzoai/o11y/pkg/types/serviceaccounttypes"
"github.com/hanzoai/o11y/pkg/valuer"
)
// These routes name THREE different path segments — {id}, {rid} and {fid} — and
// a handler that reads the wrong one still compiles and still returns 2xx, it
// just acts on the wrong resource. So each test drives a request whose segments
// are DISTINCT uuids and asserts which value landed where.
//
// Embedding the interface keeps the double to the methods these routes are
// allowed to reach — every other call is a nil call, which states "this route
// must not touch that".
type recordingModule struct {
serviceaccount.Module
serviceAccountID valuer.UUID
roleID valuer.UUID
factorAPIKeyID valuer.UUID
}
func (m *recordingModule) GetWithRoles(_ context.Context, _ valuer.UUID, id valuer.UUID) (*serviceaccounttypes.ServiceAccountWithRoles, error) {
m.serviceAccountID = id
return &serviceaccounttypes.ServiceAccountWithRoles{}, nil
}
func (m *recordingModule) Get(_ context.Context, _ valuer.UUID, id valuer.UUID) (*serviceaccounttypes.ServiceAccount, error) {
m.serviceAccountID = id
return &serviceaccounttypes.ServiceAccount{}, nil
}
func (m *recordingModule) DeleteRole(_ context.Context, _ valuer.UUID, id valuer.UUID, roleID valuer.UUID) error {
m.serviceAccountID, m.roleID = id, roleID
return nil
}
func (m *recordingModule) RevokeFactorAPIKey(_ context.Context, _ valuer.UUID, factorAPIKeyID valuer.UUID) error {
m.factorAPIKeyID = factorAPIKeyID
return nil
}
// servedServiceAccount drives one request through a REAL router registered at
// the template the service registers, so the segments under assertion are ones
// the router matched — injecting them by hand would prove only that the
// injector and the reader agree with each other.
func servedServiceAccount(t *testing.T, template, method, target string, pick func(serviceaccount.Handler) http.HandlerFunc) (*recordingModule, *httptest.ResponseRecorder) {
t.Helper()
module := new(recordingModule)
router := routing.Serve(method, template, http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
pick(NewHandler(module))(w, r.WithContext(authtypes.NewContextWithClaims(r.Context(), authtypes.Claims{
UserID: valuer.GenerateUUID().String(),
OrgID: valuer.GenerateUUID().String(),
})))
}))
req := httptest.NewRequest(method, target, http.NoBody)
rec := httptest.NewRecorder()
router.ServeHTTP(rec, req)
return module, rec
}
func TestGetActsOnTheServiceAccountTheRouterMatched(t *testing.T) {
want := valuer.GenerateUUID()
module, rec := servedServiceAccount(t, "/v1/o11y/service_accounts/{id}", http.MethodGet, "/v1/o11y/service_accounts/"+want.String(), func(h serviceaccount.Handler) http.HandlerFunc { return h.Get })
require.Equal(t, http.StatusOK, rec.Code, rec.Body.String())
assert.Equal(t, want, module.serviceAccountID)
}
// {id} and {rid} are two segments of one path: the account and the role it
// loses. Swapping them would revoke by the wrong identifier.
func TestDeleteRoleSeparatesTheAccountFromTheRole(t *testing.T) {
account, role := valuer.GenerateUUID(), valuer.GenerateUUID()
module, rec := servedServiceAccount(t, "/v1/o11y/service_accounts/{id}/roles/{rid}", http.MethodDelete, "/v1/o11y/service_accounts/"+account.String()+"/roles/"+role.String(), func(h serviceaccount.Handler) http.HandlerFunc { return h.DeleteRole })
require.Equal(t, http.StatusNoContent, rec.Code, rec.Body.String())
assert.Equal(t, account, module.serviceAccountID)
assert.Equal(t, role, module.roleID)
}
// Same for {id} and {fid}: the account owning the key, and the key revoked.
func TestRevokeFactorAPIKeySeparatesTheAccountFromTheKey(t *testing.T) {
account, key := valuer.GenerateUUID(), valuer.GenerateUUID()
module, rec := servedServiceAccount(t, "/v1/o11y/service_accounts/{id}/keys/{fid}", http.MethodDelete, "/v1/o11y/service_accounts/"+account.String()+"/keys/"+key.String(), func(h serviceaccount.Handler) http.HandlerFunc { return h.RevokeFactorAPIKey })
require.Equal(t, http.StatusNoContent, rec.Code, rec.Body.String())
assert.Equal(t, account, module.serviceAccountID)
assert.Equal(t, key, module.factorAPIKeyID)
}
// A segment that is not a uuid is refused before the store is reached — an
// empty or malformed id must never arrive as a lookup for "whatever the zero
// value matches".
func TestGetRefusesANonUUIDSegment(t *testing.T) {
module, rec := servedServiceAccount(t, "/v1/o11y/service_accounts/{id}", http.MethodGet, "/v1/o11y/service_accounts/not-a-uuid", func(h serviceaccount.Handler) http.HandlerFunc { return h.Get })
assert.Equal(t, http.StatusBadRequest, rec.Code)
assert.Contains(t, rec.Body.String(), "invalid uuid not-a-uuid")
assert.Equal(t, valuer.UUID{}, module.serviceAccountID, "a refused id must not reach the store")
}