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
260 lines
7.2 KiB
Go
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)")
|
|
}
|
|
}
|