Skip to content

Commit e67a065

Browse files
khode-mxclaude
authored andcommitted
fix: return ListConsumedRestServices error; add OpenAPI import mock tests
- cmd_rest_clients.go: propagate ListConsumedRestServices error in both createRestClient and createRestClientFromSpec instead of silently discarding it with _. A listing failure now returns an error rather than silently creating a duplicate on OR MODIFY. - cmd_rest_openapi_mock_test.go: 6 new mock-based executor tests for the OpenAPI import path using an embedded httptest server (no network): - Create from spec (verifies name, BaseUrl, operations, OpenApiContent) - BaseUrl override (replaces servers[0].url) - OR MODIFY preserves existing UnitID - Duplicate without OR MODIFY returns error - ListConsumedRestServices error propagates - DESCRIBE CONTRACT OPERATION FROM OPENAPI previews MDL output Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 5def52d commit e67a065

2 files changed

Lines changed: 289 additions & 2 deletions

File tree

mdl/executor/cmd_rest_clients.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -310,7 +310,10 @@ func createRestClient(ctx *ExecContext, stmt *ast.CreateRestClientStmt) error {
310310
}
311311

312312
// Check for existing service with same name
313-
existingServices, _ := ctx.Backend.ListConsumedRestServices()
313+
existingServices, err := ctx.Backend.ListConsumedRestServices()
314+
if err != nil {
315+
return mdlerrors.NewBackend("list rest clients", err)
316+
}
314317
h, err := getHierarchy(ctx)
315318
if err != nil {
316319
return mdlerrors.NewBackend("build hierarchy", err)
@@ -664,7 +667,10 @@ func createRestClientFromSpec(ctx *ExecContext, stmt *ast.CreateRestClientStmt)
664667

665668
// Handle OR MODIFY: delete existing if present, preserving UnitID so any
666669
// SEND REST REQUEST microflows that reference this service by ID remain valid.
667-
existingServices, _ := ctx.Backend.ListConsumedRestServices()
670+
existingServices, err := ctx.Backend.ListConsumedRestServices()
671+
if err != nil {
672+
return mdlerrors.NewBackend("list rest clients", err)
673+
}
668674
h, err := getHierarchy(ctx)
669675
if err != nil {
670676
return mdlerrors.NewBackend("build hierarchy", err)
Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
package executor
4+
5+
import (
6+
"net/http"
7+
"net/http/httptest"
8+
"testing"
9+
10+
"github.com/mendixlabs/mxcli/mdl/ast"
11+
"github.com/mendixlabs/mxcli/mdl/backend/mock"
12+
mdlerrors "github.com/mendixlabs/mxcli/mdl/errors"
13+
"github.com/mendixlabs/mxcli/mdl/types"
14+
"github.com/mendixlabs/mxcli/model"
15+
)
16+
17+
// minimalOpenAPISpec is a self-contained OpenAPI 3.0 fixture used in all
18+
// OpenAPI import tests. No network access required.
19+
const minimalOpenAPISpec = `{
20+
"openapi": "3.0.0",
21+
"info": { "title": "Pet Store", "version": "1.0.0" },
22+
"servers": [{ "url": "https://api.example.com/v1" }],
23+
"paths": {
24+
"/pets": {
25+
"get": {
26+
"operationId": "listPets",
27+
"summary": "List all pets",
28+
"tags": ["pets"],
29+
"parameters": [
30+
{ "name": "limit", "in": "query", "schema": { "type": "integer" } }
31+
],
32+
"responses": { "200": { "description": "OK" } }
33+
}
34+
}
35+
},
36+
"components": {
37+
"securitySchemes": {
38+
"basicAuth": { "type": "http", "scheme": "basic" }
39+
}
40+
},
41+
"security": [{ "basicAuth": [] }]
42+
}`
43+
44+
// newOpenAPIMockBackend returns a MockBackend pre-wired for OpenAPI import
45+
// tests: write-connected, a project version that satisfies the REST client
46+
// feature gate, and no pre-existing REST clients.
47+
func newOpenAPIMockBackend() *mock.MockBackend {
48+
return &mock.MockBackend{
49+
IsConnectedFunc: func() bool { return true },
50+
ProjectVersionFunc: func() *types.ProjectVersion {
51+
return &types.ProjectVersion{
52+
ProductVersion: "10.6.0",
53+
MajorVersion: 10,
54+
MinorVersion: 6,
55+
PatchVersion: 0,
56+
}
57+
},
58+
ListModulesFunc: func() ([]*model.Module, error) {
59+
return nil, nil
60+
},
61+
ListConsumedRestServicesFunc: func() ([]*model.ConsumedRestService, error) {
62+
return nil, nil
63+
},
64+
}
65+
}
66+
67+
func TestCreateRestClientFromSpec_Create(t *testing.T) {
68+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
69+
w.Header().Set("Content-Type", "application/json")
70+
w.Write([]byte(minimalOpenAPISpec))
71+
}))
72+
defer srv.Close()
73+
74+
mod := mkModule("PetModule")
75+
h := mkHierarchy(mod)
76+
77+
var created *model.ConsumedRestService
78+
mb := newOpenAPIMockBackend()
79+
mb.ListModulesFunc = func() ([]*model.Module, error) {
80+
return []*model.Module{mod}, nil
81+
}
82+
mb.CreateConsumedRestServiceFunc = func(svc *model.ConsumedRestService) error {
83+
created = svc
84+
return nil
85+
}
86+
87+
ctx, buf := newMockCtx(t, withBackend(mb), withHierarchy(h))
88+
stmt := &ast.CreateRestClientStmt{
89+
Name: ast.QualifiedName{Module: "PetModule", Name: "PetStoreAPI"},
90+
OpenApiPath: srv.URL,
91+
}
92+
93+
assertNoError(t, createRestClient(ctx, stmt))
94+
95+
if created == nil {
96+
t.Fatal("CreateConsumedRestService was not called")
97+
}
98+
if created.Name != "PetStoreAPI" {
99+
t.Errorf("expected service name PetStoreAPI, got %s", created.Name)
100+
}
101+
if created.BaseUrl != "https://api.example.com/v1" {
102+
t.Errorf("expected base URL from spec, got %s", created.BaseUrl)
103+
}
104+
if len(created.Operations) == 0 {
105+
t.Error("expected at least one operation from spec")
106+
}
107+
if created.OpenApiContent == "" {
108+
t.Error("expected OpenApiContent to be populated")
109+
}
110+
assertContainsStr(t, buf.String(), "PetModule.PetStoreAPI")
111+
}
112+
113+
func TestCreateRestClientFromSpec_BaseUrlOverride(t *testing.T) {
114+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
115+
w.Header().Set("Content-Type", "application/json")
116+
w.Write([]byte(minimalOpenAPISpec))
117+
}))
118+
defer srv.Close()
119+
120+
mod := mkModule("PetModule")
121+
h := mkHierarchy(mod)
122+
123+
var created *model.ConsumedRestService
124+
mb := newOpenAPIMockBackend()
125+
mb.ListModulesFunc = func() ([]*model.Module, error) {
126+
return []*model.Module{mod}, nil
127+
}
128+
mb.CreateConsumedRestServiceFunc = func(svc *model.ConsumedRestService) error {
129+
created = svc
130+
return nil
131+
}
132+
133+
ctx, _ := newMockCtx(t, withBackend(mb), withHierarchy(h))
134+
stmt := &ast.CreateRestClientStmt{
135+
Name: ast.QualifiedName{Module: "PetModule", Name: "PetStoreStaging"},
136+
OpenApiPath: srv.URL,
137+
BaseUrl: "https://staging.example.com/v1",
138+
}
139+
140+
assertNoError(t, createRestClient(ctx, stmt))
141+
142+
if created == nil {
143+
t.Fatal("CreateConsumedRestService was not called")
144+
}
145+
if created.BaseUrl != "https://staging.example.com/v1" {
146+
t.Errorf("expected overridden BaseUrl, got %s", created.BaseUrl)
147+
}
148+
}
149+
150+
func TestCreateRestClientFromSpec_OrModifyPreservesID(t *testing.T) {
151+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
152+
w.Header().Set("Content-Type", "application/json")
153+
w.Write([]byte(minimalOpenAPISpec))
154+
}))
155+
defer srv.Close()
156+
157+
mod := mkModule("PetModule")
158+
h := mkHierarchy(mod)
159+
existingID := nextID("rest")
160+
existing := &model.ConsumedRestService{
161+
BaseElement: model.BaseElement{ID: existingID},
162+
ContainerID: mod.ID,
163+
Name: "PetStoreAPI",
164+
}
165+
withContainer(h, existing.ContainerID, mod.ID)
166+
167+
var deletedID model.ID
168+
var created *model.ConsumedRestService
169+
mb := newOpenAPIMockBackend()
170+
mb.ListModulesFunc = func() ([]*model.Module, error) {
171+
return []*model.Module{mod}, nil
172+
}
173+
mb.ListConsumedRestServicesFunc = func() ([]*model.ConsumedRestService, error) {
174+
return []*model.ConsumedRestService{existing}, nil
175+
}
176+
mb.DeleteConsumedRestServiceFunc = func(id model.ID) error {
177+
deletedID = id
178+
return nil
179+
}
180+
mb.CreateConsumedRestServiceFunc = func(svc *model.ConsumedRestService) error {
181+
created = svc
182+
return nil
183+
}
184+
185+
ctx, _ := newMockCtx(t, withBackend(mb), withHierarchy(h))
186+
stmt := &ast.CreateRestClientStmt{
187+
Name: ast.QualifiedName{Module: "PetModule", Name: "PetStoreAPI"},
188+
OpenApiPath: srv.URL,
189+
CreateOrModify: true,
190+
}
191+
192+
assertNoError(t, createRestClient(ctx, stmt))
193+
194+
if deletedID != existingID {
195+
t.Errorf("expected existing service to be deleted, got deletedID=%v", deletedID)
196+
}
197+
if created == nil {
198+
t.Fatal("CreateConsumedRestService was not called")
199+
}
200+
if created.ID != existingID {
201+
t.Errorf("expected recreated service to reuse existing ID %v, got %v", existingID, created.ID)
202+
}
203+
}
204+
205+
func TestCreateRestClientFromSpec_DuplicateWithoutOrModify(t *testing.T) {
206+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
207+
w.Header().Set("Content-Type", "application/json")
208+
w.Write([]byte(minimalOpenAPISpec))
209+
}))
210+
defer srv.Close()
211+
212+
mod := mkModule("PetModule")
213+
h := mkHierarchy(mod)
214+
existing := &model.ConsumedRestService{
215+
BaseElement: model.BaseElement{ID: nextID("rest")},
216+
ContainerID: mod.ID,
217+
Name: "PetStoreAPI",
218+
}
219+
withContainer(h, existing.ContainerID, mod.ID)
220+
221+
mb := newOpenAPIMockBackend()
222+
mb.ListModulesFunc = func() ([]*model.Module, error) {
223+
return []*model.Module{mod}, nil
224+
}
225+
mb.ListConsumedRestServicesFunc = func() ([]*model.ConsumedRestService, error) {
226+
return []*model.ConsumedRestService{existing}, nil
227+
}
228+
229+
ctx, _ := newMockCtx(t, withBackend(mb), withHierarchy(h))
230+
stmt := &ast.CreateRestClientStmt{
231+
Name: ast.QualifiedName{Module: "PetModule", Name: "PetStoreAPI"},
232+
OpenApiPath: srv.URL,
233+
}
234+
235+
assertError(t, createRestClient(ctx, stmt))
236+
}
237+
238+
func TestCreateRestClientFromSpec_ListError(t *testing.T) {
239+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
240+
w.Header().Set("Content-Type", "application/json")
241+
w.Write([]byte(minimalOpenAPISpec))
242+
}))
243+
defer srv.Close()
244+
245+
mod := mkModule("PetModule")
246+
h := mkHierarchy(mod)
247+
248+
mb := newOpenAPIMockBackend()
249+
mb.ListModulesFunc = func() ([]*model.Module, error) {
250+
return []*model.Module{mod}, nil
251+
}
252+
mb.ListConsumedRestServicesFunc = func() ([]*model.ConsumedRestService, error) {
253+
return nil, mdlerrors.NewBackend("list rest clients", nil)
254+
}
255+
256+
ctx, _ := newMockCtx(t, withBackend(mb), withHierarchy(h))
257+
stmt := &ast.CreateRestClientStmt{
258+
Name: ast.QualifiedName{Module: "PetModule", Name: "PetStoreAPI"},
259+
OpenApiPath: srv.URL,
260+
}
261+
262+
assertError(t, createRestClient(ctx, stmt))
263+
}
264+
265+
func TestDescribeContractFromOpenAPI_Mock(t *testing.T) {
266+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
267+
w.Header().Set("Content-Type", "application/json")
268+
w.Write([]byte(minimalOpenAPISpec))
269+
}))
270+
defer srv.Close()
271+
272+
ctx, buf := newMockCtx(t)
273+
stmt := &ast.DescribeContractFromOpenAPIStmt{SpecPath: srv.URL}
274+
275+
assertNoError(t, describeContractFromOpenAPI(ctx, stmt))
276+
277+
out := buf.String()
278+
assertContainsStr(t, out, "create rest client")
279+
assertContainsStr(t, out, "https://api.example.com/v1")
280+
assertContainsStr(t, out, "listPets")
281+
}

0 commit comments

Comments
 (0)