diff options
Diffstat (limited to 'internal/api/context_audit_test.go')
| -rw-r--r-- | internal/api/context_audit_test.go | 87 |
1 files changed, 87 insertions, 0 deletions
diff --git a/internal/api/context_audit_test.go b/internal/api/context_audit_test.go new file mode 100644 index 0000000..da073f5 --- /dev/null +++ b/internal/api/context_audit_test.go @@ -0,0 +1,87 @@ +package api + +import ( + "go/ast" + "go/parser" + "go/token" + "path/filepath" + "strings" + "testing" +) + +// TestGoogleAPICallsHaveContext is a static regression guard for the +// 2026-08-04 incident: three .Do() calls in google_calendar.go accepted a +// ctx parameter but never chained .Context(ctx) into the actual Google API +// SDK builder, so the caller's timeout/cancellation silently never reached +// the network call -- one hung call wedged the production server for three +// days. Go's compiler allows this (both Context() and Do() are optional +// builder methods on the generated SDK types), so this walks the AST of +// every google_*.go file instead and fails if any .Do() call's method +// chain doesn't include a .Context(...) call anywhere in it. +// +// Scoped to files named google_*.go, not the whole package: internal/api's +// hand-rolled BaseClient (http.go) bakes ctx into the request object itself +// via http.NewRequestWithContext before calling HTTPClient.Do(req), which +// is a structurally different (and already-safe) pattern that this check +// would false-positive on if it ran there too. +func TestGoogleAPICallsHaveContext(t *testing.T) { + files, err := filepath.Glob("google_*.go") + if err != nil { + t.Fatalf("glob: %v", err) + } + + fset := token.NewFileSet() + found := 0 + for _, file := range files { + if strings.HasSuffix(file, "_test.go") { + continue + } + f, err := parser.ParseFile(fset, file, nil, 0) + if err != nil { + t.Fatalf("parse %s: %v", file, err) + } + ast.Inspect(f, 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 != "Do" { + return true + } + found++ + if !chainHasContext(sel.X) { + pos := fset.Position(call.Pos()) + t.Errorf("%s:%d: .Do() call has no .Context(ctx) anywhere in its method chain -- "+ + "cancellation and timeouts set by the caller will be silently ignored "+ + "(this is exactly the 2026-08-04 incident's root cause)", pos.Filename, pos.Line) + } + return true + }) + } + + if found == 0 { + t.Fatal("found zero .Do() calls in google_*.go -- this check may have stopped matching " + + "the code it's meant to guard (file renamed? SDK call style changed?), which would " + + "make it silently pass without checking anything") + } +} + +// chainHasContext walks backward through a method-chain expression (e.g. the +// receiver of a .Do() call) looking for a .Context(...) call anywhere in it. +func chainHasContext(expr ast.Expr) bool { + for { + call, ok := expr.(*ast.CallExpr) + if !ok { + return false + } + sel, ok := call.Fun.(*ast.SelectorExpr) + if !ok { + return false + } + if sel.Sel.Name == "Context" { + return true + } + expr = sel.X + } +} |
