account-lookup-service 17.12.10 → 17.13.0-snapshot.10

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/src/constants.js CHANGED
@@ -29,7 +29,7 @@ const { API_TYPES } = require('@mojaloop/central-services-shared').Util.Hapi
29
29
 
30
30
  const ERROR_MESSAGES = Object.freeze({
31
31
  emptyFilteredPartyList: 'Empty oracle partyList, filtered based on callbackEndpointType',
32
- externalPartyError: 'External party error', // todo: think better message
32
+ externalPartyError: 'External party resolution error',
33
33
  failedToCacheSendToProxiesList: 'Failed to cache sendToProxiesList',
34
34
  noDiscoveryRequestsForwarded: 'No discovery requests forwarded to participants',
35
35
  sourceFspNotFound: 'Requester FSP not found',
@@ -28,8 +28,8 @@
28
28
  const { Util } = require('@mojaloop/central-services-shared')
29
29
  const { logger } = require('../../lib')
30
30
  const config = require('../../lib/config')
31
- const oracle = require('../../models/oracle/facade')
32
- const participant = require('../../models/participantEndpoint/facade')
31
+ const oracleFacade = require('../../models/oracle/facade')
32
+ const participantFacade = require('../../models/participantEndpoint/facade')
33
33
  const partiesUtils = require('./partiesUtils')
34
34
 
35
35
  /** @returns {PartiesDeps} */
@@ -37,6 +37,8 @@ const createDeps = ({
37
37
  cache,
38
38
  proxyCache,
39
39
  proxies = Util.proxies,
40
+ participant = participantFacade,
41
+ oracle = oracleFacade,
40
42
  childSpan = null,
41
43
  log = logger
42
44
  }) => Object.freeze({
@@ -44,10 +46,10 @@ const createDeps = ({
44
46
  proxyCache,
45
47
  childSpan,
46
48
  log,
47
- config,
48
49
  oracle,
49
- participant,
50
50
  proxies,
51
+ participant,
52
+ config,
51
53
  partiesUtils
52
54
  })
53
55
 
@@ -47,7 +47,7 @@ const services = require('./services')
47
47
  * @param {IProxyCache} [proxyCache] - IProxyCache instance
48
48
  */
49
49
  const putPartiesByTypeAndID = async (headers, params, method, payload, dataUri, cache, proxyCache = undefined) => {
50
- // todo: think, if we need to pass span here
50
+ // think, if we need to pass span here
51
51
  const component = putPartiesByTypeAndID.name
52
52
  const histTimerEnd = Metrics.getHistogram(
53
53
  component,
@@ -26,12 +26,10 @@
26
26
  ******/
27
27
 
28
28
  const ErrorHandler = require('@mojaloop/central-services-error-handling')
29
- const { Enum } = require('@mojaloop/central-services-shared')
30
- const { decodePayload } = require('@mojaloop/central-services-shared').Util.StreamingProtocol
29
+ const { Enum, Util } = require('@mojaloop/central-services-shared')
31
30
  const { initStepState } = require('../../../lib/util')
32
31
  const { createCallbackHeaders } = require('../../../lib/headers')
33
32
  const { ERROR_MESSAGES } = require('../../../constants')
34
- const { makeAcceptContentTypeHeader } = require('@mojaloop/central-services-shared').Util.Headers
35
33
 
36
34
  const { FspEndpointTypes, FspEndpointTemplates } = Enum.EndPoints
37
35
  const { Headers, RestMethods, HeaderResources } = Enum.Http
@@ -170,6 +168,26 @@ class BasePartiesService {
170
168
  this.log.info('sendErrorCallback is done', { sendTo, errorInfo })
171
169
  }
172
170
 
171
+ /**
172
+ * @returns {Promise<{ fspId: string, partySubIdOrType?: string }[]>} List of parties from oracle response
173
+ */
174
+ async sendOracleDiscoveryRequest () {
175
+ this.stepInProgress('#sendOracleDiscoveryRequest')
176
+ const { headers, params, query } = this.inputs
177
+
178
+ const response = await this.deps.oracle.oracleRequest(headers, RestMethods.GET, params, query, undefined, this.deps.cache)
179
+ this.log.verbose('oracle discovery raw response:', { response })
180
+
181
+ let { partyList } = response?.data || {}
182
+ if (!Array.isArray(partyList)) {
183
+ this.log.warn('invalid oracle discovery response:', { response })
184
+ // todo: maybe, it's better to throw an error
185
+ partyList = []
186
+ }
187
+
188
+ return partyList
189
+ }
190
+
173
191
  async sendDeleteOracleRequest (headers, params) {
174
192
  this.stepInProgress('sendDeleteOracleRequest')
175
193
  const result = await this.deps.oracle.oracleRequest(headers, RestMethods.DELETE, params, null, null, this.deps.cache)
@@ -184,6 +202,24 @@ class BasePartiesService {
184
202
  return isRemoved
185
203
  }
186
204
 
205
+ async sendPartyResolutionErrorCallback () {
206
+ this.stepInProgress('sendPartyResolutionErrorCallback')
207
+ const { headers, params } = this.inputs
208
+
209
+ const error = this.createFspiopPartyResolutionError(ERROR_MESSAGES.externalPartyError)
210
+ const callbackHeaders = BasePartiesService.createErrorCallbackHeaders(headers, params)
211
+ const errorInfo = await this.deps.partiesUtils.makePutPartiesErrorPayload(this.deps.config, error, callbackHeaders, params)
212
+ this.state.destination = callbackHeaders[Headers.FSPIOP.DESTINATION]
213
+
214
+ await this.identifyDestinationForCallback()
215
+ await this.sendErrorCallback({
216
+ errorInfo,
217
+ headers: callbackHeaders,
218
+ params
219
+ })
220
+ this.log.verbose('sendPartyResolutionErrorCallback is done', { callbackHeaders, errorInfo })
221
+ }
222
+
187
223
  createFspiopIdNotFoundError (errMessage, log = this.log) {
188
224
  log.warn(errMessage)
189
225
  return ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.ID_NOT_FOUND, errMessage)
@@ -194,9 +230,9 @@ class BasePartiesService {
194
230
  return ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.PARTY_NOT_FOUND, errMessage)
195
231
  }
196
232
 
197
- createFspiopServiceUnavailableError (errMessage, log = this.log) {
233
+ createFspiopPartyResolutionError (errMessage, log = this.log) {
198
234
  log.warn(errMessage)
199
- return ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.SERVICE_CURRENTLY_UNAVAILABLE, errMessage)
235
+ return ErrorHandler.Factory.createFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.PARTY_RESOLUTION_FAILURE, errMessage)
200
236
  }
201
237
 
202
238
  stepInProgress (stepName) {
@@ -222,7 +258,7 @@ class BasePartiesService {
222
258
  }
223
259
 
224
260
  static decodeDataUriPayload (dataUri) {
225
- const decoded = decodePayload(dataUri, { asParsed: false })
261
+ const decoded = Util.StreamingProtocol.decodePayload(dataUri, { asParsed: false })
226
262
  return decoded.body.toString()
227
263
  }
228
264
 
@@ -257,7 +293,7 @@ class BasePartiesService {
257
293
  return {
258
294
  [Headers.FSPIOP.SOURCE]: hubName,
259
295
  [Headers.FSPIOP.DESTINATION]: destination,
260
- [Headers.GENERAL.CONTENT_TYPE.value]: makeAcceptContentTypeHeader(
296
+ [Headers.GENERAL.CONTENT_TYPE.value]: Util.Headers.makeAcceptContentTypeHeader(
261
297
  HeaderResources.PARTIES,
262
298
  config.PROTOCOL_VERSIONS.CONTENT.DEFAULT.toString(),
263
299
  config.API_TYPE
@@ -28,7 +28,7 @@
28
28
  const { ERROR_MESSAGES } = require('../../../constants')
29
29
  const BasePartiesService = require('./BasePartiesService')
30
30
 
31
- const { FspEndpointTypes, RestMethods } = BasePartiesService.enums()
31
+ const { RestMethods } = BasePartiesService.enums()
32
32
  const proxyCacheTtlSec = 40 // todo: make configurable
33
33
 
34
34
  class GetPartiesService extends BasePartiesService {
@@ -45,8 +45,8 @@ class GetPartiesService extends BasePartiesService {
45
45
  return
46
46
  }
47
47
 
48
- const response = await this.sendOracleDiscoveryRequest()
49
- const isSent = await this.processOraclePartyListResponse(response)
48
+ const partyList = await this.sendOracleDiscoveryRequest()
49
+ const isSent = await this.processOraclePartyListResponse(partyList)
50
50
  this.log.info(`getParties request is ${isSent ? '' : 'NOT '}forwarded to oracle lookup DFSP`)
51
51
  if (isSent) return
52
52
 
@@ -63,7 +63,7 @@ class GetPartiesService extends BasePartiesService {
63
63
 
64
64
  const schemeSource = await this.validateParticipant(source)
65
65
  if (schemeSource) {
66
- log.debug('source participant is in scheme')
66
+ log.verbose('source participant is in scheme')
67
67
  return source
68
68
  }
69
69
 
@@ -91,9 +91,9 @@ class GetPartiesService extends BasePartiesService {
91
91
  const log = this.log.child({ method: 'forwardRequestToDestination' })
92
92
  let sendTo = destination
93
93
 
94
- const schemeParticipant = await this.validateParticipant(destination)
95
- if (!schemeParticipant) {
96
- this.stepInProgress('lookupProxyDestination-2')
94
+ const localParticipant = await this.validateParticipant(destination)
95
+ if (!localParticipant) {
96
+ this.stepInProgress('lookupProxyDestination')
97
97
  const proxyId = this.state.proxyEnabled && await this.deps.proxyCache.lookupProxyByDfspId(destination)
98
98
 
99
99
  if (!proxyId) {
@@ -103,26 +103,28 @@ class GetPartiesService extends BasePartiesService {
103
103
  return
104
104
  }
105
105
  sendTo = proxyId
106
+ } else {
107
+ // OSS-4203: Oracle validation for external source + local destination
108
+ const isValid = await this.#validateLocalDestinationForExternalSource()
109
+ if (!isValid) {
110
+ log.warn('incorrect destination from external source', { destination })
111
+ await this.sendPartyResolutionErrorCallback()
112
+ return
113
+ }
106
114
  }
107
115
 
108
116
  await this.#forwardGetPartiesRequest({ sendTo, headers, params })
109
117
  log.info('discovery getPartiesByTypeAndID request was sent', { sendTo })
110
118
  }
111
119
 
112
- async sendOracleDiscoveryRequest () {
113
- this.stepInProgress('#sendOracleDiscoveryRequest')
114
- const { headers, params, query } = this.inputs
115
- return this.deps.oracle.oracleRequest(headers, RestMethods.GET, params, query, undefined, this.deps.cache)
116
- }
117
-
118
- async processOraclePartyListResponse (response) {
119
- if (!Array.isArray(response?.data?.partyList) || response.data.partyList.length === 0) {
120
+ async processOraclePartyListResponse (rawPartyList) {
121
+ if (rawPartyList.length === 0) {
120
122
  this.log.verbose('oracle partyList is empty')
121
123
  return false
122
124
  }
123
125
 
124
126
  this.stepInProgress('processOraclePartyList')
125
- const partyList = this.#filterOraclePartyList(response)
127
+ const partyList = this.#filterOraclePartyList(rawPartyList)
126
128
 
127
129
  let sentCount = 0
128
130
  await Promise.all(partyList.map(async party => {
@@ -166,28 +168,20 @@ class GetPartiesService extends BasePartiesService {
166
168
  return isLocal
167
169
  }
168
170
 
169
- #filterOraclePartyList (response) {
171
+ #filterOraclePartyList (partyList) {
170
172
  // Oracle's API is a standard rest-style end-point Thus a GET /party on the oracle will return all participant-party records.
171
173
  // We must filter the results based on the callbackEndpointType to make sure we remove records containing partySubIdOrType when we are in FSPIOP_CALLBACK_URL_PARTIES_GET mode:
172
- this.stepInProgress('filterOraclePartyList')
174
+ this.stepInProgress('#filterOraclePartyList')
173
175
  const { params } = this.inputs
174
- const callbackEndpointType = this.deps.partiesUtils.getPartyCbType(params.SubId)
175
- let filteredPartyList
176
-
177
- switch (callbackEndpointType) {
178
- case FspEndpointTypes.FSPIOP_CALLBACK_URL_PARTIES_GET:
179
- filteredPartyList = response.data.partyList.filter(party => party.partySubIdOrType == null) // Filter records that DON'T contain a partySubIdOrType
180
- break
181
- case FspEndpointTypes.FSPIOP_CALLBACK_URL_PARTIES_SUB_ID_GET:
182
- filteredPartyList = response.data.partyList.filter(party => party.partySubIdOrType === params.SubId) // Filter records that match partySubIdOrType
183
- break
184
- default:
185
- filteredPartyList = response // Fallback to providing the standard list
186
- }
187
176
 
188
- if (!Array.isArray(filteredPartyList) || !filteredPartyList.length) {
177
+ const filteredPartyList = !params?.SubId
178
+ ? partyList.filter(party => party.partySubIdOrType == null) // Filter records that DON'T contain a partySubIdOrType
179
+ : partyList.filter(party => party.partySubIdOrType === params.SubId) // Filter records that match partySubIdOrType
180
+
181
+ if (!filteredPartyList.length) {
189
182
  throw super.createFspiopIdNotFoundError(ERROR_MESSAGES.emptyFilteredPartyList)
190
183
  }
184
+ this.log.verbose('#filterOraclePartyList is done:', { filteredPartyList })
191
185
 
192
186
  return filteredPartyList
193
187
  }
@@ -321,6 +315,25 @@ class GetPartiesService extends BasePartiesService {
321
315
  return isSet
322
316
  }
323
317
  }
318
+
319
+ async #validateLocalDestinationForExternalSource () {
320
+ // this method is called ONLY for local destination
321
+ const { state, log } = this
322
+
323
+ const needValidation = state.requester !== state.source
324
+ log.verbose('needOracleValidation: ', { needValidation })
325
+ if (!needValidation) return true
326
+
327
+ const partyList = await this.sendOracleDiscoveryRequest()
328
+ if (partyList.length === 0) {
329
+ log.warn('Oracle returned empty party list')
330
+ return false
331
+ }
332
+
333
+ const isValid = partyList.some(party => party.fspId === state.destination)
334
+ log.verbose('#validateLocalDestinationForExternalSource is done', { isValid })
335
+ return isValid
336
+ }
324
337
  }
325
338
 
326
339
  module.exports = GetPartiesService
@@ -26,13 +26,13 @@
26
26
  ******/
27
27
 
28
28
  const BasePartiesService = require('./BasePartiesService')
29
- const { ERROR_MESSAGES } = require('../../../constants')
30
29
 
31
30
  class PutPartiesErrorService extends BasePartiesService {
32
31
  async handleRequest () {
33
32
  if (this.state.proxyEnabled && this.state.proxy) {
34
- const alsReq = this.deps.partiesUtils.alsRequestDto(this.state.destination, this.inputs.params) // or source?
33
+ const alsReq = this.deps.partiesUtils.alsRequestDto(this.state.destination, this.inputs.params)
35
34
  const isInterSchemeDiscoveryCase = await this.deps.proxyCache.isPendingCallback(alsReq)
35
+ this.log.verbose(`isInterSchemeDiscoveryCase: ${isInterSchemeDiscoveryCase}`, this.state)
36
36
 
37
37
  if (isInterSchemeDiscoveryCase) {
38
38
  const isLast = await this.checkLastProxyCallback(alsReq)
@@ -41,13 +41,11 @@ class PutPartiesErrorService extends BasePartiesService {
41
41
  return
42
42
  }
43
43
  } else {
44
- const schemeParticipant = await this.validateParticipant(this.state.destination)
45
- if (!schemeParticipant) {
46
- this.log.info('Need to cleanup oracle and forward SERVICE_CURRENTLY_UNAVAILABLE error')
44
+ const isExternal = await this.#isPartyFromExternalDfsp()
45
+ if (isExternal) {
46
+ this.log.info('need to cleanup oracle coz party is from external DFSP')
47
47
  await this.cleanupOracle()
48
- await this.removeProxyGetPartiesTimeoutCache(alsReq)
49
- await this.forwardServiceUnavailableErrorCallback()
50
- return
48
+ await this.removeProxyGetPartiesTimeoutCache(alsReq) // think if we need this
51
49
  }
52
50
  }
53
51
  }
@@ -79,20 +77,18 @@ class PutPartiesErrorService extends BasePartiesService {
79
77
  return super.sendErrorCallback({ errorInfo, headers, params })
80
78
  }
81
79
 
82
- async forwardServiceUnavailableErrorCallback () {
83
- this.stepInProgress('forwardServiceUnavailableErrorCallback')
84
- const { headers, params } = this.inputs
85
- const error = super.createFspiopServiceUnavailableError(ERROR_MESSAGES.externalPartyError)
86
- const callbackHeaders = BasePartiesService.createErrorCallbackHeaders(headers, params, this.state.destination)
87
- const errorInfo = await this.deps.partiesUtils.makePutPartiesErrorPayload(this.deps.config, error, callbackHeaders, params)
80
+ async #isPartyFromExternalDfsp () {
81
+ this.stepInProgress('#isPartyFromExternalDfsp')
82
+ const partyList = await super.sendOracleDiscoveryRequest()
83
+ if (!partyList.length) {
84
+ this.log.verbose('oracle returns empty partyList')
85
+ return false
86
+ }
87
+ // think, if we have several parties from oracle
88
+ const isExternal = !(await this.validateParticipant(partyList[0].fspId))
89
+ this.log.verbose('#isPartyFromExternalDfsp is done:', { isExternal, partyList })
88
90
 
89
- await this.identifyDestinationForCallback()
90
- await super.sendErrorCallback({
91
- errorInfo,
92
- headers: callbackHeaders,
93
- params
94
- })
95
- this.log.verbose('#forwardServiceUnavailableErrorCallback is done', { callbackHeaders, errorInfo })
91
+ return isExternal
96
92
  }
97
93
  }
98
94
 
@@ -104,7 +104,7 @@ class PutPartiesService extends BasePartiesService {
104
104
  fspId: source
105
105
  }
106
106
  await this.deps.oracle.oracleRequest(headers, RestMethods.POST, params, null, mappingPayload, this.deps.cache)
107
- this.log.info('oracle was updated with mappingPayload', { mappingPayload })
107
+ this.log.info('oracle was updated with mappingPayload: ', mappingPayload)
108
108
  }
109
109
  }
110
110
 
@@ -41,6 +41,7 @@ const Config = require('../lib/config')
41
41
  */
42
42
  exports.createCallbackHeaders = (params) => {
43
43
  const callbackHeaders = { ...params.requestHeaders }
44
+ delete callbackHeaders[Enums.Http.Headers.FSPIOP.PROXY]
44
45
 
45
46
  callbackHeaders[Enums.Http.Headers.FSPIOP.SOURCE] = Config.HUB_NAME
46
47
  callbackHeaders[Enums.Http.Headers.FSPIOP.DESTINATION] = params.requestHeaders[Enums.Http.Headers.FSPIOP.SOURCE]
@@ -947,6 +947,7 @@ describe('Parties Tests', () => {
947
947
  Config.PROXY_CACHE_CONFIG.enabled = true
948
948
  const errorCode = MojaloopApiErrorCodes.PAYEE_IDENTIFIER_NOT_VALID.code
949
949
  const payload = fixtures.errorCallbackResponseDto({ errorCode })
950
+ const dataUri = encodePayload(JSON.stringify(payload), 'application/json')
950
951
  const destination = `dest-${Date.now()}`
951
952
  const proxy = `proxy-${Date.now()}`
952
953
  const headers = fixtures.partiesCallHeadersDto({ destination, proxy })
@@ -958,11 +959,13 @@ describe('Parties Tests', () => {
958
959
  oracleEndpointCached.getOracleEndpointByType = sandbox.stub().resolves([
959
960
  { value: 'http://oracle.endpoint' }
960
961
  ])
961
- oracle.oracleRequest = sandbox.stub().resolves()
962
+ oracle.oracleRequest = sandbox.stub().resolves({
963
+ data: { partyList: [{ fspId: 'fspId' }] }
964
+ })
962
965
 
963
- await partiesDomain.putPartiesErrorByTypeAndID(headers, params, payload, '', null, null, proxyCache)
966
+ await partiesDomain.putPartiesErrorByTypeAndID(headers, params, payload, dataUri, null, null, proxyCache)
964
967
 
965
- expect(oracle.oracleRequest.callCount).toBe(1)
968
+ expect(oracle.oracleRequest.callCount).toBe(2)
966
969
  expect(oracle.oracleRequest.lastCall.args[1]).toBe(RestMethods.DELETE)
967
970
  expect(participant.sendRequest.callCount).toBe(0)
968
971
  expect(participant.sendErrorToParticipant.callCount).toBe(1)
@@ -970,7 +973,7 @@ describe('Parties Tests', () => {
970
973
  const [sentTo, _, data, cbHeaders] = participant.sendErrorToParticipant.lastCall.args
971
974
  expect(sentTo).toBe(proxy)
972
975
  expect(cbHeaders[Headers.FSPIOP.DESTINATION]).toBe(destination)
973
- expect(data.errorInformation.errorCode).toBe('2003')
976
+ expect(JSON.parse(data).errorInformation.errorCode).toBe(errorCode)
974
977
  })
975
978
  })
976
979
  })
@@ -28,10 +28,12 @@
28
28
  const { setTimeout: sleep } = require('node:timers/promises')
29
29
  const {
30
30
  createMockDeps,
31
+ createOracleFacadeMock,
32
+ createParticipantFacadeMock,
31
33
  createProxyCacheMock,
32
34
  createProxiesUtilMock,
33
- oracleMock,
34
- participantMock
35
+ oracleMock, // deprecated! use createOracleFacadeMock instead
36
+ participantMock // deprecated! use createParticipantFacadeMock instead
35
37
  } = require('./deps')
36
38
  // ↑ should be first require to mock external deps ↑
37
39
  const { GetPartiesService } = require('#src/domain/parties/services/index')
@@ -42,11 +44,16 @@ const { RestMethods, Headers } = GetPartiesService.enums()
42
44
 
43
45
  describe('GetPartiesService Tests -->', () => {
44
46
  const { config } = createMockDeps()
47
+ const { API_TYPE } = config
45
48
 
46
49
  beforeEach(() => {
47
50
  jest.clearAllMocks()
48
51
  })
49
52
 
53
+ afterEach(() => {
54
+ config.API_TYPE = API_TYPE // to avoid side effects
55
+ })
56
+
50
57
  describe('forwardRequestToDestination method', () => {
51
58
  test('should delete party info from oracle, if no destination DFSP in proxy mapping', async () => {
52
59
  participantMock.validateParticipant = jest.fn().mockResolvedValueOnce(null)
@@ -234,12 +241,14 @@ describe('GetPartiesService Tests -->', () => {
234
241
  })
235
242
 
236
243
  test('should send error callback in ISO format if proxyRequest failed after delay, and other proxies have already replied', async () => {
244
+ participantMock.validateParticipant = jest.fn().mockResolvedValue({})
237
245
  const service = prepareGetPartiesServiceForDelayedProxyError()
238
246
  const { headers } = service.inputs
239
247
  service.deps.config.API_TYPE = API_TYPES.iso20022
240
248
 
241
249
  await service.triggerInterSchemeDiscoveryFlow(headers)
242
250
  .catch(err => service.handleError(err))
251
+
243
252
  expect(participantMock.sendErrorToParticipant).toHaveBeenCalledTimes(1)
244
253
  expect(participantMock.sendErrorToParticipant.mock.lastCall[2].Rpt.Rsn.Cd).toBe('3200')
245
254
  })
@@ -325,12 +334,181 @@ describe('GetPartiesService Tests -->', () => {
325
334
  }
326
335
  const headers = fixtures.partiesCallHeadersDto({ destination: '' })
327
336
  const params = fixtures.partiesParamsDto()
328
- const service = new GetPartiesService(deps, { headers, params })
329
337
 
338
+ const service = new GetPartiesService(deps, { headers, params })
330
339
  await service.handleRequest()
340
+
331
341
  expect(participantMock.sendErrorToParticipant).toHaveBeenCalledTimes(1)
332
342
  const isoPayload = participantMock.sendErrorToParticipant.mock.lastCall[2]
333
343
  expect(isoPayload.Assgnmt).toBeDefined()
334
344
  expect(isoPayload.Rpt).toBeDefined()
335
345
  })
346
+
347
+ describe('OSS-4203: oracle validation for external source + local destination', () => {
348
+ const EXTERNAL_SOURCE_DFSP = 'externalSourceDfsp'
349
+ const LOCAL_DESTINATION_DFSP = 'localDestinationDfsp'
350
+ const ORACLE_DFSP_DIFFERENT = 'differentDfsp'
351
+ const PROXY_ID = 'proxyForExternal'
352
+
353
+ let deps
354
+ let oracle // facade
355
+ let participant // facade
356
+ let proxyCache
357
+ let headers
358
+ let params
359
+
360
+ beforeEach(() => {
361
+ oracle = createOracleFacadeMock()
362
+ participant = createParticipantFacadeMock()
363
+ proxyCache = createProxyCacheMock({
364
+ addDfspIdToProxyMapping: jest.fn().mockResolvedValueOnce(true)
365
+ })
366
+ deps = createMockDeps({ oracle, participant, proxyCache })
367
+ headers = fixtures.partiesCallHeadersDto({
368
+ source: EXTERNAL_SOURCE_DFSP,
369
+ destination: LOCAL_DESTINATION_DFSP,
370
+ proxy: PROXY_ID
371
+ })
372
+ params = fixtures.partiesParamsDto()
373
+ })
374
+
375
+ test('should forward request when oracle DFSP matches destination DFSP', async () => {
376
+ participant.validateParticipant = jest.fn()
377
+ .mockResolvedValueOnce(null) // external source (validateRequester)
378
+ .mockResolvedValueOnce({}) // proxy exists (validateRequester)
379
+ .mockResolvedValueOnce({}) // local destination (forwardRequestToDestination)
380
+ .mockResolvedValueOnce(null) // external source (shouldValidateViaOracle)
381
+ .mockResolvedValueOnce({}) // local destination (shouldValidateViaOracle)
382
+ oracle.oracleRequest = jest.fn().mockResolvedValueOnce(
383
+ fixtures.oracleRequestResponseDto({
384
+ partyList: [{ fspId: LOCAL_DESTINATION_DFSP }]
385
+ })
386
+ )
387
+
388
+ const service = new GetPartiesService(deps, { headers, params })
389
+ await service.handleRequest()
390
+
391
+ expect(oracle.oracleRequest).toHaveBeenCalledWith(
392
+ headers,
393
+ RestMethods.GET,
394
+ params,
395
+ undefined,
396
+ undefined,
397
+ deps.cache
398
+ )
399
+ expect(participant.sendRequest).toHaveBeenCalledWith(
400
+ expect.objectContaining({
401
+ [Headers.FSPIOP.DESTINATION]: LOCAL_DESTINATION_DFSP
402
+ }),
403
+ LOCAL_DESTINATION_DFSP,
404
+ expect.any(String),
405
+ RestMethods.GET,
406
+ undefined,
407
+ expect.any(Object),
408
+ null
409
+ )
410
+ expect(participant.sendErrorToParticipant).not.toHaveBeenCalled()
411
+ })
412
+
413
+ test('should send error callback when oracle DFSP differs from destination DFSP', async () => {
414
+ participant.validateParticipant = jest.fn()
415
+ .mockResolvedValueOnce(null) // external source (validateRequester)
416
+ .mockResolvedValueOnce({}) // proxy exists (validateRequester)
417
+ .mockResolvedValueOnce({}) // local destination (forwardRequestToDestination)
418
+ .mockResolvedValueOnce(null) // external source (shouldValidateViaOracle)
419
+ .mockResolvedValueOnce({})
420
+ oracle.oracleRequest = jest.fn().mockResolvedValueOnce(
421
+ fixtures.oracleRequestResponseDto({
422
+ partyList: [{ fspId: ORACLE_DFSP_DIFFERENT }]
423
+ })
424
+ )
425
+ proxyCache.lookupProxyByDfspId = jest.fn().mockResolvedValueOnce(PROXY_ID)
426
+
427
+ const service = new GetPartiesService(deps, { headers, params })
428
+ await service.handleRequest()
429
+
430
+ expect(oracle.oracleRequest).toHaveBeenCalledTimes(1)
431
+ expect(participant.sendErrorToParticipant).toHaveBeenCalledTimes(1)
432
+ expect(participant.sendRequest).not.toHaveBeenCalled()
433
+
434
+ const [sentTo, , errorPayload] = participant.sendErrorToParticipant.mock.lastCall
435
+ expect(sentTo).toBe(PROXY_ID) // Error is sent to the proxy (requester)
436
+ expect(errorPayload.errorInformation.errorCode).toBeDefined()
437
+ })
438
+
439
+ test('should send error callback when oracle returns empty result', async () => {
440
+ participant.validateParticipant = jest.fn()
441
+ .mockResolvedValueOnce(null) // external source (validateRequester)
442
+ .mockResolvedValueOnce({}) // proxy exists (validateRequester)
443
+ .mockResolvedValueOnce(null) // external source (shouldValidateViaOracle)
444
+ .mockResolvedValueOnce({}) // local destination (shouldValidateViaOracle)
445
+ .mockResolvedValueOnce({}) // local destination (forwardRequestToDestination)
446
+
447
+ oracle.oracleRequest = jest.fn().mockResolvedValueOnce(
448
+ fixtures.oracleRequestResponseDto({ partyList: [] })
449
+ )
450
+
451
+ const service = new GetPartiesService(deps, { headers, params })
452
+ await service.handleRequest()
453
+
454
+ expect(oracle.oracleRequest).toHaveBeenCalledTimes(1)
455
+ expect(participant.sendErrorToParticipant).toHaveBeenCalledTimes(1)
456
+ expect(participant.sendRequest).not.toHaveBeenCalled()
457
+ })
458
+
459
+ test('should skip oracle validation when source is local (not external)', async () => {
460
+ const headers = fixtures.partiesCallHeadersDto()
461
+ participant.validateParticipant = jest.fn().mockResolvedValue({})
462
+
463
+ const service = new GetPartiesService(deps, { headers, params })
464
+ await service.handleRequest()
465
+
466
+ expect(oracle.oracleRequest).not.toHaveBeenCalled()
467
+ expect(participant.sendRequest).toHaveBeenCalledTimes(1)
468
+ })
469
+
470
+ test('should skip oracle validation when destination is external (not local)', async () => {
471
+ const externalDestHeaders = fixtures.partiesCallHeadersDto({
472
+ source: EXTERNAL_SOURCE_DFSP,
473
+ destination: 'externalDestinationDfsp',
474
+ proxy: PROXY_ID
475
+ })
476
+ participant.validateParticipant = jest.fn()
477
+ .mockResolvedValueOnce(null) // external source
478
+ .mockResolvedValueOnce({}) // proxy exists
479
+ .mockResolvedValueOnce(null) // external destination
480
+ proxyCache.lookupProxyByDfspId = jest.fn().mockResolvedValueOnce(PROXY_ID)
481
+
482
+ const service = new GetPartiesService(deps, { headers: externalDestHeaders, params })
483
+ await service.handleRequest()
484
+
485
+ expect(oracle.oracleRequest).not.toHaveBeenCalled()
486
+ expect(participant.sendRequest).toHaveBeenCalledTimes(1)
487
+ })
488
+
489
+ test('should send error callback in ISO20022 format when API_TYPE is iso20022', async () => {
490
+ const isoConfig = { ...deps.config, API_TYPE: API_TYPES.iso20022 }
491
+ const isoDeps = { ...deps, config: isoConfig }
492
+
493
+ participant.validateParticipant = jest.fn()
494
+ .mockResolvedValueOnce(null) // external source (validateRequester)
495
+ .mockResolvedValueOnce({}) // proxy exists (validateRequester)
496
+ .mockResolvedValueOnce(null) // external source (shouldValidateViaOracle)
497
+ .mockResolvedValueOnce({}) // local destination (shouldValidateViaOracle)
498
+ .mockResolvedValueOnce({}) // local destination (forwardRequestToDestination)
499
+ oracle.oracleRequest = jest.fn().mockResolvedValueOnce(
500
+ fixtures.oracleRequestResponseDto({
501
+ partyList: [{ fspId: ORACLE_DFSP_DIFFERENT }]
502
+ })
503
+ )
504
+
505
+ const service = new GetPartiesService(isoDeps, { headers, params })
506
+ await service.handleRequest()
507
+
508
+ expect(participant.sendErrorToParticipant).toHaveBeenCalledTimes(1)
509
+ const isoPayload = participant.sendErrorToParticipant.mock.lastCall[2]
510
+ expect(isoPayload.Assgnmt).toBeDefined()
511
+ expect(isoPayload.Rpt).toBeDefined()
512
+ })
513
+ })
336
514
  })