Files
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

273 lines
9.4 KiB
Go

package contracts
import (
"testing"
"github.com/zzet/gortex/internal/graph"
"github.com/zzet/gortex/internal/parser"
sitter "github.com/zzet/gortex/internal/parser/tsitter"
)
// fakeEndpointStore is an in-memory EndpointConstStore: a name → const-node
// index plus a node-id → literal-value map. It stands in for the graph store
// so the resolver's graph-wide const dereference can be exercised without a
// full indexer run — a "single graph built from several files" in map form.
type fakeEndpointStore struct {
nodes map[string][]*graph.Node
vals map[string]string
}
func (f *fakeEndpointStore) FindNodesByNames(names []string) map[string][]*graph.Node {
out := make(map[string][]*graph.Node, len(names))
for _, n := range names {
if v, ok := f.nodes[n]; ok {
out[n] = v
}
}
return out
}
func (f *fakeEndpointStore) ConstantValuesByNodeIDs(ids []string) (map[string]string, error) {
out := make(map[string]string, len(ids))
for _, id := range ids {
if v, ok := f.vals[id]; ok {
out[id] = v
}
}
return out, nil
}
func constStore(rows ...[3]string) *fakeEndpointStore {
// each row is {id, name, file}; values supplied separately.
s := &fakeEndpointStore{nodes: map[string][]*graph.Node{}, vals: map[string]string{}}
for _, r := range rows {
id, name, file := r[0], r[1], r[2]
s.nodes[name] = append(s.nodes[name], &graph.Node{
ID: id,
Name: name,
Kind: graph.KindConstant,
FilePath: file,
})
}
return s
}
// goCallArg parses a Go snippet and returns the argIdx'th argument node of the
// first call_expression whose function-selector field name is field. The
// returned tree owns the node's memory, so the caller must Release() it after
// use.
func goCallArg(t *testing.T, src []byte, field string, argIdx int) (*sitter.Node, *parser.ParseTree) {
t.Helper()
tree := ParseTreeForLang("go", src)
if tree == nil || tree.Tree() == nil {
t.Fatalf("ParseTreeForLang returned nil tree")
}
var found *sitter.Node
walkGoCallExprs(tree.Tree().RootNode(), func(call *sitter.Node) {
if found != nil {
return
}
fn := call.ChildByFieldName("function")
if fn == nil || fn.Type() != "selector_expression" {
return
}
f := fn.ChildByFieldName("field")
if f == nil || f.Content(src) != field {
return
}
args := namedChildren(call.ChildByFieldName("arguments"))
if argIdx < len(args) {
found = args[argIdx]
}
})
if found == nil {
tree.Release()
t.Fatalf("no call %q arg %d found", field, argIdx)
}
return found, tree
}
func TestResolveEndpointArg_StringLiteralUnchanged(t *testing.T) {
src := []byte("package p\nfunc setup(r R) { r.GET(\"/api/users\", h) }\n")
arg, tree := goCallArg(t, src, "GET", 0)
defer tree.Release()
// A plain literal resolves the same with or without a store, and is never
// gated through the route guard (forRoute has no effect on a literal).
for _, forRoute := range []bool{true, false} {
got, ok := ResolveEndpointArg(arg, src, "repo/a.go", "repo", nil, forRoute)
if !ok || got != "/api/users" {
t.Fatalf("literal forRoute=%v: got (%q,%v), want (/api/users,true)", forRoute, got, ok)
}
}
}
func TestResolveEndpointArg_ConstSameFile(t *testing.T) {
src := []byte("package p\nconst P = \"/api/x\"\nfunc setup(r R) { r.GET(P, h) }\n")
arg, tree := goCallArg(t, src, "GET", 0)
defer tree.Release()
store := constStore([3]string{"repo/a.go::P", "P", "repo/a.go"})
store.vals["repo/a.go::P"] = "/api/x"
got, ok := ResolveEndpointArg(arg, src, "repo/a.go", "repo", store, true)
if !ok || got != "/api/x" {
t.Fatalf("same-file const: got (%q,%v), want (/api/x,true)", got, ok)
}
}
func TestResolveEndpointArg_ConstCrossFile(t *testing.T) {
// The route lives in b.go; the const is declared in a.go of the SAME repo.
// A repo-wide unique value resolves — the graph-wide win over a per-file map.
src := []byte("package p\nfunc setup(r R) { r.GET(P, h) }\n")
arg, tree := goCallArg(t, src, "GET", 0)
defer tree.Release()
store := constStore([3]string{"repo/a.go::P", "P", "repo/a.go"})
store.vals["repo/a.go::P"] = "/api/x"
got, ok := ResolveEndpointArg(arg, src, "repo/b.go", "repo", store, true)
if !ok || got != "/api/x" {
t.Fatalf("cross-file const: got (%q,%v), want (/api/x,true)", got, ok)
}
}
func TestResolveEndpointArg_AmbiguousDropped(t *testing.T) {
src := []byte("package p\nfunc setup(r R) { r.GET(P, h) }\n")
arg, tree := goCallArg(t, src, "GET", 0)
defer tree.Release()
// Two distinct values for P across the repo, neither in the enclosing file.
store := constStore(
[3]string{"repo/a.go::P", "P", "repo/a.go"},
[3]string{"repo/b.go::P", "P", "repo/b.go"},
)
store.vals["repo/a.go::P"] = "/x"
store.vals["repo/b.go::P"] = "/y"
if got, ok := ResolveEndpointArg(arg, src, "repo/c.go", "repo", store, true); ok {
t.Fatalf("ambiguous const should drop, got (%q,%v)", got, ok)
}
// Same name + same value in two files is NOT ambiguous — it resolves.
store.vals["repo/b.go::P"] = "/x"
if got, ok := ResolveEndpointArg(arg, src, "repo/c.go", "repo", store, true); !ok || got != "/x" {
t.Fatalf("repo-wide identical value: got (%q,%v), want (/x,true)", got, ok)
}
// A same-file definition wins outright even when the repo is otherwise
// ambiguous: resolving from a.go takes a.go's value.
store.vals["repo/b.go::P"] = "/y"
if got, ok := ResolveEndpointArg(arg, src, "repo/a.go", "repo", store, true); !ok || got != "/x" {
t.Fatalf("same-file preference under ambiguity: got (%q,%v), want (/x,true)", got, ok)
}
}
func TestResolveEndpointArg_RouteGuardRejectsFilesystemPath(t *testing.T) {
src := []byte("package p\nfunc setup(r R) { r.GET(P, h) }\n")
arg, tree := goCallArg(t, src, "GET", 0)
defer tree.Release()
store := constStore([3]string{"repo/a.go::P", "P", "repo/a.go"})
store.vals["repo/a.go::P"] = "/etc/passwd"
// ROUTE context: the guard rejects a resolved filesystem path.
if got, ok := ResolveEndpointArg(arg, src, "repo/a.go", "repo", store, true); ok {
t.Fatalf("route guard should reject /etc/passwd, got (%q,%v)", got, ok)
}
// TOPIC context (forRoute=false): the same value resolves — the guard
// does not apply to queue/topic endpoints.
if got, ok := ResolveEndpointArg(arg, src, "repo/a.go", "repo", store, false); !ok || got != "/etc/passwd" {
t.Fatalf("topic context: got (%q,%v), want (/etc/passwd,true)", got, ok)
}
}
func TestResolveEndpointArg_CompositeQueueLiteralField(t *testing.T) {
src := []byte("package p\nfunc send(c C) { c.SendMessage(&sqs.SendMessageInput{QueueUrl: \"https://sqs/q\"}) }\n")
arg, tree := goCallArg(t, src, "SendMessage", 0)
defer tree.Release()
// The field value is a literal, so no store is required.
got, ok := ResolveEndpointArg(arg, src, "repo/a.go", "repo", nil, false)
if !ok || got != "https://sqs/q" {
t.Fatalf("composite literal field: got (%q,%v), want (https://sqs/q,true)", got, ok)
}
}
func TestResolveEndpointArg_CompositeQueueConstField(t *testing.T) {
src := []byte("package p\nfunc send(c C) { c.SendMessage(&sqs.SendMessageInput{QueueUrl: Q}) }\n")
arg, tree := goCallArg(t, src, "SendMessage", 0)
defer tree.Release()
store := constStore([3]string{"repo/a.go::Q", "Q", "repo/a.go"})
store.vals["repo/a.go::Q"] = "https://sqs/myqueue"
got, ok := ResolveEndpointArg(arg, src, "repo/a.go", "repo", store, false)
if !ok || got != "https://sqs/myqueue" {
t.Fatalf("composite const field: got (%q,%v), want (https://sqs/myqueue,true)", got, ok)
}
}
func TestHTTPExtractor_ExtractWithStore_ConstRoute(t *testing.T) {
src := []byte("package p\nconst P = \"/api/x\"\nfunc setup(r R) { r.GET(P, h) }\n")
store := constStore([3]string{"repo/a.go::P", "P", "repo/a.go"})
store.vals["repo/a.go::P"] = "/api/x"
h := &HTTPExtractor{}
got := h.ExtractWithStore("repo/a.go", src, nil, nil, nil, store, "repo")
if !hasRoute(got, "GET", "/api/x") {
t.Fatalf("expected a GET /api/x route from a const path, got %v", routeSummaries(got))
}
}
func TestHTTPExtractor_ExtractWithStore_LiteralRouteUnchanged(t *testing.T) {
src := []byte("package p\nfunc setup(r R) { r.GET(\"/api/users\", h) }\n")
h := &HTTPExtractor{}
got := h.ExtractWithStore("repo/a.go", src, nil, nil, nil, nil, "repo")
if !hasRoute(got, "GET", "/api/users") {
t.Fatalf("expected a GET /api/users literal route, got %v", routeSummaries(got))
}
}
func TestTopicExtractor_ExtractWithStore_SQSStruct(t *testing.T) {
src := []byte("package p\nconst Q = \"https://sqs.amazonaws.com/123/myqueue\"\nfunc send(c C) { c.SendMessage(&sqs.SendMessageInput{QueueUrl: Q}) }\n")
store := constStore([3]string{"repo/a.go::Q", "Q", "repo/a.go"})
store.vals["repo/a.go::Q"] = "https://sqs.amazonaws.com/123/myqueue"
e := &TopicExtractor{}
got := e.ExtractWithStore("repo/a.go", src, nil, nil, nil, store, "repo")
found := false
for _, c := range got {
if c.Type == ContractTopic && c.Role == RoleProvider &&
c.Meta["broker"] == "sqs" && c.Meta["topic"] == "https://sqs.amazonaws.com/123/myqueue" {
found = true
}
}
if !found {
t.Fatalf("expected an sqs provider topic for the resolved QueueUrl const, got %d contracts", len(got))
}
}
func hasRoute(cs []Contract, method, path string) bool {
for _, c := range cs {
if c.Type == ContractHTTP && c.Meta["method"] == method && c.Meta["path"] == path {
return true
}
}
return false
}
func routeSummaries(cs []Contract) []string {
var out []string
for _, c := range cs {
if c.Type == ContractHTTP {
m, _ := c.Meta["method"].(string)
p, _ := c.Meta["path"].(string)
out = append(out, m+" "+p)
}
}
return out
}