diff --git a/config/default.json b/config/default.json index 9d72be1b..e1327c91 100644 --- a/config/default.json +++ b/config/default.json @@ -68,5 +68,9 @@ } } } + }, + "CACHE": { + "ENUM_DATA_EXPIRES_IN_MS": 4170000, + "PARTICIPANT_DATA_EXPIRES_IN_MS": 60000 } } diff --git a/package-lock.json b/package-lock.json index 65ec3c6f..c649e2a8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "quoting-service", - "version": "15.2.2", + "version": "15.2.3-snapshot.3", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "quoting-service", - "version": "15.2.2", + "version": "15.2.3-snapshot.3", "license": "Apache-2.0", "dependencies": { "@hapi/good": "9.0.1", @@ -30,7 +30,7 @@ "good-squeeze": "5.1.0", "joi": "17.11.0", "json-rules-engine": "5.0.2", - "knex": "2.5.1", + "knex": "3.0.1", "memory-cache": "0.2.0", "minimist": "1.2.8", "mysql": "2.18.1", @@ -44,7 +44,7 @@ "eslint-config-standard": "17.1.0", "jest": "29.7.0", "jest-junit": "16.0.0", - "npm-check-updates": "16.14.5", + "npm-check-updates": "16.14.6", "nyc": "15.1.0", "pre-commit": "1.2.2", "proxyquire": "2.1.3", @@ -6862,19 +6862,6 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -9751,9 +9738,9 @@ } }, "node_modules/knex": { - "version": "2.5.1", - "resolved": "https://registry.npmjs.org/knex/-/knex-2.5.1.tgz", - "integrity": "sha512-z78DgGKUr4SE/6cm7ku+jHvFT0X97aERh/f0MUKAKgFnwCYBEW4TFBqtHWFYiJFid7fMrtpZ/gxJthvz5mEByA==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/knex/-/knex-3.0.1.tgz", + "integrity": "sha512-ruASxC6xPyDklRdrcDy6a9iqK+R9cGK214aiQa+D9gX2ZnHZKv6o6JC9ZfgxILxVAul4bZ13c3tgOAHSuQ7/9g==", "dependencies": { "colorette": "2.0.19", "commander": "^10.0.0", @@ -9774,7 +9761,7 @@ "knex": "bin/cli.js" }, "engines": { - "node": ">=12" + "node": ">=16" }, "peerDependenciesMeta": { "better-sqlite3": { @@ -11009,9 +10996,9 @@ } }, "node_modules/npm-check-updates": { - "version": "16.14.5", - "resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-16.14.5.tgz", - "integrity": "sha512-f7v3YzPUgadtkB2LAVhiWMjrSejJ0N8OM9JjjVfxBz2neHqmPSWQUAUA+U/p3xeXHl9bghRD6knRqBhm9dkRGg==", + "version": "16.14.6", + "resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-16.14.6.tgz", + "integrity": "sha512-sJ6w4AmSDP7YzBXah94Ul2JhiIbjBDfx9XYgib15um2wtiQkOyjE7Lov3MNUSQ84Ry7T81mE4ynMbl/mGbK4HQ==", "dev": true, "dependencies": { "chalk": "^5.3.0", @@ -11019,7 +11006,7 @@ "commander": "^10.0.1", "fast-memoize": "^2.5.2", "find-up": "5.0.0", - "fp-and-or": "^0.1.3", + "fp-and-or": "^0.1.4", "get-stdin": "^8.0.0", "globby": "^11.0.4", "hosted-git-info": "^5.1.0", @@ -11037,11 +11024,11 @@ "prompts-ncu": "^3.0.0", "rc-config-loader": "^4.1.3", "remote-git-tags": "^3.0.0", - "rimraf": "^5.0.1", + "rimraf": "^5.0.5", "semver": "^7.5.4", "semver-utils": "^1.1.4", "source-map-support": "^0.5.21", - "spawn-please": "^2.0.1", + "spawn-please": "^2.0.2", "strip-ansi": "^7.1.0", "strip-json-comments": "^5.0.1", "untildify": "^4.0.0", diff --git a/package.json b/package.json index 7bbca03d..baab96e2 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "quoting-service", "description": "Quoting Service hosted by a scheme", "license": "Apache-2.0", - "version": "15.2.2", + "version": "15.2.3-snapshot.3", "author": "ModusBox", "contributors": [ "Georgi Georgiev ", @@ -55,7 +55,7 @@ "lint": "npx standard", "lint:fix": "npx standard --fix", "test": "npm run test:unit", - "test:unit": "jest --runInBand --testMatch '**/test/unit/**/*.test.js'", + "test:unit": "jest --runInBand --testMatch '**/test/unit/**/*.test.js' ", "test:coverage": "jest --runInBand --coverage --coverageThreshold='{}' --testMatch '**/test/unit/**/*.test.js'", "test:coverage-check": "jest --runInBand --coverage --testMatch '**/test/unit/**/*.test.js'", "test:junit": "jest --runInBand --reporters=default --reporters=jest-junit --testMatch '**/test/unit/**/*.test.js'", @@ -99,7 +99,7 @@ "good-squeeze": "5.1.0", "joi": "17.11.0", "json-rules-engine": "5.0.2", - "knex": "2.5.1", + "knex": "3.0.1", "memory-cache": "0.2.0", "minimist": "1.2.8", "mysql": "2.18.1", @@ -113,7 +113,7 @@ "eslint-config-standard": "17.1.0", "jest": "29.7.0", "jest-junit": "16.0.0", - "npm-check-updates": "16.14.5", + "npm-check-updates": "16.14.6", "nyc": "15.1.0", "pre-commit": "1.2.2", "proxyquire": "2.1.3", diff --git a/src/data/cachedDatabase.js b/src/data/cachedDatabase.js index bb3ec23c..d25bcb11 100644 --- a/src/data/cachedDatabase.js +++ b/src/data/cachedDatabase.js @@ -38,8 +38,6 @@ const Metrics = require('@mojaloop/central-services-metrics') const { getStackOrInspect } = require('../lib/util') -const DEFAULT_TTL_SECONDS = 60 - /** * An extension of the Database class that caches enum values in memory */ @@ -83,10 +81,13 @@ class CachedDatabase extends Database { return this.getCacheValue('getPartyIdentifierType', [partyIdentifierType]) } - // This has been commented out as the participant data should not be cached. This is mainly due to the scenario when the participant is made inactive vs active. Ref: https://github.com/mojaloop/project/issues/933 - // async getParticipant (participantName) { - // return this.getCacheValue('getParticipant', [participantName]) - // } + async getParticipant (participantName, participantType, currencyId, ledgerAccountTypeId) { + return this.getCacheValue('getParticipant', [participantName, participantType, currencyId, ledgerAccountTypeId]) + } + + async getParticipantByName (participantName, participantType) { + return this.getCacheValue('getParticipantByName', [participantName, participantType]) + } async getTransferParticipantRoleType (name) { return this.getCacheValue('getTransferParticipantRoleType', [name]) @@ -96,9 +97,9 @@ class CachedDatabase extends Database { return this.getCacheValue('getLedgerEntryType', [name]) } - // async getParticipantEndpoint (participantName, endpointType) { - // return this.getCacheValue('getParticipantEndpoint', [participantName, endpointType]) - // } + async getParticipantEndpoint (participantName, endpointType) { + return this.getCacheValue('getParticipantEndpoint', [participantName, endpointType]) + } async getCacheValue (type, params) { const histTimer = Metrics.getHistogram( @@ -113,7 +114,16 @@ class CachedDatabase extends Database { // we need to get the value from the db and cache it this.writeLog(`Cache miss for ${type}: ${util.inspect(params)}`) value = await super[type].apply(this, params) - this.cachePut(type, params, value) + // cache participant with a shorter TTL than enums (participant data is more likely to change) + if ( + type === 'getParticipant' || + type === 'getParticipantByName' || + type === 'getParticipantEndpoint' + ) { + this.cachePut(type, params, value, this.config.participantDataCacheExpiresInMs) + } else { + this.cachePut(type, params, value, this.config.enumDataCacheExpiresInMs) + } histTimer({ success: true, queryName: type, hit: false }) } else { this.writeLog(`Cache hit for ${type} ${util.inspect(params)}: ${value}`) @@ -133,9 +143,9 @@ class CachedDatabase extends Database { * * @returns {undefined} */ - cachePut (type, params, value) { + cachePut (type, params, value, ttl) { const key = this.getCacheKey(type, params) - this.cache.put(key, value, (this.config.database.cacheTtlSeconds || DEFAULT_TTL_SECONDS) * 1000) + this.cache.put(key, value, ttl) } /** @@ -156,6 +166,10 @@ class CachedDatabase extends Database { getCacheKey (type, params) { return `${type}_${params.join('__')}` } + + invalidateCache () { + this.cache.clear() + } } module.exports = CachedDatabase diff --git a/src/data/database.js b/src/data/database.js index 661d5e41..13705efb 100644 --- a/src/data/database.js +++ b/src/data/database.js @@ -97,23 +97,6 @@ class Database { } } - /** - * Gets the set of enabled transfer rules - * - * @returns {promise} - all enabled transfer rules - */ - async getTransferRules () { - try { - const rows = await this.queryBuilder('transferRules') - .where('enabled', true) - .select() - return rows.map(r => JSON.parse(r.rule)) - } catch (err) { - this.writeLog(`Error in getTransferRules: ${getStackOrInspect(err)}`) - throw ErrorHandler.Factory.reformatFSPIOPError(err) - } - } - /** * Gets the id of the specified transaction initiator type * @@ -463,9 +446,9 @@ class Database { * * @returns {promise} */ - async createPayerQuoteParty (txn, quoteId, party, amount, currency) { + async createPayerQuoteParty (txn, quoteId, party, amount, currency, enumVals) { // note amount is negative for payee and positive for payer - return this.createQuoteParty(txn, quoteId, LOCAL_ENUM.PAYER, LOCAL_ENUM.PAYER_DFSP, LOCAL_ENUM.PRINCIPLE_VALUE, party, amount, currency) + return this.createQuoteParty(txn, quoteId, LOCAL_ENUM.PAYER, party, amount, currency, enumVals) } /** @@ -473,9 +456,9 @@ class Database { * * @returns {promise} */ - async createPayeeQuoteParty (txn, quoteId, party, amount, currency) { + async createPayeeQuoteParty (txn, quoteId, party, amount, currency, enumVals) { // note amount is negative for payee and positive for payer - return this.createQuoteParty(txn, quoteId, LOCAL_ENUM.PAYEE, LOCAL_ENUM.PAYEE_DFSP, LOCAL_ENUM.PRINCIPLE_VALUE, party, -amount, currency) + return this.createQuoteParty(txn, quoteId, LOCAL_ENUM.PAYEE, party, -amount, currency, enumVals) } /** @@ -483,19 +466,10 @@ class Database { * * @returns {integer} - id of created quoteParty */ - async createQuoteParty (txn, quoteId, partyType, participantType, ledgerEntryType, party, amount, currency) { + async createQuoteParty (txn, quoteId, partyType, party, amount, currency, enumVals) { try { const refs = {} - // get various enum ids (async, as parallel as possible) - const enumVals = await Promise.all([ - this.getPartyType(partyType), - this.getPartyIdentifierType(party.partyIdInfo.partyIdType), - this.getParticipantByName(party.partyIdInfo.fspId), - this.getTransferParticipantRoleType(participantType), - this.getLedgerEntryType(ledgerEntryType) - ]) - refs.partyTypeId = enumVals[0] refs.partyIdentifierTypeId = enumVals[1] refs.participantId = enumVals[2] @@ -566,83 +540,6 @@ class Database { } } - /** - * Returns an array of quote parties associated with the specified quote - * that have enum values resolved to their text identifiers - * - * @returns {object[]} - */ - async getQuotePartyView (quoteId) { - try { - const rows = await this.queryBuilder('quotePartyView') - .where({ - quoteId - }) - .select() - - return rows - } catch (err) { - this.writeLog(`Error in getQuotePartyView: ${getStackOrInspect(err)}`) - throw ErrorHandler.Factory.reformatFSPIOPError(err) - } - } - - /** - * Returns a quote that has enum values resolved to their text identifiers - * - * @returns {object} - */ - async getQuoteView (quoteId) { - try { - const rows = await this.queryBuilder('quoteView') - .where({ - quoteId - }) - .select() - - if ((!rows) || rows.length < 1) { - return null - } - - if (rows.length > 1) { - throw ErrorHandler.Factory.createInternalServerFSPIOPError(`Expected 1 row for quoteId ${quoteId} but got: ${util.inspect(rows)}`) - } - - return rows[0] - } catch (err) { - this.writeLog(`Error in getQuoteView: ${getStackOrInspect(err)}`) - throw ErrorHandler.Factory.reformatFSPIOPError(err) - } - } - - /** - * Returns a quote response that has enum values resolved to their text identifiers - * - * @returns {object} - */ - async getQuoteResponseView (quoteId) { - try { - const rows = await this.queryBuilder('quoteResponseView') - .where({ - quoteId - }) - .select() - - if ((!rows) || rows.length < 1) { - return null - } - - if (rows.length > 1) { - throw ErrorHandler.Factory.createInternalServerFSPIOPError(`Expected 1 row for quoteId ${quoteId} but got: ${util.inspect(rows)}`) - } - - return rows[0] - } catch (err) { - this.writeLog(`Error in getQuoteResponseView: ${getStackOrInspect(err)}`) - throw ErrorHandler.Factory.reformatFSPIOPError(err) - } - } - /** * Creates the specifid party and returns its id * @@ -767,35 +664,6 @@ class Database { throw ErrorHandler.Factory.reformatFSPIOPError(err) } } - /** - * Gets the specified endpoint for the specified quote party - * - * @returns {promise} - resolves to the endpoint base url - */ - - async getQuotePartyEndpoint (quoteId, endpointType, partyType) { - try { - const rows = await this.queryBuilder('participantEndpoint') - .innerJoin('endpointType', 'participantEndpoint.endpointTypeId', 'endpointType.endpointTypeId') - .innerJoin('quoteParty', 'quoteParty.participantId', 'participantEndpoint.participantId') - .innerJoin('partyType', 'partyType.partyTypeId', 'quoteParty.partyTypeId') - .innerJoin('quote', 'quote.quoteId', 'quoteParty.quoteId') - .where('endpointType.name', endpointType) - .andWhere('partyType.name', partyType) - .andWhere('quote.quoteId', quoteId) - .andWhere('participantEndpoint.isActive', 1) - .select('participantEndpoint.value') - - if ((!rows) || rows.length < 1) { - return null - } - - return rows[0].value - } catch (err) { - this.writeLog(`Error in getQuotePartyEndpoint: ${getStackOrInspect(err)}`) - throw ErrorHandler.Factory.reformatFSPIOPError(err) - } - } /** * Gets the specified endpoint of the specified type for the specified participant @@ -871,30 +739,6 @@ class Database { } } - /** - * Gets any transactionReference for the specified quote from the database - * - * @returns {object} - transaction reference or null if none found - */ - async getTransactionReference (quoteId) { - try { - const rows = await this.queryBuilder('transactionReference') - .where({ - quoteId - }) - .select() - - if ((!rows) || rows.length < 1) { - return null - } - - return rows[0] - } catch (err) { - this.writeLog(`Error in getTransactionReference: ${getStackOrInspect(err)}`) - throw ErrorHandler.Factory.reformatFSPIOPError(err) - } - } - /** * Creates a quoteResponse object in the database * diff --git a/src/handlers/quotes.js b/src/handlers/quotes.js index 8201f7b0..2b918ac6 100644 --- a/src/handlers/quotes.js +++ b/src/handlers/quotes.js @@ -89,7 +89,7 @@ module.exports = { }, EventSdk.AuditEventAction.start) // call the quote request handler in the model - model.handleQuoteRequest(quoteRequest.headers, quoteRequest.payload, span).catch(err => { + model.handleQuoteRequest(quoteRequest.headers, quoteRequest.payload, span, request.server.app.cache).catch(err => { Logger.isErrorEnabled && Logger.error(`ERROR - handleQuoteRequest: ${LibUtil.getStackOrInspect(err)}`) }) histTimerEnd({ success: true }) diff --git a/src/lib/config.js b/src/lib/config.js index 20b628f2..5cedbefb 100644 --- a/src/lib/config.js +++ b/src/lib/config.js @@ -152,6 +152,8 @@ class Config { this.instrumentationMetricsDisabled = (RC.INSTRUMENTATION.METRICS.DISABLED === true || RC.INSTRUMENTATION.METRICS.DISABLED === 'true') this.instrumentationMetricsLabels = RC.INSTRUMENTATION.METRICS.labels this.instrumentationMetricsConfig = RC.INSTRUMENTATION.METRICS.config + this.enumDataCacheExpiresInMs = RC.CACHE.ENUM_DATA_EXPIRES_IN_MS || 4170000 + this.participantDataCacheExpiresInMs = RC.CACHE.PARTICIPANT_DATA_EXPIRES_IN_MS || 60000 } } diff --git a/src/lib/util.js b/src/lib/util.js index c5fab61f..5149bdd6 100644 --- a/src/lib/util.js +++ b/src/lib/util.js @@ -40,7 +40,7 @@ const Config = require('./config') const axios = require('axios') const failActionHandler = async (request, h, err) => { - Logger.error(`validation failure: ${getStackOrInspect}`) + Logger.isErrorEnabled && Logger.error(`validation failure: ${getStackOrInspect}`) throw err } @@ -200,15 +200,33 @@ function calculateRequestHash (request) { return crypto.createHash('sha256').update(requestStr).digest('hex') } -const fetchParticipantInfo = async (source, destination) => { +// Add caching to the participant endpoint +const fetchParticipantInfo = async (source, destination, cache) => { // Get quote participants from central ledger admin const { switchEndpoint } = new Config() const url = `${switchEndpoint}/participants` - const [payer, payee] = await Promise.all([ - axios.request({ url: `${url}/${source}` }), - axios.request({ url: `${url}/${destination}` }) - ]) - return { payer: payer.data, payee: payee.data } + let requestPayer + let requestPayee + const cachedPayer = cache && cache.get(`fetchParticipantInfo_${source}`) + const cachedPayee = cache && cache.get(`fetchParticipantInfo_${destination}`) + + if (!cachedPayer) { + requestPayer = await axios.request({ url: `${url}/${source}` }) + cache && cache.put(`fetchParticipantInfo_${source}`, requestPayer, Config.participantDataCacheExpiresInMs) + Logger.isDebugEnabled && Logger.debug(`${new Date().toISOString()}, [fetchParticipantInfo]: cache miss for payer ${source}`) + } else { + Logger.isDebugEnabled && Logger.debug(`${new Date().toISOString()}, [fetchParticipantInfo]: cache hit for payer ${source}`) + } + if (!cachedPayee) { + requestPayee = await axios.request({ url: `${url}/${destination}` }) + cache && cache.put(`fetchParticipantInfo_${destination}`, requestPayee, Config.participantDataCacheExpiresInMs) + Logger.isDebugEnabled && Logger.debug(`${new Date().toISOString()}, [fetchParticipantInfo]: cache miss for payer ${source}`) + } else { + Logger.isDebugEnabled && Logger.debug(`${new Date().toISOString()}, [fetchParticipantInfo]: cache hit for payee ${destination}`) + } + const payer = cachedPayer || requestPayer.data + const payee = cachedPayee || requestPayee.data + return { payer, payee } } module.exports = { diff --git a/src/model/quotes.js b/src/model/quotes.js index 7b752eb3..c325a3f2 100644 --- a/src/model/quotes.js +++ b/src/model/quotes.js @@ -175,11 +175,21 @@ class QuotesModel { // Following is the validation to make sure valid fsp's are used in the payload for simple routing mode if (envConfig.simpleRoutingMode) { // Lets make sure the optional fspId exists in the payer's partyIdInfo before we validate it - if (quoteRequest.payer && quoteRequest.payer.partyIdInfo && quoteRequest.payer.partyIdInfo.fspId) { + if ( + quoteRequest.payer && + quoteRequest.payer.partyIdInfo && + quoteRequest.payer.partyIdInfo.fspId && + quoteRequest.payer.partyIdInfo.fspId !== fspiopSource + ) { await this.db.getParticipant(quoteRequest.payer.partyIdInfo.fspId, LOCAL_ENUM.PAYER_DFSP, quoteRequest.amount.currency, ENUM.Accounts.LedgerAccountType.POSITION) } // Lets make sure the optional fspId exists in the payee's partyIdInfo before we validate it - if (quoteRequest.payee && quoteRequest.payee.partyIdInfo && quoteRequest.payee.partyIdInfo.fspId) { + if ( + quoteRequest.payee && + quoteRequest.payee.partyIdInfo && + quoteRequest.payee.partyIdInfo.fspId && + quoteRequest.payee.partyIdInfo.fspId !== fspiopDestination + ) { await this.db.getParticipant(quoteRequest.payee.partyIdInfo.fspId, LOCAL_ENUM.PAYEE_DFSP, quoteRequest.amount.currency, ENUM.Accounts.LedgerAccountType.POSITION) } } @@ -200,7 +210,7 @@ class QuotesModel { * * @returns {object} - returns object containing keys for created database entities */ - async handleQuoteRequest (headers, quoteRequest, span) { + async handleQuoteRequest (headers, quoteRequest, span, cache) { const histTimer = Metrics.getHistogram( 'model_quote', 'handleQuoteRequest - Metrics for quote model', @@ -221,7 +231,7 @@ class QuotesModel { // validate - this will throw if the request is invalid await this.validateQuoteRequest(fspiopSource, fspiopDestination, quoteRequest) - const { payer, payee } = await fetchParticipantInfo(fspiopSource, fspiopDestination) + const { payer, payee } = await fetchParticipantInfo(fspiopSource, fspiopDestination, cache) this.writeLog(`Got payer ${payer} and payee ${payee}`) // Run the rules engine. If the user does not want to run the rules engine, they need only to @@ -255,13 +265,53 @@ class QuotesModel { handledRuleEvents.quoteRequest, handleQuoteRequestSpan, handledRuleEvents.additionalHeaders) } - // do everything in a db txn so we can rollback multiple operations if something goes wrong - txn = await this.db.newTransaction() - // todo: validation + // get various enum ids (async, as parallel as possible) + const payerEnumVals = [] + const payeeEnumVals = [] + ;[ + refs.transactionInitiatorTypeId, + refs.transactionInitiatorId, + refs.transactionScenarioId, + refs.amountTypeId, + payerEnumVals[0], + payerEnumVals[1], + payerEnumVals[2], + payerEnumVals[3], + payerEnumVals[4], + payeeEnumVals[0], + payeeEnumVals[1], + payeeEnumVals[2], + payeeEnumVals[3], + payeeEnumVals[4] + ] = await Promise.all([ + this.db.getInitiatorType(quoteRequest.transactionType.initiatorType), + this.db.getInitiator(quoteRequest.transactionType.initiator), + this.db.getScenario(quoteRequest.transactionType.scenario), + this.db.getAmountType(quoteRequest.amountType), + this.db.getPartyType(LOCAL_ENUM.PAYER), + this.db.getPartyIdentifierType(quoteRequest.payer.partyIdInfo.partyIdType), + this.db.getParticipantByName(quoteRequest.payer.partyIdInfo.fspId), + this.db.getTransferParticipantRoleType(LOCAL_ENUM.PAYER_DFSP), + this.db.getLedgerEntryType(LOCAL_ENUM.PRINCIPLE_VALUE), + this.db.getPartyType(LOCAL_ENUM.PAYEE), + this.db.getPartyIdentifierType(quoteRequest.payee.partyIdInfo.partyIdType), + this.db.getParticipantByName(quoteRequest.payee.partyIdInfo.fspId), + this.db.getTransferParticipantRoleType(LOCAL_ENUM.PAYEE_DFSP), + this.db.getLedgerEntryType(LOCAL_ENUM.PRINCIPLE_VALUE) + ]) + + if (quoteRequest.transactionType.subScenario) { + // a sub scenario is specified, we need to look it up + refs.transactionSubScenarioId = await this.db.getSubScenario(quoteRequest.transactionType.subScenario) + } + // if we get here we need to create a duplicate check row const hash = calculateRequestHash(quoteRequest) + + // do everything in a db txn so we can rollback multiple operations if something goes wrong + txn = await this.db.newTransaction() await this.db.createQuoteDuplicateCheck(txn, quoteRequest.quoteId, hash) // create a txn reference @@ -270,23 +320,6 @@ class QuotesModel { quoteRequest.quoteId, quoteRequest.transactionId) this.writeLog(`transactionReference created transactionReferenceId: ${refs.transactionReferenceId}`) - // get the initiator type - refs.transactionInitiatorTypeId = await this.db.getInitiatorType(quoteRequest.transactionType.initiatorType) - - // get the initiator - refs.transactionInitiatorId = await this.db.getInitiator(quoteRequest.transactionType.initiator) - - // get the txn scenario id - refs.transactionScenarioId = await this.db.getScenario(quoteRequest.transactionType.scenario) - - if (quoteRequest.transactionType.subScenario) { - // a sub scenario is specified, we need to look it up - refs.transactionSubScenarioId = await this.db.getSubScenario(quoteRequest.transactionType.subScenario) - } - - // get amount type - refs.amountTypeId = await this.db.getAmountType(quoteRequest.amountType) - // create the quote row itself // eslint-disable-next-line require-atomic-updates refs.quoteId = await this.db.createQuote(txn, { @@ -305,13 +338,15 @@ class QuotesModel { currencyId: quoteRequest.amount.currency }) - // eslint-disable-next-line require-atomic-updates - refs.payerId = await this.db.createPayerQuoteParty(txn, refs.quoteId, quoteRequest.payer, - quoteRequest.amount.amount, quoteRequest.amount.currency) - - // eslint-disable-next-line require-atomic-updates - refs.payeeId = await this.db.createPayeeQuoteParty(txn, refs.quoteId, quoteRequest.payee, - quoteRequest.amount.amount, quoteRequest.amount.currency) + ;[ + refs.payerId, + refs.payeeId + ] = await Promise.all([ + this.db.createPayerQuoteParty(txn, refs.quoteId, quoteRequest.payer, + quoteRequest.amount.amount, quoteRequest.amount.currency, payerEnumVals), + this.db.createPayeeQuoteParty(txn, refs.quoteId, quoteRequest.payee, + quoteRequest.amount.amount, quoteRequest.amount.currency, payeeEnumVals) + ]) // store any extension list items if (quoteRequest.extensionList && @@ -509,6 +544,7 @@ class QuotesModel { ['success', 'queryName', 'duplicateResult'] ).startTimer() let txn = null + let payeeParty = null const fspiopSource = headers[ENUM.Http.Headers.FSPIOP.SOURCE] const envConfig = new Config() const handleQuoteUpdateSpan = span.getChild('qs_quote_handleQuoteUpdate') @@ -541,6 +577,15 @@ class QuotesModel { return this.handleQuoteUpdateResend(headers, quoteId, quoteUpdateRequest, handleQuoteUpdateSpan) } + if (quoteUpdateRequest.geoCode) { + payeeParty = await this.db.getQuoteParty(quoteId, 'PAYEE') + + if (!payeeParty) { + // internal-error + throw ErrorHandler.CreateFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.PARTY_NOT_FOUND, `Unable to find payee party for quote ${quoteId}`, null, fspiopSource) + } + } + // do everything in a transaction so we can rollback multiple operations if something goes wrong txn = await this.db.newTransaction() @@ -569,13 +614,6 @@ class QuotesModel { // did we get a geoCode for the payee? if (quoteUpdateRequest.geoCode) { - const payeeParty = await this.db.getQuoteParty(quoteId, 'PAYEE') - - if (!payeeParty) { - // internal-error - throw ErrorHandler.CreateFSPIOPError(ErrorHandler.Enums.FSPIOPErrorCodes.PARTY_NOT_FOUND, `Unable to find payee party for quote ${quoteId}`, null, fspiopSource) - } - refs.geoCodeId = await this.db.createGeoCode(txn, { quotePartyId: payeeParty.quotePartyId, latitude: quoteUpdateRequest.geoCode.latitude, diff --git a/src/server.js b/src/server.js index cbae61d0..a46afaf0 100644 --- a/src/server.js +++ b/src/server.js @@ -54,15 +54,16 @@ const Config = require('./lib/config.js') const Database = require('./data/cachedDatabase') const Handlers = require('./handlers') const Routes = require('./handlers/routes') +const { Cache } = require('memory-cache') const OpenAPISpecPath = Path.resolve(__dirname, './interface/QuotingService-swagger.yaml') /** * Initializes a database connection pool */ -const initDb = function (config) { +const initDb = function (config, cache) { // try open a db connection pool - const database = new Database(config) + const database = new Database(config, cache) return database.connect() } @@ -72,7 +73,7 @@ const initDb = function (config) { * @param db - database instance * @param config - configuration object */ -const initServer = async function (db, config) { +const initServer = async function (db, config, cache) { // init a server const server = new Hapi.Server({ address: config.listenAddress, @@ -88,6 +89,8 @@ const initServer = async function (db, config) { // put the database pool somewhere handlers can use it server.app.database = db + server.app.cache = cache + if (config.apiDocumentationEndpoints) { await server.register({ plugin: APIDocumentation, @@ -196,10 +199,11 @@ const config = new Config() * @description Starts the web server */ async function start () { + const cache = new Cache() initializeInstrumentation(config) // initialize database connection pool and start the api server return initDb(config) - .then(db => initServer(db, config)) + .then(db => initServer(db, config, cache)) .then(server => { // Ignore coverage here as simulating `process.on('SIGTERM'...)` kills jest /* istanbul ignore next */ diff --git a/test/unit/data/cachedDatabase.test.js b/test/unit/data/cachedDatabase.test.js index 84da757c..59d3d771 100644 --- a/test/unit/data/cachedDatabase.test.js +++ b/test/unit/data/cachedDatabase.test.js @@ -146,6 +146,39 @@ describe('cachedDatabase', () => { // Assert expect(result).toBe('getLedgerEntryTypeValue') }) + + it('getParticipant', async () => { + // Arrange + cachedDb.cachePut('getParticipant', ['paramA', 'paramB', 'paramC', 'paramD'], 'getParticipantValue') + + // Act + const result = await cachedDb.getParticipant('paramA', 'paramB', 'paramC', 'paramD') + + // Assert + expect(result).toBe('getParticipantValue') + }) + + it('getParticipantByName', async () => { + // Arrange + cachedDb.cachePut('getParticipantByName', ['paramA', 'paramB'], 'getParticipantByNameValue') + + // Act + const result = await cachedDb.getParticipantByName('paramA', 'paramB') + + // Assert + expect(result).toBe('getParticipantByNameValue') + }) + + it('getParticipantEndpoint', async () => { + // Arrange + cachedDb.cachePut('getParticipantEndpoint', ['paramA', 'paramB'], 'getParticipantEndpointValue') + + // Act + const result = await cachedDb.getParticipantEndpoint('paramA', 'paramB') + + // Assert + expect(result).toBe('getParticipantEndpointValue') + }) }) describe('Cache Handling', () => { @@ -181,6 +214,9 @@ describe('cachedDatabase', () => { expect(Database.prototype.getLedgerEntryType).toBeCalledTimes(1) expect(result).toStrictEqual(expected) expect(result2).toStrictEqual(expected) + + // invalidate to stop jest open handles + await cachedDb.invalidateCache() }) it('handles an exception', async () => { diff --git a/test/unit/data/database.test.js b/test/unit/data/database.test.js index 4318a62a..93d670ef 100644 --- a/test/unit/data/database.test.js +++ b/test/unit/data/database.test.js @@ -166,50 +166,6 @@ describe('/database', () => { await database.connect() }) - describe('getTransferRules', () => { - it('gets the initiator', async () => { - // Arrange - const mockList = mockKnexBuilder( - mockKnex, - [ - { rule: '{"testRule1": true}' }, - { rule: '{"testRule2": true}' } - ], - ['where', 'select'] - ) - const expected = [ - { testRule1: true }, - { testRule2: true } - ] - - // Act - const result = await database.getTransferRules() - - // Assert - expect(result).toStrictEqual(expected) - expect(mockList[0]).toHaveBeenCalledWith('transferRules') - expect(mockList[1]).toHaveBeenCalledWith('enabled', true) - expect(mockList[2]).toHaveBeenCalledTimes(1) - }) - - it('handles a JSON.parse error', async () => { - // Arrange - mockKnexBuilder( - mockKnex, - [ - { rule: '{"invalidJSON: true}' } - ], - ['where', 'select'] - ) - - // Act - const action = async () => database.getTransferRules() - - // Assert - await expect(action()).rejects.toThrowError('Unexpected end of JSON input') - }) - }) - describe('getInitiatorType', () => { it('gets the initiator', async () => { // Arrange @@ -1079,21 +1035,27 @@ describe('/database', () => { const party = {} const amount = 100 const currency = 'AUD' + const enumVals = [ + 'testPartyTypeId', + 'testPartyIdentifierTypeId', + 'testParticipantId', + 'testTransferParticipantRoleTypeId', + 'testLedgerEntryTypeId' + ] database.createQuoteParty = jest.fn() // Act - database.createPayerQuoteParty(txn, quoteId, party, amount, currency) + database.createPayerQuoteParty(txn, quoteId, party, amount, currency, enumVals) // Assert expect(database.createQuoteParty).toHaveBeenCalledWith( txn, quoteId, LibEnum.PAYER, - LibEnum.PAYER_DFSP, - LibEnum.PRINCIPLE_VALUE, party, 100, - 'AUD' + 'AUD', + enumVals ) }) }) @@ -1106,21 +1068,27 @@ describe('/database', () => { const party = {} const amount = 100 const currency = 'AUD' + const enumVals = [ + 'testPartyTypeId', + 'testPartyIdentifierTypeId', + 'testParticipantId', + 'testTransferParticipantRoleTypeId', + 'testLedgerEntryTypeId' + ] database.createQuoteParty = jest.fn() // Act - database.createPayeeQuoteParty(txn, quoteId, party, amount, currency) + database.createPayeeQuoteParty(txn, quoteId, party, amount, currency, enumVals) // Assert expect(database.createQuoteParty).toHaveBeenCalledWith( txn, quoteId, LibEnum.PAYEE, - LibEnum.PAYEE_DFSP, - LibEnum.PRINCIPLE_VALUE, party, -100, - 'AUD' + 'AUD', + enumVals ) }) }) @@ -1128,13 +1096,18 @@ describe('/database', () => { describe('createQuoteParty', () => { const quoteId = 'ddaa67b3-5bf8-45c1-bfcf-1e8781177c37' const partyType = LibEnum.PAYEE - const participantType = LibEnum.PAYEE_DFSP - const ledgerEntryType = LibEnum.PRINCIPLE_VALUE const amount = 100 const currency = 'AUD' const quoteParty = { quotePartyId: 'ddaa67b3-5bf8-45c1-bfcf-1e8781177c37' } + const enumVals = [ + 'testPartyTypeId', + 'testPartyIdentifierTypeId', + 'testParticipantId', + 'testTransferParticipantRoleTypeId', + 'testLedgerEntryTypeId' + ] beforeEach(() => { database.getPartyType = jest.fn().mockResolvedValueOnce('testPartyTypeId') database.getPartyIdentifierType = jest.fn().mockResolvedValueOnce('testPartyIdentifierTypeId') @@ -1188,7 +1161,7 @@ describe('/database', () => { } // Act - const result = await database.createQuoteParty(txn, quoteId, partyType, participantType, ledgerEntryType, party, amount, currency) + const result = await database.createQuoteParty(txn, quoteId, partyType, party, amount, currency, enumVals) // Assert expect(result).toBe('12345') @@ -1234,7 +1207,7 @@ describe('/database', () => { } // Act - const result = await database.createQuoteParty(txn, quoteId, partyType, participantType, ledgerEntryType, party, amount, currency) + const result = await database.createQuoteParty(txn, quoteId, partyType, party, amount, currency, enumVals) // Assert expect(result).toBe('12345') @@ -1292,7 +1265,7 @@ describe('/database', () => { } // Act - const result = await database.createQuoteParty(txn, quoteId, partyType, participantType, ledgerEntryType, party, amount, currency) + const result = await database.createQuoteParty(txn, quoteId, partyType, party, amount, currency, enumVals) // Assert expect(result).toBe('12345') @@ -1317,196 +1290,13 @@ describe('/database', () => { mockKnex.mockImplementationOnce(() => { throw new Error('Test Error') }) // Act - const action = async () => database.createQuoteParty(txn, quoteId, partyType, participantType, ledgerEntryType, party, amount, currency) - - // Assert - await expect(action()).rejects.toThrowError('Test Error') - }) - }) - - describe('getQuotePartyView', () => { - it('gets the quotePartyView', async () => { - // Arrange - const quoteId = 'ddaa67b3-5bf8-45c1-bfcf-1e8781177c37' - const mockList = mockKnexBuilder( - mockKnex, - ['12345'], - ['where', 'select'] - ) - - // Act - const result = await database.getQuotePartyView(quoteId) - - // Assert - expect(result).toStrictEqual(['12345']) - expect(mockList[0]).toHaveBeenCalledWith('quotePartyView') - expect(mockList[1]).toHaveBeenCalledWith({ quoteId }) - expect(mockList[2]).toHaveBeenCalledTimes(1) - }) - - it('handles an exception', async () => { - // Arrange - const quoteId = 'ddaa67b3-5bf8-45c1-bfcf-1e8781177c37' - mockKnex.mockImplementationOnce(() => { throw new Error('Test Error') }) - - // Act - const action = async () => database.getQuotePartyView(quoteId) + const action = async () => database.createQuoteParty(txn, quoteId, partyType, party, amount, currency, enumVals) // Assert await expect(action()).rejects.toThrowError('Test Error') }) }) - describe('getQuoteView', () => { - it('gets the getQuoteView', async () => { - // Arrange - const quoteId = 'ddaa67b3-5bf8-45c1-bfcf-1e8781177c37' - const mockList = mockKnexBuilder( - mockKnex, - ['12345'], - ['where', 'select'] - ) - - // Act - const result = await database.getQuoteView(quoteId) - - // Assert - expect(result).toStrictEqual('12345') - expect(mockList[0]).toHaveBeenCalledWith('quoteView') - expect(mockList[1]).toHaveBeenCalledWith({ quoteId }) - expect(mockList[2]).toHaveBeenCalledTimes(1) - }) - - it('handles the case where the return rows are undefined', async () => { - // Arrange - const quoteId = 'ddaa67b3-5bf8-45c1-bfcf-1e8781177c37' - const mockList = mockKnexBuilder( - mockKnex, - undefined, - ['where', 'select'] - ) - - // Act - const result = await database.getQuoteView(quoteId) - - // Assert - expect(result).toStrictEqual(null) - expect(mockList[0]).toHaveBeenCalledWith('quoteView') - expect(mockList[1]).toHaveBeenCalledWith({ quoteId }) - expect(mockList[2]).toHaveBeenCalledTimes(1) - }) - - it('handles the case where the return rows are empty', async () => { - // Arrange - const quoteId = 'ddaa67b3-5bf8-45c1-bfcf-1e8781177c37' - const mockList = mockKnexBuilder( - mockKnex, - [], - ['where', 'select'] - ) - - // Act - const result = await database.getQuoteView(quoteId) - - // Assert - expect(result).toStrictEqual(null) - expect(mockList[0]).toHaveBeenCalledWith('quoteView') - expect(mockList[1]).toHaveBeenCalledWith({ quoteId }) - expect(mockList[2]).toHaveBeenCalledTimes(1) - }) - - it('handles the case where there is more than 1 row', async () => { - // Arrange - const quoteId = 'ddaa67b3-5bf8-45c1-bfcf-1e8781177c37' - mockKnexBuilder( - mockKnex, - ['12345', '67890'], - ['where', 'select'] - ) - - // Act - const action = async () => database.getQuoteView(quoteId) - - // Assert - await expect(action()).rejects.toThrowError(/Expected 1 row for quoteId .*/) - }) - }) - - describe('getQuoteResponseView', () => { - it('gets the quoteResponseView', async () => { - // Arrange - const quoteId = 'ddaa67b3-5bf8-45c1-bfcf-1e8781177c37' - const mockList = mockKnexBuilder( - mockKnex, - ['12345'], - ['where', 'select'] - ) - - // Act - const result = await database.getQuoteResponseView(quoteId) - - // Assert - expect(result).toStrictEqual('12345') - expect(mockList[0]).toHaveBeenCalledWith('quoteResponseView') - expect(mockList[1]).toHaveBeenCalledWith({ quoteId }) - expect(mockList[2]).toHaveBeenCalledTimes(1) - }) - - it('handles the case where the return rows are undefined', async () => { - // Arrange - const quoteId = 'ddaa67b3-5bf8-45c1-bfcf-1e8781177c37' - const mockList = mockKnexBuilder( - mockKnex, - undefined, - ['where', 'select'] - ) - - // Act - const result = await database.getQuoteResponseView(quoteId) - - // Assert - expect(result).toStrictEqual(null) - expect(mockList[0]).toHaveBeenCalledWith('quoteResponseView') - expect(mockList[1]).toHaveBeenCalledWith({ quoteId }) - expect(mockList[2]).toHaveBeenCalledTimes(1) - }) - - it('handles the case where the return rows are empty', async () => { - // Arrange - const quoteId = 'ddaa67b3-5bf8-45c1-bfcf-1e8781177c37' - const mockList = mockKnexBuilder( - mockKnex, - [], - ['where', 'select'] - ) - - // Act - const result = await database.getQuoteResponseView(quoteId) - - // Assert - expect(result).toStrictEqual(null) - expect(mockList[0]).toHaveBeenCalledWith('quoteResponseView') - expect(mockList[1]).toHaveBeenCalledWith({ quoteId }) - expect(mockList[2]).toHaveBeenCalledTimes(1) - }) - - it('handles the case where there is more than 1 row', async () => { - // Arrange - const quoteId = 'ddaa67b3-5bf8-45c1-bfcf-1e8781177c37' - mockKnexBuilder( - mockKnex, - ['12345', '67890'], - ['where', 'select'] - ) - - // Act - const action = async () => database.getQuoteResponseView(quoteId) - - // Assert - await expect(action()).rejects.toThrowError(/Expected 1 row for quoteId .*/) - }) - }) - describe('createParty', () => { const quotePartyId = '12345' const party = { @@ -1836,83 +1626,6 @@ describe('/database', () => { }) }) - describe('getQuotePartyEndpoint', () => { - it('gets the quote party endpoint', async () => { - // Arrange - const quoteId = 'ddaa67b3-5bf8-45c1-bfcf-1e8781177c37' - const endpointType = 'FSPIOP_CALLBACK_URL_QUOTES' - const partyType = 'PAYEE' - const mockList = mockKnexBuilder( - mockKnex, - [{ value: 'http://localhost:3000/testEndpoint' }], - ['innerJoin', 'innerJoin', 'innerJoin', 'innerJoin', 'where', 'andWhere', 'andWhere', 'andWhere', 'select'] - ) - - // Act - const result = await database.getQuotePartyEndpoint(quoteId, endpointType, partyType) - - // Assert - expect(result).toBe('http://localhost:3000/testEndpoint') - expect(mockList[0]).toHaveBeenCalledWith('participantEndpoint') - expect(mockList[1]).toHaveBeenCalledWith('endpointType', 'participantEndpoint.endpointTypeId', 'endpointType.endpointTypeId') - expect(mockList[2]).toHaveBeenCalledWith('quoteParty', 'quoteParty.participantId', 'participantEndpoint.participantId') - expect(mockList[3]).toHaveBeenCalledWith('partyType', 'partyType.partyTypeId', 'quoteParty.partyTypeId') - expect(mockList[4]).toHaveBeenCalledWith('quote', 'quote.quoteId', 'quoteParty.quoteId') - expect(mockList[5]).toHaveBeenCalledWith('endpointType.name', endpointType) - expect(mockList[6]).toHaveBeenCalledWith('partyType.name', partyType) - expect(mockList[7]).toHaveBeenCalledWith('quote.quoteId', quoteId) - expect(mockList[8]).toHaveBeenCalledWith('participantEndpoint.isActive', 1) - expect(mockList[9]).toHaveBeenCalledWith('participantEndpoint.value') - }) - - it('returns null when the query returns undefined', async () => { - // Arrange - const participantName = 'fsp1' - const endpointType = 'FSPIOP_CALLBACK_URL_QUOTES' - mockKnexBuilder( - mockKnex, - undefined, - ['innerJoin', 'innerJoin', 'innerJoin', 'innerJoin', 'where', 'andWhere', 'andWhere', 'andWhere', 'select'] - ) - - // Act - const result = await database.getQuotePartyEndpoint(participantName, endpointType) - - // Assert - expect(result).toBe(null) - }) - - it('returns null when there are no rows found', async () => { - // Arrange - const participantName = 'fsp1' - const endpointType = 'FSPIOP_CALLBACK_URL_QUOTES' - mockKnexBuilder( - mockKnex, - [], - ['innerJoin', 'innerJoin', 'innerJoin', 'innerJoin', 'where', 'andWhere', 'andWhere', 'andWhere', 'select'] - ) - - // Act - const result = await database.getQuotePartyEndpoint(participantName, endpointType) - - // Assert - expect(result).toBe(null) - }) - - it('handles an exception', async () => { - // Arrange - const participantName = 'fsp1' - const endpointType = 'FSPIOP_CALLBACK_URL_QUOTES' - mockKnex.mockImplementationOnce(() => { throw new Error('Test Error') }) - - // Act - const action = async () => database.getQuotePartyEndpoint(participantName, endpointType) - - // Assert - await expect(action()).rejects.toThrowError('Test Error') - }) - }) - describe('getParticipantEndpoint', () => { it('gets the participant endpoint', async () => { // Arrange @@ -2104,65 +1817,6 @@ describe('/database', () => { }) }) - describe('getTransactionReference', () => { - it('gets the transaction reference', async () => { - // Arrange - const quoteId = 'ddaa67b3-5bf8-45c1-bfcf-1e8781177c37' - const mockList = mockKnexBuilder(mockKnex, ['1'], ['where', 'select']) - - // Act - const result = await database.getTransactionReference(quoteId) - - // Assert - expect(result).toBe('1') - expect(mockList[0]).toHaveBeenCalledWith('transactionReference') - expect(mockList[1]).toHaveBeenCalledWith({ quoteId }) - expect(mockList[2]).toHaveBeenCalledTimes(1) - }) - - it('returns null when the query returns undefined', async () => { - // Arrange - const quoteId = 'ddaa67b3-5bf8-45c1-bfcf-1e8781177c37' - const mockList = mockKnexBuilder(mockKnex, undefined, ['where', 'select']) - - // Act - const result = await database.getTransactionReference(quoteId) - - // Assert - expect(result).toBe(null) - expect(mockList[0]).toHaveBeenCalledWith('transactionReference') - expect(mockList[1]).toHaveBeenCalledWith({ quoteId }) - expect(mockList[2]).toHaveBeenCalledTimes(1) - }) - - it('returns null when there are no rows found', async () => { - // Arrange - const quoteId = 'ddaa67b3-5bf8-45c1-bfcf-1e8781177c37' - const mockList = mockKnexBuilder(mockKnex, [], ['where', 'select']) - - // Act - const result = await database.getTransactionReference(quoteId) - - // Assert - expect(result).toBe(null) - expect(mockList[0]).toHaveBeenCalledWith('transactionReference') - expect(mockList[1]).toHaveBeenCalledWith({ quoteId }) - expect(mockList[2]).toHaveBeenCalledTimes(1) - }) - - it('handles an exception', async () => { - // Arrange - const quoteId = 'ddaa67b3-5bf8-45c1-bfcf-1e8781177c37' - mockKnex.mockImplementationOnce(() => { throw new Error('Test Error') }) - - // Act - const action = async () => database.getTransactionReference(quoteId) - - // Assert - await expect(action()).rejects.toThrowError('Test Error') - }) - }) - describe('createQuoteResponse', () => { const completeQuoteResponse = { transferAmount: { diff --git a/test/unit/lib/config.test.js b/test/unit/lib/config.test.js index d59055fa..25c5c780 100644 --- a/test/unit/lib/config.test.js +++ b/test/unit/lib/config.test.js @@ -92,6 +92,10 @@ const mockDefaultFile = { } } } + }, + CACHE: { + CACHE_ENABLED: true, + EXPIRES_IN_MS: 1000 } } diff --git a/test/unit/lib/util.test.js b/test/unit/lib/util.test.js index b73c3482..a56a96dc 100644 --- a/test/unit/lib/util.test.js +++ b/test/unit/lib/util.test.js @@ -24,13 +24,20 @@ ******/ 'use strict' +jest.mock('@mojaloop/central-services-logger') + const Enum = require('@mojaloop/central-services-shared').Enum jest.mock('axios') const axios = require('axios') +const Logger = require('@mojaloop/central-services-logger') +Logger.isDebugEnabled = jest.fn(() => true) +Logger.isErrorEnabled = jest.fn(() => true) +Logger.isInfoEnabled = jest.fn(() => true) const { failActionHandler, getStackOrInspect, getSpanTags, generateRequestHeaders, generateRequestHeadersForJWS, removeEmptyKeys, fetchParticipantInfo } = require('../../../src/lib/util') const Config = require('../../../src/lib/config.js') +const { Cache } = require('memory-cache') // load config const config = new Config() @@ -518,49 +525,50 @@ describe('util', () => { expect(axios.request.mock.calls[1][0]).toEqual({ url: 'http://localhost:3001/participants/' + mockData.headers['fspiop-destination'] }) }) - it('throws an unhandled exception if the first attempt of `axios.request` throws an exception', async () => { - axios.request - .mockImplementationOnce(() => { throw new Error('foo') }) - - await expect(fetchParticipantInfo(mockData.headers['fspiop-source'], mockData.headers['fspiop-destination'])) - .rejects - .toHaveProperty('message', 'foo') - - expect(axios.request.mock.calls.length).toBe(1) - expect(axios.request.mock.calls[0][0]).toEqual({ url: 'http://localhost:3001/participants/' + mockData.headers['fspiop-source'] }) - }) - - it('throws an unhandled exception if the second attempt of `axios.request` throws an exception', async () => { + it('caches payer and payee when cache is provided', async () => { + const cache = new Cache() + // Arrange + const payer = { data: { accounts: [{ accountId: 1, ledgerAccountType: 'POSITION', isActive: 1 }] } } + const payee = { data: { accounts: [{ accountId: 2, ledgerAccountType: 'POSITION', isActive: 1 }] } } axios.request - .mockImplementationOnce(() => { return { success: true } }) - .mockImplementationOnce(() => { throw new Error('foo') }) - - await expect(fetchParticipantInfo(mockData.headers['fspiop-source'], mockData.headers['fspiop-destination'])) - .rejects - .toHaveProperty('message', 'foo') - + .mockImplementationOnce(() => { return payer }) + .mockImplementationOnce(() => { return payee }) + // Act + const result = await fetchParticipantInfo( + mockData.headers['fspiop-source'], + mockData.headers['fspiop-destination'], + cache + ) + await fetchParticipantInfo( + mockData.headers['fspiop-source'], + mockData.headers['fspiop-destination'], + cache + ) + // Assert + expect(result).toEqual({ payer: payer.data, payee: payee.data }) expect(axios.request.mock.calls.length).toBe(2) expect(axios.request.mock.calls[0][0]).toEqual({ url: 'http://localhost:3001/participants/' + mockData.headers['fspiop-source'] }) expect(axios.request.mock.calls[1][0]).toEqual({ url: 'http://localhost:3001/participants/' + mockData.headers['fspiop-destination'] }) + expect(axios.request.mock.calls[2]).toBeUndefined() + cache.clear() }) - it('throws an unhandled exception if the first attempt of `axios.request` fails', async () => { + it('throws an unhandled exception if the first attempt of `axios.request` throws an exception', async () => { axios.request - .mockImplementationOnce(() => { return Promise.reject(new Error('foo')) }) - .mockImplementationOnce(() => { return Promise.resolve({ ok: true }) }) + .mockImplementationOnce(() => { throw new Error('foo') }) await expect(fetchParticipantInfo(mockData.headers['fspiop-source'], mockData.headers['fspiop-destination'])) .rejects .toHaveProperty('message', 'foo') + expect(axios.request.mock.calls.length).toBe(1) expect(axios.request.mock.calls[0][0]).toEqual({ url: 'http://localhost:3001/participants/' + mockData.headers['fspiop-source'] }) - expect(axios.request.mock.calls[1][0]).toEqual({ url: 'http://localhost:3001/participants/' + mockData.headers['fspiop-destination'] }) }) - it('throws an unhandled exception if the second attempt of `axios.request` fails', async () => { + it('throws an unhandled exception if the second attempt of `axios.request` throws an exception', async () => { axios.request - .mockImplementationOnce(() => { return Promise.resolve({ ok: true }) }) - .mockImplementationOnce(() => { return Promise.reject(new Error('foo')) }) + .mockImplementationOnce(() => { return { success: true } }) + .mockImplementationOnce(() => { throw new Error('foo') }) await expect(fetchParticipantInfo(mockData.headers['fspiop-source'], mockData.headers['fspiop-destination'])) .rejects diff --git a/test/unit/model/bulkQuotes.test.js b/test/unit/model/bulkQuotes.test.js index 7288d9f9..a3789bab 100644 --- a/test/unit/model/bulkQuotes.test.js +++ b/test/unit/model/bulkQuotes.test.js @@ -420,25 +420,13 @@ describe('BulkQuotesModel', () => { }) it('should get http status code 202 Accepted in simple routing mode', async () => { - expect.assertions(2) + expect.assertions(1) mockConfig.simpleRoutingMode = true bulkQuotesModel.db.getParticipantEndpoint.mockReturnValueOnce(mockData.endpoints.payeefsp) await bulkQuotesModel.forwardBulkQuoteRequest(mockData.headers, mockData.bulkQuotePostRequest.bulkQuoteId, mockData.bulkQuotePostRequest, mockChildSpan) expect(bulkQuotesModel.db.getParticipantEndpoint).toBeCalled() - expect(bulkQuotesModel.db.getQuotePartyEndpoint).not.toBeCalled() - }) - it('should throw when participant endpoint is not found', async () => { - expect.assertions(1) - - mockConfig.simpleRoutingMode = false - - bulkQuotesModel.db.getQuotePartyEndpoint.mockReturnValueOnce(undefined) - - await expect(bulkQuotesModel.forwardBulkQuoteRequest(mockData.headers, mockData.bulkQuotePostRequest.bulkQuoteId, mockData.bulkQuotePostRequest)) - .rejects - .toHaveProperty('apiErrorCode.code', ErrorHandler.Enums.FSPIOPErrorCodes.DESTINATION_FSP_ERROR.code) }) }) diff --git a/test/unit/model/quotes.test.js b/test/unit/model/quotes.test.js index 74a3b4a2..895d8615 100644 --- a/test/unit/model/quotes.test.js +++ b/test/unit/model/quotes.test.js @@ -333,6 +333,12 @@ describe('QuotesModel', () => { quotesModel.db.createGeoCode.mockImplementation(() => mockData.geoCode) quotesModel.db.createQuoteExtensions.mockImplementation(() => mockData.quoteRequest.extensionList.extension) + quotesModel.db.getPartyType.mockImplementation(() => 'testPartyTypeId') + quotesModel.db.getPartyIdentifierType.mockImplementation(() => 'testPartyIdentifierTypeId') + quotesModel.db.getTransferParticipantRoleType.mockImplementation(() => 'testTransferParticipantRoleType') + quotesModel.db.getParticipantByName.mockImplementation(() => 'testParticipantId') + quotesModel.db.getLedgerEntryType.mockImplementation(() => 'testLedgerEntryTypeId') + // make all methods of the quotesModel instance be a mock. This helps us re-mock in every // method's test suite. const propertyNames = Object.getOwnPropertyNames(QuotesModel.prototype) @@ -567,14 +573,32 @@ describe('QuotesModel', () => { await quotesModel.validateQuoteRequest(fspiopSource, fspiopDestination, mockData.quoteRequest) + expect(quotesModel.db).toBeTruthy() // Constructor should have been called + expect(quotesModel.db.getParticipant).toHaveBeenCalledTimes(2) + + expect(quotesModel.db.getParticipant.mock.calls[0][0]).toBe(mockData.quoteRequest.payer.partyIdInfo.fspId) + expect(quotesModel.db.getParticipant.mock.calls[1][0]).toBe(mockData.quoteRequest.payee.partyIdInfo.fspId) + }) + it('should validate payer and payee fspId and headers for simple routing mode', async () => { + expect.assertions(7) + + const fspiopSource = 'dfsp123' + const fspiopDestination = 'dfsp234' + + expect(quotesModel.db.getParticipant).not.toHaveBeenCalled() // Validates mockClear() + + await quotesModel.validateQuoteRequest(fspiopSource, fspiopDestination, mockData.quoteRequest) + expect(quotesModel.db).toBeTruthy() // Constructor should have been called if (mockConfig.simpleRoutingMode) { expect(quotesModel.db.getParticipant).toHaveBeenCalledTimes(4) } else { expect(quotesModel.db.getParticipant).toHaveBeenCalledTimes(2) } - expect(quotesModel.db.getParticipant.mock.calls[0][0]).toBe(mockData.quoteRequest.payer.partyIdInfo.fspId) - expect(quotesModel.db.getParticipant.mock.calls[1][0]).toBe(mockData.quoteRequest.payee.partyIdInfo.fspId) + expect(quotesModel.db.getParticipant.mock.calls[0][0]).toBe(fspiopSource) + expect(quotesModel.db.getParticipant.mock.calls[1][0]).toBe(fspiopDestination) + expect(quotesModel.db.getParticipant.mock.calls[2][0]).toBe(mockData.quoteRequest.payer.partyIdInfo.fspId) + expect(quotesModel.db.getParticipant.mock.calls[3][0]).toBe(mockData.quoteRequest.payee.partyIdInfo.fspId) }) it('should throw internal error if no quoteRequest was supplied', async () => { expect.assertions(4) @@ -1135,10 +1159,22 @@ describe('QuotesModel', () => { }] const mockCreatePayerQuotePartyArgs = [mockTransaction, mockData.quoteRequest.quoteId, mockData.quoteRequest.payer, mockData.quoteRequest.amount.amount, - mockData.quoteRequest.amount.currency] + mockData.quoteRequest.amount.currency, [ + 'testPartyTypeId', + 'testPartyIdentifierTypeId', + 'testParticipantId', + 'testTransferParticipantRoleType', + 'testLedgerEntryTypeId' + ]] const mockCreatePayeeQuotePartyArgs = [mockTransaction, mockData.quoteRequest.quoteId, mockData.quoteRequest.payee, mockData.quoteRequest.amount.amount, - mockData.quoteRequest.amount.currency] + mockData.quoteRequest.amount.currency, [ + 'testPartyTypeId', + 'testPartyIdentifierTypeId', + 'testParticipantId', + 'testTransferParticipantRoleType', + 'testLedgerEntryTypeId' + ]] const mockCreateQuoteExtensionsArgs = [mockTransaction, mockData.quoteRequest.extensionList.extension, mockData.quoteRequest.quoteId, @@ -1229,7 +1265,6 @@ describe('QuotesModel', () => { await quotesModel.forwardQuoteRequest(mockData.headers, mockData.quoteRequest.quoteId, mockData.quoteRequest, mockChildSpan) expect(quotesModel.db.getParticipantEndpoint).toBeCalled() - // expect(quotesModel.db.getQuotePartyEndpoint).not.toBeCalled() }) it('should get http status code 202 Accepted in switch mode', async () => { expect.assertions(1) @@ -1240,7 +1275,6 @@ describe('QuotesModel', () => { await quotesModel.forwardQuoteRequest(mockData.headers, mockData.quoteRequest.quoteId, mockData.quoteRequest, mockChildSpan) expect(quotesModel.db.getParticipantEndpoint).toBeCalled() - // expect(quotesModel.db.getQuotePartyEndpoint).toBeCalled() }) it('should throw when quoteRequest is undefined', async () => { expect.assertions(1) @@ -1249,17 +1283,6 @@ describe('QuotesModel', () => { .rejects .toHaveProperty('apiErrorCode.code', ErrorHandler.Enums.FSPIOPErrorCodes.INTERNAL_SERVER_ERROR.code) }) - it('should throw when participant endpoint is not found', async () => { - expect.assertions(1) - - mockConfig.simpleRoutingMode = false - - quotesModel.db.getQuotePartyEndpoint.mockReturnValueOnce(undefined) - - await expect(quotesModel.forwardQuoteRequest(mockData.headers, mockData.quoteRequest.quoteId, mockData.quoteRequest)) - .rejects - .toHaveProperty('apiErrorCode.code', ErrorHandler.Enums.FSPIOPErrorCodes.DESTINATION_FSP_ERROR.code) - }) it('should not use spans when undefined and should throw when participant endpoint is invalid', async () => { expect.assertions(3) mockConfig.simpleRoutingMode = false @@ -1555,7 +1578,7 @@ describe('QuotesModel', () => { expect(mockChildSpan.finish).not.toBeCalled() expect(refs).toEqual(expected) }) - it('should throw partyNotFound error when getQuoteParty coldn\'t find a record in switch mode', async () => { + it('should throw partyNotFound error when getQuoteParty couldn\'t find a record in switch mode', async () => { expect.assertions(4) mockConfig.simpleRoutingMode = false @@ -1571,8 +1594,8 @@ describe('QuotesModel', () => { try { await quotesModel.handleQuoteUpdate(mockData.headers, mockData.quoteId, mockData.quoteUpdate, mockSpan) } catch (err) { - expect(quotesModel.db.newTransaction.mock.calls.length).toBe(1) - expect(mockTransaction.rollback.mock.calls.length).toBe(1) + expect(quotesModel.db.newTransaction.mock.calls.length).toBe(0) + expect(mockTransaction.rollback.mock.calls.length).toBe(0) expect(err instanceof ErrorHandler.Factory.FSPIOPError).toBeTruthy() expect(err.apiErrorCode.code).toBe(ErrorHandler.Enums.FSPIOPErrorCodes.PARTY_NOT_FOUND.code) } @@ -1613,7 +1636,7 @@ describe('QuotesModel', () => { }) it('should get http status code 200 OK in simple routing mode', async () => { - expect.assertions(3) + expect.assertions(2) mockConfig.simpleRoutingMode = true quotesModel.db.getParticipantEndpoint.mockReturnValueOnce(mockData.endpoints.payeefsp) @@ -1622,10 +1645,9 @@ describe('QuotesModel', () => { .toBe(undefined) expect(quotesModel.db.getParticipantEndpoint).toBeCalled() - expect(quotesModel.db.getQuotePartyEndpoint).not.toBeCalled() }) it('should get http status code 200 OK in switch mode', async () => { - expect.assertions(3) + expect.assertions(2) mockConfig.simpleRoutingMode = false quotesModel.db.getParticipantEndpoint.mockReturnValueOnce(mockData.endpoints.payeefsp) @@ -1635,7 +1657,6 @@ describe('QuotesModel', () => { .toBe(undefined) expect(quotesModel.db.getParticipantEndpoint).toBeCalled() - expect(quotesModel.db.getQuotePartyEndpoint).not.toBeCalled() }) it('should throw when quoteUpdate is undefined', async () => { expect.assertions(1) @@ -1644,18 +1665,6 @@ describe('QuotesModel', () => { .rejects .toHaveProperty('apiErrorCode.code', ErrorHandler.Enums.FSPIOPErrorCodes.INTERNAL_SERVER_ERROR.code) }) - it('should throw when participant endpoint is not found', async () => { - expect.assertions(1) - - mockConfig.simpleRoutingMode = false - const endpoint = undefined - quotesModel.db.getQuotePartyEndpoint.mockReturnValueOnce(endpoint) - quotesModel.sendErrorCallback = jest.fn((_, fspiopError) => { throw fspiopError }) - - await expect(quotesModel.forwardQuoteUpdate(mockData.headers, mockData.quoteId, mockData.quoteUpdate, mockChildSpan)) - .rejects - .toHaveProperty('apiErrorCode.code', ErrorHandler.Enums.FSPIOPErrorCodes.DESTINATION_FSP_ERROR.code) - }) it('should not use spans when undefined and should throw when participant endpoint is invalid', async () => { expect.assertions(3)