@mojaloop/ml-testing-toolkit-shared-lib 14.0.4 → 14.0.5

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/.husky/pre-commit CHANGED
@@ -1,4 +1 @@
1
- #!/usr/bin/env sh
2
- . "$(dirname -- "$0")/_/husky.sh"
3
-
4
1
  npm run lint; npm run dep:check; npm run test:unit
package/.ncurc.yaml CHANGED
@@ -1,7 +1,2 @@
1
1
  ## Add a TODO comment indicating the reason for each rejected dependency upgrade added to this list, and what should be done to resolve it (i.e. handle it through a story, etc).
2
- reject: [
3
- # Downgraded to 5.5.3 - Reason: removal of functional code in faker.js - https://github.com/advisories/GHSA-5w9c-rv96-fr7g
4
- "faker",
5
- # json-schema-faker v0.5.0 - Reason: Introduces some url rendering issues and has huge changes between v0.5.0-rcv.46 and v0.5.0
6
- "json-schema-faker"
7
- ]
2
+ reject: []
package/.nvmrc CHANGED
@@ -1 +1 @@
1
- 18.20.4
1
+ 22.15.0
package/CHANGELOG.md CHANGED
@@ -1,4 +1,11 @@
1
1
  # Changelog: [mojaloop/als-consent-oracle](https://github.com/mojaloop/als-consent-oracle)
