a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
158 lines
4.3 KiB
Go
158 lines
4.3 KiB
Go
package contracts
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/zzet/gortex/internal/graph"
|
|
"github.com/zzet/gortex/internal/parser"
|
|
gosrc "github.com/zzet/gortex/internal/parser/tsitter/golang"
|
|
)
|
|
|
|
func TestGoRouteAST_BasicRoutes(t *testing.T) {
|
|
src := []byte(`package h
|
|
|
|
func register(mux *http.ServeMux) {
|
|
mux.HandleFunc("GET /users", listUsers)
|
|
mux.HandleFunc("/legacy", legacyHandler)
|
|
}
|
|
|
|
func chiRoutes(r chi.Router) {
|
|
r.Get("/api/users", h.List)
|
|
r.Post("/api/users", h.Create)
|
|
}
|
|
|
|
func fiberRoutes(app *fiber.App) {
|
|
app.GET("/v1/items", listItems)
|
|
app.DELETE("/v1/items/:id", deleteItem)
|
|
}
|
|
`)
|
|
tree, err := parser.ParseFile(src, gosrc.GetLanguage())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer tree.Close()
|
|
matches := detectGoRoutesAST(tree.RootNode(), src, "", "", nil)
|
|
if len(matches) != 6 {
|
|
t.Fatalf("matches: want 6, got %d (%+v)", len(matches), matches)
|
|
}
|
|
|
|
want := []struct{ method, path, framework string }{
|
|
{"GET", "/users", "net/http"},
|
|
{"ANY", "/legacy", "net/http"},
|
|
{"GET", "/api/users", "gin/echo/chi"}, // r.Get(...) → TitleCase → gin/echo/chi label
|
|
{"POST", "/api/users", "gin/echo/chi"},
|
|
{"GET", "/v1/items", "fiber"},
|
|
{"DELETE", "/v1/items/:id", "fiber"},
|
|
}
|
|
for i, w := range want {
|
|
if i >= len(matches) {
|
|
t.Errorf("missing match[%d]: %+v", i, w)
|
|
continue
|
|
}
|
|
got := matches[i]
|
|
if got.method != w.method || got.path != w.path || got.framework != w.framework {
|
|
t.Errorf("match[%d]: want %s %s (%s), got %s %s (%s)",
|
|
i, w.method, w.path, w.framework,
|
|
got.method, got.path, got.framework)
|
|
}
|
|
}
|
|
}
|
|
|
|
// THE Fiber bug fixture: `[]byte(".GET(")` must NOT produce a route.
|
|
// This is the original failure that motivated the entire spec — the
|
|
// regex at http.go:97 matched `.GET(` inside its own prefilter-marker
|
|
// source, emitting a contract whose path contained the substring
|
|
// "[]byte(". The AST detector structurally rejects it: a string
|
|
// literal inside a type-conversion expression is not a route
|
|
// registration's path argument.
|
|
func TestGoRouteAST_FiberSelfReflexiveBugStructurallyImpossible(t *testing.T) {
|
|
src := []byte(`package contracts
|
|
|
|
// httpPrefilterMarkers is the per-language substring prefilter.
|
|
var httpPrefilterMarkers = map[string][][]byte{
|
|
"go": {
|
|
[]byte("Handle"),
|
|
[]byte(".GET("), // fiber uppercase verbs
|
|
[]byte(".POST("),
|
|
[]byte(".PUT("),
|
|
[]byte(".DELETE("),
|
|
},
|
|
}
|
|
`)
|
|
tree, err := parser.ParseFile(src, gosrc.GetLanguage())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer tree.Close()
|
|
matches := detectGoRoutesAST(tree.RootNode(), src, "", "", nil)
|
|
if len(matches) != 0 {
|
|
t.Fatalf("AST detector matched non-routes: %+v", matches)
|
|
}
|
|
}
|
|
|
|
func TestGoRouteAST_ConsumerHttpGetNotMatched(t *testing.T) {
|
|
// http.Get(url) is a CONSUMER; the regex avoids it via a negative
|
|
// look-behind on `r/g/e/...`. The AST detector handles it via
|
|
// isHTTPStdlibConsumer.
|
|
src := []byte(`package h
|
|
func fetch() {
|
|
_, _ = http.Get("https://example.com")
|
|
}
|
|
`)
|
|
tree, err := parser.ParseFile(src, gosrc.GetLanguage())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer tree.Close()
|
|
matches := detectGoRoutesAST(tree.RootNode(), src, "", "", nil)
|
|
if len(matches) != 0 {
|
|
t.Fatalf("matched http.Get consumer: %+v", matches)
|
|
}
|
|
}
|
|
|
|
// HTTPExtractor end-to-end: when the tree is provided, the AST
|
|
// detector runs and produces the expected contracts; the bug fixture
|
|
// produces zero contracts.
|
|
func TestHTTPExtractor_TreeAware_BugFixtureProducesNoBogusRoutes(t *testing.T) {
|
|
src := []byte(`package contracts
|
|
var markers = [][]byte{
|
|
[]byte(".GET("),
|
|
[]byte(".POST("),
|
|
}
|
|
`)
|
|
tree, err := parser.ParseFile(src, gosrc.GetLanguage())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
pt := parser.NewParseTree(tree, src, "go")
|
|
defer pt.Release()
|
|
|
|
ext := &HTTPExtractor{}
|
|
out := ext.ExtractWithTree("contracts/markers.go", src, []*graph.Node{}, []*graph.Edge{}, pt)
|
|
for _, c := range out {
|
|
// Any route whose path contains marker text is the bug
|
|
// recurring.
|
|
if path, _ := c.Meta["path"].(string); path != "" {
|
|
if containsAny(path, []string{"[]byte(", "GET(", "POST("}) {
|
|
t.Errorf("bug-fixture produced bogus contract: id=%s path=%q", c.ID, path)
|
|
}
|
|
}
|
|
}
|
|
if t.Failed() {
|
|
t.Logf("%d contracts produced", len(out))
|
|
}
|
|
}
|
|
|
|
func containsAny(s string, substrs []string) bool {
|
|
for _, sub := range substrs {
|
|
if len(sub) > 0 && len(s) >= len(sub) {
|
|
for i := 0; i+len(sub) <= len(s); i++ {
|
|
if s[i:i+len(sub)] == sub {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return false
|
|
}
|