From 0d8fc242d292313ac3cd6b621961e23555d22685 Mon Sep 17 00:00:00 2001 From: Kevin Leyow Date: Thu, 22 Aug 2024 09:58:21 -0500 Subject: [PATCH] chore: renable coverage and improve coverage (#350) * coverage * improve coverage * fix * lint dep --- .circleci/config.yml | 86 ++++---- jest.config.js | 3 + package-lock.json | 8 +- package.json | 2 +- src/model/fxQuotes.js | 8 +- test/unit/api/quotes.test.js | 28 +++ test/unit/api/quotes/{id}.test.js | 85 ++++++++ test/unit/handlers/QuotingHandler.test.js | 2 + test/unit/mocks.js | 30 ++- test/unit/model/bulkQuotes.test.js | 9 + test/unit/model/fxQuotes.test.js | 252 +++++++++++++++++++++- test/unit/model/index.test.js | 6 +- test/unit/model/rules.test.js | 20 ++ test/unit/serverStart.test.js | 247 ++++++++++++++++++++- 14 files changed, 724 insertions(+), 62 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 442728be..0f063ba8 100755 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -245,30 +245,30 @@ jobs: - store_test_results: path: ./test/results - # test-coverage: - # executor: default-docker - # environment: - # <<: *defaults_environment - # steps: - # - run: - # name: Install general dependencies - # command: *defaults_docker_Dependencies - # - run: - # name: Install AWS CLI dependencies - # command: *defaults_awsCliDependencies - # - checkout - # - run: - # <<: *defaults_configure_nvm - # - run: - # <<: *defaults_display_versions - # - restore_cache: - # key: dependency-cache-{{ .Environment.CIRCLE_SHA1 }} - # - run: - # name: Execute code coverage check - # command: npm -s run test:coverage-check - # - store_artifacts: - # path: coverage - # destination: test + test-coverage: + executor: default-docker + environment: + <<: *defaults_environment + steps: + - run: + name: Install general dependencies + command: *defaults_docker_Dependencies + - run: + name: Install AWS CLI dependencies + command: *defaults_awsCliDependencies + - checkout + - run: + <<: *defaults_configure_nvm + - run: + <<: *defaults_display_versions + - restore_cache: + key: dependency-cache-{{ .Environment.CIRCLE_SHA1 }} + - run: + name: Execute code coverage check + command: npm -s run test:coverage-check + - store_artifacts: + path: coverage + destination: test test-integration: executor: default-machine @@ -711,18 +711,18 @@ workflows: ignore: - /feature*/ - /bugfix*/ - # - test-coverage: - # context: org-global - # requires: - # - setup - # filters: - # tags: - # ignore: - # - /v[0-9]+(\.[0-9]+)*\-snapshot+((\.[0-9]+)?)/ - # branches: - # ignore: - # - /feature*/ - # - /bugfix*/ + - test-coverage: + context: org-global + requires: + - setup + filters: + tags: + ignore: + - /v[0-9]+(\.[0-9]+)*\-snapshot+((\.[0-9]+)?)/ + branches: + ignore: + - /feature*/ + - /bugfix*/ - test-integration: context: org-global requires: @@ -811,8 +811,8 @@ workflows: - pr-tools/pr-title-check - test-lint - test-unit - # - test-coverage - # - test-integration + - test-coverage + - test-integration # - test-functional - vulnerability-check - audit-licenses @@ -838,12 +838,11 @@ workflows: - pr-tools/pr-title-check - test-lint - test-unit - # - test-coverage - # - test-integration + - test-coverage + - test-integration # - test-functional - vulnerability-check - audit-licenses - # - test-integration - license-scan - image-scan filters: @@ -858,12 +857,11 @@ workflows: - pr-tools/pr-title-check - test-lint - test-unit - # - test-coverage - # - test-integration + - test-coverage + - test-integration # - test-functional - vulnerability-check - audit-licenses - # - test-integration - license-scan - image-scan filters: diff --git a/jest.config.js b/jest.config.js index 924d0a63..9989b940 100644 --- a/jest.config.js +++ b/jest.config.js @@ -5,6 +5,9 @@ module.exports = { collectCoverageFrom: [ '**/src/**/**/*.js' ], + coveragePathIgnorePatterns: [ + './src/handlers/index.js' + ], coverageThreshold: { global: { statements: 90, diff --git a/package-lock.json b/package-lock.json index 03711b47..380a71ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -49,7 +49,7 @@ "eslint-plugin-jest": "28.8.0", "jest": "29.7.0", "jest-junit": "16.0.0", - "npm-check-updates": "17.0.6", + "npm-check-updates": "17.1.0", "nyc": "17.0.0", "pre-commit": "1.2.2", "proxyquire": "2.1.3", @@ -9851,9 +9851,9 @@ } }, "node_modules/npm-check-updates": { - "version": "17.0.6", - "resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-17.0.6.tgz", - "integrity": "sha512-KCiaJH1cfnh/RyzKiDNjNfXgcKFyQs550Uf1OF/Yzb8xO56w+RLpP/OKRUx23/GyP/mLYwEpOO65qjmVdh6j0A==", + "version": "17.1.0", + "resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-17.1.0.tgz", + "integrity": "sha512-RcohCA/tdpxyPllBlYDkqGXFJQgTuEt0f2oPSL9s05pZ3hxYdleaUtvEcSxKl0XAg3ncBhVgLAxhXSjoryUU5Q==", "dev": true, "bin": { "ncu": "build/cli.js", diff --git a/package.json b/package.json index 11a5a95e..a0679856 100644 --- a/package.json +++ b/package.json @@ -141,7 +141,7 @@ "eslint-plugin-jest": "28.8.0", "jest": "29.7.0", "jest-junit": "16.0.0", - "npm-check-updates": "17.0.6", + "npm-check-updates": "17.1.0", "nyc": "17.0.0", "pre-commit": "1.2.2", "proxyquire": "2.1.3", diff --git a/src/model/fxQuotes.js b/src/model/fxQuotes.js index 84b80a69..47d1c196 100644 --- a/src/model/fxQuotes.js +++ b/src/model/fxQuotes.js @@ -301,9 +301,9 @@ class FxQuotesModel { let txn const fspiopSource = headers[ENUM.Http.Headers.FSPIOP.SOURCE] const childSpan = span.getChild('qs_fxQuote_forwardFxQuoteUpdate') + try { await childSpan.audit({ headers, params: { conversionRequestId }, payload: fxQuoteUpdateRequest }, EventSdk.AuditEventAction.start) - if ('accept' in headers) { throw ErrorHandler.CreateFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.VALIDATION_ERROR, `Update for fx quote ${conversionRequestId} failed: "accept" header should not be sent in callbacks.`, null, headers['fspiop-source']) @@ -582,7 +582,7 @@ class FxQuotesModel { const fspiopError = ErrorHandler.ReformatFSPIOPError(err) await this.handleException(fspiopSource, fxQuoteRequest.conversionRequestId, fspiopError, headers, childSpan) } finally { - if (!childSpan.isFinished) { + if (childSpan && !childSpan.isFinished) { await childSpan.finish() } } @@ -619,7 +619,7 @@ class FxQuotesModel { this.log.error(`Error forwarding fxQuote response: ${getStackOrInspect(err)}. Attempting to send error callback to ${fspiopSource}`) await this.handleException(fspiopSource, conversionRequestId, err, headers, childSpan) } finally { - if (!childSpan.isFinished) { + if (childSpan && !childSpan.isFinished) { await childSpan.finish() } } @@ -651,7 +651,7 @@ class FxQuotesModel { histTimer({ success: false, queryName: 'handleException' }) this.log.error('error in handleException, stop request processing!', err) } finally { - if (!childSpan.isFinished) { + if (childSpan && !childSpan.isFinished) { await childSpan.finish() } } diff --git a/test/unit/api/quotes.test.js b/test/unit/api/quotes.test.js index 7f3f8f28..a6657db1 100644 --- a/test/unit/api/quotes.test.js +++ b/test/unit/api/quotes.test.js @@ -69,6 +69,34 @@ describe('POST /quotes API Tests -->', () => { expect(producerConfig).toStrictEqual(config) }) + it('should publish an fxQuote request message', async () => { + // Arrange + Producer.produceMessage = jest.fn() + const conversionRequestId = randomUUID() + const mockRequest = mocks.mockHttpRequest({ + payload: { conversionRequestId }, + headers: { + 'content-type': 'application/vnd.interoperability.fxquotes+json;version=1.0' + } + }) + const { handler, code } = mocks.createMockHapiHandler() + + // Act + await quotesApi.post(mockContext, mockRequest, handler) + + // Assert + expect(code).toHaveBeenCalledWith(Http.ReturnCodes.ACCEPTED.CODE) + expect(Producer.produceMessage).toHaveBeenCalledTimes(1) + + const [message, topicConfig, producerConfig] = Producer.produceMessage.mock.calls[0] + const { id, type, action } = message.content + expect(id).toBe(conversionRequestId) + expect(type).toBe(Events.Event.Type.FX_QUOTE) + expect(action).toBe(Events.Event.Action.POST) + expect(topicConfig.topicName).toBe(kafkaConfig.PRODUCER.FX_QUOTE.POST.topic) + expect(producerConfig).toStrictEqual(kafkaConfig.PRODUCER.FX_QUOTE.POST.config) + }) + it('should rethrow and log error in case of error on publishing quote', async () => { // Arrange const error = new Error('Create Quote Test Error') diff --git a/test/unit/api/quotes/{id}.test.js b/test/unit/api/quotes/{id}.test.js index 744c5de7..2e057909 100644 --- a/test/unit/api/quotes/{id}.test.js +++ b/test/unit/api/quotes/{id}.test.js @@ -75,6 +75,34 @@ describe('/quotes/{id} API Tests -->', () => { expect(producerConfig).toStrictEqual(config) }) + it('should publish an fxQuote request message', async () => { + // Arrange + Producer.produceMessage = jest.fn() + const conversionRequestId = randomUUID() + const mockRequest = mocks.mockHttpRequest({ + params: { id: conversionRequestId }, + headers: { + 'content-type': 'application/vnd.interoperability.fxQuotes+json;version=1.0' + } + }) + const { handler, code } = mocks.createMockHapiHandler() + + // Act + await quotesApi.get(mockContext, mockRequest, handler) + + // Assert + expect(code).toHaveBeenCalledWith(Http.ReturnCodes.ACCEPTED.CODE) + expect(Producer.produceMessage).toHaveBeenCalledTimes(1) + + const [message, topicConfig, producerConfig] = Producer.produceMessage.mock.calls[0] + const { id, type, action } = message.content + expect(id).toBe(conversionRequestId) + expect(type).toBe(Events.Event.Type.FX_QUOTE) + expect(action).toBe(Events.Event.Action.GET) + expect(topicConfig.topicName).toBe(kafkaConfig.PRODUCER.FX_QUOTE.GET.topic) + expect(producerConfig).toStrictEqual(kafkaConfig.PRODUCER.FX_QUOTE.GET.config) + }) + it('should rethrow error in case of error during publish a message', async () => { // Arrange const error = new Error('Get Quote Test Error') @@ -122,6 +150,34 @@ describe('/quotes/{id} API Tests -->', () => { expect(producerConfig).toStrictEqual(config) }) + it('should publish a message with PUT fxQuotes callback payload', async () => { + // Arrange + Producer.produceMessage = jest.fn() + const conversionRequestId = randomUUID() + const mockRequest = mocks.mockHttpRequest({ + payload: { conversionRequestId }, + headers: { + 'content-type': 'application/vnd.interoperability.fxQuotes+json;version=1.0' + } + }) + const { handler, code } = mocks.createMockHapiHandler() + + // Act + await quotesApi.put(mockContext, mockRequest, handler) + + // Assert + expect(code).toHaveBeenCalledWith(Http.ReturnCodes.OK.CODE) + expect(Producer.produceMessage).toHaveBeenCalledTimes(1) + + const [message, topicConfig, producerConfig] = Producer.produceMessage.mock.calls[0] + const { id, type, action } = message.content + expect(id).toBe(conversionRequestId) + expect(type).toBe(Events.Event.Type.FX_QUOTE) + expect(action).toBe(Events.Event.Action.PUT) + expect(topicConfig.topicName).toBe(kafkaConfig.PRODUCER.FX_QUOTE.PUT.topic) + expect(producerConfig).toStrictEqual(kafkaConfig.PRODUCER.FX_QUOTE.PUT.config) + }) + it('should rethrow error in case of error during publish callback message', async () => { // Arrange const error = new Error('Put Quote Test Error') @@ -171,6 +227,35 @@ describe('/quotes/{id} API Tests -->', () => { expect(producerConfig).toStrictEqual(config) }) + it('should publish a message with PUT fxQuotes callback error payload', async () => { + // Arrange + Producer.produceMessage = jest.fn() + const conversionRequestId = randomUUID() + const mockRequest = mocks.mockHttpRequest({ + payload: { errorInformation: {} }, + params: { id: conversionRequestId }, + headers: { + 'content-type': 'application/vnd.interoperability.fxQuotes+json;version=1.0' + } + }) + const { handler, code } = mocks.createMockHapiHandler() + + // Act + await quotesApi.put(mockContext, mockRequest, handler) + + // Assert + expect(code).toHaveBeenCalledWith(Http.ReturnCodes.OK.CODE) + expect(Producer.produceMessage).toHaveBeenCalledTimes(1) + + const [message, topicConfig, producerConfig] = Producer.produceMessage.mock.calls[0] + const { id, type, action } = message.content + expect(id).toBe(conversionRequestId) + expect(type).toBe(Events.Event.Type.FX_QUOTE) + expect(action).toBe(Events.Event.Action.PUT) + expect(topicConfig.topicName).toBe(kafkaConfig.PRODUCER.FX_QUOTE.PUT.topic) + expect(producerConfig).toStrictEqual(kafkaConfig.PRODUCER.FX_QUOTE.PUT.config) + }) + it('should rethrow error case of error during publish a message', async () => { // Arrange const error = new Error('PUT Quote Test Error') diff --git a/test/unit/handlers/QuotingHandler.test.js b/test/unit/handlers/QuotingHandler.test.js index 745c57a3..24c05d5e 100644 --- a/test/unit/handlers/QuotingHandler.test.js +++ b/test/unit/handlers/QuotingHandler.test.js @@ -35,6 +35,8 @@ const createKafkaMessage = (topic) => ({ } }) +Logger.isDebugEnabled = jest.fn(() => true) + describe('QuotingHandler Tests -->', () => { let handler let quotesModel diff --git a/test/unit/mocks.js b/test/unit/mocks.js index 3f7db016..2c4651eb 100644 --- a/test/unit/mocks.js +++ b/test/unit/mocks.js @@ -117,11 +117,37 @@ const fxQuoteMocks = { httpRequestOptions: () => ({ }), db: ({ + commit = jest.fn().mockResolvedValue({}), + rollback = jest.fn(), getParticipant = jest.fn().mockResolvedValue({}), - getParticipantEndpoint = jest.fn().mockResolvedValue(undefined) + getParticipantEndpoint = jest.fn().mockResolvedValue(undefined), + createFxQuoteResponse = jest.fn().mockResolvedValue({}), + createFxQuoteResponseConversionTerms = jest.fn().mockResolvedValue({}), + createFxQuoteResponseFxCharge = jest.fn().mockResolvedValue({}), + createFxQuoteResponseConversionTermsExtension = jest.fn().mockResolvedValue({}), + createFxQuoteResponseDuplicateCheck = jest.fn().mockResolvedValue({}), + newTransaction = jest.fn().mockResolvedValue({ commit, rollback }), + createFxQuoteDuplicateCheck = jest.fn().mockResolvedValue({}), + createFxQuote = jest.fn().mockResolvedValue({}), + createFxQuoteConversionTerms = jest.fn().mockResolvedValue({}), + createFxQuoteConversionTermsExtension = jest.fn().mockResolvedValue({}), + createFxQuoteError = jest.fn().mockResolvedValue({}) } = {}) => ({ getParticipant, - getParticipantEndpoint + getParticipantEndpoint, + createFxQuoteResponse, + createFxQuoteResponseConversionTerms, + createFxQuoteResponseFxCharge, + createFxQuoteResponseConversionTermsExtension, + createFxQuoteResponseDuplicateCheck, + newTransaction, + createFxQuoteDuplicateCheck, + createFxQuote, + createFxQuoteConversionTerms, + createFxQuoteConversionTermsExtension, + createFxQuoteError, + commit, + rollback }), proxyClient: ({ isConnected = jest.fn().mockReturnValue(true), diff --git a/test/unit/model/bulkQuotes.test.js b/test/unit/model/bulkQuotes.test.js index 723ef149..48729fe4 100644 --- a/test/unit/model/bulkQuotes.test.js +++ b/test/unit/model/bulkQuotes.test.js @@ -428,6 +428,15 @@ describe('BulkQuotesModel', () => { expect(bulkQuotesModel._getParticipantEndpoint).toBeCalled() }) + + it('should throw rethrow any errors', async () => { + expect.assertions(1) + bulkQuotesModel._getParticipantEndpoint.mockRejectedValueOnce(new Error('Test Error')) + + await expect(bulkQuotesModel.forwardBulkQuoteRequest(mockData.headers, mockData.bulkQuotePostRequest.bulkQuoteId, mockData.bulkQuotePostRequest, mockChildSpan)) + .rejects + .toThrowError() + }) }) describe('handleBulkQuoteUpdate', () => { diff --git a/test/unit/model/fxQuotes.test.js b/test/unit/model/fxQuotes.test.js index a3192f5d..dd141e8d 100644 --- a/test/unit/model/fxQuotes.test.js +++ b/test/unit/model/fxQuotes.test.js @@ -145,6 +145,74 @@ describe('FxQuotesModel Tests -->', () => { expect(fxQuotesModel.forwardFxQuoteRequest).toBeCalledWith(headers, request.conversionRequestId, request, span.getChild()) }) + test('should throw error if request is a duplicate', async () => { + fxQuotesModel = new FxQuotesModel({ db, requestId, proxyClient, log }) + fxQuotesModel.envConfig.simpleRoutingMode = false + jest.spyOn(fxQuotesModel, 'checkDuplicateFxQuoteRequest').mockResolvedValue({ + isResend: false, + isDuplicateId: true + }) + jest.spyOn(fxQuotesModel, 'handleException') + + await expect(fxQuotesModel.handleFxQuoteRequest(headers, request, span)).resolves.toBeUndefined() + expect(fxQuotesModel.handleException).toBeCalled() + }) + + test('should handle resends', async () => { + fxQuotesModel = new FxQuotesModel({ db, requestId, proxyClient, log }) + fxQuotesModel.envConfig.simpleRoutingMode = false + jest.spyOn(fxQuotesModel, 'handleFxQuoteRequestResend') + jest.spyOn(fxQuotesModel, 'forwardFxQuoteRequest') + jest.spyOn(fxQuotesModel, 'checkDuplicateFxQuoteRequest').mockResolvedValue({ + isResend: true, + isDuplicateId: true + }) + + await expect(fxQuotesModel.handleFxQuoteRequest(headers, request, span)).resolves.toBeUndefined() + expect(fxQuotesModel.handleFxQuoteRequestResend).toBeCalled() + expect(fxQuotesModel.forwardFxQuoteRequest).toBeCalled() + }) + + test('should handle fx quote request in persistent mode', async () => { + fxQuotesModel = new FxQuotesModel({ db, requestId, proxyClient, log }) + fxQuotesModel.envConfig.simpleRoutingMode = false + + jest.spyOn(fxQuotesModel, 'checkDuplicateFxQuoteRequest').mockResolvedValue({ + isResend: false, + isDuplicateId: false + }) + jest.spyOn(fxQuotesModel, 'forwardFxQuoteRequest').mockResolvedValue() + jest.spyOn(db, 'createFxQuote').mockResolvedValue({ + fxQuoteId: 1 + }) + jest.spyOn(db, 'createFxQuoteConversionTerms') + jest.spyOn(db, 'createFxQuoteConversionTermsExtension') + jest.spyOn(db, 'createFxQuoteDuplicateCheck') + + await expect(fxQuotesModel.handleFxQuoteRequest(headers, request, span)).resolves.toBeUndefined() + + expect(fxQuotesModel.forwardFxQuoteRequest).toBeCalledWith(headers, request.conversionRequestId, request, span.getChild()) + expect(db.createFxQuote).toBeCalled() + expect(db.createFxQuoteConversionTerms).toBeCalled() + expect(db.createFxQuoteConversionTermsExtension).toBeCalled() + expect(db.createFxQuoteDuplicateCheck).toBeCalled() + }) + + test('it should rollback db changes on error in persistent mode', async () => { + fxQuotesModel = new FxQuotesModel({ db, requestId, proxyClient, log }) + fxQuotesModel.envConfig.simpleRoutingMode = false + + jest.spyOn(fxQuotesModel, 'checkDuplicateFxQuoteRequest').mockResolvedValue({ + isResend: false, + isDuplicateId: false + }) + jest.spyOn(fxQuotesModel, 'forwardFxQuoteRequest').mockResolvedValue() + jest.spyOn(db, 'createFxQuote').mockRejectedValue(new Error('DB Error')) + + await expect(fxQuotesModel.handleFxQuoteRequest(headers, request, span)).resolves.toBeUndefined() + expect(db.rollback).toBeCalled() + }) + test('should handle error thrown', async () => { fxQuotesModel = new FxQuotesModel({ db, requestId, proxyClient, log }) jest.spyOn(fxQuotesModel, 'forwardFxQuoteRequest').mockRejectedValue(new Error('Forward Error')) @@ -225,16 +293,82 @@ describe('FxQuotesModel Tests -->', () => { expect(fxQuotesModel.handleException).toBeCalledWith(headers['fspiop-source'], conversionRequestId, expect.any(Error), headers, span.getChild()) }) - test('should handle fx quote update', async () => { + test('should throw error if request is a duplicate', async () => { + delete headers.accept + + fxQuotesModel = new FxQuotesModel({ db, requestId, proxyClient, log }) + fxQuotesModel.envConfig.simpleRoutingMode = false + jest.spyOn(fxQuotesModel, 'checkDuplicateFxQuoteResponse').mockResolvedValue({ + isResend: false, + isDuplicateId: true + }) + jest.spyOn(fxQuotesModel, 'handleException') + + await expect(fxQuotesModel.handleFxQuoteUpdate(headers, conversionRequestId, updateRequest, span)).resolves.toBeUndefined() + expect(fxQuotesModel.handleException).toBeCalled() + }) + + test('should handle resends', async () => { + delete headers.accept + + fxQuotesModel = new FxQuotesModel({ db, requestId, proxyClient, log }) + fxQuotesModel.envConfig.simpleRoutingMode = false + jest.spyOn(fxQuotesModel, 'handleFxQuoteUpdateResend') + jest.spyOn(fxQuotesModel, 'forwardFxQuoteUpdate') + jest.spyOn(fxQuotesModel, 'checkDuplicateFxQuoteResponse').mockResolvedValue({ + isResend: true, + isDuplicateId: true + }) + + await expect(fxQuotesModel.handleFxQuoteUpdate(headers, conversionRequestId, updateRequest, span)).resolves.toBeUndefined() + expect(fxQuotesModel.handleFxQuoteUpdateResend).toBeCalled() + expect(fxQuotesModel.forwardFxQuoteUpdate).toBeCalled() + }) + + test('should handle fx quote update in persistent mode', async () => { delete headers.accept fxQuotesModel = new FxQuotesModel({ db, requestId, proxyClient, log }) + fxQuotesModel.envConfig.simpleRoutingMode = false + + jest.spyOn(fxQuotesModel, 'checkDuplicateFxQuoteResponse').mockResolvedValue({ + isResend: false, + isDuplicateId: false + }) jest.spyOn(fxQuotesModel, 'forwardFxQuoteUpdate').mockResolvedValue() - jest.spyOn(fxQuotesModel, 'handleException').mockResolvedValue() + jest.spyOn(db, 'createFxQuoteResponse').mockResolvedValue({ + fxQuoteResponseId: 1 + }) + jest.spyOn(db, 'createFxQuoteResponseFxCharge') + jest.spyOn(db, 'createFxQuoteResponseConversionTerms') + jest.spyOn(db, 'createFxQuoteResponseConversionTermsExtension') + jest.spyOn(db, 'createFxQuoteResponseDuplicateCheck') await expect(fxQuotesModel.handleFxQuoteUpdate(headers, conversionRequestId, updateRequest, span)).resolves.toBeUndefined() expect(fxQuotesModel.forwardFxQuoteUpdate).toBeCalledWith(headers, conversionRequestId, updateRequest, span.getChild()) + expect(db.createFxQuoteResponse).toBeCalled() + expect(db.createFxQuoteResponseFxCharge).toBeCalled() + expect(db.createFxQuoteResponseConversionTerms).toBeCalled() + expect(db.createFxQuoteResponseConversionTermsExtension).toBeCalled() + expect(db.createFxQuoteResponseDuplicateCheck).toBeCalled() + }) + + test('it should rollback db changes on error in persistent mode', async () => { + delete headers.accept + + fxQuotesModel = new FxQuotesModel({ db, requestId, proxyClient, log }) + fxQuotesModel.envConfig.simpleRoutingMode = false + + jest.spyOn(fxQuotesModel, 'checkDuplicateFxQuoteResponse').mockResolvedValue({ + isResend: false, + isDuplicateId: false + }) + jest.spyOn(fxQuotesModel, 'forwardFxQuoteUpdate').mockResolvedValue() + jest.spyOn(db, 'createFxQuoteResponse').mockRejectedValue(new Error('DB Error')) + + await expect(fxQuotesModel.handleFxQuoteUpdate(headers, conversionRequestId, updateRequest, span)).resolves.toBeUndefined() + expect(db.rollback).toBeCalled() }) }) @@ -342,6 +476,34 @@ describe('FxQuotesModel Tests -->', () => { expect(fxQuotesModel.sendErrorCallback).toBeCalledWith(headers['fspiop-destination'], fspiopError, conversionRequestId, headers, childSpan, false) }) + test('should handle fx quote error in persistent mode', async () => { + fxQuotesModel = new FxQuotesModel({ db, requestId, proxyClient, log }) + fxQuotesModel.envConfig.simpleRoutingMode = false + jest.spyOn(fxQuotesModel, 'sendErrorCallback').mockResolvedValue() + jest.spyOn(db, 'createFxQuoteError') + + const error = { errorCode: '3201', errorDescription: 'Destination FSP error' } + await expect(fxQuotesModel.handleFxQuoteError(headers, conversionRequestId, error, span)).resolves.toBeUndefined() + + const fspiopError = ErrorHandler.CreateFSPIOPErrorFromErrorInformation(error) + expect(fxQuotesModel.sendErrorCallback).toBeCalledWith(headers['fspiop-destination'], fspiopError, conversionRequestId, headers, childSpan, false) + expect(db.createFxQuoteError).toBeCalledWith(expect.anything(), conversionRequestId, { + errorCode: Number(error.errorCode), + errorDescription: error.errorDescription + }) + }) + + test('should handle error thrown in persistent mode', async () => { + fxQuotesModel = new FxQuotesModel({ db, requestId, proxyClient, log }) + fxQuotesModel.envConfig.simpleRoutingMode = false + jest.spyOn(fxQuotesModel, 'sendErrorCallback').mockResolvedValue() + jest.spyOn(db, 'createFxQuoteError').mockRejectedValue(new Error('DB Error')) + + const error = { errorCode: '3201', errorDescription: 'Destination FSP error' } + await expect(fxQuotesModel.handleFxQuoteError(headers, conversionRequestId, error, span)).resolves.toBeUndefined() + expect(db.rollback).toBeCalled() + }) + test('should handle error thrown', async () => { fxQuotesModel = new FxQuotesModel({ db, requestId, proxyClient, log }) jest.spyOn(fxQuotesModel, 'sendErrorCallback').mockRejectedValue(new Error('Send Error Callback Error')) @@ -381,6 +543,92 @@ describe('FxQuotesModel Tests -->', () => { }) }) + describe('checkDuplicateFxQuoteRequest', () => { + test('should return isResend false, isDuplicateId true, if ids are same but hashes dont match', async () => { + const fxQuoteRequest = { conversionRequestId: 1 } + const duplicateFxQuoteRequest = { conversionRequestId: 1, hash: '481bd172c6dbfba81e8f864332eb0350d1bea77bdf33e9db196efdb1bbb4668' } + fxQuotesModel.db.getFxQuoteDuplicateCheck = jest.fn().mockResolvedValue(duplicateFxQuoteRequest) + + expect(await fxQuotesModel.checkDuplicateFxQuoteRequest(fxQuoteRequest)).toStrictEqual({ + isResend: false, + isDuplicateId: true + }) + }) + + test('should return isResend true, isDuplicateId true, if ids are same and hashes match', async () => { + const fxQuoteRequest = { conversionRequestId: 1 } + const duplicateFxQuoteRequest = { conversionRequestId: 1, hash: '481bd172c6dbfba81e8f864332eb0350d1bea77bdf33e9db196efdb1bbb4668d' } + fxQuotesModel.db.getFxQuoteDuplicateCheck = jest.fn().mockResolvedValue(duplicateFxQuoteRequest) + + expect(await fxQuotesModel.checkDuplicateFxQuoteRequest(fxQuoteRequest)).toStrictEqual({ + isResend: true, + isDuplicateId: true + }) + }) + + test('should return isResend false, isDuplicateId false, match not found in db', async () => { + const fxQuoteRequest = { conversionRequestId: 1 } + fxQuotesModel.db.getFxQuoteDuplicateCheck = jest.fn().mockResolvedValue(null) + + expect(await fxQuotesModel.checkDuplicateFxQuoteRequest(fxQuoteRequest)).toStrictEqual({ + isResend: false, + isDuplicateId: false + }) + }) + + test('throws error if db query fails', async () => { + const fxQuoteRequest = { conversionRequestId: 1 } + fxQuotesModel.db.getFxQuoteDuplicateCheck = jest.fn().mockRejectedValue(new Error('DB Error')) + + await expect(fxQuotesModel.checkDuplicateFxQuoteRequest(fxQuoteRequest)).rejects.toThrow() + }) + }) + + describe('checkDuplicateFxQuoteResponse', () => { + test('should return isResend false, isDuplicateId true, if ids are same but hashes dont match', async () => { + const conversionRequestId = 1 + const fxQuoteResponse = { conversionRequestId: 1 } + const duplicateFxQuoteResponse = { conversionRequestId: 1, hash: '481bd172c6dbfba81e8f864332eb0350d1bea77bdf33e9db196efdb1bbb4668' } + fxQuotesModel.db.getFxQuoteResponseDuplicateCheck = jest.fn().mockResolvedValue(duplicateFxQuoteResponse) + + expect(await fxQuotesModel.checkDuplicateFxQuoteResponse(conversionRequestId, fxQuoteResponse)).toStrictEqual({ + isResend: false, + isDuplicateId: true + }) + }) + + test('should return isResend true, isDuplicateId true, if ids are same and hashes match', async () => { + const conversionRequestId = 1 + const fxQuoteResponse = { conversionRequestId: 1 } + const duplicateFxQuoteResponse = { conversionRequestId: 1, hash: '481bd172c6dbfba81e8f864332eb0350d1bea77bdf33e9db196efdb1bbb4668d' } + fxQuotesModel.db.getFxQuoteResponseDuplicateCheck = jest.fn().mockResolvedValue(duplicateFxQuoteResponse) + + expect(await fxQuotesModel.checkDuplicateFxQuoteResponse(conversionRequestId, fxQuoteResponse)).toStrictEqual({ + isResend: true, + isDuplicateId: true + }) + }) + + test('should return isResend false, isDuplicateId false, match not found in db', async () => { + const conversionRequestId = 1 + const fxQuoteResponse = { conversionRequestId: 1 } + fxQuotesModel.db.getFxQuoteResponseDuplicateCheck = jest.fn().mockResolvedValue(null) + + expect(await fxQuotesModel.checkDuplicateFxQuoteResponse(conversionRequestId, fxQuoteResponse)).toStrictEqual({ + isResend: false, + isDuplicateId: false + }) + }) + + test('throws error if db query fails', async () => { + const conversionRequestId = 1 + const fxQuoteResponse = { conversionRequestId: 1 } + fxQuotesModel.db.getFxQuoteResponseDuplicateCheck = jest.fn().mockRejectedValue(new Error('DB Error')) + + await expect(fxQuotesModel.checkDuplicateFxQuoteResponse(conversionRequestId, fxQuoteResponse)).rejects.toThrow() + }) + }) + describe('sendErrorCallback method Tests', () => { test('should throw fspiop error if no destination found', async () => { fxQuotesModel = new FxQuotesModel({ db, requestId, proxyClient, log }) diff --git a/test/unit/model/index.test.js b/test/unit/model/index.test.js index ce66cc30..aaa1333a 100644 --- a/test/unit/model/index.test.js +++ b/test/unit/model/index.test.js @@ -1,16 +1,20 @@ const modelFactory = require('../../../src/model') const QuotesModel = require('../../../src/model/quotes') const BulkQuotesModel = require('../../../src/model/bulkQuotes') +const FxQuotesModel = require('../../../src/model/fxQuotes') describe('modelFactory Tests -->', () => { it('should create models instances', () => { const db = {} - const { quotesModelFactory, bulkQuotesModelFactory } = modelFactory(db) + const { quotesModelFactory, bulkQuotesModelFactory, fxQuotesModelFactory } = modelFactory(db) const quotesModel = quotesModelFactory('reqId_1') expect(quotesModel).toBeInstanceOf(QuotesModel) const bulkQuotesModel = bulkQuotesModelFactory('reqId_2') expect(bulkQuotesModel).toBeInstanceOf(BulkQuotesModel) + + const fxQuotesModel = fxQuotesModelFactory('reqId_3') + expect(fxQuotesModel).toBeInstanceOf(FxQuotesModel) }) }) diff --git a/test/unit/model/rules.test.js b/test/unit/model/rules.test.js index b7fb28bb..064815b9 100644 --- a/test/unit/model/rules.test.js +++ b/test/unit/model/rules.test.js @@ -90,7 +90,27 @@ const mockRules = [ message: 'The requested payee does not support the payment currency' } } + }, + { + conditions: { + all: [ + { + fact: 'payload', + path: '$.extensionList', + operator: 'isObject', + value: 'true' + } + ] + }, + event: { + type: 'INVALID_QUOTE_REQUEST', + params: { + FSPIOPError: 'PAYEE_UNSUPPORTED_CURRENCY', + message: 'The requested payee does not support the payment currency' + } + } } + ] const RulesEngine = require('../../../src/model/rules') diff --git a/test/unit/serverStart.test.js b/test/unit/serverStart.test.js index 6ce6621c..57567fa1 100644 --- a/test/unit/serverStart.test.js +++ b/test/unit/serverStart.test.js @@ -40,23 +40,34 @@ jest.mock('@mojaloop/central-services-stream', () => ({ })) jest.mock('@mojaloop/central-services-logger') jest.mock('../../src/model/quotes') +jest.mock('@mojaloop/central-services-stream') const { mockRequest: Mockgen, defaultHeaders } = require('../util/helper') const Server = require('../../src/server') const QuotesModel = require('../../src/model/quotes') +const mocks = require('../mocks') +const uuid = require('crypto').randomUUID jest.setTimeout(10_000) describe('Server Start', () => { let server - afterEach(async () => { + beforeAll(async () => { + server = await Server() + }) + + afterAll(async () => { await server.stop({ timeout: 100 }) }) + afterEach(async () => { + jest.clearAllMocks() + }) + it('runs the server', async () => { // Act - server = await Server() + const requests = Mockgen().requestsAsync('/health', 'get') // Arrange const mock = await requests @@ -75,7 +86,7 @@ describe('Server Start', () => { it('post /quotes throws error when missing mandatory header', async () => { // Act - server = await Server() + const requests = Mockgen().requestsAsync('/quotes', 'post') const mock = await requests @@ -117,7 +128,7 @@ describe('Server Start', () => { handleQuoteRequest: jest.fn().mockResolvedValueOnce() })) // Act - server = await Server() + const mock = await Mockgen().requestsAsync('/quotes', 'post') mock.request.body.payee.personalInfo.complexName = { @@ -145,4 +156,232 @@ describe('Server Start', () => { const response = await server.inject(options) expect(response.statusCode).toBe(202) }) + + it('get /quotes/{id} calls QuotesByIdGet handler', async () => { + // Act + + const mock = await Mockgen().requestsAsync('/quotes/{id}', 'get') + + // Arrange + const headers = defaultHeaders() + + const options = { + method: 'get', + url: '' + mock.request.path, + headers + } + + // Act + const response = await server.inject(options) + expect(response.statusCode).toBe(202) + }) + + it('post /quotes calls QuotesPost handler', async () => { + // Act + const payload = mocks.postQuotesPayloadDto() + + // Arrange + const headers = defaultHeaders() + const options = { + method: 'post', + url: '/quotes', + headers, + payload + } + + // Act + const response = await server.inject(options) + expect(response.statusCode).toBe(202) + }) + + it('put /quotes/{id} calls QuotesByIdPut handler', async () => { + // Act + const payload = mocks.putQuotesPayloadDto() + payload.expiration = new Date().toISOString() + payload.condition = 'aAGyvOxOr4yvZo3TalJwvhdWelZp5JNC0MRqwK4DXQI' + + // Arrange + const headers = defaultHeaders() + + const options = { + method: 'put', + url: '/quotes/' + uuid(), + headers, + payload + } + + // Act + const response = await server.inject(options) + expect(response.statusCode).toBe(200) + }) + + it('put /quotes/{id}/error calls QuotesErrorByIDPut handler', async () => { + // Act + const mock = await Mockgen().requestsAsync('/quotes/{id}/error', 'put') + + // Arrange + const headers = defaultHeaders() + + const options = { + method: 'put', + url: '' + mock.request.path, + headers, + payload: mock.request.body + } + + // Act + const response = await server.inject(options) + expect(response.statusCode).toBe(200) + }) + + it('put /bulkQuotes/{id}/error calls BulkQuotesErrorByIdPut handler', async () => { + // Act + const mock = await Mockgen().requestsAsync('/bulkQuotes/{id}/error', 'put') + + // Arrange + const headers = defaultHeaders() + + const options = { + method: 'put', + url: '' + mock.request.path, + headers, + payload: mock.request.body + } + + // Act + const response = await server.inject(options) + expect(response.statusCode).toBe(200) + }) + + it('get /bulkQuotes/{id} calls BulkQuotesByIdGet handler', async () => { + // Act + const mock = await Mockgen().requestsAsync('/bulkQuotes/{id}', 'get') + + // Arrange + const headers = defaultHeaders() + + const options = { + method: 'get', + url: '' + mock.request.path, + headers + } + + // Act + const response = await server.inject(options) + expect(response.statusCode).toBe(202) + }) + + it('put /bulkQuotes/{id} calls BulkQuotesByIdPut handler', async () => { + // Act + const payload = mocks.putBulkQuotesPayloadDto() + payload.individualQuoteResults[0].condition = 'aAGyvOxOr4yvZo3TalJwvhdWelZp5JNC0MRqwK4DXQI' + + // Arrange + const headers = defaultHeaders() + + const options = { + method: 'put', + url: `/bulkQuotes/${uuid()}`, + headers, + payload + } + + // Act + const response = await server.inject(options) + expect(response.statusCode).toBe(200) + }) + + it('post /bulkQuotes calls BulkQuotesPost handler', async () => { + // Act + const payload = mocks.postBulkQuotesPayloadDto() + + // Arrange + const headers = defaultHeaders() + + const options = { + method: 'post', + url: '/bulkQuotes', + headers, + payload + } + + // Act + const response = await server.inject(options) + expect(response.statusCode).toBe(202) + }) + + it('put /fxQuotes/{id}/error calls FxQuotesByIDAndErrorPut handler', async () => { + // Act + const mock = await Mockgen().requestsAsync('/fxQuotes/{id}/error', 'put') + + // Arrange + const headers = defaultHeaders() + + const options = { + method: 'put', + url: mock.request.path, + headers, + payload: mock.request.body + } + + // Act + const response = await server.inject(options) + expect(response.statusCode).toBe(200) + }) + + it('get /fxQuotes/{id} calls FxQuotesByIDGet handler', async () => { + // Act + const mock = await Mockgen().requestsAsync('/fxQuotes/{id}', 'get') + + // Arrange + const headers = defaultHeaders() + + const options = { + method: 'get', + url: '' + mock.request.path, + headers + } + + // Act + const response = await server.inject(options) + expect(response.statusCode).toBe(202) + }) + + it('put /fxQuotes/{id} calls FxQuotesByIdPut handler', async () => { + // Act + const payload = mocks.putFxQuotesPayloadDto() + payload.condition = 'aAGyvOxOr4yvZo3TalJwvhdWelZp5JNC0MRqwK4DXQI' + // Arrange + const headers = defaultHeaders() + + const options = { + method: 'put', + url: '/fxQuotes/' + uuid(), + headers, + payload + } + + // Act + const response = await server.inject(options) + expect(response.statusCode).toBe(200) + }) + + it('post /fxQuotes calls FxQuotesPost handler', async () => { + // Act + const payload = mocks.postFxQuotesPayloadDto() + + // Arrange + const headers = defaultHeaders() + + const options = { + method: 'post', + url: '/fxQuotes', + headers, + payload + } + + // Act + const response = await server.inject(options) + expect(response.statusCode).toBe(202) + }) })