/** * Transfer Service - Webhook Unit Tests */ import { IDynamoDbEvent, IRecord, ISns, ISnsEvent, KushkiError, } from "@kushki/core"; import { Context } from "aws-lambda"; import { expect } from "chai"; import { IDENTIFIERS } from "constant/Identifiers"; import * as faker from "faker"; import { DynamoGateway } from "gateway/DynamoGateway"; import { FireHoseGateway } from "gateway/FireHoseGateway"; import { SNSGateway } from "gateway/SNSGateway"; import { IncomingMessage } from "http"; import { CONTAINER } from "infrastructure/Container"; import { ERRORS } from "infrastructure/ErrorEnum"; import { TransactionStatusEnum } from "infrastructure/TransactionStatusEnum"; import * as jsf from "json-schema-faker"; import * as path from "path"; import { IKushkiService } from "repository/IKushkiService"; import { ITransferService } from "repository/ITransferService"; import { StatusCodeError } from "request-promise/errors"; import { from, of } from "rxjs"; import { delay } from "rxjs/operators"; import { KushkiService } from "service/KushkiService"; import { createSandbox, SinonSandbox, SinonSpy, SinonStub } from "sinon"; import { Mock } from "ts-mockery"; import { DynamoTransactionSns } from "types/dynamo_transaction_sns"; import { MerchantObject } from "types/merchant_object"; import { SnsEventTrigger } from "types/sns_event_trigger"; import { TransactionFetch } from "types/transaction_fetch"; let gSandbox: SinonSandbox; let gPublishStub: SinonStub; let gService: ITransferService; let gMerchantObjectStub: SinonStub; const SERVER_ERROR: number = 500; const CLIENT_ERROR: number = 400; let gTransactionFetchNew: TransactionFetch; const SCHEMA_PATH: string = "src/schema"; let gTransactionFetchOld: TransactionFetch; let gDynamoTransactionSns: DynamoTransactionSns; let gContext: Context; const MERCHANT_URL: string = "www.google.com"; let gMerchantObject: MerchantObject; let gSnsEventTrigger: SnsEventTrigger; const ERROR_MESSAGE: string = "this must throw an error"; let gKushki: IKushkiService; let gWebhookSnsEvent: ISnsEvent; let gQueueWebhookEvent: IDynamoDbEvent; function stubAWSWebHook(): void { gMerchantObjectStub.restore(); gSandbox .stub(DynamoGateway.prototype, "getItem") .returns(from([gTransactionFetchNew])); } async function randomSchemas(): Promise { gMerchantObject = await jsf.resolve( { $ref: "merchant_object.json", }, null, path.resolve(SCHEMA_PATH) ); gMerchantObject.serviceCode = "0"; gMerchantObject.entityCode = "1"; gTransactionFetchNew = await jsf.resolve( { $ref: "transaction_fetch.json", }, null, path.resolve(SCHEMA_PATH) ); gTransactionFetchOld = await jsf.resolve( { $ref: "transaction_fetch.json", }, null, path.resolve(SCHEMA_PATH) ); gDynamoTransactionSns = await jsf.resolve( { $ref: "dynamo_transaction_sns.json", }, null, path.resolve(SCHEMA_PATH) ); gSnsEventTrigger = await jsf.resolve( { $ref: "sns_event_trigger.json", }, null, path.resolve(SCHEMA_PATH) ); gService = CONTAINER.get(IDENTIFIERS.TransferService); gSandbox = createSandbox(); } function webHookEvents(): [ IDynamoDbEvent, ISnsEvent ] { return [ Mock.of>({ Records: [ { dynamodb: { ApproximateCreationDateTime: 0, NewImage: gTransactionFetchNew, OldImage: gTransactionFetchOld, SizeBytes: 0, Keys: {}, SequenceNumber: "", StreamViewType: "", }, }, ], }), Mock.of>({ Records: [ Mock.of>({ Sns: Mock.of>({ Message: gDynamoTransactionSns, }), }), ], }), ]; } function testSuccessWebHook( done: Mocha.Done, callCount: number, nextCount: number, webHookEvent: IDynamoDbEvent ): void { gPublishStub.restore(); gMerchantObject.url = [MERCHANT_URL]; const delay_time: number = 100; const stub: SinonStub = gSandbox .stub(SNSGateway.prototype, "publish") .onFirstCall() .returns(of({ MessageId: "test1" }).pipe(delay(delay_time))) .onSecondCall() .returns(of({ MessageId: "test2" })); const spy_next: SinonSpy = gSandbox.spy(); gService .queueWebhooks(webHookEvent, gContext) .subscribe(spy_next, done, () => { expect(spy_next.callCount).to.be.eq(nextCount); expect(stub.callCount).to.be.eq(callCount); done(); }); } function testWebhookError007( queueWebhookEvent: IDynamoDbEvent, done: Mocha.Done ): void { queueWebhookEvent.Records[0].dynamodb.NewImage.status = TransactionStatusEnum.DeclinedTransaction; gService.queueWebhooks(queueWebhookEvent, gContext).subscribe( () => { done(ERROR_MESSAGE); }, (err: KushkiError) => { expect(err.code).to.be.eq(new KushkiError(ERRORS.E007).code); done(); } ); } function webhookError( mockObj: TransactionFetch | undefined, done: Mocha.Done ): void { gSnsEventTrigger.Records[0].Sns.Message = JSON.stringify({ transaction: gTransactionFetchNew, url: MERCHANT_URL, }); gSandbox .stub(KushkiService.prototype, "buildWebHookRequest") .returns(Promise.reject("error")); gMerchantObjectStub.restore(); gSandbox.stub(DynamoGateway.prototype, "getItem").returns(from([mockObj])); gService .webhook(gWebhookSnsEvent, gContext) .subscribe((result: boolean | object) => { expect(result).to.be.true; done(); }, done); } describe("Webhooks logic - ", () => { const call_twice: number = 2; const call_once: number = 1; beforeEach(async () => { await randomSchemas(); gService = CONTAINER.get(IDENTIFIERS.TransferService); gSandbox = createSandbox(); gKushki = CONTAINER.get(IDENTIFIERS.KushkiService); }); beforeEach(() => { gContext = Mock.of(); [gQueueWebhookEvent, gWebhookSnsEvent] = webHookEvents(); gMerchantObjectStub = gSandbox .stub(DynamoGateway.prototype, "getItem") .returns(of(gMerchantObject)); gSandbox.stub(FireHoseGateway.prototype, "put").returns(of(true)); gPublishStub = gSandbox .stub(SNSGateway.prototype, "publish") .returns(of(true)); }); afterEach(() => { gSandbox.restore(); }); it("test queueWebhook - success response with one record", (done: Mocha.Done) => { if (typeof gQueueWebhookEvent.Records[0].dynamodb.NewImage === null) throw TypeError("NewImage is null"); gQueueWebhookEvent.Records[0].dynamodb.NewImage.status = TransactionStatusEnum.ApprovedTransaction; gQueueWebhookEvent.Records[0].dynamodb.NewImage.amount.extraTaxes = { agenciaDeViajes: faker.random.number({ min: 1, max: 10 }), }; gQueueWebhookEvent.Records[0].dynamodb.NewImage.returnCode = "SUCCESS"; gQueueWebhookEvent.Records[0].dynamodb.NewImage.paymentDescription = "payment description"; gQueueWebhookEvent.Records[0].dynamodb.NewImage.trazabilityCode = "123456"; gQueueWebhookEvent.Records[0].dynamodb.NewImage.ticketNumber = "123456789009876543"; testSuccessWebHook(done, 1, 1, gQueueWebhookEvent); }); it("test queueWebhook - success response with multiple records, same state", (done: Mocha.Done) => { gQueueWebhookEvent.Records[0].dynamodb.NewImage.status = TransactionStatusEnum.ApprovedTransaction; gQueueWebhookEvent.Records.push(gQueueWebhookEvent.Records[0]); testSuccessWebHook(done, call_twice, call_twice, gQueueWebhookEvent); }); it("test queueWebhook - success response with multiple records, different state", (done: Mocha.Done) => { gQueueWebhookEvent.Records[0].dynamodb.NewImage.status = TransactionStatusEnum.ApprovedTransaction; gQueueWebhookEvent.Records.push({ ...gQueueWebhookEvent.Records[0], dynamodb: { ...gQueueWebhookEvent.Records[0].dynamodb, NewImage: { ...gQueueWebhookEvent.Records[0].dynamodb.NewImage, status: TransactionStatusEnum.RequestedToken, }, }, }); testSuccessWebHook(done, call_once, call_twice, gQueueWebhookEvent); }); it("test queueWebhook - declined response", (done: Mocha.Done) => { gMerchantObject.url = [MERCHANT_URL]; gQueueWebhookEvent.Records[0].dynamodb.NewImage.status = TransactionStatusEnum.DeclinedTransaction; gService.queueWebhooks(gQueueWebhookEvent, gContext).subscribe(() => { expect(gPublishStub.callCount).to.be.eq(1); done(); }, done); }); it("test queueWebhook - no relevant change", (done: Mocha.Done) => { gQueueWebhookEvent.Records[0].dynamodb.NewImage.status = TransactionStatusEnum.InitializedTransaction; gService.queueWebhooks(gQueueWebhookEvent, gContext).subscribe(() => { expect(gPublishStub.callCount).to.be.eq(0); done(); }, done); }); it("test queueWebhook - no merchant url", (done: Mocha.Done) => { gMerchantObject.url = undefined; gQueueWebhookEvent.Records[0].dynamodb.NewImage.status = TransactionStatusEnum.ApprovedTransaction; gService.queueWebhooks(gQueueWebhookEvent, gContext).subscribe( () => { done("next should not be called"); }, done, done ); }); it("test queueWebhook - error response", (done: Mocha.Done) => { delete gMerchantObject.publicMerchantId; testWebhookError007(gQueueWebhookEvent, done); }); it("test queueWebhook - error response with merchant as undefined", (done: Mocha.Done) => { gMerchantObjectStub.resetBehavior(); gMerchantObjectStub.returns(of(undefined)); testWebhookError007(gQueueWebhookEvent, done); }); it("test webhook - success response", (done: Mocha.Done) => { gSnsEventTrigger.Records[0].Sns.Message = JSON.stringify({ transaction: gTransactionFetchNew, url: MERCHANT_URL, }); gSandbox .stub(KushkiService.prototype, "buildWebHookRequest") .returns(from([true])); gService.webhook(gWebhookSnsEvent, gContext).subscribe(() => { expect(gPublishStub.callCount).to.be.eq(0); done(); }, done); }); it("test webhook - success declined response ", (done: Mocha.Done) => { gMerchantObjectStub.restore(); gSandbox .stub(DynamoGateway.prototype, "getItem") .returns(from([gTransactionFetchNew])); gSandbox .stub(KushkiService.prototype, "buildWebHookRequest") .returns(from([true])); gSnsEventTrigger.Records[0].Sns.Message = JSON.stringify({ transaction: gTransactionFetchNew, url: MERCHANT_URL, }); gWebhookSnsEvent.Records[0].Sns.Message.transaction.status = TransactionStatusEnum.DeclinedTransaction; gService.webhook(gWebhookSnsEvent, gContext).subscribe(() => { expect(true).to.be.a("boolean"); done(); }, done); }); it("test webhook - error response from buildWebHookRequest", (done: Mocha.Done) => { webhookError(gTransactionFetchNew, done); }); it("test webhook - error response with getItem output as undefined", (done: Mocha.Done) => { webhookError(undefined, done); }); it("test webhook - error response with invalid merchant", (done: Mocha.Done) => { delete gTransactionFetchNew.publicMerchantId; webhookError(gTransactionFetchNew, done); }); it("test webhook - http error response 500", (done: Mocha.Done) => { gSnsEventTrigger.Records[0].Sns.Message = JSON.stringify({ transaction: gTransactionFetchNew, url: MERCHANT_URL, }); gSandbox .stub(KushkiService.prototype, "buildWebHookRequest") .returns( Promise.reject( new StatusCodeError( SERVER_ERROR, {}, { url: "" }, Mock.of() ) ) ); stubAWSWebHook(); gWebhookSnsEvent.Records[0].Sns.Message.transaction.created = gKushki.getTime(); gService .webhook(gWebhookSnsEvent, gContext) .subscribe((result: boolean | object) => { expect(result).to.be.true; done(); }); }); it("test webhook - http error response not 500", (done: Mocha.Done) => { gSandbox .stub(KushkiService.prototype, "buildWebHookRequest") .returns( Promise.reject( new StatusCodeError( CLIENT_ERROR, {}, { url: "" }, Mock.of() ) ) ); gSnsEventTrigger.Records[0].Sns.Message = JSON.stringify({ url: MERCHANT_URL, transaction: gTransactionFetchNew, }); gMerchantObjectStub.restore(); gSandbox .stub(DynamoGateway.prototype, "getItem") .returns(from([gTransactionFetchNew])); gService .webhook(gWebhookSnsEvent, gContext) .subscribe((res: boolean | object) => { expect(res).to.be.true; done(); }); }); });