@shortlink-org/portolan 0.2.4 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +174 -6
- package/catalog/enum_test.go +46 -0
- package/catalog/evidence_test.go +35 -0
- package/catalog/model.go +1213 -0
- package/catalog/roundtrip_test.go +210 -0
- package/catalog/via_test.go +38 -0
- package/cli/init.test.mjs +6 -1
- package/cli/portolan.mjs +14 -1
- package/cli/portolan.test.mjs +49 -0
- package/go.mod +21 -0
- package/go.sum +34 -0
- package/internal/gocall/README.md +19 -0
- package/internal/gocall/analyze.go +189 -0
- package/internal/gocall/analyze_test.go +107 -0
- package/internal/gohttp/analyze.go +2562 -0
- package/internal/gohttp/destination.go +373 -0
- package/internal/gohttp/endpoints.go +1067 -0
- package/internal/gohttp/roots.go +320 -0
- package/internal/gohttp/typed.go +96 -0
- package/internal/goscan/constants.go +85 -0
- package/internal/goscan/goscan_test.go +227 -0
- package/internal/goscan/index.go +629 -0
- package/internal/goscan/index_test.go +66 -0
- package/internal/goscan/names.go +52 -0
- package/internal/goscan/parse_test.go +11 -0
- package/internal/goscan/source.go +37 -0
- package/internal/goscan/tree.go +284 -0
- package/internal/goscan/types.go +99 -0
- package/internal/wsdl/ids.go +127 -0
- package/internal/wsdl/ids_test.go +21 -0
- package/internal/wsdl/model.go +70 -0
- package/internal/wsdl/parse.go +949 -0
- package/internal/wsdl/parse_test.go +170 -0
- package/package.json +28 -15
- package/plugin/describe.go +156 -0
- package/plugin/describe_test.go +114 -0
- package/plugin/protocol.go +141 -0
- package/plugin/schematest/schematest.go +126 -0
- package/plugins/README.md +404 -50
- package/plugins/cmd/portolan-http-clients/main.go +19 -0
- package/plugins/extract-argocd/options.schema.json +44 -0
- package/plugins/extract-celery/extract.py +0 -2
- package/plugins/extract-celery/extract_test.py +1 -1
- package/plugins/extract-celery/main.py +1 -0
- package/plugins/extract-csharp-ddd/README.md +213 -0
- package/plugins/extract-csharp-ddd/options.schema.json +55 -0
- package/plugins/extract-django/README.md +52 -19
- package/plugins/extract-django/choices.py +47 -0
- package/plugins/extract-django/domain.py +30 -18
- package/plugins/extract-django/extract.py +21 -7
- package/plugins/extract-django/extract_test.py +68 -2
- package/plugins/extract-django/lifecycle.py +4 -28
- package/plugins/extract-django/main.py +1 -0
- package/plugins/extract-django/operations.py +1 -1
- package/plugins/extract-django/routing_test.py +109 -1
- package/plugins/extract-django/rules.py +154 -0
- package/plugins/extract-django/rules_test.py +158 -0
- package/plugins/extract-django/store.py +1 -1
- package/plugins/extract-django/transport.py +101 -55
- package/plugins/extract-django/verbs.py +241 -0
- package/plugins/extract-go/README.md +47 -0
- package/plugins/extract-go-sqs/options.schema.json +21 -0
- package/plugins/extract-http-clients/describe.go +20 -0
- package/plugins/extract-http-clients/describe_test.go +11 -0
- package/plugins/extract-http-clients/extract.go +740 -0
- package/plugins/extract-http-clients/extract_test.go +1561 -0
- package/plugins/extract-http-clients/main.go +41 -0
- package/plugins/extract-java/build/org/portolan/extract/Extract.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Main.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Protocol$Builder.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Protocol$Input.class +0 -0
- package/plugins/extract-java/build/org/portolan/extract/Protocol$Options.class +0 -0
- package/plugins/extract-k8s/options.schema.json +32 -0
- package/plugins/extract-laravel/Cargo.lock +962 -0
- package/plugins/extract-laravel/Cargo.toml +20 -0
- package/plugins/extract-laravel/README.md +200 -0
- package/plugins/extract-laravel/options.schema.json +68 -0
- package/plugins/extract-laravel/rustfmt.toml +1 -0
- package/plugins/extract-php-ddd/Cargo.lock +972 -0
- package/plugins/extract-php-ddd/Cargo.toml +22 -0
- package/plugins/extract-php-ddd/README.md +141 -0
- package/plugins/extract-php-ddd/options.schema.json +50 -0
- package/plugins/extract-php-ddd/rustfmt.toml +1 -0
- package/plugins/extract-python-kafka/extract.py +0 -2
- package/plugins/extract-python-kafka/extract_test.py +1 -1
- package/plugins/extract-python-kafka/main.py +1 -0
- package/plugins/extract-terraform/options.schema.json +26 -0
- package/plugins/extract-ts/extract.test.ts +2 -2
- package/plugins/extract-ts/extract.ts +4 -5
- package/plugins/extract-ts/graphql.test.ts +1 -1
- package/plugins/extract-ts/main.ts +1 -0
- package/plugins/openapi/ids.go +261 -0
- package/plugins/openapi/ids_test.go +98 -0
- package/plugins/phpscan/Cargo.lock +953 -0
- package/plugins/phpscan/Cargo.toml +19 -0
- package/plugins/phpscan/rustfmt.toml +1 -0
- package/plugins/portolan-go.wasm +0 -0
- package/plugins/pyplugin/catalog.py +24 -1
- package/plugins/pyplugin/protocol.py +1 -5
- package/plugins/verify-otel/options.schema.json +12 -0
- package/portolan.json +132 -4
- package/rules/builtin.json +218 -0
- package/schema/portolan.schema.json +905 -4
- package/scripts/README.md +21 -13
- package/scripts/catalog-sources.mjs +6 -0
- package/scripts/delivery-presets.mjs +57 -14
- package/scripts/diff.mjs +5 -1
- package/scripts/django-aggregates.test.mjs +58 -0
- package/scripts/gen-likec4.mjs +150 -17
- package/scripts/gen-likec4.test.mjs +96 -0
- package/scripts/gen.mjs +148 -118
- package/scripts/gitops-example.test.mjs +108 -0
- package/scripts/go-discovery.test.mjs +30 -0
- package/scripts/history.mjs +186 -3
- package/scripts/history.test.mjs +1 -1
- package/scripts/host-plugins/fetch-argocd.mjs +338 -0
- package/scripts/host-plugins/fetch-argocd.options.json +49 -0
- package/scripts/host-plugins/fetch-argocd.test.mjs +274 -0
- package/scripts/host-plugins/fetch-bsr.mjs +1 -0
- package/scripts/host-plugins/fetch-csr.mjs +1 -0
- package/scripts/host-plugins/fetch-git.mjs +78 -21
- package/scripts/host-plugins/fetch-git.test.mjs +62 -8
- package/scripts/host-plugins/fetch-k8s.mjs +263 -0
- package/scripts/host-plugins/fetch-k8s.options.json +50 -0
- package/scripts/host-plugins/fetch-k8s.test.mjs +259 -0
- package/scripts/host-plugins/k8s-topology.mjs +183 -0
- package/scripts/json-format.mjs +192 -0
- package/scripts/json-format.test.mjs +97 -0
- package/scripts/local-api.mjs +287 -12
- package/scripts/local-api.test.mjs +150 -5
- package/scripts/local-discovery.mjs +94 -9
- package/scripts/manifest.mjs +14 -3
- package/scripts/manifest.test.mjs +24 -0
- package/scripts/output-diff.mjs +94 -0
- package/scripts/output-diff.test.mjs +36 -0
- package/scripts/package-smoke.mjs +62 -4
- package/scripts/plugin-host.mjs +45 -3
- package/scripts/plugin-host.test.mjs +9 -0
- package/scripts/plugin-wasm-worker.mjs +4 -1
- package/scripts/plugins-fresh.mjs +97 -0
- package/scripts/plugins-fresh.test.mjs +64 -0
- package/scripts/provenance.mjs +72 -0
- package/scripts/provenance.test.mjs +149 -0
- package/scripts/run-builtin.mjs +62 -7
- package/scripts/schema.mjs +167 -19
- package/scripts/trace-trials.mjs +176 -0
- package/scripts/trace-trials.test.mjs +142 -0
- package/scripts/warning-policy.mjs +167 -0
- package/scripts/warning-policy.test.mjs +93 -0
- package/src/app/Breadcrumbs.test.ts +3 -0
- package/src/app/Breadcrumbs.tsx +3 -0
- package/src/app/CatalogApp.tsx +2 -0
- package/src/app/Sidebar.tsx +3 -3
- package/src/app/SidebarFooter.tsx +20 -4
- package/src/catalog-docs.test.ts +64 -0
- package/src/catalog-docs.ts +35 -0
- package/src/catalog-error.test.ts +15 -0
- package/src/catalog-index.ts +25 -0
- package/src/catalog-model.ts +293 -5
- package/src/catalog-validation.ts +113 -2
- package/src/catalog.test.ts +40 -1
- package/src/chat/Starter.tsx +5 -11
- package/src/chat/tools.test.ts +27 -0
- package/src/chat/tools.ts +5 -9
- package/src/components/CatalogStamp.tsx +10 -8
- package/src/components/ChannelRows.test.tsx +76 -1
- package/src/components/ChannelRows.tsx +223 -60
- package/src/components/DeploymentRows.tsx +131 -0
- package/src/components/DocsLinks.test.tsx +27 -0
- package/src/components/DocsLinks.tsx +56 -0
- package/src/components/FieldTree.tsx +5 -2
- package/src/components/HTTPDestinationEvidence.test.tsx +23 -0
- package/src/components/HTTPDestinationEvidence.tsx +31 -0
- package/src/components/Integrations.tsx +1 -1
- package/src/components/MachineDocs.tsx +6 -5
- package/src/components/MethodRows.tsx +9 -2
- package/src/components/PluginIcon.tsx +77 -0
- package/src/components/ProblemRow.tsx +173 -153
- package/src/components/RelationEvidence.test.tsx +14 -0
- package/src/components/RelationEvidence.tsx +53 -0
- package/src/components/RuleMarks.tsx +22 -0
- package/src/components/ShapeRows.tsx +24 -20
- package/src/data.ts +46 -7
- package/src/enrich.test.ts +459 -4
- package/src/enrich.ts +308 -7
- package/src/er/ErCanvas.tsx +218 -12
- package/src/er/GroupNode.tsx +57 -0
- package/src/er/StoreHeader.tsx +1 -0
- package/src/er/layout.test.ts +85 -2
- package/src/er/layout.ts +140 -5
- package/src/er/spec.test.ts +17 -0
- package/src/er/spec.ts +23 -10
- package/src/flow/Recordings.test.tsx +52 -0
- package/src/flow/Recordings.tsx +236 -0
- package/src/flow/StepDetail.tsx +59 -0
- package/src/flow/TraceTrial.tsx +419 -0
- package/src/flow/evidence.test.ts +16 -0
- package/src/flow/evidence.ts +34 -0
- package/src/flow/examples.test.ts +33 -0
- package/src/flow/examples.ts +37 -0
- package/src/flow/outline.test.ts +28 -0
- package/src/flow/outline.ts +7 -2
- package/src/flow/trace-trial-resume.test.ts +42 -0
- package/src/flow/trace-trial-resume.ts +74 -0
- package/src/graph/elk.ts +78 -0
- package/src/index.css +68 -0
- package/src/landing/DraggableReveal.tsx +3 -2
- package/src/landing/EvidencePipeline.tsx +105 -0
- package/src/landing/LandingPage.tsx +17 -70
- package/src/landing/ProductTour.tsx +6 -6
- package/src/lib/all-problems.ts +27 -17
- package/src/lib/catalog-diff.ts +1 -1
- package/src/lib/centrality.test.ts +251 -0
- package/src/lib/centrality.ts +232 -0
- package/src/lib/confluence.test.ts +37 -0
- package/src/lib/confluence.ts +41 -0
- package/src/lib/context-color.ts +1 -1
- package/src/lib/deployment-drift.ts +22 -0
- package/src/lib/derive.ts +43 -69
- package/src/lib/django-aggregates.d.mts +9 -0
- package/src/lib/django-aggregates.mjs +36 -0
- package/src/lib/django-aggregates.test.ts +29 -0
- package/src/lib/django-aggregates.ts +5 -0
- package/src/lib/environments.test.ts +83 -0
- package/src/lib/environments.ts +57 -0
- package/src/lib/integration-url.test.ts +30 -0
- package/src/lib/integration-url.ts +63 -0
- package/src/lib/kafka-ui.ts +3 -45
- package/src/lib/local-api.ts +116 -4
- package/src/lib/notion.ts +13 -0
- package/src/lib/plugin-index.json +2765 -0
- package/src/lib/plugins.test.ts +68 -0
- package/src/lib/plugins.ts +264 -0
- package/src/lib/problem-flows.test.ts +61 -0
- package/src/lib/problem-flows.ts +78 -0
- package/src/lib/problem-rules-cel.d.mts +43 -0
- package/src/lib/problem-rules-cel.mjs +407 -0
- package/src/lib/problem-rules.test.ts +287 -0
- package/src/lib/problem-rules.ts +271 -0
- package/src/lib/problem-subjects.ts +737 -0
- package/src/lib/rule-entries.ts +39 -0
- package/src/lib/{data-problems.test.ts → rules-data.test.ts} +18 -16
- package/src/lib/rules-deploy.test.ts +133 -0
- package/src/lib/{problems.test.ts → rules-edges.test.ts} +8 -3
- package/src/lib/{proto-problems.test.ts → rules-proto.test.ts} +9 -25
- package/src/lib/{wire-problems.test.ts → rules-wire.test.ts} +13 -11
- package/src/lib/rules.test.ts +51 -0
- package/src/lib/rules.ts +86 -0
- package/src/lib/setup-info.test.ts +17 -0
- package/src/lib/setup-info.ts +58 -0
- package/src/lib/shape.test.ts +32 -0
- package/src/lib/shape.ts +30 -6
- package/src/lib/tech.ts +16 -0
- package/src/lib/trace-project.test.ts +34 -0
- package/src/lib/trace-project.ts +50 -0
- package/src/lib/use-problems.ts +23 -0
- package/src/lib/warnings.test.ts +63 -0
- package/src/lib/warnings.ts +260 -0
- package/src/likec4/ids.test.ts +6 -2
- package/src/likec4/ids.ts +43 -0
- package/src/main.tsx +23 -0
- package/src/map/ContextMapGraph.tsx +76 -32
- package/src/merge-deployments.test.ts +127 -0
- package/src/merge.test.ts +82 -0
- package/src/merge.ts +189 -18
- package/src/pages/AggregatePage.tsx +65 -14
- package/src/pages/ContextMap.tsx +45 -3
- package/src/pages/ContextPage.tsx +8 -5
- package/src/pages/EventPage.tsx +15 -5
- package/src/pages/FlowDetail.tsx +23 -2
- package/src/pages/GraphPage.tsx +40 -3
- package/src/pages/Overview.tsx +152 -12
- package/src/pages/PluginIndex.tsx +190 -0
- package/src/pages/Problems.tsx +396 -128
- package/src/pages/ServicePage.tsx +62 -5
- package/src/pages/Settings.tsx +217 -43
- package/src/pages/settings/AboutSettings.tsx +8 -1
- package/src/pages/settings/DjangoAggregateChoices.tsx +79 -0
- package/src/pages/settings/IntegrationsSettings.tsx +63 -17
- package/src/pages/settings/RecordingSettings.tsx +138 -0
- package/src/pages/settings/RulesSettings.tsx +825 -0
- package/src/routes.test.ts +9 -0
- package/src/routes.ts +22 -1
- package/src/selection/DetailPanel.tsx +15 -0
- package/src/virtual-provenance.d.ts +11 -0
- package/vite.config.ts +5 -0
- package/scripts/vendor-lock.mjs +0 -58
- package/scripts/vendor-lock.test.mjs +0 -69
- package/src/lib/data-problems.ts +0 -314
- package/src/lib/proto-problems.ts +0 -237
- package/src/lib/wire-problems.ts +0 -342
|
@@ -0,0 +1,1561 @@
|
|
|
1
|
+
package extracthttpclients
|
|
2
|
+
|
|
3
|
+
import (
|
|
4
|
+
"encoding/json"
|
|
5
|
+
"fmt"
|
|
6
|
+
"os"
|
|
7
|
+
"path/filepath"
|
|
8
|
+
"strings"
|
|
9
|
+
"testing"
|
|
10
|
+
|
|
11
|
+
"github.com/shortlink-org/portolan/catalog"
|
|
12
|
+
"github.com/shortlink-org/portolan/internal/gohttp"
|
|
13
|
+
"github.com/shortlink-org/portolan/plugin"
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
func writeHTTPFixture(t *testing.T, root, name, contents string) {
|
|
17
|
+
t.Helper()
|
|
18
|
+
path := filepath.Join(root, name)
|
|
19
|
+
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
|
|
20
|
+
t.Fatal(err)
|
|
21
|
+
}
|
|
22
|
+
if err := os.WriteFile(path, []byte(contents), 0o644); err != nil {
|
|
23
|
+
t.Fatal(err)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
func TestExtractsRawGeneratedAndSOAPClients(t *testing.T) {
|
|
28
|
+
root := t.TempDir()
|
|
29
|
+
writeHTTPFixture(t, root, "go.mod", "module example.com/gateway\n")
|
|
30
|
+
writeHTTPFixture(t, root, "partner/openapi.yaml", `openapi: 3.0.3
|
|
31
|
+
info:
|
|
32
|
+
title: Partner API
|
|
33
|
+
version: 1.2.0
|
|
34
|
+
paths:
|
|
35
|
+
/v1/trips/{tripId}:
|
|
36
|
+
get:
|
|
37
|
+
operationId: getTrip
|
|
38
|
+
tags: [trips]
|
|
39
|
+
`)
|
|
40
|
+
writeHTTPFixture(t, root, "partner/client.gen.go", `// Code generated by oapi-codegen; DO NOT EDIT.
|
|
41
|
+
package partner
|
|
42
|
+
import (
|
|
43
|
+
"context"
|
|
44
|
+
"fmt"
|
|
45
|
+
"net/http"
|
|
46
|
+
)
|
|
47
|
+
type Client struct{}
|
|
48
|
+
type ClientInterface interface { GetTrip(context.Context, string) (*http.Response, error) }
|
|
49
|
+
func NewGetTripRequest(server, tripID string) (*http.Request, error) {
|
|
50
|
+
operationPath := fmt.Sprintf("/v1/trips/%s", tripID)
|
|
51
|
+
return http.NewRequest(http.MethodGet, server+operationPath, nil)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
`)
|
|
55
|
+
writeHTTPFixture(t, root, "partner/adapter.go", `package partner
|
|
56
|
+
import "context"
|
|
57
|
+
func Load(ctx context.Context, c *Client, id string) error {
|
|
58
|
+
_, err := c.GetTripWithResponse(ctx, id)
|
|
59
|
+
return err
|
|
60
|
+
}
|
|
61
|
+
`)
|
|
62
|
+
writeHTTPFixture(t, root, "core/update.go", `package core
|
|
63
|
+
import (
|
|
64
|
+
"bytes"
|
|
65
|
+
"context"
|
|
66
|
+
"net/http"
|
|
67
|
+
)
|
|
68
|
+
const updatePath = "/v1/update"
|
|
69
|
+
func Update(ctx context.Context, baseURL string, body []byte) error {
|
|
70
|
+
req, err := http.NewRequestWithContext(ctx, http.MethodPost, baseURL+updatePath, bytes.NewReader(body))
|
|
71
|
+
if err != nil { return err }
|
|
72
|
+
_ = req
|
|
73
|
+
return nil
|
|
74
|
+
}
|
|
75
|
+
`)
|
|
76
|
+
writeHTTPFixture(t, root, "transport/handler.go", `package transport
|
|
77
|
+
import (
|
|
78
|
+
"context"
|
|
79
|
+
"example.com/gateway/core"
|
|
80
|
+
)
|
|
81
|
+
func Handle(ctx context.Context, baseURL string, body []byte) error {
|
|
82
|
+
return core.Update(ctx, baseURL, body)
|
|
83
|
+
}
|
|
84
|
+
`)
|
|
85
|
+
writeHTTPFixture(t, root, "soap/cancel.go", `package soapclient
|
|
86
|
+
import (
|
|
87
|
+
"context"
|
|
88
|
+
soap "github.com/hooklift/gowsdl/soap"
|
|
89
|
+
)
|
|
90
|
+
const CancelAction = "urn:CancelBooking"
|
|
91
|
+
func Cancel(ctx context.Context, client soap.HTTPClient, request, response any) error {
|
|
92
|
+
if request == nil {
|
|
93
|
+
return nil
|
|
94
|
+
}
|
|
95
|
+
return client.CallContext(ctx, CancelAction, request, response)
|
|
96
|
+
}
|
|
97
|
+
`)
|
|
98
|
+
writeHTTPFixture(t, root, "soap/booking.wsdl", `<?xml version="1.0"?>
|
|
99
|
+
<definitions name="BookingService" targetNamespace="urn:booking"
|
|
100
|
+
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
|
|
101
|
+
xmlns:tns="urn:booking"
|
|
102
|
+
xmlns:xsd="http://www.w3.org/2001/XMLSchema"
|
|
103
|
+
xmlns="http://schemas.xmlsoap.org/wsdl/">
|
|
104
|
+
<message name="CancelRequest"><part name="body" type="xsd:string"/></message>
|
|
105
|
+
<message name="CancelResponse"><part name="body" type="xsd:string"/></message>
|
|
106
|
+
<portType name="BookingPortType">
|
|
107
|
+
<operation name="CancelBooking"><input message="tns:CancelRequest"/><output message="tns:CancelResponse"/></operation>
|
|
108
|
+
</portType>
|
|
109
|
+
<binding name="BookingBinding" type="tns:BookingPortType">
|
|
110
|
+
<soap:binding style="document"/>
|
|
111
|
+
<operation name="CancelBooking">
|
|
112
|
+
<soap:operation soapAction="urn:CancelBooking"/>
|
|
113
|
+
</operation>
|
|
114
|
+
</binding>
|
|
115
|
+
<service name="BookingService">
|
|
116
|
+
<port name="BookingPort" binding="tns:BookingBinding">
|
|
117
|
+
<soap:address location="https://booking.example/soap"/>
|
|
118
|
+
</port>
|
|
119
|
+
</service>
|
|
120
|
+
</definitions>
|
|
121
|
+
`)
|
|
122
|
+
|
|
123
|
+
resp, err := extract(plugin.Input{Root: root}, Options{Context: "edge", Service: "gateway"})
|
|
124
|
+
if err != nil {
|
|
125
|
+
t.Fatal(err)
|
|
126
|
+
}
|
|
127
|
+
var got catalog.Catalog
|
|
128
|
+
if err := json.Unmarshal([]byte(resp.Files[0].Contents), &got); err != nil {
|
|
129
|
+
t.Fatal(err)
|
|
130
|
+
}
|
|
131
|
+
service := got.Contexts[0].Services[0]
|
|
132
|
+
if len(service.Consumes) != 3 {
|
|
133
|
+
t.Fatalf("consumes = %+v", service.Consumes)
|
|
134
|
+
}
|
|
135
|
+
var generated, raw, soapCall *catalog.RpcCall
|
|
136
|
+
for i := range service.Consumes {
|
|
137
|
+
call := &service.Consumes[i]
|
|
138
|
+
switch {
|
|
139
|
+
case strings.Contains(call.ID, "getTrip"):
|
|
140
|
+
generated = call
|
|
141
|
+
case strings.Contains(call.ID, "POST /v1/update"):
|
|
142
|
+
raw = call
|
|
143
|
+
case strings.Contains(call.ID, "CancelBooking"):
|
|
144
|
+
soapCall = call
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if generated == nil || generated.Status != catalog.StatusDeclared || generated.Peer != "partner" {
|
|
148
|
+
t.Fatalf("generated = %+v", generated)
|
|
149
|
+
}
|
|
150
|
+
if raw == nil || raw.Status != catalog.StatusUnresolved || raw.Source != "core/update.go:9" {
|
|
151
|
+
t.Fatalf("raw = %+v", raw)
|
|
152
|
+
}
|
|
153
|
+
if soapCall == nil || soapCall.Status != catalog.StatusDeclared || soapCall.Peer != "booking" {
|
|
154
|
+
t.Fatalf("soap = %+v", soapCall)
|
|
155
|
+
}
|
|
156
|
+
if len(got.Externals) != 2 {
|
|
157
|
+
t.Fatalf("externals = %+v", got.Externals)
|
|
158
|
+
}
|
|
159
|
+
if len(got.Flows) != 4 {
|
|
160
|
+
t.Fatalf("flows = %+v", got.Flows)
|
|
161
|
+
}
|
|
162
|
+
var sawCondition, sawChain bool
|
|
163
|
+
for _, flow := range got.Flows {
|
|
164
|
+
for _, node := range flow.Steps {
|
|
165
|
+
if step, ok := node.(*catalog.Step); ok && strings.Contains(step.Note, "request == nil") {
|
|
166
|
+
sawCondition = true
|
|
167
|
+
}
|
|
168
|
+
if step, ok := node.(*catalog.Step); ok && strings.Contains(step.Note, "Handle → Update") {
|
|
169
|
+
sawChain = true
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if !sawCondition {
|
|
174
|
+
t.Fatal("SOAP source condition was not preserved")
|
|
175
|
+
}
|
|
176
|
+
if !sawChain {
|
|
177
|
+
t.Fatal("generic entry point was not followed to its outbound call")
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
func TestFlowTracesConfigThroughConstructorFieldAndWrapper(t *testing.T) {
|
|
182
|
+
root := t.TempDir()
|
|
183
|
+
writeHTTPFixture(t, root, "go.mod", "module example.com/payments\n")
|
|
184
|
+
writeHTTPFixture(t, root, "client/client.go", `package client
|
|
185
|
+
import (
|
|
186
|
+
"context"
|
|
187
|
+
"net/http"
|
|
188
|
+
"net/url"
|
|
189
|
+
)
|
|
190
|
+
type Config struct { BaseURL string }
|
|
191
|
+
type Client struct { baseURL url.URL }
|
|
192
|
+
func Build(config map[string]string) (*Client, error) {
|
|
193
|
+
return New(Config{BaseURL: config["url"]})
|
|
194
|
+
}
|
|
195
|
+
func New(cfg Config) (*Client, error) {
|
|
196
|
+
parsed, err := url.Parse(cfg.BaseURL)
|
|
197
|
+
if err != nil { return nil, err }
|
|
198
|
+
return &Client{baseURL: *parsed}, nil
|
|
199
|
+
}
|
|
200
|
+
func (c *Client) request(ctx context.Context, method, path string) error {
|
|
201
|
+
req, err := http.NewRequestWithContext(ctx, method, c.baseURL.String()+path, nil)
|
|
202
|
+
if err != nil { return err }
|
|
203
|
+
_ = req
|
|
204
|
+
return nil
|
|
205
|
+
}
|
|
206
|
+
func (c *Client) Search(ctx context.Context) error {
|
|
207
|
+
return c.request(ctx, http.MethodPost, "/v1/search")
|
|
208
|
+
}
|
|
209
|
+
`)
|
|
210
|
+
|
|
211
|
+
resp, err := extract(plugin.Input{Root: root}, Options{Context: "pay", Service: "gateway"})
|
|
212
|
+
if err != nil {
|
|
213
|
+
t.Fatal(err)
|
|
214
|
+
}
|
|
215
|
+
var got catalog.Catalog
|
|
216
|
+
if err := json.Unmarshal([]byte(resp.Files[0].Contents), &got); err != nil {
|
|
217
|
+
t.Fatal(err)
|
|
218
|
+
}
|
|
219
|
+
if len(got.Flows) != 1 {
|
|
220
|
+
t.Fatalf("flows = %+v", got.Flows)
|
|
221
|
+
}
|
|
222
|
+
flow := got.Flows[0]
|
|
223
|
+
if flow.Name != "Client Search → outbound APIs" {
|
|
224
|
+
t.Fatalf("flow name = %q", flow.Name)
|
|
225
|
+
}
|
|
226
|
+
if flow.EntryPoint != "client:Client.Search" {
|
|
227
|
+
t.Fatalf("flow entrypoint = %q", flow.EntryPoint)
|
|
228
|
+
}
|
|
229
|
+
step, ok := flow.Steps[0].(*catalog.Step)
|
|
230
|
+
if !ok {
|
|
231
|
+
t.Fatalf("step = %#v", flow.Steps[0])
|
|
232
|
+
}
|
|
233
|
+
if step.Label != "POST /v1/search" {
|
|
234
|
+
t.Fatalf("step label = %q", step.Label)
|
|
235
|
+
}
|
|
236
|
+
if consumes := got.Contexts[0].Services[0].Consumes; len(consumes) != 1 || consumes[0].ID != "http-client/POST /v1/search" {
|
|
237
|
+
t.Fatalf("consumes = %+v", consumes)
|
|
238
|
+
}
|
|
239
|
+
if !strings.Contains(step.Note, `config["url"] → Build → New → Client.baseURL → request URL`) {
|
|
240
|
+
t.Fatalf("step note = %q", step.Note)
|
|
241
|
+
}
|
|
242
|
+
if !strings.Contains(step.Note, "Client.Search → Client.request") {
|
|
243
|
+
t.Fatalf("step chain = %q", step.Note)
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
func TestFlowKeepsDistinctCallsThroughClosureArguments(t *testing.T) {
|
|
248
|
+
root := t.TempDir()
|
|
249
|
+
writeHTTPFixture(t, root, "go.mod", "module example.com/closure\n")
|
|
250
|
+
writeHTTPFixture(t, root, "client.go", `package closure
|
|
251
|
+
import (
|
|
252
|
+
"context"
|
|
253
|
+
"net/http"
|
|
254
|
+
)
|
|
255
|
+
func request(ctx context.Context, path string) error {
|
|
256
|
+
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://example.test"+path, nil)
|
|
257
|
+
if err != nil { return err }
|
|
258
|
+
_ = req
|
|
259
|
+
return nil
|
|
260
|
+
}
|
|
261
|
+
func Refresh(ctx context.Context) {
|
|
262
|
+
go func(path string) { _ = request(ctx, path) }("/v1/primary")
|
|
263
|
+
go func(path string) { _ = request(ctx, path) }("/v1/secondary")
|
|
264
|
+
}
|
|
265
|
+
`)
|
|
266
|
+
|
|
267
|
+
resp, err := extract(plugin.Input{Root: root}, Options{Context: "ops", Service: "refresher"})
|
|
268
|
+
if err != nil {
|
|
269
|
+
t.Fatal(err)
|
|
270
|
+
}
|
|
271
|
+
var got catalog.Catalog
|
|
272
|
+
if err := json.Unmarshal([]byte(resp.Files[0].Contents), &got); err != nil {
|
|
273
|
+
t.Fatal(err)
|
|
274
|
+
}
|
|
275
|
+
if len(got.Flows) != 1 {
|
|
276
|
+
t.Fatalf("flows = %+v", got.Flows)
|
|
277
|
+
}
|
|
278
|
+
var labels []string
|
|
279
|
+
for _, node := range got.Flows[0].Steps {
|
|
280
|
+
if step, ok := node.(*catalog.Step); ok {
|
|
281
|
+
labels = append(labels, step.Label)
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
if strings.Join(labels, ",") != "GET /v1/primary,GET /v1/secondary" {
|
|
285
|
+
t.Fatalf("labels = %+v", labels)
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
func TestFlowDoesNotLinkMethodsByNameAlone(t *testing.T) {
|
|
290
|
+
root := t.TempDir()
|
|
291
|
+
writeHTTPFixture(t, root, "go.mod", "module example.com/close\n")
|
|
292
|
+
writeHTTPFixture(t, root, "client.go", `package close
|
|
293
|
+
import "net/http"
|
|
294
|
+
type LocalCloser struct{}
|
|
295
|
+
func (LocalCloser) Close() error {
|
|
296
|
+
_, err := http.Get("https://audit.example/v1/closed")
|
|
297
|
+
return err
|
|
298
|
+
}
|
|
299
|
+
func Fetch() error {
|
|
300
|
+
response, err := http.Get("https://data.example/v1/items")
|
|
301
|
+
if err != nil { return err }
|
|
302
|
+
defer response.Body.Close()
|
|
303
|
+
return nil
|
|
304
|
+
}
|
|
305
|
+
`)
|
|
306
|
+
|
|
307
|
+
resp, err := extract(plugin.Input{Root: root}, Options{Context: "edge", Service: "reader"})
|
|
308
|
+
if err != nil {
|
|
309
|
+
t.Fatal(err)
|
|
310
|
+
}
|
|
311
|
+
var got catalog.Catalog
|
|
312
|
+
if err := json.Unmarshal([]byte(resp.Files[0].Contents), &got); err != nil {
|
|
313
|
+
t.Fatal(err)
|
|
314
|
+
}
|
|
315
|
+
for _, flow := range got.Flows {
|
|
316
|
+
if flow.Name != "Fetch → outbound APIs" {
|
|
317
|
+
continue
|
|
318
|
+
}
|
|
319
|
+
if len(flow.Steps) != 1 {
|
|
320
|
+
t.Fatalf("Fetch steps = %+v", flow.Steps)
|
|
321
|
+
}
|
|
322
|
+
step := flow.Steps[0].(*catalog.Step)
|
|
323
|
+
if step.Label != "GET /v1/items" {
|
|
324
|
+
t.Fatalf("Fetch step = %+v", step)
|
|
325
|
+
}
|
|
326
|
+
return
|
|
327
|
+
}
|
|
328
|
+
t.Fatalf("Fetch flow missing: %+v", got.Flows)
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
func TestBuildsEndpointRootedProviderFlowFromSource(t *testing.T) {
|
|
332
|
+
root := t.TempDir()
|
|
333
|
+
writeHTTPFixture(t, root, "go.mod", "module example.com/travel\n")
|
|
334
|
+
writeHTTPFixture(t, root, "app/main.go", `package app
|
|
335
|
+
import (
|
|
336
|
+
"example.com/travel/actions/search"
|
|
337
|
+
"example.com/travel/connector"
|
|
338
|
+
)
|
|
339
|
+
type Router struct{}
|
|
340
|
+
func (*Router) POST(string, func()) {}
|
|
341
|
+
type Requester interface { ConnExec(connector.API) }
|
|
342
|
+
func Start(r *Router) {
|
|
343
|
+
r.POST("/search", func() { SearchAction() })
|
|
344
|
+
app := App{}
|
|
345
|
+
r.POST("/direct-search", app.DirectSearch)
|
|
346
|
+
}
|
|
347
|
+
type App struct{}
|
|
348
|
+
func (App) DirectSearch() {
|
|
349
|
+
conn := connector.BuildConnector("runtime-name")
|
|
350
|
+
conn.Search()
|
|
351
|
+
}
|
|
352
|
+
func SearchAction() {
|
|
353
|
+
request := &search.Request{}
|
|
354
|
+
ActionFlow(request)
|
|
355
|
+
}
|
|
356
|
+
func ActionFlow(request Requester) {
|
|
357
|
+
conn := connector.BuildConnector("runtime-name")
|
|
358
|
+
request.ConnExec(conn)
|
|
359
|
+
}
|
|
360
|
+
`)
|
|
361
|
+
writeHTTPFixture(t, root, "actions/search/request.go", `package search
|
|
362
|
+
import "example.com/travel/connector"
|
|
363
|
+
type Request struct{}
|
|
364
|
+
func (*Request) ConnExec(conn connector.API) { conn.Search() }
|
|
365
|
+
`)
|
|
366
|
+
writeHTTPFixture(t, root, "connector/factory.go", `package connector
|
|
367
|
+
import (
|
|
368
|
+
"example.com/travel/provider/alpha"
|
|
369
|
+
"example.com/travel/provider/beta"
|
|
370
|
+
"example.com/travel/provider/opaque"
|
|
371
|
+
)
|
|
372
|
+
type API interface { Search() }
|
|
373
|
+
func BuildConnector(name string) API {
|
|
374
|
+
switch name {
|
|
375
|
+
case "alpha":
|
|
376
|
+
return alpha.New()
|
|
377
|
+
case "beta":
|
|
378
|
+
client := beta.New()
|
|
379
|
+
return client
|
|
380
|
+
case "opaque":
|
|
381
|
+
return opaque.New()
|
|
382
|
+
default:
|
|
383
|
+
return nil
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
`)
|
|
387
|
+
writeHTTPFixture(t, root, "provider/opaque/client.go", `package opaque
|
|
388
|
+
type Connector struct{}
|
|
389
|
+
func New() *Connector { return &Connector{} }
|
|
390
|
+
func (*Connector) Search() {}
|
|
391
|
+
`)
|
|
392
|
+
writeHTTPFixture(t, root, "provider/alpha/client.go", `package alpha
|
|
393
|
+
import (
|
|
394
|
+
alphaclient "example.com/travel/provider/alpha/client"
|
|
395
|
+
alphaport "example.com/travel/provider/alpha/port"
|
|
396
|
+
)
|
|
397
|
+
type Connector struct{ Client alphaport.API }
|
|
398
|
+
func New() *Connector {
|
|
399
|
+
client := alphaclient.New()
|
|
400
|
+
return &Connector{Client: client}
|
|
401
|
+
}
|
|
402
|
+
func (c *Connector) Search() {
|
|
403
|
+
c.Client.Search()
|
|
404
|
+
c.Client.Retrieve()
|
|
405
|
+
}
|
|
406
|
+
`)
|
|
407
|
+
writeHTTPFixture(t, root, "provider/alpha/port/api.go", `package port
|
|
408
|
+
type API interface {
|
|
409
|
+
Search()
|
|
410
|
+
Retrieve()
|
|
411
|
+
}
|
|
412
|
+
`)
|
|
413
|
+
writeHTTPFixture(t, root, "provider/alpha/client/client.go", `package client
|
|
414
|
+
import "net/http"
|
|
415
|
+
type Client struct{}
|
|
416
|
+
func New() *Client { return &Client{} }
|
|
417
|
+
func (c *Client) Search() { c.SearchRequest() }
|
|
418
|
+
func (*Client) SearchRequest() { _, _ = http.Get("https://alpha.example/v1/search") }
|
|
419
|
+
func (c *Client) Retrieve() { c.RetrieveRequest() }
|
|
420
|
+
func (*Client) RetrieveRequest() { _, _ = http.Get("https://alpha.example/v1/retrieve") }
|
|
421
|
+
`)
|
|
422
|
+
writeHTTPFixture(t, root, "provider/beta/client.go", `package beta
|
|
423
|
+
import betaclient "example.com/travel/provider/beta/client"
|
|
424
|
+
type Connector struct{ Client *betaclient.Client }
|
|
425
|
+
func New() *Connector { return &Connector{Client: &betaclient.Client{}} }
|
|
426
|
+
func (c *Connector) Search() { c.Client.Search() }
|
|
427
|
+
`)
|
|
428
|
+
writeHTTPFixture(t, root, "provider/beta/client/client.go", `package client
|
|
429
|
+
import "net/http"
|
|
430
|
+
type Client struct{}
|
|
431
|
+
func (*Client) Search() { _, _ = http.Get("https://beta.example/v2/offers") }
|
|
432
|
+
`)
|
|
433
|
+
writeHTTPFixture(t, root, "jobs/refresh.go", `package jobs
|
|
434
|
+
import "net/http"
|
|
435
|
+
func Refresh() { _, _ = http.Get("https://cache.example/refresh") }
|
|
436
|
+
`)
|
|
437
|
+
|
|
438
|
+
resp, err := extract(plugin.Input{Root: root}, Options{Context: "travel", Service: "search"})
|
|
439
|
+
if err != nil {
|
|
440
|
+
t.Fatal(err)
|
|
441
|
+
}
|
|
442
|
+
var got catalog.Catalog
|
|
443
|
+
if err := json.Unmarshal([]byte(resp.Files[0].Contents), &got); err != nil {
|
|
444
|
+
t.Fatal(err)
|
|
445
|
+
}
|
|
446
|
+
if len(got.Flows) != 3 {
|
|
447
|
+
t.Fatalf("flows = %+v", got.Flows)
|
|
448
|
+
}
|
|
449
|
+
var endpoint, direct, background *catalog.Flow
|
|
450
|
+
for index := range got.Flows {
|
|
451
|
+
flow := &got.Flows[index]
|
|
452
|
+
if flow.Name == "POST /search → provider APIs" {
|
|
453
|
+
endpoint = flow
|
|
454
|
+
}
|
|
455
|
+
if flow.Name == "POST /direct-search → provider APIs" {
|
|
456
|
+
direct = flow
|
|
457
|
+
}
|
|
458
|
+
if flow.Name == "Refresh → outbound APIs" {
|
|
459
|
+
background = flow
|
|
460
|
+
}
|
|
461
|
+
if strings.Contains(flow.Name, "Connector Search") {
|
|
462
|
+
t.Fatalf("covered provider fragment was retained: %+v", flow)
|
|
463
|
+
}
|
|
464
|
+
if flow.Name == "Alpha Search Request → outbound APIs" {
|
|
465
|
+
t.Fatalf("covered nested provider fragment was retained: %+v", flow)
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
if endpoint == nil || direct == nil || background == nil {
|
|
469
|
+
t.Fatalf("endpoint = %+v, direct = %+v, background = %+v", endpoint, direct, background)
|
|
470
|
+
}
|
|
471
|
+
if !strings.Contains(background.Summary, "No source caller was found") {
|
|
472
|
+
t.Fatalf("standalone reason = %q", background.Summary)
|
|
473
|
+
}
|
|
474
|
+
if len(endpoint.Steps) != 2 {
|
|
475
|
+
t.Fatalf("steps = %+v", endpoint.Steps)
|
|
476
|
+
}
|
|
477
|
+
inbound, ok := endpoint.Steps[0].(*catalog.Step)
|
|
478
|
+
if !ok || inbound.Label != "POST /search" || inbound.Line != "app/main.go:10" {
|
|
479
|
+
t.Fatalf("inbound = %+v", endpoint.Steps[0])
|
|
480
|
+
}
|
|
481
|
+
providers, ok := endpoint.Steps[1].(*catalog.Alt)
|
|
482
|
+
if !ok || len(providers.Branches) != 3 {
|
|
483
|
+
t.Fatalf("providers = %+v", endpoint.Steps[1])
|
|
484
|
+
}
|
|
485
|
+
if providers.Branches[0].Title != `connector = "alpha"` || providers.Branches[1].Title != `connector = "beta"` || providers.Branches[2].Title != `connector = "opaque"` {
|
|
486
|
+
t.Fatalf("branches = %+v", providers.Branches)
|
|
487
|
+
}
|
|
488
|
+
alphaStep := providers.Branches[0].Steps[0].(*catalog.Step)
|
|
489
|
+
betaStep := providers.Branches[1].Steps[0].(*catalog.Step)
|
|
490
|
+
if alphaStep.Label != "GET /v1/search" || betaStep.Label != "GET /v2/offers" {
|
|
491
|
+
t.Fatalf("provider steps = %+v / %+v", alphaStep, betaStep)
|
|
492
|
+
}
|
|
493
|
+
if len(providers.Branches[0].Steps) != 2 || providers.Branches[0].Steps[1].(*catalog.Step).Label != "GET /v1/retrieve" {
|
|
494
|
+
t.Fatalf("constructor-wired call order = %+v", providers.Branches[0].Steps)
|
|
495
|
+
}
|
|
496
|
+
if !strings.Contains(alphaStep.Note, "Connector.Search → Client.Search → Client.SearchRequest") {
|
|
497
|
+
t.Fatalf("nested provider chain = %+v", alphaStep)
|
|
498
|
+
}
|
|
499
|
+
if !strings.Contains(betaStep.Note, "Connector.Search → Client.Search") {
|
|
500
|
+
t.Fatalf("concrete imported client chain = %+v", betaStep)
|
|
501
|
+
}
|
|
502
|
+
opaqueStep := providers.Branches[2].Steps[0].(*catalog.Step)
|
|
503
|
+
if opaqueStep.Kind != catalog.StepCall || opaqueStep.Label != "Opaque Search" || !strings.Contains(opaqueStep.Note, "outbound transport was not resolved") {
|
|
504
|
+
t.Fatalf("opaque step = %+v", opaqueStep)
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
func TestBuildsEndpointThroughMultiStageInterfaceDispatch(t *testing.T) {
|
|
509
|
+
root := t.TempDir()
|
|
510
|
+
writeHTTPFixture(t, root, "go.mod", "module example.com/typed\n\ngo 1.27.0\n")
|
|
511
|
+
writeHTTPFixture(t, root, "app/main.go", `package app
|
|
512
|
+
import (
|
|
513
|
+
"example.com/typed/actions/rules"
|
|
514
|
+
"example.com/typed/connector"
|
|
515
|
+
)
|
|
516
|
+
type Router struct{}
|
|
517
|
+
func (*Router) POST(string, func()) {}
|
|
518
|
+
type Requester interface { ConnExec(connector.API) }
|
|
519
|
+
func Start(r *Router) { r.POST("/rules", RulesAction) }
|
|
520
|
+
func RulesAction() {
|
|
521
|
+
request := &rules.Request{}
|
|
522
|
+
Dispatch(request)
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
func Dispatch(request Requester) { Invoke(request) }
|
|
526
|
+
func Invoke(request Requester) {
|
|
527
|
+
conn := connector.Build("runtime")
|
|
528
|
+
request.ConnExec(conn)
|
|
529
|
+
}
|
|
530
|
+
`)
|
|
531
|
+
writeHTTPFixture(t, root, "actions/rules/request.go", `package rules
|
|
532
|
+
import "example.com/typed/connector"
|
|
533
|
+
type Request struct{}
|
|
534
|
+
func (*Request) ConnExec(conn connector.API) { conn.Rules() }
|
|
535
|
+
`)
|
|
536
|
+
writeHTTPFixture(t, root, "connector/factory.go", `package connector
|
|
537
|
+
import "example.com/typed/provider/alpha"
|
|
538
|
+
type API interface { Rules() }
|
|
539
|
+
func Build(name string) API {
|
|
540
|
+
switch name {
|
|
541
|
+
case "alpha": return alpha.New()
|
|
542
|
+
default: return nil
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
`)
|
|
546
|
+
writeHTTPFixture(t, root, "provider/alpha/connector.go", `package alpha
|
|
547
|
+
import "example.com/typed/provider/alpha/client"
|
|
548
|
+
type rulesClient interface { FetchRules() }
|
|
549
|
+
type Connector struct { client rulesClient }
|
|
550
|
+
func New() *Connector { return &Connector{client: &client.Client{}} }
|
|
551
|
+
func (c *Connector) Rules() { c.client.FetchRules() }
|
|
552
|
+
`)
|
|
553
|
+
writeHTTPFixture(t, root, "provider/alpha/client/client.go", `package client
|
|
554
|
+
import "net/http"
|
|
555
|
+
type Client struct{}
|
|
556
|
+
func (c *Client) FetchRules() { c.fetchRules() }
|
|
557
|
+
func (c *Client) fetchRules() { c.finishRules() }
|
|
558
|
+
func (c *Client) finishRules() {
|
|
559
|
+
_, _ = http.Get("https://alpha.example/v1/rules")
|
|
560
|
+
if false { c.fetchRules() }
|
|
561
|
+
}
|
|
562
|
+
func (*Client) CheckRules() { _, _ = http.Get("https://alpha.example/v1/check-rules") }
|
|
563
|
+
`)
|
|
564
|
+
|
|
565
|
+
analysis, err := gohttp.Analyze(root)
|
|
566
|
+
if err != nil {
|
|
567
|
+
t.Fatal(err)
|
|
568
|
+
}
|
|
569
|
+
if !analysis.TypedCallGraph || analysis.TypedCallGraphError != "" {
|
|
570
|
+
t.Fatalf("typed analysis = %v / %q", analysis.TypedCallGraph, analysis.TypedCallGraphError)
|
|
571
|
+
}
|
|
572
|
+
if len(analysis.EndpointFlows) != 1 || len(analysis.EndpointFlows[0].Branches) != 1 {
|
|
573
|
+
t.Fatalf("endpoint flows = %+v", analysis.EndpointFlows)
|
|
574
|
+
}
|
|
575
|
+
branch := analysis.EndpointFlows[0].Branches[0]
|
|
576
|
+
if branch.Operation != "Rules" || len(branch.Calls) != 1 || branch.Calls[0].Path != "/v1/rules" {
|
|
577
|
+
t.Fatalf("typed provider branch = %+v", branch)
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
resp, err := extract(plugin.Input{Root: root}, Options{Context: "travel", Service: "rules"})
|
|
581
|
+
if err != nil {
|
|
582
|
+
t.Fatal(err)
|
|
583
|
+
}
|
|
584
|
+
var got catalog.Catalog
|
|
585
|
+
if err := json.Unmarshal([]byte(resp.Files[0].Contents), &got); err != nil {
|
|
586
|
+
t.Fatal(err)
|
|
587
|
+
}
|
|
588
|
+
var endpoint, unused *catalog.Flow
|
|
589
|
+
for index := range got.Flows {
|
|
590
|
+
flow := &got.Flows[index]
|
|
591
|
+
if flow.Name == "POST /rules → provider APIs" {
|
|
592
|
+
endpoint = flow
|
|
593
|
+
}
|
|
594
|
+
if strings.Contains(flow.Name, "CheckRules") {
|
|
595
|
+
unused = flow
|
|
596
|
+
}
|
|
597
|
+
if strings.Contains(flow.Name, "Fetch Rules") {
|
|
598
|
+
t.Fatalf("covered typed fragment retained: %+v", flow)
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
if endpoint == nil || unused == nil {
|
|
602
|
+
t.Fatalf("endpoint = %+v, unused = %+v, flows = %+v", endpoint, unused, got.Flows)
|
|
603
|
+
}
|
|
604
|
+
step := endpoint.Steps[1].(*catalog.Step)
|
|
605
|
+
if step.Label != "GET /v1/rules" || !strings.Contains(step.Note, "Connector.Rules → Client.FetchRules") {
|
|
606
|
+
t.Fatalf("typed step = %+v", step)
|
|
607
|
+
}
|
|
608
|
+
if unused.Trigger == nil || unused.Trigger.Kind != "unproven" {
|
|
609
|
+
t.Fatalf("unused trigger = %+v", unused.Trigger)
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
func TestTypedAnalysisMapsPositionsFromAManifestRelativeRoot(t *testing.T) {
|
|
614
|
+
root := t.TempDir()
|
|
615
|
+
writeHTTPFixture(t, root, "go.mod", "module example.com/relative\n\ngo 1.27.0\n")
|
|
616
|
+
writeHTTPFixture(t, root, "app/main.go", `package app
|
|
617
|
+
import (
|
|
618
|
+
"example.com/relative/actions/rules"
|
|
619
|
+
"example.com/relative/connector"
|
|
620
|
+
)
|
|
621
|
+
type Router struct{}
|
|
622
|
+
func (*Router) POST(string, func()) {}
|
|
623
|
+
type Requester interface { ConnExec(connector.API) }
|
|
624
|
+
func Start(r *Router) { r.POST("/rules", RulesAction) }
|
|
625
|
+
func RulesAction() { request := &rules.Request{}; Dispatch(request) }
|
|
626
|
+
func Dispatch(request Requester) { Invoke(request) }
|
|
627
|
+
func Invoke(request Requester) { request.ConnExec(connector.Build()) }
|
|
628
|
+
`)
|
|
629
|
+
writeHTTPFixture(t, root, "actions/rules/request.go", `package rules
|
|
630
|
+
import "example.com/relative/connector"
|
|
631
|
+
type Request struct{}
|
|
632
|
+
func (*Request) ConnExec(conn connector.API) { conn.Rules() }
|
|
633
|
+
`)
|
|
634
|
+
writeHTTPFixture(t, root, "connector/factory.go", `package connector
|
|
635
|
+
import "example.com/relative/provider/alpha"
|
|
636
|
+
type API interface { Rules() }
|
|
637
|
+
func Build() API { return alpha.New() }
|
|
638
|
+
`)
|
|
639
|
+
writeHTTPFixture(t, root, "provider/alpha/client.go", `package alpha
|
|
640
|
+
import "net/http"
|
|
641
|
+
type Client struct{}
|
|
642
|
+
func New() *Client { return &Client{} }
|
|
643
|
+
func (*Client) Rules() { _, _ = http.Get("https://alpha.example/v1/rules") }
|
|
644
|
+
`)
|
|
645
|
+
|
|
646
|
+
old, err := os.Getwd()
|
|
647
|
+
if err != nil {
|
|
648
|
+
t.Fatal(err)
|
|
649
|
+
}
|
|
650
|
+
if err := os.Chdir(root); err != nil {
|
|
651
|
+
t.Fatal(err)
|
|
652
|
+
}
|
|
653
|
+
t.Cleanup(func() { _ = os.Chdir(old) })
|
|
654
|
+
|
|
655
|
+
analysis, err := gohttp.Analyze(".")
|
|
656
|
+
if err != nil {
|
|
657
|
+
t.Fatal(err)
|
|
658
|
+
}
|
|
659
|
+
if !analysis.TypedCallGraph || analysis.TypedCallGraphError != "" {
|
|
660
|
+
t.Fatalf("typed analysis = %v / %q", analysis.TypedCallGraph, analysis.TypedCallGraphError)
|
|
661
|
+
}
|
|
662
|
+
if len(analysis.EndpointFlows) != 1 || len(analysis.EndpointFlows[0].Branches) != 1 {
|
|
663
|
+
t.Fatalf("endpoint flows = %+v", analysis.EndpointFlows)
|
|
664
|
+
}
|
|
665
|
+
branch := analysis.EndpointFlows[0].Branches[0]
|
|
666
|
+
if branch.Operation != "Rules" || len(branch.Calls) != 1 || branch.Calls[0].Path != "/v1/rules" {
|
|
667
|
+
t.Fatalf("typed provider branch = %+v", branch)
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
func TestPartialTypedAnalysisAndSyntaxFallback(t *testing.T) {
|
|
672
|
+
for _, dependent := range []bool{false, true} {
|
|
673
|
+
t.Run(fmt.Sprintf("broken_dependency_%v", dependent), func(t *testing.T) {
|
|
674
|
+
root := t.TempDir()
|
|
675
|
+
writeHTTPFixture(t, root, "go.mod", "module example.com/fallback\n\ngo 1.27.0\n")
|
|
676
|
+
writeHTTPFixture(t, root, "app/main.go", `package app
|
|
677
|
+
import (
|
|
678
|
+
"net/http"
|
|
679
|
+
"example.com/fallback/connector"
|
|
680
|
+
)
|
|
681
|
+
type Router struct{}
|
|
682
|
+
func (*Router) POST(string, func()) {}
|
|
683
|
+
func Start(r *Router) { r.POST("/search", Search) }
|
|
684
|
+
func Search() {
|
|
685
|
+
conn := connector.Build("runtime")
|
|
686
|
+
conn.Search()
|
|
687
|
+
_, _ = http.Get("https://fallback.example/search")
|
|
688
|
+
}
|
|
689
|
+
`)
|
|
690
|
+
writeHTTPFixture(t, root, "connector/factory.go", `package connector
|
|
691
|
+
import "example.com/fallback/provider/alpha"
|
|
692
|
+
type API interface { Search() }
|
|
693
|
+
func Build(name string) API {
|
|
694
|
+
switch name { case "only": return alpha.New(); default: return nil }
|
|
695
|
+
}
|
|
696
|
+
`)
|
|
697
|
+
writeHTTPFixture(t, root, "provider/alpha/connector.go", `package alpha
|
|
698
|
+
type Connector struct{}
|
|
699
|
+
func New() *Connector { return &Connector{} }
|
|
700
|
+
func (*Connector) Search() {}
|
|
701
|
+
`)
|
|
702
|
+
writeHTTPFixture(t, root, "broken/broken.go", `package broken
|
|
703
|
+
import _ "example.com/missing/private"
|
|
704
|
+
`)
|
|
705
|
+
|
|
706
|
+
if dependent {
|
|
707
|
+
writeHTTPFixture(t, root, "provider/alpha/broken.go", `package alpha
|
|
708
|
+
import _ "example.com/missing/private"
|
|
709
|
+
`)
|
|
710
|
+
}
|
|
711
|
+
analysis, err := gohttp.Analyze(root)
|
|
712
|
+
if err != nil {
|
|
713
|
+
t.Fatal(err)
|
|
714
|
+
}
|
|
715
|
+
if dependent {
|
|
716
|
+
if analysis.TypedCallGraph || analysis.TypedCallGraphError == "" {
|
|
717
|
+
t.Fatalf("typed fallback = %v / %q", analysis.TypedCallGraph, analysis.TypedCallGraphError)
|
|
718
|
+
}
|
|
719
|
+
} else {
|
|
720
|
+
if !analysis.TypedCallGraph || analysis.TypedCallGraphError != "" {
|
|
721
|
+
t.Fatalf("independent typed facts lost: %v / %q", analysis.TypedCallGraph, analysis.TypedCallGraphError)
|
|
722
|
+
}
|
|
723
|
+
if !strings.Contains(strings.Join(analysis.Warnings, "\n"), "typed call graph is partial") {
|
|
724
|
+
t.Fatalf("missing partial diagnostic: %v", analysis.Warnings)
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
if len(analysis.Calls) != 1 || analysis.Calls[0].Path != "/search" {
|
|
728
|
+
t.Fatalf("syntax calls = %+v", analysis.Calls)
|
|
729
|
+
}
|
|
730
|
+
})
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
func TestBuildsEndpointThroughFixedFactorySetterAndWrapper(t *testing.T) {
|
|
735
|
+
root := t.TempDir()
|
|
736
|
+
writeHTTPFixture(t, root, "go.mod", "module example.com/wired\n")
|
|
737
|
+
writeHTTPFixture(t, root, "app/main.go", `package app
|
|
738
|
+
import "example.com/wired/connector"
|
|
739
|
+
type Router struct{}
|
|
740
|
+
func (*Router) POST(string, func()) {}
|
|
741
|
+
func Start(r *Router) { r.POST("/cancel", Cancel) }
|
|
742
|
+
func Cancel() {
|
|
743
|
+
conn := connector.Build()
|
|
744
|
+
conn.Cancel()
|
|
745
|
+
}
|
|
746
|
+
`)
|
|
747
|
+
writeHTTPFixture(t, root, "connector/factory.go", `package connector
|
|
748
|
+
import (
|
|
749
|
+
"example.com/wired/provider/alpha"
|
|
750
|
+
"example.com/wired/provider/wrapper"
|
|
751
|
+
)
|
|
752
|
+
type API interface{ Cancel() }
|
|
753
|
+
func Build() API {
|
|
754
|
+
conn := wrapper.New()
|
|
755
|
+
client := alpha.New()
|
|
756
|
+
conn.SetClient(client)
|
|
757
|
+
return conn
|
|
758
|
+
}
|
|
759
|
+
`)
|
|
760
|
+
writeHTTPFixture(t, root, "provider/port/api.go", `package port
|
|
761
|
+
type API interface{ Cancel() }
|
|
762
|
+
`)
|
|
763
|
+
writeHTTPFixture(t, root, "provider/wrapper/connector.go", `package wrapper
|
|
764
|
+
import "example.com/wired/provider/port"
|
|
765
|
+
type Connector struct{ client port.API }
|
|
766
|
+
func New() *Connector { return &Connector{} }
|
|
767
|
+
func (c *Connector) SetClient(client port.API) { c.client = client }
|
|
768
|
+
func (c *Connector) Cancel() { c.client.Cancel() }
|
|
769
|
+
`)
|
|
770
|
+
writeHTTPFixture(t, root, "provider/alpha/client.go", `package alpha
|
|
771
|
+
import "net/http"
|
|
772
|
+
type Client struct{}
|
|
773
|
+
func New() *Client { return &Client{} }
|
|
774
|
+
func (*Client) Cancel() { _, _ = http.Get("https://alpha.example/v1/cancel") }
|
|
775
|
+
`)
|
|
776
|
+
|
|
777
|
+
resp, err := extract(plugin.Input{Root: root}, Options{Context: "travel", Service: "cancel"})
|
|
778
|
+
if err != nil {
|
|
779
|
+
t.Fatal(err)
|
|
780
|
+
}
|
|
781
|
+
var got catalog.Catalog
|
|
782
|
+
if err := json.Unmarshal([]byte(resp.Files[0].Contents), &got); err != nil {
|
|
783
|
+
t.Fatal(err)
|
|
784
|
+
}
|
|
785
|
+
if len(got.Flows) != 1 {
|
|
786
|
+
t.Fatalf("flows = %+v", got.Flows)
|
|
787
|
+
}
|
|
788
|
+
flow := got.Flows[0]
|
|
789
|
+
if flow.Name != "POST /cancel → provider APIs" || len(flow.Steps) != 2 {
|
|
790
|
+
t.Fatalf("endpoint = %+v", flow)
|
|
791
|
+
}
|
|
792
|
+
step, ok := flow.Steps[1].(*catalog.Step)
|
|
793
|
+
if !ok || step.Label != "GET /v1/cancel" {
|
|
794
|
+
t.Fatalf("provider step = %+v", flow.Steps[1])
|
|
795
|
+
}
|
|
796
|
+
if !strings.Contains(step.Note, "Connector.Cancel → Client.Cancel") {
|
|
797
|
+
t.Fatalf("setter-wired wrapper chain = %q", step.Note)
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
func TestBuildsEndpointThroughCapabilityAssertion(t *testing.T) {
|
|
802
|
+
root := t.TempDir()
|
|
803
|
+
writeHTTPFixture(t, root, "go.mod", "module example.com/capability\n")
|
|
804
|
+
writeHTTPFixture(t, root, "app/main.go", `package app
|
|
805
|
+
import (
|
|
806
|
+
"example.com/capability/actions/documents"
|
|
807
|
+
"example.com/capability/connector"
|
|
808
|
+
)
|
|
809
|
+
type Router struct{}
|
|
810
|
+
func (*Router) POST(string, func()) {}
|
|
811
|
+
type Requester interface{ ConnExec(connector.API) }
|
|
812
|
+
func Start(r *Router) { r.POST("/documents", DocumentsAction) }
|
|
813
|
+
func DocumentsAction() {
|
|
814
|
+
request := documents.Request{}
|
|
815
|
+
ActionFlow(&request)
|
|
816
|
+
}
|
|
817
|
+
func ActionFlow(request Requester) {
|
|
818
|
+
conn := connector.Build("runtime")
|
|
819
|
+
request.ConnExec(conn)
|
|
820
|
+
}
|
|
821
|
+
`)
|
|
822
|
+
writeHTTPFixture(t, root, "actions/documents/request.go", `package documents
|
|
823
|
+
import "example.com/capability/connector"
|
|
824
|
+
type Request struct{}
|
|
825
|
+
func (*Request) ConnExec(conn connector.API) {
|
|
826
|
+
getter, ok := conn.(interface{ GetDocuments() })
|
|
827
|
+
if ok { getter.GetDocuments() }
|
|
828
|
+
}
|
|
829
|
+
`)
|
|
830
|
+
writeHTTPFixture(t, root, "connector/factory.go", `package connector
|
|
831
|
+
import (
|
|
832
|
+
"example.com/capability/provider/alpha"
|
|
833
|
+
"example.com/capability/provider/beta"
|
|
834
|
+
)
|
|
835
|
+
type API interface{}
|
|
836
|
+
func Build(name string) API {
|
|
837
|
+
switch name {
|
|
838
|
+
case "alpha": return alpha.New()
|
|
839
|
+
case "beta": return beta.New()
|
|
840
|
+
default: return nil
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
`)
|
|
844
|
+
writeHTTPFixture(t, root, "provider/alpha/client.go", `package alpha
|
|
845
|
+
import "net/http"
|
|
846
|
+
type Client struct{}
|
|
847
|
+
func New() *Client { return &Client{} }
|
|
848
|
+
func (*Client) GetDocuments() { _, _ = http.Get("https://alpha.example/v1/documents") }
|
|
849
|
+
`)
|
|
850
|
+
writeHTTPFixture(t, root, "provider/beta/client.go", `package beta
|
|
851
|
+
type Client struct{}
|
|
852
|
+
func New() *Client { return &Client{} }
|
|
853
|
+
`)
|
|
854
|
+
|
|
855
|
+
resp, err := extract(plugin.Input{Root: root}, Options{Context: "travel", Service: "documents"})
|
|
856
|
+
if err != nil {
|
|
857
|
+
t.Fatal(err)
|
|
858
|
+
}
|
|
859
|
+
var got catalog.Catalog
|
|
860
|
+
if err := json.Unmarshal([]byte(resp.Files[0].Contents), &got); err != nil {
|
|
861
|
+
t.Fatal(err)
|
|
862
|
+
}
|
|
863
|
+
if len(got.Flows) != 1 {
|
|
864
|
+
t.Fatalf("flows = %+v", got.Flows)
|
|
865
|
+
}
|
|
866
|
+
flow := got.Flows[0]
|
|
867
|
+
if flow.Name != "POST /documents → provider APIs" || len(flow.Steps) != 2 {
|
|
868
|
+
t.Fatalf("endpoint = %+v", flow)
|
|
869
|
+
}
|
|
870
|
+
step, ok := flow.Steps[1].(*catalog.Step)
|
|
871
|
+
if !ok || step.Label != "GET /v1/documents" {
|
|
872
|
+
t.Fatalf("capability step = %+v", flow.Steps[1])
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
func TestBuildsProviderBranchesFromConstructorMap(t *testing.T) {
|
|
877
|
+
root := t.TempDir()
|
|
878
|
+
writeHTTPFixture(t, root, "go.mod", "module example.com/registry\n")
|
|
879
|
+
writeHTTPFixture(t, root, "app/main.go", `package app
|
|
880
|
+
import "example.com/registry/connector"
|
|
881
|
+
type Router struct{}
|
|
882
|
+
func (*Router) POST(string, func()) {}
|
|
883
|
+
func Start(r *Router) { r.POST("/search", Search) }
|
|
884
|
+
func Search() {
|
|
885
|
+
conn := connector.Build("runtime")
|
|
886
|
+
conn.Search()
|
|
887
|
+
}
|
|
888
|
+
`)
|
|
889
|
+
writeHTTPFixture(t, root, "connector/factory.go", `package connector
|
|
890
|
+
import (
|
|
891
|
+
"example.com/registry/provider/alpha"
|
|
892
|
+
"example.com/registry/provider/beta"
|
|
893
|
+
)
|
|
894
|
+
type API interface{ Search() }
|
|
895
|
+
var providers = map[string]func() API{
|
|
896
|
+
"alpha": func() API { return alpha.New() },
|
|
897
|
+
"beta": beta.New,
|
|
898
|
+
}
|
|
899
|
+
func Build(name string) API { return providers[name]() }
|
|
900
|
+
`)
|
|
901
|
+
writeHTTPFixture(t, root, "provider/alpha/client.go", `package alpha
|
|
902
|
+
import "net/http"
|
|
903
|
+
type Client struct{}
|
|
904
|
+
func New() *Client { return &Client{} }
|
|
905
|
+
func (*Client) Search() { _, _ = http.Get("https://alpha.example/search") }
|
|
906
|
+
`)
|
|
907
|
+
writeHTTPFixture(t, root, "provider/beta/client.go", `package beta
|
|
908
|
+
import "net/http"
|
|
909
|
+
type Client struct{}
|
|
910
|
+
func New() *Client { return &Client{} }
|
|
911
|
+
func (*Client) Search() { _, _ = http.Get("https://beta.example/search") }
|
|
912
|
+
`)
|
|
913
|
+
|
|
914
|
+
resp, err := extract(plugin.Input{Root: root}, Options{Context: "travel", Service: "search"})
|
|
915
|
+
if err != nil {
|
|
916
|
+
t.Fatal(err)
|
|
917
|
+
}
|
|
918
|
+
var got catalog.Catalog
|
|
919
|
+
if err := json.Unmarshal([]byte(resp.Files[0].Contents), &got); err != nil {
|
|
920
|
+
t.Fatal(err)
|
|
921
|
+
}
|
|
922
|
+
if len(got.Flows) != 1 {
|
|
923
|
+
t.Fatalf("flows = %+v", got.Flows)
|
|
924
|
+
}
|
|
925
|
+
flow := got.Flows[0]
|
|
926
|
+
providers, ok := flow.Steps[1].(*catalog.Alt)
|
|
927
|
+
if !ok || len(providers.Branches) != 2 {
|
|
928
|
+
t.Fatalf("provider map branches = %+v", flow.Steps)
|
|
929
|
+
}
|
|
930
|
+
if providers.Branches[0].Title != `connector = "alpha"` || providers.Branches[1].Title != `connector = "beta"` {
|
|
931
|
+
t.Fatalf("provider map titles = %+v", providers.Branches)
|
|
932
|
+
}
|
|
933
|
+
for _, branch := range providers.Branches {
|
|
934
|
+
step := branch.Steps[len(branch.Steps)-1].(*catalog.Step)
|
|
935
|
+
if len(step.Reaches) != 1 || !strings.HasSuffix(step.Reaches[0], ":Client.Search") {
|
|
936
|
+
t.Fatalf("provider reachability = %+v", step.Reaches)
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
func TestBuildsDirectHTTPRootThroughClosureAndLocalVariable(t *testing.T) {
|
|
942
|
+
root := t.TempDir()
|
|
943
|
+
writeHTTPFixture(t, root, "go.mod", "module example.com/refresh\n")
|
|
944
|
+
writeHTTPFixture(t, root, "app/routes.go", `package app
|
|
945
|
+
import "example.com/refresh/cache"
|
|
946
|
+
type Router struct{}
|
|
947
|
+
func (*Router) POST(string, func()) {}
|
|
948
|
+
func Routes(r *Router) {
|
|
949
|
+
state := cache.New()
|
|
950
|
+
r.POST("/debug/refresh", func() { _ = state.Update() })
|
|
951
|
+
}
|
|
952
|
+
`)
|
|
953
|
+
writeHTTPFixture(t, root, "cache/cache.go", `package cache
|
|
954
|
+
import "net/http"
|
|
955
|
+
type Cache struct{}
|
|
956
|
+
func New() *Cache { return &Cache{} }
|
|
957
|
+
func (*Cache) Update() error {
|
|
958
|
+
_, err := http.Get("https://geo.example/v1/airports")
|
|
959
|
+
return err
|
|
960
|
+
}
|
|
961
|
+
`)
|
|
962
|
+
|
|
963
|
+
resp, err := extract(plugin.Input{Root: root}, Options{Context: "ops", Service: "catalog"})
|
|
964
|
+
if err != nil {
|
|
965
|
+
t.Fatal(err)
|
|
966
|
+
}
|
|
967
|
+
var got catalog.Catalog
|
|
968
|
+
if err := json.Unmarshal([]byte(resp.Files[0].Contents), &got); err != nil {
|
|
969
|
+
t.Fatal(err)
|
|
970
|
+
}
|
|
971
|
+
if len(got.Flows) != 1 {
|
|
972
|
+
t.Fatalf("flows = %+v", got.Flows)
|
|
973
|
+
}
|
|
974
|
+
flow := got.Flows[0]
|
|
975
|
+
if flow.Name != "POST /debug/refresh → outbound APIs" || len(flow.Steps) != 2 {
|
|
976
|
+
t.Fatalf("rooted flow = %+v", flow)
|
|
977
|
+
}
|
|
978
|
+
if flow.Trigger == nil || flow.Trigger.Kind != "http" || flow.Trigger.Confidence != "high" {
|
|
979
|
+
t.Fatalf("trigger = %+v", flow.Trigger)
|
|
980
|
+
}
|
|
981
|
+
if step := flow.Steps[1].(*catalog.Step); step.Label != "GET /v1/airports" || len(step.Reaches) != 1 || step.Reaches[0] != "cache:Cache.Update" {
|
|
982
|
+
t.Fatalf("outbound step = %+v", step)
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
func TestBuildsAnnotatedCallbackRootAndCoversNestedHelper(t *testing.T) {
|
|
987
|
+
root := t.TempDir()
|
|
988
|
+
writeHTTPFixture(t, root, "go.mod", "module example.com/callback\n")
|
|
989
|
+
writeHTTPFixture(t, root, "callbacks/callback.go", `package callbacks
|
|
990
|
+
import "net/http"
|
|
991
|
+
type Registry struct{}
|
|
992
|
+
func (*Registry) Register(string, string, func()) {}
|
|
993
|
+
func Mount(registry *Registry) {
|
|
994
|
+
registry.Register("provider", "completed", Completed())
|
|
995
|
+
}
|
|
996
|
+
// Completed receives the provider webhook.
|
|
997
|
+
// @Router /callbacks/provider/completed [post]
|
|
998
|
+
func Completed() func() {
|
|
999
|
+
return func() { notifyCore() }
|
|
1000
|
+
}
|
|
1001
|
+
func notifyCore() { _, _ = http.Post("https://core.example/protected/update", "application/json", nil) }
|
|
1002
|
+
`)
|
|
1003
|
+
|
|
1004
|
+
resp, err := extract(plugin.Input{Root: root}, Options{Context: "travel", Service: "callbacks"})
|
|
1005
|
+
if err != nil {
|
|
1006
|
+
t.Fatal(err)
|
|
1007
|
+
}
|
|
1008
|
+
var got catalog.Catalog
|
|
1009
|
+
if err := json.Unmarshal([]byte(resp.Files[0].Contents), &got); err != nil {
|
|
1010
|
+
t.Fatal(err)
|
|
1011
|
+
}
|
|
1012
|
+
if len(got.Flows) != 1 {
|
|
1013
|
+
t.Fatalf("flows = %+v", got.Flows)
|
|
1014
|
+
}
|
|
1015
|
+
flow := got.Flows[0]
|
|
1016
|
+
if flow.Name != "POST /callbacks/provider/completed → outbound APIs" || len(flow.Steps) != 2 {
|
|
1017
|
+
t.Fatalf("callback flow = %+v", flow)
|
|
1018
|
+
}
|
|
1019
|
+
if flow.Trigger == nil || flow.Trigger.Kind != "callback" || flow.Trigger.Confidence != "high" {
|
|
1020
|
+
t.Fatalf("trigger = %+v", flow.Trigger)
|
|
1021
|
+
}
|
|
1022
|
+
if flow.Source != "callbacks/callback.go:6" {
|
|
1023
|
+
t.Fatalf("registration source = %q", flow.Source)
|
|
1024
|
+
}
|
|
1025
|
+
step := flow.Steps[1].(*catalog.Step)
|
|
1026
|
+
if step.Label != "POST /protected/update" || !strings.Contains(step.Note, "Completed → notifyCore") {
|
|
1027
|
+
t.Fatalf("callback continuation = %+v", step)
|
|
1028
|
+
}
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
func TestBuildsStartupRootFromMainAssembly(t *testing.T) {
|
|
1032
|
+
root := t.TempDir()
|
|
1033
|
+
writeHTTPFixture(t, root, "go.mod", "module example.com/startup\n")
|
|
1034
|
+
writeHTTPFixture(t, root, "cmd/catalog/main.go", `package main
|
|
1035
|
+
import app "example.com/startup/internal/app/catalog"
|
|
1036
|
+
func main() { app.Run() }
|
|
1037
|
+
`)
|
|
1038
|
+
writeHTTPFixture(t, root, "internal/app/catalog/run.go", `package catalog
|
|
1039
|
+
import "example.com/startup/cache"
|
|
1040
|
+
func Run() {
|
|
1041
|
+
state := cache.New()
|
|
1042
|
+
if err := state.Warm(); err != nil { panic(err) }
|
|
1043
|
+
}
|
|
1044
|
+
`)
|
|
1045
|
+
writeHTTPFixture(t, root, "cache/cache.go", `package cache
|
|
1046
|
+
import "net/http"
|
|
1047
|
+
type Cache struct{}
|
|
1048
|
+
func New() *Cache { return &Cache{} }
|
|
1049
|
+
func (*Cache) Warm() error {
|
|
1050
|
+
_, err := http.Get("https://geo.example/v1/bootstrap")
|
|
1051
|
+
return err
|
|
1052
|
+
}
|
|
1053
|
+
`)
|
|
1054
|
+
|
|
1055
|
+
resp, err := extract(plugin.Input{Root: root}, Options{Context: "ops", Service: "catalog"})
|
|
1056
|
+
if err != nil {
|
|
1057
|
+
t.Fatal(err)
|
|
1058
|
+
}
|
|
1059
|
+
var got catalog.Catalog
|
|
1060
|
+
if err := json.Unmarshal([]byte(resp.Files[0].Contents), &got); err != nil {
|
|
1061
|
+
t.Fatal(err)
|
|
1062
|
+
}
|
|
1063
|
+
if len(got.Flows) != 1 {
|
|
1064
|
+
t.Fatalf("flows = %+v", got.Flows)
|
|
1065
|
+
}
|
|
1066
|
+
flow := got.Flows[0]
|
|
1067
|
+
if flow.Trigger == nil || flow.Trigger.Kind != "startup" || flow.Trigger.Confidence != "high" {
|
|
1068
|
+
t.Fatalf("trigger = %+v", flow.Trigger)
|
|
1069
|
+
}
|
|
1070
|
+
if flow.Name != "Startup → Cache.Warm → outbound APIs" || flow.Source != "internal/app/catalog/run.go:5" {
|
|
1071
|
+
t.Fatalf("startup flow = %+v", flow)
|
|
1072
|
+
}
|
|
1073
|
+
}
|
|
1074
|
+
|
|
1075
|
+
func TestBuildsScheduledRootFromTimerRegistration(t *testing.T) {
|
|
1076
|
+
root := t.TempDir()
|
|
1077
|
+
writeHTTPFixture(t, root, "go.mod", "module example.com/scheduled\n")
|
|
1078
|
+
writeHTTPFixture(t, root, "jobs/refresh.go", `package jobs
|
|
1079
|
+
import (
|
|
1080
|
+
"net/http"
|
|
1081
|
+
"time"
|
|
1082
|
+
)
|
|
1083
|
+
func Register() {
|
|
1084
|
+
time.AfterFunc(5*time.Minute, func() { Refresh() })
|
|
1085
|
+
}
|
|
1086
|
+
func Refresh() { _, _ = http.Get("https://catalog.example/v1/refresh") }
|
|
1087
|
+
`)
|
|
1088
|
+
|
|
1089
|
+
resp, err := extract(plugin.Input{Root: root}, Options{Context: "ops", Service: "catalog"})
|
|
1090
|
+
if err != nil {
|
|
1091
|
+
t.Fatal(err)
|
|
1092
|
+
}
|
|
1093
|
+
var got catalog.Catalog
|
|
1094
|
+
if err := json.Unmarshal([]byte(resp.Files[0].Contents), &got); err != nil {
|
|
1095
|
+
t.Fatal(err)
|
|
1096
|
+
}
|
|
1097
|
+
if len(got.Flows) != 1 {
|
|
1098
|
+
t.Fatalf("flows = %+v", got.Flows)
|
|
1099
|
+
}
|
|
1100
|
+
flow := got.Flows[0]
|
|
1101
|
+
if flow.Trigger == nil || flow.Trigger.Kind != "scheduled" || flow.Trigger.Confidence != "high" {
|
|
1102
|
+
t.Fatalf("trigger = %+v", flow.Trigger)
|
|
1103
|
+
}
|
|
1104
|
+
if !strings.Contains(flow.Name, "Schedule 5 * time.Minute → Refresh") || len(flow.Steps) != 2 {
|
|
1105
|
+
t.Fatalf("scheduled flow = %+v", flow)
|
|
1106
|
+
}
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
func TestProviderInitIsBusinessOperationAndIncludesNestedHelper(t *testing.T) {
|
|
1110
|
+
root := t.TempDir()
|
|
1111
|
+
writeHTTPFixture(t, root, "go.mod", "module example.com/providerinit\n")
|
|
1112
|
+
writeHTTPFixture(t, root, "app/routes.go", `package app
|
|
1113
|
+
import "example.com/providerinit/connector"
|
|
1114
|
+
type Router struct{}
|
|
1115
|
+
func (*Router) POST(string, func()) {}
|
|
1116
|
+
func Routes(r *Router) { r.POST("/initialize", Initialize) }
|
|
1117
|
+
func Initialize() {
|
|
1118
|
+
conn := connector.Build()
|
|
1119
|
+
conn.Init()
|
|
1120
|
+
}
|
|
1121
|
+
`)
|
|
1122
|
+
writeHTTPFixture(t, root, "connector/factory.go", `package connector
|
|
1123
|
+
import "example.com/providerinit/provider"
|
|
1124
|
+
type API interface{ Init() }
|
|
1125
|
+
func Build() API { return provider.New() }
|
|
1126
|
+
`)
|
|
1127
|
+
writeHTTPFixture(t, root, "provider/provider.go", `package provider
|
|
1128
|
+
import "net/http"
|
|
1129
|
+
type Connector struct{}
|
|
1130
|
+
func New() *Connector { return &Connector{} }
|
|
1131
|
+
func (c *Connector) Init() { c.waitUntilReady() }
|
|
1132
|
+
func (*Connector) waitUntilReady() { _, _ = http.Get("https://provider.example/v1/status") }
|
|
1133
|
+
`)
|
|
1134
|
+
|
|
1135
|
+
resp, err := extract(plugin.Input{Root: root}, Options{Context: "travel", Service: "booking"})
|
|
1136
|
+
if err != nil {
|
|
1137
|
+
t.Fatal(err)
|
|
1138
|
+
}
|
|
1139
|
+
var got catalog.Catalog
|
|
1140
|
+
if err := json.Unmarshal([]byte(resp.Files[0].Contents), &got); err != nil {
|
|
1141
|
+
t.Fatal(err)
|
|
1142
|
+
}
|
|
1143
|
+
if len(got.Flows) != 1 {
|
|
1144
|
+
t.Fatalf("flows = %+v", got.Flows)
|
|
1145
|
+
}
|
|
1146
|
+
flow := got.Flows[0]
|
|
1147
|
+
if flow.Name != "POST /initialize → provider APIs" || len(flow.Steps) != 2 {
|
|
1148
|
+
t.Fatalf("endpoint flow = %+v", flow)
|
|
1149
|
+
}
|
|
1150
|
+
step := flow.Steps[1].(*catalog.Step)
|
|
1151
|
+
if step.Label != "GET /v1/status" || !strings.Contains(step.Note, "Connector.Init → Connector.waitUntilReady") {
|
|
1152
|
+
t.Fatalf("nested Init flow = %+v", step)
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
func TestDoesNotGuessBetweenMultipleConstructorWiredImplementations(t *testing.T) {
|
|
1157
|
+
root := t.TempDir()
|
|
1158
|
+
writeHTTPFixture(t, root, "go.mod", "module example.com/ambiguous\n")
|
|
1159
|
+
writeHTTPFixture(t, root, "port/api.go", `package port
|
|
1160
|
+
type API interface{ Cancel() }
|
|
1161
|
+
`)
|
|
1162
|
+
writeHTTPFixture(t, root, "connector/connector.go", `package connector
|
|
1163
|
+
import (
|
|
1164
|
+
"example.com/ambiguous/port"
|
|
1165
|
+
"example.com/ambiguous/provider/alpha"
|
|
1166
|
+
"example.com/ambiguous/provider/beta"
|
|
1167
|
+
)
|
|
1168
|
+
type Connector struct{ client port.API }
|
|
1169
|
+
func NewAlpha() *Connector { return &Connector{client: &alpha.Client{}} }
|
|
1170
|
+
func NewBeta() *Connector { return &Connector{client: &beta.Client{}} }
|
|
1171
|
+
func (c *Connector) Cancel() { c.client.Cancel() }
|
|
1172
|
+
`)
|
|
1173
|
+
writeHTTPFixture(t, root, "provider/alpha/client.go", `package alpha
|
|
1174
|
+
import "net/http"
|
|
1175
|
+
type Client struct{}
|
|
1176
|
+
func (*Client) Cancel() { _, _ = http.Get("https://alpha.example/cancel") }
|
|
1177
|
+
`)
|
|
1178
|
+
writeHTTPFixture(t, root, "provider/beta/client.go", `package beta
|
|
1179
|
+
import "net/http"
|
|
1180
|
+
type Client struct{}
|
|
1181
|
+
func (*Client) Cancel() { _, _ = http.Get("https://beta.example/cancel") }
|
|
1182
|
+
`)
|
|
1183
|
+
|
|
1184
|
+
resp, err := extract(plugin.Input{Root: root}, Options{Context: "travel", Service: "cancel"})
|
|
1185
|
+
if err != nil {
|
|
1186
|
+
t.Fatal(err)
|
|
1187
|
+
}
|
|
1188
|
+
var got catalog.Catalog
|
|
1189
|
+
if err := json.Unmarshal([]byte(resp.Files[0].Contents), &got); err != nil {
|
|
1190
|
+
t.Fatal(err)
|
|
1191
|
+
}
|
|
1192
|
+
if len(got.Flows) != 2 {
|
|
1193
|
+
t.Fatalf("ambiguous wiring created a composed flow: %+v", got.Flows)
|
|
1194
|
+
}
|
|
1195
|
+
for _, flow := range got.Flows {
|
|
1196
|
+
if flow.Name == "Connector Cancel → outbound APIs" {
|
|
1197
|
+
t.Fatalf("ambiguous receiver was guessed: %+v", flow)
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
func TestCoveredEndpointCallRemovesWrapperWithoutAChainName(t *testing.T) {
|
|
1203
|
+
call := gohttp.Call{
|
|
1204
|
+
Function: "provider:Client.Send",
|
|
1205
|
+
ID: "http-client/POST /send",
|
|
1206
|
+
Source: gohttp.Source{File: "provider/client.go", Line: 42},
|
|
1207
|
+
}
|
|
1208
|
+
groups := []gohttp.FlowGroup{{Function: "provider:Connector.Publish", Calls: []gohttp.Call{call}}}
|
|
1209
|
+
endpoints := []gohttp.EndpointFlow{{Branches: []gohttp.EndpointBranch{{
|
|
1210
|
+
Function: "provider:Client.Send",
|
|
1211
|
+
Calls: []gohttp.Call{call},
|
|
1212
|
+
}}}}
|
|
1213
|
+
covered := map[string]bool{}
|
|
1214
|
+
|
|
1215
|
+
coverEndpointDescendants(covered, endpoints, groups)
|
|
1216
|
+
|
|
1217
|
+
if !covered["provider:Connector.Publish"] {
|
|
1218
|
+
t.Fatalf("exact source-backed wrapper was not covered: %+v", covered)
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
func TestOnlySourceBackedComplementsBecomeAnAlt(t *testing.T) {
|
|
1223
|
+
context := "edge"
|
|
1224
|
+
calls := []gohttp.Call{
|
|
1225
|
+
{Function: "choose", ID: "http-client/GET /one", Method: "GET", Path: "/one", Conditions: []string{"ready"}},
|
|
1226
|
+
{Function: "choose", ID: "http-client/GET /two", Method: "GET", Path: "/two", Conditions: []string{"not (ready)"}},
|
|
1227
|
+
}
|
|
1228
|
+
flows := flowsOf("edge.gateway", context, calls, Options{})
|
|
1229
|
+
if len(flows) != 1 || len(flows[0].Steps) != 1 {
|
|
1230
|
+
t.Fatalf("flows = %+v", flows)
|
|
1231
|
+
}
|
|
1232
|
+
alt, ok := flows[0].Steps[0].(*catalog.Alt)
|
|
1233
|
+
if !ok || alt.Branches[0].Title != "ready" || alt.Branches[1].Title != "not (ready)" {
|
|
1234
|
+
t.Fatalf("alt = %+v", flows[0].Steps[0])
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
calls[1].Conditions = nil
|
|
1238
|
+
flows = flowsOf("edge.gateway", context, calls, Options{})
|
|
1239
|
+
if _, ok := flows[0].Steps[0].(*catalog.Alt); ok {
|
|
1240
|
+
t.Fatal("unrelated calls became an alternative")
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
func TestLearnsHandWrittenSOAPWrapperArgumentPositions(t *testing.T) {
|
|
1245
|
+
root := t.TempDir()
|
|
1246
|
+
writeHTTPFixture(t, root, "go.mod", "module example.com/checkout\n")
|
|
1247
|
+
writeHTTPFixture(t, root, "soap/booking.wsdl", `
|
|
1248
|
+
<definitions xmlns="http://schemas.xmlsoap.org/wsdl/" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:tns="urn:booking" targetNamespace="urn:booking" name="Booking">
|
|
1249
|
+
<message name="CancelRequest"/><message name="CancelResponse"/>
|
|
1250
|
+
<portType name="BookingPortType"><operation name="Cancel"><input message="tns:CancelRequest"/><output message="tns:CancelResponse"/></operation></portType>
|
|
1251
|
+
<binding name="BookingBinding" type="tns:BookingPortType"><soap:binding style="document"/><operation name="Cancel"><soap:operation soapAction="urn:booking:cancel"/></operation></binding>
|
|
1252
|
+
<service name="BookingService"><port name="BookingPort" binding="tns:BookingBinding"><soap:address location="https://booking.example/soap"/></port></service>
|
|
1253
|
+
</definitions>`)
|
|
1254
|
+
writeHTTPFixture(t, root, "soap/transport.go", `package soap
|
|
1255
|
+
import "net/http"
|
|
1256
|
+
type Transport struct{}
|
|
1257
|
+
func (t *Transport) Call(actionBase, operation string, messageID string, query, reply any) error {
|
|
1258
|
+
req, err := http.NewRequest(http.MethodPost, "https://booking.example/soap", nil)
|
|
1259
|
+
if err != nil { return err }
|
|
1260
|
+
req.Header.Add("SOAPAction", actionBase+operation)
|
|
1261
|
+
return nil
|
|
1262
|
+
}
|
|
1263
|
+
|
|
1264
|
+
type Service struct{ transport *Transport }
|
|
1265
|
+
func (s *Service) Call(actionBase, operation string, query, reply any, stage string, options ...func()) error {
|
|
1266
|
+
return s.transport.Call(actionBase, operation, "message-id", query, reply)
|
|
1267
|
+
}`)
|
|
1268
|
+
writeHTTPFixture(t, root, "checkout/cancel.go", `package checkout
|
|
1269
|
+
import soapclient "example.com/checkout/soap"
|
|
1270
|
+
const actionBase = "urn:booking:"
|
|
1271
|
+
func CancelBooking(client *soapclient.Service, request, response any) error {
|
|
1272
|
+
return client.Call(actionBase, "cancel", request, response, "checkout")
|
|
1273
|
+
}`)
|
|
1274
|
+
|
|
1275
|
+
resp, err := extract(plugin.Input{Root: root}, Options{Context: "checkout", Service: "gateway"})
|
|
1276
|
+
if err != nil {
|
|
1277
|
+
t.Fatal(err)
|
|
1278
|
+
}
|
|
1279
|
+
var got catalog.Catalog
|
|
1280
|
+
if err := json.Unmarshal([]byte(resp.Files[0].Contents), &got); err != nil {
|
|
1281
|
+
t.Fatal(err)
|
|
1282
|
+
}
|
|
1283
|
+
service := got.Contexts[0].Services[0]
|
|
1284
|
+
if len(service.Consumes) != 1 {
|
|
1285
|
+
t.Fatalf("consumes = %+v", service.Consumes)
|
|
1286
|
+
}
|
|
1287
|
+
call := service.Consumes[0]
|
|
1288
|
+
if call.ID != "bookingservice.soap.BookingPort/Cancel" || call.Status != catalog.StatusDeclared || call.Source != "soap/booking.wsdl" {
|
|
1289
|
+
t.Fatalf("call = %+v", call)
|
|
1290
|
+
}
|
|
1291
|
+
if !strings.Contains(call.Note, "request 'request'") || !strings.Contains(call.Note, "response 'response'") {
|
|
1292
|
+
t.Fatalf("note = %q", call.Note)
|
|
1293
|
+
}
|
|
1294
|
+
if len(got.Flows) != 1 || got.Flows[0].Source != "checkout/cancel.go:4" {
|
|
1295
|
+
t.Fatalf("flows = %+v", got.Flows)
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
|
|
1299
|
+
func TestReadsSOAP12ActionFromContentType(t *testing.T) {
|
|
1300
|
+
root := t.TempDir()
|
|
1301
|
+
writeHTTPFixture(t, root, "go.mod", "module example.com/soap12\n")
|
|
1302
|
+
writeHTTPFixture(t, root, "contract.wsdl", `
|
|
1303
|
+
<definitions xmlns="http://schemas.xmlsoap.org/wsdl/" xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/" xmlns:soap12="http://schemas.xmlsoap.org/wsdl/soap12/" xmlns:tns="urn:status" targetNamespace="urn:status" name="Status">
|
|
1304
|
+
<portType name="StatusPortType"><operation name="Check"/></portType>
|
|
1305
|
+
<binding name="Status11Binding" type="tns:StatusPortType"><soap:binding style="document"/><operation name="Check"><soap:operation soapAction="urn:status:check"/></operation></binding>
|
|
1306
|
+
<binding name="Status12Binding" type="tns:StatusPortType"><soap12:binding style="document"/><operation name="Check"><soap12:operation soapAction="urn:status:check"/></operation></binding>
|
|
1307
|
+
<service name="StatusService">
|
|
1308
|
+
<port name="Status11Port" binding="tns:Status11Binding"><soap:address location="https://status.example/soap11"/></port>
|
|
1309
|
+
<port name="Status12Port" binding="tns:Status12Binding"><soap12:address location="https://status.example/soap12"/></port>
|
|
1310
|
+
</service>
|
|
1311
|
+
</definitions>`)
|
|
1312
|
+
writeHTTPFixture(t, root, "transport.go", `package soap12
|
|
1313
|
+
import "net/http"
|
|
1314
|
+
type Client struct{}
|
|
1315
|
+
func (c *Client) Send(action string, request, response any) error {
|
|
1316
|
+
req, err := http.NewRequest(http.MethodPost, "https://status.example/soap", nil)
|
|
1317
|
+
if err != nil { return err }
|
|
1318
|
+
req.Header.Set("Content-Type", "application/soap+xml; charset=utf-8; action=\""+action+"\"")
|
|
1319
|
+
return nil
|
|
1320
|
+
}
|
|
1321
|
+
func Check(client *Client, request, response any) error {
|
|
1322
|
+
return client.Send("urn:status:check", request, response)
|
|
1323
|
+
}`)
|
|
1324
|
+
|
|
1325
|
+
resp, err := extract(plugin.Input{Root: root}, Options{Context: "monitoring", Service: "probe"})
|
|
1326
|
+
if err != nil {
|
|
1327
|
+
t.Fatal(err)
|
|
1328
|
+
}
|
|
1329
|
+
var got catalog.Catalog
|
|
1330
|
+
if err := json.Unmarshal([]byte(resp.Files[0].Contents), &got); err != nil {
|
|
1331
|
+
t.Fatal(err)
|
|
1332
|
+
}
|
|
1333
|
+
calls := got.Contexts[0].Services[0].Consumes
|
|
1334
|
+
if len(calls) != 1 || calls[0].ID != "statusservice.soap.Status12Port/Check" || calls[0].Status != catalog.StatusDeclared {
|
|
1335
|
+
t.Fatalf("calls = %+v", calls)
|
|
1336
|
+
}
|
|
1337
|
+
method := got.Externals[0].Provides[0].Methods[0]
|
|
1338
|
+
if method.SOAP == nil || method.SOAP.Version != "1.2" || method.SOAP.Action != "urn:status:check" {
|
|
1339
|
+
t.Fatalf("method = %+v", method)
|
|
1340
|
+
}
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
func TestDestinationThroughFunctionalOption(t *testing.T) {
|
|
1344
|
+
for _, ambiguous := range []bool{false, true} {
|
|
1345
|
+
t.Run(fmt.Sprint(ambiguous), func(t *testing.T) {
|
|
1346
|
+
root := t.TempDir()
|
|
1347
|
+
writeHTTPFixture(t, root, "go.mod", "module example.com/destination\n")
|
|
1348
|
+
writeHTTPFixture(t, root, "client.go", `package destination
|
|
1349
|
+
import "net/http"
|
|
1350
|
+
type Config struct { SettingAddr string `+"`envconfig:\"SETTINGS_ADDR\" default:\"http://localhost:8000/settings\"`"+` }
|
|
1351
|
+
type Client struct { baseURL string }
|
|
1352
|
+
type Option func(*Client)
|
|
1353
|
+
func WithBaseURL(value string) Option { return func(c *Client) { c.baseURL = value } }
|
|
1354
|
+
func NewClient(options ...Option) *Client {
|
|
1355
|
+
c := &Client{}
|
|
1356
|
+
for _, opt := range options { opt(c) }
|
|
1357
|
+
return c
|
|
1358
|
+
}
|
|
1359
|
+
func runtimeURL(path string) string { return path }
|
|
1360
|
+
func (c *Client) request(method, path string) { path = c.baseURL + path; path = runtimeURL(path); http.NewRequest(method, path, nil) }
|
|
1361
|
+
func (c *Client) POST(path string) { c.request(http.MethodPost, path) }
|
|
1362
|
+
type Manager struct { client *Client }
|
|
1363
|
+
func New(client *Client) *Manager { return &Manager{client: client} }
|
|
1364
|
+
func Build(cfg Config) *Manager { return New(NewClient(WithBaseURL(cfg.SettingAddr))) }
|
|
1365
|
+
func (m *Manager) Settings() { m.client.POST("/get-admin-settings") }
|
|
1366
|
+
type Other struct { client *Client }
|
|
1367
|
+
func NewOther(client *Client) *Other { return &Other{client: client} }
|
|
1368
|
+
func BuildOther() *Other { return NewOther(NewClient(WithBaseURL("http://other/billing"))) }
|
|
1369
|
+
func (m *Other) Settings() { m.client.POST("/get-admin-settings") }
|
|
1370
|
+
`)
|
|
1371
|
+
if ambiguous {
|
|
1372
|
+
writeHTTPFixture(t, root, "other.go", `package destination
|
|
1373
|
+
func Unknown(value string) *Manager { return New(NewClient(WithBaseURL(value))) }
|
|
1374
|
+
`)
|
|
1375
|
+
}
|
|
1376
|
+
resp, err := extract(plugin.Input{Root: root}, Options{Context: "avia", Service: "aviacore"})
|
|
1377
|
+
if err != nil {
|
|
1378
|
+
t.Fatal(err)
|
|
1379
|
+
}
|
|
1380
|
+
var got catalog.Catalog
|
|
1381
|
+
if err := json.Unmarshal([]byte(resp.Files[0].Contents), &got); err != nil {
|
|
1382
|
+
t.Fatal(err)
|
|
1383
|
+
}
|
|
1384
|
+
found := false
|
|
1385
|
+
for _, flow := range got.Flows {
|
|
1386
|
+
if flow.EntryPoint == "Other.Settings" {
|
|
1387
|
+
step := flow.Steps[0].(*catalog.Step)
|
|
1388
|
+
if d := step.Destination; d == nil || d.FullPath != "/billing/get-admin-settings" || d.ServiceDiscoveryAlias != "other" {
|
|
1389
|
+
t.Fatalf("other client destination = %+v", d)
|
|
1390
|
+
}
|
|
1391
|
+
}
|
|
1392
|
+
if flow.EntryPoint != "Manager.Settings" {
|
|
1393
|
+
continue
|
|
1394
|
+
}
|
|
1395
|
+
for _, node := range flow.Steps {
|
|
1396
|
+
step, ok := node.(*catalog.Step)
|
|
1397
|
+
if !ok || step.Kind != catalog.StepRPC {
|
|
1398
|
+
continue
|
|
1399
|
+
}
|
|
1400
|
+
found = true
|
|
1401
|
+
d := step.Destination
|
|
1402
|
+
if d == nil {
|
|
1403
|
+
t.Fatalf("missing destination: %+v", step)
|
|
1404
|
+
}
|
|
1405
|
+
if ambiguous {
|
|
1406
|
+
if d.FullPath != "" {
|
|
1407
|
+
t.Fatalf("ambiguous destination resolved: %+v", d)
|
|
1408
|
+
}
|
|
1409
|
+
continue
|
|
1410
|
+
}
|
|
1411
|
+
if d.FullPath != "/settings/get-admin-settings" || d.LocalPath != "/get-admin-settings" || d.BaseURL == nil || d.BaseURL.ConfigField != "Config.SettingAddr" || d.BaseURL.EnvironmentVariable != "SETTINGS_ADDR" || d.BaseURL.Kind != "config-default" || d.Join == nil || d.Join.Expression != "c.baseURL + path" || len(d.Transforms) != 1 || d.Transforms[0].Expression != "runtimeURL(path)" {
|
|
1412
|
+
t.Fatalf("destination = %+v; base = %+v", d, d.BaseURL)
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
}
|
|
1416
|
+
if !found {
|
|
1417
|
+
t.Fatalf("no Manager.Settings flow: %+v", got.Flows)
|
|
1418
|
+
}
|
|
1419
|
+
})
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
// PORTOLAN-19: the verb and path are not a call identity. One shared request
|
|
1424
|
+
// helper addressed to two hosts is two dependencies, and neither the analyzer's
|
|
1425
|
+
// call-site deduplication nor the consumer list may glue them together.
|
|
1426
|
+
func TestSameRouteToDifferentHostsStaysTwoCalls(t *testing.T) {
|
|
1427
|
+
root := t.TempDir()
|
|
1428
|
+
writeHTTPFixture(t, root, "go.mod", "module example.com/clients\n")
|
|
1429
|
+
writeHTTPFixture(t, root, "client.go", `package clients
|
|
1430
|
+
import "net/http"
|
|
1431
|
+
type Client struct { baseURL string }
|
|
1432
|
+
type Option func(*Client)
|
|
1433
|
+
func WithBaseURL(value string) Option { return func(c *Client) { c.baseURL = value } }
|
|
1434
|
+
func NewClient(options ...Option) *Client {
|
|
1435
|
+
c := &Client{}
|
|
1436
|
+
for _, opt := range options { opt(c) }
|
|
1437
|
+
return c
|
|
1438
|
+
}
|
|
1439
|
+
func (c *Client) request(method, path string) { path = c.baseURL + path; http.NewRequest(method, path, nil) }
|
|
1440
|
+
func (c *Client) post(path string) { c.request(http.MethodPost, path) }
|
|
1441
|
+
type Payments struct { client *Client }
|
|
1442
|
+
func NewPayments(client *Client) *Payments { return &Payments{client: client} }
|
|
1443
|
+
func BuildPayments() *Payments { return NewPayments(NewClient(WithBaseURL("https://payments.internal"))) }
|
|
1444
|
+
func (p *Payments) Settle() { p.client.post("/foo") }
|
|
1445
|
+
type Ledger struct { client *Client }
|
|
1446
|
+
func NewLedger(client *Client) *Ledger { return &Ledger{client: client} }
|
|
1447
|
+
func BuildLedger() *Ledger { return NewLedger(NewClient(WithBaseURL("https://ledger.internal"))) }
|
|
1448
|
+
func (l *Ledger) Record() { l.client.post("/foo") }
|
|
1449
|
+
`)
|
|
1450
|
+
resp, err := extract(plugin.Input{Root: root}, Options{Context: "shop", Service: "checkout"})
|
|
1451
|
+
if err != nil {
|
|
1452
|
+
t.Fatal(err)
|
|
1453
|
+
}
|
|
1454
|
+
var got catalog.Catalog
|
|
1455
|
+
if err := json.Unmarshal([]byte(resp.Files[0].Contents), &got); err != nil {
|
|
1456
|
+
t.Fatal(err)
|
|
1457
|
+
}
|
|
1458
|
+
consumes := got.Contexts[0].Services[0].Consumes
|
|
1459
|
+
if len(consumes) != 2 {
|
|
1460
|
+
t.Fatalf("consumes = %+v", consumes)
|
|
1461
|
+
}
|
|
1462
|
+
byID := map[string]catalog.RpcCall{}
|
|
1463
|
+
for _, call := range consumes {
|
|
1464
|
+
byID[call.ID] = call
|
|
1465
|
+
}
|
|
1466
|
+
for host, peer := range map[string]string{"ledger.internal": "ledger-internal", "payments.internal": "payments-internal"} {
|
|
1467
|
+
call, ok := byID["http-client/POST /foo @ "+host]
|
|
1468
|
+
if !ok {
|
|
1469
|
+
t.Fatalf("no consume for %s in %+v", host, consumes)
|
|
1470
|
+
}
|
|
1471
|
+
if call.Peer != peer || call.Status != catalog.StatusUnresolved || call.Destination == nil || call.Destination.ServiceDiscoveryAlias != host || call.Destination.FullPath != "/foo" {
|
|
1472
|
+
t.Fatalf("%s = %+v (destination %+v)", host, call, call.Destination)
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
refs := map[string]string{}
|
|
1476
|
+
for _, flow := range got.Flows {
|
|
1477
|
+
for _, node := range flow.Steps {
|
|
1478
|
+
if step, ok := node.(*catalog.Step); ok && step.Kind == catalog.StepRPC {
|
|
1479
|
+
refs[flow.EntryPoint] = step.Ref
|
|
1480
|
+
}
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
if refs["Payments.Settle"] != "http-client/POST /foo @ payments.internal" || refs["Ledger.Record"] != "http-client/POST /foo @ ledger.internal" {
|
|
1484
|
+
t.Fatalf("flow refs = %+v", refs)
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
// Several call sites addressed to one destination are one dependency: the entry
|
|
1489
|
+
// keeps the first site in source order and every site as evidence, and the
|
|
1490
|
+
// order files are visited in cannot change which entries exist.
|
|
1491
|
+
func TestCallSitesToOneDestinationAggregateWithProvenance(t *testing.T) {
|
|
1492
|
+
settle := `package clients
|
|
1493
|
+
import "net/http"
|
|
1494
|
+
func Settle() { http.Post("https://payments.internal/foo", "application/json", nil) }
|
|
1495
|
+
`
|
|
1496
|
+
retry := `package clients
|
|
1497
|
+
import "net/http"
|
|
1498
|
+
func Retry() { http.Post("https://payments.internal/foo", "application/json", nil) }
|
|
1499
|
+
`
|
|
1500
|
+
record := `package clients
|
|
1501
|
+
import "net/http"
|
|
1502
|
+
func Record() { http.Post("https://ledger.internal/foo", "application/json", nil) }
|
|
1503
|
+
`
|
|
1504
|
+
run := func(files map[string]string) []catalog.RpcCall {
|
|
1505
|
+
root := t.TempDir()
|
|
1506
|
+
writeHTTPFixture(t, root, "go.mod", "module example.com/clients\n")
|
|
1507
|
+
for name, contents := range files {
|
|
1508
|
+
writeHTTPFixture(t, root, name, contents)
|
|
1509
|
+
}
|
|
1510
|
+
resp, err := extract(plugin.Input{Root: root}, Options{Context: "shop", Service: "checkout"})
|
|
1511
|
+
if err != nil {
|
|
1512
|
+
t.Fatal(err)
|
|
1513
|
+
}
|
|
1514
|
+
var got catalog.Catalog
|
|
1515
|
+
if err := json.Unmarshal([]byte(resp.Files[0].Contents), &got); err != nil {
|
|
1516
|
+
t.Fatal(err)
|
|
1517
|
+
}
|
|
1518
|
+
return got.Contexts[0].Services[0].Consumes
|
|
1519
|
+
}
|
|
1520
|
+
consumes := run(map[string]string{"a_settle.go": settle, "b_retry.go": retry, "c_record.go": record})
|
|
1521
|
+
if len(consumes) != 2 {
|
|
1522
|
+
t.Fatalf("consumes = %+v", consumes)
|
|
1523
|
+
}
|
|
1524
|
+
var payments *catalog.RpcCall
|
|
1525
|
+
for i := range consumes {
|
|
1526
|
+
if consumes[i].ID == "http-client/POST /foo @ payments.internal" {
|
|
1527
|
+
payments = &consumes[i]
|
|
1528
|
+
}
|
|
1529
|
+
}
|
|
1530
|
+
if payments == nil || payments.Source != "a_settle.go:3" {
|
|
1531
|
+
t.Fatalf("payments = %+v", payments)
|
|
1532
|
+
}
|
|
1533
|
+
var sites []string
|
|
1534
|
+
for _, item := range payments.Evidence {
|
|
1535
|
+
if item.Kind == "call-site" {
|
|
1536
|
+
sites = append(sites, item.Source)
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
if strings.Join(sites, ",") != "a_settle.go:3,b_retry.go:3" {
|
|
1540
|
+
t.Fatalf("call sites = %v", sites)
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
// Reverse the file order: the same identities come out, only the first
|
|
1544
|
+
// site (and with it the entry's source) follows the new order.
|
|
1545
|
+
reversed := run(map[string]string{"c_settle.go": settle, "b_retry.go": retry, "a_record.go": record})
|
|
1546
|
+
ids := func(calls []catalog.RpcCall) []string {
|
|
1547
|
+
var out []string
|
|
1548
|
+
for _, call := range calls {
|
|
1549
|
+
out = append(out, call.ID+" -> "+call.Peer)
|
|
1550
|
+
}
|
|
1551
|
+
return out
|
|
1552
|
+
}
|
|
1553
|
+
if strings.Join(ids(consumes), "\n") != strings.Join(ids(reversed), "\n") {
|
|
1554
|
+
t.Fatalf("file order changed the result:\n%v\n%v", ids(consumes), ids(reversed))
|
|
1555
|
+
}
|
|
1556
|
+
for _, call := range reversed {
|
|
1557
|
+
if call.ID == "http-client/POST /foo @ payments.internal" && call.Source != "b_retry.go:3" {
|
|
1558
|
+
t.Fatalf("reversed payments source = %q", call.Source)
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
}
|