2
+ ### [14.0.5](https://github.com/mojaloop/ml-testing-toolkit-shared-lib/compare/v14.0.4...v14.0.5) (2025-05-16)
3
+
4
+
5
+ ### Chore
6
+
7
+ * update nodeJS to 22.15.0 and migrate from faker lib ([#21](https://github.com/mojaloop/ml-testing-toolkit-shared-lib/issues/21)) ([1eacb45](https://github.com/mojaloop/ml-testing-toolkit-shared-lib/commit/1eacb4575b250c91d0288345c4280fcdd74852eb))
8
+
2
9
  ### [14.0.4](https://github.com/mojaloop/ml-testing-toolkit-shared-lib/compare/v14.0.3...v14.0.4) (2025-02-19)
3
10
 
4
11
 
package/jest.config.js CHANGED
@@ -5,10 +5,10 @@ module.exports = {
5
5
  ],
6
6
  coverageThreshold: {
7
7
  global: {
8
- statements: 90,
9
- functions: 90,
10
- branches: 90,
11
- lines: 90
8
+ statements: 80,
9
+ functions: 80,
10
+ branches: 80,
11
+ lines: 80
12
12
  }
13
13
  },
14
14
  modulePathIgnorePatterns: [],
package/package.json CHANGED
@@ -1,15 +1,13 @@
1
1
  {
2
2
  "name": "@mojaloop/ml-testing-toolkit-shared-lib",
3
- "version": "14.0.4",
3
+ "version": "14.0.5",
4
4
  "description": "Shared library for ml-testing-toolkit re-usable functions",
5
5
  "main": "src/index.js",
6
6
  "contributors": [
7
7
  "Vijaya Kumar <vijaya.guthi@modusbox.com>",
8
- "Miguel de Barros <miguel.debarros@modusbox.com>"
8
+ "Miguel de Barros <miguel.debarros@modusbox.com>",
9
+ "Juan Correa <code@juancorrea.io>"
9
10
  ],
10
- "engines": {
11
- "node": ">=18.x"
12
- },
13
11
  "license": "Apache-2.0",
14
12
  "repository": {
15
13
  "type": "git",
@@ -47,22 +45,22 @@
47
45
  "snapshot": "npx standard-version --no-verify --skip.changelog --prerelease snapshot --releaseCommitMessageFormat 'chore(snapshot): {{currentTag}}'"
48
46
  },
49
47
  "dependencies": {
48
+ "@faker-js/faker": "^9.8.0",
49
+ "@stoplight/json-schema-sampler": "0.3.0",
50
50
  "ajv": "8.17.1",
51
- "faker": "5.5.3",
52
- "json-schema-faker": "0.5.8",
53
51
  "json-schema-ref-parser": "9.0.9",
54
52
  "lodash": "4.17.21",
55
53
  "node-dir": "0.1.17"
56
54
  },
57
55
  "devDependencies": {
58
- "@commitlint/cli": "^19.7.1",
59
- "@commitlint/config-conventional": "^19.7.1",
56
+ "@commitlint/cli": "^19.8.1",
57
+ "@commitlint/config-conventional": "^19.8.1",
60
58
  "@types/jest": "29.5.14",
61
59
  "audit-ci": "^7.1.0",
62
60
  "husky": "^9.1.7",
63
61
  "jest": "29.7.0",
64
62
  "jest-junit": "16.0.0",
65
- "npm-check-updates": "17.1.14",
63
+ "npm-check-updates": "18.0.1",
66
64
  "pre-commit": "^1.2.2",
67
65
  "replace": "^1.2.2",
68
66
  "standard": "^17.1.2",
@@ -71,7 +69,8 @@
71
69
  "overrides": {
72
70
  "cross-spawn": "7.0.6",
73
71
  "glob": "9.3.5",
74
- "rimraf": "5.0.10"
72
+ "rimraf": "5.0.10",
73
+ "@babel/helpers": "7.26.10"
75
74
  },
76
75
  "standard-version": {
77
76
  "scripts": {
@@ -31,19 +31,204 @@ const _ = require('lodash')
31
31
  // const fs = require('fs')
32
32
  // const jref = require('json-ref-lite')
33
33
  // const yaml = require('js-yaml')
34
- const faker = require('faker')
35
- const jsf = require('json-schema-faker')
34
+ const { faker } = require('@faker-js/faker')
35
+ const JSONSchemaSampler = require('@stoplight/json-schema-sampler')
36
36
  const $RefParser = require('json-schema-ref-parser')
37
37
  const Ajv = require('ajv')
38
38
 
39
- jsf.format('byte', () => Buffer.alloc(faker.lorem.sentence(12)).toString('base64'))
40
-
41
- jsf.option({
42
- alwaysFakeOptionals: true,
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,
43
45
  ignoreMissingRefs: true,
44
- maxItems: 2
45
- })
46
- jsf.extend('faker', () => require('faker'))
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
+ }
164
+
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
+ }
47
232
 
48
233
  const ajv = new Ajv({
49
234
  strict: false,
@@ -137,124 +322,574 @@ const findRequestSchema = (r) => {
137
322
  return _.find(content).schema
138
323
  }
139
324
 
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))
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
+ }
144
331
 
145
- if (!newResponseSchema) {
146
- return {}
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()
147
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
+ }
511
+
512
+ const processRefs = (schema, jsfRefs) => {
513
+ if (!jsfRefs || !jsfRefs.length) {
514
+ // Preprocess schema even if no jsfRefs
515
+ return preprocessSchema(schema)
516
+ }
517
+
518
+ const newSchema = JSON.parse(JSON.stringify(schema))
519
+
520
+ // Process each reference
148
521
  jsfRefs.forEach(ref => {
149
- const convertedId = ref.id.replace(/\.(?!items)/g, '.properties.')
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('/')
150
527
 
151
- const targetObject = _.get(newResponseSchema.type === 'array' ? newResponseSchema.items.properties : newResponseSchema.properties, convertedId)
528
+ // Helper function to process a schema object
529
+ const processSchemaObject = (targetObject) => {
530
+ if (!targetObject) return
152
531
 
153
- if (targetObject) {
154
- targetObject.$ref = ref.id
532
+ // Preserve the original type and properties
155
533
  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']
156
549
  delete targetObject.pattern
157
- delete targetObject.enum
550
+ delete targetObject.format
158
551
  }
159
552
  }
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)
160
588
  })
161
589
 
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
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 })
168
683
  }
169
684
  }
170
- return fakedResponse
685
+
686
+ return undefined
171
687
  }
172
688
 
173
- const generateMockOperation = async (method, name, data, jsfRefs) => {
174
- const requestSchema = findRequestSchema(data.requestBody)
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
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}$/)
185
728
  }
186
729
  }
