Merge branch 'INDA-330-pii-update' into 'main'
[ProtonMail-WebClient.git] / packages / chargebee / src / message-bus.test.ts
blob85a7a63094474d748fd714accac7559a4815ff81
1 import { fireEvent } from '@testing-library/dom';
3 import type {
4     BinData,
5     ChargebeeSubmitEventResponse,
6     FormValidationErrors,
7     GetHeightResponsePayload,
8     MessageBusResponse,
9     PaymentIntent,
10     PaypalAuthorizedPayload,
11     ThreeDsChallengePayload,
12 } from '../lib';
13 import { chargebeeWrapperVersion } from './checkpoints';
14 import type {
15     ChargebeeSubmitEvent,
16     DirectDebitSubmitEvent,
17     GetBinEvent,
18     GetHeightEvent,
19     ParentMessagesProps,
20     SetConfigurationEvent,
21     SetPaypalPaymentIntentEvent,
22     ValidateFormEvent,
23     VerifySavedCardEvent,
24 } from './message-bus';
25 import { MessageBus, createMessageBus, getMessageBus, verifySavedCardMessageType } from './message-bus';
27 window.parent.postMessage = jest.fn();
29 let messageBus: MessageBus | null = null;
30 beforeEach(() => {
31     jest.clearAllMocks();
33     const config: ParentMessagesProps = {
34         onSetConfiguration: jest.fn(),
35         onSubmit: jest.fn(),
36         onSetPaypalPaymentIntent: jest.fn(),
37         onGetHeight: jest.fn(),
38         onGetBin: jest.fn(),
39         onValidateForm: jest.fn(),
40         onVerifySavedCard: jest.fn(),
41     };
43     createMessageBus(config);
45     messageBus = getMessageBus();
46 });
48 it('should create message bus', () => {
49     const messageBus = getMessageBus();
50     expect(messageBus).toBeInstanceOf(MessageBus);
51 });
53 it('should listen to set configuration event', () => {
54     const event: SetConfigurationEvent = {
55         type: 'set-configuration',
56         correlationId: 'id-1',
57         paymentMethodType: 'card',
58         publishableKey: 'pk',
59         site: 'site',
60         domain: 'domain',
61         cssVariables: {
62             '--signal-danger': '#000000',
63             '--border-radius-md': '#000000',
64             '--border-norm': '#000000',
65             '--focus-outline': '#000000',
66             '--focus-ring': '#000000',
67             '--field-norm': '#000000',
68             '--field-background-color': '#000000',
69             '--field-focus-background-color': '#000000',
70             '--field-focus-text-color': '#000000',
71             '--field-placeholder-color': '#000000',
72             '--field-text-color': '#000000',
73             '--selection-text-color': '#000000',
74             '--selection-background-color': '#000000',
75         },
76         translations: {
77             cardNumberPlaceholder: '0000 0000 0000 0000',
78             cardExpiryPlaceholder: 'MM/YY',
79             cardCvcPlaceholder: '000',
80             invalidCardNumberMessage: 'Invalid card number',
81             invalidCardExpiryMessage: 'Invalid card expiry',
82             invalidCardCvcMessage: 'Invalid card cvc',
83         },
84         renderMode: 'one-line',
85     };
87     fireEvent(
88         window,
89         new MessageEvent('message', {
90             data: event,
91         })
92     );
93     expect(messageBus?.onSetConfiguration).toHaveBeenCalled();
94     const firstArg = (messageBus?.onSetConfiguration as jest.Mock).mock.calls[0][0];
95     expect(firstArg).toEqual(event);
97     const secondArg = (messageBus?.onSetConfiguration as jest.Mock).mock.calls[0][1];
98     expect(secondArg).toBeInstanceOf(Function);
100     const response: MessageBusResponse<{}> = {
101         status: 'success',
102         data: {},
103     };
104     secondArg(response);
105     expect(window.parent.postMessage).toHaveBeenCalledWith(
106         JSON.stringify({
107             type: 'set-configuration-response',
108             correlationId: event.correlationId,
109             status: 'success',
110             data: {},
111         }),
112         '*'
113     );
116 it('should listen to submit event', () => {
117     const event: ChargebeeSubmitEvent = {
118         type: 'chargebee-submit',
119         correlationId: 'id-1',
120         paymentIntent: {
121             type: 'payment_intent',
122         } as any,
123         countryCode: 'US',
124         zip: '12345',
125     };
127     fireEvent(
128         window,
129         new MessageEvent('message', {
130             data: event,
131         })
132     );
134     expect(messageBus?.onSubmit).toHaveBeenCalled();
135     const firstArg = (messageBus?.onSubmit as jest.Mock).mock.calls[0][0];
136     expect(firstArg).toEqual(event);
138     const secondArg = (messageBus?.onSubmit as jest.Mock).mock.calls[0][1];
139     expect(secondArg).toBeInstanceOf(Function);
141     const response: MessageBusResponse<{}> = {
142         status: 'success',
143         data: {},
144     };
145     secondArg(response);
146     expect(window.parent.postMessage).toHaveBeenCalledWith(
147         JSON.stringify({
148             type: 'chargebee-submit-response',
149             correlationId: event.correlationId,
150             status: 'success',
151             data: {},
152         }),
153         '*'
154     );
157 it('should listen to set paypal payment intent event', () => {
158     const event: SetPaypalPaymentIntentEvent = {
159         type: 'set-paypal-payment-intent',
160         correlationId: 'id-1',
161         paymentIntent: {
162             type: 'payment_intent',
163         } as any,
165         // todo: perhaps remove it
166         countryCode: 'US',
167         zip: '12345',
168     };
170     fireEvent(
171         window,
172         new MessageEvent('message', {
173             data: event,
174         })
175     );
177     expect(messageBus?.onSetPaypalPaymentIntent).toHaveBeenCalled();
178     const firstArg = (messageBus?.onSetPaypalPaymentIntent as jest.Mock).mock.calls[0][0];
179     expect(firstArg).toEqual(event);
181     const secondArg = (messageBus?.onSetPaypalPaymentIntent as jest.Mock).mock.calls[0][1];
182     expect(secondArg).toBeInstanceOf(Function);
184     const response: MessageBusResponse<ChargebeeSubmitEventResponse> = {
185         status: 'success',
186         data: {
187             authorized: false,
188             approvalUrl: 'https://proton.me',
189         } as any, // todo: fix types
190     };
192     secondArg(response);
193     expect(window.parent.postMessage).toHaveBeenCalledWith(
194         JSON.stringify({
195             type: 'set-paypal-payment-intent-response',
196             correlationId: event.correlationId,
197             status: 'success',
198             data: {
199                 authorized: false,
200                 approvalUrl: 'https://proton.me',
201             },
202         }),
203         '*'
204     );
207 it('should listen to get height event', () => {
208     const event: GetHeightEvent = {
209         type: 'get-height',
210         correlationId: 'id-1',
211     };
213     fireEvent(
214         window,
215         new MessageEvent('message', {
216             data: event,
217         })
218     );
220     expect(messageBus?.onGetHeight).toHaveBeenCalled();
221     const firstArg = (messageBus?.onGetHeight as jest.Mock).mock.calls[0][0];
222     expect(firstArg).toEqual(event);
224     const secondArg = (messageBus?.onGetHeight as jest.Mock).mock.calls[0][1];
225     expect(secondArg).toBeInstanceOf(Function);
227     const response: MessageBusResponse<GetHeightResponsePayload> = {
228         status: 'success',
229         data: {
230             height: 100,
231             extraBottom: 8,
232         },
233     };
234     secondArg(response);
235     expect(window.parent.postMessage).toHaveBeenCalledWith(
236         JSON.stringify({
237             type: 'get-height-response',
238             correlationId: event.correlationId,
239             status: 'success',
240             data: {
241                 height: 100,
242                 extraBottom: 8,
243             },
244         }),
245         '*'
246     );
249 it('should listen to get bin event', () => {
250     const event: GetBinEvent = {
251         type: 'get-bin',
252         correlationId: 'id-1',
253     };
255     fireEvent(
256         window,
257         new MessageEvent('message', {
258             data: event,
259         })
260     );
262     expect(messageBus?.onGetBin).toHaveBeenCalled();
263     const firstArg = (messageBus?.onGetBin as jest.Mock).mock.calls[0][0];
264     expect(firstArg).toEqual(event);
266     const secondArg = (messageBus?.onGetBin as jest.Mock).mock.calls[0][1];
267     expect(secondArg).toBeInstanceOf(Function);
269     const response: MessageBusResponse<BinData> = {
270         status: 'success',
271         data: {
272             bin: '424242',
273             last4: '4242',
274         },
275     };
276     secondArg(response);
278     expect(window.parent.postMessage).toHaveBeenCalledWith(
279         JSON.stringify({
280             type: 'get-bin-response',
281             correlationId: event.correlationId,
282             ...response,
283         }),
284         '*'
285     );
288 it('should listen to validate form event', () => {
289     const event: ValidateFormEvent = {
290         type: 'validate-form',
291         correlationId: 'id-1',
292     };
294     fireEvent(
295         window,
296         new MessageEvent('message', {
297             data: event,
298         })
299     );
301     expect(messageBus?.onValidateForm).toHaveBeenCalled();
302     const firstArg = (messageBus?.onValidateForm as jest.Mock).mock.calls[0][0];
303     expect(firstArg).toEqual(event);
305     const secondArg = (messageBus?.onValidateForm as jest.Mock).mock.calls[0][1];
306     expect(secondArg).toBeInstanceOf(Function);
308     const response: MessageBusResponse<FormValidationErrors> = {
309         status: 'success',
310         data: [],
311     };
313     secondArg(response);
315     expect(window.parent.postMessage).toHaveBeenCalledWith(
316         JSON.stringify({
317             type: 'validate-form-response',
318             correlationId: event.correlationId,
319             status: 'success',
320             data: [],
321         }),
322         '*'
323     );
326 it('should listen to verify saved card event', () => {
327     const event: VerifySavedCardEvent = {
328         type: verifySavedCardMessageType,
329         correlationId: 'id-1',
330         paymentIntent: {
331             type: 'payment_intent',
332         } as any,
333     };
335     fireEvent(
336         window,
337         new MessageEvent('message', {
338             data: event,
339         })
340     );
342     expect(messageBus?.onVerifySavedCard).toHaveBeenCalled();
344     const firstArg = (messageBus?.onVerifySavedCard as jest.Mock).mock.calls[0][0];
345     expect(firstArg).toEqual(event);
347     const secondArg = (messageBus?.onVerifySavedCard as jest.Mock).mock.calls[0][1];
348     expect(secondArg).toBeInstanceOf(Function);
350     const response: MessageBusResponse<ChargebeeSubmitEventResponse> = {
351         status: 'success',
352         data: {
353             authorized: false,
354             approvalUrl: 'https://proton.me',
355         } as any, // todo: fix types
356     };
357     secondArg(response);
359     expect(window.parent.postMessage).toHaveBeenCalledWith(
360         JSON.stringify({
361             type: 'chargebee-verify-saved-card-response',
362             correlationId: event.correlationId,
363             status: 'success',
364             data: {
365                 authorized: false,
366                 approvalUrl: 'https://proton.me',
367             },
368         }),
369         '*'
370     );
373 it('should send paypal authorized message', () => {
374     const data: PaypalAuthorizedPayload = {
375         paymentIntent: {
376             type: 'payment_intent',
377         } as any,
378     };
380     messageBus?.sendPaypalAuthorizedMessage(data);
381     const expectedMessage = {
382         status: 'success',
383         data,
384     };
386     expect(window.parent.postMessage).toHaveBeenCalledWith(
387         JSON.stringify({
388             type: 'paypal-authorized',
389             ...expectedMessage,
390         }),
391         '*'
392     );
395 it('should send 3ds challenge message', () => {
396     const data: ThreeDsChallengePayload = {
397         url: 'https://proton.me',
398     };
400     const correlationId = 'id-1';
402     messageBus?.send3dsChallengeMessage(data, correlationId);
403     const expectedMessage = {
404         status: 'success',
405         data,
406         correlationId,
407     };
409     expect(window.parent.postMessage).toHaveBeenCalledWith(
410         JSON.stringify({
411             type: '3ds-challenge',
412             ...expectedMessage,
413         }),
414         '*'
415     );
418 it('should send 3ds failed message', () => {
419     const error = {
420         message: 'error',
421     };
423     const correlationId = 'id-1';
425     messageBus?.send3dsFailedMessage(error, correlationId);
426     const expectedMessage = {
427         status: 'failure',
428         error,
429         correlationId,
430     };
432     expect(window.parent.postMessage).toHaveBeenCalledWith(
433         JSON.stringify({
434             type: 'chargebee-submit-response',
435             ...expectedMessage,
436         }),
437         '*'
438     );
441 it('should send form validation error message', () => {
442     const errors: FormValidationErrors = [
443         {
444             message: 'error',
445             error: 'error',
446         },
447     ];
449     const correlationId = 'id-1';
451     messageBus?.sendFormValidationErrorMessage(errors, correlationId);
452     const expectedMessage = {
453         status: 'failure',
454         error: errors,
455         correlationId,
456     };
458     expect(window.parent.postMessage).toHaveBeenCalledWith(
459         JSON.stringify({
460             type: 'chargebee-submit-response',
461             ...expectedMessage,
462         }),
463         '*'
464     );
467 it('should send 3ds success message', () => {
468     const paymentIntent: PaymentIntent = {
469         type: 'payment_intent',
470     } as any;
472     const correlationId = 'id-1';
474     messageBus?.send3dsSuccessMessage(
475         paymentIntent as any, // todo: fix types
476         correlationId
477     );
479     const expectedMessage = {
480         status: 'success',
481         data: paymentIntent,
482         correlationId,
483     };
485     expect(window.parent.postMessage).toHaveBeenCalledWith(
486         JSON.stringify({
487             type: 'chargebee-submit-response',
488             ...expectedMessage,
489         }),
490         '*'
491     );
494 it('should send unhandled error message', () => {
495     const error = new Error('Unhandled error');
497     messageBus?.sendUnhandledErrorMessage(error);
499     expect(window.parent.postMessage).toHaveBeenCalled();
501     const postMessageArg = (window.parent.postMessage as jest.Mock).mock.calls[0][0];
502     const parsedArg = JSON.parse(postMessageArg);
504     expect(parsedArg).toEqual({
505         type: 'chargebee-unhandled-error',
506         status: 'failure',
507         error: expect.objectContaining({
508             message: 'Unhandled error',
509             stack: error.stack,
510             name: 'Error',
511             checkpoints: expect.any(Array),
512             chargebeeWrapperVersion,
513         }),
514     });
517 it('should listen to direct debit submit event', () => {
518     const event: DirectDebitSubmitEvent = {
519         type: 'direct-debit-submit',
520         correlationId: 'dd-123',
521         paymentIntent: {
522             id: 'pi_123',
523             status: 'inited',
524             amount: 1000,
525             currency_code: 'USD',
526             gateway_account_id: 'ga_123',
527             gateway: 'stripe',
528             customer_id: 'cust_123',
529             payment_method_type: 'card',
530             expires_at: 1234567890,
531             created_at: 1234567890,
532             modified_at: 1234567890,
533             updated_at: 1234567890,
534             resource_version: 1234567890,
535             object: 'payment_intent',
536         },
537         customer: {
538             email: 'test@example.com',
539             company: 'Test Company',
540             firstName: 'John',
541             lastName: 'Doe',
542             customerNameType: 'individual',
543             countryCode: 'US',
544             addressLine1: '123 Test St',
545         },
546         bankAccount: {
547             iban: 'DE89370400440532013000',
548         },
549     };
551     // Mock the onDirectDebitSubmit handler
552     const mockOnDirectDebitSubmit = jest.fn();
553     messageBus!.onDirectDebitSubmit = mockOnDirectDebitSubmit;
555     fireEvent(
556         window,
557         new MessageEvent('message', {
558             data: event,
559         })
560     );
562     expect(mockOnDirectDebitSubmit).toHaveBeenCalled();
563     const firstArg = mockOnDirectDebitSubmit.mock.calls[0][0];
564     expect(firstArg).toEqual(event);
566     const secondArg = mockOnDirectDebitSubmit.mock.calls[0][1];
567     expect(secondArg).toBeInstanceOf(Function);
569     const response: MessageBusResponse<{}> = {
570         status: 'success',
571         data: {},
572     };
573     secondArg(response);
574     expect(window.parent.postMessage).toHaveBeenCalledWith(
575         JSON.stringify({
576             type: 'direct-debit-submit-response',
577             correlationId: event.correlationId,
578             status: 'success',
579             data: {},
580         }),
581         '*'
582     );