From 9acef9663a3c6caa779880ce97d20447c90f4051 Mon Sep 17 00:00:00 2001 From: Guilherme Ribeiro Date: Wed, 10 Jan 2024 10:11:34 -0300 Subject: [PATCH] Click to Pay - Adding unit tests (#2502) * feat: mc test * feat: more tests * logout tests * test for ctplogin --- .../components/CtPLogin/CtpLogin.test.tsx | 120 ++++++ .../CtPSection/CtPLogoutLink.test.tsx | 88 ++++ .../components/CtPSection/CtPLogoutLink.tsx | 4 +- .../services/sdks/MastercardSdk.test.ts | 398 ++++++++++++++++++ .../services/sdks/SrcSdkLoader.test.ts | 58 +++ .../ClickToPay/services/sdks/SrcSdkLoader.ts | 2 +- .../ClickToPay/services/sdks/SrciError.ts | 4 +- .../ClickToPay/services/sdks/VisaSdk.test.ts | 177 ++++++++ .../ClickToPay/services/sdks/types.ts | 9 + yarn.lock | 8 +- 10 files changed, 859 insertions(+), 9 deletions(-) create mode 100644 packages/lib/src/components/internal/ClickToPay/components/CtPLogin/CtpLogin.test.tsx create mode 100644 packages/lib/src/components/internal/ClickToPay/components/CtPSection/CtPLogoutLink.test.tsx create mode 100644 packages/lib/src/components/internal/ClickToPay/services/sdks/MastercardSdk.test.ts create mode 100644 packages/lib/src/components/internal/ClickToPay/services/sdks/SrcSdkLoader.test.ts create mode 100644 packages/lib/src/components/internal/ClickToPay/services/sdks/VisaSdk.test.ts diff --git a/packages/lib/src/components/internal/ClickToPay/components/CtPLogin/CtpLogin.test.tsx b/packages/lib/src/components/internal/ClickToPay/components/CtPLogin/CtpLogin.test.tsx new file mode 100644 index 0000000000..0a47c5c6fd --- /dev/null +++ b/packages/lib/src/components/internal/ClickToPay/components/CtPLogin/CtpLogin.test.tsx @@ -0,0 +1,120 @@ +import { ComponentChildren, h } from 'preact'; +import { ClickToPayContext, IClickToPayContext } from '../../context/ClickToPayContext'; +import { render, screen } from '@testing-library/preact'; +import CoreProvider from '../../../../../core/Context/CoreProvider'; +import { mock } from 'jest-mock-extended'; +import CtPLogin from './CtPLogin'; +import userEvent from '@testing-library/user-event'; +import SrciError from '../../services/sdks/SrciError'; + +const customRender = (children: ComponentChildren, providerProps: IClickToPayContext) => { + return render( + // @ts-ignore TODO: Fix this weird complain + + {/* eslint-disable-next-line react/no-children-prop */} + + + ); +}; + +test('should set CTP to primary payment method if shopper interacts with the login input', async () => { + const user = userEvent.setup(); + + const contextProps = mock(); + contextProps.isCtpPrimaryPaymentMethod = false; + contextProps.schemes = ['mc', 'visa']; + contextProps.setIsCtpPrimaryPaymentMethod.mockImplementation(isPrimary => { + contextProps.isCtpPrimaryPaymentMethod = isPrimary; + }); + + const { rerender } = customRender(, contextProps); + + let button = await screen.findByRole('button', { name: 'Continue' }); + expect(button).toHaveClass('adyen-checkout__button--secondary'); + + const input = await screen.findByLabelText('Email'); + + await user.click(input); + await user.keyboard('shopper@example.com'); + + rerender(customRender(, contextProps)); + + expect(contextProps.setIsCtpPrimaryPaymentMethod).toHaveBeenCalledWith(true); + + button = await screen.findByRole('button', { name: 'Continue' }); + expect(button).not.toHaveClass('adyen-checkout__button--secondary'); +}); + +test('should not start the user login if the email is invalid', async () => { + const user = userEvent.setup(); + + const contextProps = mock(); + contextProps.isCtpPrimaryPaymentMethod = true; + contextProps.setIsCtpPrimaryPaymentMethod.mockImplementation(() => {}); + contextProps.verifyIfShopperIsEnrolled.mockRejectedValue( + new SrciError({ reason: 'ID_FORMAT_UNSUPPORTED', message: '' }, 'verifyIfShopperIsEnrolled', 'visa') + ); + contextProps.schemes = ['mc', 'visa']; + + customRender(, contextProps); + + const input = await screen.findByLabelText('Email'); + await user.click(input); + await user.keyboard('my.invalid.email@example'); + + const button = await screen.findByRole('button', { name: 'Continue' }); + await user.click(button); + + expect(contextProps.startIdentityValidation).toHaveBeenCalledTimes(0); + expect(input).toBeInvalid(); + expect(await screen.findByText('Format not supported')).toBeInTheDocument(); + expect(button).not.toHaveClass('adyen-checkout__button--loading'); +}); + +test('should display not found if the email is not registered', async () => { + const user = userEvent.setup(); + + const contextProps = mock(); + contextProps.isCtpPrimaryPaymentMethod = true; + contextProps.setIsCtpPrimaryPaymentMethod.mockImplementation(() => {}); + contextProps.verifyIfShopperIsEnrolled.mockResolvedValue({ isEnrolled: false }); + contextProps.startIdentityValidation.mockImplementation(); + contextProps.schemes = ['mc', 'visa']; + + customRender(, contextProps); + + const input = await screen.findByLabelText('Email'); + await user.click(input); + await user.keyboard('my.invalid.email@example'); + + const button = await screen.findByRole('button', { name: 'Continue' }); + await user.click(button); + + expect(contextProps.startIdentityValidation).toHaveBeenCalledTimes(0); + expect(input).toBeInvalid(); + expect(await screen.findByText('No account found, enter a valid email or continue using manual card entry')).toBeInTheDocument(); + expect(button).not.toHaveClass('adyen-checkout__button--loading'); +}); + +test('should start the identity validation if the user is enrolled', async () => { + const user = userEvent.setup(); + + const contextProps = mock(); + contextProps.isCtpPrimaryPaymentMethod = true; + contextProps.setIsCtpPrimaryPaymentMethod.mockImplementation(() => {}); + contextProps.verifyIfShopperIsEnrolled.mockResolvedValue({ isEnrolled: true }); + contextProps.startIdentityValidation.mockImplementation(); + contextProps.schemes = ['mc', 'visa']; + + customRender(, contextProps); + + const input = await screen.findByLabelText('Email'); + await user.click(input); + await user.keyboard('shopper@email.com'); + + const button = await screen.findByRole('button', { name: 'Continue' }); + await user.click(button); + + expect(contextProps.startIdentityValidation).toHaveBeenCalledTimes(1); + expect(button).toHaveClass('adyen-checkout__button--loading'); +}); diff --git a/packages/lib/src/components/internal/ClickToPay/components/CtPSection/CtPLogoutLink.test.tsx b/packages/lib/src/components/internal/ClickToPay/components/CtPSection/CtPLogoutLink.test.tsx new file mode 100644 index 0000000000..fe9feab751 --- /dev/null +++ b/packages/lib/src/components/internal/ClickToPay/components/CtPSection/CtPLogoutLink.test.tsx @@ -0,0 +1,88 @@ +import { ComponentChildren, h } from 'preact'; +import { mock } from 'jest-mock-extended'; +import { render, screen } from '@testing-library/preact'; +import userEvent from '@testing-library/user-event'; +import { ClickToPayContext, IClickToPayContext } from '../../context/ClickToPayContext'; +import CtPLogoutLink from './CtPLogoutLink'; +import { CtpState } from '../../services/ClickToPayService'; +import ShopperCard from '../../models/ShopperCard'; +import CoreProvider from '../../../../../core/Context/CoreProvider'; + +const customRender = (children: ComponentChildren, providerProps: IClickToPayContext) => { + return render( + // @ts-ignore TODO: Fix this weird complain + + {/* eslint-disable-next-line react/no-children-prop */} + + + ); +}; + +test('should not render if shopper is not recognized', async () => { + const contextProps = mock(); + contextProps.ctpState = CtpState.Login; + + const { container } = customRender(, contextProps); + + // @ts-ignore FIX TYPES + expect(container).toBeEmptyDOMElement(); +}); + +test('should render i18n message of ctp.logout.notYourCards if there are multiple cards available', async () => { + const user = userEvent.setup(); + + const contextProps = mock(); + contextProps.ctpState = CtpState.Ready; + contextProps.cards = [mock(), mock(), mock()]; + contextProps.logoutShopper.mockImplementation(); + + customRender(, contextProps); + expect(await screen.findByRole('button', { name: 'Not your cards?' })).toBeTruthy(); + + await user.click(screen.getByRole('button', { name: 'Not your cards?' })); + expect(contextProps.logoutShopper).toHaveBeenCalledTimes(1); +}); + +test('should render i18n message of ctp.logout.notYourCard if there is only one card available', async () => { + const user = userEvent.setup(); + + const contextProps = mock(); + contextProps.ctpState = CtpState.Ready; + contextProps.cards = [mock()]; + contextProps.logoutShopper.mockImplementation(); + + customRender(, contextProps); + expect(await screen.findByRole('button', { name: 'Not your card?' })).toBeTruthy(); + + await user.click(screen.getByRole('button', { name: 'Not your card?' })); + expect(contextProps.logoutShopper).toHaveBeenCalledTimes(1); +}); + +test('should render i18n message of ctp.logout.notYourProfile if there is no card available', async () => { + const user = userEvent.setup(); + + const contextProps = mock(); + contextProps.ctpState = CtpState.Ready; + contextProps.cards = []; + contextProps.logoutShopper.mockImplementation(); + + customRender(, contextProps); + expect(await screen.findByRole('button', { name: 'Not your profile?' })).toBeTruthy(); + + await user.click(screen.getByRole('button', { name: 'Not your profile?' })); + expect(contextProps.logoutShopper).toHaveBeenCalledTimes(1); +}); + +test('should render i18n message of ctp.logout.notYou if the shopper is going through OTP ', async () => { + const user = userEvent.setup(); + + const contextProps = mock(); + contextProps.ctpState = CtpState.OneTimePassword; + contextProps.logoutShopper.mockImplementation(); + + customRender(, contextProps); + expect(await screen.findByRole('button', { name: 'Not you?' })).toBeTruthy(); + + await user.click(screen.getByRole('button', { name: 'Not you?' })); + expect(contextProps.logoutShopper).toHaveBeenCalledTimes(1); +}); diff --git a/packages/lib/src/components/internal/ClickToPay/components/CtPSection/CtPLogoutLink.tsx b/packages/lib/src/components/internal/ClickToPay/components/CtPSection/CtPLogoutLink.tsx index 877985b9e9..34e4bd14dc 100644 --- a/packages/lib/src/components/internal/ClickToPay/components/CtPSection/CtPLogoutLink.tsx +++ b/packages/lib/src/components/internal/ClickToPay/components/CtPSection/CtPLogoutLink.tsx @@ -3,10 +3,10 @@ import useClickToPayContext from '../../context/useClickToPayContext'; import { CtpState } from '../../services/ClickToPayService'; import classnames from 'classnames'; import { useMemo } from 'preact/hooks'; -import './CtPLogoutLink.scss'; import useCoreContext from '../../../../../core/Context/useCoreContext'; +import './CtPLogoutLink.scss'; -const CtPLogoutLink = (): h.JSX.Element => { +const CtPLogoutLink = () => { const { ctpState, logoutShopper, status, cards } = useClickToPayContext(); const { i18n } = useCoreContext(); diff --git a/packages/lib/src/components/internal/ClickToPay/services/sdks/MastercardSdk.test.ts b/packages/lib/src/components/internal/ClickToPay/services/sdks/MastercardSdk.test.ts new file mode 100644 index 0000000000..ae794b7969 --- /dev/null +++ b/packages/lib/src/components/internal/ClickToPay/services/sdks/MastercardSdk.test.ts @@ -0,0 +1,398 @@ +import MastercardSdk from './MastercardSdk'; +import Script from '../../../../../utils/Script'; +import { MC_SDK_PROD, MC_SDK_TEST } from './config'; + +const mockScriptLoaded = jest.fn().mockImplementation(() => { + window.SRCSDK_MASTERCARD = { + init: jest.fn().mockResolvedValue(() => {}), + identityLookup: jest.fn().mockResolvedValue({ consumerPresent: true }), + completeIdentityValidation: jest.fn().mockResolvedValue({ idToken: 'id-token' }), + unbindAppInstance: jest.fn(), + initiateIdentityValidation: jest.fn().mockResolvedValue({ maskedValidationChannel: '+31*******55' }), + isRecognized: jest.fn().mockResolvedValue({ + recognized: 'true', + idTokens: ['id-token'] + }), + checkout: jest.fn().mockResolvedValue({ + dcfActionCode: 'COMPLETE', + checkoutResponse: 'checkout-response' + }), + getSrcProfile: jest.fn().mockResolvedValue({ + srcCorrelationId: '1a2b3c', + profiles: [ + { + maskedCards: [ + { + srcDigitalCardId: 'yyyy', + panLastFour: '4302', + dateOfCardLastUsed: '2019-12-25T20:20:02.942Z', + paymentCardDescriptor: 'mc', + panExpirationMonth: '12', + panExpirationYear: '2020', + digitalCardData: { + descriptorName: 'Mastercard', + artUri: 'https://image.com/mc' + }, + tokenId: '2a2a3b3b' + } + ] + } + ] + }) + }; +}); + +const mockScriptRemoved = jest.fn(); + +jest.mock('../../../../../utils/Script', () => { + return jest.fn().mockImplementation(() => { + return { load: mockScriptLoaded, remove: mockScriptRemoved }; + }); +}); + +beforeEach(() => { + // @ts-ignore 'mockClear' is provided by jest.mock + Script.mockClear(); + mockScriptLoaded.mockClear(); + jest.resetModules(); +}); + +afterEach(() => { + delete window.SRCSDK_MASTERCARD; +}); + +describe('SDK urls', () => { + test('should load sdk script with correct URL for live', async () => { + const sdk = new MastercardSdk('live', { dpaLocale: 'en-US', dpaPresentationName: 'MyStore' }); + expect(sdk.schemeSdk).toBeNull; + expect(sdk.schemeName).toBe('mc'); + + await sdk.loadSdkScript(); + + expect(Script).toHaveBeenCalledWith(MC_SDK_PROD); + expect(mockScriptLoaded).toHaveBeenCalledTimes(1); + }); + + test('should load sdk script with correct URL for test', async () => { + const sdk = new MastercardSdk('test', { dpaLocale: 'en-US', dpaPresentationName: 'MyStore' }); + await sdk.loadSdkScript(); + + expect(Script).toHaveBeenCalledWith(MC_SDK_TEST); + expect(mockScriptLoaded).toHaveBeenCalledTimes(1); + }); +}); + +describe('init()', () => { + test('should init with the correct values', async () => { + const sdk = new MastercardSdk('test', { dpaLocale: 'en-US', dpaPresentationName: 'MyStore' }); + await sdk.loadSdkScript(); + + const srcInitiatorId = 'xxxx-yyyy'; + const srciDpaId = '123456'; + const srciTransactionId = '99999999'; + + await sdk.init({ srciDpaId, srcInitiatorId }, srciTransactionId); + + expect(sdk.schemeSdk.init).toHaveBeenCalledWith({ + dpaData: { + dpaPresentationName: 'MyStore' + }, + dpaTransactionOptions: { + confirmPayment: false, + consumerNameRequested: true, + customInputData: { + 'com.mastercard.dcfExperience': 'PAYMENT_SETTINGS' + }, + dpaLocale: 'en-US', + paymentOptions: { + dynamicDataType: 'CARD_APPLICATION_CRYPTOGRAM_SHORT_FORM' + } + }, + srcInitiatorId: 'xxxx-yyyy', + srciDpaId: '123456', + srciTransactionId: '99999999' + }); + }); + + test('should trigger error if init fails', async () => { + const sdk = new MastercardSdk('test', { dpaLocale: 'en-US', dpaPresentationName: 'MyStore' }); + await sdk.loadSdkScript(); + + const mcError = { + message: 'Something went wrong', + reason: 'FAILED' + }; + + sdk.schemeSdk.init = jest.fn().mockRejectedValue(mcError); + + expect.assertions(4); + + await sdk.init({ srciDpaId: 'dpa-id', srcInitiatorId: 'initiator-id' }, 'transaction-id').catch(error => { + expect(error.scheme).toBe('mc'); + expect(error.source).toBe('init'); + expect(error.reason).toBe('FAILED'); + expect(error.message).toBe('Something went wrong'); + }); + }); +}); + +describe('identityLookup()', () => { + test('should call identityLookup with the correct values', async () => { + const sdk = new MastercardSdk('test', { dpaLocale: 'en-US', dpaPresentationName: 'MyStore' }); + await sdk.loadSdkScript(); + + const response = await sdk.identityLookup({ identityValue: 'john@example.com', type: 'email' }); + + expect(response.consumerPresent).toBeTruthy(); + expect(sdk.schemeSdk.identityLookup).toHaveBeenCalledWith({ + consumerIdentity: { + identityValue: 'john@example.com', + identityType: 'EMAIL_ADDRESS' + } + }); + }); + + test('should trigger error if identityLookup fails', async () => { + const sdk = new MastercardSdk('test', { dpaLocale: 'en-US', dpaPresentationName: 'MyStore' }); + await sdk.loadSdkScript(); + + const mcError = { + message: 'Something went wrong', + reason: 'FAILED' + }; + + sdk.schemeSdk.identityLookup = jest.fn().mockRejectedValue(mcError); + + expect.assertions(4); + + await sdk.identityLookup({ identityValue: 'test@example.com', type: 'email' }).catch(error => { + expect(error.scheme).toBe('mc'); + expect(error.source).toBe('identityLookup'); + expect(error.reason).toBe('FAILED'); + expect(error.message).toBe('Something went wrong'); + }); + }); +}); + +describe('completeIdentityValidation()', () => { + test('should call completeIdentityValidation with the correct values', async () => { + const sdk = new MastercardSdk('test', { dpaLocale: 'en-US', dpaPresentationName: 'MyStore' }); + await sdk.loadSdkScript(); + + const otp = '123456'; + + const response = await sdk.completeIdentityValidation(otp); + + expect(response.idToken).toBeDefined(); + expect(sdk.schemeSdk.completeIdentityValidation).toHaveBeenCalledWith({ + validationData: otp + }); + }); + + test('should trigger error if completeIdentityValidation fails', async () => { + const sdk = new MastercardSdk('test', { dpaLocale: 'en-US', dpaPresentationName: 'MyStore' }); + await sdk.loadSdkScript(); + + const mcError = { + message: 'Something went wrong', + reason: 'FAILED' + }; + + sdk.schemeSdk.completeIdentityValidation = jest.fn().mockRejectedValue(mcError); + + expect.assertions(4); + + await sdk.completeIdentityValidation('123456').catch(error => { + expect(error.scheme).toBe('mc'); + expect(error.source).toBe('completeIdentityValidation'); + expect(error.reason).toBe('FAILED'); + expect(error.message).toBe('Something went wrong'); + }); + }); +}); + +describe('checkout()', () => { + test('should call checkout with the correct values', async () => { + const sdk = new MastercardSdk('test', { dpaLocale: 'en-US', dpaPresentationName: 'MyStore' }); + await sdk.loadSdkScript(); + + const response = await sdk.checkout({ + srcDigitalCardId: 'digital-id', + srcCorrelationId: 'correlation-id', + complianceSettings: { complianceResources: [{ complianceType: 'REMEMBER_ME', uri: '' }] } + }); + + expect(response.dcfActionCode).toBe('COMPLETE'); + expect(response.checkoutResponse).toBe('checkout-response'); + expect(sdk.schemeSdk.checkout).toHaveBeenCalledWith({ + srcDigitalCardId: 'digital-id', + srcCorrelationId: 'correlation-id', + complianceSettings: { complianceResources: [{ complianceType: 'REMEMBER_ME', uri: '' }] } + }); + }); + + test('should trigger error if checkout fails', async () => { + const sdk = new MastercardSdk('test', { dpaLocale: 'en-US', dpaPresentationName: 'MyStore' }); + await sdk.loadSdkScript(); + + const mcError = { + message: 'Something went wrong', + reason: 'FAILED' + }; + + sdk.schemeSdk.checkout = jest.fn().mockRejectedValue(mcError); + + expect.assertions(4); + + await sdk.checkout({ srcCorrelationId: 'xxx', srcDigitalCardId: 'yyyy' }).catch(error => { + expect(error.scheme).toBe('mc'); + expect(error.source).toBe('checkout'); + expect(error.reason).toBe('FAILED'); + expect(error.message).toBe('Something went wrong'); + }); + }); +}); + +describe('unbindAppInstance()', () => { + test('should call unbind', async () => { + const sdk = new MastercardSdk('test', { dpaLocale: 'en-US', dpaPresentationName: 'MyStore' }); + await sdk.loadSdkScript(); + await sdk.unbindAppInstance(); + + expect(sdk.schemeSdk.unbindAppInstance).toHaveBeenCalled(); + }); + + test('should trigger error if unbindAppInstance fails', async () => { + const sdk = new MastercardSdk('test', { dpaLocale: 'en-US', dpaPresentationName: 'MyStore' }); + await sdk.loadSdkScript(); + + const mcError = { + message: 'Something went wrong', + reason: 'FAILED' + }; + + sdk.schemeSdk.unbindAppInstance = jest.fn().mockRejectedValue(mcError); + + expect.assertions(4); + + await sdk.unbindAppInstance().catch(error => { + expect(error.scheme).toBe('mc'); + expect(error.source).toBe('unbindAppInstance'); + expect(error.reason).toBe('FAILED'); + expect(error.message).toBe('Something went wrong'); + }); + }); +}); + +describe('isRecognized()', () => { + test('should call isRecognized', async () => { + const sdk = new MastercardSdk('test', { dpaLocale: 'en-US', dpaPresentationName: 'MyStore' }); + await sdk.loadSdkScript(); + + const response = await sdk.isRecognized(); + + expect(sdk.schemeSdk.isRecognized).toHaveBeenCalledTimes(1); + expect(response.recognized).toBeTruthy(); + expect(response.idTokens).toBeDefined(); + }); + + test('should trigger error if isRecognized fails', async () => { + const sdk = new MastercardSdk('test', { dpaLocale: 'en-US', dpaPresentationName: 'MyStore' }); + await sdk.loadSdkScript(); + + const mcError = { + message: 'Something went wrong', + reason: 'FAILED' + }; + + sdk.schemeSdk.isRecognized = jest.fn().mockRejectedValue(mcError); + + expect.assertions(4); + + await sdk.isRecognized().catch(error => { + expect(error.scheme).toBe('mc'); + expect(error.source).toBe('isRecognized'); + expect(error.reason).toBe('FAILED'); + expect(error.message).toBe('Something went wrong'); + }); + }); +}); + +describe('initiateIdentityValidation()', () => { + test('should call initiateIdentityValidation', async () => { + const sdk = new MastercardSdk('test', { dpaLocale: 'en-US', dpaPresentationName: 'MyStore' }); + await sdk.loadSdkScript(); + + const response = await sdk.initiateIdentityValidation(); + + expect(response.maskedValidationChannel).toBe('+31*******55'); + expect(sdk.schemeSdk.initiateIdentityValidation).toHaveBeenCalledTimes(1); + }); + + test('should trigger error if initiateIdentityValidation fails', async () => { + const sdk = new MastercardSdk('test', { dpaLocale: 'en-US', dpaPresentationName: 'MyStore' }); + await sdk.loadSdkScript(); + + const mcError = { + message: 'Something went wrong', + reason: 'FAILED' + }; + + sdk.schemeSdk.initiateIdentityValidation = jest.fn().mockRejectedValue(mcError); + + expect.assertions(4); + + await sdk.initiateIdentityValidation().catch(error => { + expect(error.scheme).toBe('mc'); + expect(error.source).toBe('initiateIdentityValidation'); + expect(error.reason).toBe('FAILED'); + expect(error.message).toBe('Something went wrong'); + }); + }); +}); + +describe('getSrcProfile()', () => { + test('should call getSrcProfile', async () => { + const sdk = new MastercardSdk('test', { dpaLocale: 'en-US', dpaPresentationName: 'MyStore' }); + await sdk.loadSdkScript(); + + const response = await sdk.getSrcProfile(['id-token']); + + expect(response.srcCorrelationId).toBe('1a2b3c'); + expect(response.profiles[0].maskedCards[0]).toBeDefined(); + expect(sdk.schemeSdk.getSrcProfile).toHaveBeenCalledWith({ + idTokens: ['id-token'] + }); + }); + + test('should trigger error if getSrcProfile fails', async () => { + const sdk = new MastercardSdk('test', { dpaLocale: 'en-US', dpaPresentationName: 'MyStore' }); + await sdk.loadSdkScript(); + + const mcError = { + message: 'Something went wrong', + reason: 'FAILED' + }; + + sdk.schemeSdk.getSrcProfile = jest.fn().mockRejectedValue(mcError); + + expect.assertions(4); + + await sdk.getSrcProfile(['xxxx']).catch(error => { + expect(error.scheme).toBe('mc'); + expect(error.source).toBe('getSrcProfile'); + expect(error.reason).toBe('FAILED'); + expect(error.message).toBe('Something went wrong'); + }); + }); +}); + +describe('Removing script', () => { + test('should remove script', async () => { + const sdk = new MastercardSdk('test', { dpaLocale: 'en-US', dpaPresentationName: 'MyStore' }); + await sdk.loadSdkScript(); + sdk.removeSdkScript(); + + expect(mockScriptRemoved).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/lib/src/components/internal/ClickToPay/services/sdks/SrcSdkLoader.test.ts b/packages/lib/src/components/internal/ClickToPay/services/sdks/SrcSdkLoader.test.ts new file mode 100644 index 0000000000..d0bb7fec7a --- /dev/null +++ b/packages/lib/src/components/internal/ClickToPay/services/sdks/SrcSdkLoader.test.ts @@ -0,0 +1,58 @@ +import SrcSdkLoader from './SrcSdkLoader'; + +import VisaSdk from './VisaSdk'; +import MastercardSdk from './MastercardSdk'; +import AdyenCheckoutError from '../../../../../core/Errors/AdyenCheckoutError'; + +jest.mock('./VisaSdk'); +jest.mock('./MastercardSdk'); + +describe('load()', () => { + test('should resolve Promise when all SDKs load sucessfully', async () => { + jest.spyOn(VisaSdk.prototype, 'loadSdkScript').mockResolvedValueOnce(); + jest.spyOn(MastercardSdk.prototype, 'loadSdkScript').mockResolvedValueOnce(); + + const loader = new SrcSdkLoader(['visa', 'mc'], { dpaLocale: 'pt_BR', dpaPresentationName: 'MyStore' }); + const sdks = await loader.load('test'); + + expect(VisaSdk).toHaveBeenCalledWith('test', { dpaLocale: 'pt_BR', dpaPresentationName: 'MyStore' }); + expect(MastercardSdk).toHaveBeenCalledWith('test', { dpaLocale: 'pt_BR', dpaPresentationName: 'MyStore' }); + expect(sdks.length).toBe(2); + }); + + test('should reject Promise when all SDKs fail to load', async () => { + jest.spyOn(VisaSdk.prototype, 'loadSdkScript').mockRejectedValueOnce({}); + jest.spyOn(MastercardSdk.prototype, 'loadSdkScript').mockRejectedValueOnce({}); + + const loader = new SrcSdkLoader(['visa', 'mc'], { dpaLocale: 'pt_BR', dpaPresentationName: 'MyStore' }); + + expect.assertions(2); + + await loader.load('test').catch(error => { + expect(error).toBeInstanceOf(AdyenCheckoutError); + expect(error.message).toContain('ClickToPay -> SrcSdkLoader # Unable to load network schemes'); + }); + }); + + test('should resolve when at least one SDK loaded sucessfully', async () => { + jest.spyOn(VisaSdk.prototype, 'loadSdkScript').mockRejectedValueOnce({}); + jest.spyOn(MastercardSdk.prototype, 'loadSdkScript').mockResolvedValue(); + + const loader = new SrcSdkLoader(['visa', 'mc'], { dpaLocale: 'pt_BR', dpaPresentationName: 'MyStore' }); + const sdks = await loader.load('live'); + + expect(sdks.length).toBe(1); + expect(sdks[0]).toBeInstanceOf(MastercardSdk); + }); + + test('should throw error if no schemes are passed', async () => { + const loader = new SrcSdkLoader([], { dpaLocale: 'pt_BR', dpaPresentationName: 'MyStore' }); + + expect.assertions(2); + + await loader.load('test').catch(error => { + expect(error).toBeInstanceOf(AdyenCheckoutError); + expect(error.message).toContain('ClickToPay -> SrcSdkLoader: There are no schemes set to be loaded'); + }); + }); +}); diff --git a/packages/lib/src/components/internal/ClickToPay/services/sdks/SrcSdkLoader.ts b/packages/lib/src/components/internal/ClickToPay/services/sdks/SrcSdkLoader.ts index af0b8c4cac..975abd7d97 100644 --- a/packages/lib/src/components/internal/ClickToPay/services/sdks/SrcSdkLoader.ts +++ b/packages/lib/src/components/internal/ClickToPay/services/sdks/SrcSdkLoader.ts @@ -31,7 +31,7 @@ class SrcSdkLoader implements ISrcSdkLoader { } public async load(environment: string): Promise { - if (!this.schemes) { + if (!this.schemes || this.schemes.length === 0) { throw new AdyenCheckoutError('ERROR', 'ClickToPay -> SrcSdkLoader: There are no schemes set to be loaded'); } diff --git a/packages/lib/src/components/internal/ClickToPay/services/sdks/SrciError.ts b/packages/lib/src/components/internal/ClickToPay/services/sdks/SrciError.ts index e09f4a3a0e..58d29fac7d 100644 --- a/packages/lib/src/components/internal/ClickToPay/services/sdks/SrciError.ts +++ b/packages/lib/src/components/internal/ClickToPay/services/sdks/SrciError.ts @@ -1,11 +1,11 @@ import { ClickToPayScheme } from '../../types'; -type MastercardError = { +export type MastercardError = { message: string; reason: string; }; -type VisaError = { +export type VisaError = { error: { message: string; reason: string; diff --git a/packages/lib/src/components/internal/ClickToPay/services/sdks/VisaSdk.test.ts b/packages/lib/src/components/internal/ClickToPay/services/sdks/VisaSdk.test.ts new file mode 100644 index 0000000000..21150d0d89 --- /dev/null +++ b/packages/lib/src/components/internal/ClickToPay/services/sdks/VisaSdk.test.ts @@ -0,0 +1,177 @@ +import VisaSdk from './VisaSdk'; +import Script from '../../../../../utils/Script'; +import { VISA_SDK_PROD, VISA_SDK_TEST } from './config'; +import { VisaError } from './SrciError'; + +const mockScriptLoaded = jest.fn().mockImplementation(() => { + window.vAdapters = { + VisaSRCI: jest.fn().mockImplementation(() => ({ + init: jest.fn().mockResolvedValue(() => {}), + identityLookup: jest.fn().mockResolvedValue({ consumerPresent: true }), + completeIdentityValidation: jest.fn().mockResolvedValue({ idToken: 'id-token' }) + })) + }; +}); + +const mockScriptRemoved = jest.fn(); + +jest.mock('../../../../../utils/Script', () => { + return jest.fn().mockImplementation(() => { + return { load: mockScriptLoaded, remove: mockScriptRemoved }; + }); +}); + +beforeEach(() => { + // @ts-ignore 'mockClear' is provided by jest.mock + Script.mockClear(); + mockScriptLoaded.mockClear(); + jest.resetModules(); +}); + +afterEach(() => { + delete window?.vAdapters?.VisaSRCI; +}); + +describe('SDK urls', () => { + test('should load sdk script with correct URL for live', async () => { + const sdk = new VisaSdk('live', { dpaLocale: 'en-US', dpaPresentationName: 'MyStore' }); + expect(sdk.schemeSdk).toBeNull; + expect(sdk.schemeName).toBe('visa'); + + await sdk.loadSdkScript(); + + expect(Script).toHaveBeenCalledWith(VISA_SDK_PROD); + expect(mockScriptLoaded).toHaveBeenCalledTimes(1); + }); + + test('should load sdk script with correct URL for test', async () => { + const sdk = new VisaSdk('test', { dpaLocale: 'en-US', dpaPresentationName: 'MyStore' }); + await sdk.loadSdkScript(); + + expect(Script).toHaveBeenCalledWith(VISA_SDK_TEST); + expect(mockScriptLoaded).toHaveBeenCalledTimes(1); + }); +}); + +describe('init()', () => { + test('should init with the correct values', async () => { + const sdk = new VisaSdk('test', { dpaLocale: 'en-US', dpaPresentationName: 'MyStore' }); + await sdk.loadSdkScript(); + + const srcInitiatorId = 'xxxx-yyyy'; + const srciDpaId = '123456'; + const srciTransactionId = '99999999'; + + await sdk.init({ srciDpaId, srcInitiatorId }, srciTransactionId); + + expect(sdk.schemeSdk.init).toHaveBeenCalledWith({ + dpaData: { dpaPresentationName: 'MyStore' }, + dpaTransactionOptions: { + customInputData: { checkoutOrchestrator: 'merchant' }, + dpaLocale: 'en-US', + payloadTypeIndicator: 'NON_PAYMENT' + }, + srcInitiatorId: 'xxxx-yyyy', + srciDpaId: '123456', + srciTransactionId: '99999999' + }); + }); + + test('should trigger error if init fails', async () => { + const sdk = new VisaSdk('test', { dpaLocale: 'en-US', dpaPresentationName: 'MyStore' }); + await sdk.loadSdkScript(); + + const error: VisaError = { + error: { + message: 'Something went wrong', + reason: 'FAILED' + } + }; + + sdk.schemeSdk.init = jest.fn().mockRejectedValue(error); + + expect.assertions(4); + + await sdk.init({ srciDpaId: 'dpa-id', srcInitiatorId: 'initiator-id' }, 'transaction-id').catch(error => { + expect(error.scheme).toBe('visa'); + expect(error.source).toBe('init'); + expect(error.reason).toBe('FAILED'); + expect(error.message).toBe('Something went wrong'); + }); + }); +}); + +describe('identityLookup()', () => { + test('should call identityLookup with the correct values', async () => { + const sdk = new VisaSdk('test', { dpaLocale: 'en-US', dpaPresentationName: 'MyStore' }); + await sdk.loadSdkScript(); + + const response = await sdk.identityLookup({ identityValue: 'john@example.com', type: 'email' }); + + expect(response.consumerPresent).toBeTruthy(); + expect(sdk.schemeSdk.identityLookup).toHaveBeenCalledWith({ + identityValue: 'john@example.com', + type: 'EMAIL' + }); + }); + + test('should trigger error if identityLookup fails', async () => { + const sdk = new VisaSdk('test', { dpaLocale: 'en-US', dpaPresentationName: 'MyStore' }); + await sdk.loadSdkScript(); + + const error: VisaError = { + error: { + message: 'Something went wrong', + reason: 'FAILED' + } + }; + + sdk.schemeSdk.identityLookup = jest.fn().mockRejectedValue(error); + + expect.assertions(4); + + await sdk.identityLookup({ identityValue: 'test@example.com', type: 'email' }).catch(error => { + expect(error.scheme).toBe('visa'); + expect(error.source).toBe('identityLookup'); + expect(error.reason).toBe('FAILED'); + expect(error.message).toBe('Something went wrong'); + }); + }); +}); + +describe('completeValidation()', () => { + test('should call completeIdentityValidation with the correct values', async () => { + const sdk = new VisaSdk('test', { dpaLocale: 'en-US', dpaPresentationName: 'MyStore' }); + await sdk.loadSdkScript(); + + const otp = '123456'; + + const response = await sdk.completeIdentityValidation(otp); + + expect(response.idToken).toBeDefined(); + expect(sdk.schemeSdk.completeIdentityValidation).toHaveBeenCalledWith(otp); + }); + + test('should trigger error if completeIdentityValidation fails', async () => { + const sdk = new VisaSdk('test', { dpaLocale: 'en-US', dpaPresentationName: 'MyStore' }); + await sdk.loadSdkScript(); + + const error: VisaError = { + error: { + message: 'Something went wrong', + reason: 'FAILED' + } + }; + + sdk.schemeSdk.completeIdentityValidation = jest.fn().mockRejectedValue(error); + + expect.assertions(4); + + await sdk.completeIdentityValidation('123456').catch(error => { + expect(error.scheme).toBe('visa'); + expect(error.source).toBe('completeIdentityValidation'); + expect(error.reason).toBe('FAILED'); + expect(error.message).toBe('Something went wrong'); + }); + }); +}); diff --git a/packages/lib/src/components/internal/ClickToPay/services/sdks/types.ts b/packages/lib/src/components/internal/ClickToPay/services/sdks/types.ts index 38e09149c1..9311f7f174 100644 --- a/packages/lib/src/components/internal/ClickToPay/services/sdks/types.ts +++ b/packages/lib/src/components/internal/ClickToPay/services/sdks/types.ts @@ -1,3 +1,12 @@ +declare global { + interface Window { + SRCSDK_MASTERCARD?: object; + vAdapters: { + VisaSRCI?: object; + }; + } +} + /** * Type that represent the object which contains the customizable properties of the SDK initialization */ diff --git a/yarn.lock b/yarn.lock index 9e23812707..b087fbace2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16118,10 +16118,10 @@ vite-plugin-stylelint@^4.3.0: "@rollup/pluginutils" "^5.0.2" chokidar "^3.5.3" -vite@4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/vite/-/vite-4.5.0.tgz#ec406295b4167ac3bc23e26f9c8ff559287cff26" - integrity sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw== +vite@4.5.1: + version "4.5.1" + resolved "https://registry.yarnpkg.com/vite/-/vite-4.5.1.tgz#3370986e1ed5dbabbf35a6c2e1fb1e18555b968a" + integrity sha512-AXXFaAJ8yebyqzoNB9fu2pHoo/nWX+xZlaRwoeYUxEqBO+Zj4msE5G+BhGBll9lYEKv9Hfks52PAF2X7qDYXQA== dependencies: esbuild "^0.18.10" postcss "^8.4.27"