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

260 lines
7.2 KiB
Go

package contracts
import (
"testing"
)
// TestHTTPExtractor_ClientAlias_NameSuffixMethod covers the primary
// shape: a configured wrapper whose name ends in an HTTP verb
// (apiGet/apiPost). The verb comes from the name suffix and the path
// from the first string-literal argument, producing the canonical
// http::METHOD::/path consumer contract the matcher pairs against
// providers.
func TestHTTPExtractor_ClientAlias_NameSuffixMethod(t *testing.T) {
src := []byte(`
async function loadUsers() {
const users = await apiGet('/users');
await apiPost('/users', { name: 'x' });
}
`)
nodes := makeNodes("api.ts", []struct {
name string
start, end int
}{
{"loadUsers", 2, 5},
})
ext := &HTTPExtractor{ClientAliases: []string{"apiGet", "apiPost"}}
contracts := ext.Extract("api.ts", src, nodes, nil)
byID := map[string]Contract{}
for _, c := range contracts {
byID[c.ID] = c
}
get, ok := byID["http::GET::/users"]
if !ok {
t.Fatalf("missing http::GET::/users; have: %v", keysOf(byID))
}
if get.Role != RoleConsumer {
t.Errorf("GET role = %s, want consumer", get.Role)
}
if get.Type != ContractHTTP {
t.Errorf("GET type = %s, want http", get.Type)
}
if get.Meta["framework"] != "client-alias" {
t.Errorf("GET framework = %v, want client-alias", get.Meta["framework"])
}
if get.Meta["alias"] != "apiGet" {
t.Errorf("GET alias = %v, want apiGet", get.Meta["alias"])
}
if get.SymbolID != "api.ts::loadUsers" {
t.Errorf("GET symbol = %q, want api.ts::loadUsers", get.SymbolID)
}
post, ok := byID["http::POST::/users"]
if !ok {
t.Fatalf("missing http::POST::/users; have: %v", keysOf(byID))
}
if post.Role != RoleConsumer {
t.Errorf("POST role = %s, want consumer", post.Role)
}
if post.Meta["method"] != "POST" {
t.Errorf("POST method = %v, want POST", post.Meta["method"])
}
}
// TestHTTPExtractor_ClientAlias_TwoArgMethodForm covers the generic
// (method, path) two-arg form for a suffix-less alias: apiCall('GET',
// '/users') and client.request('POST', '/orders'). The first literal is
// read as the method, the second as the path.
func TestHTTPExtractor_ClientAlias_TwoArgMethodForm(t *testing.T) {
src := []byte(`
async function go() {
await apiCall('GET', '/users');
await client.request('POST', '/orders', body);
}
`)
nodes := makeNodes("client.ts", []struct {
name string
start, end int
}{
{"go", 2, 5},
})
ext := &HTTPExtractor{ClientAliases: []string{"apiCall", "client.request"}}
contracts := ext.Extract("client.ts", src, nodes, nil)
byID := map[string]Contract{}
for _, c := range contracts {
byID[c.ID] = c
}
if c, ok := byID["http::GET::/users"]; !ok {
t.Errorf("missing http::GET::/users; have: %v", keysOf(byID))
} else if c.Role != RoleConsumer {
t.Errorf("GET role = %s, want consumer", c.Role)
}
if c, ok := byID["http::POST::/orders"]; !ok {
t.Errorf("missing http::POST::/orders; have: %v", keysOf(byID))
} else {
if c.Role != RoleConsumer {
t.Errorf("POST role = %s, want consumer", c.Role)
}
if c.Meta["alias"] != "client.request" {
t.Errorf("POST alias = %v, want client.request", c.Meta["alias"])
}
}
}
// TestHTTPExtractor_ClientAlias_ParametricPath asserts a template-literal
// path with an inline placeholder normalises to the positional {p1}
// form so the alias-derived consumer pairs with a provider's parametric
// route ID — the whole point of the canonical scheme.
func TestHTTPExtractor_ClientAlias_ParametricPath(t *testing.T) {
src := []byte("async function del(id) {\n" +
" await apiDelete(`/users/${id}`);\n" +
"}\n")
nodes := makeNodes("api.ts", []struct {
name string
start, end int
}{
{"del", 1, 3},
})
ext := &HTTPExtractor{ClientAliases: []string{"apiDelete"}}
contracts := ext.Extract("api.ts", src, nodes, nil)
byID := map[string]Contract{}
for _, c := range contracts {
byID[c.ID] = c
}
c, ok := byID["http::DELETE::/users/{p1}"]
if !ok {
t.Fatalf("missing http::DELETE::/users/{p1}; have: %v", keysOf(byID))
}
if c.Meta["path"] != "/users/{p1}" {
t.Errorf("path = %v, want /users/{p1}", c.Meta["path"])
}
}
// TestHTTPExtractor_ClientAlias_WholeIdentifierOnly guards against an
// alias matching a longer identifier it's a suffix of: alias "apiGet"
// must not fire on a call to "myApiGet". Only the exact whole-name call
// counts.
func TestHTTPExtractor_ClientAlias_WholeIdentifierOnly(t *testing.T) {
src := []byte(`
function f() {
myApiGet('/should-not-match');
apiGet('/should-match');
}
`)
nodes := makeNodes("api.ts", []struct {
name string
start, end int
}{
{"f", 2, 5},
})
ext := &HTTPExtractor{ClientAliases: []string{"apiGet"}}
contracts := ext.Extract("api.ts", src, nodes, nil)
byID := map[string]Contract{}
for _, c := range contracts {
byID[c.ID] = c
}
if _, ok := byID["http::GET::/should-not-match"]; ok {
t.Errorf("alias apiGet wrongly matched myApiGet; have: %v", keysOf(byID))
}
if _, ok := byID["http::GET::/should-match"]; !ok {
t.Errorf("alias apiGet missed the exact call; have: %v", keysOf(byID))
}
}
// TestHTTPExtractor_ClientAlias_Disabled confirms the alias pass is a
// no-op when no aliases are configured — existing detection behaviour is
// unchanged for a plain non-fetch/non-axios wrapper call.
func TestHTTPExtractor_ClientAlias_Disabled(t *testing.T) {
src := []byte(`
function f() {
apiGet('/users');
}
`)
nodes := makeNodes("api.ts", []struct {
name string
start, end int
}{
{"f", 2, 4},
})
ext := &HTTPExtractor{} // no ClientAliases
contracts := ext.Extract("api.ts", src, nodes, nil)
for _, c := range contracts {
if c.Meta["framework"] == "client-alias" {
t.Errorf("alias pass ran with no aliases configured: %+v", c)
}
}
}
// TestHTTPExtractor_ClientAlias_PairsWithProvider is the end-to-end pin:
// an alias-derived consumer in one repo pairs with a real provider in a
// sibling repo because both mint the same http::GET::/users ID.
func TestHTTPExtractor_ClientAlias_PairsWithProvider(t *testing.T) {
providerSrc := []byte(`package main
import "net/http"
func wire(mux *http.ServeMux, h *Handler) {
mux.HandleFunc("GET /users", h.ListUsers)
}
`)
providerNodes := makeNodes("server.go", []struct {
name string
start, end int
}{
{"wire", 5, 7},
})
consumerSrc := []byte(`
async function loadUsers() {
return apiGet('/users');
}
`)
consumerNodes := makeNodes("api.ts", []struct {
name string
start, end int
}{
{"loadUsers", 2, 4},
})
prov := (&HTTPExtractor{}).Extract("server.go", providerSrc, providerNodes, nil)
cons := (&HTTPExtractor{ClientAliases: []string{"apiGet"}}).Extract("api.ts", consumerSrc, consumerNodes, nil)
reg := NewRegistry()
for _, c := range prov {
c.RepoPrefix, c.WorkspaceID, c.ProjectID = "core", "ws", "ws"
reg.Add(c)
}
for _, c := range cons {
c.RepoPrefix, c.WorkspaceID, c.ProjectID = "web", "ws", "ws"
reg.Add(c)
}
result := Match(reg)
var paired *CrossLink
for i, m := range result.Matched {
if m.ContractID == "http::GET::/users" {
paired = &result.Matched[i]
break
}
}
if paired == nil {
t.Fatalf("expected http::GET::/users pair; matched=%d orphan_p=%d orphan_c=%d",
len(result.Matched), len(result.OrphanProviders), len(result.OrphanConsumers))
}
if !paired.CrossRepo {
t.Errorf("expected CrossRepo=true (provider core, consumer web)")
}
}