import React from 'react'; import { renderHook, act } from '@testing-library/react'; import { Provider } from 'react-redux'; import { configureStore } from '@reduxjs/toolkit'; import masterpassRestReducer from '../redux/reducer'; import { getPaymentPending, setPaymentPending } from '../utils/payment-utils'; const handleVerificationMock = jest.fn(); const accountAccessMock = jest.fn(); jest.mock('../utils/response-handler', () => ({ handleVerification: (...args: unknown[]) => handleVerificationMock(...args), handleMasterpassResponse: jest.fn(async (response) => response) })); jest.mock('../services/account', () => ({ AccountService: class { accountAccess = accountAccessMock; linkToMerchant = jest.fn(); removeCard = jest.fn(); addCard = jest.fn(); } })); jest.mock('./useMasterpassToken', () => ({ useMasterpassToken: () => ({ refetch: jest.fn(async () => ({ data: { token: 'fresh-token' } })) }) })); // Imported after the mocks so the hook picks them up. import { useMasterpassAccount } from './useMasterpassAccount'; const renderAccountHook = (otpContext: 'account' | 'payment') => { const store = configureStore({ reducer: { masterpassRest: masterpassRestReducer }, middleware: (getDefault) => getDefault({ serializableCheck: false }) }); store.dispatch({ type: 'masterpassRest/setMasterpassRestTokenData', payload: { MerchantId: 'merchant-1', AccountKey: 'account-1', UserId: 'user-1' } }); store.dispatch({ type: 'masterpassRest/updateModalState', payload: { otpContext, showOTPModal: true } }); const wrapper = ({ children }: { children: React.ReactNode }) => ( {children} ); return { ...renderHook(() => useMasterpassAccount(), { wrapper }), store }; }; beforeEach(() => { accountAccessMock.mockResolvedValue({ statusCode: 200, result: { cards: [] } }); }); describe('verification during a payment', () => { it('hands the verification result back so the caller can complete the order', async () => { handleVerificationMock.mockResolvedValue({ success: true, data: { result: { responseCode: '0000', token: 'verified-token' } } }); const { result } = renderAccountHook('payment'); let outcome: any; await act(async () => { outcome = await result.current.handleOTPSubmit('123456'); }); expect(outcome).toEqual({ success: true, isPaymentContext: true, verificationData: { result: { responseCode: '0000', token: 'verified-token' } } }); }); it('does not treat a refreshed card list as a completed payment', async () => { handleVerificationMock.mockResolvedValue({ success: true, data: { result: { responseCode: '0000', token: 'verified-token' } } }); const { result } = renderAccountHook('payment'); accountAccessMock.mockClear(); await act(async () => { await result.current.handleOTPSubmit('123456'); }); // Refreshing the account here used to be the only thing that happened, which // left the order uncompleted on the backend. expect(accountAccessMock).not.toHaveBeenCalled(); }); it('keeps the order number pending when verification sends the shopper to 3D', async () => { setPaymentPending('ORDER-1'); handleVerificationMock.mockResolvedValue({ requires3D: true, redirectUrl: 'https://bank.example/3d' }); const location = window.location; delete (window as any).location; (window as any).location = { ...location, href: '' }; const { result } = renderAccountHook('payment'); await act(async () => { await result.current.handleOTPSubmit('123456'); }); expect(getPaymentPending()?.orderNo).toBe('ORDER-1'); (window as any).location = location; }); }); describe('verification during an account operation', () => { it('refreshes the saved cards and reports plain success', async () => { handleVerificationMock.mockResolvedValue({ success: true, data: { result: { responseCode: '0000' } } }); const { result } = renderAccountHook('account'); accountAccessMock.mockClear(); let outcome: any; await act(async () => { outcome = await result.current.handleOTPSubmit('123456'); }); expect(outcome).toEqual({ success: true }); expect(accountAccessMock).toHaveBeenCalled(); }); it('leaves the pending record untouched on a 3D redirect', async () => { setPaymentPending('ORDER-1'); const before = getPaymentPending()?.startedAt; handleVerificationMock.mockResolvedValue({ requires3D: true, redirectUrl: 'https://bank.example/3d' }); const location = window.location; delete (window as any).location; (window as any).location = { ...location, href: '' }; const { result } = renderAccountHook('account'); await act(async () => { await result.current.handleOTPSubmit('123456'); }); expect(getPaymentPending()?.startedAt).toBe(before); (window as any).location = location; }); }); describe('the default verification context', () => { it('is the account flow, so an unset context never skips the card refresh', async () => { handleVerificationMock.mockResolvedValue({ success: true, data: { result: {} } }); const store = configureStore({ reducer: { masterpassRest: masterpassRestReducer }, middleware: (getDefault) => getDefault({ serializableCheck: false }) }); expect(store.getState().masterpassRest.modalState.otpContext).toBe('account'); }); });