package main import ( "bytes" "context" "encoding/json" "errors" "go/ast" "go/parser" "go/token" "io" "log" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" ) func TestWriteProblemSanitizesAndCorrelates(t *testing.T) { var logs bytes.Buffer old := problemLogger problemLogger = logForTest(&logs) t.Cleanup(func() { problemLogger = old }) const id = "TESTREQUESTID" r := httptest.NewRequest(http.MethodPost, "/tools", nil) r = r.WithContext(context.WithValue(r.Context(), requestIDKey{}, id)) w := httptest.NewRecorder() internal := errors.New("dial unix /run/private/mavend.sock: bearer secret-token") writeProblem(w, r, http.StatusBadGateway, problemToolsChange, "enable failed", internal) if w.Code != http.StatusBadGateway { t.Fatalf("status = %d, want 502", w.Code) } if got := w.Header().Get("X-Request-ID"); got != id { t.Fatalf("X-Request-ID = %q, want %q", got, id) } if got := w.Header().Get("Content-Type"); !strings.HasPrefix(got, "application/problem+json") { t.Fatalf("Content-Type = %q", got) } var got problemResponse if err := json.Unmarshal(w.Body.Bytes(), &got); err != nil { t.Fatalf("decode problem: %v", err) } if got.Error != "enable failed" || got.Code != problemToolsChange || got.RequestID != id { t.Fatalf("problem = %+v", got) } if strings.Contains(w.Body.String(), "private") || strings.Contains(w.Body.String(), "secret-token") { t.Fatalf("HTTP response disclosed the wrapped error: %s", w.Body.String()) } for _, want := range []string{id, string(problemToolsChange), internal.Error()} { if !strings.Contains(logs.String(), want) { t.Errorf("server log missing %q: %s", want, logs.String()) } } } func TestRequestIDMiddlewareMintsAndIgnoresCallerID(t *testing.T) { var seen string h := withRequestID(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { seen = requestIDFromContext(r.Context()) w.WriteHeader(http.StatusNoContent) })) r := httptest.NewRequest(http.MethodGet, "/api/ping", nil) r.Header.Set("X-Request-ID", "caller-chosen") w := httptest.NewRecorder() h.ServeHTTP(w, r) if seen == "" || seen == "caller-chosen" { t.Fatalf("request ID = %q; want a server-generated value", seen) } if got := w.Header().Get("X-Request-ID"); got != seen { t.Fatalf("response request ID = %q, context ID = %q", got, seen) } } func TestEcosystemPanelPropagatesRequestID(t *testing.T) { const id = "WEBREQUESTCORRELATION" var correlation, requester string srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { correlation = r.Header.Get("X-Correlation-ID") requester = r.Header.Get("X-Requested-By") _, _ = io.WriteString(w, `[]`) })) defer srv.Close() ctx := context.WithValue(context.Background(), requestIDKey{}, id) var rows []ecoEntity if got := getEco(ctx, srv.URL, "/entities", &rows); got != "" { t.Fatalf("getEco error = %q", got) } if correlation != id || requester != "mavweb" { t.Fatalf("correlation = %q, requester = %q", correlation, requester) } } // The contract is architectural, not a convention people must remember. Keep // a syntax-level guard so a new handler cannot bypass writeProblem by adding // another http.Error call. func TestProductionHandlersUseOneProblemWriter(t *testing.T) { entries, err := os.ReadDir(".") if err != nil { t.Fatal(err) } for _, entry := range entries { name := entry.Name() if entry.IsDir() || !strings.HasSuffix(name, ".go") || strings.HasSuffix(name, "_test.go") { continue } file, err := parser.ParseFile(token.NewFileSet(), filepath.Clean(name), nil, 0) if err != nil { t.Fatalf("parse %s: %v", name, err) } ast.Inspect(file, func(n ast.Node) bool { call, ok := n.(*ast.CallExpr) if !ok { return true } sel, ok := call.Fun.(*ast.SelectorExpr) if !ok || sel.Sel.Name != "Error" { return true } pkg, ok := sel.X.(*ast.Ident) if ok && pkg.Name == "http" { t.Errorf("%s contains http.Error; use writeProblem", name) } return true }) } } func logForTest(w io.Writer) *log.Logger { return log.New(w, "", 0) }