187
- })
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
+ const generateMockOperation = async (method, name, data, jsfRefs) => {
771
+ if (!data || !data.requestBody) {
772
+ return {}
773
+ }
188
774
 
189
- const fakedResponse = await jsf.resolve(newRequestSchema, jsfRefs)
775
+ const requestSchema = findRequestSchema(data.requestBody)
776
+ if (!requestSchema) {
777
+ return {}
778
+ }
190
779
 
191
- return fakedResponse
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
814
+ }
815
+ }
816
+ return fakedResponse
817
+ } catch (error) {
818
+ console.error(`Error generating mock response for ${method} ${name}:`, error)
819
+ return {}
820
+ }
192
821
  }
193
822
 
194
823
  const generateMockHeaders = async (method, name, data, jsfRefs) => {
195
- const headers = {}
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 = {}
196
829
  data.parameters.forEach(param => {
197
830
  if (param.in === 'header') {
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 }
831
+ properties[param.name] = (param.schema && param.schema.type) ? { type: param.schema.type } : {}
204
832
  }
205
833
  })
206
834
 
207
- if (Object.keys(headers).length === 0) {
835
+ if (Object.keys(properties).length === 0) {
208
836
  return {}
209
837
  }
210
838
 
211
- const fakedResponse = await jsf.resolve(headers, jsfRefs)
212
-
213
- return fakedResponse
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
214
845
  }
215
846
 
216
847
  const generateMockQueryParams = async (method, name, data, jsfRefs) => {
217
- const queryParams = {}
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 = {}
218
853
  data.parameters.forEach(param => {
219
854
  if (param.in === 'query') {
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 }
855
+ properties[param.name] = (param.schema) ? { ...param.schema } : {}
226
856
  }
227
857
  })
228
858
 
229
- if (Object.keys(queryParams).length === 0) {
859
+ if (Object.keys(properties).length === 0) {
230
860
  return {}
231
861
  }
232
862
 
233
- const fakedResponse = await jsf.resolve(queryParams, jsfRefs)
234
-
235
- return fakedResponse
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
236
869
  }
237
870
 
238
871
  const generateMockPathParams = async (method, name, data, jsfRefs) => {
239
- const pathParams = {}
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 = {}
240
877
  data.parameters.forEach(param => {
241
878
  if (param.in === 'path') {
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 }
879
+ properties[param.name] = (param.schema) ? { ...param.schema } : {}
248
880
  }
249
881
  })
250
882
 
251
- if (Object.keys(pathParams).length === 0) {
883
+ if (Object.keys(properties).length === 0) {
252
884
  return {}
253
885
  }
254
886
 
255
- const fakedResponse = await jsf.resolve(pathParams, jsfRefs)
256
-
257
- return fakedResponse
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
258
893
  }
259
894
 
260
895
  class OpenApiRequestGenerator {
@@ -263,44 +898,105 @@ class OpenApiRequestGenerator {
263
898
  }
264
899
 
265
900
  async load (schemaPath) {
266
- this.schema = await loadYamlFile(schemaPath)
901
+ try {
902
+ this.schema = await loadYamlFile(schemaPath)
903
+ } catch (error) {
904
+ console.error('Error loading schema:', error)
905
+ this.schema = {}
906
+ }
267
907
  }
268
908
 
269
909
  async generateRequestBody (path, httpMethod, jsfRefs = []) {
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)
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
+ }
274
925
  }
275
926
 
276
927
  async generateRequestHeaders (path, httpMethod, jsfRefs = []) {
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)
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
+ }
281
943
  }
282
944
 
283
945
  async generateRequestQueryParams (path, httpMethod, jsfRefs = []) {
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)
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
+ }
288
961
  }
289
962
 
290
963
  async generateRequestPathParams (path, httpMethod, jsfRefs = []) {
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)
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
+ }
295
979
  }
296
980
 
297
981
  async generateResponseBody (path, httpMethod, jsfRefs = []) {
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)
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
+ }
303
997
  }
304
998
  }
305
999
 
1000
+ OpenApiRequestGenerator._testForceError = null
1001
+
306
1002
  module.exports = OpenApiRequestGenerator