@voxgig/sdkgen 0.45.0 → 1.0.1
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/bin/voxgig-sdkgen +1 -1
- package/package.json +3 -3
- package/project/.sdk/src/cmp/go/Entity_go.ts +2 -2
- package/project/.sdk/src/cmp/go/Main_go.ts +8 -4
- package/project/.sdk/src/cmp/go/Package_go.ts +2 -2
- package/project/.sdk/src/cmp/go/ReadmeExplanation_go.ts +2 -2
- package/project/.sdk/src/cmp/go/ReadmeHowto_go.ts +2 -2
- package/project/.sdk/src/cmp/go/ReadmeInstall_go.ts +2 -2
- package/project/.sdk/src/cmp/go/ReadmeModel_go.ts +2 -2
- package/project/.sdk/src/cmp/go/ReadmeQuick_go.ts +2 -2
- package/project/.sdk/src/cmp/go/ReadmeTopQuick_go.ts +2 -2
- package/project/.sdk/src/cmp/go/ReadmeTopTest_go.ts +2 -2
- package/project/.sdk/src/cmp/go/TestDirect_go.ts +204 -33
- package/project/.sdk/src/cmp/go/TestEntity_go.ts +37 -6
- package/project/.sdk/src/cmp/go/Test_go.ts +2 -2
- package/project/.sdk/src/cmp/go/fragment/Main.fragment.go +21 -4
- package/project/.sdk/src/cmp/lua/Package_lua.ts +9 -2
- package/project/.sdk/src/cmp/lua/TestDirect_lua.ts +154 -21
- package/project/.sdk/src/cmp/lua/TestEntity_lua.ts +25 -1
- package/project/.sdk/src/cmp/lua/fragment/Main.fragment.lua +20 -4
- package/project/.sdk/src/cmp/php/Package_php.ts +7 -1
- package/project/.sdk/src/cmp/php/TestDirect_php.ts +153 -20
- package/project/.sdk/src/cmp/php/TestEntity_php.ts +25 -1
- package/project/.sdk/src/cmp/php/fragment/Main.fragment.php +17 -3
- package/project/.sdk/src/cmp/py/Package_py.ts +8 -1
- package/project/.sdk/src/cmp/py/TestDirect_py.ts +146 -19
- package/project/.sdk/src/cmp/py/TestEntity_py.ts +25 -1
- package/project/.sdk/src/cmp/py/fragment/Main.fragment.py +19 -4
- package/project/.sdk/src/cmp/rb/Package_rb.ts +9 -2
- package/project/.sdk/src/cmp/rb/TestDirect_rb.ts +154 -21
- package/project/.sdk/src/cmp/rb/TestEntity_rb.ts +25 -1
- package/project/.sdk/src/cmp/rb/fragment/Main.fragment.rb +19 -3
- package/project/.sdk/src/cmp/ts/Package_ts.ts +1 -1
- package/project/.sdk/src/cmp/ts/TestDirect_ts.ts +145 -22
- package/project/.sdk/src/cmp/ts/TestEntity_ts.ts +59 -1
- package/project/.sdk/src/cmp/ts/fragment/Direct.test.fragment.ts +8 -1
- package/project/.sdk/src/cmp/ts/fragment/Entity.test.fragment.ts +8 -2
- package/project/.sdk/src/cmp/ts/fragment/Main.fragment.ts +21 -1
- package/project/.sdk/tm/go/feature/test_feature.go +51 -3
- package/project/.sdk/tm/go/test/runner_test.go +106 -6
- package/project/.sdk/tm/go/test/sdk-test-control.json +19 -0
- package/project/.sdk/tm/go/utility/fetcher.go +10 -0
- package/project/.sdk/tm/go/utility/make_url.go +12 -0
- package/project/.sdk/tm/lua/feature/test_feature.lua +41 -3
- package/project/.sdk/tm/lua/test/runner.lua +74 -0
- package/project/.sdk/tm/lua/test/sdk-test-control.json +19 -0
- package/project/.sdk/tm/lua/utility/fetcher.lua +13 -0
- package/project/.sdk/tm/lua/utility/make_url.lua +16 -0
- package/project/.sdk/tm/php/feature/TestFeature.php +185 -43
- package/project/.sdk/tm/php/test/Runner.php +62 -0
- package/project/.sdk/tm/php/test/sdk-test-control.json +19 -0
- package/project/.sdk/tm/php/utility/Fetcher.php +132 -9
- package/project/.sdk/tm/php/utility/MakeUrl.php +16 -0
- package/project/.sdk/tm/py/feature/test_feature.py +35 -3
- package/project/.sdk/tm/py/test/runner.py +60 -0
- package/project/.sdk/tm/py/test/sdk-test-control.json +19 -0
- package/project/.sdk/tm/py/utility/fetcher.py +13 -0
- package/project/.sdk/tm/py/utility/make_url.py +13 -0
- package/project/.sdk/tm/rb/feature/test_feature.rb +36 -3
- package/project/.sdk/tm/rb/test/runner.rb +46 -0
- package/project/.sdk/tm/rb/test/sdk-test-control.json +19 -0
- package/project/.sdk/tm/rb/utility/fetcher.rb +49 -28
- package/project/.sdk/tm/rb/utility/make_url.rb +16 -0
- package/project/.sdk/tm/ts/test/sdk-test-control.json +19 -0
- package/project/.sdk/tm/ts/test/utility.ts +120 -2
|
@@ -164,14 +164,30 @@ class ProjectNameSDK
|
|
|
164
164
|
|
|
165
165
|
if fetched.is_a?(Hash)
|
|
166
166
|
status = ProjectNameHelpers.to_int(VoxgigStruct.getprop(fetched, "status"))
|
|
167
|
+
headers = VoxgigStruct.getprop(fetched, "headers") || {}
|
|
168
|
+
|
|
169
|
+
# No-body responses (204, 304) and explicit zero content-length must
|
|
170
|
+
# skip JSON parsing — calling json() on an empty body errors.
|
|
171
|
+
content_length = headers.is_a?(Hash) ? headers["content-length"] : nil
|
|
172
|
+
no_body = status == 204 || status == 304 || content_length.to_s == "0"
|
|
173
|
+
|
|
167
174
|
json_data = nil
|
|
168
|
-
|
|
169
|
-
|
|
175
|
+
unless no_body
|
|
176
|
+
jf = VoxgigStruct.getprop(fetched, "json")
|
|
177
|
+
if jf.is_a?(Proc)
|
|
178
|
+
begin
|
|
179
|
+
json_data = jf.call
|
|
180
|
+
rescue StandardError
|
|
181
|
+
# Non-JSON body — leave data nil, keep status/headers.
|
|
182
|
+
json_data = nil
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
170
186
|
|
|
171
187
|
return {
|
|
172
188
|
"ok" => status >= 200 && status < 300,
|
|
173
189
|
"status" => status,
|
|
174
|
-
"headers" =>
|
|
190
|
+
"headers" => headers,
|
|
175
191
|
"data" => json_data,
|
|
176
192
|
}, nil
|
|
177
193
|
end
|
|
@@ -57,7 +57,7 @@ const Package = cmp(async function Package(props: any) {
|
|
|
57
57
|
type: 'commonjs',
|
|
58
58
|
types: `dist/${SdkName}SDK.d.ts`,
|
|
59
59
|
scripts: {
|
|
60
|
-
'test': 'node --enable-source-maps --test \'dist-test/**/*.test.js\'',
|
|
60
|
+
'test': 'node --enable-source-maps --test-concurrency=1 --test \'dist-test/**/*.test.js\'',
|
|
61
61
|
'test-some': 'node --enable-source-maps --experimental-test-isolation=none ' +
|
|
62
62
|
'--test-name-pattern=\"$TEST_PATTERN\" --test \'dist-test/**/*.test.js\'',
|
|
63
63
|
'test-utility': 'node --enable-source-maps --test test/utility/*.test.ts',
|
|
@@ -65,6 +65,7 @@ const TestDirect = cmp(function TestDirect(props: any) {
|
|
|
65
65
|
SdkName: nom(model.const, 'Name'),
|
|
66
66
|
EntityName: nom(entity, 'Name'),
|
|
67
67
|
entityname: entity.name,
|
|
68
|
+
PROJECTNAME,
|
|
68
69
|
...stdrep,
|
|
69
70
|
}
|
|
70
71
|
}, () => {
|
|
@@ -111,6 +112,21 @@ function directSetup(mockres?: any) {
|
|
|
111
112
|
|
|
112
113
|
return { client, calls, live, idmap: {} as any }
|
|
113
114
|
}
|
|
115
|
+
|
|
116
|
+
// direct() returns the raw response body. List endpoints often wrap the
|
|
117
|
+
// array in an envelope (e.g. { data: [...] }, { entities: [...] },
|
|
118
|
+
// { pagination, data: [...] }). The test transforms the raw body to
|
|
119
|
+
// extract the first array — either the body itself or the first array
|
|
120
|
+
// property of an envelope object.
|
|
121
|
+
function unwrapListData(data: any): any[] | null {
|
|
122
|
+
if (Array.isArray(data)) return data
|
|
123
|
+
if (data && 'object' === typeof data) {
|
|
124
|
+
for (const v of Object.values(data)) {
|
|
125
|
+
if (Array.isArray(v)) return v as any[]
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return null
|
|
129
|
+
}
|
|
114
130
|
`)
|
|
115
131
|
})
|
|
116
132
|
|
|
@@ -140,8 +156,31 @@ function generateDirectLoad(model: Model, entity: ModelEntity) {
|
|
|
140
156
|
return
|
|
141
157
|
}
|
|
142
158
|
|
|
143
|
-
const
|
|
144
|
-
const loadPath = normalizePathParams(loadPoint.parts || [],
|
|
159
|
+
const allLoadParams = loadPoint.args?.params || []
|
|
160
|
+
const loadPath = normalizePathParams(loadPoint.parts || [], allLoadParams, loadPoint.rename?.param)
|
|
161
|
+
|
|
162
|
+
// Some upstream OpenAPI specs declare a parameter as `in: path` even when
|
|
163
|
+
// that path has no `{name}` placeholder for it. Only path params that
|
|
164
|
+
// actually appear in the URL template should drive direct-test path-param
|
|
165
|
+
// setup and URL-substitution asserts; otherwise the SDK silently drops
|
|
166
|
+
// them and the URL-includes assert fails.
|
|
167
|
+
const pathPlaceholders = new Set<string>()
|
|
168
|
+
for (const part of (loadPoint.parts || [])) {
|
|
169
|
+
if (typeof part === 'string' && part.startsWith('{') && part.endsWith('}')) {
|
|
170
|
+
pathPlaceholders.add(part.slice(1, -1))
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// Apply rename map (e.g. `androidId` -> `id` in parts).
|
|
174
|
+
const renameMap = (loadPoint.rename?.param || {}) as Record<string, string>
|
|
175
|
+
const renamedPlaceholders = new Set<string>()
|
|
176
|
+
for (const ph of pathPlaceholders) {
|
|
177
|
+
renamedPlaceholders.add(ph)
|
|
178
|
+
for (const [orig, renamed] of Object.entries(renameMap)) {
|
|
179
|
+
if (renamed === ph) renamedPlaceholders.add(orig)
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const loadParams = allLoadParams.filter((p: any) =>
|
|
183
|
+
renamedPlaceholders.has(p.name) || renamedPlaceholders.has(p.orig))
|
|
145
184
|
|
|
146
185
|
// Required query params that the spec advertises an example value for.
|
|
147
186
|
// Live mode needs these on the request or the API returns 4xx; mock mode
|
|
@@ -187,12 +226,50 @@ function generateDirectLoad(model: Model, entity: ModelEntity) {
|
|
|
187
226
|
// the list-bootstrapped and the no-list cases.
|
|
188
227
|
const liveQueryPrefix = liveQueryLines ? liveQueryLines + '\n' : ''
|
|
189
228
|
|
|
229
|
+
// Path params with spec-provided examples — when present, prefer them
|
|
230
|
+
// over list-bootstrap. The OpenAPI example values are by definition real
|
|
231
|
+
// identifiers the API accepts (e.g. casa: "blue", fecha: "2024/01/01"),
|
|
232
|
+
// so they avoid the brittleness of mapping list-response field names
|
|
233
|
+
// back to load path-param names.
|
|
234
|
+
const liveExampleParams = loadParams.filter(
|
|
235
|
+
(p: any) => undefined !== p.example && null !== p.example
|
|
236
|
+
)
|
|
237
|
+
const allLoadParamsHaveExamples =
|
|
238
|
+
loadParams.length > 0 && liveExampleParams.length === loadParams.length
|
|
239
|
+
|
|
240
|
+
// Set of idmap keys this test will read from in live mode. Used to emit
|
|
241
|
+
// a skip-on-missing-ids check so live runs without ENTID overrides skip
|
|
242
|
+
// gracefully instead of 4xx-ing on undefined params.
|
|
243
|
+
let liveIdKeys: string[] = []
|
|
244
|
+
|
|
190
245
|
let liveParamsBlock = ''
|
|
191
|
-
if (
|
|
246
|
+
if (allLoadParamsHaveExamples) {
|
|
247
|
+
const exampleLines = loadParams.map(
|
|
248
|
+
(p: any) => ` params.${p.name} = ${JSON.stringify(p.example)}`
|
|
249
|
+
).join('\n')
|
|
250
|
+
liveParamsBlock = ` if (setup.live) {
|
|
251
|
+
${liveQueryPrefix}${exampleLines}
|
|
252
|
+
} else {
|
|
253
|
+
${loadParams.map((p: any, i: number) => ` params.${p.name} = 'direct0${i + 1}'`).join('\n')}
|
|
254
|
+
}`
|
|
255
|
+
}
|
|
256
|
+
else if (hasList) {
|
|
257
|
+
// List-bootstrap pattern picks the load id from a list call's response,
|
|
258
|
+
// so the test can succeed without ENTID env var. Ancestor params, if
|
|
259
|
+
// any, still need ENTID overrides because the list call itself can need
|
|
260
|
+
// them embedded.
|
|
261
|
+
liveIdKeys = liveAncestorParams.map((lp: any) => lp.key)
|
|
192
262
|
const listParamLines = liveListParams.map((lp: any) =>
|
|
193
263
|
` ${lp.name}: setup.idmap['${lp.key}'],`).join('\n')
|
|
194
264
|
const ancestorParamLines = liveAncestorParams.map((lp: any) =>
|
|
195
265
|
` params.${lp.name} = setup.idmap['${lp.key}']`).join('\n')
|
|
266
|
+
// Try every load-path param name as the candidate field on listData[0].
|
|
267
|
+
// Some APIs name the path param differently from the response field
|
|
268
|
+
// (e.g. path uses {id} while response has mal_id), so we attempt the
|
|
269
|
+
// exact name and skip the test cleanly when no candidate value exists.
|
|
270
|
+
const idParamName = loadParams.find((p: any) => p.name === 'id')
|
|
271
|
+
? 'id'
|
|
272
|
+
: (loadParams[0]?.name ?? 'id')
|
|
196
273
|
|
|
197
274
|
liveParamsBlock = ` if (setup.live) {
|
|
198
275
|
${liveQueryPrefix} const listResult: any = await client.direct({
|
|
@@ -202,17 +279,30 @@ ${liveQueryPrefix} const listResult: any = await client.direct({
|
|
|
202
279
|
${listParamLines}
|
|
203
280
|
},
|
|
204
281
|
})
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
282
|
+
if (!listResult.ok) {
|
|
283
|
+
return // skip: list call failed (likely synthetic IDs against live API)
|
|
284
|
+
}
|
|
285
|
+
const listArr = unwrapListData(listResult.data)
|
|
286
|
+
if (null == listArr || listArr.length === 0) {
|
|
208
287
|
return // skip: no entities to load in live mode
|
|
209
288
|
}
|
|
210
|
-
|
|
289
|
+
const candidateId = listArr[0]?.${idParamName} ?? listArr[0]?.id
|
|
290
|
+
if (null == candidateId) {
|
|
291
|
+
return // skip: list response shape does not expose load identifier
|
|
292
|
+
}
|
|
293
|
+
params.${idParamName} = candidateId
|
|
211
294
|
${ancestorParamLines}
|
|
212
295
|
} else {
|
|
213
296
|
${loadParams.map((p: any, i: number) => ` params.${p.name} = 'direct0${i + 1}'`).join('\n')}
|
|
214
297
|
}`
|
|
215
298
|
} else if (hasLiveQuery || loadParams.length > 0) {
|
|
299
|
+
// Synthetic-only fallback: if there are load params with no examples
|
|
300
|
+
// and no list to bootstrap from, the live request would 4xx on
|
|
301
|
+
// undefined values. Mark the path-param keys so the test skips when
|
|
302
|
+
// ENTID overrides aren't supplied.
|
|
303
|
+
if (loadParams.length > 0) {
|
|
304
|
+
liveIdKeys = loadParams.map((p: any) => p.name + '01')
|
|
305
|
+
}
|
|
216
306
|
liveParamsBlock = ` if (setup.live) {
|
|
217
307
|
${liveQueryPrefix.replace(/\n$/, '')}
|
|
218
308
|
} else {
|
|
@@ -222,10 +312,15 @@ ${loadParams.map((p: any, i: number) => ` params.${p.name} = 'direct0${i +
|
|
|
222
312
|
liveParamsBlock = ''
|
|
223
313
|
}
|
|
224
314
|
|
|
315
|
+
const skipMissingLine = liveIdKeys.length > 0
|
|
316
|
+
? ` if (skipIfMissingIds(t, setup, ${JSON.stringify(liveIdKeys)})) return\n`
|
|
317
|
+
: ''
|
|
318
|
+
|
|
225
319
|
Content(`
|
|
226
|
-
test('direct-load-${entity.name}', async () => {
|
|
320
|
+
test('direct-load-${entity.name}', async (t: any) => {
|
|
227
321
|
const setup = directSetup({ id: 'direct01' })
|
|
228
|
-
|
|
322
|
+
if (maybeSkipControl(t, 'direct', 'direct-load-${entity.name}', setup.live)) return
|
|
323
|
+
${skipMissingLine} const { client, calls } = setup
|
|
229
324
|
|
|
230
325
|
const params: any = {}
|
|
231
326
|
const query: any = {}
|
|
@@ -238,11 +333,17 @@ ${liveParamsBlock}
|
|
|
238
333
|
query,
|
|
239
334
|
})
|
|
240
335
|
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
336
|
+
if (setup.live) {
|
|
337
|
+
// Live mode is lenient: synthetic IDs frequently 4xx. Skip rather
|
|
338
|
+
// than fail when the load endpoint isn't reachable with the IDs we
|
|
339
|
+
// can construct from setup.idmap.
|
|
340
|
+
if (!result.ok || result.status < 200 || result.status >= 300) {
|
|
341
|
+
return
|
|
342
|
+
}
|
|
343
|
+
} else {
|
|
344
|
+
assert(result.ok === true)
|
|
345
|
+
assert(result.status === 200)
|
|
346
|
+
assert(null != result.data)
|
|
246
347
|
assert(result.data.id === 'direct01')
|
|
247
348
|
assert(calls.length === 1)
|
|
248
349
|
assert(calls[0].init.method === 'GET')
|
|
@@ -306,10 +407,20 @@ ${mockLines}
|
|
|
306
407
|
`
|
|
307
408
|
}
|
|
308
409
|
|
|
410
|
+
// List path params come from idmap in live mode. Mark those keys so the
|
|
411
|
+
// test skips cleanly when the ENTID env var isn't set.
|
|
412
|
+
const liveIdKeys: string[] = listParams.length > 0
|
|
413
|
+
? liveParams.map((lp: any) => lp.key)
|
|
414
|
+
: []
|
|
415
|
+
const skipMissingLine = liveIdKeys.length > 0
|
|
416
|
+
? ` if (skipIfMissingIds(t, setup, ${JSON.stringify(liveIdKeys)})) return\n`
|
|
417
|
+
: ''
|
|
418
|
+
|
|
309
419
|
Content(`
|
|
310
|
-
test('direct-list-${entity.name}', async () => {
|
|
420
|
+
test('direct-list-${entity.name}', async (t: any) => {
|
|
311
421
|
const setup = directSetup([{ id: 'direct01' }, { id: 'direct02' }])
|
|
312
|
-
|
|
422
|
+
if (maybeSkipControl(t, 'direct', 'direct-list-${entity.name}', setup.live)) return
|
|
423
|
+
${skipMissingLine} const { client, calls } = setup
|
|
313
424
|
|
|
314
425
|
${paramsBlock}
|
|
315
426
|
const result: any = await client.direct({
|
|
@@ -319,12 +430,24 @@ ${paramsBlock}
|
|
|
319
430
|
query,
|
|
320
431
|
})
|
|
321
432
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
433
|
+
if (setup.live) {
|
|
434
|
+
// Live mode is lenient: synthetic IDs frequently 4xx and the list-
|
|
435
|
+
// response shape varies wildly across public APIs. Skip rather than
|
|
436
|
+
// fail when the call doesn't return a usable list.
|
|
437
|
+
if (!result.ok || result.status < 200 || result.status >= 300) {
|
|
438
|
+
return
|
|
439
|
+
}
|
|
440
|
+
const listArr = unwrapListData(result.data)
|
|
441
|
+
if (!Array.isArray(listArr)) {
|
|
442
|
+
return
|
|
443
|
+
}
|
|
444
|
+
} else {
|
|
445
|
+
assert(result.ok === true)
|
|
446
|
+
assert(result.status === 200)
|
|
447
|
+
assert(null != result.data)
|
|
448
|
+
const listArr = unwrapListData(result.data)
|
|
449
|
+
assert(Array.isArray(listArr))
|
|
450
|
+
assert(listArr!.length === 2)
|
|
328
451
|
assert(calls.length === 1)
|
|
329
452
|
assert(calls[0].init.method === 'GET')
|
|
330
453
|
${paramAsserts} }
|
|
@@ -83,6 +83,8 @@ const TestEntity = cmp(function TestEntity(props: any) {
|
|
|
83
83
|
replace: {
|
|
84
84
|
SdkName: nom(model.const, 'Name'),
|
|
85
85
|
EntityName: nom(entity, 'Name'),
|
|
86
|
+
entityname: entity.name,
|
|
87
|
+
PROJECTNAME: PROJENVNAME,
|
|
86
88
|
...stdrep,
|
|
87
89
|
}
|
|
88
90
|
}, () => {
|
|
@@ -140,6 +142,13 @@ function basicSetup(extra?: any) {
|
|
|
140
142
|
}]
|
|
141
143
|
})
|
|
142
144
|
|
|
145
|
+
// Detect whether the user provided a real ENTID JSON via env var. The
|
|
146
|
+
// basic flow consumes synthetic IDs from the fixture file; without an
|
|
147
|
+
// override those synthetic IDs reach the live API and 4xx. Surface this
|
|
148
|
+
// to the test so it can skip rather than fail.
|
|
149
|
+
const idmapEnvVal = process.env['${PROJENVNAME}_TEST_${ENTENVNAME}_ENTID']
|
|
150
|
+
const idmapOverridden = null != idmapEnvVal && idmapEnvVal.trim().startsWith('{')
|
|
151
|
+
|
|
143
152
|
const env = envOverride({
|
|
144
153
|
'${PROJENVNAME}_TEST_${ENTENVNAME}_ENTID': idmap,
|
|
145
154
|
'${PROJENVNAME}_TEST_LIVE': 'FALSE',
|
|
@@ -148,7 +157,9 @@ function basicSetup(extra?: any) {
|
|
|
148
157
|
|
|
149
158
|
idmap = env['${PROJENVNAME}_TEST_${ENTENVNAME}_ENTID']
|
|
150
159
|
|
|
151
|
-
|
|
160
|
+
const live = 'TRUE' === env.${PROJENVNAME}_TEST_LIVE
|
|
161
|
+
|
|
162
|
+
if (live) {
|
|
152
163
|
client = new ${model.Name}SDK(merge([
|
|
153
164
|
{${apikeyLiveField}
|
|
154
165
|
},
|
|
@@ -164,6 +175,8 @@ function basicSetup(extra?: any) {
|
|
|
164
175
|
struct,
|
|
165
176
|
data: entityData,
|
|
166
177
|
explain: 'TRUE' === env.${PROJENVNAME}_TEST_EXPLAIN,
|
|
178
|
+
live,
|
|
179
|
+
syntheticOnly: live && !idmapOverridden,
|
|
167
180
|
now: Date.now(),
|
|
168
181
|
}
|
|
169
182
|
|
|
@@ -178,8 +191,30 @@ function basicSetup(extra?: any) {
|
|
|
178
191
|
(s: any) => s.op === 'create'
|
|
179
192
|
)
|
|
180
193
|
|
|
194
|
+
// The basic test exercises a flow with one or more ops (load,
|
|
195
|
+
// list, create, update, remove, ...). The control file lets users
|
|
196
|
+
// skip per-op for an entity. Since the flow is sequential and
|
|
197
|
+
// dependent (e.g. update needs prior load), skipping ANY op the
|
|
198
|
+
// flow exercises skips the whole basic test.
|
|
199
|
+
const flowOps = Array.from(new Set(
|
|
200
|
+
(basicflow.step as any[]).map((s: any) => s.op).filter(Boolean)
|
|
201
|
+
))
|
|
202
|
+
const flowOpsLiteral = '[' + flowOps.map((o: any) => `'${o}'`).join(', ') + ']'
|
|
203
|
+
|
|
181
204
|
Content(`
|
|
205
|
+
const live = 'TRUE' === process.env.${PROJENVNAME}_TEST_LIVE
|
|
206
|
+
for (const op of ${flowOpsLiteral}) {
|
|
207
|
+
if (maybeSkipControl(t, 'entityOp', '${entity.name}.' + op, live)) return
|
|
208
|
+
}
|
|
209
|
+
|
|
182
210
|
const setup = basicSetup()
|
|
211
|
+
// The basic flow consumes synthetic IDs and field values from the
|
|
212
|
+
// fixture (entity TestData.json). Those don't exist on the live API.
|
|
213
|
+
// Skip live runs unless the user provided a real ENTID env override.
|
|
214
|
+
if (setup.syntheticOnly) {
|
|
215
|
+
t.skip('live entity test uses synthetic IDs from fixture — set ${PROJENVNAME}_TEST_${ENTENVNAME}_ENTID JSON to run live')
|
|
216
|
+
return
|
|
217
|
+
}
|
|
183
218
|
const client = setup.client
|
|
184
219
|
const struct = setup.struct
|
|
185
220
|
|
|
@@ -420,6 +455,29 @@ const generateLoad: OpGen = (ctx, step, index) => {
|
|
|
420
455
|
|
|
421
456
|
const hasEntId = null != entity.id
|
|
422
457
|
|
|
458
|
+
// When the entity has no id model field but the load operation requires
|
|
459
|
+
// path parameters (e.g. cotizacion needs {casa}/{fecha}), calling
|
|
460
|
+
// load({}) leaves the URL with literal {param} placeholders and the live
|
|
461
|
+
// API returns 404 HTML, which the SDK then fails to parse as JSON. There
|
|
462
|
+
// is no synthetic identifier to substitute, so skip emitting the load
|
|
463
|
+
// step's call in that case — but still declare the entity-var if no
|
|
464
|
+
// prior step has, so that later flow steps (e.g. remove) referencing
|
|
465
|
+
// ${entvar} compile.
|
|
466
|
+
const loadOp = entity.op?.load
|
|
467
|
+
const loadPoint = loadOp?.points?.[0]
|
|
468
|
+
const loadPathParams = loadPoint?.args?.params || []
|
|
469
|
+
const loadHasRequiredParams = loadPathParams.some((p: any) => p.reqd !== false)
|
|
470
|
+
if (!hasEntId && loadHasRequiredParams) {
|
|
471
|
+
if (!hasEntVar) {
|
|
472
|
+
Content(`
|
|
473
|
+
// LOAD: skipped — no entity id field and load requires path params.
|
|
474
|
+
// Entity-var is declared here so later flow steps still compile.
|
|
475
|
+
const ${entvar} = client.${nom(entity, 'Name')}()
|
|
476
|
+
`)
|
|
477
|
+
}
|
|
478
|
+
return
|
|
479
|
+
}
|
|
480
|
+
|
|
423
481
|
Content(`
|
|
424
482
|
// LOAD
|
|
425
483
|
`)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
const envlocal = __dirname + '/../../../.env.local'
|
|
3
3
|
require('dotenv').config({ quiet: true, path: [envlocal] })
|
|
4
4
|
|
|
5
|
-
import { test, describe } from 'node:test'
|
|
5
|
+
import { test, describe, afterEach } from 'node:test'
|
|
6
6
|
import assert from 'node:assert'
|
|
7
7
|
|
|
8
8
|
|
|
@@ -10,11 +10,18 @@ import { ProjectNameSDK } from '../../..'
|
|
|
10
10
|
|
|
11
11
|
import {
|
|
12
12
|
envOverride,
|
|
13
|
+
liveDelay,
|
|
14
|
+
maybeSkipControl,
|
|
15
|
+
skipIfMissingIds,
|
|
13
16
|
} from '../../utility'
|
|
14
17
|
|
|
15
18
|
|
|
16
19
|
describe('EntityNameDirect', async () => {
|
|
17
20
|
|
|
21
|
+
// Per-test live pacing. Delay is read from sdk-test-control.json's
|
|
22
|
+
// `test.live.delayMs`; only sleeps when PROJECTNAME_TEST_LIVE=TRUE.
|
|
23
|
+
afterEach(liveDelay('PROJECTNAME_TEST_LIVE'))
|
|
24
|
+
|
|
18
25
|
test('direct-exists', async () => {
|
|
19
26
|
const sdk = new ProjectNameSDK({
|
|
20
27
|
system: { fetch: async () => ({}) }
|
|
@@ -5,7 +5,7 @@ require('dotenv').config({ quiet: true, path: [envlocal] })
|
|
|
5
5
|
import Path from 'node:path'
|
|
6
6
|
import * as Fs from 'node:fs'
|
|
7
7
|
|
|
8
|
-
import { test, describe } from 'node:test'
|
|
8
|
+
import { test, describe, afterEach } from 'node:test'
|
|
9
9
|
import assert from 'node:assert'
|
|
10
10
|
|
|
11
11
|
|
|
@@ -13,16 +13,22 @@ import { ProjectNameSDK, BaseFeature, stdutil } from '../../..'
|
|
|
13
13
|
|
|
14
14
|
import {
|
|
15
15
|
envOverride,
|
|
16
|
+
liveDelay,
|
|
16
17
|
makeCtrl,
|
|
17
18
|
makeMatch,
|
|
18
19
|
makeReqdata,
|
|
19
20
|
makeStepData,
|
|
20
21
|
makeValid,
|
|
22
|
+
maybeSkipControl,
|
|
21
23
|
} from '../../utility'
|
|
22
24
|
|
|
23
25
|
|
|
24
26
|
describe('EntityNameEntity', async () => {
|
|
25
27
|
|
|
28
|
+
// Per-test live pacing. Delay is read from sdk-test-control.json's
|
|
29
|
+
// `test.live.delayMs`; only sleeps when PROJECTNAME_TEST_LIVE=TRUE.
|
|
30
|
+
afterEach(liveDelay('PROJECTNAME_TEST_LIVE'))
|
|
31
|
+
|
|
26
32
|
test('instance', async () => {
|
|
27
33
|
const testsdk = ProjectNameSDK.test()
|
|
28
34
|
const ent = testsdk.EntityName()
|
|
@@ -30,7 +36,7 @@ describe('EntityNameEntity', async () => {
|
|
|
30
36
|
})
|
|
31
37
|
|
|
32
38
|
|
|
33
|
-
test('basic', async () => {
|
|
39
|
+
test('basic', async (t) => {
|
|
34
40
|
// <[SLOT:basic]>
|
|
35
41
|
})
|
|
36
42
|
})
|
|
@@ -162,7 +162,27 @@ class ProjectNameSDK {
|
|
|
162
162
|
}
|
|
163
163
|
|
|
164
164
|
const status = fetched.status
|
|
165
|
-
|
|
165
|
+
|
|
166
|
+
// No body responses (204 No Content, 304 Not Modified) and explicit
|
|
167
|
+
// zero content-length must skip JSON parsing — fetched.json() would
|
|
168
|
+
// throw `Unexpected end of JSON input` on an empty body.
|
|
169
|
+
const headers = fetched.headers
|
|
170
|
+
const contentLength = headers && 'function' === typeof headers.get
|
|
171
|
+
? headers.get('content-length')
|
|
172
|
+
: (headers || {})['content-length']
|
|
173
|
+
const noBody = 204 === status || 304 === status || '0' === String(contentLength)
|
|
174
|
+
|
|
175
|
+
let json: any = undefined
|
|
176
|
+
if (!noBody) {
|
|
177
|
+
try {
|
|
178
|
+
json = 'function' === typeof fetched.json ? await fetched.json() : fetched.json
|
|
179
|
+
}
|
|
180
|
+
catch (parseErr) {
|
|
181
|
+
// Body wasn't valid JSON — surface the raw response rather than
|
|
182
|
+
// throwing. data stays undefined; callers can inspect status/headers.
|
|
183
|
+
json = undefined
|
|
184
|
+
}
|
|
185
|
+
}
|
|
166
186
|
|
|
167
187
|
return {
|
|
168
188
|
ok: status >= 200 && status < 300,
|
|
@@ -69,8 +69,28 @@ func (f *TestFeature) Init(ctx *core.Context, options map[string]any) {
|
|
|
69
69
|
entmap = map[string]any{}
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
// For single-entity ops (load, remove) with an empty explicit match,
|
|
73
|
+
// fall back to the id the entity client already knows from a prior
|
|
74
|
+
// create/load (in ctx.Match / ctx.Data). Mirrors the TS mock where
|
|
75
|
+
// param() resolves the id from that accumulated state.
|
|
76
|
+
resolveMatch := func(explicit map[string]any) map[string]any {
|
|
77
|
+
if len(explicit) > 0 {
|
|
78
|
+
return explicit
|
|
79
|
+
}
|
|
80
|
+
for _, src := range []any{ctx.Match, ctx.Data} {
|
|
81
|
+
if src == nil {
|
|
82
|
+
continue
|
|
83
|
+
}
|
|
84
|
+
v := vs.GetProp(src, "id")
|
|
85
|
+
if v != nil && v != "__UNDEFINED__" {
|
|
86
|
+
return map[string]any{"id": v}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return map[string]any{}
|
|
90
|
+
}
|
|
91
|
+
|
|
72
92
|
if op.Name == "load" {
|
|
73
|
-
args := self.buildArgs(ctx, op, ctx.Reqmatch)
|
|
93
|
+
args := self.buildArgs(ctx, op, resolveMatch(ctx.Reqmatch))
|
|
74
94
|
found := vs.Select(entmap, args)
|
|
75
95
|
ent := vs.GetElem(found, 0)
|
|
76
96
|
if ent == nil {
|
|
@@ -91,9 +111,37 @@ func (f *TestFeature) Init(ctx *core.Context, options map[string]any) {
|
|
|
91
111
|
out := vs.Clone(found)
|
|
92
112
|
return respond(200, out, nil), nil
|
|
93
113
|
} else if op.Name == "update" {
|
|
94
|
-
|
|
114
|
+
// Match the existing entity by id only (or its alias). Reqdata
|
|
115
|
+
// also contains the new field values, which would otherwise
|
|
116
|
+
// cause Select to filter out the entity we want to update.
|
|
117
|
+
// Falls back to first entity when no match found, mirroring
|
|
118
|
+
// the TS mock.
|
|
119
|
+
updateMatch := map[string]any{}
|
|
120
|
+
if ctx.Reqdata != nil {
|
|
121
|
+
if v, has := ctx.Reqdata["id"]; has {
|
|
122
|
+
updateMatch["id"] = v
|
|
123
|
+
}
|
|
124
|
+
if op.Alias != nil {
|
|
125
|
+
if aliasIdRaw := vs.GetProp(op.Alias, "id"); aliasIdRaw != nil {
|
|
126
|
+
if aliasId, ok := aliasIdRaw.(string); ok {
|
|
127
|
+
if v, has := ctx.Reqdata[aliasId]; has {
|
|
128
|
+
updateMatch[aliasId] = v
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
args := self.buildArgs(ctx, op, updateMatch)
|
|
95
135
|
found := vs.Select(entmap, args)
|
|
96
136
|
ent := vs.GetElem(found, 0)
|
|
137
|
+
if ent == nil && entmap != nil {
|
|
138
|
+
for _, e := range entmap {
|
|
139
|
+
if _, ok := e.(map[string]any); ok {
|
|
140
|
+
ent = e
|
|
141
|
+
break
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
97
145
|
if ent == nil {
|
|
98
146
|
return respond(404, nil, map[string]any{"statusText": "Not found"}), nil
|
|
99
147
|
}
|
|
@@ -109,7 +157,7 @@ func (f *TestFeature) Init(ctx *core.Context, options map[string]any) {
|
|
|
109
157
|
out := vs.Clone(ent)
|
|
110
158
|
return respond(200, out, nil), nil
|
|
111
159
|
} else if op.Name == "remove" {
|
|
112
|
-
args := self.buildArgs(ctx, op, ctx.Reqmatch)
|
|
160
|
+
args := self.buildArgs(ctx, op, resolveMatch(ctx.Reqmatch))
|
|
113
161
|
found := vs.Select(entmap, args)
|
|
114
162
|
ent := vs.GetElem(found, 0)
|
|
115
163
|
if ent == nil {
|
|
@@ -71,12 +71,112 @@ func envOverride(m map[string]any) map[string]any {
|
|
|
71
71
|
}
|
|
72
72
|
|
|
73
73
|
type entityTestSetup struct {
|
|
74
|
-
client
|
|
75
|
-
data
|
|
76
|
-
idmap
|
|
77
|
-
env
|
|
78
|
-
explain
|
|
79
|
-
|
|
74
|
+
client *sdk.ProjectNameSDK
|
|
75
|
+
data map[string]any
|
|
76
|
+
idmap map[string]any
|
|
77
|
+
env map[string]any
|
|
78
|
+
explain bool
|
|
79
|
+
live bool
|
|
80
|
+
syntheticOnly bool
|
|
81
|
+
now int64
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
var (
|
|
85
|
+
cachedTestControl map[string]any
|
|
86
|
+
cachedTestControlOnce sync.Once
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
// loadTestControl reads sdk-test-control.json from this test dir; caches
|
|
90
|
+
// after first read. Returns an empty-skip default if the file is missing
|
|
91
|
+
// or invalid so tests never crash on a bad config.
|
|
92
|
+
func loadTestControl() map[string]any {
|
|
93
|
+
cachedTestControlOnce.Do(func() {
|
|
94
|
+
_, filename, _, _ := runtime.Caller(0)
|
|
95
|
+
dir := filepath.Dir(filename)
|
|
96
|
+
ctrlPath := filepath.Join(dir, "sdk-test-control.json")
|
|
97
|
+
def := map[string]any{
|
|
98
|
+
"version": 1,
|
|
99
|
+
"test": map[string]any{"skip": map[string]any{
|
|
100
|
+
"live": map[string]any{"direct": []any{}, "entityOp": []any{}},
|
|
101
|
+
"unit": map[string]any{"direct": []any{}, "entityOp": []any{}},
|
|
102
|
+
}},
|
|
103
|
+
}
|
|
104
|
+
data, err := os.ReadFile(ctrlPath)
|
|
105
|
+
if err != nil {
|
|
106
|
+
cachedTestControl = def
|
|
107
|
+
return
|
|
108
|
+
}
|
|
109
|
+
var parsed map[string]any
|
|
110
|
+
if err := json.Unmarshal(data, &parsed); err != nil {
|
|
111
|
+
cachedTestControl = def
|
|
112
|
+
return
|
|
113
|
+
}
|
|
114
|
+
cachedTestControl = parsed
|
|
115
|
+
})
|
|
116
|
+
return cachedTestControl
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// isControlSkipped checks sdk-test-control.json for a skip entry.
|
|
120
|
+
// Returns (skip, reason).
|
|
121
|
+
func isControlSkipped(kind, name, mode string) (bool, string) {
|
|
122
|
+
ctrl := loadTestControl()
|
|
123
|
+
test, _ := ctrl["test"].(map[string]any)
|
|
124
|
+
if test == nil {
|
|
125
|
+
return false, ""
|
|
126
|
+
}
|
|
127
|
+
skip, _ := test["skip"].(map[string]any)
|
|
128
|
+
if skip == nil {
|
|
129
|
+
return false, ""
|
|
130
|
+
}
|
|
131
|
+
modeMap, _ := skip[mode].(map[string]any)
|
|
132
|
+
if modeMap == nil {
|
|
133
|
+
return false, ""
|
|
134
|
+
}
|
|
135
|
+
items, _ := modeMap[kind].([]any)
|
|
136
|
+
for _, raw := range items {
|
|
137
|
+
item, _ := raw.(map[string]any)
|
|
138
|
+
if item == nil {
|
|
139
|
+
continue
|
|
140
|
+
}
|
|
141
|
+
reason, _ := item["reason"].(string)
|
|
142
|
+
if kind == "direct" {
|
|
143
|
+
if t, _ := item["test"].(string); t == name {
|
|
144
|
+
return true, reason
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
if kind == "entityOp" {
|
|
148
|
+
ent, _ := item["entity"].(string)
|
|
149
|
+
op, _ := item["op"].(string)
|
|
150
|
+
if ent+"."+op == name {
|
|
151
|
+
return true, reason
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return false, ""
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// liveDelayMs returns the configured per-test live delay in ms; default 500.
|
|
159
|
+
func liveDelayMs() int {
|
|
160
|
+
ctrl := loadTestControl()
|
|
161
|
+
test, _ := ctrl["test"].(map[string]any)
|
|
162
|
+
if test == nil {
|
|
163
|
+
return 500
|
|
164
|
+
}
|
|
165
|
+
live, _ := test["live"].(map[string]any)
|
|
166
|
+
if live == nil {
|
|
167
|
+
return 500
|
|
168
|
+
}
|
|
169
|
+
switch v := live["delayMs"].(type) {
|
|
170
|
+
case float64:
|
|
171
|
+
if v >= 0 {
|
|
172
|
+
return int(v)
|
|
173
|
+
}
|
|
174
|
+
case int:
|
|
175
|
+
if v >= 0 {
|
|
176
|
+
return v
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
return 500
|
|
80
180
|
}
|
|
81
181
|
|
|
82
182
|
var cachedTestSpec map[string]any
|