package api_impl import ( "fmt" "go/ast" "go/parser" "go/token" "strings" "testing" ) // handlerLintSkip lists handlers exempt from the wiring lint entirely, with // the reason. Keep this list short: an entry here is either a known bug or // a handler whose telemetry story is intentionally different. var handlerLintSkip = map[string]string{} // handlerNoBody lists handlers that take no request body (identity comes // from the session or URL), exempt from the read() requirement only. var handlerNoBody = map[string]string{ "ProfilesImpl.Get": "identity from session; no request body", "IdentityImpl.Logout": "session cookie only; no request body", "SsoImpl.Login": "browser redirect flow; params from URL", "SsoImpl.Callback": "browser redirect flow; params from URL", } // TestHandlersWired statically verifies that every HTTP handler method is // wired through the standard helpers that produce telemetry: read (request // decode) and write/writeErr (response + error logging). A handler missing // these can fail silently — no error response, no log lines, no trace // evidence — which is exactly the bug class traces are meant to expose. func TestHandlersWired(t *testing.T) { fset := token.NewFileSet() pkgs, err := parser.ParseDir(fset, ".", nil, parser.ParseComments) if err != nil { t.Fatal(err) } for _, pkg := range pkgs { for _, file := range pkg.Files { for _, decl := range file.Decls { fn, ok := decl.(*ast.FuncDecl) if !ok || !isHandlerMethod(fn) { continue } name := handlerName(fn) if reason, ok := handlerLintSkip[name]; ok { t.Logf("skipping %s: %s", name, reason) continue } calls := calledIdents(fn) if _, noBody := handlerNoBody[name]; !noBody && !calls["read"] { t.Errorf("%s: handler never calls read() — request is not decoded or validated", name) } if !calls["write"] && !calls["writeErr"] { t.Errorf("%s: handler never calls write()/writeErr() — errors and responses are silent", name) } } } } } // isHandlerMethod matches exported methods on *XImpl receivers with the // http.HandlerFunc signature, excluding ServeMux registration methods. func isHandlerMethod(fn *ast.FuncDecl) bool { if fn.Recv == nil || !fn.Name.IsExported() || fn.Name.Name == "ServeMux" { return false } star, ok := fn.Recv.List[0].Type.(*ast.StarExpr) if !ok { return false } recv, ok := star.X.(*ast.Ident) if !ok || !strings.HasSuffix(recv.Name, "Impl") { return false } params := fn.Type.Params.List return len(params) == 2 && isSelector(params[0].Type, "http", "ResponseWriter") && isPointerTo(params[1].Type, "http", "Request") } func isSelector(expr ast.Expr, pkg, name string) bool { sel, ok := expr.(*ast.SelectorExpr) if !ok { return false } id, ok := sel.X.(*ast.Ident) return ok && id.Name == pkg && sel.Sel.Name == name } func isPointerTo(expr ast.Expr, pkg, name string) bool { star, ok := expr.(*ast.StarExpr) return ok && isSelector(star.X, pkg, name) } func handlerName(fn *ast.FuncDecl) string { recv := fn.Recv.List[0].Type.(*ast.StarExpr).X.(*ast.Ident).Name return fmt.Sprintf("%s.%s", recv, fn.Name.Name) } // calledIdents collects the names of plain function calls in the body. func calledIdents(fn *ast.FuncDecl) map[string]bool { calls := map[string]bool{} ast.Inspect(fn.Body, func(n ast.Node) bool { call, ok := n.(*ast.CallExpr) if !ok { return true } if id, ok := call.Fun.(*ast.Ident); ok { calls[id.Name] = true } return true }) return calls }