@mojaloop/ml-testing-toolkit-shared-lib 14.0.5 → 14.2.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/.grype.yaml ADDED
@@ -0,0 +1,13 @@
1
+ disabled: true
2
+ ignore:
3
+
4
+ # Set output format defaults
5
+ output:
6
+ - "table"
7
+ - "json"
8
+
9
+ # Modify your CircleCI job to check critical count
10
+ search:
11
+ scope: "squashed"
12
+ quiet: false
13
+ check-for-app-update: false
package/.nvmrc CHANGED
@@ -1 +1 @@
1
- 22.15.0
1
+ 22.15.1
package/CHANGELOG.md CHANGED
@@ -1,4 +1,18 @@
1
1
  # Changelog: [mojaloop/als-consent-oracle](https://github.com/mojaloop/als-consent-oracle)
2
+ ## [14.2.0](https://github.com/mojaloop/ml-testing-toolkit-shared-lib/compare/v14.1.0...v14.2.0) (2025-07-15)
3
+
4
+
5
+ ### Features
6
+
7
+ * fix vuls and update deps ([#24](https://github.com/mojaloop/ml-testing-toolkit-shared-lib/issues/24)) ([0f8a020](https://github.com/mojaloop/ml-testing-toolkit-shared-lib/commit/0f8a0202058b5ba6cd855ba987f22b384911403a))
8
+
9
+ ## [14.1.0](https://github.com/mojaloop/ml-testing-toolkit-shared-lib/compare/v14.0.5...v14.1.0) (2025-06-03)
10
+
11
+
12
+ ### Features
13
+
14
+ * bump up the node version to v22.15.1 ([#23](https://github.com/mojaloop/ml-testing-toolkit-shared-lib/issues/23)) ([ccd59bd](https://github.com/mojaloop/ml-testing-toolkit-shared-lib/commit/ccd59bdc348ab1b9bce1cb2ee69f6553fb7021a5))
15
+
2
16
  ### [14.0.5](https://github.com/mojaloop/ml-testing-toolkit-shared-lib/compare/v14.0.4...v14.0.5) (2025-05-16)
3
17
 
4
18
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mojaloop/ml-testing-toolkit-shared-lib",
3
- "version": "14.0.5",
3
+ "version": "14.2.0",
4
4
  "description": "Shared library for ml-testing-toolkit re-usable functions",
5
5
  "main": "src/index.js",
6
6
  "contributors": [
@@ -20,7 +20,9 @@
20
20
  "homepage": "https://github.com/mojaloop/ml-testing-toolkit-shared-lib#readme",
21
21
  "pre-commit": [
22
22
  "lint",
23
- "dep:check"
23
+ "dep:check",
24
+ "audit:check",
25
+ "test"
24
26
  ],
25
27
  "scripts": {
26
28
  "start": "node src/index.js",
@@ -30,35 +32,35 @@
30
32
  "lint:fix": "npm run standard:fix",
31
33
  "test": "npm run test:unit",
32
34
  "test:unit": "jest --testMatch '**/test/unit/**/*.test.js'",
35
+ "test:xunit": "npm run test:unit",
33
36
  "test:int": "jest --reporters=default --reporters=jest-junit --testMatch '**/test/integration/**/*.test.js'",
34
37
  "test:coverage": "jest --coverage --coverageThreshold='{}' --testMatch '**/test/unit/**/*.test.js'",
35
38
  "test:coverage-check": "jest --coverage --testMatch '**/test/unit/**/*.test.js'",
36
39
  "test:junit": "jest --reporters=default --reporters=jest-junit --testMatch '**/test/unit/**/*.test.js'",
37
- "test:integration": "./test/integration-runner.sh ",
40
+ "test:integration": "echo 'No integration tests defined'",
41
+ "test:functional": "echo 'No functional tests defined'",
38
42
  "cover": "npx nyc --all report --reporter=lcov npm run test",
39
43
  "audit:check": "npx audit-ci --config ./audit-ci.jsonc",
40
44
  "audit:fix": "npm audit fix",
41
45
  "dep:check": "npx ncu -e 2",
42
46
  "dep:update": "npx ncu -u",
43
- "prepare": "husky || true",
44
47
  "release": "npx standard-version --no-verify --releaseCommitMessageFormat 'chore(release): {{currentTag}} [skip ci]'",
45
48
  "snapshot": "npx standard-version --no-verify --skip.changelog --prerelease snapshot --releaseCommitMessageFormat 'chore(snapshot): {{currentTag}}'"
46
49
  },
47
50
  "dependencies": {
48
- "@faker-js/faker": "^9.8.0",
49
- "@stoplight/json-schema-sampler": "0.3.0",
51
+ "@apidevtools/json-schema-ref-parser": "14.1.0",
52
+ "@faker-js/faker": "9.9.0",
50
53
  "ajv": "8.17.1",
51
- "json-schema-ref-parser": "9.0.9",
54
+ "json-schema-faker": "0.5.9",
52
55
  "lodash": "4.17.21",
53
56
  "node-dir": "0.1.17"
54
57
  },
55
58
  "devDependencies": {
56
59
  "@commitlint/cli": "^19.8.1",
57
60
  "@commitlint/config-conventional": "^19.8.1",
58
- "@types/jest": "29.5.14",
61
+ "@types/jest": "30.0.0",
59
62
  "audit-ci": "^7.1.0",
60
- "husky": "^9.1.7",
61
- "jest": "29.7.0",
63
+ "jest": "30.0.4",
62
64
  "jest-junit": "16.0.0",
63
65
  "npm-check-updates": "18.0.1",
64
66
  "pre-commit": "^1.2.2",
@@ -70,7 +72,8 @@
70
72
  "cross-spawn": "7.0.6",
71
73
  "glob": "9.3.5",
72
74
  "rimraf": "5.0.10",
73
- "@babel/helpers": "7.26.10"
75
+ "@babel/helpers": "7.26.10",
76
+ "brace-expansion": "1.1.12"
74
77
  },
75
78
  "standard-version": {
76
79
  "scripts": {
@@ -32,203 +32,18 @@ const _ = require('lodash')
32
32
  // const jref = require('json-ref-lite')
33
33
  // const yaml = require('js-yaml')
34
34
  const { faker } = require('@faker-js/faker')
35
- const JSONSchemaSampler = require('@stoplight/json-schema-sampler')
36
- const $RefParser = require('json-schema-ref-parser')
35
+ const jsf = require('json-schema-faker')
36
+ const $RefParser = require('@apidevtools/json-schema-ref-parser')
37
37
  const Ajv = require('ajv')
38
38
 
39
- // Configure the sampler options to match original jsf behavior
40
- const samplerOptions = {
41
- skipNonRequired: false,
42
- quiet: true,
43
- maxSampleDepth: 5,
44
- maxItems: 2,
45
- ignoreMissingRefs: true,
46
- formatHandlers: {
47
- byte: () => Buffer.alloc(faker.lorem.sentence(12)).toString('base64'),
48
- date: () => faker.date.anytime().toISOString().split('T')[0],
49
- 'date-time': () => faker.date.anytime().toISOString(),
50
- email: () => faker.internet.email(),
51
- uri: () => faker.internet.url(),
52
- uuid: () => faker.string.uuid(),
53
- phone: () => faker.phone.number('+###########'),
54
- 'geo-latitude': () => {
55
- const val = faker.location.latitude().toFixed(4)
56
- return (parseFloat(val) >= 0 ? '+' : '') + val
57
- },
58
- 'geo-longitude': () => {
59
- const val = faker.location.longitude().toFixed(4)
60
- return (parseFloat(val) >= 0 ? '+' : '') + val
61
- },
62
- 'fsp-id': () => generateFspId(),
63
- currency: () => faker.helpers.arrayElement(['USD', 'EUR', 'GBP', 'JPY', 'AUD', 'CAD', 'CHF', 'CNY', 'INR', 'BRL']),
64
- 'sub-scenario': () => faker.helpers.fromRegExp(/^[A-Z_]{1,32}$/),
65
- name: () => `${faker.person.firstName()} ${faker.person.lastName()}`,
66
- 'complex-name': () => ({
67
- firstName: faker.person.firstName(),
68
- middleName: faker.helpers.maybe(() => faker.person.middleName(), { probability: 0.3 }),
69
- lastName: faker.person.lastName()
70
- }),
71
- 'party-identifier': () => faker.phone.number('##########'),
72
- amount: () => faker.number.float({ min: 0.01, max: 1000, precision: 0.01 }).toFixed(2),
73
- 'transaction-scenario': () => faker.helpers.arrayElement(['DEPOSIT', 'WITHDRAWAL', 'TRANSFER', 'PAYMENT', 'REFUND']),
74
- 'transaction-initiator': () => faker.helpers.arrayElement(['PAYER', 'PAYEE']),
75
- 'transaction-initiator-type': () => faker.helpers.arrayElement(['CONSUMER', 'AGENT', 'BUSINESS', 'DEVICE']),
76
- 'transfer-state': () => faker.helpers.arrayElement(['RECEIVED', 'RESERVED', 'COMMITTED', 'ABORTED'])
77
- },
78
- typeHandlers: {
79
- string: (schema) => {
80
- const key = (schema.key || '').toLowerCase()
81
- const parentKey = (schema.parentKey || '').toLowerCase()
82
-
83
- // Handle patterns
84
- if (schema.pattern) {
85
- if (schema.pattern === 'Created|Closed') {
86
- return faker.helpers.arrayElement(['Created', 'Closed'])
87
- }
88
- if (schema.pattern === 'PERSONAL_ID|BUSINESS_ID') {
89
- return faker.helpers.arrayElement(['PERSONAL_ID', 'BUSINESS_ID'])
90
- }
91
- if (schema.pattern === '123') {
92
- return '123'
93
- }
94
- if (schema.pattern === '^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$') {
95
- return faker.internet.email()
96
- }
97
- if (schema.pattern === '^[A-Z0-9]{1,32}$') {
98
- return generateFspId()
99
- }
100
- if (schema.pattern === '^\\d{10,15}$') {
101
- // Always generate a string of 10-15 digits
102
- return generateDigitsPhoneNumber(faker.number.int({ min: 10, max: 15 }))
103
- }
104
- }
105
-
106
- // Handle Content-Length
107
- if (key === 'content-length' || key === 'contentlength') {
108
- return '123'
109
- }
110
-
111
- // Handle state, reason, status
112
- if (key === 'state' || key === 'reason' || key === 'status') {
113
- return faker.helpers.arrayElement(['Created', 'Closed'])
114
- }
115
-
116
- // Handle partyIdType
117
- if (key === 'partyidtype' || (parentKey === 'partyidinfo' && key === 'partyidtype')) {
118
- return faker.helpers.arrayElement(['PERSONAL_ID', 'BUSINESS_ID'])
119
- }
120
-
121
- // Handle transferState
122
- if (key === 'transferstate') {
123
- return faker.helpers.arrayElement(['RECEIVED', 'RESERVED', 'COMMITTED', 'ABORTED'])
124
- }
125
-
126
- // Handle FSP ID fields (fspid, payerfsp, payeefsp)
127
- if (key === 'fspid' || key === 'payerfsp' || key === 'payeefsp' || (parentKey === 'partyidinfo' && key === 'fspid')) {
128
- return generateFspId()
129
- }
130
-
131
- // Handle currency
132
- if (key === 'currency' || (parentKey === 'amount' && key === 'currency')) {
133
- const currency = faker.helpers.arrayElement(['USD', 'EUR', 'GBP', 'JPY', 'AUD', 'CAD', 'CHF', 'CNY', 'INR', 'BRL'])
134
- if (schema.parentKey === 'amount') {
135
- schema._currency = currency
136
- }
137
- return currency
138
- }
139
-
140
- // Handle amount (including nested amount.amount)
141
- if (key === 'amount') {
142
- return faker.number.float({ min: 0.01, max: 1000, precision: 0.01 }).toFixed(2)
143
- }
144
-
145
- // Handle partyIdentifier (digits only)
146
- if (key === 'partyidentifier' || (parentKey === 'partyidinfo' && key === 'partyidentifier')) {
147
- return generateDigitsPhoneNumber(faker.number.int({ min: 10, max: 15 }))
148
- }
149
-
150
- // Handle geo coordinates
151
- if (key === 'latitude') {
152
- const val = faker.location.latitude().toFixed(4)
153
- return (parseFloat(val) >= 0 ? '+' : '') + val
154
- }
155
- if (key === 'longitude') {
156
- const val = faker.location.longitude().toFixed(4)
157
- return (parseFloat(val) >= 0 ? '+' : '') + val
158
- }
159
-
160
- // Handle UUIDs for IDs
161
- if (key === 'bulkquoteid' || key === 'bulktransferid' || key === 'quoteid' || key === 'transferid' || key === 'id') {
162
- return faker.string.uuid()
163
- }
39
+ jsf.format('byte', () => Buffer.alloc(faker.lorem.sentence({ min: 12, max: 12 })).toString('base64'))
164
40
 
165
- // Handle scenario, initiator, initiatorType
166
- if (key === 'scenario' || (parentKey === 'transactiontype' && key === 'scenario')) {
167
- return faker.helpers.arrayElement(['DEPOSIT', 'WITHDRAWAL', 'TRANSFER', 'PAYMENT', 'REFUND'])
168
- }
169
- if (key === 'initiator' || (parentKey === 'transactiontype' && key === 'initiator')) {
170
- return faker.helpers.arrayElement(['PAYER', 'PAYEE'])
171
- }
172
- if (key === 'initiatortype' || (parentKey === 'transactiontype' && key === 'initiatortype')) {
173
- return faker.helpers.arrayElement(['CONSUMER', 'AGENT', 'BUSINESS', 'DEVICE'])
174
- }
175
-
176
- // Handle firstName and lastName fields (single name, capitalized, strictly alphabetic)
177
- if (key === 'firstname') {
178
- let first
179
- do {
180
- first = faker.person.firstName()
181
- first = `${first.charAt(0).toUpperCase()}${first.slice(1)}`
182
- } while (!/^[A-Z][a-z]+$/.test(first))
183
- return first
184
- }
185
- if (key === 'lastname') {
186
- let last
187
- do {
188
- last = faker.person.lastName()
189
- last = `${last.charAt(0).toUpperCase()}${last.slice(1)}`
190
- } while (!/^[A-Z][a-z]+$/.test(last))
191
- return last
192
- }
193
-
194
- // Handle name field (full name)
195
- if (key === 'name') {
196
- const first = faker.person.firstName()
197
- const last = faker.person.lastName()
198
- return `${first.charAt(0).toUpperCase()}${first.slice(1)} ${last.charAt(0).toUpperCase()}${last.slice(1)}`
199
- }
200
-
201
- // Handle complexName (string) to always return a full name (Firstname Lastname)
202
- if (key === 'complexname' && schema.type === 'string') {
203
- const first = faker.person.firstName()
204
- const last = faker.person.lastName()
205
- return `${first.charAt(0).toUpperCase()}${first.slice(1)} ${last.charAt(0).toUpperCase()}${last.slice(1)}`
206
- }
207
-
208
- // For any other key ending with 'name' (but not 'name', 'firstname', 'lastname', or 'complexname'), generate a single capitalized name.
209
- if (/name$/i.test(key) && key !== 'name' && key !== 'firstname' && key !== 'lastname' && key !== 'complexname') {
210
- const single = faker.person.firstName()
211
- return `${single.charAt(0).toUpperCase()}${single.slice(1)}`
212
- }
213
-
214
- // Fallback to customTypeHandler for any other string field
215
- const custom = customTypeHandler(schema)
216
- if (custom !== undefined) return custom
217
-
218
- // Default fallback
219
- return faker.lorem.sentence()
220
- },
221
- number: (schema) => {
222
- const key = (schema.key || '').toLowerCase()
223
- if (key === 'amount' || /amount$/i.test(key)) {
224
- return faker.number.float({ min: 0.01, max: 1000, precision: 0.01 }).toFixed(2)
225
- }
226
- return faker.number.float({ min: 1, max: 1000, precision: 0.01 })
227
- },
228
- integer: () => faker.number.int({ min: 1, max: 1000 }),
229
- boolean: () => faker.datatype.boolean()
230
- }
231
- }
41
+ jsf.option({
42
+ alwaysFakeOptionals: true,
43
+ ignoreMissingRefs: true,
44
+ maxItems: 2
45
+ })
46
+ jsf.extend('faker', () => require('@faker-js/faker').faker)
232
47
 
233
48
  const ajv = new Ajv({
234
49
  strict: false,
@@ -322,574 +137,124 @@ const findRequestSchema = (r) => {
322
137
  return _.find(content).schema
323
138
  }
324
139
 
325
- // Helper function to match field names
326
- function matchesField (key, ...names) {
327
- if (!key) return false
328
- const keyLower = key.toLowerCase()
329
- return names.some(name => keyLower === name.toLowerCase() || keyLower.endsWith(name.toLowerCase()))
330
- }
331
-
332
- // Fix date-time format handler to always generate a valid ISO string with milliseconds
333
- const customFormatHandlers = {
334
- name: () => {
335
- const firstNames = ['John', 'Jane', 'Michael', 'Sarah', 'David', 'Emma']
336
- const lastNames = ['Smith', 'Johnson', 'Williams', 'Brown', 'Jones', 'Garcia']
337
- return `${firstNames[Math.floor(Math.random() * firstNames.length)]} ${lastNames[Math.floor(Math.random() * lastNames.length)]}`
338
- },
339
- amount: () => {
340
- return (Math.random() * 1000).toFixed(2)
341
- },
342
- phone: () => {
343
- return Math.floor(Math.random() * 9000000000 + 1000000000).toString()
344
- },
345
- 'geo-latitude': () => {
346
- const val = (Math.random() * 180 - 90).toFixed(4)
347
- return (val >= 0 ? '+' : '') + val
348
- },
349
- 'geo-longitude': () => {
350
- const val = (Math.random() * 360 - 180).toFixed(4)
351
- return (val >= 0 ? '+' : '') + val
352
- },
353
- uuid: () => {
354
- return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
355
- const r = Math.random() * 16 | 0
356
- const v = c === 'x' ? r : (r & 0x3 | 0x8)
357
- return v.toString(16)
358
- })
359
- },
360
- 'date-time': () => {
361
- // Always use toISOString() which includes milliseconds
362
- return new Date().toISOString()
363
- }
364
- }
365
-
366
- // Custom type handler for the sampler
367
- const customTypeHandler = (schema) => {
368
- if (!schema || typeof schema !== 'object') return undefined
369
-
370
- const key = schema.key || ''
371
-
372
- // Always handle state, reason, status fields
373
- if (matchesField(key, 'state', 'status', 'reason')) {
374
- return ['Created', 'Closed'][Math.floor(Math.random() * 2)]
375
- }
376
-
377
- // Always handle party ID type fields
378
- if (matchesField(key, 'partyIdType', 'idType')) {
379
- return ['PERSONAL_ID', 'BUSINESS_ID'][Math.floor(Math.random() * 2)]
380
- }
381
-
382
- // Always handle content length
383
- if (matchesField(key, 'contentLength', 'content-length')) {
384
- return '123'
385
- }
386
-
387
- // Handle email fields
388
- if (matchesField(key, 'email', 'emailAddress')) {
389
- const domains = ['example.com', 'test.com', 'domain.com']
390
- const name = Math.random().toString(36).substring(2, 8)
391
- return `${name}@${domains[Math.floor(Math.random() * domains.length)]}`
392
- }
393
-
394
- // Handle date-time fields
395
- if (matchesField(key, 'expiration', 'dateTime', 'datetime', 'timestamp')) {
396
- return customFormatHandlers['date-time']()
397
- }
398
-
399
- // Handle date fields
400
- if (matchesField(key, 'dateOfBirth', 'date')) {
401
- const date = new Date()
402
- return date.toISOString().split('T')[0]
403
- }
404
-
405
- // Use format handlers for other fields
406
- if (schema.format && customFormatHandlers[schema.format]) {
407
- return customFormatHandlers[schema.format]()
408
- }
409
-
410
- return undefined
411
- }
412
-
413
- // Preprocess schema to inject default values for known fields
414
- function preprocessSchema (schema, parentKey = null) {
415
- if (!schema || typeof schema !== 'object') return schema
416
-
417
- const processed = JSON.parse(JSON.stringify(schema))
418
- processed.parentKey = parentKey
419
-
420
- // Process properties recursively
421
- if (processed.properties) {
422
- Object.keys(processed.properties).forEach(key => {
423
- processed.properties[key] = preprocessSchema(processed.properties[key], key)
424
- })
425
- }
426
-
427
- // Process items for arrays
428
- if (processed.items) {
429
- processed.items = preprocessSchema(processed.items, parentKey)
430
- }
431
-
432
- // Handle specific field types
433
- if (processed.type === 'string') {
434
- const key = processed.key || ''
435
- // Handle state and reason fields
436
- if (matchesField(key, 'state', 'status', 'reason')) {
437
- processed.format = 'transaction-state'
438
- return processed
439
- }
440
- // Handle party ID type fields
441
- if (matchesField(key, 'partyIdType', 'idType')) {
442
- processed.format = 'party-id-type'
443
- return processed
444
- }
445
- // Handle FSP ID fields
446
- if (matchesField(key, 'fspId', 'fspid')) {
447
- processed.format = 'fsp-id'
448
- return processed
449
- }
450
- // Handle currency fields
451
- if (matchesField(key, 'currency')) {
452
- processed.format = 'currency'
453
- return processed
454
- }
455
- // Handle date-time fields
456
- if (matchesField(key, 'expiration', 'datetime', 'timestamp')) {
457
- processed.format = 'date-time'
458
- return processed
459
- }
460
- // Handle date fields
461
- if (matchesField(key, 'dateOfBirth', 'date')) {
462
- processed.format = 'date'
463
- return processed
464
- }
465
- // Handle party identifier fields
466
- if (matchesField(key, 'partyIdentifier')) {
467
- processed.format = 'party-identifier'
468
- return processed
469
- }
470
- // Handle transaction type fields
471
- if (matchesField(key, 'scenario')) {
472
- processed.format = 'transaction-scenario'
473
- return processed
474
- }
475
- if (matchesField(key, 'initiator')) {
476
- processed.format = 'transaction-initiator'
477
- return processed
478
- }
479
- if (matchesField(key, 'initiatorType')) {
480
- processed.format = 'transaction-initiator-type'
481
- return processed
482
- }
483
- // Handle transfer state fields
484
- if (matchesField(key, 'transferState')) {
485
- processed.format = 'transfer-state'
486
- return processed
487
- }
488
- // Handle subScenario fields
489
- if (matchesField(key, 'subScenario')) {
490
- processed.format = 'sub-scenario'
491
- return processed
492
- }
493
- // Handle complex name fields
494
- if (matchesField(key, 'complexName')) {
495
- processed.type = 'object'
496
- processed.properties = {
497
- firstName: { type: 'string', format: 'name' },
498
- middleName: { type: 'string', format: 'name' },
499
- lastName: { type: 'string', format: 'name' }
500
- }
501
- return processed
502
- }
503
- // Handle bulk operation IDs
504
- if (matchesField(key, 'bulkQuoteId', 'bulkTransferId', 'quoteId', 'transferId', 'id')) {
505
- processed.format = 'uuid'
506
- return processed
507
- }
508
- }
509
- return processed
510
- }
140
+ const generateMockResponseBody = async (method, name, data, jsfRefs) => {
141
+ const responseSchema = findResponseSchema(data.responses)
142
+ // Create a new copy of object without copying by references
143
+ const newResponseSchema = JSON.parse(JSON.stringify(responseSchema))
511
144
 
512
- const processRefs = (schema, jsfRefs) => {
513
- if (!jsfRefs || !jsfRefs.length) {
514
- // Preprocess schema even if no jsfRefs
515
- return preprocessSchema(schema)
145
+ if (!newResponseSchema) {
146
+ return {}
516
147
  }
517
-
518
- const newSchema = JSON.parse(JSON.stringify(schema))
519
-
520
- // Process each reference
521
148
  jsfRefs.forEach(ref => {
522
- // Convert the reference ID to a valid JSON pointer path
523
- const path = ref.id.split('.').map(part => {
524
- // Escape special characters in JSON pointer
525
- return part.replace(/~/g, '~0').replace(/\//g, '~1')
526
- }).join('/')
149
+ const convertedId = ref.id.replace(/\.(?!items)/g, '.properties.')
527
150
 
528
- // Helper function to process a schema object
529
- const processSchemaObject = (targetObject) => {
530
- if (!targetObject) return
151
+ const targetObject = _.get(newResponseSchema.type === 'array' ? newResponseSchema.items.properties : newResponseSchema.properties, convertedId)
531
152
 
532
- // Preserve the original type and properties
153
+ if (targetObject) {
154
+ targetObject.$ref = ref.id
533
155
  if (ref.pattern) {
534
- // For pattern-based fields, ensure we have a string type
535
- targetObject.type = 'string'
536
- targetObject.enum = ['Created', 'Closed'] // Use enum instead of pattern
537
- delete targetObject.pattern
538
- delete targetObject.format
539
- } else if (ref.id === 'Content-Length') {
540
- // Special handling for Content-Length
541
- targetObject.type = 'string'
542
- targetObject.enum = ['123']
543
- delete targetObject.pattern
544
- delete targetObject.format
545
- } else if (ref.id === 'partyIdType') {
546
- // Special handling for partyIdType
547
- targetObject.type = 'string'
548
- targetObject.enum = ['PERSONAL_ID', 'BUSINESS_ID']
549
156
  delete targetObject.pattern
550
- delete targetObject.format
157
+ delete targetObject.enum
551
158
  }
552
159
  }
553
-
554
- // Handle array items
555
- if (newSchema.type === 'array' && newSchema.items) {
556
- if (newSchema.items.properties) {
557
- const targetObject = _.get(newSchema.items.properties, path)
558
- processSchemaObject(targetObject)
559
- }
560
- }
561
-
562
- // Handle object properties
563
- if (newSchema.properties) {
564
- const targetObject = _.get(newSchema.properties, path)
565
- processSchemaObject(targetObject)
566
- }
567
-
568
- // Handle nested objects
569
- const processNestedObjects = (obj) => {
570
- if (!obj || typeof obj !== 'object') return
571
-
572
- if (obj.properties) {
573
- Object.keys(obj.properties).forEach(key => {
574
- if (key === ref.id) {
575
- processSchemaObject(obj.properties[key])
576
- } else {
577
- processNestedObjects(obj.properties[key])
578
- }
579
- })
580
- }
581
-
582
- if (obj.items) {
583
- processNestedObjects(obj.items)
584
- }
585
- }
586
-
587
- processNestedObjects(newSchema)
588
160
  })
589
161
 
590
- // Preprocess the schema after handling refs
591
- return preprocessSchema(newSchema)
592
- }
593
-
594
- // Helper to generate a digits-only phone number of length 10-15
595
- function generateDigitsPhoneNumber (length = 12) {
596
- let num = ''
597
- while (num.length < length) {
598
- num += faker.number.int({ min: 0, max: 9 }).toString()
599
- }
600
- return num.slice(0, length)
601
- }
602
-
603
- // Helper to generate a random uppercase alphanumeric string of length 1-32
604
- function generateFspId () {
605
- const length = faker.number.int({ min: 1, max: 32 })
606
- let result = ''
607
- const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
608
- for (let i = 0; i < length; i++) {
609
- result += chars.charAt(faker.number.int({ min: 0, max: chars.length - 1 }))
610
- }
611
- return result
612
- }
613
-
614
- // Deep schema walk to generate mock data
615
- function deepSchemaWalk (schema, key = null, jsfRefs = [], parentKey = null) {
616
- if (!schema || typeof schema !== 'object') return undefined
617
-
618
- // Add parent key context
619
- schema.parentKey = parentKey
620
-
621
- // Handle arrays
622
- if (schema.type === 'array') {
623
- const maxItems = schema.maxItems || 2
624
- const minItems = schema.minItems || 0
625
- let count = Math.min(maxItems, Math.max(minItems, 1))
626
-
627
- // For bulk operations, ensure at least one item
628
- if (parentKey === 'individualQuotes' || parentKey === 'individualTransfers') {
629
- count = Math.max(1, count)
630
- }
631
-
632
- const items = []
633
- for (let i = 0; i < count; i++) {
634
- const item = deepSchemaWalk(schema.items, null, jsfRefs, key)
635
- if (item !== undefined) {
636
- items.push(item)
637
- }
638
- }
639
- return items
640
- }
641
-
642
- // Handle objects
643
- if (schema.type === 'object' || schema.properties) {
644
- const result = {}
645
- const properties = schema.properties || {}
646
- const required = schema.required || []
647
-
648
- // Always process required fields
649
- for (const propKey of required) {
650
- const propSchema = properties[propKey]
651
- const value = deepSchemaWalk(propSchema, propKey, jsfRefs, propKey)
652
- if (value !== undefined) {
653
- result[propKey] = value
654
- }
655
- }
656
-
657
- // Process optional fields based on skipNonRequired setting
658
- if (!samplerOptions.skipNonRequired) {
659
- for (const propKey in properties) {
660
- if (!required.includes(propKey)) {
661
- const propSchema = properties[propKey]
662
- const value = deepSchemaWalk(propSchema, propKey, jsfRefs, propKey)
663
- if (value !== undefined) {
664
- result[propKey] = value
665
- }
666
- }
667
- }
668
- }
669
-
670
- // Handle currency matching for fees
671
- if (result.amount && result.amount.currency && result.fees) {
672
- result.fees.currency = result.amount.currency
673
- }
674
-
675
- return result
676
- }
677
-
678
- // Handle primitive types
679
- if (schema.type) {
680
- const typeHandler = samplerOptions.typeHandlers[schema.type]
681
- if (typeHandler) {
682
- return typeHandler({ ...schema, key, parentKey })
162
+ const fakedResponse = {}
163
+ fakedResponse.body = await jsf.resolve(newResponseSchema, jsfRefs)
164
+ for (const key in data.responses) {
165
+ fakedResponse.status = key
166
+ if (key >= 200 && key <= 299) {
167
+ break
683
168
  }
684
169
  }
685
-
686
- return undefined
170
+ return fakedResponse
687
171
  }
688
172
 
689
- // Modify postProcessMock to use deepSchemaWalk
690
- function postProcessMock (obj, schema, key = null, jsfRefs = []) {
691
- // If schema is missing, try to infer type from obj
692
- if (!schema || typeof schema !== 'object') {
693
- if (obj === 'string' && key) {
694
- const fake = customTypeHandler({ key, type: 'string' })
695
- return fake !== undefined ? fake : 'string'
696
- }
697
- return obj
698
- }
699
-
700
- // Use deepSchemaWalk to generate a complete mock object
701
- const generated = deepSchemaWalk(schema, key, jsfRefs)
702
-
703
- // If deepSchemaWalk generated something, use it
704
- if (generated !== undefined && typeof generated === 'object' && !Array.isArray(generated)) {
705
- // Add missing top-level fields if required by tests
706
- // 1. state, reason, partyIdType
707
- if (schema.properties) {
708
- if (schema.properties.state && generated.state === undefined) {
709
- generated.state = faker.helpers.arrayElement(['Created', 'Closed'])
710
- }
711
- if (schema.properties.reason && generated.reason === undefined) {
712
- generated.reason = faker.helpers.arrayElement(['Created', 'Closed'])
713
- }
714
- if (schema.properties.partyIdType && generated.partyIdType === undefined) {
715
- generated.partyIdType = faker.helpers.arrayElement(['PERSONAL_ID', 'BUSINESS_ID'])
716
- }
717
- if (schema.properties.transferState && generated.transferState === undefined) {
718
- generated.transferState = faker.helpers.arrayElement(['RECEIVED', 'RESERVED', 'COMMITTED', 'ABORTED'])
719
- }
720
- if (schema.properties.bulkQuoteId && generated.bulkQuoteId === undefined) {
721
- generated.bulkQuoteId = faker.string.uuid()
722
- }
723
- if (schema.properties.bulkTransferId && generated.bulkTransferId === undefined) {
724
- generated.bulkTransferId = faker.string.uuid()
725
- }
726
- if (schema.properties.subScenario && generated.subScenario === undefined) {
727
- generated.subScenario = faker.helpers.fromRegExp(/^[A-Z_]{1,32}$/)
728
- }
729
- }
730
- // Patch for nested FSP ID and name fields
731
- function patchNested (obj) {
732
- if (!obj || typeof obj !== 'object') return
733
- for (const k in obj) {
734
- if (k.toLowerCase() === 'fspid' && (typeof obj[k] !== 'string' || !/^[A-Z0-9]{1,32}$/.test(obj[k]))) {
735
- obj[k] = generateFspId()
736
- }
737
- // Only apply full name fallback to 'name' field, not firstName, lastName, or complexName
738
- if (
739
- k.toLowerCase() === 'name' &&
740
- (typeof obj[k] !== 'string' || !/^[A-Z][a-z]+ [A-Z][a-z]+$/.test(obj[k]))
741
- ) {
742
- const first = faker.person.firstName()
743
- const last = faker.person.lastName()
744
- obj[k] = `${first.charAt(0).toUpperCase()}${first.slice(1)} ${last.charAt(0).toUpperCase()}${last.slice(1)}`
745
- }
746
- if (typeof obj[k] === 'object') patchNested(obj[k])
747
- }
748
- }
749
- patchNested(generated)
750
- return generated
751
- }
752
-
753
- // Fallback to original object if deepSchemaWalk didn't generate anything
754
- if (typeof obj === 'object' && !Array.isArray(obj)) {
755
- const result = {}
756
- for (const k in obj) {
757
- if (schema.properties && schema.properties[k]) {
758
- result[k] = postProcessMock(obj[k], schema.properties[k], k, jsfRefs)
759
- } else {
760
- result[k] = obj[k]
761
- }
762
- }
763
- return result
764
- }
765
-
766
- return obj
767
- }
768
-
769
- // Update generateMockOperation to pass jsfRefs to postProcessMock
770
173
  const generateMockOperation = async (method, name, data, jsfRefs) => {
771
- if (!data || !data.requestBody) {
772
- return {}
773
- }
774
-
775
174
  const requestSchema = findRequestSchema(data.requestBody)
776
- if (!requestSchema) {
777
- return {}
778
- }
779
-
780
- try {
781
- const processedSchema = processRefs(requestSchema, jsfRefs)
782
- let sampled = JSONSchemaSampler.sample(processedSchema, samplerOptions)
783
- sampled = postProcessMock(sampled, processedSchema, null, jsfRefs)
784
- return sampled
785
- } catch (error) {
786
- console.error(`Error generating mock operation for ${method} ${name}:`, error)
787
- return {}
788
- }
789
- }
790
-
791
- // Update other generateMock functions similarly
792
- const generateMockResponseBody = async (method, name, data, jsfRefs) => {
793
- if (OpenApiRequestGenerator._testForceError === 'generateMockResponseBody') throw new Error('Forced error for test')
794
- if (!data || !data.responses) {
795
- return {}
796
- }
797
-
798
- const responseSchema = findResponseSchema(data.responses)
799
- if (!responseSchema) {
800
- return {}
801
- }
802
-
803
- try {
804
- const processedSchema = processRefs(responseSchema, jsfRefs)
805
- const fakedResponse = {}
806
- let sampled = JSONSchemaSampler.sample(processedSchema, samplerOptions)
807
- sampled = postProcessMock(sampled, processedSchema, null, jsfRefs)
808
- fakedResponse.body = sampled
809
-
810
- for (const key in data.responses) {
811
- fakedResponse.status = key
812
- if (key >= 200 && key <= 299) {
813
- break
175
+ // Create a new copy of object without copying by references
176
+ const newRequestSchema = JSON.parse(JSON.stringify(requestSchema))
177
+ jsfRefs.forEach(ref => {
178
+ const convertedId = ref.id.replace(/\./g, '.properties.')
179
+ const targetObject = _.get(newRequestSchema.properties, convertedId)
180
+ if (targetObject) {
181
+ targetObject.$ref = ref.id
182
+ if (ref.pattern) {
183
+ delete targetObject.pattern
184
+ delete targetObject.enum
814
185
  }
815
186
  }
816
- return fakedResponse
817
- } catch (error) {
818
- console.error(`Error generating mock response for ${method} ${name}:`, error)
819
- return {}
820
- }
187
+ })
188
+
189
+ const fakedResponse = await jsf.resolve(newRequestSchema, jsfRefs)
190
+
191
+ return fakedResponse
821
192
  }
822
193
 
823
194
  const generateMockHeaders = async (method, name, data, jsfRefs) => {
824
- if (OpenApiRequestGenerator._testForceError === 'generateMockHeaders') throw new Error('Forced error for test')
825
- if (!data || !Array.isArray(data.parameters)) {
826
- return {}
827
- }
828
- const properties = {}
195
+ const headers = {}
829
196
  data.parameters.forEach(param => {
830
197
  if (param.in === 'header') {
831
- properties[param.name] = (param.schema && param.schema.type) ? { type: param.schema.type } : {}
198
+ headers[param.name] = (param.schema && param.schema.type) ? { type: param.schema.type } : {}
199
+ }
200
+ })
201
+ jsfRefs.forEach(ref => {
202
+ if (headers[ref.id]) {
203
+ headers[ref.id] = { $ref: ref.id }
832
204
  }
833
205
  })
834
206
 
835
- if (Object.keys(properties).length === 0) {
207
+ if (Object.keys(headers).length === 0) {
836
208
  return {}
837
209
  }
838
210
 
839
- const schema = { type: 'object', properties }
840
- const processedSchema = processRefs(schema, jsfRefs)
841
- let sampled = JSONSchemaSampler.sample(processedSchema, samplerOptions)
842
- sampled = postProcessMock(sampled, processedSchema, null, jsfRefs)
843
- if (sampled === null) sampled = {}
844
- return sampled
211
+ const fakedResponse = await jsf.resolve(headers, jsfRefs)
212
+
213
+ return fakedResponse
845
214
  }
846
215
 
847
216
  const generateMockQueryParams = async (method, name, data, jsfRefs) => {
848
- if (OpenApiRequestGenerator._testForceError === 'generateMockQueryParams') throw new Error('Forced error for test')
849
- if (!data || !Array.isArray(data.parameters)) {
850
- return {}
851
- }
852
- const properties = {}
217
+ const queryParams = {}
853
218
  data.parameters.forEach(param => {
854
219
  if (param.in === 'query') {
855
- properties[param.name] = (param.schema) ? { ...param.schema } : {}
220
+ queryParams[param.name] = (param.schema) ? { ...param.schema } : {}
221
+ }
222
+ })
223
+ jsfRefs.forEach(ref => {
224
+ if (queryParams[ref.id]) {
225
+ queryParams[ref.id] = { $ref: ref.id }
856
226
  }
857
227
  })
858
228
 
859
- if (Object.keys(properties).length === 0) {
229
+ if (Object.keys(queryParams).length === 0) {
860
230
  return {}
861
231
  }
862
232
 
863
- const schema = { type: 'object', properties }
864
- const processedSchema = processRefs(schema, jsfRefs)
865
- let sampled = JSONSchemaSampler.sample(processedSchema, samplerOptions)
866
- sampled = postProcessMock(sampled, processedSchema, null, jsfRefs)
867
- if (sampled === null) sampled = {}
868
- return sampled
233
+ const fakedResponse = await jsf.resolve(queryParams, jsfRefs)
234
+
235
+ return fakedResponse
869
236
  }
870
237
 
871
238
  const generateMockPathParams = async (method, name, data, jsfRefs) => {
872
- if (OpenApiRequestGenerator._testForceError === 'generateMockPathParams') throw new Error('Forced error for test')
873
- if (!data || !Array.isArray(data.parameters)) {
874
- return {}
875
- }
876
- const properties = {}
239
+ const pathParams = {}
877
240
  data.parameters.forEach(param => {
878
241
  if (param.in === 'path') {
879
- properties[param.name] = (param.schema) ? { ...param.schema } : {}
242
+ pathParams[param.name] = (param.schema) ? { ...param.schema } : {}
243
+ }
244
+ })
245
+ jsfRefs.forEach(ref => {
246
+ if (pathParams[ref.id]) {
247
+ pathParams[ref.id] = { $ref: ref.id }
880
248
  }
881
249
  })
882
250
 
883
- if (Object.keys(properties).length === 0) {
251
+ if (Object.keys(pathParams).length === 0) {
884
252
  return {}
885
253
  }
886
254
 
887
- const schema = { type: 'object', properties }
888
- const processedSchema = processRefs(schema, jsfRefs)
889
- let sampled = JSONSchemaSampler.sample(processedSchema, samplerOptions)
890
- sampled = postProcessMock(sampled, processedSchema, null, jsfRefs)
891
- if (sampled === null) sampled = {}
892
- return sampled
255
+ const fakedResponse = await jsf.resolve(pathParams, jsfRefs)
256
+
257
+ return fakedResponse
893
258
  }
894
259
 
895
260
  class OpenApiRequestGenerator {
@@ -898,105 +263,44 @@ class OpenApiRequestGenerator {
898
263
  }
899
264
 
900
265
  async load (schemaPath) {
901
- try {
902
- this.schema = await loadYamlFile(schemaPath)
903
- } catch (error) {
904
- console.error('Error loading schema:', error)
905
- this.schema = {}
906
- }
266
+ this.schema = await loadYamlFile(schemaPath)
907
267
  }
908
268
 
909
269
  async generateRequestBody (path, httpMethod, jsfRefs = []) {
910
- try {
911
- const pathValue = this.schema.paths?.[path]
912
- if (!pathValue) {
913
- return {}
914
- }
915
- const operation = pathValue[httpMethod]
916
- if (!operation) {
917
- return {}
918
- }
919
- const id = operation.operationId || operation.summary
920
- return generateMockOperation(httpMethod, id, operation, jsfRefs)
921
- } catch (error) {
922
- console.error(`Error generating request body for ${path} ${httpMethod}:`, error)
923
- return {}
924
- }
270
+ const pathValue = this.schema.paths[path]
271
+ const operation = pathValue[httpMethod]
272
+ const id = operation.operationId || operation.summary
273
+ return generateMockOperation(httpMethod, id, operation, jsfRefs)
925
274
  }
926
275
 
927
276
  async generateRequestHeaders (path, httpMethod, jsfRefs = []) {
928
- try {
929
- const pathValue = this.schema.paths?.[path]
930
- if (!pathValue) {
931
- return {}
932
- }
933
- const operation = pathValue[httpMethod]
934
- if (!operation) {
935
- return {}
936
- }
937
- const id = operation.operationId || operation.summary
938
- return generateMockHeaders(httpMethod, id, operation, jsfRefs)
939
- } catch (error) {
940
- console.error(`Error generating request headers for ${path} ${httpMethod}:`, error)
941
- return {}
942
- }
277
+ const pathValue = this.schema.paths[path]
278
+ const operation = pathValue[httpMethod]
279
+ const id = operation.operationId || operation.summary
280
+ return generateMockHeaders(httpMethod, id, operation, jsfRefs)
943
281
  }
944
282
 
945
283
  async generateRequestQueryParams (path, httpMethod, jsfRefs = []) {
946
- try {
947
- const pathValue = this.schema.paths?.[path]
948
- if (!pathValue) {
949
- return {}
950
- }
951
- const operation = pathValue[httpMethod]
952
- if (!operation) {
953
- return {}
954
- }
955
- const id = operation.operationId || operation.summary
956
- return generateMockQueryParams(httpMethod, id, operation, jsfRefs)
957
- } catch (error) {
958
- console.error(`Error generating query params for ${path} ${httpMethod}:`, error)
959
- return {}
960
- }
284
+ const pathValue = this.schema.paths[path]
285
+ const operation = pathValue[httpMethod]
286
+ const id = operation.operationId || operation.summary
287
+ return generateMockQueryParams(httpMethod, id, operation, jsfRefs)
961
288
  }
962
289
 
963
290
  async generateRequestPathParams (path, httpMethod, jsfRefs = []) {
964
- try {
965
- const pathValue = this.schema.paths?.[path]
966
- if (!pathValue) {
967
- return {}
968
- }
969
- const operation = pathValue[httpMethod]
970
- if (!operation) {
971
- return {}
972
- }
973
- const id = operation.operationId || operation.summary
974
- return generateMockPathParams(httpMethod, id, operation, jsfRefs)
975
- } catch (error) {
976
- console.error(`Error generating path params for ${path} ${httpMethod}:`, error)
977
- return {}
978
- }
291
+ const pathValue = this.schema.paths[path]
292
+ const operation = pathValue[httpMethod]
293
+ const id = operation.operationId || operation.summary
294
+ return generateMockPathParams(httpMethod, id, operation, jsfRefs)
979
295
  }
980
296
 
981
297
  async generateResponseBody (path, httpMethod, jsfRefs = []) {
982
- try {
983
- const pathValue = this.schema.paths?.[path]
984
- if (!pathValue) {
985
- return {}
986
- }
987
- const operation = pathValue[httpMethod]
988
- if (!operation) {
989
- return {}
990
- }
991
- const id = operation.operationId || operation.summary
992
- return generateMockResponseBody(httpMethod, id, operation, jsfRefs)
993
- } catch (error) {
994
- console.error(`Error generating response body for ${path} ${httpMethod}:`, error)
995
- return {}
996
- }
298
+ const pathValue = this.schema.paths[path]
299
+ const operation = pathValue[httpMethod]
300
+ const id = operation.operationId || operation.summary
301
+
302
+ return generateMockResponseBody(httpMethod, id, operation, jsfRefs)
997
303
  }
998
304
  }
999
305
 
1000
- OpenApiRequestGenerator._testForceError = null
1001
-
1002
306
  module.exports = OpenApiRequestGenerator
package/.husky/commit-msg DELETED
@@ -1,4 +0,0 @@
1
- #!/usr/bin/env sh
2
- . "$(dirname -- "$0")/_/husky.sh"
3
-
4
- npx commitlint --edit $1
@@ -1,4 +0,0 @@
1
- #!/usr/bin/env sh
2
- . "$(dirname -- "$0")/_/husky.sh"
3
-
4
- git update-index --again
package/.husky/pre-commit DELETED
@@ -1 +0,0 @@
1
- npm run lint; npm run dep:check; npm run test:unit