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

285 lines
9.6 KiB
Go

package contracts
import (
"testing"
"github.com/zzet/gortex/internal/graph"
)
// extractClientConsumer is a small helper: extract contracts from a non-Go
// source file and return the consumer contracts only.
func clientConsumers(cs []Contract) []Contract {
var out []Contract
for _, c := range cs {
if c.Type == ContractHTTP && c.Role == RoleConsumer {
out = append(out, c)
}
}
return out
}
// TestClientLib_Python_Requests_CrossRepo_PairsFlaskProvider is the headline
// acceptance: a Python `requests.get("/api/users")` consumer in one repo pairs
// with a Flask provider for the same route in ANOTHER repo behind a shared
// workspace — a cross-repo link with CrossRepo:true.
func TestClientLib_Python_Requests_CrossRepo_PairsFlaskProvider(t *testing.T) {
// Provider repo: a Flask route.
provSrc := []byte(`from flask import Flask
app = Flask(__name__)
@app.get('/api/users')
def list_users():
return []
`)
provNodes := makeNodes("app.py", []struct {
name string
start, end int
}{{"list_users", 6, 7}})
provCs := (&HTTPExtractor{}).Extract("app.py", provSrc, provNodes, nil)
prov := findContract(t, provCs, "http::GET::/api/users", RoleProvider)
// Consumer repo: a `requests` client call to the same route.
consSrc := []byte(`import requests
def fetch_users():
return requests.get("/api/users")
`)
consNodes := makeNodes("client.py", []struct {
name string
start, end int
}{{"fetch_users", 3, 4}})
consCs := (&HTTPExtractor{}).Extract("client.py", consSrc, consNodes, nil)
cons := findContract(t, consCs, "http::GET::/api/users", RoleConsumer)
assertMetaString(t, cons, "method", "GET")
assertMetaString(t, cons, "path", "/api/users")
// Stitch them into one registry across two repos in a shared workspace.
reg := NewRegistry()
prov.RepoPrefix = "svc-api"
prov.WorkspaceID = "acme"
prov.ProjectID = "users"
cons.RepoPrefix = "webapp"
cons.WorkspaceID = "acme"
cons.ProjectID = "users"
reg.Add(prov)
reg.Add(cons)
result := Match(reg)
if len(result.Matched) != 1 {
t.Fatalf("expected 1 cross-repo match, got %d (orphanP=%d orphanC=%d)",
len(result.Matched), len(result.OrphanProviders), len(result.OrphanConsumers))
}
m := result.Matched[0]
if !m.CrossRepo {
t.Error("expected CrossRepo:true (provider svc-api, consumer webapp)")
}
if m.Provider.RepoPrefix != "svc-api" || m.Consumer.RepoPrefix != "webapp" {
t.Errorf("wrong sides: provider=%s consumer=%s", m.Provider.RepoPrefix, m.Consumer.RepoPrefix)
}
if m.ContractID != "http::GET::/api/users" {
t.Errorf("wrong contract ID: %s", m.ContractID)
}
}
// TestClientLib_Precision_NoImport_NoConsumer proves the import-resolution gate:
// a local variable named `requests` in a file that never imports the library
// must NOT mint a consumer contract.
func TestClientLib_Precision_NoImport_NoConsumer(t *testing.T) {
src := []byte(`def fetch():
requests = make_client()
return requests.get("/api/users")
`)
nodes := makeNodes("client.py", []struct {
name string
start, end int
}{{"fetch", 1, 3}})
cs := (&HTTPExtractor{}).Extract("client.py", src, nodes, nil)
if got := clientConsumers(cs); len(got) != 0 {
t.Fatalf("local `requests` var without `import requests` must not be a consumer, got %d: %v", len(got), routeSummaries(cs))
}
}
// TestClientLib_Precision_Rust_SurfNotRegistered proves an unregistered crate
// (surf) is ignored even when its call looks like a registered one, and a bare
// `client.get` with no HTTP-client import is also ignored.
func TestClientLib_Precision_Rust_SurfNotRegistered(t *testing.T) {
src := []byte(`use surf;
async fn fetch(client: &Surf) {
surf::get("/api/users").await;
client.get("/api/orders").await;
}
`)
nodes := makeNodes("client.rs", []struct {
name string
start, end int
}{{"fetch", 3, 6}})
cs := (&HTTPExtractor{}).Extract("client.rs", src, nodes, nil)
if got := clientConsumers(cs); len(got) != 0 {
t.Fatalf("surf (unregistered) must not mint consumers, got %d: %v", len(got), routeSummaries(cs))
}
}
// TestClientLib_Python_ConstURL_ResolvesViaStore proves the URL argument is run
// through ResolveEndpointArg: a const-referenced path resolves graph-wide.
func TestClientLib_Python_ConstURL_ResolvesViaStore(t *testing.T) {
src := []byte(`import requests
URL = "/api/users"
def fetch_users():
return requests.get(URL)
`)
nodes := makeNodes("client.py", []struct {
name string
start, end int
}{{"fetch_users", 5, 6}})
store := constStore([3]string{"repo/client.py::URL", "URL", "repo/client.py"})
store.vals["repo/client.py::URL"] = "/api/users"
cs := (&HTTPExtractor{}).ExtractWithStore("repo/client.py", src, nodes, nil, nil, store, "repo")
c := findContract(t, cs, "http::GET::/api/users", RoleConsumer)
assertMetaString(t, c, "path", "/api/users")
}
// TestClientLib_Ruby_Faraday_ImportGated covers a Ruby Faraday consumer (gated
// on `require 'faraday'`) and the negative — a same-shaped call with no require.
func TestClientLib_Ruby_Faraday_ImportGated(t *testing.T) {
src := []byte(`require 'faraday'
conn = Faraday.new
conn.get('/api/users')
Faraday.get('/api/orders')
`)
nodes := []*graph.Node{}
cs := (&HTTPExtractor{}).Extract("client.rb", src, nodes, nil)
if !hasConsumer(cs, "GET", "/api/users") {
t.Errorf("expected Faraday conn.get consumer, got %v", routeSummaries(cs))
}
if !hasConsumer(cs, "GET", "/api/orders") {
t.Errorf("expected Faraday.get consumer, got %v", routeSummaries(cs))
}
// Negative: no require → no consumer.
noReq := []byte(`conn = something
conn.get('/api/users')
`)
if got := clientConsumers((&HTTPExtractor{}).Extract("client.rb", noReq, nil, nil)); len(got) != 0 {
t.Errorf("ruby without require must not mint a consumer, got %d", len(got))
}
}
// TestClientLib_PHP_Guzzle_ImportGated covers a PHP Guzzle consumer (gated on
// `use GuzzleHttp\Client` plus a `new Client()` construction).
func TestClientLib_PHP_Guzzle_ImportGated(t *testing.T) {
src := []byte(`<?php
use GuzzleHttp\Client;
$client = new Client();
$client->get('/api/users');
$client->request('POST', '/api/orders');
`)
cs := (&HTTPExtractor{}).Extract("client.php", src, nil, nil)
if !hasConsumer(cs, "GET", "/api/users") {
t.Errorf("expected Guzzle ->get consumer, got %v", routeSummaries(cs))
}
if !hasConsumer(cs, "POST", "/api/orders") {
t.Errorf("expected Guzzle ->request('POST',…) consumer, got %v", routeSummaries(cs))
}
// Negative: no `use GuzzleHttp` import → no consumer.
noUse := []byte(`<?php
$client = new Client();
$client->get('/api/users');
`)
if got := clientConsumers((&HTTPExtractor{}).Extract("client.php", noUse, nil, nil)); len(got) != 0 {
t.Errorf("php without GuzzleHttp use must not mint a consumer, got %d", len(got))
}
}
// TestClientLib_Java_RestTemplate_ImportGated covers a Java Spring RestTemplate
// consumer (gated on the RestTemplate import + a typed local variable).
func TestClientLib_Java_RestTemplate_ImportGated(t *testing.T) {
src := []byte(`import org.springframework.web.client.RestTemplate;
class UserClient {
String fetch() {
RestTemplate rt = new RestTemplate();
return rt.getForObject("/api/users", String.class);
}
}
`)
cs := (&HTTPExtractor{}).Extract("UserClient.java", src, nil, nil)
if !hasConsumer(cs, "GET", "/api/users") {
t.Errorf("expected RestTemplate getForObject consumer, got %v", routeSummaries(cs))
}
// Negative: no RestTemplate import → no consumer.
noImport := []byte(`class UserClient {
String fetch(Helper rt) {
return rt.getForObject("/api/users", String.class);
}
}
`)
if got := clientConsumers((&HTTPExtractor{}).Extract("UserClient.java", noImport, nil, nil)); len(got) != 0 {
t.Errorf("java without RestTemplate import must not mint a consumer, got %d", len(got))
}
}
// TestClientLib_Kotlin_RestTemplate_ImportGated covers a Kotlin Spring
// RestTemplate consumer (gated on the import + a constructed local val).
func TestClientLib_Kotlin_RestTemplate_ImportGated(t *testing.T) {
src := []byte(`import org.springframework.web.client.RestTemplate
fun fetch(): String {
val rt = RestTemplate()
return rt.getForObject("/api/users", String::class.java)
}
`)
cs := (&HTTPExtractor{}).Extract("Client.kt", src, nil, nil)
if !hasConsumer(cs, "GET", "/api/users") {
t.Errorf("expected Kotlin RestTemplate getForObject consumer, got %v", routeSummaries(cs))
}
noImport := []byte(`fun fetch(rt: Helper): String {
return rt.getForObject("/api/users", String::class.java)
}
`)
if got := clientConsumers((&HTTPExtractor{}).Extract("Client.kt", noImport, nil, nil)); len(got) != 0 {
t.Errorf("kotlin without RestTemplate import must not mint a consumer, got %d", len(got))
}
}
// TestClientLib_Scala_Sttp_ImportGated covers an sttp consumer (gated on the
// `import sttp.client3._` import; the URL is an sttp `uri"…"` interpolation).
func TestClientLib_Scala_Sttp_ImportGated(t *testing.T) {
src := []byte(`import sttp.client3._
val request = basicRequest.get(uri"/api/users")
`)
cs := (&HTTPExtractor{}).Extract("Client.scala", src, nil, nil)
if !hasConsumer(cs, "GET", "/api/users") {
t.Errorf("expected sttp basicRequest.get consumer, got %v", routeSummaries(cs))
}
noImport := []byte(`val request = basicRequest.get(uri"/api/users")
`)
if got := clientConsumers((&HTTPExtractor{}).Extract("Client.scala", noImport, nil, nil)); len(got) != 0 {
t.Errorf("scala without sttp import must not mint a consumer, got %d", len(got))
}
}
// hasConsumer reports whether cs contains a consumer contract with the given
// method and path.
func hasConsumer(cs []Contract, method, path string) bool {
for _, c := range cs {
if c.Type == ContractHTTP && c.Role == RoleConsumer &&
c.Meta["method"] == method && c.Meta["path"] == path {
return true
}
}
return false
}