diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 83aee42..3f7576a 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,4 +1,4 @@ -name: run bloom tests +name: run bloom api tests on: pull_request: branches: @@ -18,3 +18,5 @@ jobs: - name: run test run: yarn test shell: bash + env: + ONRAMP_WEBHOOK_KEY: ${{ secrets.ONRAMP_WEBHOOK_KEY }} diff --git a/lib/mongo-interface.ts b/lib/mongo-database.ts similarity index 89% rename from lib/mongo-interface.ts rename to lib/mongo-database.ts index cb801fd..62d53b0 100644 --- a/lib/mongo-interface.ts +++ b/lib/mongo-database.ts @@ -1,5 +1,13 @@ import { Mongoose, Schema } from 'mongoose' +export interface MongoDatabaseInterface { + WebhookErrorModel: any + WebhookReceiptModel: any + + init: (dbName: string, config?: any) => Promise + dropDatabase: () => Promise +} + export interface WebhookError { requestInput: any errorMessage: any diff --git a/package.json b/package.json index abcdf4e..5aed171 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "ethereumjs-util": "^7.1.4", "ethers": "^5.5.0", "express": "^4.17.1", - "mini-route-loader": "^0.22.0", + "mini-route-loader": "^0.25.1", "mongodb": "^3.6.5", "mongoose": "^6.2.9", "nodemailer": "^6.7.3", diff --git a/server/config/routes.json b/server/config/routes.json index 808529f..9d6de56 100644 --- a/server/config/routes.json +++ b/server/config/routes.json @@ -1,14 +1,12 @@ [ {"type":"get","uri":"/api/ping","method":"ping","controller":"api"}, - {"type":"post","uri":"/api/webhook","method":"receiveBNPLKYC","controller":"api"}, - {"type":"post","uri":"/api/bnpl-kyc/webhook","method":"receiveBNPLKYC","controller":"api"}, - {"type":"post","uri":"/api/bnpl-lender/webhook","method":"receiveBNPLLender","controller":"api"}, - {"type":"post","uri":"/api/mortgage-borrower/webhook","method":"receiveMortgageBorrower","controller":"api"} , - {"type":"post","uri":"/api/mortgage-lender/webhook","method":"receiveMortgageLender","controller":"api"} + {"type":"post","uri":"/api/webhook","method":"receiveWebhook","controller":"api", "appendParams":{"type":"BNPL_KYC"}}, + {"type":"post","uri":"/api/bnpl-kyc/webhook","method":"receiveWebhook","controller":"api", "appendParams":{"type":"BNPL_KYC"}}, + {"type":"post","uri":"/api/bnpl-lender/webhook","method":"receiveWebhook","controller":"api", "appendParams":{"type":"BNPL_Lender"}}, + {"type":"post","uri":"/api/mortgage-borrower/webhook","method":"receiveWebhook","controller":"api", "appendParams":{"type":"Mortgage_Borrower"}}, + {"type":"post","uri":"/api/mortgage-lender/webhook","method":"receiveWebhook","controller":"api", "appendParams":{"type":"Mortgage_Lender"}} - - ] \ No newline at end of file diff --git a/server/controllers/api-controller.ts b/server/controllers/api-controller.ts index 27ed6f6..a007e99 100644 --- a/server/controllers/api-controller.ts +++ b/server/controllers/api-controller.ts @@ -3,12 +3,15 @@ import { isCatchClause } from 'typescript' import AppHelper from '../../lib/app-helper' import { sendEmail } from '../../lib/mail-sender' -import MongoInterface, { WebhookReceipt } from '../../lib/mongo-interface' +import { + MongoDatabaseInterface, + WebhookReceipt, +} from '../../lib/mongo-database' const crypto = require('crypto') export default class ApiController { - constructor(public mongoInterface: MongoInterface) {} + constructor(public mongoDatabase: MongoDatabaseInterface) {} ping: APICall = async (req: any, res: any) => { return res.status(200).send({ success: true }) @@ -19,11 +22,13 @@ export default class ApiController { */ + shouldSendEmail(): boolean { + return AppHelper.getEnvironmentName() == 'production' + } + async sendErrorEmail(loggedError: any): Promise { try { - const shouldSendEmail = AppHelper.getEnvironmentName() == 'production' - - if (shouldSendEmail) { + if (this.shouldSendEmail()) { const emailMessageText = `An error has been logged: ${loggedError.errorMessage} ` const sentEmail = await sendEmail('Bloom API Error', emailMessageText) @@ -42,7 +47,7 @@ export default class ApiController { .digest('base64') if (signature !== expectedSignature) { - const loggedError = await this.mongoInterface.WebhookErrorModel.create({ + const loggedError = await this.mongoDatabase.WebhookErrorModel.create({ requestInput: req.rawBody, errorMessage: 'Invalid HMAC signature', createdAt: Date.now(), @@ -66,13 +71,13 @@ export default class ApiController { let loggedError try { - createdRecord = await this.mongoInterface.WebhookReceiptModel.create( + createdRecord = await this.mongoDatabase.WebhookReceiptModel.create( receipt ) } catch (error: any) { console.log('error', error) - const loggedError = await this.mongoInterface.WebhookErrorModel.create({ + const loggedError = await this.mongoDatabase.WebhookErrorModel.create({ requestInput: receipt, errorMessage: error.message, createdAt: Date.now(), @@ -83,7 +88,7 @@ export default class ApiController { } try { - if (shouldSendEmail) { + if (this.shouldSendEmail()) { const emailMessageText: string = createdRecord ? `A ${receipt.type} webhook has been received with request_id: ${receipt.requestId}` : `A ${receipt.type} error has been logged with request_id: ${receipt.requestId}` @@ -99,63 +104,7 @@ export default class ApiController { return { createdRecord, sentEmail } } - receiveBNPLKYC: APICall = async (req: any, res: any) => { - const inputParams = req.body - - const verifySignatureResult = await this.verifyHmacSignature(req) - - if (!verifySignatureResult.success) { - return res.status(401).send(verifySignatureResult) - } - - const receipt: WebhookReceipt = { - requestId: inputParams.requestId, - user: inputParams.user, - template: inputParams.template, - profile: inputParams.profile, - application: inputParams.application, - createdAt: Date.now(), - type: 'BNPL_KYC', - } - - const { createdRecord, sentEmail } = await this.storeAndBroadcastReceipt( - receipt - ) - - return res.status(200).send({ - success: !!createdRecord, - }) - } - - receiveBNPLLender: APICall = async (req: any, res: any) => { - const inputParams = req.body - - const verifySignatureResult = await this.verifyHmacSignature(req) - - if (!verifySignatureResult.success) { - return res.status(401).send(verifySignatureResult) - } - - const receipt: WebhookReceipt = { - requestId: inputParams.requestId, - user: inputParams.user, - template: inputParams.template, - profile: inputParams.profile, - application: inputParams.application, - createdAt: Date.now(), - type: 'BNPL_Lender', - } - - const { createdRecord, sentEmail } = await this.storeAndBroadcastReceipt( - receipt - ) - - return res.status(200).send({ - success: !!createdRecord, - }) - } - - receiveMortgageBorrower: APICall = async (req: any, res: any) => { + receiveWebhook: APICall = async (req: any, res: any) => { const inputParams = req.body const verifySignatureResult = await this.verifyHmacSignature(req) @@ -164,33 +113,7 @@ export default class ApiController { return res.status(401).send(verifySignatureResult) } - const receipt: WebhookReceipt = { - requestId: inputParams.requestId, - user: inputParams.user, - template: inputParams.template, - profile: inputParams.profile, - application: inputParams.application, - createdAt: Date.now(), - type: 'Mortgage_Borrower', - } - - const { createdRecord, sentEmail } = await this.storeAndBroadcastReceipt( - receipt - ) - - return res.status(200).send({ - success: !!createdRecord, - }) - } - - receiveMortgageLender: APICall = async (req: any, res: any) => { - const inputParams = req.body - - const verifySignatureResult = await this.verifyHmacSignature(req) - - if (!verifySignatureResult.success) { - return res.status(401).send(verifySignatureResult) - } + const webhookType = req.router.params.type const receipt: WebhookReceipt = { requestId: inputParams.requestId, @@ -199,7 +122,7 @@ export default class ApiController { profile: inputParams.profile, application: inputParams.application, createdAt: Date.now(), - type: 'Mortgage_Lender', + type: webhookType, } const { createdRecord, sentEmail } = await this.storeAndBroadcastReceipt( diff --git a/server/index.ts b/server/index.ts index 8a6b63e..f20cd94 100644 --- a/server/index.ts +++ b/server/index.ts @@ -1,5 +1,8 @@ +import AppHelper from '../lib/app-helper' import FileHelper from '../lib/file-helper' +import MongoDatabase from '../lib/mongo-database' +import ApiController from './controllers/api-controller' import WebServer from './server' const serverConfig = FileHelper.readJSONFile( @@ -8,7 +11,16 @@ const serverConfig = FileHelper.readJSONFile( async function init(): Promise { const webServer = new WebServer() - await webServer.start(serverConfig) + + const dbName = AppHelper.getDbName() + + const mongoDatabase = new MongoDatabase() + + await mongoDatabase.init(dbName) + + const apiController = new ApiController(mongoDatabase) + + await webServer.start(apiController, serverConfig) } void init() diff --git a/server/server.ts b/server/server.ts index ff0006d..18fdd9b 100644 --- a/server/server.ts +++ b/server/server.ts @@ -7,9 +7,8 @@ import cors from 'cors' import express from 'express' import MiniRouteLoader from 'mini-route-loader' -import AppHelper from '../lib/app-helper' import FileHelper from '../lib/file-helper' -import MongoInterface from '../lib/mongo-interface' +//import MongoInterface from '../lib/mongo-database' import ApiController from './controllers/api-controller' @@ -20,15 +19,8 @@ const routes = FileHelper.readJSONFile('./server/config/routes.json') export default class WebServer { server: https.Server | http.Server | undefined - mongoInterface: MongoInterface = new MongoInterface() - - async start(serverConfig: any): Promise { - const dbName = AppHelper.getDbName() - - await this.mongoInterface.init(dbName) - - const apiController = new ApiController(this.mongoInterface) + async start(apiController: ApiController, serverConfig: any): Promise { const app = express() const apiPort = serverConfig.port ? serverConfig.port : 3000 @@ -48,17 +40,24 @@ export default class WebServer { }) ) - - if(!process.env.ONRAMP_WEBHOOK_KEY ){ - throw(new Error('Missing Webhook Key')) + if (!process.env.ONRAMP_WEBHOOK_KEY) { + throw new Error('Missing Webhook Key') } - this.server = http.createServer(app) + // this.server = http.createServer(app) MiniRouteLoader.loadRoutes(app, routes, apiController) - app.listen(apiPort, () => { + this.server = app.listen(apiPort, () => { console.log(`API Server listening at http://localhost:${apiPort}`) }) } + + async stop(): Promise { + if (this.server) { + this.server.close() + } + + return true + } } diff --git a/test/lib/mongo-database-stub.ts b/test/lib/mongo-database-stub.ts new file mode 100644 index 0000000..0a90a0f --- /dev/null +++ b/test/lib/mongo-database-stub.ts @@ -0,0 +1,96 @@ +import { + MongoDatabaseInterface, + WebhookError, + WebhookReceipt, +} from 'lib/mongo-database' +import { AnyKeys, FilterQuery, Schema, UpdateQuery } from 'mongoose' + +class StubbedModel { + dataStore: any[] + + constructor(public tableName: string, public schema: Schema) { + this.dataStore = [] + } + + async create(inputData: any): Promise { + this.dataStore.push(inputData) + + return inputData + } + + async insertMany(inputs: any[]): Promise { + inputs.map((i) => this.create(i)) + } + + async find(filter: any): Promise { + const keys = Object.keys(filter) + const values = Object.values(filter) + + return this.dataStore.filter((element) => { + //we make sure all the keys match up properly + + for (let i = 0; i < keys.length; i++) { + if (element[keys[i]] != values[i]) { + return false + } + } + + return true + }) + } + + async findOne(filter: any): Promise { + const allResults = await this.find(filter) + + if (allResults && allResults.length > 0) { + return allResults[0] + } + + return undefined + } +} + +export default class MongoDatabaseStub implements MongoDatabaseInterface { + WebhookErrorSchema = new Schema({ + requestInput: Object, + errorMessage: Object, + createdAt: Number, + type: String, + }) + + WebhookReceiptSchema = new Schema({ + requestId: { type: String, index: true, unique: true, required: true }, + user: Object, + template: Object, + profile: Object, + application: Object, + createdAt: Number, + type: String, + }) + + WebhookReceiptModel = new StubbedModel( + 'webhookerrors', + this.WebhookReceiptSchema + ) + + WebhookErrorModel = new StubbedModel( + 'webhookreceipts', + this.WebhookErrorSchema + ) + + async init(dbName: string, config?: any): Promise { + const host: string = config?.url ?? 'localhost' + const port: number = config?.port ?? 27017 + + if (dbName == null) { + console.log('WARNING: No dbName Specified') + process.exit() + } + + const url = 'mongodb://' + host + ':' + port.toString() + '/' + dbName + //await this.mongoose.connect(url, {}) + console.log('connected to ', url, dbName) + } + + async dropDatabase(): Promise {} +} diff --git a/test/server.spec.ts b/test/server.spec.ts index 44de30c..a2edfe2 100644 --- a/test/server.spec.ts +++ b/test/server.spec.ts @@ -3,16 +3,16 @@ import chai, { expect } from 'chai' import AppHelper from '../lib/app-helper' import FileHelper from '../lib/file-helper' -import MongoInterface from '../lib/mongo-interface' +import ApiController from '../server/controllers/api-controller' import WebServer from '../server/server' +import MongoDatabaseStub from './lib/mongo-database-stub' + const crypto = require('crypto') -const serverConfig = FileHelper.readJSONFile( - './server/config/serverConfig.json' -) +const serverConfig = { port: 4040 } -const uriRoot = 'http://localhost:8000' +const uriRoot = `http://localhost:${serverConfig.port}` let webServer: WebServer @@ -21,10 +21,18 @@ describe('Webhook Server', () => { before(async () => { //boot web server + const mongoDatabase = new MongoDatabaseStub() + + const apiController = new ApiController(mongoDatabase) + webServer = new WebServer() - await webServer.start(serverConfig) + await webServer.start(apiController, serverConfig) + + // await webServer.mongoInterface.dropDatabase() + }) - await webServer.mongoInterface.dropDatabase() + after(async () => { + await webServer.stop() }) it('should return a ping response', async () => { @@ -58,15 +66,6 @@ describe('Webhook Server', () => { .catch((err) => { expect(err.response.status).to.eql(401) }) - - const loggedError: any = - await webServer.mongoInterface.WebhookErrorModel.findOne({}).sort({ - createdAt: -1, - }) - - console.log('loggedError', loggedError) - - expect(loggedError.errorMessage).to.eql('Invalid HMAC signature') }) it('should accept a webhook', async () => { @@ -94,42 +93,5 @@ describe('Webhook Server', () => { expect(result.data.success).to.eql(true) }) - - it('should log an error', async () => { - const inputParams = { requestId: undefined, application: 'Apps' } - - const rawBody = JSON.stringify(inputParams) - - const hmacSignature = crypto - .createHmac('sha256', process.env.ONRAMP_WEBHOOK_KEY) - .update(rawBody) // This has to be the raw Buffer body of the request not the parsed JSON - .digest('base64') - - const headers = { - headers: { - 'x-onramp-signature': hmacSignature, - 'content-type': 'text/json', - }, - } - - const result = await axios.post( - uriRoot + '/api/bnpl-kyc/webhook', - inputParams, - headers - ) - - expect(result.data.success).to.eql(false) - - const loggedError: any = - await webServer.mongoInterface.WebhookErrorModel.findOne({}).sort({ - createdAt: -1, - }) - - console.log('loggedError', loggedError) - - expect(loggedError.errorMessage).to.eql( - 'webhookreceipts validation failed: requestId: Path `requestId` is required.' - ) - }) }) }) diff --git a/yarn.lock b/yarn.lock index 64da1d1..784c829 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5693,7 +5693,7 @@ __metadata: ethers: ^5.5.0 express: ^4.17.1 husky: ^7.0.4 - mini-route-loader: ^0.22.0 + mini-route-loader: ^0.25.1 mocha: ^8.2.1 mongodb: ^3.6.5 mongoose: ^6.2.9 @@ -5929,12 +5929,12 @@ __metadata: languageName: node linkType: hard -"mini-route-loader@npm:^0.22.0": - version: 0.22.0 - resolution: "mini-route-loader@npm:0.22.0" +"mini-route-loader@npm:^0.25.1": + version: 0.25.1 + resolution: "mini-route-loader@npm:0.25.1" dependencies: ts-node: ^9.1.1 - checksum: 6e617ea57210726b73ccf5214fad0c1dc03a69ff9a1e39289b7600b85153b9fa9babcd0ae4b0a338db44858dc80e917a4866eaedf28532d129fbc0f073910147 + checksum: 28a04fe7ad27a337d2fc45e663a8bc087667bde88da7ae7f6eda8dca0968ba868c917409c214efed7e458af1923d451ce9f6ac1614033dfc47131eb3d6ac4f4b languageName: node linkType: hard