From 6784502a79ca8eaf046a90a992e6dc5e497c6c46 Mon Sep 17 00:00:00 2001 From: ChiragKV-Juspay Date: Fri, 10 Apr 2026 00:15:54 +0530 Subject: [PATCH] feat: added unit tests --- .gitignore | 4 + __mocks__/rescriptReactStub.js | 4 + __tests__/shared-code/CardValidations.test.ts | 473 ++++ __tests__/shared-code/CnpjValidation.test.ts | 174 ++ __tests__/shared-code/CommonUtils.test.ts | 706 +++++ __tests__/shared-code/LocaleDataType.test.ts | 444 +++ .../shared-code/PhoneNumberValidation.test.ts | 89 + __tests__/shared-code/SharedCodeWeb.test.ts | 200 ++ .../shared-code/SuperpositionHelper.test.ts | 384 +++ .../shared-code/SuperpositionTypes.test.ts | 65 + __tests__/shared-code/Validation.test.ts | 346 +++ jest.config.js | 106 + jest.setup.js | 34 + package.json | 15 + src/__tests__/APIUtils.test.ts | 303 ++ src/__tests__/ApiEndpoint.test.ts | 33 + src/__tests__/ApplePayHelpers.test.ts | 1379 ++++++++++ src/__tests__/ApplePayTypes.test.ts | 216 ++ src/__tests__/Bank.test.ts | 192 ++ src/__tests__/BraintreeHelpers.test.ts | 142 + src/__tests__/BrowserSpec.test.ts | 176 ++ src/__tests__/CardThemeType.test.ts | 200 ++ src/__tests__/CardUtils.test.ts | 1033 +++++++ .../ClickToPayCardEncryption.test.ts | 101 + src/__tests__/ClickToPayHelpers.test.ts | 1130 ++++++++ src/__tests__/ClickToPayHook.test.ts | 678 +++++ src/__tests__/CommonCardProps.test.ts | 367 +++ src/__tests__/CommonHooks.test.ts | 575 ++++ src/__tests__/ConfirmType.test.ts | 117 + .../CustomPaymentMethodsConfig.test.ts | 374 +++ src/__tests__/DynamicFieldsUtils.test.ts | 1645 +++++++++++ src/__tests__/ElementType.test.ts | 287 ++ src/__tests__/ErrorUtils.test.ts | 119 + src/__tests__/GooglePayHelpers.test.ts | 520 ++++ src/__tests__/GooglePayType.test.ts | 293 ++ src/__tests__/HyperLogger.test.ts | 550 ++++ src/__tests__/HyperVaultHelpers.test.ts | 80 + src/__tests__/LocaleStringHelper.test.ts | 137 + src/__tests__/LogAPIResponse.test.ts | 165 ++ src/__tests__/LoggerUtils.test.ts | 495 ++++ src/__tests__/NetworkInformation.test.ts | 221 ++ src/__tests__/OutsideClick.test.ts | 337 +++ src/__tests__/PaymentBody.test.ts | 891 ++++++ src/__tests__/PaymentConfirmTypes.test.ts | 259 ++ src/__tests__/PaymentConfirmTypesV2.test.ts | 232 ++ src/__tests__/PaymentError.test.ts | 114 + src/__tests__/PaymentHelpers.test.ts | 2449 +++++++++++++++++ src/__tests__/PaymentHelpersV2.test.ts | 592 ++++ src/__tests__/PaymentManagementBody.test.ts | 233 ++ .../PaymentMethodCollectTypes.test.ts | 725 +++++ .../PaymentMethodCollectUtils.test.ts | 1547 +++++++++++ src/__tests__/PaymentMethodsRecord.test.ts | 686 +++++ src/__tests__/PaymentModeType.test.ts | 230 ++ src/__tests__/PaymentType.test.ts | 1849 +++++++++++++ src/__tests__/PaymentUtils.test.ts | 1523 ++++++++++ src/__tests__/PaymentUtilsV2.test.ts | 230 ++ src/__tests__/PaypalSDKTypes.test.ts | 253 ++ src/__tests__/PmAuthConnectorUtils.test.ts | 219 ++ src/__tests__/RecoilAtomTypes.test.ts | 120 + src/__tests__/S3Utils.test.ts | 575 ++++ src/__tests__/SamsungPayHelpers.test.ts | 608 ++++ src/__tests__/TaxCalculation.test.ts | 244 ++ src/__tests__/ThirdPartyFlowHelpers.test.ts | 244 ++ src/__tests__/UnifiedHelpersV2.test.ts | 536 ++++ src/__tests__/UtilityHooks.test.ts | 430 +++ src/__tests__/VaultHelpers.test.ts | 222 ++ src/__tests__/WebHyperLogger.test.ts | 393 +++ src/__tests__/WebLoggerUtils.test.ts | 130 + src/__tests__/WebPaymentUtils.test.ts | 214 ++ src/__tests__/WebSessionsType.test.ts | 304 ++ src/__tests__/WebUtils.test.ts | 1617 +++++++++++ tsconfig.test.json | 16 + 72 files changed, 32594 insertions(+) create mode 100644 __mocks__/rescriptReactStub.js create mode 100644 __tests__/shared-code/CardValidations.test.ts create mode 100644 __tests__/shared-code/CnpjValidation.test.ts create mode 100644 __tests__/shared-code/CommonUtils.test.ts create mode 100644 __tests__/shared-code/LocaleDataType.test.ts create mode 100644 __tests__/shared-code/PhoneNumberValidation.test.ts create mode 100644 __tests__/shared-code/SharedCodeWeb.test.ts create mode 100644 __tests__/shared-code/SuperpositionHelper.test.ts create mode 100644 __tests__/shared-code/SuperpositionTypes.test.ts create mode 100644 __tests__/shared-code/Validation.test.ts create mode 100644 jest.config.js create mode 100644 jest.setup.js create mode 100644 src/__tests__/APIUtils.test.ts create mode 100644 src/__tests__/ApiEndpoint.test.ts create mode 100644 src/__tests__/ApplePayHelpers.test.ts create mode 100644 src/__tests__/ApplePayTypes.test.ts create mode 100644 src/__tests__/Bank.test.ts create mode 100644 src/__tests__/BraintreeHelpers.test.ts create mode 100644 src/__tests__/BrowserSpec.test.ts create mode 100644 src/__tests__/CardThemeType.test.ts create mode 100644 src/__tests__/CardUtils.test.ts create mode 100644 src/__tests__/ClickToPayCardEncryption.test.ts create mode 100644 src/__tests__/ClickToPayHelpers.test.ts create mode 100644 src/__tests__/ClickToPayHook.test.ts create mode 100644 src/__tests__/CommonCardProps.test.ts create mode 100644 src/__tests__/CommonHooks.test.ts create mode 100644 src/__tests__/ConfirmType.test.ts create mode 100644 src/__tests__/CustomPaymentMethodsConfig.test.ts create mode 100644 src/__tests__/DynamicFieldsUtils.test.ts create mode 100644 src/__tests__/ElementType.test.ts create mode 100644 src/__tests__/ErrorUtils.test.ts create mode 100644 src/__tests__/GooglePayHelpers.test.ts create mode 100644 src/__tests__/GooglePayType.test.ts create mode 100644 src/__tests__/HyperLogger.test.ts create mode 100644 src/__tests__/HyperVaultHelpers.test.ts create mode 100644 src/__tests__/LocaleStringHelper.test.ts create mode 100644 src/__tests__/LogAPIResponse.test.ts create mode 100644 src/__tests__/LoggerUtils.test.ts create mode 100644 src/__tests__/NetworkInformation.test.ts create mode 100644 src/__tests__/OutsideClick.test.ts create mode 100644 src/__tests__/PaymentBody.test.ts create mode 100644 src/__tests__/PaymentConfirmTypes.test.ts create mode 100644 src/__tests__/PaymentConfirmTypesV2.test.ts create mode 100644 src/__tests__/PaymentError.test.ts create mode 100644 src/__tests__/PaymentHelpers.test.ts create mode 100644 src/__tests__/PaymentHelpersV2.test.ts create mode 100644 src/__tests__/PaymentManagementBody.test.ts create mode 100644 src/__tests__/PaymentMethodCollectTypes.test.ts create mode 100644 src/__tests__/PaymentMethodCollectUtils.test.ts create mode 100644 src/__tests__/PaymentMethodsRecord.test.ts create mode 100644 src/__tests__/PaymentModeType.test.ts create mode 100644 src/__tests__/PaymentType.test.ts create mode 100644 src/__tests__/PaymentUtils.test.ts create mode 100644 src/__tests__/PaymentUtilsV2.test.ts create mode 100644 src/__tests__/PaypalSDKTypes.test.ts create mode 100644 src/__tests__/PmAuthConnectorUtils.test.ts create mode 100644 src/__tests__/RecoilAtomTypes.test.ts create mode 100644 src/__tests__/S3Utils.test.ts create mode 100644 src/__tests__/SamsungPayHelpers.test.ts create mode 100644 src/__tests__/TaxCalculation.test.ts create mode 100644 src/__tests__/ThirdPartyFlowHelpers.test.ts create mode 100644 src/__tests__/UnifiedHelpersV2.test.ts create mode 100644 src/__tests__/UtilityHooks.test.ts create mode 100644 src/__tests__/VaultHelpers.test.ts create mode 100644 src/__tests__/WebHyperLogger.test.ts create mode 100644 src/__tests__/WebLoggerUtils.test.ts create mode 100644 src/__tests__/WebPaymentUtils.test.ts create mode 100644 src/__tests__/WebSessionsType.test.ts create mode 100644 src/__tests__/WebUtils.test.ts create mode 100644 tsconfig.test.json diff --git a/.gitignore b/.gitignore index f806227d1..4f1a02317 100644 --- a/.gitignore +++ b/.gitignore @@ -26,3 +26,7 @@ user_data.sh *.pem # Sentry Auth Token .env.sentry-build-plugin + +# testing +coverage/ +test-report.xml diff --git a/__mocks__/rescriptReactStub.js b/__mocks__/rescriptReactStub.js new file mode 100644 index 000000000..25b42d319 --- /dev/null +++ b/__mocks__/rescriptReactStub.js @@ -0,0 +1,4 @@ +// Stub for @rescript/react modules that don't ship compiled JS. +// These are normally compiled by the project's ReScript build but aren't +// available in the Jest environment. +module.exports = {}; diff --git a/__tests__/shared-code/CardValidations.test.ts b/__tests__/shared-code/CardValidations.test.ts new file mode 100644 index 000000000..15f006c58 --- /dev/null +++ b/__tests__/shared-code/CardValidations.test.ts @@ -0,0 +1,473 @@ +import { + toInt, + getobjFromCardPattern, + clearSpaces, + slice, + getAllMatchedCardSchemes, + isCardSchemeEnabled, + formatCVCNumber, + getStrFromIndex, + splitExpiryDates, + formatCardExpiryNumber, + cardType, + formatCardNumber, +} from '../../shared-code/sdk-utils/validation/CardValidations.bs.js'; + +describe('CardValidations', () => { + describe('toInt', () => { + it('should convert string number to integer', () => { + expect(toInt('123')).toBe(123); + expect(toInt('0')).toBe(0); + expect(toInt('999')).toBe(999); + }); + + it('should return 0 for non-numeric strings', () => { + expect(toInt('abc')).toBe(0); + expect(toInt('')).toBe(0); + expect(toInt('12abc')).toBe(12); + }); + + it('should handle negative numbers', () => { + expect(toInt('-123')).toBe(-123); + expect(toInt('-0')).toBe(0); + }); + + it('should handle decimal strings by parsing integer part', () => { + expect(toInt('12.34')).toBe(12); + expect(toInt('0.5')).toBe(0); + }); + + it('should handle whitespace in string', () => { + expect(toInt(' 123 ')).toBe(123); + expect(toInt(' 456 ')).toBe(456); + }); + + it('should handle large numbers', () => { + expect(toInt('1234567890')).toBe(1234567890); + }); + }); + + describe('getobjFromCardPattern', () => { + it('should return Visa pattern for "Visa"', () => { + const result = getobjFromCardPattern('Visa'); + expect(result.issuer).toBe('Visa'); + expect(result.maxCVCLength).toBe(3); + expect(result.cvcLength).toEqual([3]); + }); + + it('should return Mastercard pattern for "Mastercard"', () => { + const result = getobjFromCardPattern('Mastercard'); + expect(result.issuer).toBe('Mastercard'); + expect(result.maxCVCLength).toBe(3); + }); + + it('should return AmericanExpress pattern for "AmericanExpress"', () => { + const result = getobjFromCardPattern('AmericanExpress'); + expect(result.issuer).toBe('AmericanExpress'); + expect(result.maxCVCLength).toBe(4); + expect(result.cvcLength).toEqual([4]); + }); + + it('should return default pattern for unknown card type', () => { + const result = getobjFromCardPattern('UnknownCard'); + expect(result.issuer).toBe(''); + expect(result.maxCVCLength).toBe(4); + }); + + it('should return DinersClub pattern', () => { + const result = getobjFromCardPattern('DinersClub'); + expect(result.issuer).toBe('DinersClub'); + expect(result.maxCVCLength).toBe(3); + }); + + it('should return Discover pattern', () => { + const result = getobjFromCardPattern('Discover'); + expect(result.issuer).toBe('Discover'); + expect(result.maxCVCLength).toBe(3); + }); + }); + + describe('clearSpaces', () => { + it('should remove non-digit characters', () => { + expect(clearSpaces('4242 4242 4242 4242')).toBe('4242424242424242'); + expect(clearSpaces('1234-5678-9012')).toBe('123456789012'); + expect(clearSpaces('12/34')).toBe('1234'); + }); + + it('should return only digits from mixed string', () => { + expect(clearSpaces('abc123def456')).toBe('123456'); + expect(clearSpaces('card 4242 number')).toBe('4242'); + }); + + it('should handle string with only digits', () => { + expect(clearSpaces('1234567890')).toBe('1234567890'); + }); + + it('should handle empty string', () => { + expect(clearSpaces('')).toBe(''); + }); + + it('should handle string with no digits', () => { + expect(clearSpaces('abcdef')).toBe(''); + }); + + it('should remove special characters', () => { + expect(clearSpaces('42.42.42.42')).toBe('42424242'); + expect(clearSpaces('42-42-42-42')).toBe('42424242'); + }); + }); + + describe('slice', () => { + it('should slice string with start and end index', () => { + expect(slice('hello world', 0, 5)).toBe('hello'); + expect(slice('1234567890', 2, 7)).toBe('34567'); + }); + + it('should handle start index only', () => { + expect(slice('hello world', 6)).toBe('world'); + expect(slice('12345', 2)).toBe('345'); + }); + + it('should handle out of bounds gracefully', () => { + expect(slice('hello', 0, 100)).toBe('hello'); + expect(slice('hello', 10, 20)).toBe(''); + }); + + it('should handle negative indices', () => { + expect(slice('hello', -2)).toBe('lo'); + expect(slice('hello', -5, -2)).toBe('hel'); + }); + + it('should return empty string for invalid range', () => { + expect(slice('hello', 3, 2)).toBe(''); + }); + + it('should handle empty string', () => { + expect(slice('', 0, 5)).toBe(''); + }); + }); + + describe('getAllMatchedCardSchemes', () => { + it('should return Visa for card starting with 4', () => { + const result = getAllMatchedCardSchemes('4242424242424242'); + expect(result).toContain('Visa'); + }); + + it('should return Mastercard for card starting with 51-55', () => { + const result = getAllMatchedCardSchemes('5242424242424242'); + expect(result).toContain('Mastercard'); + }); + + it('should return Mastercard for card starting with 2221-2720', () => { + const result = getAllMatchedCardSchemes('2221004242424242'); + expect(result).toContain('Mastercard'); + }); + + it('should return AmericanExpress for card starting with 34 or 37', () => { + const result34 = getAllMatchedCardSchemes('342424242424242'); + expect(result34).toContain('AmericanExpress'); + + const result37 = getAllMatchedCardSchemes('372424242424242'); + expect(result37).toContain('AmericanExpress'); + }); + + it('should return Discover for matching pattern', () => { + const result = getAllMatchedCardSchemes('6011424242424242'); + expect(result).toContain('Discover'); + }); + + it('should return DinersClub for matching pattern', () => { + const result = getAllMatchedCardSchemes('36242424242424'); + expect(result).toContain('DinersClub'); + }); + + it('should return JCB for matching pattern', () => { + const result = getAllMatchedCardSchemes('3530111333300000'); + expect(result).toContain('JCB'); + }); + + it('should return empty array for non-matching card number', () => { + const result = getAllMatchedCardSchemes('0000000000000000'); + expect(result).toEqual([]); + }); + + it('should return Maestro for matching pattern', () => { + const result = getAllMatchedCardSchemes('5018424242424242'); + expect(result).toContain('Maestro'); + }); + }); + + describe('isCardSchemeEnabled', () => { + it('should return true when scheme is in enabled list', () => { + expect(isCardSchemeEnabled('Visa', ['Visa', 'Mastercard'])).toBe(true); + expect(isCardSchemeEnabled('Mastercard', ['Visa', 'Mastercard'])).toBe(true); + }); + + it('should return false when scheme is not in enabled list', () => { + expect(isCardSchemeEnabled('Amex', ['Visa', 'Mastercard'])).toBe(false); + expect(isCardSchemeEnabled('Visa', ['Mastercard'])).toBe(false); + }); + + it('should return false for empty enabled list', () => { + expect(isCardSchemeEnabled('Visa', [])).toBe(false); + }); + + it('should be case sensitive', () => { + expect(isCardSchemeEnabled('visa', ['Visa'])).toBe(false); + expect(isCardSchemeEnabled('VISA', ['Visa'])).toBe(false); + }); + + it('should handle duplicate entries in list', () => { + expect(isCardSchemeEnabled('Visa', ['Visa', 'Visa', 'Mastercard'])).toBe(true); + }); + }); + + describe('formatCVCNumber', () => { + it('should format CVC for Visa (max 3 digits)', () => { + expect(formatCVCNumber('123', 'Visa')).toBe('123'); + expect(formatCVCNumber('1234', 'Visa')).toBe('123'); + expect(formatCVCNumber('12', 'Visa')).toBe('12'); + }); + + it('should format CVC for AmericanExpress (max 4 digits)', () => { + expect(formatCVCNumber('1234', 'AmericanExpress')).toBe('1234'); + expect(formatCVCNumber('12345', 'AmericanExpress')).toBe('1234'); + expect(formatCVCNumber('123', 'AmericanExpress')).toBe('123'); + }); + + it('should format CVC for Mastercard (max 3 digits)', () => { + expect(formatCVCNumber('123', 'Mastercard')).toBe('123'); + expect(formatCVCNumber('1234', 'Mastercard')).toBe('123'); + }); + + it('should remove non-digits before formatting', () => { + expect(formatCVCNumber('12a3', 'Visa')).toBe('123'); + expect(formatCVCNumber('1 2 3', 'Visa')).toBe('123'); + }); + + it('should handle empty string', () => { + expect(formatCVCNumber('', 'Visa')).toBe(''); + }); + + it('should handle unknown card type (default max 4)', () => { + expect(formatCVCNumber('12345', 'Unknown')).toBe('1234'); + }); + }); + + describe('getStrFromIndex', () => { + it('should return string at valid index', () => { + expect(getStrFromIndex(['a', 'b', 'c'], 0)).toBe('a'); + expect(getStrFromIndex(['a', 'b', 'c'], 1)).toBe('b'); + expect(getStrFromIndex(['a', 'b', 'c'], 2)).toBe('c'); + }); + + it('should return empty string for out of bounds index', () => { + expect(getStrFromIndex(['a', 'b', 'c'], 3)).toBe(''); + expect(getStrFromIndex(['a', 'b', 'c'], 10)).toBe(''); + expect(getStrFromIndex(['a', 'b', 'c'], -1)).toBe(''); + }); + + it('should return empty string for empty array', () => { + expect(getStrFromIndex([], 0)).toBe(''); + expect(getStrFromIndex([], 5)).toBe(''); + }); + + it('should handle undefined elements', () => { + const arr: (string | undefined)[] = ['a', undefined, 'c']; + expect(getStrFromIndex(arr as string[], 1)).toBe(''); + }); + }); + + describe('splitExpiryDates', () => { + it('should split MM/YY format', () => { + const result = splitExpiryDates('12/25'); + expect(result[0]).toBe('12'); + expect(result[1]).toBe('25'); + }); + + it('should split MM / YY format with spaces', () => { + const result = splitExpiryDates('12 / 25'); + expect(result[0]).toBe('12'); + expect(result[1]).toBe('25'); + }); + + it('should handle single digit month', () => { + const result = splitExpiryDates('1/25'); + expect(result[0]).toBe('1'); + expect(result[1]).toBe('25'); + }); + + it('should handle empty parts', () => { + const result = splitExpiryDates('/25'); + expect(result[0]).toBe(''); + expect(result[1]).toBe('25'); + }); + + it('should handle string without separator', () => { + const result = splitExpiryDates('1225'); + expect(result[0]).toBe('1225'); + expect(result[1]).toBe(''); + }); + + it('should handle empty string', () => { + const result = splitExpiryDates(''); + expect(result[0]).toBe(''); + expect(result[1]).toBe(''); + }); + }); + + describe('formatCardExpiryNumber', () => { + it('should format single digit 2-9 as 0X / ', () => { + expect(formatCardExpiryNumber('2')).toBe('02 / '); + expect(formatCardExpiryNumber('5')).toBe('05 / '); + expect(formatCardExpiryNumber('9')).toBe('09 / '); + }); + + it('should format month > 12 as 0X / Y', () => { + expect(formatCardExpiryNumber('13')).toBe('01 / 3'); + expect(formatCardExpiryNumber('15')).toBe('01 / 5'); + expect(formatCardExpiryNumber('99')).toBe('09 / 9'); + }); + + it('should keep months 01-12 as is', () => { + expect(formatCardExpiryNumber('01')).toBe('01'); + expect(formatCardExpiryNumber('12')).toBe('12'); + }); + + it('should format 3+ digits as MM / YY', () => { + expect(formatCardExpiryNumber('123')).toBe('12 / 3'); + expect(formatCardExpiryNumber('1225')).toBe('12 / 25'); + expect(formatCardExpiryNumber('0626')).toBe('06 / 26'); + }); + + it('should handle month 1 (not 2-9 range)', () => { + expect(formatCardExpiryNumber('1')).toBe('1'); + }); + + it('should remove non-digits before processing', () => { + expect(formatCardExpiryNumber('12/25')).toBe('12 / 25'); + }); + + it('should handle empty string', () => { + expect(formatCardExpiryNumber('')).toBe(''); + }); + + it('should handle 0 as input', () => { + expect(formatCardExpiryNumber('0')).toBe('0'); + }); + }); + + describe('cardType', () => { + it('should return AMEX for AMEX', () => { + expect(cardType('AMEX')).toBe('AMEX'); + }); + + it('should return VISA for VISA', () => { + expect(cardType('VISA')).toBe('VISA'); + }); + + it('should return MASTERCARD for MASTERCARD', () => { + expect(cardType('MASTERCARD')).toBe('MASTERCARD'); + }); + + it('should return DINERSCLUB for DINERSCLUB', () => { + expect(cardType('DINERSCLUB')).toBe('DINERSCLUB'); + }); + + it('should return DISCOVER for DISCOVER', () => { + expect(cardType('DISCOVER')).toBe('DISCOVER'); + }); + + it('should return JCB for JCB', () => { + expect(cardType('JCB')).toBe('JCB'); + }); + + it('should return MAESTRO for MAESTRO', () => { + expect(cardType('MAESTRO')).toBe('MAESTRO'); + }); + + it('should return RUPAY for RUPAY', () => { + expect(cardType('RUPAY')).toBe('RUPAY'); + }); + + it('should return SODEXO for SODEXO', () => { + expect(cardType('SODEXO')).toBe('SODEXO'); + }); + + it('should return BAJAJ for BAJAJ', () => { + expect(cardType('BAJAJ')).toBe('BAJAJ'); + }); + + it('should return CARTESBANCAIRES for CARTESBANCAIRES', () => { + expect(cardType('CARTESBANCAIRES')).toBe('CARTESBANCAIRES'); + }); + + it('should return NOTFOUND for unknown card type', () => { + expect(cardType('UNKNOWN')).toBe('NOTFOUND'); + expect(cardType('')).toBe('NOTFOUND'); + }); + + it('should be case insensitive (uppercase)', () => { + expect(cardType('visa')).toBe('VISA'); + expect(cardType('mastercard')).toBe('MASTERCARD'); + expect(cardType('amex')).toBe('AMEX'); + }); + }); + + describe('formatCardNumber', () => { + it('should format Visa card number (16 digits, groups of 4)', () => { + expect(formatCardNumber('4242424242424242', 'Visa')).toBe('4242 4242 4242 4242'); + }); + + it('should format Mastercard card number (16 digits, groups of 4)', () => { + expect(formatCardNumber('5555555555554444', 'Mastercard')).toBe('5555 5555 5555 4444'); + }); + + it('should format AmericanExpress card number (15 digits, 4-6-5)', () => { + expect(formatCardNumber('378282246310005', 'AMEX')).toBe('3782 822463 10005'); + }); + + it('should format DinersClub card number (14 digits)', () => { + expect(formatCardNumber('38520000023237', 'DINERSCLUB')).toBe('3852 000002 3237'); + }); + + it('should format DinersClub card number (16+ digits)', () => { + const result = formatCardNumber('3852000002323788', 'DINERSCLUB'); + expect(result).toBe('3852 0000 0232 3788'); + }); + + it('should format Discover card number', () => { + expect(formatCardNumber('6011111111111117', 'Discover')).toBe('6011 1111 1111 1117'); + }); + + it('should format unknown card type with default spacing', () => { + expect(formatCardNumber('1234567890123456789', 'Unknown')).toBe('1234 5678 9012 3456789'); + }); + + it('should remove non-digits before formatting', () => { + expect(formatCardNumber('4242-4242-4242-4242', 'Visa')).toBe('4242 4242 4242 4242'); + }); + + it('should handle short card numbers', () => { + expect(formatCardNumber('4242', 'Visa')).toBe('4242'); + expect(formatCardNumber('424242', 'Visa')).toBe('4242 42'); + }); + + it('should trim trailing spaces', () => { + const result = formatCardNumber('4242424242424242', 'Visa'); + expect(result.endsWith(' ')).toBe(false); + }); + + it('should format RuPay card number', () => { + expect(formatCardNumber('5082123456789012', 'RuPay')).toBe('5082 1234 5678 9012'); + }); + + it('should format SODEXO card number', () => { + expect(formatCardNumber('6375131234567890', 'SODEXO')).toBe('6375 1312 3456 7890'); + }); + + it('should format JCB card number', () => { + expect(formatCardNumber('3530111333300000', 'JCB')).toBe('3530 1113 3330 0000'); + }); + }); +}); diff --git a/__tests__/shared-code/CnpjValidation.test.ts b/__tests__/shared-code/CnpjValidation.test.ts new file mode 100644 index 000000000..063d53a63 --- /dev/null +++ b/__tests__/shared-code/CnpjValidation.test.ts @@ -0,0 +1,174 @@ +import { + cnpjLength, + invalidCNPJs, + isNumeric, + isUppercaseAlphanumeric, + isCNPJValidFormat, + charToValue, + calculateCheckDigit, + validateCNPJ, + isValidCNPJ, +} from '../../shared-code/sdk-utils/validation/CnpjValidation.bs.js'; + +describe('CnpjValidation', () => { + describe('cnpjLength', () => { + it('should be 14', () => { + expect(cnpjLength).toBe(14); + }); + }); + + describe('invalidCNPJs', () => { + it('should contain known invalid CNPJs', () => { + expect(invalidCNPJs).toContain('00000000000000'); + expect(invalidCNPJs).toContain('11111111111111'); + expect(invalidCNPJs).toContain('99999999999999'); + }); + + it('should have 10 entries', () => { + expect(invalidCNPJs).toHaveLength(10); + }); + }); + + describe('isNumeric', () => { + it('should return true for digits only', () => { + expect(isNumeric('1234567890')).toBe(true); + }); + + it('should return true for empty string', () => { + expect(isNumeric('')).toBe(true); + }); + + it('should return false for string with letters', () => { + expect(isNumeric('123ABC')).toBe(false); + }); + + it('should return false for string with special characters', () => { + expect(isNumeric('123-456')).toBe(false); + }); + }); + + describe('isUppercaseAlphanumeric', () => { + it('should return true for uppercase letters and digits', () => { + expect(isUppercaseAlphanumeric('ABC123')).toBe(true); + }); + + it('should return true for empty string', () => { + expect(isUppercaseAlphanumeric('')).toBe(true); + }); + + it('should return false for lowercase letters', () => { + expect(isUppercaseAlphanumeric('abc123')).toBe(false); + }); + + it('should return false for special characters', () => { + expect(isUppercaseAlphanumeric('ABC-123')).toBe(false); + }); + }); + + describe('isCNPJValidFormat', () => { + it('should return true for valid CNPJ format with all digits', () => { + expect(isCNPJValidFormat('11222333000181')).toBe(true); + }); + + it('should return true for valid CNPJ format with alphanumeric base', () => { + expect(isCNPJValidFormat('ABC12345678901')).toBe(true); + }); + + it('should return false for CNPJ with letters in check digits', () => { + expect(isCNPJValidFormat('112223330001AB')).toBe(false); + }); + + it('should return false for CNPJ with lowercase letters', () => { + expect(isCNPJValidFormat('abc12345678901')).toBe(false); + }); + + it('should return true for valid format even if check digits section is short', () => { + expect(isCNPJValidFormat('1234567890123')).toBe(true); + }); + + it('should return false for CNPJ with special characters in base', () => { + expect(isCNPJValidFormat('ABC-1234567891')).toBe(false); + }); + }); + + describe('charToValue', () => { + it('should return correct value for digit 0', () => { + expect(charToValue('0')).toBe(0); + }); + + it('should return correct value for digit 9', () => { + expect(charToValue('9')).toBe(9); + }); + + it('should return correct value for uppercase A', () => { + expect(charToValue('A')).toBe(17); + }); + + it('should return correct value for uppercase Z', () => { + expect(charToValue('Z')).toBe(42); + }); + + it('should return 0 for lowercase letters', () => { + expect(charToValue('a')).toBe(0); + }); + + it('should return 0 for special characters', () => { + expect(charToValue('-')).toBe(0); + }); + }); + + describe('calculateCheckDigit', () => { + it('should calculate correct check digit for first digit', () => { + const values = [1, 1, 2, 2, 2, 3, 3, 3, 0, 0, 0, 1]; + const weights = [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2]; + expect(calculateCheckDigit(values, weights)).toBe(8); + }); + + it('should return 0 when remainder is less than 2', () => { + const values = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + const weights = [5, 4, 3, 2, 9, 8, 7, 6, 5, 4, 3, 2]; + expect(calculateCheckDigit(values, weights)).toBe(0); + }); + + it('should handle empty values array', () => { + expect(calculateCheckDigit([], [])).toBe(0); + }); + }); + + describe('validateCNPJ', () => { + it('should return true for valid CNPJ', () => { + expect(validateCNPJ('11222333000181')).toBe(true); + }); + + it('should return false for invalid check digits', () => { + expect(validateCNPJ('11222333000182')).toBe(false); + }); + + it('should return false for CNPJ with wrong length', () => { + expect(validateCNPJ('1234567890123')).toBe(false); + }); + }); + + describe('isValidCNPJ', () => { + it('should return true for valid CNPJ', () => { + expect(isValidCNPJ('11222333000181')).toBe(true); + }); + + it('should return false for CNPJ in invalid list', () => { + expect(isValidCNPJ('00000000000000')).toBe(false); + expect(isValidCNPJ('11111111111111')).toBe(false); + }); + + it('should return false for wrong length', () => { + expect(isValidCNPJ('1234567890')).toBe(false); + }); + + it('should return false for invalid format', () => { + expect(isValidCNPJ('abcdefghijklmn')).toBe(false); + }); + + it('should return false for invalid check digits', () => { + expect(isValidCNPJ('11222333000182')).toBe(false); + }); + }); +}); diff --git a/__tests__/shared-code/CommonUtils.test.ts b/__tests__/shared-code/CommonUtils.test.ts new file mode 100644 index 000000000..2d5ebd549 --- /dev/null +++ b/__tests__/shared-code/CommonUtils.test.ts @@ -0,0 +1,706 @@ +import { + getOptionString, + getString, + getStringFromJson, + getInt, + getFloatFromString, + getFloatFromJson, + getFloat, + getJsonBoolValue, + getJsonStringFromDict, + getJsonArrayFromDict, + getJsonFromDict, + getJsonObjFromDict, + getDecodedStringFromJson, + getDecodedBoolFromJson, + getDictFromObj, + getJsonObjectFromDict, + getOptionBool, + getDictFromJson, + getDictFromDict, + getBool, + getOptionsDict, + getOptionalArrayFromDict, + getArray, + getStrArray, + convertDictToArrayOfKeyStringTuples, + getStringFromOptionalJson, + snakeToPascalCase, + getArrayElement, + mergeDict, + getDisplayName, +} from '../../shared-code/sdk-utils/utils/CommonUtils.bs.js'; + +describe('CommonUtils', () => { + describe('getOptionString', () => { + it('should return string value when key exists with string', () => { + const dict = { name: 'test' }; + expect(getOptionString(dict, 'name')).toBe('test'); + }); + + it('should return undefined when key does not exist', () => { + const dict = { name: 'test' }; + expect(getOptionString(dict, 'missing')).toBeUndefined(); + }); + + it('should return undefined when value is not a string', () => { + const dict = { count: 42 }; + expect(getOptionString(dict, 'count')).toBeUndefined(); + }); + + it('should return empty string when value is empty string', () => { + const dict = { name: '' }; + expect(getOptionString(dict, 'name')).toBe(''); + }); + + it('should handle null value', () => { + const dict = { name: null }; + expect(getOptionString(dict, 'name')).toBeUndefined(); + }); + }); + + describe('getString', () => { + it('should return string value when key exists', () => { + const dict = { name: 'test' }; + expect(getString(dict, 'name', 'default')).toBe('test'); + }); + + it('should return default when key does not exist', () => { + const dict = { name: 'test' }; + expect(getString(dict, 'missing', 'default')).toBe('default'); + }); + + it('should return default when value is not a string', () => { + const dict = { count: 42 }; + expect(getString(dict, 'count', 'default')).toBe('default'); + }); + + it('should return empty string when value is empty string', () => { + const dict = { name: '' }; + expect(getString(dict, 'name', 'default')).toBe(''); + }); + + it('should handle undefined dict value', () => { + const dict = {}; + expect(getString(dict, 'key', 'fallback')).toBe('fallback'); + }); + }); + + describe('getStringFromJson', () => { + it('should return string value from JSON string', () => { + expect(getStringFromJson('hello', 'default')).toBe('hello'); + }); + + it('should return default for non-string JSON', () => { + expect(getStringFromJson(42, 'default')).toBe('default'); + }); + + it('should return default for null', () => { + expect(getStringFromJson(null, 'default')).toBe('default'); + }); + + it('should return empty string when value is empty string', () => { + expect(getStringFromJson('', 'default')).toBe(''); + }); + }); + + describe('getInt', () => { + it('should return int value when key exists with number', () => { + const dict = { count: 42 }; + expect(getInt(dict, 'count', 0)).toBe(42); + }); + + it('should return default when key does not exist', () => { + const dict = {}; + expect(getInt(dict, 'count', 10)).toBe(10); + }); + + it('should return default when value is not a number', () => { + const dict = { count: 'not a number' }; + expect(getInt(dict, 'count', 5)).toBe(5); + }); + + it('should truncate float to int', () => { + const dict = { count: 42.7 }; + expect(getInt(dict, 'count', 0)).toBe(42); + }); + + it('should return default for string number value', () => { + const dict = { count: '42' }; + expect(getInt(dict, 'count', 0)).toBe(0); + }); + }); + + describe('getFloatFromString', () => { + it('should parse valid float string', () => { + expect(getFloatFromString('3.14', 0)).toBe(3.14); + }); + + it('should return default for invalid string', () => { + expect(getFloatFromString('invalid', 0)).toBe(0); + }); + + it('should parse integer string', () => { + expect(getFloatFromString('42', 0)).toBe(42); + }); + + it('should handle negative numbers', () => { + expect(getFloatFromString('-10.5', 0)).toBe(-10.5); + }); + + it('should return default for empty string', () => { + expect(getFloatFromString('', 99)).toBe(99); + }); + }); + + describe('getFloatFromJson', () => { + it('should return float from number JSON', () => { + expect(getFloatFromJson(3.14, 0)).toBe(3.14); + }); + + it('should parse float from string JSON', () => { + expect(getFloatFromJson('3.14', 0)).toBe(3.14); + }); + + it('should return default for non-numeric string', () => { + expect(getFloatFromJson('invalid', 0)).toBe(0); + }); + + it('should return default for null', () => { + expect(getFloatFromJson(null, 99)).toBe(99); + }); + + it('should return default for boolean', () => { + expect(getFloatFromJson(true, 0)).toBe(0); + }); + + it('should return default for object', () => { + expect(getFloatFromJson({}, 0)).toBe(0); + }); + }); + + describe('getFloat', () => { + it('should return float value when key exists with number', () => { + const dict = { price: 99.99 }; + expect(getFloat(dict, 'price', 0)).toBe(99.99); + }); + + it('should parse float from string value', () => { + const dict = { price: '99.99' }; + expect(getFloat(dict, 'price', 0)).toBe(99.99); + }); + + it('should return default when key does not exist', () => { + const dict = {}; + expect(getFloat(dict, 'price', 50)).toBe(50); + }); + + it('should return default when value is not numeric', () => { + const dict = { price: 'invalid' }; + expect(getFloat(dict, 'price', 0)).toBe(0); + }); + }); + + describe('getJsonBoolValue', () => { + it('should return boolean value when key exists', () => { + const dict = { enabled: true }; + expect(getJsonBoolValue(dict, 'enabled', false)).toBe(true); + }); + + it('should return default when key does not exist', () => { + const dict = {}; + expect(getJsonBoolValue(dict, 'enabled', true)).toBe(true); + }); + + it('should handle false value', () => { + const dict = { enabled: false }; + expect(getJsonBoolValue(dict, 'enabled', true)).toBe(false); + }); + + it('should return default for non-boolean value', () => { + const dict = { enabled: 'yes' }; + expect(getJsonBoolValue(dict, 'enabled', false)).toBe('yes'); + }); + }); + + describe('getJsonStringFromDict', () => { + it('should return string value when key exists', () => { + const dict = { name: 'test' }; + expect(getJsonStringFromDict(dict, 'name', 'default')).toBe('test'); + }); + + it('should return default when key does not exist', () => { + const dict = {}; + expect(getJsonStringFromDict(dict, 'name', 'default')).toBe('default'); + }); + + it('should return the raw value even if not string', () => { + const dict = { count: 42 }; + expect(getJsonStringFromDict(dict, 'count', 'default')).toBe(42); + }); + }); + + describe('getJsonArrayFromDict', () => { + it('should return array value when key exists', () => { + const dict = { items: [1, 2, 3] }; + expect(getJsonArrayFromDict(dict, 'items', [])).toEqual([1, 2, 3]); + }); + + it('should return default when key does not exist', () => { + const dict = {}; + expect(getJsonArrayFromDict(dict, 'items', [1])).toEqual([1]); + }); + + it('should return the raw value even if not array', () => { + const dict = { items: 'not an array' }; + expect(getJsonArrayFromDict(dict, 'items', [])).toBe('not an array'); + }); + }); + + describe('getJsonFromDict', () => { + it('should return value when key exists', () => { + const dict = { data: { nested: true } }; + expect(getJsonFromDict(dict, 'data', null)).toEqual({ nested: true }); + }); + + it('should return default when key does not exist', () => { + const dict = {}; + expect(getJsonFromDict(dict, 'data', 'default')).toBe('default'); + }); + + it('should return null value when value is null', () => { + const dict = { data: null }; + expect(getJsonFromDict(dict, 'data', 'default')).toBe(null); + }); + }); + + describe('getJsonObjFromDict', () => { + it('should return object value when key exists', () => { + const dict = { config: { theme: 'dark' } }; + expect(getJsonObjFromDict(dict, 'config', {})).toEqual({ theme: 'dark' }); + }); + + it('should return default when key does not exist', () => { + const dict = {}; + expect(getJsonObjFromDict(dict, 'config', { default: true })).toEqual({ default: true }); + }); + + it('should return default when value is not an object', () => { + const dict = { config: 'string' }; + expect(getJsonObjFromDict(dict, 'config', {})).toEqual({}); + }); + }); + + describe('getDecodedStringFromJson', () => { + it('should decode nested string from JSON object', () => { + const json = { nested: 'found' }; + const result = getDecodedStringFromJson(json, (obj: any) => obj['nested'], 'default'); + expect(result).toBe('found'); + }); + + it('should return default when path does not exist', () => { + const json = { other: {} }; + const result = getDecodedStringFromJson(json, (obj: any) => obj['missing'], 'default'); + expect(result).toBe('default'); + }); + + it('should return default for null JSON', () => { + const result = getDecodedStringFromJson(null, (obj: any) => obj, 'default'); + expect(result).toBe('default'); + }); + }); + + describe('getDecodedBoolFromJson', () => { + it('should decode nested bool from JSON object', () => { + const json = { nested: true }; + const result = getDecodedBoolFromJson(json, (obj: any) => obj['nested'], false); + expect(result).toBe(true); + }); + + it('should return default when path does not exist', () => { + const json = { other: {} }; + const result = getDecodedBoolFromJson(json, (obj: any) => obj['missing'], true); + expect(result).toBe(true); + }); + + it('should return default for null JSON', () => { + const result = getDecodedBoolFromJson(null, (obj: any) => obj, false); + expect(result).toBe(false); + }); + }); + + describe('getDictFromObj', () => { + it('should return dict when key exists with object', () => { + const dict = { nested: { key: 'value' } }; + expect(getDictFromObj(dict, 'nested')).toEqual({ key: 'value' }); + }); + + it('should return empty object when key does not exist', () => { + const dict = {}; + expect(getDictFromObj(dict, 'missing')).toEqual({}); + }); + + it('should return empty object when value is not an object', () => { + const dict = { nested: 'string' }; + expect(getDictFromObj(dict, 'nested')).toEqual({}); + }); + }); + + describe('getJsonObjectFromDict', () => { + it('should return object when key exists', () => { + const dict = { data: { key: 'value' } }; + expect(getJsonObjectFromDict(dict, 'data')).toEqual({ key: 'value' }); + }); + + it('should return empty object when key does not exist', () => { + const dict = {}; + expect(getJsonObjectFromDict(dict, 'missing')).toEqual({}); + }); + + it('should return null value when value is null', () => { + const dict = { data: null }; + expect(getJsonObjectFromDict(dict, 'data')).toBe(null); + }); + }); + + describe('getOptionBool', () => { + it('should return boolean value when key exists with bool', () => { + const dict = { enabled: true }; + expect(getOptionBool(dict, 'enabled')).toBe(true); + }); + + it('should return undefined when key does not exist', () => { + const dict = {}; + expect(getOptionBool(dict, 'enabled')).toBeUndefined(); + }); + + it('should return undefined when value is not a boolean', () => { + const dict = { enabled: 'yes' }; + expect(getOptionBool(dict, 'enabled')).toBeUndefined(); + }); + + it('should handle false value', () => { + const dict = { enabled: false }; + expect(getOptionBool(dict, 'enabled')).toBe(false); + }); + }); + + describe('getDictFromJson', () => { + it('should return dict from JSON object', () => { + expect(getDictFromJson({ key: 'value' })).toEqual({ key: 'value' }); + }); + + it('should return empty object for null', () => { + expect(getDictFromJson(null)).toEqual({}); + }); + + it('should return empty object for non-object', () => { + expect(getDictFromJson('string')).toEqual({}); + }); + + it('should return empty object for array', () => { + expect(getDictFromJson([1, 2, 3])).toEqual({}); + }); + }); + + describe('getDictFromDict', () => { + it('should return nested dict when key exists', () => { + const dict = { nested: { inner: 'value' } }; + expect(getDictFromDict(dict, 'nested')).toEqual({ inner: 'value' }); + }); + + it('should return empty object when key does not exist', () => { + const dict = {}; + expect(getDictFromDict(dict, 'missing')).toEqual({}); + }); + + it('should return empty object when value is not an object', () => { + const dict = { nested: 'string' }; + expect(getDictFromDict(dict, 'nested')).toEqual({}); + }); + }); + + describe('getBool', () => { + it('should return boolean value when key exists', () => { + const dict = { enabled: true }; + expect(getBool(dict, 'enabled', false)).toBe(true); + }); + + it('should return default when key does not exist', () => { + const dict = {}; + expect(getBool(dict, 'enabled', true)).toBe(true); + }); + + it('should return default when value is not a boolean', () => { + const dict = { enabled: 'yes' }; + expect(getBool(dict, 'enabled', false)).toBe(false); + }); + + it('should handle false value', () => { + const dict = { enabled: false }; + expect(getBool(dict, 'enabled', true)).toBe(false); + }); + }); + + describe('getOptionsDict', () => { + it('should return dict from options with object', () => { + expect(getOptionsDict({ key: 'value' })).toEqual({ key: 'value' }); + }); + + it('should return empty object for null options', () => { + expect(getOptionsDict(null)).toEqual({}); + }); + + it('should return empty object for undefined options', () => { + expect(getOptionsDict(undefined)).toEqual({}); + }); + }); + + describe('getOptionalArrayFromDict', () => { + it('should return array when key exists with array', () => { + const dict = { items: [1, 2, 3] }; + expect(getOptionalArrayFromDict(dict, 'items')).toEqual([1, 2, 3]); + }); + + it('should return undefined when key does not exist', () => { + const dict = {}; + expect(getOptionalArrayFromDict(dict, 'items')).toBeUndefined(); + }); + + it('should return undefined when value is not an array', () => { + const dict = { items: 'not an array' }; + expect(getOptionalArrayFromDict(dict, 'items')).toBeUndefined(); + }); + }); + + describe('getArray', () => { + it('should return array when key exists with array', () => { + const dict = { items: [1, 2, 3] }; + expect(getArray(dict, 'items')).toEqual([1, 2, 3]); + }); + + it('should return empty array when key does not exist', () => { + const dict = {}; + expect(getArray(dict, 'items')).toEqual([]); + }); + + it('should return empty array when value is not an array', () => { + const dict = { items: 'not an array' }; + expect(getArray(dict, 'items')).toEqual([]); + }); + }); + + describe('getStrArray', () => { + it('should return string array when key exists', () => { + const dict = { names: ['a', 'b', 'c'] }; + expect(getStrArray(dict, 'names')).toEqual(['a', 'b', 'c']); + }); + + it('should return empty array when key does not exist', () => { + const dict = {}; + expect(getStrArray(dict, 'names')).toEqual([]); + }); + + it('should convert non-string items to empty strings', () => { + const dict = { mixed: [1, 'b', true] }; + expect(getStrArray(dict, 'mixed')).toEqual(['', 'b', '']); + }); + + it('should return empty array when value is not an array', () => { + const dict = { names: 'not an array' }; + expect(getStrArray(dict, 'names')).toEqual([]); + }); + }); + + describe('convertDictToArrayOfKeyStringTuples', () => { + it('should convert dict to array of key-value tuples', () => { + const dict = { a: '1', b: '2' }; + const result = convertDictToArrayOfKeyStringTuples(dict); + expect(result).toContainEqual(['a', '1']); + expect(result).toContainEqual(['b', '2']); + }); + + it('should return empty array for empty dict', () => { + expect(convertDictToArrayOfKeyStringTuples({})).toEqual([]); + }); + + it('should use empty string for non-string values', () => { + const dict = { num: 42, bool: true }; + const result = convertDictToArrayOfKeyStringTuples(dict); + expect(result).toContainEqual(['num', '']); + expect(result).toContainEqual(['bool', '']); + }); + }); + + describe('getStringFromOptionalJson', () => { + it('should return string value from JSON', () => { + expect(getStringFromOptionalJson('hello', 'default')).toBe('hello'); + }); + + it('should return default for null JSON', () => { + expect(getStringFromOptionalJson(null, 'default')).toBe('default'); + }); + + it('should return default for undefined JSON', () => { + expect(getStringFromOptionalJson(undefined, 'default')).toBe('default'); + }); + + it('should return default for non-string JSON', () => { + expect(getStringFromOptionalJson(42, 'default')).toBe('default'); + }); + }); + + describe('snakeToPascalCase', () => { + it('should convert snake_case to PascalCase', () => { + expect(snakeToPascalCase('hello_world')).toBe('HelloWorld'); + }); + + it('should handle single word', () => { + expect(snakeToPascalCase('hello')).toBe('Hello'); + }); + + it('should handle empty string', () => { + expect(snakeToPascalCase('')).toBe(''); + }); + + it('should handle multiple underscores', () => { + expect(snakeToPascalCase('hello__world')).toBe('HelloWorld'); + }); + + it('should handle leading underscore', () => { + expect(snakeToPascalCase('_hello_world')).toBe('HelloWorld'); + }); + + it('should handle trailing underscore', () => { + expect(snakeToPascalCase('hello_world_')).toBe('HelloWorld'); + }); + + it('should handle already capitalized words', () => { + expect(snakeToPascalCase('Hello_World')).toBe('HelloWorld'); + }); + + it('should handle three words', () => { + expect(snakeToPascalCase('one_two_three')).toBe('OneTwoThree'); + }); + }); + + describe('getArrayElement', () => { + it('should return element at valid index', () => { + expect(getArrayElement(['a', 'b', 'c'], 1, 'default')).toBe('b'); + }); + + it('should return default for out of bounds index', () => { + expect(getArrayElement(['a', 'b', 'c'], 5, 'default')).toBe('default'); + }); + + it('should return default for negative index', () => { + expect(getArrayElement(['a', 'b', 'c'], -1, 'default')).toBe('default'); + }); + + it('should return first element at index 0', () => { + expect(getArrayElement(['a', 'b', 'c'], 0, 'default')).toBe('a'); + }); + + it('should return default for empty array', () => { + expect(getArrayElement([], 0, 'default')).toBe('default'); + }); + }); + + describe('mergeDict', () => { + it('should merge two flat dicts', () => { + const dict1 = { a: '1', b: '2' }; + const dict2 = { c: '3', d: '4' }; + expect(mergeDict(dict1, dict2)).toEqual({ a: '1', b: '2', c: '3', d: '4' }); + }); + + it('should overwrite values from dict2', () => { + const dict1 = { a: '1', b: '2' }; + const dict2 = { b: 'new', c: '3' }; + expect(mergeDict(dict1, dict2)).toEqual({ a: '1', b: 'new', c: '3' }); + }); + + it('should recursively merge nested objects', () => { + const dict1 = { nested: { a: '1', b: '2' } }; + const dict2 = { nested: { b: 'new', c: '3' } }; + expect(mergeDict(dict1, dict2)).toEqual({ nested: { a: '1', b: 'new', c: '3' } }); + }); + + it('should return dict1 when dict2 is empty', () => { + const dict1 = { a: '1' }; + expect(mergeDict(dict1, {})).toEqual({ a: '1' }); + }); + + it('should return dict2 values when dict1 is empty', () => { + const dict2 = { a: '1' }; + expect(mergeDict({}, dict2)).toEqual({ a: '1' }); + }); + + it('should not modify original dicts', () => { + const dict1 = { a: '1' }; + const dict2 = { b: '2' }; + const result = mergeDict(dict1, dict2); + expect(dict1).toEqual({ a: '1' }); + expect(dict2).toEqual({ b: '2' }); + }); + + it('should replace object with non-object from dict2', () => { + const dict1 = { a: { nested: true } }; + const dict2 = { a: 'string' }; + expect(mergeDict(dict1, dict2)).toEqual({ a: 'string' }); + }); + }); + + describe('getDisplayName', () => { + it('should transform afterpay_clearpay to Afterpay', () => { + expect(getDisplayName('afterpay_clearpay')).toBe('Afterpay'); + }); + + it('should transform bnb_smart_chain to BNB Smart Chain', () => { + expect(getDisplayName('bnb_smart_chain')).toBe('BNB Smart Chain'); + }); + + it('should transform classic to Cash / Voucher', () => { + expect(getDisplayName('classic')).toBe('Cash / Voucher'); + }); + + it('should transform credit to Card', () => { + expect(getDisplayName('credit')).toBe('Card'); + }); + + it('should transform crypto_currency to Crypto', () => { + expect(getDisplayName('crypto_currency')).toBe('Crypto'); + }); + + it('should transform evoucher to E-Voucher', () => { + expect(getDisplayName('evoucher')).toBe('E-Voucher'); + }); + + it('should append Debit to ach', () => { + expect(getDisplayName('ach')).toBe('Ach Debit'); + }); + + it('should append Debit to bacs', () => { + expect(getDisplayName('bacs')).toBe('Bacs Debit'); + }); + + it('should append Debit to becs', () => { + expect(getDisplayName('becs')).toBe('Becs Debit'); + }); + + it('should append Debit to sepa', () => { + expect(getDisplayName('sepa')).toBe('Sepa Debit'); + }); + + it('should capitalize and space-separate unknown values', () => { + expect(getDisplayName('some_method')).toBe('Some Method'); + }); + + it('should handle single word', () => { + expect(getDisplayName('visa')).toBe('Visa'); + }); + + it('should handle empty string', () => { + expect(getDisplayName('')).toBe(''); + }); + }); +}); diff --git a/__tests__/shared-code/LocaleDataType.test.ts b/__tests__/shared-code/LocaleDataType.test.ts new file mode 100644 index 000000000..137fe47a0 --- /dev/null +++ b/__tests__/shared-code/LocaleDataType.test.ts @@ -0,0 +1,444 @@ +import { + localeTypeToString, + localeStringToType, + localeStringToLocaleName, + defaultLocale, +} from '../../shared-code/sdk-utils/types/LocaleDataType.bs.js'; + +describe('LocaleDataType', () => { + describe('localeTypeToString', () => { + it('should convert English locale type to string', () => { + expect(localeTypeToString('En')).toBe('en'); + }); + + it('should convert Hebrew locale type to string', () => { + expect(localeTypeToString('He')).toBe('he'); + }); + + it('should convert French locale type to string', () => { + expect(localeTypeToString('Fr')).toBe('fr'); + }); + + it('should convert English GB locale type to string', () => { + expect(localeTypeToString('En_GB')).toBe('en-GB'); + }); + + it('should convert Arabic locale type to string', () => { + expect(localeTypeToString('Ar')).toBe('ar'); + }); + + it('should convert Japanese locale type to string', () => { + expect(localeTypeToString('Ja')).toBe('ja'); + }); + + it('should convert German locale type to string', () => { + expect(localeTypeToString('De')).toBe('de'); + }); + + it('should convert French Belgium locale type to string', () => { + expect(localeTypeToString('Fr_BE')).toBe('fr-BE'); + }); + + it('should convert Spanish locale type to string', () => { + expect(localeTypeToString('Es')).toBe('es'); + }); + + it('should convert Catalan locale type to string', () => { + expect(localeTypeToString('Ca')).toBe('ca'); + }); + + it('should convert Portuguese locale type to string', () => { + expect(localeTypeToString('Pt')).toBe('pt'); + }); + + it('should convert Italian locale type to string', () => { + expect(localeTypeToString('It')).toBe('it'); + }); + + it('should convert Polish locale type to string', () => { + expect(localeTypeToString('Pl')).toBe('pl'); + }); + + it('should convert Dutch locale type to string', () => { + expect(localeTypeToString('Nl')).toBe('nl'); + }); + + it('should convert Dutch Belgium locale type to string', () => { + expect(localeTypeToString('NI_BE')).toBe('nI-BE'); + }); + + it('should convert Swedish locale type to string', () => { + expect(localeTypeToString('Sv')).toBe('sv'); + }); + + it('should convert Russian locale type to string', () => { + expect(localeTypeToString('Ru')).toBe('ru'); + }); + + it('should convert Lithuanian locale type to string', () => { + expect(localeTypeToString('Lt')).toBe('lt'); + }); + + it('should convert Czech locale type to string', () => { + expect(localeTypeToString('Cs')).toBe('cs'); + }); + + it('should convert Slovak locale type to string', () => { + expect(localeTypeToString('Sk')).toBe('sk'); + }); + + it('should convert Lesotho locale type to string', () => { + expect(localeTypeToString('Ls')).toBe('ls'); + }); + + it('should convert Welsh locale type to string', () => { + expect(localeTypeToString('Cy')).toBe('cy'); + }); + + it('should convert Greek locale type to string', () => { + expect(localeTypeToString('El')).toBe('el'); + }); + + it('should convert Estonian locale type to string', () => { + expect(localeTypeToString('Et')).toBe('et'); + }); + + it('should convert Finnish locale type to string', () => { + expect(localeTypeToString('Fi')).toBe('fi'); + }); + + it('should convert Norwegian Bokmal locale type to string', () => { + expect(localeTypeToString('Nb')).toBe('nb'); + }); + + it('should convert Bosnian locale type to string', () => { + expect(localeTypeToString('Bs')).toBe('bs'); + }); + + it('should convert Danish locale type to string', () => { + expect(localeTypeToString('Da')).toBe('da'); + }); + + it('should convert Malay locale type to string', () => { + expect(localeTypeToString('Ms')).toBe('ms'); + }); + + it('should convert Turkish Cyprus locale type to string', () => { + expect(localeTypeToString('Tr_CY')).toBe('tr-CY'); + }); + + it('should return "en" for undefined input', () => { + expect(localeTypeToString(undefined)).toBe('en'); + }); + }); + + describe('localeStringToType', () => { + it('should convert "en" string to En type', () => { + expect(localeStringToType('en')).toBe('En'); + }); + + it('should convert "he" string to He type', () => { + expect(localeStringToType('he')).toBe('He'); + }); + + it('should convert "fr" string to Fr type', () => { + expect(localeStringToType('fr')).toBe('Fr'); + }); + + it('should convert "en-GB" string to En_GB type', () => { + expect(localeStringToType('en-GB')).toBe('En_GB'); + }); + + it('should convert "ar" string to Ar type', () => { + expect(localeStringToType('ar')).toBe('Ar'); + }); + + it('should convert "ja" string to Ja type', () => { + expect(localeStringToType('ja')).toBe('Ja'); + }); + + it('should convert "de" string to De type', () => { + expect(localeStringToType('de')).toBe('De'); + }); + + it('should convert "es" string to Es type', () => { + expect(localeStringToType('es')).toBe('Es'); + }); + + it('should convert "fr-BE" string to Fr_BE type', () => { + expect(localeStringToType('fr-BE')).toBe('Fr_BE'); + }); + + it('should convert "ca" string to Ca type', () => { + expect(localeStringToType('ca')).toBe('Ca'); + }); + + it('should convert "cs" string to Cs type', () => { + expect(localeStringToType('cs')).toBe('Cs'); + }); + + it('should convert "cy" string to Cy type', () => { + expect(localeStringToType('cy')).toBe('Cy'); + }); + + it('should convert "da" string to Da type', () => { + expect(localeStringToType('da')).toBe('Da'); + }); + + it('should convert "el" string to El type', () => { + expect(localeStringToType('el')).toBe('El'); + }); + + it('should convert "et" string to Et type', () => { + expect(localeStringToType('et')).toBe('Et'); + }); + + it('should convert "fi" string to Fi type', () => { + expect(localeStringToType('fi')).toBe('Fi'); + }); + + it('should convert "nI-BE" string to NI_BE type', () => { + expect(localeStringToType('nI-BE')).toBe('NI_BE'); + }); + + it('should convert "nb" string to Nb type', () => { + expect(localeStringToType('nb')).toBe('Nb'); + }); + + it('should convert "bs" string to Bs type', () => { + expect(localeStringToType('bs')).toBe('Bs'); + }); + + it('should convert "ms" string to Ms type', () => { + expect(localeStringToType('ms')).toBe('Ms'); + }); + + it('should convert "lt" string to Lt type', () => { + expect(localeStringToType('lt')).toBe('Lt'); + }); + + it('should convert "ls" string to Ls type', () => { + expect(localeStringToType('ls')).toBe('Ls'); + }); + + it('should convert "nl" string to Nl type', () => { + expect(localeStringToType('nl')).toBe('Nl'); + }); + + it('should convert "pl" string to Pl type', () => { + expect(localeStringToType('pl')).toBe('Pl'); + }); + + it('should convert "pt" string to Pt type', () => { + expect(localeStringToType('pt')).toBe('Pt'); + }); + + it('should convert "ru" string to Ru type', () => { + expect(localeStringToType('ru')).toBe('Ru'); + }); + + it('should convert "sk" string to Sk type', () => { + expect(localeStringToType('sk')).toBe('Sk'); + }); + + it('should convert "sv" string to Sv type', () => { + expect(localeStringToType('sv')).toBe('Sv'); + }); + + it('should convert "tr-CY" to Tr_CY type', () => { + expect(localeStringToType('tr-CY')).toBe('Tr_CY'); + }); + + it('should return "En" for unknown locale string', () => { + expect(localeStringToType('unknown')).toBe('En'); + }); + + it('should return "En" for empty string', () => { + expect(localeStringToType('')).toBe('En'); + }); + }); + + describe('localeStringToLocaleName', () => { + it('should convert "DE" to German', () => { + expect(localeStringToLocaleName('DE')).toBe('German'); + }); + + it('should convert "DA" to Danish', () => { + expect(localeStringToLocaleName('DA')).toBe('Danish'); + }); + + it('should convert "DA_DK" to Danish', () => { + expect(localeStringToLocaleName('DA_DK')).toBe('Danish'); + }); + + it('should convert "DK" to Danish', () => { + expect(localeStringToLocaleName('DK')).toBe('Danish'); + }); + + it('should convert "EN" to English', () => { + expect(localeStringToLocaleName('EN')).toBe('English'); + }); + + it('should convert "ES" to Spanish', () => { + expect(localeStringToLocaleName('ES')).toBe('Spanish'); + }); + + it('should convert "FI" to Finnish', () => { + expect(localeStringToLocaleName('FI')).toBe('Finnish'); + }); + + it('should convert "FR" to French', () => { + expect(localeStringToLocaleName('FR')).toBe('French'); + }); + + it('should convert "EL" to Greek', () => { + expect(localeStringToLocaleName('EL')).toBe('Greek'); + }); + + it('should convert "EL_GR" to Greek', () => { + expect(localeStringToLocaleName('EL_GR')).toBe('Greek'); + }); + + it('should convert "GR" to Greek', () => { + expect(localeStringToLocaleName('GR')).toBe('Greek'); + }); + + it('should convert "HR" to Croatian', () => { + expect(localeStringToLocaleName('HR')).toBe('Croatian'); + }); + + it('should convert "IT" to Italian', () => { + expect(localeStringToLocaleName('IT')).toBe('Italian'); + }); + + it('should convert "JA" to Japanese', () => { + expect(localeStringToLocaleName('JA')).toBe('Japanese'); + }); + + it('should convert "JA_JP" to Japanese', () => { + expect(localeStringToLocaleName('JA_JP')).toBe('Japanese'); + }); + + it('should convert "JP" to Japanese', () => { + expect(localeStringToLocaleName('JP')).toBe('Japanese'); + }); + + it('should convert "ES_LA" to Spanish (Latin America)', () => { + expect(localeStringToLocaleName('ES_LA')).toBe('Spanish (Latin America)'); + }); + + it('should convert "LA" to Spanish (Latin America)', () => { + expect(localeStringToLocaleName('LA')).toBe('Spanish (Latin America)'); + }); + + it('should convert "NL" to Dutch', () => { + expect(localeStringToLocaleName('NL')).toBe('Dutch'); + }); + + it('should convert "NO" to Norwegian', () => { + expect(localeStringToLocaleName('NO')).toBe('Norwegian'); + }); + + it('should convert "PL" to Polish', () => { + expect(localeStringToLocaleName('PL')).toBe('Polish'); + }); + + it('should convert "PT" to Portuguese', () => { + expect(localeStringToLocaleName('PT')).toBe('Portuguese'); + }); + + it('should convert "BR" to Portuguese (Brazil)', () => { + expect(localeStringToLocaleName('BR')).toBe('Portuguese (Brazil)'); + }); + + it('should convert "PT_BR" to Portuguese (Brazil)', () => { + expect(localeStringToLocaleName('PT_BR')).toBe('Portuguese (Brazil)'); + }); + + it('should convert "RU" to Russian', () => { + expect(localeStringToLocaleName('RU')).toBe('Russian'); + }); + + it('should convert "SE" to Swedish', () => { + expect(localeStringToLocaleName('SE')).toBe('Swedish'); + }); + + it('should convert "SV" to Swedish', () => { + expect(localeStringToLocaleName('SV')).toBe('Swedish'); + }); + + it('should convert "SV_SE" to Swedish', () => { + expect(localeStringToLocaleName('SV_SE')).toBe('Swedish'); + }); + + it('should convert "CN" to Chinese (Simplified)', () => { + expect(localeStringToLocaleName('CN')).toBe('Chinese (Simplified)'); + }); + + it('should convert "ZH_CN" to Chinese (Simplified)', () => { + expect(localeStringToLocaleName('ZH_CN')).toBe('Chinese (Simplified)'); + }); + + it('should convert "TW" to Chinese (Traditional)', () => { + expect(localeStringToLocaleName('TW')).toBe('Chinese (Traditional)'); + }); + + it('should convert "ZH" to Chinese (Traditional)', () => { + expect(localeStringToLocaleName('ZH')).toBe('Chinese (Traditional)'); + }); + + it('should convert "ZH_TW" to Chinese (Traditional)', () => { + expect(localeStringToLocaleName('ZH_TW')).toBe('Chinese (Traditional)'); + }); + + it('should return input string for unknown locale', () => { + expect(localeStringToLocaleName('UNKNOWN')).toBe('UNKNOWN'); + }); + + it('should return input string for empty string', () => { + expect(localeStringToLocaleName('')).toBe(''); + }); + }); + + describe('defaultLocale', () => { + it('should have locale set to "en"', () => { + expect(defaultLocale.locale).toBe('en'); + }); + + it('should have localeDirection set to "ltr"', () => { + expect(defaultLocale.localeDirection).toBe('ltr'); + }); + + it('should have cardNumberLabel', () => { + expect(defaultLocale.cardNumberLabel).toBe('Card Number'); + }); + + it('should have emailLabel', () => { + expect(defaultLocale.emailLabel).toBe('Email'); + }); + + it('should have payNowButton', () => { + expect(defaultLocale.payNowButton).toBe('Pay Now'); + }); + + it('should have billingDetails', () => { + expect(defaultLocale.billingDetails).toBe('Billing Details'); + }); + + it('should have invalid email error text', () => { + expect(defaultLocale.emailInvalidText).toBe('Invalid email address'); + }); + + it('should have card expiry placeholder', () => { + expect(defaultLocale.expiryPlaceholder).toBe('MM / YY'); + }); + + it('should have cvcTextLabel', () => { + expect(defaultLocale.cvcTextLabel).toBe('CVC'); + }); + + it('should have poweredBy text', () => { + expect(defaultLocale.poweredBy).toBe('Powered By Hyperswitch'); + }); + }); +}); diff --git a/__tests__/shared-code/PhoneNumberValidation.test.ts b/__tests__/shared-code/PhoneNumberValidation.test.ts new file mode 100644 index 000000000..a8659e89f --- /dev/null +++ b/__tests__/shared-code/PhoneNumberValidation.test.ts @@ -0,0 +1,89 @@ +import { formatPhoneNumber } from '../../shared-code/sdk-utils/validation/PhoneNumberValidation.bs.js'; + +const mockCountries = [ + { phone_number_code: '+1' }, + { phone_number_code: '+44' }, + { phone_number_code: '+91' }, + { phone_number_code: '+55' }, +]; + +describe('PhoneNumberValidation', () => { + describe('formatPhoneNumber', () => { + describe('happy path', () => { + it('should parse phone number with +1 country code', () => { + const result = formatPhoneNumber('+14155551234', mockCountries); + expect(result).toEqual(['+1', '4155551234']); + }); + + it('should parse phone number with +44 country code', () => { + const result = formatPhoneNumber('+442071234567', mockCountries); + expect(result).toEqual(['+44', '2071234567']); + }); + + it('should parse phone number with +91 country code', () => { + const result = formatPhoneNumber('+919876543210', mockCountries); + expect(result).toEqual(['+91', '9876543210']); + }); + + it('should return empty country code for number without plus', () => { + const result = formatPhoneNumber('4155551234', mockCountries); + expect(result).toEqual(['', '4155551234']); + }); + }); + + describe('edge cases', () => { + it('should return empty result for empty string', () => { + const result = formatPhoneNumber('', mockCountries); + expect(result).toEqual(['', '']); + }); + + it('should return original text for string longer than 20 chars', () => { + const longText = 'a'.repeat(25); + const result = formatPhoneNumber(longText, mockCountries); + expect(result).toEqual(['', longText]); + }); + + it('should return original text when no valid phone chars found', () => { + const result = formatPhoneNumber('abcdef', mockCountries); + expect(result).toEqual(['', 'abcdef']); + }); + + it('should return original text when no digits present', () => { + const result = formatPhoneNumber('+++', mockCountries); + expect(result).toEqual(['', '+++']); + }); + + it('should return empty national number when only country code present', () => { + const result = formatPhoneNumber('+1', mockCountries); + expect(result).toEqual(['+1', '']); + }); + }); + + describe('error/boundary', () => { + it('should return original text when plus present but country code not found', () => { + const result = formatPhoneNumber('+99912345678', mockCountries); + expect(result).toEqual(['', '+99912345678']); + }); + + it('should handle phone number with special characters', () => { + const result = formatPhoneNumber('+1 (415) 555-1234', mockCountries); + expect(result).toEqual(['+1', '4155551234']); + }); + + it('should handle empty countries array', () => { + const result = formatPhoneNumber('+14155551234', []); + expect(result).toEqual(['', '+14155551234']); + }); + + it('should handle phone number starting with digits only', () => { + const result = formatPhoneNumber('4155551234', mockCountries); + expect(result).toEqual(['', '4155551234']); + }); + + it('should strip non-digit characters from national number', () => { + const result = formatPhoneNumber('+1-415-555-1234', mockCountries); + expect(result).toEqual(['+1', '4155551234']); + }); + }); + }); +}); diff --git a/__tests__/shared-code/SharedCodeWeb.test.ts b/__tests__/shared-code/SharedCodeWeb.test.ts new file mode 100644 index 000000000..da7f6291c --- /dev/null +++ b/__tests__/shared-code/SharedCodeWeb.test.ts @@ -0,0 +1,200 @@ +import { + calculateLuhn, + isEmailValid, + containsOnlyDigits, + cardValid, + maxCardLength, + cvcNumberInRange, + getCardBrand, + checkCardCVC, + checkCardExpiry, +} from '../../shared-code/sdk-utils/validation/Validation.bs.js'; + +import { snakeToPascalCase } from '../../shared-code/sdk-utils/utils/CommonUtils.bs.js'; + +import { isEmailValid as isEmailValidFromEmailValidation } from '../../shared-code/sdk-utils/validation/EmailValidation.bs.js'; + +import { isValidCPF as isValidCPFFromCpfValidation } from '../../shared-code/sdk-utils/validation/CpfValidation.bs.js'; + +describe('SharedCodeWeb', () => { + describe('Validation.bs.js', () => { + describe('calculateLuhn', () => { + it('should return true for valid card number', () => { + expect(calculateLuhn('4111111111111111')).toBe(true); + }); + + it('should return false for invalid card number', () => { + expect(calculateLuhn('4111111111111112')).toBe(false); + }); + + it('should handle empty string', () => { + expect(calculateLuhn('')).toBe(true); + }); + }); + + describe('isEmailValid', () => { + it('should return true for valid email', () => { + expect(isEmailValid('user@example.com')).toBe(true); + }); + + it('should return false for invalid email', () => { + expect(isEmailValid('invalid-email')).toBe(false); + }); + + it('should return undefined for empty email', () => { + expect(isEmailValid('')).toBeUndefined(); + }); + }); + + describe('containsOnlyDigits', () => { + it('should return true for digits only', () => { + expect(containsOnlyDigits('12345')).toBe(true); + }); + + it('should return false for non-digits', () => { + expect(containsOnlyDigits('abc123')).toBe(false); + }); + + it('should return true for empty string', () => { + expect(containsOnlyDigits('')).toBe(true); + }); + }); + + describe('cardValid', () => { + it('should return true for valid Visa card', () => { + expect(cardValid('4111111111111111', 'Visa')).toBe(true); + }); + + it('should return false for invalid card', () => { + expect(cardValid('4111111111111112', 'Visa')).toBe(false); + }); + }); + + describe('maxCardLength', () => { + it('should return correct max length for Visa', () => { + expect(maxCardLength('Visa')).toBe(19); + }); + + it('should return correct max length for Amex', () => { + expect(maxCardLength('AmericanExpress')).toBe(15); + }); + }); + + describe('cvcNumberInRange', () => { + it('should return true for valid Visa CVC', () => { + expect(cvcNumberInRange('123', 'Visa')).toBe(true); + }); + + it('should return true for valid Amex CVC', () => { + expect(cvcNumberInRange('1234', 'AmericanExpress')).toBe(true); + }); + }); + + describe('getCardBrand', () => { + it('should return Visa for Visa number', () => { + expect(getCardBrand('4111111111111111')).toBe('Visa'); + }); + + it('should return Mastercard for Mastercard number', () => { + expect(getCardBrand('5555555555554444')).toBe('Mastercard'); + }); + }); + + describe('checkCardCVC', () => { + it('should return true for valid CVC', () => { + expect(checkCardCVC('123', 'Visa')).toBe(true); + }); + + it('should return false for empty CVC', () => { + expect(checkCardCVC('', 'Visa')).toBe(false); + }); + }); + + describe('checkCardExpiry', () => { + it('should return true for valid future expiry', () => { + const futureYear = new Date().getFullYear() + 1; + const expiry = `12/${futureYear.toString().slice(-2)}`; + expect(checkCardExpiry(expiry)).toBe(true); + }); + + it('should return false for past expiry', () => { + expect(checkCardExpiry('12/20')).toBe(false); + }); + }); + + describe('isValidCPF', () => { + it('should return true for valid CPF', () => { + expect(isValidCPFFromCpfValidation('52998224725')).toBe(true); + }); + + it('should return false for invalid CPF', () => { + expect(isValidCPFFromCpfValidation('12345678901')).toBe(false); + }); + + it('should return false for CPF with all same digits', () => { + expect(isValidCPFFromCpfValidation('11111111111')).toBe(false); + }); + }); + }); + + describe('CommonUtils.bs.js', () => { + describe('snakeToPascalCase', () => { + it('should convert snake_case to PascalCase', () => { + expect(snakeToPascalCase('hello_world')).toBe('HelloWorld'); + }); + + it('should handle single word', () => { + expect(snakeToPascalCase('hello')).toBe('Hello'); + }); + + it('should handle empty string', () => { + expect(snakeToPascalCase('')).toBe(''); + }); + + it('should handle multiple underscores', () => { + expect(snakeToPascalCase('hello__world')).toBe('HelloWorld'); + }); + }); + }); + + describe('EmailValidation.bs.js', () => { + describe('isEmailValid', () => { + it('should return true for valid email', () => { + expect(isEmailValidFromEmailValidation('user@example.com')).toBe(true); + }); + + it('should return false for invalid email', () => { + expect(isEmailValidFromEmailValidation('invalid')).toBe(false); + }); + + it('should return undefined for empty string', () => { + expect(isEmailValidFromEmailValidation('')).toBeUndefined(); + }); + + it('should handle complex email', () => { + expect(isEmailValidFromEmailValidation('user.name+tag@subdomain.example.com')).toBe(true); + }); + }); + }); + + describe('CpfValidation.bs.js', () => { + describe('isValidCPF', () => { + it('should return true for valid CPF', () => { + expect(isValidCPFFromCpfValidation('52998224725')).toBe(true); + }); + + it('should return false for invalid CPF format', () => { + expect(isValidCPFFromCpfValidation('123')).toBe(false); + }); + + it('should return false for CPF with letters', () => { + expect(isValidCPFFromCpfValidation('abcdefghijk')).toBe(false); + }); + + it('should return false for known invalid CPF', () => { + expect(isValidCPFFromCpfValidation('00000000000')).toBe(false); + expect(isValidCPFFromCpfValidation('11111111111')).toBe(false); + }); + }); + }); +}); diff --git a/__tests__/shared-code/SuperpositionHelper.test.ts b/__tests__/shared-code/SuperpositionHelper.test.ts new file mode 100644 index 000000000..a61eeb3ea --- /dev/null +++ b/__tests__/shared-code/SuperpositionHelper.test.ts @@ -0,0 +1,384 @@ +import { + sortFieldsByPriorityOrder, + removeDuplicateConnectors, + removeShippingAndDuplicateFields, + extractFieldValuesFromPML, + filterFieldsBasedOnMissingData, + getOrCreateNestedDictionary, + setValueAtNestedPath, + removeEmptyObjects, + convertFlatDictToNestedObject, + convertConfigurationToRequiredFields, +} from '../../shared-code/sdk-utils/utils/SuperpositionHelper.bs.js'; + +describe('SuperpositionHelper', () => { + describe('sortFieldsByPriorityOrder', () => { + it('should sort fields by priority in ascending order', () => { + const fields = [ + { name: 'field3', priority: 3 }, + { name: 'field1', priority: 1 }, + { name: 'field2', priority: 2 }, + ]; + const result = sortFieldsByPriorityOrder([...fields]); + expect(result[0].name).toBe('field1'); + expect(result[1].name).toBe('field2'); + expect(result[2].name).toBe('field3'); + }); + + it('should handle empty array', () => { + const result = sortFieldsByPriorityOrder([]); + expect(result).toEqual([]); + }); + + it('should handle single element array', () => { + const fields = [{ name: 'only', priority: 5 }]; + const result = sortFieldsByPriorityOrder([...fields]); + expect(result).toEqual(fields); + }); + + it('should handle fields with same priority', () => { + const fields = [ + { name: 'fieldA', priority: 1 }, + { name: 'fieldB', priority: 1 }, + ]; + const result = sortFieldsByPriorityOrder([...fields]); + expect(result.length).toBe(2); + }); + }); + + describe('removeDuplicateConnectors', () => { + it('should remove duplicate strings from array', () => { + const connectors = ['stripe', 'adyen', 'stripe', 'paypal', 'adyen']; + const result = removeDuplicateConnectors(connectors); + expect(result).toEqual(['stripe', 'adyen', 'paypal']); + }); + + it('should return empty array for empty input', () => { + expect(removeDuplicateConnectors([])).toEqual([]); + }); + + it('should handle array with no duplicates', () => { + const connectors = ['stripe', 'adyen', 'paypal']; + const result = removeDuplicateConnectors(connectors); + expect(result).toEqual(['stripe', 'adyen', 'paypal']); + }); + + it('should handle single element array', () => { + expect(removeDuplicateConnectors(['stripe'])).toEqual(['stripe']); + }); + }); + + describe('removeShippingAndDuplicateFields', () => { + it('should remove fields with name starting with "shipping."', () => { + const fields = [ + { name: 'billing.address', outputPath: 'billing.address' }, + { name: 'shipping.address', outputPath: 'shipping.address' }, + ]; + const result = removeShippingAndDuplicateFields(fields); + expect(result.length).toBe(1); + expect(result[0].name).toBe('billing.address'); + }); + + it('should remove duplicate fields based on outputPath', () => { + const fields = [ + { name: 'field1', outputPath: 'same.path' }, + { name: 'field2', outputPath: 'same.path' }, + ]; + const result = removeShippingAndDuplicateFields(fields); + expect(result.length).toBe(1); + }); + + it('should keep all unique non-shipping fields', () => { + const fields = [ + { name: 'billing.name', outputPath: 'billing.name' }, + { name: 'billing.email', outputPath: 'billing.email' }, + ]; + const result = removeShippingAndDuplicateFields(fields); + expect(result.length).toBe(2); + }); + + it('should handle empty array', () => { + expect(removeShippingAndDuplicateFields([])).toEqual([]); + }); + }); + + describe('extractFieldValuesFromPML', () => { + it('should extract field values from payment method list', () => { + const requiredFields = { + field1: { required_field: 'email', value: 'test@example.com' }, + field2: { required_field: 'name', value: 'John Doe' }, + }; + const result = extractFieldValuesFromPML(requiredFields); + expect(result['email']).toBe('test@example.com'); + expect(result['name']).toBe('John Doe'); + }); + + it('should return empty object for empty input', () => { + expect(extractFieldValuesFromPML({})).toEqual({}); + }); + + it('should skip entries without required_field', () => { + const requiredFields = { + field1: { value: 'test@example.com' }, + }; + const result = extractFieldValuesFromPML(requiredFields); + expect(result).toEqual({}); + }); + + it('should skip entries without value', () => { + const requiredFields = { + field1: { required_field: 'email' }, + }; + const result = extractFieldValuesFromPML(requiredFields); + expect(result).toEqual({}); + }); + + it('should skip entries with empty required_field', () => { + const requiredFields = { + field1: { required_field: '', value: 'test@example.com' }, + }; + const result = extractFieldValuesFromPML(requiredFields); + expect(result).toEqual({}); + }); + }); + + describe('filterFieldsBasedOnMissingData', () => { + it('should filter fields that are missing from PML data', () => { + const superpositionFields = [ + { name: 'email', outputPath: 'billing.email' }, + { name: 'phone', outputPath: 'billing.phone' }, + ]; + const pmlData = { 'billing.email': 'test@example.com' }; + const result = filterFieldsBasedOnMissingData(superpositionFields, pmlData); + expect(result.length).toBe(1); + expect(result[0].name).toBe('phone'); + }); + + it('should return all fields when PML data is empty', () => { + const superpositionFields = [ + { name: 'email', outputPath: 'billing.email' }, + ]; + const result = filterFieldsBasedOnMissingData(superpositionFields, {}); + expect(result.length).toBe(1); + }); + + it('should handle empty fields array', () => { + expect(filterFieldsBasedOnMissingData([], {})).toEqual([]); + }); + + it('should include name fields together when any name field is missing', () => { + const superpositionFields = [ + { name: 'first_name', outputPath: 'billing.address.first_name' }, + { name: 'last_name', outputPath: 'billing.address.last_name' }, + ]; + const pmlData = { 'billing.address.first_name': 'John' }; + const result = filterFieldsBasedOnMissingData(superpositionFields, pmlData); + expect(result.length).toBe(2); + }); + }); + + describe('getOrCreateNestedDictionary', () => { + it('should return existing nested dictionary', () => { + const dict = { nested: { key: 'value' } }; + const result = getOrCreateNestedDictionary(dict, 'nested'); + expect(result).toEqual({ key: 'value' }); + }); + + it('should create empty dictionary for missing key', () => { + const dict = { other: 'value' }; + const result = getOrCreateNestedDictionary(dict, 'missing'); + expect(result).toEqual({}); + }); + + it('should handle empty dictionary', () => { + expect(getOrCreateNestedDictionary({}, 'any')).toEqual({}); + }); + }); + + describe('setValueAtNestedPath', () => { + it('should set value at single key path', () => { + const dict = {}; + const result = setValueAtNestedPath(dict, ['key'], 'value'); + expect(result['key']).toBe('value'); + }); + + it('should set value at nested path', () => { + const dict = {}; + const result = setValueAtNestedPath(dict, ['level1', 'level2'], 'value'); + expect(result['level1']['level2']).toBe('value'); + }); + + it('should return original dict for empty keys', () => { + const dict = { existing: 'value' }; + const result = setValueAtNestedPath(dict, [], 'value'); + expect(result).toEqual(dict); + }); + + it('should not set empty key or value', () => { + const dict = {}; + const result = setValueAtNestedPath(dict, [''], 'value'); + expect(result).toEqual({}); + }); + + it('should not set value if value is empty string', () => { + const dict = {}; + const result = setValueAtNestedPath(dict, ['key'], ''); + expect(result).toEqual({}); + }); + + it('should handle deeply nested paths', () => { + const dict = {}; + const result = setValueAtNestedPath(dict, ['a', 'b', 'c', 'd'], 'deep'); + expect(result['a']['b']['c']['d']).toBe('deep'); + }); + }); + + describe('removeEmptyObjects', () => { + it('should remove empty nested objects', () => { + const dict = { + keep: 'value', + remove: {}, + nested: { empty: {} }, + }; + const result = removeEmptyObjects(dict); + expect(result['keep']).toBe('value'); + expect(result['remove']).toBeUndefined(); + expect(result['nested']).toBeUndefined(); + }); + + it('should keep non-empty nested objects', () => { + const dict = { + nested: { key: 'value' }, + }; + const result = removeEmptyObjects(dict); + expect(result['nested']).toEqual({ key: 'value' }); + }); + + it('should handle empty input', () => { + expect(removeEmptyObjects({})).toEqual({}); + }); + + it('should handle non-object values', () => { + const dict = { + string: 'value', + number: 123, + boolean: true, + }; + const result = removeEmptyObjects(dict); + expect(result).toEqual(dict); + }); + }); + + describe('convertFlatDictToNestedObject', () => { + it('should convert flat dot-path dict to nested object', () => { + const flatDict = { + 'billing.name': 'John', + 'billing.email': 'john@example.com', + }; + const result = convertFlatDictToNestedObject(flatDict); + expect(result['billing']['name']).toBe('John'); + expect(result['billing']['email']).toBe('john@example.com'); + }); + + it('should handle single level keys', () => { + const flatDict = { name: 'John', email: 'john@example.com' }; + const result = convertFlatDictToNestedObject(flatDict); + expect(result['name']).toBe('John'); + expect(result['email']).toBe('john@example.com'); + }); + + it('should handle empty input', () => { + expect(convertFlatDictToNestedObject({})).toEqual({}); + }); + + it('should handle deeply nested paths', () => { + const flatDict = { + 'a.b.c.d': 'deep', + }; + const result = convertFlatDictToNestedObject(flatDict); + expect(result['a']['b']['c']['d']).toBe('deep'); + }); + + it('should skip empty keys', () => { + const flatDict = { + '': 'value', + 'valid': 'keep', + }; + const result = convertFlatDictToNestedObject(flatDict); + expect(result['valid']).toBe('keep'); + expect(result['']).toBeUndefined(); + }); + }); + + describe('convertConfigurationToRequiredFields', () => { + it('should convert configuration to required fields array', () => { + const config = { + 'email._required': true, + 'email._display_name': 'Email', + 'email._field_type': 'email_input', + 'email._priority': 1, + 'email._output_path': 'billing.email', + }; + const result = convertConfigurationToRequiredFields(config); + expect(result.length).toBe(1); + expect(result[0].name).toBe('email'); + expect(result[0].displayName).toBe('Email'); + expect(result[0].required).toBe(true); + }); + + it('should skip non-required fields', () => { + const config = { + 'optional._required': false, + 'optional._display_name': 'Optional', + }; + const result = convertConfigurationToRequiredFields(config); + expect(result.length).toBe(0); + }); + + it('should skip entries without proper format', () => { + const config = { + 'no_underscore': true, + }; + const result = convertConfigurationToRequiredFields(config); + expect(result.length).toBe(0); + }); + + it('should handle empty configuration', () => { + expect(convertConfigurationToRequiredFields({})).toEqual([]); + }); + + it('should use default values for missing metadata', () => { + const config = { + 'field._required': true, + }; + const result = convertConfigurationToRequiredFields(config); + expect(result[0].displayName).toBe('field'); + expect(result[0].priority).toBe(1000); + expect(result[0].outputPath).toBe('field'); + }); + + it('should parse options array', () => { + const config = { + 'country._required': true, + 'country._options': ['US', 'UK', 'CA'], + }; + const result = convertConfigurationToRequiredFields(config); + expect(result[0].options).toEqual(['US', 'UK', 'CA']); + }); + + it('should handle multiple fields', () => { + const config = { + 'email._required': true, + 'email._display_name': 'Email', + 'email._priority': 1, + 'email._output_path': 'email', + 'phone._required': true, + 'phone._display_name': 'Phone', + 'phone._priority': 2, + 'phone._output_path': 'phone', + }; + const result = convertConfigurationToRequiredFields(config); + expect(result.length).toBe(2); + }); + }); +}); diff --git a/__tests__/shared-code/SuperpositionTypes.test.ts b/__tests__/shared-code/SuperpositionTypes.test.ts new file mode 100644 index 000000000..3397be925 --- /dev/null +++ b/__tests__/shared-code/SuperpositionTypes.test.ts @@ -0,0 +1,65 @@ +import { stringToFieldType } from '../../shared-code/sdk-utils/types/SuperpositionTypes.bs.js'; + +describe('SuperpositionTypes', () => { + describe('stringToFieldType', () => { + it('should map "card_number_text_input" to CardNumberTextInput', () => { + expect(stringToFieldType('card_number_text_input')).toBe('CardNumberTextInput'); + }); + + it('should map "email_input" to EmailInput', () => { + expect(stringToFieldType('email_input')).toBe('EmailInput'); + }); + + it('should map "phone_input" to PhoneInput', () => { + expect(stringToFieldType('phone_input')).toBe('PhoneInput'); + }); + + it('should map "country_select" to CountrySelect', () => { + expect(stringToFieldType('country_select')).toBe('CountrySelect'); + }); + + it('should map "state_select" to StateSelect', () => { + expect(stringToFieldType('state_select')).toBe('StateSelect'); + }); + + it('should map "currency_select" to CurrencySelect', () => { + expect(stringToFieldType('currency_select')).toBe('CurrencySelect'); + }); + + it('should map "country_code_select" to CountryCodeSelect', () => { + expect(stringToFieldType('country_code_select')).toBe('CountryCodeSelect'); + }); + + it('should map "dropdown_select" to DropdownSelect', () => { + expect(stringToFieldType('dropdown_select')).toBe('DropdownSelect'); + }); + + it('should map "password_input" to PasswordInput', () => { + expect(stringToFieldType('password_input')).toBe('PasswordInput'); + }); + + it('should map "cvc_password_input" to CvcPasswordInput', () => { + expect(stringToFieldType('cvc_password_input')).toBe('CvcPasswordInput'); + }); + + it('should map "date_picker" to DatePicker', () => { + expect(stringToFieldType('date_picker')).toBe('DatePicker'); + }); + + it('should map "month_select" to MonthSelect', () => { + expect(stringToFieldType('month_select')).toBe('MonthSelect'); + }); + + it('should map "year_select" to YearSelect', () => { + expect(stringToFieldType('year_select')).toBe('YearSelect'); + }); + + it('should return "TextInput" for unknown field type', () => { + expect(stringToFieldType('unknown_field_type')).toBe('TextInput'); + }); + + it('should return "TextInput" for empty string', () => { + expect(stringToFieldType('')).toBe('TextInput'); + }); + }); +}); diff --git a/__tests__/shared-code/Validation.test.ts b/__tests__/shared-code/Validation.test.ts new file mode 100644 index 000000000..c3c22166e --- /dev/null +++ b/__tests__/shared-code/Validation.test.ts @@ -0,0 +1,346 @@ +import { + splitExpiryDates, + createFieldValidator, + formatValue, + validateField, + format, + getKeyboardType, + getSecureTextEntry, + containsOnlyDigits, + containsDigit, + containsMoreThanTwoDigits, + clearAlphas, + isEmailValid, + isValidIban, + checkCardExpiry, + getCurrentMonthAndYear, +} from '../../shared-code/sdk-utils/validation/Validation.bs.js'; + +const mockLocaleObject = { + mandatoryFieldText: 'This field is required', + cardNumberEmptyText: 'Card number is empty', + inValidCardErrorText: 'Invalid card number', + emailEmptyText: 'Email is empty', + emailInvalidText: 'Invalid email', + cardHolderNameRequiredText: 'Cardholder name is required', + lastNameRequiredText: 'Last name is required', + invalidDigitsCardHolderNameError: 'Name cannot contain digits', + cardExpiryDateEmptyText: 'Expiry date is empty', + inValidExpiryErrorText: 'Invalid expiry date', + cvcNumberEmptyText: 'CVC is empty', + inValidCVCErrorText: 'Invalid CVC', + unsupportedCardErrorText: 'Unsupported card', +}; + +const enabledCardSchemes = ['Visa', 'Mastercard', 'Amex']; + +describe('Validation', () => { + describe('splitExpiryDates', () => { + it('should split MM/YY format', () => { + const result = splitExpiryDates('12/25'); + expect(result[0]).toBe('12'); + expect(result[1]).toBe('25'); + }); + + it('should split MM / YY format with spaces', () => { + const result = splitExpiryDates('12 / 25'); + expect(result[0]).toBe('12'); + expect(result[1]).toBe('25'); + }); + + it('should handle single digit month', () => { + const result = splitExpiryDates('1/25'); + expect(result[0]).toBe('1'); + expect(result[1]).toBe('25'); + }); + + it('should handle empty parts', () => { + const result = splitExpiryDates('/25'); + expect(result[0]).toBe(''); + expect(result[1]).toBe('25'); + }); + + it('should handle string without separator', () => { + const result = splitExpiryDates('1225'); + expect(result[0]).toBe('1225'); + expect(result[1]).toBe(''); + }); + + it('should handle empty string', () => { + const result = splitExpiryDates(''); + expect(result[0]).toBe(''); + expect(result[1]).toBe(''); + }); + + it('should trim whitespace from parts', () => { + const result = splitExpiryDates(' 12 / 25 '); + expect(result[0]).toBe('12'); + expect(result[1]).toBe('25'); + }); + }); + + describe('createFieldValidator', () => { + it('should create a validator function for Required rule', () => { + const validator = createFieldValidator('Required', enabledCardSchemes, mockLocaleObject); + expect(validator('')).toBe('This field is required'); + expect(validator(' ')).toBe('This field is required'); + expect(validator('test')).toBeUndefined(); + }); + + it('should create a validator function for Email rule', () => { + const validator = createFieldValidator('Email', enabledCardSchemes, mockLocaleObject); + expect(validator('')).toBe('Email is empty'); + expect(validator('invalid-email')).toBe('Invalid email'); + expect(validator('test@example.com')).toBeUndefined(); + }); + + it('should create a validator function for FirstName rule', () => { + const validator = createFieldValidator('FirstName', enabledCardSchemes, mockLocaleObject); + expect(validator('')).toBe('Cardholder name is required'); + expect(validator('John123')).toBe('Name cannot contain digits'); + expect(validator('John')).toBeUndefined(); + }); + + it('should create a validator function for LastName rule', () => { + const validator = createFieldValidator('LastName', enabledCardSchemes, mockLocaleObject); + expect(validator('')).toBe('Last name is required'); + expect(validator('Doe123')).toBe('Name cannot contain digits'); + expect(validator('Doe')).toBeUndefined(); + }); + + it('should handle undefined value', () => { + const validator = createFieldValidator('Required', enabledCardSchemes, mockLocaleObject); + expect(validator(undefined)).toBe('This field is required'); + }); + + it('should add MaxLength validation automatically', () => { + const validator = createFieldValidator('Required', enabledCardSchemes, mockLocaleObject); + const longValue = 'a'.repeat(300); + expect(validator(longValue)).toBe('Maximum 255 characters allowed'); + }); + }); + + describe('formatValue', () => { + it('should create a formatter for CardNumber', () => { + const formatter = formatValue('CardNumber'); + const result = formatter('4242424242424242', 'cardNumber'); + expect(result).toBe('4242 4242 4242 4242'); + }); + + it('should create a formatter for CardExpiry', () => { + const formatter = formatValue({ TAG: 'CardExpiry', _0: '1225' }); + const result = formatter('1225', 'expiry'); + expect(result).toBe('12 / 25'); + }); + + it('should create a formatter for CardCVC', () => { + const formatter = formatValue({ TAG: 'CardCVC', _0: 'Visa' }); + const result = formatter('1234', 'cvc'); + expect(result).toBe('1234'); + }); + + it('should return undefined for undefined value', () => { + const formatter = formatValue('CardNumber'); + const result = formatter(undefined, 'cardNumber'); + expect(result).toBeUndefined(); + }); + + it('should pass through value for unknown rule', () => { + const formatter = formatValue('Unknown'); + const result = formatter('test value', 'field'); + expect(result).toBe('test value'); + }); + }); + + describe('format', () => { + it('should format card number', () => { + const result = format('4242424242424242', 'CardNumber'); + expect(result).toBe('4242 4242 4242 4242'); + }); + + it('should format expiry date', () => { + const result = format('', { TAG: 'CardExpiry', _0: '1225' }); + expect(result).toBe('12 / 25'); + }); + + it('should format CVC', () => { + const result = format('1234', { TAG: 'CardCVC', _0: 'Visa' }); + expect(result).toBe('1234'); + }); + + it('should return value as-is for unknown rule', () => { + const result = format('test value', 'Unknown'); + expect(result).toBe('test value'); + }); + }); + + describe('getKeyboardType', () => { + it('should return numeric for CardNumber', () => { + expect(getKeyboardType('CardNumber')).toBe('numeric'); + }); + + it('should return email-address for Email', () => { + expect(getKeyboardType('Email')).toBe('email-address'); + }); + + it('should return phone-pad for Phone', () => { + expect(getKeyboardType('Phone')).toBe('phone-pad'); + }); + + it('should return numeric for CardExpiry', () => { + expect(getKeyboardType({ TAG: 'CardExpiry', _0: '' })).toBe('numeric'); + }); + + it('should return numeric for CardCVC', () => { + expect(getKeyboardType({ TAG: 'CardCVC', _0: 'Visa' })).toBe('numeric'); + }); + + it('should return default for unknown rule', () => { + expect(getKeyboardType('Unknown')).toBe('default'); + }); + }); + + describe('getSecureTextEntry', () => { + it('should return true for CardCVC', () => { + expect(getSecureTextEntry({ TAG: 'CardCVC', _0: 'Visa' })).toBe(true); + }); + + it('should return false for other rules', () => { + expect(getSecureTextEntry('CardNumber')).toBe(false); + expect(getSecureTextEntry('Email')).toBe(false); + expect(getSecureTextEntry({ TAG: 'CardExpiry', _0: '' })).toBe(false); + }); + }); + + describe('containsOnlyDigits', () => { + it('should return true for digits only', () => { + expect(containsOnlyDigits('123456')).toBe(true); + expect(containsOnlyDigits('0')).toBe(true); + }); + + it('should return true for empty string', () => { + expect(containsOnlyDigits('')).toBe(true); + }); + + it('should return false for non-digit characters', () => { + expect(containsOnlyDigits('123abc')).toBe(false); + expect(containsOnlyDigits('abc')).toBe(false); + expect(containsOnlyDigits('12.34')).toBe(false); + }); + }); + + describe('containsDigit', () => { + it('should return true if string contains digit', () => { + expect(containsDigit('abc123')).toBe(true); + expect(containsDigit('a1b')).toBe(true); + expect(containsDigit('9')).toBe(true); + }); + + it('should return false if string has no digits', () => { + expect(containsDigit('abcdef')).toBe(false); + expect(containsDigit('')).toBe(false); + }); + }); + + describe('containsMoreThanTwoDigits', () => { + it('should return true if string has more than 2 digits', () => { + expect(containsMoreThanTwoDigits('abc123')).toBe(true); + expect(containsMoreThanTwoDigits('1a2b3c')).toBe(true); + }); + + it('should return false if string has 2 or fewer digits', () => { + expect(containsMoreThanTwoDigits('ab')).toBe(false); + expect(containsMoreThanTwoDigits('a1b2')).toBe(false); + expect(containsMoreThanTwoDigits('12')).toBe(false); + }); + }); + + describe('clearAlphas', () => { + it('should remove alpha characters, keeping only digits and spaces', () => { + expect(clearAlphas('abc123')).toBe('123'); + expect(clearAlphas('abc-123')).toBe('123'); + expect(clearAlphas('abc 123')).toBe(' 123'); + }); + + it('should keep digits and spaces', () => { + expect(clearAlphas('12 34 56')).toBe('12 34 56'); + }); + }); + + describe('isEmailValid', () => { + it('should return true for valid emails', () => { + expect(isEmailValid('test@example.com')).toBe(true); + expect(isEmailValid('user.name@domain.co.uk')).toBe(true); + }); + + it('should return false for invalid emails', () => { + expect(isEmailValid('invalid')).toBe(false); + expect(isEmailValid('test@')).toBe(false); + expect(isEmailValid('@example.com')).toBe(false); + }); + + it('should return undefined for empty string', () => { + expect(isEmailValid('')).toBeUndefined(); + }); + }); + + describe('isValidIban', () => { + it('should return true for non-empty trimmed string', () => { + expect(isValidIban('DE89370400440532013000')).toBe(true); + expect(isValidIban(' DE89370400440532013000 ')).toBe(true); + }); + + it('should return false for empty string', () => { + expect(isValidIban('')).toBe(false); + expect(isValidIban(' ')).toBe(false); + }); + }); + + describe('validateField', () => { + it('should validate Required rule', () => { + expect(validateField('', ['Required'], enabledCardSchemes, mockLocaleObject)).toBe('This field is required'); + expect(validateField('value', ['Required'], enabledCardSchemes, mockLocaleObject)).toBeUndefined(); + }); + + it('should validate Email rule', () => { + expect(validateField('', ['Email'], enabledCardSchemes, mockLocaleObject)).toBe('Email is empty'); + expect(validateField('invalid', ['Email'], enabledCardSchemes, mockLocaleObject)).toBe('Invalid email'); + expect(validateField('test@example.com', ['Email'], enabledCardSchemes, mockLocaleObject)).toBeUndefined(); + }); + + it('should validate Phone rule', () => { + expect(validateField('123', ['Phone'], enabledCardSchemes, mockLocaleObject)).toBe('Enter a valid phone number'); + expect(validateField('1234567890', ['Phone'], enabledCardSchemes, mockLocaleObject)).toBeUndefined(); + }); + + it('should validate IBAN rule', () => { + expect(validateField('', ['IBAN'], enabledCardSchemes, mockLocaleObject)).toBe('Enter a valid IBAN'); + expect(validateField('DE89370400440532013000', ['IBAN'], enabledCardSchemes, mockLocaleObject)).toBeUndefined(); + }); + + it('should validate MinLength rule', () => { + expect(validateField('ab', [{ TAG: 'MinLength', _0: 5 }], enabledCardSchemes, mockLocaleObject)).toBe('Minimum 5 characters required'); + expect(validateField('abcde', [{ TAG: 'MinLength', _0: 5 }], enabledCardSchemes, mockLocaleObject)).toBeUndefined(); + }); + + it('should validate MaxLength rule', () => { + expect(validateField('abcdefgh', [{ TAG: 'MaxLength', _0: 5 }], enabledCardSchemes, mockLocaleObject)).toBe('Maximum 5 characters allowed'); + expect(validateField('abc', [{ TAG: 'MaxLength', _0: 5 }], enabledCardSchemes, mockLocaleObject)).toBeUndefined(); + }); + + it('should return first error from multiple rules', () => { + const rules = ['Required', 'Email']; + expect(validateField('', rules, enabledCardSchemes, mockLocaleObject)).toBe('This field is required'); + }); + }); + + describe('getCurrentMonthAndYear', () => { + it('should return current month and year', () => { + const result = getCurrentMonthAndYear(new Date().toISOString()); + expect(result).toHaveLength(2); + expect(result[0]).toBeGreaterThanOrEqual(1); + expect(result[0]).toBeLessThanOrEqual(12); + expect(result[1]).toBeGreaterThanOrEqual(2024); + }); + }); +}); diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 000000000..60bb9c3c9 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,106 @@ +/** @type {import('jest').Config} */ +module.exports = { + testEnvironment: 'jest-environment-jsdom', + testMatch: [ + '/__tests__/**/*.test.ts', + '/src/__tests__/**/*.test.ts', + ], + testPathIgnorePatterns: [ + '/node_modules/', + '/cypress-tests/', + ], + transform: { + '\\.bs\\.js$': ['babel-jest', { + presets: ['@babel/preset-env', '@babel/preset-react'], + }], + '\\.tsx?$': ['ts-jest', { + tsconfig: 'tsconfig.test.json', + }], + '\\.mjs$': ['babel-jest', { + presets: ['@babel/preset-env'], + }], + '\\.js$': ['babel-jest', { + presets: ['@babel/preset-env'], + }], + }, + transformIgnorePatterns: [ + 'node_modules/(?!(@rescript|@glennsl|rescript)/)', + ], + moduleNameMapper: { + '^@rescript/core/src/(.*)\\.bs\\.js$': '@rescript/core/src/$1.mjs', + '^@rescript/react/src/(.*)\\.bs\\.js$': '/__mocks__/rescriptReactStub.js', + '^/package\\.json$': '/package.json', + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node', 'mjs'], + setupFiles: ['/jest.setup.js'], + setupFilesAfterEnv: ['@testing-library/jest-dom'], + collectCoverage: true, + coverageDirectory: '/coverage', + coverageReporters: ['text', 'lcov', 'json-summary'], + collectCoverageFrom: [ + // --- Shared-code (pure logic) --- + 'shared-code/sdk-utils/validation/**/*.bs.js', + 'shared-code/sdk-utils/utils/**/*.bs.js', + 'shared-code/sdk-utils/events/**/*.bs.js', + 'shared-code/sdk-utils/types/**/*.bs.js', + + // --- src/Types (pure mappers & decoders) --- + 'src/Types/**/*.bs.js', + + // --- src/Utilities (mixed – pure helpers + impure DOM/fetch) --- + 'src/Utilities/**/*.bs.js', + + // --- Root src files with pure functions --- + 'src/CardUtils.bs.js', + 'src/Bank.bs.js', + 'src/Country.bs.js', + 'src/BrowserSpec.bs.js', + + // --- Payments: helper/record files with pure functions --- + 'src/Payments/PaymentMethodsRecord.bs.js', + + // --- LocaleStrings: helper with pure mapping logic --- + 'src/LocaleStrings/LocaleStringHelper.bs.js', + + // --- hyper-log-catcher: files with some pure exports --- + 'src/hyper-log-catcher/HyperLogger.bs.js', + 'src/hyper-log-catcher/LogAPIResponse.bs.js', + + // --- Hooks (testable with renderHook + providers) --- + 'src/Hooks/**/*.bs.js', + + // --- Context (testable with renderHook + providers) --- + 'src/Context/**/*.bs.js', + + // --- Exclusions: impure / untestable --- + '!**/node_modules/**', + '!**/__tests__/**', + '!**/cypress-tests/**', + // Recoil atoms are state declarations, not testable logic + '!src/Utilities/RecoilAtoms.bs.js', + '!src/Utilities/RecoilAtomsV2.bs.js', + // Event listener manager uses window directly + '!src/Utilities/EventListenerManager.bs.js', + // Test utils are just constants for test IDs + '!src/Utilities/TestUtils.bs.js', + // Empty/optimized-away files + '!src/Types/ACHTypes.bs.js', + '!src/Types/HyperLoggerTypes.bs.js', + '!src/Types/SamsungPayType.bs.js', + '!src/Types/ThemeImporter.bs.js', + '!src/Types/UnifiedPaymentsTypesV2.bs.js', + '!src/Utilities/AbortController.bs.js', + '!src/Utilities/Identity.bs.js', + '!src/Utilities/URLModule.bs.js', + '!src/Utilities/PaymentHelpersTypes.bs.js', + // Shared-code hooks/components are React-dependent + '!shared-code/sdk-utils/hooks/**', + '!shared-code/sdk-utils/components/**', + ], + coveragePathIgnorePatterns: [ + '/node_modules/', + '/cypress-tests/', + '/__tests__/', + ], + testResultsProcessor: 'jest-sonar-reporter', +}; diff --git a/jest.setup.js b/jest.setup.js new file mode 100644 index 000000000..3b2233120 --- /dev/null +++ b/jest.setup.js @@ -0,0 +1,34 @@ +// Mock webpack DefinePlugin globals that are injected at build time. +// These values mirror the defaults from webpack.common.js definePluginValues. + +// Core SDK URLs +global.sdkUrl = "https://beta.hyperswitch.io/v1"; +global.publicPath = "/"; + +// API endpoints +global.backendEndPoint = "https://beta.hyperswitch.io"; +global.confirmEndPoint = "https://beta.hyperswitch.io"; +global.logEndpoint = "https://beta.hyperswitch.io/logs"; + +// Logging +global.enableLogging = true; +global.loggingLevel = "DEBUG"; +global.maxLogsPushedPerEventName = "100"; + +// Sentry +global.sentryDSN = ""; +global.sentryScriptUrl = ""; + +// Environment flags +global.isIntegrationEnv = false; +global.isSandboxEnv = false; +global.isProductionEnv = false; +global.isLocal = false; + +// Repo metadata +global.repoName = "hyperswitch-web"; +global.repoVersion = "0.0.0-test"; + +// Visa Click-to-Pay +global.visaAPIKeyId = ""; +global.visaAPICertificatePem = ""; diff --git a/package.json b/package.json index d06644e25..8c2c1bb2e 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "start": "npm run update:submodules && cross-env sdkEnv=local webpack serve --config webpack.dev.js", "start:playground": "npm run setup:playground && npm run start", "test": "cd cypress-tests && npm run cypress:run", + "test:unit": "jest --config jest.config.js", "test:hooks": "eslint src/" }, "husky": { @@ -68,7 +69,12 @@ "@babel/core": "^7.25.2", "@babel/preset-env": "^7.25.3", "@babel/preset-react": "^7.24.7", + "@testing-library/dom": "^10.4.1", + "@testing-library/jest-dom": "^6.9.1", + "@testing-library/react": "^16.3.2", + "@types/jest": "^30.0.0", "autoprefixer": "^10.4.8", + "babel-jest": "^29.6.3", "babel-loader": "^10.0.0", "babel-plugin-add-react-displayname": "^0.0.5", "copy-webpack-plugin": "^13.0.0", @@ -80,18 +86,27 @@ "eslint-plugin-react-hooks": "^5.2.0", "html-webpack-plugin": "^5.6.0", "husky": "^9.1.7", + "jest": "^29.6.3", + "jest-environment-jsdom": "^29.7.0", + "jest-sonar-reporter": "^2.0.0", "mini-css-extract-plugin": "^2.9.2", "postcss": "^8.4.16", "postcss-loader": "^8.1.1", "rescript": "^11.1.0", "tailwindcss": "^3.1.8", "terser-webpack-plugin": "^5.3.10", + "ts-jest": "^29.2.5", + "typescript": "^6.0.2", "webpack": "^5.94.0", "webpack-bundle-analyzer": "^4.10.2", "webpack-cli": "^6.0.1", "webpack-dev-server": "^5.1.0", "webpack-subresource-integrity": "^5.2.0-rc.1" }, + "jestSonar": { + "reportPath": "coverage", + "reportFile": "test-report.xml" + }, "release": { "branches": [ "main", diff --git a/src/__tests__/APIUtils.test.ts b/src/__tests__/APIUtils.test.ts new file mode 100644 index 000000000..845628582 --- /dev/null +++ b/src/__tests__/APIUtils.test.ts @@ -0,0 +1,303 @@ +import * as APIUtils from "../Utilities/APIHelpers/APIUtils.bs.js"; + +describe("APIUtils", () => { + describe("CommonUtils.buildQueryParams", () => { + it("returns empty string for empty list", () => { + const result = APIUtils.CommonUtils.buildQueryParams(undefined as any); + expect(result).toBe(""); + }); + + it("builds query string with single param", () => { + const params = { + hd: ["key", "value"], + tl: undefined as any, + }; + const result = APIUtils.CommonUtils.buildQueryParams(params); + expect(result).toBe("?key=value"); + }); + + it("builds query string with multiple params", () => { + const params = { + hd: ["key1", "value1"], + tl: { + hd: ["key2", "value2"], + tl: undefined as any, + }, + }; + const result = APIUtils.CommonUtils.buildQueryParams(params); + expect(result).toBe("?key1=value1&key2=value2"); + }); + + it("handles empty param list (zero)", () => { + const result = APIUtils.CommonUtils.buildQueryParams(0 as any); + expect(result).toBe(""); + }); + + it("builds query with three params", () => { + const params = { + hd: ["a", "1"], + tl: { + hd: ["b", "2"], + tl: { + hd: ["c", "3"], + tl: undefined as any, + }, + }, + }; + const result = APIUtils.CommonUtils.buildQueryParams(params); + expect(result).toBe("?a=1&b=2&c=3"); + }); + + it("encodes param values correctly", () => { + const params = { + hd: ["client_secret", "cs_test_123"], + tl: undefined as any, + }; + const result = APIUtils.CommonUtils.buildQueryParams(params); + expect(result).toBe("?client_secret=cs_test_123"); + }); + + it("handles numeric-like values as strings", () => { + const params = { + hd: ["amount", "1000"], + tl: undefined as any, + }; + const result = APIUtils.CommonUtils.buildQueryParams(params); + expect(result).toBe("?amount=1000"); + }); + }); + + describe("generateApiUrlV1", () => { + const baseUrl = "https://api.test.com"; + + describe("FetchPaymentMethodList", () => { + it("should generate URL for FetchPaymentMethodList with client_secret", () => { + const params = { + clientSecret: "pay_abc123_secret_xyz", + customBackendBaseUrl: baseUrl, + }; + const result = APIUtils.generateApiUrlV1(params, "FetchPaymentMethodList"); + expect(result).toBe(`${baseUrl}/account/payment_methods?client_secret=pay_abc123_secret_xyz`); + }); + + it("should generate URL without client_secret when not provided", () => { + const params = { + customBackendBaseUrl: baseUrl, + }; + const result = APIUtils.generateApiUrlV1(params, "FetchPaymentMethodList"); + expect(result).toBe(`${baseUrl}/account/payment_methods`); + }); + + it("should not include client_secret when sdkAuthorization is provided", () => { + const params = { + clientSecret: "pay_abc123_secret_xyz", + sdkAuthorization: "Bearer token123", + customBackendBaseUrl: baseUrl, + }; + const result = APIUtils.generateApiUrlV1(params, "FetchPaymentMethodList"); + expect(result).toBe(`${baseUrl}/account/payment_methods`); + }); + }); + + describe("FetchCustomerPaymentMethodList", () => { + it("should generate URL for FetchCustomerPaymentMethodList", () => { + const params = { + clientSecret: "pay_test_secret_abc", + customBackendBaseUrl: baseUrl, + }; + const result = APIUtils.generateApiUrlV1(params, "FetchCustomerPaymentMethodList"); + expect(result).toBe(`${baseUrl}/customers/payment_methods?client_secret=pay_test_secret_abc`); + }); + }); + + describe("RetrievePaymentIntent", () => { + it("should generate URL for RetrievePaymentIntent with payment ID", () => { + const params = { + clientSecret: "pay_abc123_secret_xyz", + customBackendBaseUrl: baseUrl, + }; + const result = APIUtils.generateApiUrlV1(params, "RetrievePaymentIntent"); + expect(result).toBe(`${baseUrl}/payments/pay_abc123?client_secret=pay_abc123_secret_xyz`); + }); + + it("should include force_sync when provided and true", () => { + const params = { + clientSecret: "pay_abc123_secret_xyz", + forceSync: "true", + customBackendBaseUrl: baseUrl, + }; + const result = APIUtils.generateApiUrlV1(params, "RetrievePaymentIntent"); + expect(result).toContain("force_sync=true"); + }); + + it("should not include force_sync for other apiCallTypes", () => { + const params = { + clientSecret: "pay_abc123_secret_xyz", + forceSync: "true", + customBackendBaseUrl: baseUrl, + }; + const result = APIUtils.generateApiUrlV1(params, "FetchPaymentMethodList"); + expect(result).not.toContain("force_sync"); + }); + }); + + describe("FetchBlockedBins", () => { + it("should generate URL for FetchBlockedBins with data_kind param", () => { + const params = { + clientSecret: "pay_test_secret_abc", + customBackendBaseUrl: baseUrl, + }; + const result = APIUtils.generateApiUrlV1(params, "FetchBlockedBins"); + expect(result).toContain(`${baseUrl}/blocklist`); + expect(result).toContain("data_kind=card_bin"); + expect(result).toContain("client_secret=pay_test_secret_abc"); + }); + }); + + describe("FetchSessions", () => { + it("should generate URL for FetchSessions", () => { + const params = { + customBackendBaseUrl: baseUrl, + }; + const result = APIUtils.generateApiUrlV1(params, "FetchSessions"); + expect(result).toBe(`${baseUrl}/payments/session_tokens`); + }); + }); + + describe("FetchThreeDsAuth", () => { + it("should generate URL for FetchThreeDsAuth with payment ID", () => { + const params = { + clientSecret: "pay_xyz789_secret_token", + customBackendBaseUrl: baseUrl, + }; + const result = APIUtils.generateApiUrlV1(params, "FetchThreeDsAuth"); + expect(result).toBe(`${baseUrl}/payments/pay_xyz789/3ds/authentication`); + }); + }); + + describe("CalculateTax", () => { + it("should generate URL for CalculateTax with payment ID", () => { + const params = { + clientSecret: "pay_tax123_secret_abc", + customBackendBaseUrl: baseUrl, + }; + const result = APIUtils.generateApiUrlV1(params, "CalculateTax"); + expect(result).toBe(`${baseUrl}/payments/pay_tax123/calculate_tax`); + }); + }); + + describe("CreatePaymentMethod", () => { + it("should generate URL for CreatePaymentMethod", () => { + const params = { + customBackendBaseUrl: baseUrl, + }; + const result = APIUtils.generateApiUrlV1(params, "CreatePaymentMethod"); + expect(result).toBe(`${baseUrl}/payment_methods`); + }); + }); + + describe("CallAuthLink", () => { + it("should generate URL for CallAuthLink", () => { + const params = { + customBackendBaseUrl: baseUrl, + }; + const result = APIUtils.generateApiUrlV1(params, "CallAuthLink"); + expect(result).toBe(`${baseUrl}/payment_methods/auth/link`); + }); + }); + + describe("CallAuthExchange", () => { + it("should generate URL for CallAuthExchange", () => { + const params = { + customBackendBaseUrl: baseUrl, + }; + const result = APIUtils.generateApiUrlV1(params, "CallAuthExchange"); + expect(result).toBe(`${baseUrl}/payment_methods/auth/exchange`); + }); + }); + + describe("RetrieveStatus", () => { + it("should generate URL for RetrieveStatus with poll ID", () => { + const params = { + pollId: "poll_abc123", + customBackendBaseUrl: baseUrl, + }; + const result = APIUtils.generateApiUrlV1(params, "RetrieveStatus"); + expect(result).toBe(`${baseUrl}/poll/status/poll_abc123`); + }); + }); + + describe("ConfirmPayout", () => { + it("should generate URL for ConfirmPayout with payout ID", () => { + const params = { + payoutId: "payout_xyz789", + customBackendBaseUrl: baseUrl, + }; + const result = APIUtils.generateApiUrlV1(params, "ConfirmPayout"); + expect(result).toBe(`${baseUrl}/payouts/payout_xyz789/confirm`); + }); + }); + + describe("FetchEnabledAuthnMethodsToken", () => { + it("should generate URL for FetchEnabledAuthnMethodsToken with authentication ID", () => { + const params = { + authenticationId: "auth_abc123", + customBackendBaseUrl: baseUrl, + }; + const result = APIUtils.generateApiUrlV1(params, "FetchEnabledAuthnMethodsToken"); + expect(result).toBe(`${baseUrl}/authentication/auth_abc123/enabled_authn_methods_token`); + }); + }); + + describe("FetchEligibilityCheck", () => { + it("should generate URL for FetchEligibilityCheck with authentication ID", () => { + const params = { + authenticationId: "auth_xyz789", + customBackendBaseUrl: baseUrl, + }; + const result = APIUtils.generateApiUrlV1(params, "FetchEligibilityCheck"); + expect(result).toBe(`${baseUrl}/authentication/auth_xyz789/eligibility-check`); + }); + }); + + describe("FetchAuthenticationSync", () => { + it("should generate URL for FetchAuthenticationSync with merchant ID and authentication ID", () => { + const params = { + merchantId: "merchant_123", + authenticationId: "auth_abc456", + customBackendBaseUrl: baseUrl, + }; + const result = APIUtils.generateApiUrlV1(params, "FetchAuthenticationSync"); + expect(result).toBe(`${baseUrl}/authentication/merchant_123/auth_abc456/sync`); + }); + }); + + describe("edge cases", () => { + it("should handle empty clientSecret by including it in query params", () => { + const params = { + clientSecret: "", + customBackendBaseUrl: baseUrl, + }; + const result = APIUtils.generateApiUrlV1(params, "FetchPaymentMethodList"); + expect(result).toBe(`${baseUrl}/account/payment_methods?client_secret=`); + }); + + it("should handle clientSecret without _secret_ delimiter", () => { + const params = { + clientSecret: "pay_abc123", + customBackendBaseUrl: baseUrl, + }; + const result = APIUtils.generateApiUrlV1(params, "RetrievePaymentIntent"); + expect(result).toContain("/payments/"); + }); + + it("should handle undefined optional params", () => { + const params = { + customBackendBaseUrl: baseUrl, + }; + const result = APIUtils.generateApiUrlV1(params, "FetchPaymentMethodList"); + expect(result).toBe(`${baseUrl}/account/payment_methods`); + }); + }); + }); +}); diff --git a/src/__tests__/ApiEndpoint.test.ts b/src/__tests__/ApiEndpoint.test.ts new file mode 100644 index 000000000..2eb122304 --- /dev/null +++ b/src/__tests__/ApiEndpoint.test.ts @@ -0,0 +1,33 @@ +import { switchToInteg, isLocal, sdkDomainUrl } from '../Utilities/ApiEndpoint.bs.js'; + +describe('ApiEndpoint', () => { + describe('switchToInteg', () => { + it('should be a boolean', () => { + expect(typeof switchToInteg).toBe('boolean'); + }); + + it('should be false by default', () => { + expect(switchToInteg).toBe(false); + }); + }); + + describe('isLocal', () => { + it('should be a boolean', () => { + expect(typeof isLocal).toBe('boolean'); + }); + + it('should be false by default', () => { + expect(isLocal).toBe(false); + }); + }); + + describe('sdkDomainUrl', () => { + it('should be a string', () => { + expect(typeof sdkDomainUrl).toBe('string'); + }); + + it('should be a valid URL', () => { + expect(sdkDomainUrl.length).toBeGreaterThan(0); + }); + }); +}); diff --git a/src/__tests__/ApplePayHelpers.test.ts b/src/__tests__/ApplePayHelpers.test.ts new file mode 100644 index 000000000..0bf7c9988 --- /dev/null +++ b/src/__tests__/ApplePayHelpers.test.ts @@ -0,0 +1,1379 @@ +import { renderHook, act } from '@testing-library/react'; +import * as ApplePayHelpers from '../Utilities/ApplePayHelpers.bs.js'; + +const mockGetDictFromJson = jest.fn((obj: any) => (typeof obj === 'object' && obj !== null ? obj : {})); +const mockGetString = jest.fn((obj: any, key: string, def: string) => obj?.[key] ?? def); +const mockMergeAndFlattenToTuples = jest.fn((a: any, b: any) => [...(a || []), ...(b || [])]); +const mockMessageParentWindow = jest.fn(); +const mockGetDictFromDict = jest.fn((obj: any, key: string) => obj?.[key] || {}); +const mockGetArray = jest.fn((obj: any, key: string) => obj?.[key] || []); +const mockGetDictFromObj = jest.fn((obj: any, key: string) => obj?.[key] || {}); +const mockGetStrArray = jest.fn((obj: any, key: string) => obj?.[key] || []); +const mockGetOptionsDict = jest.fn((obj: any) => obj || {}); +const mockFormatException = jest.fn((e: any) => e?.message || String(e)); +const mockSafeParse = jest.fn((str: string) => { + try { + return JSON.parse(str); + } catch { + return null; + } +}); +const mockPostFailedSubmitResponse = jest.fn(); +const mockHandleFailureResponse = jest.fn((msg: string, type: string) => ({ error: { message: msg, type } })); +const mockTransformKeys = jest.fn((obj: any) => obj); +const mockMinorUnitToString = jest.fn((val: number) => String(val)); +const mockSendPostMessage = jest.fn(); +const mockGetJsonFromArrayOfJson = jest.fn((arr: any) => Object.fromEntries(arr || [])); + +jest.mock('../Utilities/Utils.bs.js', () => ({ + getDictFromJson: (obj: any) => mockGetDictFromJson(obj), + getString: (obj: any, key: string, def: string) => mockGetString(obj, key, def), + mergeAndFlattenToTuples: (a: any, b: any) => mockMergeAndFlattenToTuples(a, b), + messageParentWindow: (a: any, b: any) => mockMessageParentWindow(a, b), + getDictFromDict: (obj: any, key: string) => mockGetDictFromDict(obj, key), + getArray: (obj: any, key: string) => mockGetArray(obj, key), + getDictFromObj: (obj: any, key: string) => mockGetDictFromObj(obj, key), + getStrArray: (obj: any, key: string) => mockGetStrArray(obj, key), + getOptionsDict: (obj: any) => mockGetOptionsDict(obj), + formatException: (e: any) => mockFormatException(e), + defaultCountryCode: 'US', + safeParse: (str: string) => mockSafeParse(str), + postFailedSubmitResponse: (type: string, msg: string) => mockPostFailedSubmitResponse(type, msg), + handleFailureResponse: (msg: string, type: string) => mockHandleFailureResponse(msg, type), + transformKeys: (obj: any) => mockTransformKeys(obj), + minorUnitToString: (val: number) => mockMinorUnitToString(val), + getJsonFromArrayOfJson: (arr: any) => mockGetJsonFromArrayOfJson(arr), +})); + +jest.mock('../Window.bs.js', () => ({ + sendPostMessage: (source: any, msg: any) => mockSendPostMessage(source, msg), +})); + +jest.mock('../Utilities/PaymentBody.bs.js', () => ({ + applePayBody: jest.fn((token: any, connectors: any) => [['apple_pay', { token }]]), + applePayThirdPartySdkBody: jest.fn((connectors: any, token: string) => [['apple_pay', { token }]]), +})); + +jest.mock('../Utilities/PaymentUtils.bs.js', () => ({ + appendedCustomerAcceptance: jest.fn((isGuest: boolean, type: string, body: any) => body), + paymentMethodListValue: { key: 'paymentMethodListValue' }, +})); + +jest.mock('../Utilities/DynamicFieldsUtils.bs.js', () => ({ + getApplePayRequiredFields: jest.fn((billing: any, shipping: any, fields: any) => [['required', {}]]), + usePaymentMethodTypeFromList: jest.fn(() => ({ required_fields: [] })), +})); + +jest.mock('../Types/ApplePayTypes.bs.js', () => ({ + billingContactItemToObjMapper: jest.fn((obj: any) => obj || {}), + shippingContactItemToObjMapper: jest.fn((obj: any) => obj || { + administrativeArea: '', + countryCode: '', + postalCode: '', + }), + getPaymentRequestFromSession: jest.fn(() => ({})), + getTotal: jest.fn((obj: any) => obj || { label: 'Test', amount: '100' }), +})); + +jest.mock('../Payments/PaymentMethodsRecord.bs.js', () => ({ + defaultList: { payment_type: 'NORMAL' }, +})); + +jest.mock('../Utilities/TaxCalculation.bs.js', () => ({ + calculateTax: jest.fn(() => Promise.resolve({ net_amount: 100, order_tax_amount: 10, shipping_cost: 5 })), + taxResponseToObjMapper: jest.fn((obj: any) => obj), +})); + +jest.mock('recoil', () => { + const actualRecoil = jest.requireActual('recoil'); + return { + ...actualRecoil, + useRecoilValue: jest.fn((atom: any) => { + if (atom?.key === 'optionAtom') { + return { wallets: { walletReturnUrl: 'https://return.url' }, readOnly: false }; + } + if (atom?.key === 'keys') { + return { publishableKey: 'pk_test', iframeId: 'iframe-123' }; + } + if (atom?.key === 'isManualRetryEnabled') { + return false; + } + if (atom?.key === 'areRequiredFieldsValid') { + return true; + } + if (atom?.key === 'areRequiredFieldsEmpty') { + return false; + } + if (atom?.key === 'configAtom') { + return { localeString: { enterFieldsText: 'Please enter fields', enterValidDetailsText: 'Please enter valid details' } }; + } + if (atom?.key === 'loggerAtom') { + return { setLogInfo: jest.fn(), setLogError: jest.fn() }; + } + return { key: 'paymentMethodListValue', payment_type: 'NORMAL' }; + }), + useSetRecoilState: jest.fn(() => jest.fn()), + }; +}); + +jest.mock('../Utilities/RecoilAtoms.bs.js', () => ({ + optionAtom: { key: 'optionAtom' }, + keys: { key: 'keys' }, + isManualRetryEnabled: { key: 'isManualRetryEnabled' }, + areRequiredFieldsValid: { key: 'areRequiredFieldsValid' }, + areRequiredFieldsEmpty: { key: 'areRequiredFieldsEmpty' }, + configAtom: { key: 'configAtom' }, + loggerAtom: { key: 'loggerAtom' }, +})); + +jest.mock('../Hooks/UtilityHooks.bs.js', () => ({ + useIsGuestCustomer: jest.fn(() => false), +})); + +describe('ApplePayHelpers', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('thirdPartyApplePayConnectors', () => { + it('contains braintree as a supported connector', () => { + expect(ApplePayHelpers.thirdPartyApplePayConnectors).toContain('braintree'); + }); + + it('is an array', () => { + expect(Array.isArray(ApplePayHelpers.thirdPartyApplePayConnectors)).toBe(true); + }); + + it('has expected length', () => { + expect(ApplePayHelpers.thirdPartyApplePayConnectors).toHaveLength(1); + }); + }); + + describe('processPayment', () => { + it('calls intent with correct parameters', () => { + const mockIntent = jest.fn(); + const bodyArr = [['card', { number: '4242' }]]; + const options = { wallets: { walletReturnUrl: 'https://return.url' } }; + + ApplePayHelpers.processPayment( + bodyArr, + false, + true, + { payment_type: 'NORMAL' }, + mockIntent, + options, + 'pk_test', + false + ); + + expect(mockIntent).toHaveBeenCalledWith( + true, + bodyArr, + { return_url: 'https://return.url', publishableKey: 'pk_test' }, + undefined, + false, + undefined, + false + ); + }); + + it('uses default values for optional parameters', () => { + const mockIntent = jest.fn(); + const bodyArr = [['card', { number: '4242' }]]; + const options = { wallets: { walletReturnUrl: 'https://return.url' } }; + + ApplePayHelpers.processPayment( + bodyArr, + undefined, + undefined, + undefined, + mockIntent, + options, + 'pk_test', + undefined + ); + + expect(mockIntent).toHaveBeenCalledWith( + true, + bodyArr, + { return_url: 'https://return.url', publishableKey: 'pk_test' }, + undefined, + false, + undefined, + undefined + ); + }); + + it('passes isThirdPartyFlow as true when provided', () => { + const mockIntent = jest.fn(); + const bodyArr = [['card', { number: '4242' }]]; + const options = { wallets: { walletReturnUrl: 'https://return.url' } }; + + ApplePayHelpers.processPayment( + bodyArr, + true, + false, + { payment_type: 'NORMAL' }, + mockIntent, + options, + 'pk_test', + false + ); + + expect(mockIntent).toHaveBeenCalledWith( + true, + bodyArr, + expect.any(Object), + undefined, + true, + undefined, + false + ); + }); + + it('passes isManualRetryEnabled when true', () => { + const mockIntent = jest.fn(); + const bodyArr = [['card', { number: '4242' }]]; + const options = { wallets: { walletReturnUrl: 'https://return.url' } }; + + ApplePayHelpers.processPayment( + bodyArr, + false, + false, + { payment_type: 'NORMAL' }, + mockIntent, + options, + 'pk_test', + true + ); + + expect(mockIntent).toHaveBeenCalledWith( + true, + bodyArr, + expect.any(Object), + undefined, + false, + undefined, + true + ); + }); + }); + + describe('getApplePayFromResponse', () => { + it('returns merged tuples for billing and shipping contacts', () => { + const token = { paymentData: 'test-data' }; + const billingContact = { givenName: 'John', familyName: 'Doe' }; + const shippingContact = { emailAddress: 'test@example.com' }; + + mockMergeAndFlattenToTuples.mockReturnValue([ + ['apple_pay', { token }], + ['billing', billingContact], + ]); + + const result = ApplePayHelpers.getApplePayFromResponse( + token, + billingContact, + shippingContact, + [], + {} + ); + + expect(mockMergeAndFlattenToTuples).toHaveBeenCalled(); + expect(result).toBeDefined(); + }); + + it('handles empty billing and shipping contacts', () => { + const token = { paymentData: 'test-data' }; + + mockMergeAndFlattenToTuples.mockReturnValue([['apple_pay', { token }]]); + + const result = ApplePayHelpers.getApplePayFromResponse(token, {}, {}, [], {}); + + expect(result).toBeDefined(); + }); + + it('uses default empty array for requiredFields when not provided', () => { + const token = { paymentData: 'test-data' }; + + ApplePayHelpers.getApplePayFromResponse(token, {}, {}); + + expect(mockMergeAndFlattenToTuples).toHaveBeenCalled(); + }); + + it('handles payment session flow', () => { + const token = { paymentData: 'test-data' }; + + mockMergeAndFlattenToTuples.mockReturnValue([['apple_pay', { token }]]); + + const result = ApplePayHelpers.getApplePayFromResponse( + token, + {}, + {}, + [], + {}, + true, + undefined + ); + + expect(result).toBeDefined(); + }); + + it('handles saved methods flow', () => { + const token = { paymentData: 'test-data' }; + + mockMergeAndFlattenToTuples.mockReturnValue([['apple_pay', { token }]]); + + const result = ApplePayHelpers.getApplePayFromResponse( + token, + {}, + {}, + [], + {}, + false, + true + ); + + expect(result).toBeDefined(); + }); + }); + + describe('createApplePayTransactionInfo', () => { + it('creates transaction info with all fields', () => { + const jsonDict = { + countryCode: 'US', + currencyCode: 'USD', + total: { label: 'Test Merchant', amount: '1000' }, + merchantCapabilities: ['supports3DS'], + supportedNetworks: ['visa', 'masterCard'], + }; + + mockGetString.mockImplementation((obj: any, key: string, def: string) => obj?.[key] ?? def); + mockGetArray.mockImplementation((obj: any, key: string) => obj?.[key] || []); + mockGetDictFromObj.mockImplementation((obj: any, key: string) => obj?.[key] || {}); + + const result = ApplePayHelpers.createApplePayTransactionInfo(jsonDict); + + expect(result.countryCode).toBe('US'); + expect(result.currencyCode).toBe('USD'); + }); + + it('uses default country code when not provided', () => { + const jsonDict = { + currencyCode: 'EUR', + total: { label: 'Test', amount: '500' }, + merchantCapabilities: [], + supportedNetworks: [], + }; + + mockGetString.mockImplementation((obj: any, key: string, def: string) => { + if (key === 'countryCode') return def; + return obj?.[key] ?? def; + }); + + const result = ApplePayHelpers.createApplePayTransactionInfo(jsonDict); + + expect(result.countryCode).toBe('US'); + }); + + it('handles empty jsonDict', () => { + mockGetString.mockImplementation((_obj: any, _key: string, def: string) => def); + mockGetArray.mockImplementation(() => []); + mockGetDictFromObj.mockImplementation(() => ({})); + + const result = ApplePayHelpers.createApplePayTransactionInfo({}); + + expect(result).toBeDefined(); + expect(result.merchantCapabilities).toEqual([]); + expect(result.supportedNetworks).toEqual([]); + }); + }); + + describe('handleApplePayButtonClicked', () => { + it('sends message to parent window with correct data', () => { + const sessionObj = { + session_token_data: { secrets: { display: 'test-token' } }, + connector: 'stripe', + }; + const paymentMethodListValue = { is_tax_calculation_enabled: false }; + + mockGetDictFromJson.mockImplementation((obj: any) => obj); + mockGetDictFromDict.mockImplementation((obj: any, key: string) => obj?.[key] || {}); + mockGetString.mockImplementation((obj: any, key: string, def: string) => obj?.[key] ?? def); + + ApplePayHelpers.handleApplePayButtonClicked(sessionObj, 'apple-pay-component', paymentMethodListValue); + + expect(mockMessageParentWindow).toHaveBeenCalled(); + }); + + it('handles missing session token data', () => { + const sessionObj = {}; + const paymentMethodListValue = { is_tax_calculation_enabled: false }; + + mockGetDictFromJson.mockImplementation((obj: any) => obj); + mockGetDictFromDict.mockImplementation((obj: any, key: string) => obj?.[key] || {}); + mockGetString.mockImplementation((obj: any, key: string, def: string) => def); + + ApplePayHelpers.handleApplePayButtonClicked(sessionObj, 'apple-pay-component', paymentMethodListValue); + + expect(mockMessageParentWindow).toHaveBeenCalled(); + }); + + it('includes tax calculation flag in message', () => { + const sessionObj = { + session_token_data: { secrets: { display: 'test-token' } }, + connector: 'adyen', + }; + const paymentMethodListValue = { is_tax_calculation_enabled: true }; + + mockGetDictFromJson.mockImplementation((obj: any) => obj); + mockGetDictFromDict.mockImplementation((obj: any, key: string) => obj?.[key] || {}); + mockGetString.mockImplementation((obj: any, key: string, def: string) => obj?.[key] ?? def); + + ApplePayHelpers.handleApplePayButtonClicked(sessionObj, 'apple-pay-component', paymentMethodListValue); + + const callArgs = mockMessageParentWindow.mock.calls[0]; + const messageData = callArgs[1]; + const taxCalculationEntry = messageData.find((entry: any) => entry[0] === 'isTaxCalculationEnabled'); + expect(taxCalculationEntry[1]).toBe(true); + }); + }); + + describe('useHandleApplePayResponse', () => { + let addEventListenerSpy: jest.SpyInstance; + let removeEventListenerSpy: jest.SpyInstance; + + beforeEach(() => { + addEventListenerSpy = jest.spyOn(window, 'addEventListener'); + removeEventListenerSpy = jest.spyOn(window, 'removeEventListener'); + }); + + afterEach(() => { + addEventListenerSpy.mockRestore(); + removeEventListenerSpy.mockRestore(); + }); + + it('hook exists and is a function', () => { + expect(typeof ApplePayHelpers.useHandleApplePayResponse).toBe('function'); + }); + + it('adds message event listener on mount', () => { + renderHook(() => ApplePayHelpers.useHandleApplePayResponse({}, jest.fn())); + + expect(addEventListenerSpy).toHaveBeenCalledWith('message', expect.any(Function)); + }); + + it('removes message event listener on unmount', () => { + const { unmount } = renderHook(() => ApplePayHelpers.useHandleApplePayResponse({}, jest.fn())); + + unmount(); + + expect(removeEventListenerSpy).toHaveBeenCalledWith('message', expect.any(Function)); + }); + + it('handles applePayPaymentToken message', () => { + const mockIntent = jest.fn(); + mockSafeParse.mockImplementation((str: string) => { + try { + return JSON.parse(str); + } catch { + return null; + } + }); + mockGetDictFromJson.mockImplementation((obj: any) => obj); + mockGetDictFromDict.mockImplementation((obj: any, key: string) => obj?.[key]); + mockMergeAndFlattenToTuples.mockReturnValue([['apple_pay', {}]]); + + renderHook(() => ApplePayHelpers.useHandleApplePayResponse({}, mockIntent)); + + const messageHandler = addEventListenerSpy.mock.calls.find( + (call) => call[0] === 'message' + )?.[1]; + + act(() => { + messageHandler?.({ + data: JSON.stringify({ + applePayPaymentToken: { paymentData: 'test' }, + applePayBillingContact: {}, + applePayShippingContact: {}, + }), + }); + }); + + expect(mockMergeAndFlattenToTuples).toHaveBeenCalled(); + }); + + it('handles showApplePayButton message', () => { + const mockSetApplePayClicked = jest.fn(); + mockSafeParse.mockImplementation((str: string) => { + try { + return JSON.parse(str); + } catch { + return null; + } + }); + mockGetDictFromJson.mockImplementation((obj: any) => obj); + + renderHook(() => + ApplePayHelpers.useHandleApplePayResponse( + {}, + jest.fn(), + mockSetApplePayClicked + ) + ); + + const messageHandler = addEventListenerSpy.mock.calls.find( + (call) => call[0] === 'message' + )?.[1]; + + act(() => { + messageHandler?.({ + data: JSON.stringify({ + showApplePayButton: true, + }), + }); + }); + + expect(mockSetApplePayClicked).toHaveBeenCalled(); + }); + + it('handles applePaySyncPayment message', () => { + const mockSyncPayment = jest.fn(); + mockSafeParse.mockImplementation((str: string) => { + try { + return JSON.parse(str); + } catch { + return null; + } + }); + mockGetDictFromJson.mockImplementation((obj: any) => obj); + + renderHook(() => + ApplePayHelpers.useHandleApplePayResponse( + {}, + jest.fn(), + jest.fn(), + mockSyncPayment + ) + ); + + const messageHandler = addEventListenerSpy.mock.calls.find( + (call) => call[0] === 'message' + )?.[1]; + + act(() => { + messageHandler?.({ + data: JSON.stringify({ + applePaySyncPayment: true, + }), + }); + }); + + expect(mockSyncPayment).toHaveBeenCalled(); + }); + + it('handles applePayBraintreeSuccess message', () => { + const mockIntent = jest.fn(); + mockSafeParse.mockImplementation((str: string) => { + try { + return JSON.parse(str); + } catch { + return null; + } + }); + mockGetDictFromJson.mockImplementation((obj: any) => obj); + mockGetString.mockImplementation((obj: any, key: string, def: string) => obj?.[key] ?? def); + + renderHook(() => ApplePayHelpers.useHandleApplePayResponse({}, mockIntent)); + + const messageHandler = addEventListenerSpy.mock.calls.find( + (call) => call[0] === 'message' + )?.[1]; + + act(() => { + messageHandler?.({ + data: JSON.stringify({ + applePayBraintreeSuccess: true, + token: 'test-nonce', + }), + }); + }); + + expect(mockIntent).toHaveBeenCalled(); + }); + + it('sends applePaySessionAbort on unmount', () => { + const { unmount } = renderHook(() => ApplePayHelpers.useHandleApplePayResponse({}, jest.fn())); + + unmount(); + + expect(mockMessageParentWindow).toHaveBeenCalledWith( + undefined, + [['applePaySessionAbort', true]] + ); + }); + }); + + describe('useSubmitCallback', () => { + it('hook exists and is a function', () => { + expect(typeof ApplePayHelpers.useSubmitCallback).toBe('function'); + }); + + it('returns a callback function', () => { + const { result } = renderHook(() => + ApplePayHelpers.useSubmitCallback(true, {}, 'test-component') + ); + + expect(typeof result.current).toBe('function'); + }); + + it('does nothing when isWallet is true', () => { + mockSafeParse.mockImplementation((str: string) => { + try { + return JSON.parse(str); + } catch { + return null; + } + }); + + const { result } = renderHook(() => + ApplePayHelpers.useSubmitCallback(true, {}, 'test-component') + ); + + act(() => { + result.current({ data: JSON.stringify({ doSubmit: true }) }); + }); + + expect(mockMessageParentWindow).not.toHaveBeenCalled(); + }); + }); + + describe('startApplePaySession', () => { + let mockSession: any; + + beforeEach(() => { + mockSession = { + abort: jest.fn(), + begin: jest.fn(), + completeMerchantValidation: jest.fn(), + completeShippingContactSelection: jest.fn(), + completePayment: jest.fn(), + onvalidatemerchant: null, + onshippingcontactselected: null, + onpaymentauthorized: null, + oncancel: null, + STATUS_SUCCESS: 1, + STATUS_FAILURE: 0, + }; + + (globalThis as any).ApplePaySession = jest.fn(() => mockSession); + }); + + afterEach(() => { + delete (globalThis as any).ApplePaySession; + }); + + it('function exists', () => { + expect(typeof ApplePayHelpers.startApplePaySession).toBe('function'); + }); + + it('creates new ApplePaySession and calls begin', () => { + const mockLogger = { setLogInfo: jest.fn(), setLogError: jest.fn() }; + const paymentRequest = JSON.stringify({ + total: { label: 'Test', amount: '100', type: 'final' }, + }); + const applePaySessionRef = { contents: null }; + const callBackFunc = jest.fn(); + const resolvePromise = jest.fn(); + + ApplePayHelpers.startApplePaySession( + paymentRequest, + applePaySessionRef, + undefined, + mockLogger, + callBackFunc, + resolvePromise, + 'client_secret', + 'pk_test' + ); + + expect(mockSession.begin).toHaveBeenCalled(); + expect(mockSession.onvalidatemerchant).not.toBeNull(); + expect(mockSession.onpaymentauthorized).not.toBeNull(); + expect(mockSession.oncancel).not.toBeNull(); + }); + + it('aborts existing session before creating new one', () => { + const mockLogger = { setLogInfo: jest.fn(), setLogError: jest.fn() }; + const paymentRequest = JSON.stringify({ total: { label: 'Test', amount: '100' } }); + const existingSession = { abort: jest.fn() }; + const applePaySessionRef = { contents: existingSession }; + const callBackFunc = jest.fn(); + const resolvePromise = jest.fn(); + + ApplePayHelpers.startApplePaySession( + paymentRequest, + applePaySessionRef, + undefined, + mockLogger, + callBackFunc, + resolvePromise, + 'client_secret', + 'pk_test' + ); + + expect(existingSession.abort).toHaveBeenCalled(); + }); + + it('handles abort failure gracefully', () => { + const mockAbort = jest.fn(() => { throw new Error('Abort failed'); }); + const mockLogger = { setLogInfo: jest.fn(), setLogError: jest.fn() }; + const paymentRequest = JSON.stringify({ total: { label: 'Test', amount: '100' } }); + const existingSession = { abort: mockAbort }; + const applePaySessionRef = { contents: existingSession }; + const callBackFunc = jest.fn(); + const resolvePromise = jest.fn(); + + ApplePayHelpers.startApplePaySession( + paymentRequest, + applePaySessionRef, + undefined, + mockLogger, + callBackFunc, + resolvePromise, + 'client_secret', + 'pk_test' + ); + + expect(mockSession.begin).toHaveBeenCalled(); + }); + + it('onvalidatemerchant completes merchant validation', () => { + const mockLogger = { setLogInfo: jest.fn(), setLogError: jest.fn() }; + const paymentRequest = JSON.stringify({ + total: { label: 'Test', amount: '100', type: 'final' }, + }); + const applePaySessionRef = { contents: null }; + const callBackFunc = jest.fn(); + const resolvePromise = jest.fn(); + + mockGetDictFromJson.mockImplementation((obj: any) => obj || {}); + mockGetDictFromDict.mockImplementation((obj: any, key: string) => obj?.[key] || {}); + mockTransformKeys.mockImplementation((obj: any) => obj); + + ApplePayHelpers.startApplePaySession( + paymentRequest, + applePaySessionRef, + { session_token_data: {} }, + mockLogger, + callBackFunc, + resolvePromise, + 'client_secret', + 'pk_test' + ); + + act(() => { + mockSession.onvalidatemerchant({}); + }); + + expect(mockSession.completeMerchantValidation).toHaveBeenCalled(); + }); + + it('onshippingcontactselected completes without tax calculation', () => { + const mockLogger = { setLogInfo: jest.fn(), setLogError: jest.fn() }; + const paymentRequest = JSON.stringify({ + total: { label: 'Test', amount: '100', type: 'final' }, + }); + const applePaySessionRef = { contents: null }; + const callBackFunc = jest.fn(); + const resolvePromise = jest.fn(); + + mockGetDictFromJson.mockImplementation((obj: any) => obj || {}); + mockGetString.mockImplementation((obj: any, key: string, def: string) => obj?.[key] ?? def); + + ApplePayHelpers.startApplePaySession( + paymentRequest, + applePaySessionRef, + undefined, + mockLogger, + callBackFunc, + resolvePromise, + 'client_secret', + 'pk_test', + false + ); + + act(() => { + mockSession.onshippingcontactselected({ + shippingContact: {}, + }); + }); + + expect(mockSession.completeShippingContactSelection).toHaveBeenCalled(); + }); + + it('onshippingcontactselected with tax calculation enabled', async () => { + const mockLogger = { setLogInfo: jest.fn(), setLogError: jest.fn() }; + const paymentRequest = JSON.stringify({ + total: { label: 'Test', amount: '100', type: 'final' }, + }); + const applePaySessionRef = { contents: null }; + const callBackFunc = jest.fn(); + const resolvePromise = jest.fn(); + + mockGetDictFromJson.mockImplementation((obj: any) => obj || {}); + mockGetString.mockImplementation((obj: any, key: string, def: string) => obj?.[key] ?? def); + mockMinorUnitToString.mockImplementation((val: number) => String(val)); + + ApplePayHelpers.startApplePaySession( + paymentRequest, + applePaySessionRef, + undefined, + mockLogger, + callBackFunc, + resolvePromise, + 'client_secret', + 'pk_test', + true + ); + + await act(async () => { + await mockSession.onshippingcontactselected({ + shippingContact: JSON.stringify({ + administrativeArea: 'CA', + countryCode: 'US', + postalCode: '12345', + }), + }); + }); + + expect(mockSession.completeShippingContactSelection).toHaveBeenCalled(); + }); + + it('onpaymentauthorized completes payment and calls callback', () => { + const mockLogger = { setLogInfo: jest.fn(), setLogError: jest.fn() }; + const paymentRequest = JSON.stringify({ + total: { label: 'Test', amount: '100', type: 'final' }, + }); + const applePaySessionRef = { contents: null }; + const callBackFunc = jest.fn(); + const resolvePromise = jest.fn(); + + ApplePayHelpers.startApplePaySession( + paymentRequest, + applePaySessionRef, + undefined, + mockLogger, + callBackFunc, + resolvePromise, + 'client_secret', + 'pk_test' + ); + + act(() => { + mockSession.onpaymentauthorized({ + payment: { token: { paymentData: 'test' } }, + }); + }); + + expect(mockSession.completePayment).toHaveBeenCalled(); + expect(callBackFunc).toHaveBeenCalled(); + expect(applePaySessionRef.contents).toBeNull(); + }); + + it('oncancel resolves promise with failure response', () => { + const mockLogger = { setLogInfo: jest.fn(), setLogError: jest.fn() }; + const paymentRequest = JSON.stringify({ + total: { label: 'Test', amount: '100', type: 'final' }, + }); + const applePaySessionRef = { contents: null }; + const callBackFunc = jest.fn(); + const resolvePromise = jest.fn(); + + ApplePayHelpers.startApplePaySession( + paymentRequest, + applePaySessionRef, + undefined, + mockLogger, + callBackFunc, + resolvePromise, + 'client_secret', + 'pk_test' + ); + + act(() => { + mockSession.oncancel(); + }); + + expect(mockLogger.setLogError).toHaveBeenCalled(); + expect(resolvePromise).toHaveBeenCalled(); + expect(applePaySessionRef.contents).toBeNull(); + }); + }); + + describe('handleApplePayBraintreePaymentSession', () => { + let mockSession: any; + + beforeEach(() => { + mockSession = { + begin: jest.fn(), + abort: jest.fn(), + completeMerchantValidation: jest.fn(), + completePayment: jest.fn(), + onvalidatemerchant: null, + onpaymentauthorized: null, + oncancel: null, + STATUS_SUCCESS: 1, + STATUS_FAILURE: 0, + }; + + (globalThis as any).ApplePaySession = jest.fn(() => mockSession); + }); + + afterEach(() => { + delete (globalThis as any).ApplePaySession; + }); + + it('function exists', () => { + expect(typeof ApplePayHelpers.handleApplePayBraintreePaymentSession).toBe('function'); + }); + + it('creates session and calls begin', () => { + mockGetDictFromJson.mockImplementation((obj: any) => obj || {}); + mockGetString.mockImplementation((obj: any, key: string, def: string) => obj?.[key] ?? def); + mockGetArray.mockImplementation((obj: any, key: string) => obj?.[key] || []); + mockGetDictFromObj.mockImplementation((obj: any, key: string) => obj?.[key] || {}); + + const mockApplePayInstance = { + createPaymentRequest: jest.fn(() => ({})), + performValidation: jest.fn(), + tokenize: jest.fn(), + }; + + const onError = jest.fn(); + const onSuccess = jest.fn(); + + ApplePayHelpers.handleApplePayBraintreePaymentSession({}, mockApplePayInstance, onError, onSuccess); + + expect(mockSession.begin).toHaveBeenCalled(); + }); + + it('onvalidatemerchant calls performValidation', () => { + mockGetDictFromJson.mockImplementation((obj: any) => obj || {}); + mockGetString.mockImplementation((obj: any, key: string, def: string) => obj?.[key] ?? def); + mockGetArray.mockImplementation((obj: any, key: string) => obj?.[key] || []); + mockGetDictFromObj.mockImplementation((obj: any, key: string) => obj?.[key] || {}); + + const mockApplePayInstance = { + createPaymentRequest: jest.fn(() => ({})), + performValidation: jest.fn((_config: any, callback: any) => callback(null, {})), + tokenize: jest.fn(), + }; + + const onError = jest.fn(); + const onSuccess = jest.fn(); + + ApplePayHelpers.handleApplePayBraintreePaymentSession( + { total: { label: 'Test', amount: '100' } }, + mockApplePayInstance, + onError, + onSuccess + ); + + act(() => { + mockSession.onvalidatemerchant({ validationURL: 'https://test.com' }); + }); + + expect(mockApplePayInstance.performValidation).toHaveBeenCalled(); + }); + + it('onvalidatemerchant handles validation error', () => { + mockGetDictFromJson.mockImplementation((obj: any) => obj || {}); + mockGetString.mockImplementation((obj: any, key: string, def: string) => obj?.[key] ?? def); + mockGetArray.mockImplementation((obj: any, key: string) => obj?.[key] || []); + mockGetDictFromObj.mockImplementation((obj: any, key: string) => obj?.[key] || {}); + + const mockApplePayInstance = { + createPaymentRequest: jest.fn(() => ({})), + performValidation: jest.fn((_config: any, callback: any) => callback(new Error('Validation failed'), null)), + tokenize: jest.fn(), + }; + + const onError = jest.fn(); + const onSuccess = jest.fn(); + + ApplePayHelpers.handleApplePayBraintreePaymentSession( + { total: { label: 'Test', amount: '100' } }, + mockApplePayInstance, + onError, + onSuccess + ); + + act(() => { + mockSession.onvalidatemerchant({ validationURL: 'https://test.com' }); + }); + + expect(onError).toHaveBeenCalled(); + expect(mockSession.abort).toHaveBeenCalled(); + }); + + it('onpaymentauthorized tokenizes and calls onSuccess', () => { + mockGetDictFromJson.mockImplementation((obj: any) => obj || {}); + mockGetString.mockImplementation((obj: any, key: string, def: string) => obj?.[key] ?? def); + mockGetArray.mockImplementation((obj: any, key: string) => obj?.[key] || []); + mockGetDictFromObj.mockImplementation((obj: any, key: string) => obj?.[key] || {}); + + const mockApplePayInstance = { + createPaymentRequest: jest.fn(() => ({})), + performValidation: jest.fn(), + tokenize: jest.fn((_config: any, callback: any) => callback(null, { nonce: 'test-nonce' })), + }; + + const onError = jest.fn(); + const onSuccess = jest.fn(); + + (globalThis as any).ApplePaySession = jest.fn(() => mockSession); + (globalThis as any).window = { ApplePaySession: mockSession }; + + ApplePayHelpers.handleApplePayBraintreePaymentSession( + { total: { label: 'Test', amount: '100' } }, + mockApplePayInstance, + onError, + onSuccess + ); + + act(() => { + mockSession.onpaymentauthorized({ payment: { token: {} } }); + }); + + expect(mockApplePayInstance.tokenize).toHaveBeenCalled(); + }); + + it('onpaymentauthorized handles tokenization error', () => { + mockGetDictFromJson.mockImplementation((obj: any) => obj || {}); + mockGetString.mockImplementation((obj: any, key: string, def: string) => obj?.[key] ?? def); + mockGetArray.mockImplementation((obj: any, key: string) => obj?.[key] || []); + mockGetDictFromObj.mockImplementation((obj: any, key: string) => obj?.[key] || {}); + + const mockApplePayInstance = { + createPaymentRequest: jest.fn(() => ({})), + performValidation: jest.fn(), + tokenize: jest.fn((_config: any, callback: any) => callback(new Error('Tokenization failed'), null)), + }; + + const onError = jest.fn(); + const onSuccess = jest.fn(); + + (globalThis as any).window = { ApplePaySession: mockSession }; + + ApplePayHelpers.handleApplePayBraintreePaymentSession( + { total: { label: 'Test', amount: '100' } }, + mockApplePayInstance, + onError, + onSuccess + ); + + act(() => { + mockSession.onpaymentauthorized({ payment: { token: {} } }); + }); + + expect(onError).toHaveBeenCalledWith('ApplePay Tokenization Failed'); + }); + + it('oncancel calls onError', () => { + mockGetDictFromJson.mockImplementation((obj: any) => obj || {}); + mockGetString.mockImplementation((obj: any, key: string, def: string) => obj?.[key] ?? def); + mockGetArray.mockImplementation((obj: any, key: string) => obj?.[key] || []); + mockGetDictFromObj.mockImplementation((obj: any, key: string) => obj?.[key] || {}); + + const mockApplePayInstance = { + createPaymentRequest: jest.fn(() => ({})), + performValidation: jest.fn(), + tokenize: jest.fn(), + }; + + const onError = jest.fn(); + const onSuccess = jest.fn(); + + ApplePayHelpers.handleApplePayBraintreePaymentSession({}, mockApplePayInstance, onError, onSuccess); + + act(() => { + mockSession.oncancel(); + }); + + expect(onError).toHaveBeenCalledWith('Apple Pay Payment Cancelled.'); + }); + + it('handles exception during session creation', () => { + mockGetDictFromJson.mockImplementation((obj: any) => obj || {}); + mockGetString.mockImplementation((obj: any, key: string, def: string) => obj?.[key] ?? def); + mockGetArray.mockImplementation((obj: any, key: string) => obj?.[key] || []); + mockGetDictFromObj.mockImplementation((obj: any, key: string) => obj?.[key] || {}); + mockFormatException.mockImplementation((e: any) => e?.message || String(e)); + + (globalThis as any).ApplePaySession = jest.fn(() => { + throw new Error('Session creation failed'); + }); + + const mockApplePayInstance = { + createPaymentRequest: jest.fn(() => ({})), + }; + + const onError = jest.fn(); + const onSuccess = jest.fn(); + + ApplePayHelpers.handleApplePayBraintreePaymentSession({}, mockApplePayInstance, onError, onSuccess); + + expect(onError).toHaveBeenCalled(); + }); + }); + + describe('handleApplePayBraintreeClick', () => { + let mockBraintree: any; + + beforeEach(() => { + mockBraintree = { + client: { + create: jest.fn(), + }, + applePay: { + create: jest.fn(), + }, + }; + + (globalThis as any).braintree = mockBraintree; + }); + + afterEach(() => { + delete (globalThis as any).braintree; + }); + + it('function exists', () => { + expect(typeof ApplePayHelpers.handleApplePayBraintreeClick).toBe('function'); + }); + + it('calls messageParentWindow with fullscreen true on start', () => { + const mockLogger = { setLogInfo: jest.fn(), setLogError: jest.fn() }; + mockBraintree.client.create.mockImplementation((_config: any, callback: any) => { + callback(null, {}); + }); + mockBraintree.applePay.create.mockImplementation((_config: any, callback: any) => { + callback(null, { createPaymentRequest: jest.fn(() => ({})) }); + }); + + const mockEvent = { source: { postMessage: jest.fn() } }; + + ApplePayHelpers.handleApplePayBraintreeClick( + 'test-auth', + {}, + 'test-selector', + mockLogger, + mockEvent + ); + + expect(mockMessageParentWindow).toHaveBeenCalledWith( + undefined, + expect.arrayContaining([['fullscreen', true]]) + ); + }); + + it('calls braintree.client.create with authorization', () => { + const mockLogger = { setLogInfo: jest.fn(), setLogError: jest.fn() }; + mockBraintree.client.create.mockImplementation((_config: any, callback: any) => { + callback(null, {}); + }); + mockBraintree.applePay.create.mockImplementation((_config: any, callback: any) => { + callback(null, { createPaymentRequest: jest.fn(() => ({})) }); + }); + + const mockEvent = { source: { postMessage: jest.fn() } }; + + ApplePayHelpers.handleApplePayBraintreeClick( + 'test-auth', + {}, + 'test-selector', + mockLogger, + mockEvent + ); + + expect(mockBraintree.client.create).toHaveBeenCalledWith( + { authorization: 'test-auth' }, + expect.any(Function) + ); + }); + + it('handles braintree client creation error', () => { + const mockLogger = { setLogInfo: jest.fn(), setLogError: jest.fn() }; + mockBraintree.client.create.mockImplementation((_config: any, callback: any) => { + callback(new Error('Client creation failed'), null); + }); + + const mockEvent = { source: { postMessage: jest.fn() } }; + + ApplePayHelpers.handleApplePayBraintreeClick( + 'test-auth', + {}, + 'test-selector', + mockLogger, + mockEvent + ); + + expect(mockLogger.setLogError).toHaveBeenCalled(); + }); + + it('handles braintree applePay creation error', () => { + const mockLogger = { setLogInfo: jest.fn(), setLogError: jest.fn() }; + mockBraintree.client.create.mockImplementation((_config: any, callback: any) => { + callback(null, {}); + }); + mockBraintree.applePay.create.mockImplementation((_config: any, callback: any) => { + callback(new Error('ApplePay creation failed'), null); + }); + + const mockEvent = { source: { postMessage: jest.fn() } }; + + ApplePayHelpers.handleApplePayBraintreeClick( + 'test-auth', + {}, + 'test-selector', + mockLogger, + mockEvent + ); + + expect(mockLogger.setLogError).toHaveBeenCalled(); + }); + + it('handles exception during client creation', () => { + const mockLogger = { setLogInfo: jest.fn(), setLogError: jest.fn() }; + mockFormatException.mockImplementation((e: any) => e?.message || String(e)); + mockBraintree.client.create.mockImplementation(() => { + throw new Error('Unexpected error'); + }); + + const mockEvent = { source: { postMessage: jest.fn() } }; + + ApplePayHelpers.handleApplePayBraintreeClick( + 'test-auth', + {}, + 'test-selector', + mockLogger, + mockEvent + ); + + expect(mockLogger.setLogError).toHaveBeenCalled(); + }); + + it('handles exception during applePay creation', () => { + const mockLogger = { setLogInfo: jest.fn(), setLogError: jest.fn() }; + mockFormatException.mockImplementation((e: any) => e?.message || String(e)); + mockBraintree.client.create.mockImplementation((_config: any, callback: any) => { + callback(null, {}); + }); + mockBraintree.applePay.create.mockImplementation(() => { + throw new Error('ApplePay creation error'); + }); + + const mockEvent = { source: { postMessage: jest.fn() } }; + + ApplePayHelpers.handleApplePayBraintreeClick( + 'test-auth', + {}, + 'test-selector', + mockLogger, + mockEvent + ); + + expect(mockLogger.setLogError).toHaveBeenCalled(); + }); + + it('onSuccess with empty token logs error', () => { + const mockLogger = { setLogInfo: jest.fn(), setLogError: jest.fn() }; + mockBraintree.client.create.mockImplementation((_config: any, callback: any) => { + callback(null, {}); + }); + + const mockSession = { + begin: jest.fn(), + abort: jest.fn(), + completeMerchantValidation: jest.fn(), + completePayment: jest.fn(), + onvalidatemerchant: null, + onpaymentauthorized: null, + oncancel: null, + STATUS_SUCCESS: 1, + STATUS_FAILURE: 0, + }; + (globalThis as any).ApplePaySession = jest.fn(() => mockSession); + + mockBraintree.applePay.create.mockImplementation((_config: any, callback: any) => { + callback(null, { + createPaymentRequest: jest.fn(() => ({})), + performValidation: jest.fn((_c: any, cb: any) => cb(null, {})), + tokenize: jest.fn((_c: any, cb: any) => cb(null, { nonce: '' })), + }); + }); + + mockGetDictFromJson.mockImplementation((obj: any) => obj || {}); + mockGetString.mockImplementation((obj: any, key: string, def: string) => obj?.[key] ?? def); + mockGetArray.mockImplementation((obj: any, key: string) => obj?.[key] || []); + mockGetDictFromObj.mockImplementation((obj: any, key: string) => obj?.[key] || {}); + + const mockEvent = { source: { postMessage: jest.fn() } }; + + ApplePayHelpers.handleApplePayBraintreeClick( + 'test-auth', + { total: { label: 'Test', amount: '100' } }, + 'test-selector', + mockLogger, + mockEvent + ); + + delete (globalThis as any).ApplePaySession; + }); + + it('onSuccess with valid token sends post message', () => { + const mockLogger = { setLogInfo: jest.fn(), setLogError: jest.fn() }; + mockBraintree.client.create.mockImplementation((_config: any, callback: any) => { + callback(null, {}); + }); + + const mockSession = { + begin: jest.fn(), + abort: jest.fn(), + completeMerchantValidation: jest.fn(), + completePayment: jest.fn(), + onvalidatemerchant: null, + onpaymentauthorized: null, + oncancel: null, + STATUS_SUCCESS: 1, + STATUS_FAILURE: 0, + }; + (globalThis as any).ApplePaySession = jest.fn(() => mockSession); + (globalThis as any).window = { ApplePaySession: mockSession }; + + mockBraintree.applePay.create.mockImplementation((_config: any, callback: any) => { + callback(null, { + createPaymentRequest: jest.fn(() => ({})), + performValidation: jest.fn((_c: any, cb: any) => cb(null, {})), + tokenize: jest.fn((_c: any, cb: any) => cb(null, { nonce: 'valid-nonce' })), + }); + }); + + mockGetDictFromJson.mockImplementation((obj: any) => obj || {}); + mockGetString.mockImplementation((obj: any, key: string, def: string) => obj?.[key] ?? def); + mockGetArray.mockImplementation((obj: any, key: string) => obj?.[key] || []); + mockGetDictFromObj.mockImplementation((obj: any, key: string) => obj?.[key] || {}); + + const mockEvent = { source: { postMessage: jest.fn() } }; + + ApplePayHelpers.handleApplePayBraintreeClick( + 'test-auth', + { total: { label: 'Test', amount: '100' } }, + 'test-selector', + mockLogger, + mockEvent + ); + + delete (globalThis as any).ApplePaySession; + }); + }); +}); diff --git a/src/__tests__/ApplePayTypes.test.ts b/src/__tests__/ApplePayTypes.test.ts new file mode 100644 index 000000000..1cb6bc34e --- /dev/null +++ b/src/__tests__/ApplePayTypes.test.ts @@ -0,0 +1,216 @@ +import { + getTotal, + jsonToPaymentRequestDataType, + billingContactItemToObjMapper, + shippingContactItemToObjMapper, + defaultHeadlessApplePayToken, +} from '../Types/ApplePayTypes.bs.js'; + +describe('ApplePayTypes', () => { + describe('defaultHeadlessApplePayToken', () => { + it('should have null paymentRequestData', () => { + expect(defaultHeadlessApplePayToken.paymentRequestData).toBeNull(); + }); + + it('should have undefined sessionTokenData', () => { + expect(defaultHeadlessApplePayToken.sessionTokenData).toBeUndefined(); + }); + }); + + describe('getTotal', () => { + it('should extract total without type when type is empty', () => { + const totalDict = { + label: 'Total', + amount: '10.00', + type: '', + }; + const result = getTotal(totalDict); + expect(result.label).toBe('Total'); + expect(result.amount).toBe('10.00'); + expect(result.type).toBeUndefined(); + }); + + it('should extract total with type when type is present', () => { + const totalDict = { + label: 'Total', + amount: '10.00', + type: 'final', + }; + const result = getTotal(totalDict); + expect(result.label).toBe('Total'); + expect(result.amount).toBe('10.00'); + expect(result.type).toBe('final'); + }); + + it('should handle missing fields with defaults', () => { + const totalDict = {}; + const result = getTotal(totalDict); + expect(result.label).toBe(''); + expect(result.amount).toBe(''); + }); + + it('should handle partial fields', () => { + const totalDict = { + label: 'Subtotal', + }; + const result = getTotal(totalDict); + expect(result.label).toBe('Subtotal'); + expect(result.amount).toBe(''); + }); + }); + + describe('jsonToPaymentRequestDataType', () => { + it('should parse basic payment request without merchant identifier', () => { + const jsonDict = { + country_code: 'US', + currency_code: 'USD', + total: { + label: 'Total', + amount: '10.00', + }, + merchant_capabilities: ['supports3DS'], + supported_networks: ['visa', 'mastercard'], + }; + const result = jsonToPaymentRequestDataType(jsonDict); + expect(result.countryCode).toBe('US'); + expect(result.currencyCode).toBe('USD'); + expect(result.total.label).toBe('Total'); + expect(result.total.amount).toBe('10.00'); + expect(result.merchantCapabilities).toEqual(['supports3DS']); + expect(result.supportedNetworks).toEqual(['visa', 'mastercard']); + expect(result.merchantIdentifier).toBeUndefined(); + }); + + it('should parse payment request with merchant identifier', () => { + const jsonDict = { + country_code: 'US', + currency_code: 'USD', + total: { + label: 'Total', + amount: '10.00', + }, + merchant_capabilities: ['supports3DS'], + supported_networks: ['visa'], + merchant_identifier: 'merchant.com.example', + }; + const result = jsonToPaymentRequestDataType(jsonDict); + expect(result.merchantIdentifier).toBe('merchant.com.example'); + }); + + it('should use default country code when not provided', () => { + const jsonDict = { + currency_code: 'USD', + total: {}, + merchant_capabilities: [], + supported_networks: [], + }; + const result = jsonToPaymentRequestDataType(jsonDict); + expect(result.countryCode).toBe('IN'); + }); + + it('should handle empty arrays for capabilities and networks', () => { + const jsonDict = { + country_code: 'GB', + currency_code: 'GBP', + total: {}, + merchant_capabilities: [], + supported_networks: [], + }; + const result = jsonToPaymentRequestDataType(jsonDict); + expect(result.merchantCapabilities).toEqual([]); + expect(result.supportedNetworks).toEqual([]); + }); + }); + + describe('billingContactItemToObjMapper', () => { + it('should map billing contact with all fields', () => { + const dict = { + addressLines: ['123 Main St', 'Apt 4'], + administrativeArea: 'CA', + countryCode: 'US', + familyName: 'Doe', + givenName: 'John', + locality: 'San Francisco', + postalCode: '94105', + }; + const result = billingContactItemToObjMapper(dict); + expect(result.addressLines).toEqual(['123 Main St', 'Apt 4']); + expect(result.administrativeArea).toBe('CA'); + expect(result.countryCode).toBe('US'); + expect(result.familyName).toBe('Doe'); + expect(result.givenName).toBe('John'); + expect(result.locality).toBe('San Francisco'); + expect(result.postalCode).toBe('94105'); + }); + + it('should handle empty dict with defaults', () => { + const dict = {}; + const result = billingContactItemToObjMapper(dict); + expect(result.addressLines).toEqual([]); + expect(result.administrativeArea).toBe(''); + expect(result.countryCode).toBe(''); + expect(result.familyName).toBe(''); + expect(result.givenName).toBe(''); + expect(result.locality).toBe(''); + expect(result.postalCode).toBe(''); + }); + + it('should handle partial billing contact', () => { + const dict = { + givenName: 'Jane', + familyName: 'Smith', + }; + const result = billingContactItemToObjMapper(dict); + expect(result.givenName).toBe('Jane'); + expect(result.familyName).toBe('Smith'); + expect(result.addressLines).toEqual([]); + }); + }); + + describe('shippingContactItemToObjMapper', () => { + it('should map shipping contact with all fields', () => { + const dict = { + emailAddress: 'john@example.com', + phoneNumber: '+14155551234', + addressLines: ['456 Oak Ave'], + administrativeArea: 'NY', + countryCode: 'US', + familyName: 'Doe', + givenName: 'Jane', + locality: 'New York', + postalCode: '10001', + }; + const result = shippingContactItemToObjMapper(dict); + expect(result.emailAddress).toBe('john@example.com'); + expect(result.phoneNumber).toBe('+14155551234'); + expect(result.addressLines).toEqual(['456 Oak Ave']); + expect(result.administrativeArea).toBe('NY'); + expect(result.countryCode).toBe('US'); + expect(result.familyName).toBe('Doe'); + expect(result.givenName).toBe('Jane'); + expect(result.locality).toBe('New York'); + expect(result.postalCode).toBe('10001'); + }); + + it('should handle empty dict with defaults', () => { + const dict = {}; + const result = shippingContactItemToObjMapper(dict); + expect(result.emailAddress).toBe(''); + expect(result.phoneNumber).toBe(''); + expect(result.addressLines).toEqual([]); + expect(result.administrativeArea).toBe(''); + expect(result.countryCode).toBe(''); + }); + + it('should handle partial shipping contact', () => { + const dict = { + emailAddress: 'test@example.com', + phoneNumber: '555-1234', + }; + const result = shippingContactItemToObjMapper(dict); + expect(result.emailAddress).toBe('test@example.com'); + expect(result.phoneNumber).toBe('555-1234'); + expect(result.addressLines).toEqual([]); + }); + }); +}); diff --git a/src/__tests__/Bank.test.ts b/src/__tests__/Bank.test.ts new file mode 100644 index 000000000..5785aed62 --- /dev/null +++ b/src/__tests__/Bank.test.ts @@ -0,0 +1,192 @@ +import { + defaultEpsBank, + defaultIdealBank, + defaultBank, + polandBanks, + czechBanks, + p24Banks, + idealBanks, + epsBanks, + slovakiaBanks, + fpxBanks, + thailandBanks, + getBanks, +} from '../../src/Bank.bs.js'; + +describe('Bank', () => { + describe('constants', () => { + describe('defaultEpsBank', () => { + it('should have correct display name', () => { + expect(defaultEpsBank.displayName).toBe('Ärzte- und Apothekerbank'); + }); + + it('should have correct value', () => { + expect(defaultEpsBank.value).toBe('arzte_und_apotheker_bank'); + }); + }); + + describe('defaultIdealBank', () => { + it('should have correct display name', () => { + expect(defaultIdealBank.displayName).toBe('ABN AMRO'); + }); + + it('should have correct value', () => { + expect(defaultIdealBank.value).toBe('abn_amro'); + }); + }); + + describe('defaultBank', () => { + it('should have empty display name', () => { + expect(defaultBank.displayName).toBe(''); + }); + + it('should have empty value', () => { + expect(defaultBank.value).toBe(''); + }); + }); + }); + + describe('bank arrays', () => { + describe('polandBanks', () => { + it('should have correct length', () => { + expect(polandBanks.length).toBe(19); + }); + + it('should contain Alior Bank', () => { + expect(polandBanks.find(b => b.displayName === 'Alior Bank')).toBeDefined(); + }); + + it('should have correct structure', () => { + expect(polandBanks[0]).toHaveProperty('displayName'); + expect(polandBanks[0]).toHaveProperty('value'); + }); + }); + + describe('czechBanks', () => { + it('should have correct length', () => { + expect(czechBanks.length).toBe(3); + }); + + it('should contain Česká spořitelna', () => { + expect(czechBanks.find(b => b.displayName === 'Česká spořitelna')).toBeDefined(); + }); + }); + + describe('p24Banks', () => { + it('should have correct length', () => { + expect(p24Banks.length).toBe(22); + }); + + it('should contain BLIK', () => { + expect(p24Banks.find(b => b.displayName === 'BLIK')).toBeDefined(); + }); + }); + + describe('idealBanks', () => { + it('should have correct length', () => { + expect(idealBanks.length).toBe(16); + }); + + it('should contain ABN AMRO', () => { + expect(idealBanks.find(b => b.displayName === 'ABN AMRO')).toBeDefined(); + }); + }); + + describe('epsBanks', () => { + it('should have correct length', () => { + expect(epsBanks.length).toBe(31); + }); + + it('should contain Bank Austria', () => { + expect(epsBanks.find(b => b.displayName === 'Bank Austria')).toBeDefined(); + }); + }); + + describe('slovakiaBanks', () => { + it('should have correct length', () => { + expect(slovakiaBanks.length).toBe(5); + }); + + it('should contain Tatra Pay', () => { + expect(slovakiaBanks.find(b => b.displayName === 'Tatra Pay')).toBeDefined(); + }); + }); + + describe('fpxBanks', () => { + it('should have correct length', () => { + expect(fpxBanks.length).toBe(20); + }); + + it('should contain Maybank', () => { + expect(fpxBanks.find(b => b.displayName === 'Maybank')).toBeDefined(); + }); + }); + + describe('thailandBanks', () => { + it('should have correct length', () => { + expect(thailandBanks.length).toBe(5); + }); + + it('should contain Bangkok Bank', () => { + expect(thailandBanks.find(b => b.displayName === 'Bangkok Bank')).toBeDefined(); + }); + }); + }); + + describe('getBanks', () => { + describe('happy path', () => { + it('should return epsBanks for "eps"', () => { + expect(getBanks('eps')).toBe(epsBanks); + }); + + it('should return idealBanks for "ideal"', () => { + expect(getBanks('ideal')).toBe(idealBanks); + }); + + it('should return czechBanks for "online_banking_czech_republic"', () => { + expect(getBanks('online_banking_czech_republic')).toBe(czechBanks); + }); + + it('should return fpxBanks for "online_banking_fpx"', () => { + expect(getBanks('online_banking_fpx')).toBe(fpxBanks); + }); + + it('should return polandBanks for "online_banking_poland"', () => { + expect(getBanks('online_banking_poland')).toBe(polandBanks); + }); + + it('should return slovakiaBanks for "online_banking_slovakia"', () => { + expect(getBanks('online_banking_slovakia')).toBe(slovakiaBanks); + }); + + it('should return thailandBanks for "online_banking_thailand"', () => { + expect(getBanks('online_banking_thailand')).toBe(thailandBanks); + }); + + it('should return p24Banks for "przelewy24"', () => { + expect(getBanks('przelewy24')).toBe(p24Banks); + }); + }); + + describe('edge cases', () => { + it('should return empty array for unknown payment method', () => { + expect(getBanks('unknown')).toEqual([]); + }); + + it('should return empty array for empty string', () => { + expect(getBanks('')).toEqual([]); + }); + }); + + describe('error/boundary', () => { + it('should return empty array for null-like values', () => { + expect(getBanks('random_string')).toEqual([]); + }); + + it('should be case sensitive', () => { + expect(getBanks('EPS')).toEqual([]); + expect(getBanks('IDEAL')).toEqual([]); + }); + }); + }); +}); diff --git a/src/__tests__/BraintreeHelpers.test.ts b/src/__tests__/BraintreeHelpers.test.ts new file mode 100644 index 000000000..8a73d7537 --- /dev/null +++ b/src/__tests__/BraintreeHelpers.test.ts @@ -0,0 +1,142 @@ +import * as BraintreeHelpers from '../Utilities/BraintreeHelpers.bs.js'; + +const mockLoadScriptIfNotExist = jest.fn(); + +jest.mock('../Utilities/Utils.bs.js', () => ({ + loadScriptIfNotExist: (url: string, logger: any, scriptName: string) => + mockLoadScriptIfNotExist(url, logger, scriptName), +})); + +const createMockLogger = () => ({ + setLogInfo: jest.fn(), + setLogError: jest.fn(), +}); + +describe('BraintreeHelpers', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('braintreeApplePayUrl', () => { + it('has the correct Braintree Apple Pay script URL', () => { + expect(BraintreeHelpers.braintreeApplePayUrl).toBe( + 'https://js.braintreegateway.com/web/3.92.1/js/apple-pay.min.js' + ); + }); + + it('is a string', () => { + expect(typeof BraintreeHelpers.braintreeApplePayUrl).toBe('string'); + }); + + it('contains braintreegateway domain', () => { + expect(BraintreeHelpers.braintreeApplePayUrl).toContain('braintreegateway.com'); + }); + }); + + describe('braintreeClientUrl', () => { + it('has the correct Braintree client script URL', () => { + expect(BraintreeHelpers.braintreeClientUrl).toBe( + 'https://js.braintreegateway.com/web/3.92.1/js/client.min.js' + ); + }); + + it('is a string', () => { + expect(typeof BraintreeHelpers.braintreeClientUrl).toBe('string'); + }); + + it('contains braintreegateway domain', () => { + expect(BraintreeHelpers.braintreeClientUrl).toContain('braintreegateway.com'); + }); + + it('points to client.min.js', () => { + expect(BraintreeHelpers.braintreeClientUrl).toContain('client.min.js'); + }); + }); + + describe('loadBraintreeApplePayScripts', () => { + it('calls loadScriptIfNotExist for client script', () => { + const mockLogger = createMockLogger(); + + BraintreeHelpers.loadBraintreeApplePayScripts(mockLogger); + + expect(mockLoadScriptIfNotExist).toHaveBeenCalledWith( + 'https://js.braintreegateway.com/web/3.92.1/js/client.min.js', + mockLogger, + 'BRAINTREE_CLIENT_SCRIPT' + ); + }); + + it('calls loadScriptIfNotExist for Apple Pay script', () => { + const mockLogger = createMockLogger(); + + BraintreeHelpers.loadBraintreeApplePayScripts(mockLogger); + + expect(mockLoadScriptIfNotExist).toHaveBeenCalledWith( + 'https://js.braintreegateway.com/web/3.92.1/js/apple-pay.min.js', + mockLogger, + 'APPLE_PAY_BRAINTREE_SCRIPT' + ); + }); + + it('calls loadScriptIfNotExist twice', () => { + const mockLogger = createMockLogger(); + + BraintreeHelpers.loadBraintreeApplePayScripts(mockLogger); + + expect(mockLoadScriptIfNotExist).toHaveBeenCalledTimes(2); + }); + + it('passes logger to both script loads', () => { + const mockLogger = createMockLogger(); + + BraintreeHelpers.loadBraintreeApplePayScripts(mockLogger); + + expect(mockLoadScriptIfNotExist).toHaveBeenNthCalledWith( + 1, + expect.any(String), + mockLogger, + expect.any(String) + ); + expect(mockLoadScriptIfNotExist).toHaveBeenNthCalledWith( + 2, + expect.any(String), + mockLogger, + expect.any(String) + ); + }); + + it('uses correct script names for logging', () => { + const mockLogger = createMockLogger(); + + BraintreeHelpers.loadBraintreeApplePayScripts(mockLogger); + + const calls = mockLoadScriptIfNotExist.mock.calls; + const scriptNames = calls.map((call) => call[2]); + + expect(scriptNames).toContain('BRAINTREE_CLIENT_SCRIPT'); + expect(scriptNames).toContain('APPLE_PAY_BRAINTREE_SCRIPT'); + }); + + it('handles null logger gracefully', () => { + expect(() => { + BraintreeHelpers.loadBraintreeApplePayScripts(null as any); + }).not.toThrow(); + }); + + it('handles undefined logger gracefully', () => { + expect(() => { + BraintreeHelpers.loadBraintreeApplePayScripts(undefined as any); + }).not.toThrow(); + }); + + it('loads scripts in correct order (client first, then Apple Pay)', () => { + const mockLogger = createMockLogger(); + + BraintreeHelpers.loadBraintreeApplePayScripts(mockLogger); + + const calls = mockLoadScriptIfNotExist.mock.calls; + expect(calls[0][2]).toBe('BRAINTREE_CLIENT_SCRIPT'); + expect(calls[1][2]).toBe('APPLE_PAY_BRAINTREE_SCRIPT'); + }); + }); +}); diff --git a/src/__tests__/BrowserSpec.test.ts b/src/__tests__/BrowserSpec.test.ts new file mode 100644 index 000000000..7916d35a6 --- /dev/null +++ b/src/__tests__/BrowserSpec.test.ts @@ -0,0 +1,176 @@ +import * as BrowserSpec from '../BrowserSpec.bs.js'; + +describe('BrowserSpec', () => { + describe('checkIsSafari', () => { + const originalNavigator = navigator; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + Object.defineProperty(globalThis, 'navigator', { + value: originalNavigator, + configurable: true, + writable: true, + }); + }); + + it('returns true when Safari is present but Chrome is not', () => { + Object.defineProperty(globalThis, 'navigator', { + value: { userAgent: 'Mozilla/5.0 Safari/605.1.15' }, + configurable: true, + writable: true, + }); + + expect(BrowserSpec.checkIsSafari()).toBe(true); + }); + + it('returns false when Chrome is present even if Safari is also present', () => { + Object.defineProperty(globalThis, 'navigator', { + value: { userAgent: 'Mozilla/5.0 Chrome/91.0 Safari/537.36' }, + configurable: true, + writable: true, + }); + + expect(BrowserSpec.checkIsSafari()).toBe(false); + }); + + it('returns false when neither Safari nor Chrome is present', () => { + Object.defineProperty(globalThis, 'navigator', { + value: { userAgent: 'Mozilla/5.0 Firefox/89.0' }, + configurable: true, + writable: true, + }); + + expect(BrowserSpec.checkIsSafari()).toBe(false); + }); + + it('returns false for empty user agent', () => { + Object.defineProperty(globalThis, 'navigator', { + value: { userAgent: '' }, + configurable: true, + writable: true, + }); + + expect(BrowserSpec.checkIsSafari()).toBe(false); + }); + + it('handles Chrome-only user agent', () => { + Object.defineProperty(globalThis, 'navigator', { + value: { userAgent: 'Mozilla/5.0 Chrome/91.0.4472.124' }, + configurable: true, + writable: true, + }); + + expect(BrowserSpec.checkIsSafari()).toBe(false); + }); + }); + + describe('date', () => { + it('is a Date object', () => { + expect(BrowserSpec.date).toBeInstanceOf(Date); + }); + + it('has getTimezoneOffset method', () => { + expect(typeof BrowserSpec.date.getTimezoneOffset).toBe('function'); + }); + + it('returns a number from getTimezoneOffset', () => { + const offset = BrowserSpec.date.getTimezoneOffset(); + expect(typeof offset).toBe('number'); + }); + }); + + describe('broswerInfo', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('returns an array with browser_info key', () => { + const result = BrowserSpec.broswerInfo(); + + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBe(1); + expect(result[0][0]).toBe('browser_info'); + }); + + it('includes user_agent in browser info', () => { + const result = BrowserSpec.broswerInfo(); + const browserInfo = result[0][1] as Record; + + expect(browserInfo).toHaveProperty('user_agent'); + }); + + it('includes accept_header in browser info', () => { + const result = BrowserSpec.broswerInfo(); + const browserInfo = result[0][1] as Record; + + expect(browserInfo.accept_header).toContain('text/html'); + }); + + it('includes language from navigator', () => { + const result = BrowserSpec.broswerInfo(); + const browserInfo = result[0][1] as Record; + + expect(typeof browserInfo.language).toBe('string'); + }); + + it('includes color_depth from screen', () => { + const result = BrowserSpec.broswerInfo(); + const browserInfo = result[0][1] as Record; + + expect(typeof browserInfo.color_depth).toBe('number'); + }); + + it('includes screen dimensions', () => { + const result = BrowserSpec.broswerInfo(); + const browserInfo = result[0][1] as Record; + + expect(typeof browserInfo.screen_height).toBe('number'); + expect(typeof browserInfo.screen_width).toBe('number'); + }); + + it('includes java_enabled as true', () => { + const result = BrowserSpec.broswerInfo(); + const browserInfo = result[0][1] as Record; + + expect(browserInfo.java_enabled).toBe(true); + }); + + it('includes java_script_enabled as true', () => { + const result = BrowserSpec.broswerInfo(); + const browserInfo = result[0][1] as Record; + + expect(browserInfo.java_script_enabled).toBe(true); + }); + + it('includes time_zone offset', () => { + const result = BrowserSpec.broswerInfo(); + const browserInfo = result[0][1] as Record; + + expect(typeof browserInfo.time_zone).toBe('number'); + }); + + it('includes device_model', () => { + const result = BrowserSpec.broswerInfo(); + const browserInfo = result[0][1] as Record; + + expect(browserInfo).toHaveProperty('device_model'); + }); + + it('includes os_type', () => { + const result = BrowserSpec.broswerInfo(); + const browserInfo = result[0][1] as Record; + + expect(browserInfo).toHaveProperty('os_type'); + }); + + it('includes os_version', () => { + const result = BrowserSpec.broswerInfo(); + const browserInfo = result[0][1] as Record; + + expect(browserInfo).toHaveProperty('os_version'); + }); + }); +}); diff --git a/src/__tests__/CardThemeType.test.ts b/src/__tests__/CardThemeType.test.ts new file mode 100644 index 000000000..18466b820 --- /dev/null +++ b/src/__tests__/CardThemeType.test.ts @@ -0,0 +1,200 @@ +import { + getPaymentMode, + getPaymentModeToString, + getPaymentModeToStrMapper, +} from '../Types/CardThemeType.bs.js'; + +describe('CardThemeType', () => { + describe('getPaymentMode', () => { + describe('card related modes', () => { + it('should return Card for "card"', () => { + expect(getPaymentMode('card')).toBe('Card'); + }); + + it('should return CardNumberElement for "cardNumber"', () => { + expect(getPaymentMode('cardNumber')).toBe('CardNumberElement'); + }); + + it('should return CardExpiryElement for "cardExpiry"', () => { + expect(getPaymentMode('cardExpiry')).toBe('CardExpiryElement'); + }); + + it('should return CardCVCElement for "cardCvc"', () => { + expect(getPaymentMode('cardCvc')).toBe('CardCVCElement'); + }); + }); + + describe('wallet modes', () => { + it('should return GooglePayElement for "googlePay"', () => { + expect(getPaymentMode('googlePay')).toBe('GooglePayElement'); + }); + + it('should return ApplePayElement for "applePay"', () => { + expect(getPaymentMode('applePay')).toBe('ApplePayElement'); + }); + + it('should return SamsungPayElement for "samsungPay"', () => { + expect(getPaymentMode('samsungPay')).toBe('SamsungPayElement'); + }); + + it('should return PayPalElement for "payPal"', () => { + expect(getPaymentMode('payPal')).toBe('PayPalElement'); + }); + + it('should return KlarnaElement for "klarna"', () => { + expect(getPaymentMode('klarna')).toBe('KlarnaElement'); + }); + + it('should return PazeElement for "paze"', () => { + expect(getPaymentMode('paze')).toBe('PazeElement'); + }); + + it('should return ExpressCheckoutElement for "expressCheckout"', () => { + expect(getPaymentMode('expressCheckout')).toBe('ExpressCheckoutElement'); + }); + }); + + describe('other modes', () => { + it('should return Payment for "payment"', () => { + expect(getPaymentMode('payment')).toBe('Payment'); + }); + + it('should return PaymentMethodCollectElement for "paymentMethodCollect"', () => { + expect(getPaymentMode('paymentMethodCollect')).toBe('PaymentMethodCollectElement'); + }); + + it('should return PaymentMethodsManagement for "paymentMethodsManagement"', () => { + expect(getPaymentMode('paymentMethodsManagement')).toBe('PaymentMethodsManagement'); + }); + }); + + describe('default fallback', () => { + it('should return NONE for unknown string', () => { + expect(getPaymentMode('unknown')).toBe('NONE'); + }); + + it('should return NONE for empty string', () => { + expect(getPaymentMode('')).toBe('NONE'); + }); + + it('should return NONE for random string', () => { + expect(getPaymentMode('xyz123')).toBe('NONE'); + }); + }); + }); + + describe('getPaymentModeToString', () => { + describe('card related modes', () => { + it('should return "card" for Card', () => { + expect(getPaymentModeToString('Card')).toBe('card'); + }); + + it('should return "cardNumber" for CardNumberElement', () => { + expect(getPaymentModeToString('CardNumberElement')).toBe('cardNumber'); + }); + + it('should return "cardExpiry" for CardExpiryElement', () => { + expect(getPaymentModeToString('CardExpiryElement')).toBe('cardExpiry'); + }); + + it('should return "cardCvc" for CardCVCElement', () => { + expect(getPaymentModeToString('CardCVCElement')).toBe('cardCvc'); + }); + }); + + describe('wallet modes', () => { + it('should return "googlePay" for GooglePayElement', () => { + expect(getPaymentModeToString('GooglePayElement')).toBe('googlePay'); + }); + + it('should return "applePay" for ApplePayElement', () => { + expect(getPaymentModeToString('ApplePayElement')).toBe('applePay'); + }); + + it('should return "samsungPay" for SamsungPayElement', () => { + expect(getPaymentModeToString('SamsungPayElement')).toBe('samsungPay'); + }); + + it('should return "payPal" for PayPalElement', () => { + expect(getPaymentModeToString('PayPalElement')).toBe('payPal'); + }); + + it('should return "klarna" for KlarnaElement', () => { + expect(getPaymentModeToString('KlarnaElement')).toBe('klarna'); + }); + + it('should return "paze" for PazeElement', () => { + expect(getPaymentModeToString('PazeElement')).toBe('paze'); + }); + + it('should return "expressCheckout" for ExpressCheckoutElement', () => { + expect(getPaymentModeToString('ExpressCheckoutElement')).toBe('expressCheckout'); + }); + }); + + describe('other modes', () => { + it('should return "payment" for Payment', () => { + expect(getPaymentModeToString('Payment')).toBe('payment'); + }); + + it('should return "paymentMethodCollect" for PaymentMethodCollectElement', () => { + expect(getPaymentModeToString('PaymentMethodCollectElement')).toBe('paymentMethodCollect'); + }); + + it('should return "paymentMethodsManagement" for PaymentMethodsManagement', () => { + expect(getPaymentModeToString('PaymentMethodsManagement')).toBe('paymentMethodsManagement'); + }); + + it('should return "none" for NONE', () => { + expect(getPaymentModeToString('NONE')).toBe('none'); + }); + }); + }); + + describe('getPaymentModeToStrMapper', () => { + it('should return the input string unchanged', () => { + expect(getPaymentModeToStrMapper('card')).toBe('card'); + }); + + it('should return any string unchanged', () => { + expect(getPaymentModeToStrMapper('googlePay')).toBe('googlePay'); + }); + + it('should return empty string unchanged', () => { + expect(getPaymentModeToStrMapper('')).toBe(''); + }); + + it('should return any value passed through', () => { + expect(getPaymentModeToStrMapper('anyRandomString')).toBe('anyRandomString'); + }); + }); + + describe('round-trip conversion', () => { + it('should round-trip card modes correctly', () => { + const modes = ['card', 'cardNumber', 'cardExpiry', 'cardCvc']; + modes.forEach((mode) => { + const element = getPaymentMode(mode); + const result = getPaymentModeToString(element); + expect(result).toBe(mode); + }); + }); + + it('should round-trip wallet modes correctly', () => { + const modes = ['googlePay', 'applePay', 'samsungPay', 'payPal', 'klarna', 'paze', 'expressCheckout']; + modes.forEach((mode) => { + const element = getPaymentMode(mode); + const result = getPaymentModeToString(element); + expect(result).toBe(mode); + }); + }); + + it('should round-trip other modes correctly', () => { + const modes = ['payment', 'paymentMethodCollect', 'paymentMethodsManagement']; + modes.forEach((mode) => { + const element = getPaymentMode(mode); + const result = getPaymentModeToString(element); + expect(result).toBe(mode); + }); + }); + }); +}); diff --git a/src/__tests__/CardUtils.test.ts b/src/__tests__/CardUtils.test.ts new file mode 100644 index 000000000..69639bf39 --- /dev/null +++ b/src/__tests__/CardUtils.test.ts @@ -0,0 +1,1033 @@ +import { + getCardType, + getCardStringFromType, + calculateLuhn, + formatCardNumber, + getExpiryValidity, + cardValid, + maxCardLength, + isCardLengthValid, + cvcNumberInRange, + checkCardCVC, + checkCardExpiry, + getCardBin, + getCardLast4, + checkIfCardBinIsBlocked, + pincodeVisibility, + getCardBrand, + getExpiryDates, + isExipryValid, + cardNumberInRange, + getMaxLength, + toString, + getQueryParamsDictforKey, + getCurrentMonthAndYear, + getExpiryYearPrefix, + formatExpiryToTwoDigit, + isExpiryComplete, + max, + getBoolOptionVal, + commonKeyDownEvent, + swapCardOption, + setCardValid, + setExpiryValid, + getLayoutClass, + getAllBanknames, + getFirstValidCardSchemeFromPML, + getEligibleCoBadgedCardSchemes, + getCardBrandFromStates, + getCardBrandInvalidError, + emitExpiryDate, + emitIsFormReadyForSubmission, + focusCardValid, + useDefaultCardProps, + useDefaultExpiryProps, + useDefaultCvcProps, + useDefaultZipProps, + useCardDetails, +} from '../CardUtils.bs.js'; +import { renderHook, act } from '@testing-library/react'; + +describe('CardUtils', () => { + describe('getCardType', () => { + it('should return VISA for Visa type', () => { + expect(getCardType('Visa')).toBe('VISA'); + }); + + it('should return MASTERCARD for Mastercard type', () => { + expect(getCardType('Mastercard')).toBe('MASTERCARD'); + }); + + it('should return AMEX for AmericanExpress type', () => { + expect(getCardType('AmericanExpress')).toBe('AMEX'); + }); + + it('should return NOTFOUND for unknown type', () => { + expect(getCardType('UnknownCard')).toBe('NOTFOUND'); + }); + }); + + describe('getCardStringFromType', () => { + it('should return Visa for VISA type', () => { + expect(getCardStringFromType('VISA')).toBe('Visa'); + }); + + it('should return Mastercard for MASTERCARD type', () => { + expect(getCardStringFromType('MASTERCARD')).toBe('Mastercard'); + }); + + it('should return AmericanExpress for AMEX type', () => { + expect(getCardStringFromType('AMEX')).toBe('AmericanExpress'); + }); + + it('should return NOTFOUND for unknown type', () => { + expect(getCardStringFromType('NOTFOUND')).toBe('NOTFOUND'); + }); + }); + + describe('calculateLuhn', () => { + it('should return true for valid Visa card number', () => { + expect(calculateLuhn('4111111111111111')).toBe(true); + }); + + it('should return true for valid Mastercard number', () => { + expect(calculateLuhn('5555555555554444')).toBe(true); + }); + + it('should return false for invalid card number', () => { + expect(calculateLuhn('4111111111111112')).toBe(false); + }); + + it('should return true for empty string', () => { + expect(calculateLuhn('')).toBe(true); + }); + + it('should handle card numbers with spaces', () => { + expect(calculateLuhn('4111 1111 1111 1111')).toBe(true); + }); + + // Edge case: single digit "0" — sum is 0, 0 % 10 === 0 + it('should return true for single digit 0', () => { + expect(calculateLuhn('0')).toBe(true); + }); + + // Edge case: single digit "5" + it('should handle single digit 5', () => { + // Single digit: uncheck=[5], check=[], sum=5, 5%10!==0 → false + expect(calculateLuhn('5')).toBe(false); + }); + + // Edge case: all-zeros 16-digit string + it('should return true for all-zeros card number', () => { + expect(calculateLuhn('0000000000000000')).toBe(true); + }); + }); + + describe('formatCardNumber', () => { + it('should format Visa card number with spaces (4-4-4-4)', () => { + const result = formatCardNumber('4111111111111111', 'VISA'); + expect(result).toBe('4111 1111 1111 1111'); + }); + + it('should format Amex card number (4-6-5)', () => { + const result = formatCardNumber('378282246310005', 'AMEX'); + expect(result).toBe('3782 822463 10005'); + }); + + it('should handle empty string', () => { + const result = formatCardNumber('', 'VISA'); + expect(result).toBe(''); + }); + + it('should handle short numbers', () => { + const result = formatCardNumber('4111', 'VISA'); + expect(result).toBe('4111'); + }); + }); + + describe('getExpiryValidity', () => { + it('should return true for future expiry date', () => { + const futureYear = new Date().getFullYear() + 1; + const expiry = `12/${futureYear.toString().slice(-2)}`; + expect(getExpiryValidity(expiry)).toBe(true); + }); + + it('should return false for past expiry date', () => { + expect(getExpiryValidity('12/20')).toBe(false); + }); + + it('should return false for invalid month', () => { + const futureYear = new Date().getFullYear() + 1; + const expiry = `13/${futureYear.toString().slice(-2)}`; + expect(getExpiryValidity(expiry)).toBe(false); + }); + }); + + describe('cardValid', () => { + it('should return true for valid Visa card', () => { + expect(cardValid('4111111111111111', 'Visa')).toBe(true); + }); + + it('should return false for invalid Luhn card', () => { + expect(cardValid('4111111111111112', 'Visa')).toBe(false); + }); + + it('should return false for wrong length', () => { + expect(cardValid('4111111111', 'Visa')).toBe(false); + }); + }); + + describe('maxCardLength', () => { + it('should return 19 for Visa', () => { + expect(maxCardLength('Visa')).toBe(19); + }); + + it('should return 15 for Amex', () => { + expect(maxCardLength('AmericanExpress')).toBe(15); + }); + + it('should return 16 for Mastercard', () => { + expect(maxCardLength('Mastercard')).toBe(16); + }); + }); + + describe('isCardLengthValid', () => { + it('should return true for valid Visa length (16)', () => { + expect(isCardLengthValid('Visa', 16)).toBe(true); + }); + + it('should return true for Visa length 15 (within range)', () => { + expect(isCardLengthValid('Visa', 15)).toBe(true); + }); + + it('should return true for valid Amex length (15)', () => { + expect(isCardLengthValid('AmericanExpress', 15)).toBe(true); + }); + }); + + describe('cvcNumberInRange', () => { + it('should return array with true for valid Visa CVC (3 digits)', () => { + const result = cvcNumberInRange('123', 'Visa'); + expect(result).toContain(true); + }); + + it('should return array with true for valid Amex CVC (4 digits)', () => { + const result = cvcNumberInRange('1234', 'AmericanExpress'); + expect(result).toContain(true); + }); + + it('should return array without true for invalid CVC length', () => { + const result = cvcNumberInRange('12', 'Visa'); + expect(result).not.toContain(true); + }); + }); + + describe('checkCardCVC', () => { + it('should return true for valid CVC', () => { + expect(checkCardCVC('123', 'Visa')).toBe(true); + }); + + it('should return false for empty CVC', () => { + expect(checkCardCVC('', 'Visa')).toBe(false); + }); + + it('should return false for invalid CVC length', () => { + expect(checkCardCVC('12', 'Visa')).toBe(false); + }); + }); + + describe('checkCardExpiry', () => { + it('should return true for valid future expiry', () => { + const futureYear = new Date().getFullYear() + 1; + const expiry = `12/${futureYear.toString().slice(-2)}`; + expect(checkCardExpiry(expiry)).toBe(true); + }); + + it('should return false for empty expiry', () => { + expect(checkCardExpiry('')).toBe(false); + }); + + it('should return false for past expiry', () => { + expect(checkCardExpiry('12/20')).toBe(false); + }); + }); + + describe('getCardBin', () => { + it('should return first 6 digits for valid card number', () => { + expect(getCardBin('4111111111111111')).toBe('411111'); + }); + + it('should handle spaces in card number', () => { + expect(getCardBin('4111 1111 1111 1111')).toBe('411111'); + }); + + it('should return available digits for short number', () => { + expect(getCardBin('4111')).toBe('4111'); + }); + + // Edge case: empty string input + it('should return empty string for empty input', () => { + expect(getCardBin('')).toBe(''); + }); + }); + + describe('getCardLast4', () => { + it('should return last 4 digits for valid card number', () => { + expect(getCardLast4('4111111111111111')).toBe('1111'); + }); + + it('should handle spaces in card number', () => { + expect(getCardLast4('4111 1111 1111 1234')).toBe('1234'); + }); + + it('should return available digits for short number', () => { + expect(getCardLast4('123')).toBe('123'); + }); + }); + + describe('checkIfCardBinIsBlocked', () => { + it('should return false when blockedBinsList is not loaded', () => { + expect(checkIfCardBinIsBlocked('411111', { TAG: 'Loading' })).toBe(false); + }); + + it('should throw for null blockedBinsList', () => { + expect(() => checkIfCardBinIsBlocked('411111', null)).toThrow(); + }); + + it('should return false for empty blocked list', () => { + expect(checkIfCardBinIsBlocked('411111', { TAG: 'Loaded', _0: [] })).toBe(false); + }); + }); + + describe('pincodeVisibility', () => { + it('should return boolean for known card brand', () => { + const result = pincodeVisibility('Visa'); + expect(typeof result).toBe('boolean'); + }); + + it('should return default for unknown card brand', () => { + const result = pincodeVisibility('UnknownBrand'); + expect(typeof result).toBe('boolean'); + }); + }); + + describe('getCardBrand', () => { + it('should return Visa for Visa card number', () => { + expect(getCardBrand('4111111111111111')).toBe('Visa'); + }); + + it('should return Mastercard for Mastercard number', () => { + expect(getCardBrand('5555555555554444')).toBe('Mastercard'); + }); + + it('should return empty string for invalid number', () => { + expect(getCardBrand('123')).toBe(''); + }); + + it('should handle numbers with spaces', () => { + expect(getCardBrand('4111 1111 1111 1111')).toBe('Visa'); + }); + + // Edge case: empty string input — caught by try/catch, returns "" + it('should return empty string for empty input', () => { + expect(getCardBrand('')).toBe(''); + }); + }); + + describe('getExpiryDates', () => { + it('should parse valid expiry date', () => { + const result = getExpiryDates('12/25'); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBe(2); + }); + + it('should return month and year', () => { + const result = getExpiryDates('12/25'); + expect(result[0]).toBe('12'); + }); + }); + + describe('isExipryValid', () => { + it('should return true for valid future expiry', () => { + const futureYear = new Date().getFullYear() + 1; + const expiry = `12/${futureYear.toString().slice(-2)}`; + expect(isExipryValid(expiry)).toBe(true); + }); + + it('should return false for empty expiry', () => { + expect(isExipryValid('')).toBe(false); + }); + + it('should return false for past expiry', () => { + expect(isExipryValid('12/20')).toBe(false); + }); + }); + + describe('cardNumberInRange', () => { + it('should return array of booleans for card number length check', () => { + const result = cardNumberInRange('4111111111111111', 'Visa'); + expect(Array.isArray(result)).toBe(true); + }); + + it('should handle empty card number', () => { + const result = cardNumberInRange('', 'Visa'); + expect(Array.isArray(result)).toBe(true); + }); + }); + + describe('getMaxLength', () => { + it('should return formatted max length for Visa', () => { + const result = getMaxLength('Visa'); + expect(result).toBeGreaterThan(0); + }); + + it('should return formatted max length for Amex', () => { + const result = getMaxLength('AmericanExpress'); + expect(result).toBeGreaterThan(0); + }); + }); + + describe('toString', () => { + it('should convert number to string', () => { + expect(toString(123)).toBe('123'); + }); + + it('should handle zero', () => { + expect(toString(0)).toBe('0'); + }); + + it('should handle negative numbers', () => { + expect(toString(-456)).toBe('-456'); + }); + }); + + describe('getQueryParamsDictforKey', () => { + it('should return value for existing key', () => { + const result = getQueryParamsDictforKey('key1=value1&key2=value2', 'key1'); + expect(result).toBe('value1'); + }); + + it('should return empty string for missing key', () => { + const result = getQueryParamsDictforKey('key1=value1', 'missing'); + expect(result).toBe(''); + }); + + it('should handle empty query string', () => { + const result = getQueryParamsDictforKey('', 'key'); + expect(result).toBe(''); + }); + + it('should handle param without equals sign', () => { + const result = getQueryParamsDictforKey('invalidparam', 'key'); + expect(result).toBe(''); + }); + + it('should handle multiple equals signs', () => { + const result = getQueryParamsDictforKey('url=https://example.com', 'url'); + expect(result).toBe('https://example.com'); + }); + }); + + describe('getCurrentMonthAndYear', () => { + it('should return array with month and year', () => { + const result = getCurrentMonthAndYear('2025-06-15T10:30:00Z'); + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBe(2); + }); + + it('should return correct month', () => { + const result = getCurrentMonthAndYear('2025-06-15T10:30:00Z'); + expect(result[0]).toBe(6); + }); + + it('should return correct year', () => { + const result = getCurrentMonthAndYear('2025-06-15T10:30:00Z'); + expect(result[1]).toBe(2025); + }); + + it('should handle January', () => { + const result = getCurrentMonthAndYear('2025-01-15T10:30:00Z'); + expect(result[0]).toBe(1); + }); + + it('should handle December', () => { + const result = getCurrentMonthAndYear('2025-12-15T10:30:00Z'); + expect(result[0]).toBe(12); + }); + }); + + describe('getExpiryYearPrefix', () => { + it('should return 2-digit year prefix', () => { + const result = getExpiryYearPrefix(); + expect(result.length).toBe(2); + expect(typeof result).toBe('string'); + }); + + it('should return current decade prefix', () => { + const currentYear = new Date().getFullYear(); + const expected = currentYear.toString().slice(0, 2); + expect(getExpiryYearPrefix()).toBe(expected); + }); + }); + + describe('formatExpiryToTwoDigit', () => { + it('should return 2-digit expiry as-is', () => { + expect(formatExpiryToTwoDigit('25')).toBe('25'); + }); + + it('should extract last 2 digits from 4-digit string', () => { + expect(formatExpiryToTwoDigit('2025')).toBe('25'); + }); + + it('should handle 3-digit string', () => { + expect(formatExpiryToTwoDigit('025')).toBe('5'); + }); + + it('should handle single digit', () => { + expect(formatExpiryToTwoDigit('5')).toBe(''); + }); + }); + + describe('isExpiryComplete', () => { + it('should return true for complete expiry (MM/YY)', () => { + expect(isExpiryComplete('12/25')).toBe(true); + }); + + it('should return false for incomplete month', () => { + expect(isExpiryComplete('1/25')).toBe(false); + }); + + it('should return false for incomplete year', () => { + expect(isExpiryComplete('12/2')).toBe(false); + }); + + it('should return false for empty expiry', () => { + expect(isExpiryComplete('')).toBe(false); + }); + + it('should return false for partial expiry', () => { + expect(isExpiryComplete('12')).toBe(false); + }); + }); + + describe('max', () => { + it('should return larger number', () => { + expect(max(5, 10)).toBe(10); + }); + + it('should return first number when larger', () => { + expect(max(20, 10)).toBe(20); + }); + + it('should handle equal numbers', () => { + expect(max(5, 5)).toBe(5); + }); + + it('should handle negative numbers', () => { + expect(max(-5, -10)).toBe(-5); + }); + + it('should handle zero', () => { + expect(max(0, -1)).toBe(0); + }); + }); + + describe('getBoolOptionVal', () => { + it('should return "valid" for true', () => { + expect(getBoolOptionVal(true)).toBe('valid'); + }); + + it('should return "invalid" for false', () => { + expect(getBoolOptionVal(false)).toBe('invalid'); + }); + + it('should return empty string for undefined', () => { + expect(getBoolOptionVal(undefined)).toBe(''); + }); + }); + + describe('commonKeyDownEvent', () => { + it('should not modify state for non-backspace key', () => { + const mockEv = { keyCode: 65, preventDefault: jest.fn() } as any; + const mockSetEle = jest.fn(); + const mockSrcRef = { current: null }; + const mockDestRef = { current: null }; + + commonKeyDownEvent(mockEv, mockSrcRef, mockDestRef, 'test', 'dest', mockSetEle); + expect(mockSetEle).not.toHaveBeenCalled(); + }); + + it('should not modify state when srcEle is not empty', () => { + const mockEv = { keyCode: 8, preventDefault: jest.fn() } as any; + const mockSetEle = jest.fn(); + const mockSrcRef = { current: null }; + const mockDestRef = { current: null }; + + commonKeyDownEvent(mockEv, mockSrcRef, mockDestRef, 'nonempty', 'dest', mockSetEle); + expect(mockSetEle).not.toHaveBeenCalled(); + }); + }); + + describe('swapCardOption', () => { + it('should swap selected option into card options', () => { + const cardOpts = ['visa', 'mastercard']; + const dropOpts = ['amex']; + const result = swapCardOption([...cardOpts], [...dropOpts], 'discover'); + + expect(result[0]).toContain('discover'); + expect(result[1]).not.toContain('discover'); + }); + + it('should remove selected from dropdown options', () => { + const cardOpts = ['visa']; + const dropOpts = ['mastercard', 'amex']; + const result = swapCardOption([...cardOpts], [...dropOpts], 'mastercard'); + + expect(result[1]).not.toContain('mastercard'); + }); + }); + + describe('setCardValid', () => { + it('should call setIsCardValid with true for valid card', () => { + const mockSetIsCardValid = jest.fn(); + setCardValid('4111111111111111', 'Visa', mockSetIsCardValid); + expect(mockSetIsCardValid).toHaveBeenCalled(); + }); + + it('should call setIsCardValid for invalid max length card', () => { + const mockSetIsCardValid = jest.fn(); + setCardValid('4111111111111112', 'Visa', mockSetIsCardValid); + expect(mockSetIsCardValid).toHaveBeenCalled(); + }); + + it('should handle short card number', () => { + const mockSetIsCardValid = jest.fn(); + setCardValid('4111', 'Visa', mockSetIsCardValid); + expect(mockSetIsCardValid).toHaveBeenCalled(); + }); + }); + + describe('setExpiryValid', () => { + it('should call setIsExpiryValid for valid expiry', () => { + const futureYear = new Date().getFullYear() + 1; + const expiry = `12/${futureYear.toString().slice(-2)}`; + const mockSetIsExpiryValid = jest.fn(); + setExpiryValid(expiry, mockSetIsExpiryValid); + expect(mockSetIsExpiryValid).toHaveBeenCalled(); + }); + + it('should call setIsExpiryValid for incomplete expiry', () => { + const mockSetIsExpiryValid = jest.fn(); + setExpiryValid('12', mockSetIsExpiryValid); + expect(mockSetIsExpiryValid).toHaveBeenCalled(); + }); + + it('should call setIsExpiryValid for invalid expiry', () => { + const mockSetIsExpiryValid = jest.fn(); + setExpiryValid('13/25', mockSetIsExpiryValid); + expect(mockSetIsExpiryValid).toHaveBeenCalled(); + }); + }); + + describe('getLayoutClass', () => { + it('should handle StringLayout TAG', () => { + const layout = { TAG: 'StringLayout', _0: 'accordion' }; + const result = getLayoutClass(layout); + expect(result.type).toBe('accordion'); + }); + + it('should handle non-StringLayout TAG', () => { + const layoutObj = { + type: 'tabs', + defaultCollapsed: false, + radios: false, + spacedAccordionItems: false, + maxAccordionItems: 5, + savedMethodCustomization: {}, + paymentMethodsArrangementForTabs: [], + displayOneClickPaymentMethodsOnTop: false + }; + const layout = { TAG: 'ObjectLayout', _0: layoutObj }; + const result = getLayoutClass(layout); + expect(result).toBe(layoutObj); + }); + }); + + describe('getAllBanknames', () => { + it('should flatten nested arrays of bank names', () => { + const banks = [['bank1', 'bank2'], ['bank3']]; + const result = getAllBanknames(banks); + expect(result).toEqual(['bank1', 'bank2', 'bank3']); + }); + + it('should return empty array for empty input', () => { + const result = getAllBanknames([]); + expect(result).toEqual([]); + }); + + it('should handle single array', () => { + const result = getAllBanknames([['bank1']]); + expect(result).toEqual(['bank1']); + }); + }); + + describe('getFirstValidCardSchemeFromPML', () => { + it('should return undefined for invalid card number', () => { + const result = getFirstValidCardSchemeFromPML('123', ['visa']); + expect(result).toBeUndefined(); + }); + + it('should return matching scheme for valid card', () => { + const result = getFirstValidCardSchemeFromPML('4111111111111111', ['visa']); + expect(result).toBeDefined(); + }); + + it('should return undefined when no matching scheme enabled', () => { + const result = getFirstValidCardSchemeFromPML('4111111111111111', ['mastercard']); + expect(result).toBeUndefined(); + }); + }); + + describe('getEligibleCoBadgedCardSchemes', () => { + it('should filter matched schemes by enabled list', () => { + const matched = ['visa', 'mastercard', 'amex']; + const enabled = ['visa', 'mastercard']; + const result = getEligibleCoBadgedCardSchemes(matched, enabled); + expect(result).toEqual(['visa', 'mastercard']); + }); + + it('should return empty array when no matches', () => { + const matched = ['visa']; + const enabled = ['mastercard']; + const result = getEligibleCoBadgedCardSchemes(matched, enabled); + expect(result).toEqual([]); + }); + + it('should filter with lowercase matching', () => { + const matched = ['Visa', 'Mastercard']; + const enabled = ['visa']; + const result = getEligibleCoBadgedCardSchemes(matched, enabled); + expect(result).toEqual(['Visa']); + }); + }); + + describe('getCardBrandFromStates', () => { + it('should return cardBrand when not showing payment methods screen', () => { + const result = getCardBrandFromStates('Visa', 'Mastercard', false); + expect(result).toBe('Mastercard'); + }); + + it('should return cardScheme when showing payment methods screen', () => { + const result = getCardBrandFromStates('Visa', 'Mastercard', true); + expect(result).toBe('Visa'); + }); + }); + + describe('getCardBrandInvalidError', () => { + it('should return enterValidCardNumberErrorText for empty brand', () => { + const localeString = { + enterValidCardNumberErrorText: 'Enter a valid card number', + cardBrandConfiguredErrorText: (brand: string) => `${brand} not configured` + }; + const result = getCardBrandInvalidError('', localeString); + expect(result).toBe('Enter a valid card number'); + }); + + it('should return cardBrandConfiguredErrorText for non-empty brand', () => { + const localeString = { + enterValidCardNumberErrorText: 'Enter a valid card number', + cardBrandConfiguredErrorText: (brand: string) => `${brand} not configured` + }; + const result = getCardBrandInvalidError('Visa', localeString); + expect(result).toBe('Visa not configured'); + }); + }); + + describe('emitExpiryDate', () => { + it('should call messageParentWindow with expiry date', () => { + const mockMessageParentWindow = jest.fn(); + jest.mock('../Utilities/Utils.bs.js', () => ({ + messageParentWindow: mockMessageParentWindow, + })); + emitExpiryDate('12/25'); + }); + }); + + describe('emitIsFormReadyForSubmission', () => { + it('should call messageParentWindow with ready status', () => { + emitIsFormReadyForSubmission(true); + }); + }); + + describe('focusCardValid', () => { + it('should return true for valid card at max length', () => { + expect(focusCardValid('4111111111111111', 'Visa')).toBe(true); + }); + + it('should return false for short card number', () => { + expect(focusCardValid('4111', 'Visa')).toBe(false); + }); + + it('should return false for empty brand with short card', () => { + expect(focusCardValid('4111', '')).toBe(false); + }); + + it('should handle invalid Luhn', () => { + expect(focusCardValid('4111111111111112', 'Visa')).toBe(false); + }); + + it('should handle Visa 16 digit special case', () => { + expect(focusCardValid('4111111111111111', 'Visa')).toBe(true); + }); + }); + + describe('checkIfCardBinIsBlocked - additional tests', () => { + it('should return false for short card number', () => { + expect(checkIfCardBinIsBlocked('41', { TAG: 'Loaded', _0: [] })).toBe(false); + }); + + it('should return false for card number with less than 6 digits', () => { + expect(checkIfCardBinIsBlocked('41111', { TAG: 'Loaded', _0: [] })).toBe(false); + }); + + it('should return true when bin is in blocked list', () => { + const blockedBins = [{ fingerprint_id: '411111' }]; + expect(checkIfCardBinIsBlocked('4111111111111111', { TAG: 'Loaded', _0: blockedBins })).toBe(true); + }); + + it('should return false when bin is not in blocked list', () => { + const blockedBins = [{ fingerprint_id: '411112' }]; + expect(checkIfCardBinIsBlocked('4111111111111111', { TAG: 'Loaded', _0: blockedBins })).toBe(false); + }); + + it('should handle non-object blockedBinsList', () => { + expect(checkIfCardBinIsBlocked('4111111111111111', 'invalid' as any)).toBe(false); + }); + }); + + describe('getCardType - additional tests', () => { + it('should return BAJAJ for BAJAJ type', () => { + expect(getCardType('BAJAJ')).toBe('BAJAJ'); + }); + + it('should return CARTESBANCAIRES for CartesBancaires', () => { + expect(getCardType('CartesBancaires')).toBe('CARTESBANCAIRES'); + }); + + it('should return DINERSCLUB for DinersClub', () => { + expect(getCardType('DinersClub')).toBe('DINERSCLUB'); + }); + + it('should return DISCOVER for Discover', () => { + expect(getCardType('Discover')).toBe('DISCOVER'); + }); + + it('should return INTERAC for Interac', () => { + expect(getCardType('Interac')).toBe('INTERAC'); + }); + + it('should return JCB for JCB', () => { + expect(getCardType('JCB')).toBe('JCB'); + }); + + it('should return MAESTRO for Maestro', () => { + expect(getCardType('Maestro')).toBe('MAESTRO'); + }); + + it('should return RUPAY for RuPay', () => { + expect(getCardType('RuPay')).toBe('RUPAY'); + }); + + it('should return SODEXO for SODEXO', () => { + expect(getCardType('SODEXO')).toBe('SODEXO'); + }); + + it('should return UNIONPAY for UnionPay', () => { + expect(getCardType('UnionPay')).toBe('UNIONPAY'); + }); + }); + + describe('getCardStringFromType - additional tests', () => { + it('should return BAJAJ for BAJAJ type', () => { + expect(getCardStringFromType('BAJAJ')).toBe('BAJAJ'); + }); + + it('should return SODEXO for SODEXO type', () => { + expect(getCardStringFromType('SODEXO')).toBe('SODEXO'); + }); + + it('should return RuPay for RUPAY type', () => { + expect(getCardStringFromType('RUPAY')).toBe('RuPay'); + }); + + it('should return JCB for JCB type', () => { + expect(getCardStringFromType('JCB')).toBe('JCB'); + }); + + it('should return CartesBancaires for CARTESBANCAIRES type', () => { + expect(getCardStringFromType('CARTESBANCAIRES')).toBe('CartesBancaires'); + }); + + it('should return UnionPay for UNIONPAY type', () => { + expect(getCardStringFromType('UNIONPAY')).toBe('UnionPay'); + }); + + it('should return Interac for INTERAC type', () => { + expect(getCardStringFromType('INTERAC')).toBe('Interac'); + }); + }); + + describe('formatCardNumber - additional tests', () => { + it('should format MAESTRO card number', () => { + const result = formatCardNumber('6759649826438453', 'MAESTRO'); + expect(result).toBe('6759 6498 2643 8453'); + }); + + it('should format JCB card number', () => { + const result = formatCardNumber('3530111333300000', 'JCB'); + expect(result).toBe('3530 1113 3330 0000'); + }); + + it('should format DINERSCLUB card number', () => { + const result = formatCardNumber('36070500001020', 'DINERSCLUB'); + expect(result).toBe('3607 0500 0010 20'); + }); + + it('should format DISCOVER card number', () => { + const result = formatCardNumber('6011111111111117', 'DISCOVER'); + expect(result).toBe('6011 1111 1111 1117'); + }); + + it('should format BAJAJ card number', () => { + const result = formatCardNumber('1234567890123456', 'BAJAJ'); + expect(result).toContain('1234'); + }); + + it('should format NOTFOUND type', () => { + const result = formatCardNumber('1234567890123456', 'NOTFOUND'); + expect(result).toContain('1234'); + }); + + it('should format INTERAC card number', () => { + const result = formatCardNumber('4506331111111111', 'INTERAC'); + expect(result).toContain('4506'); + }); + }); + + describe('useDefaultCardProps', () => { + it('should return default card props object', () => { + const { result } = renderHook(() => useDefaultCardProps()); + expect(result.current).toHaveProperty('cardNumber'); + expect(result.current).toHaveProperty('cardBrand'); + expect(result.current).toHaveProperty('cardError'); + expect(result.current).toHaveProperty('maxCardLength'); + expect(result.current).toHaveProperty('cardRef'); + }); + + it('should have empty string for cardNumber', () => { + const { result } = renderHook(() => useDefaultCardProps()); + expect(result.current.cardNumber).toBe(''); + }); + + it('should have empty string for cardBrand', () => { + const { result } = renderHook(() => useDefaultCardProps()); + expect(result.current.cardBrand).toBe(''); + }); + + it('should have zero for maxCardLength', () => { + const { result } = renderHook(() => useDefaultCardProps()); + expect(result.current.maxCardLength).toBe(0); + }); + }); + + describe('useDefaultExpiryProps', () => { + it('should return default expiry props object', () => { + const { result } = renderHook(() => useDefaultExpiryProps()); + expect(result.current).toHaveProperty('cardExpiry'); + expect(result.current).toHaveProperty('expiryError'); + expect(result.current).toHaveProperty('expiryRef'); + }); + + it('should have empty string for cardExpiry', () => { + const { result } = renderHook(() => useDefaultExpiryProps()); + expect(result.current.cardExpiry).toBe(''); + }); + + it('should have empty string for expiryError', () => { + const { result } = renderHook(() => useDefaultExpiryProps()); + expect(result.current.expiryError).toBe(''); + }); + }); + + describe('useDefaultCvcProps', () => { + it('should return default cvc props object', () => { + const { result } = renderHook(() => useDefaultCvcProps()); + expect(result.current).toHaveProperty('cvcNumber'); + expect(result.current).toHaveProperty('cvcError'); + expect(result.current).toHaveProperty('cvcRef'); + }); + + it('should have empty string for cvcNumber', () => { + const { result } = renderHook(() => useDefaultCvcProps()); + expect(result.current.cvcNumber).toBe(''); + }); + + it('should have empty string for cvcError', () => { + const { result } = renderHook(() => useDefaultCvcProps()); + expect(result.current.cvcError).toBe(''); + }); + }); + + describe('useDefaultZipProps', () => { + it('should return default zip props object', () => { + const { result } = renderHook(() => useDefaultZipProps()); + expect(result.current).toHaveProperty('zipCode'); + expect(result.current).toHaveProperty('zipRef'); + expect(result.current).toHaveProperty('displayPincode'); + }); + + it('should have empty string for zipCode', () => { + const { result } = renderHook(() => useDefaultZipProps()); + expect(result.current.zipCode).toBe(''); + }); + + it('should have false for displayPincode', () => { + const { result } = renderHook(() => useDefaultZipProps()); + expect(result.current.displayPincode).toBe(false); + }); + }); + + describe('useCardDetails', () => { + it('should return array of card details state', () => { + const { result } = renderHook(() => useCardDetails('', '', undefined)); + expect(Array.isArray(result.current)).toBe(true); + expect(result.current.length).toBe(3); + }); + + it('should indicate empty when cvcNumber is empty', () => { + const { result } = renderHook(() => useCardDetails('', '', undefined)); + expect(result.current[0]).toBe(true); + }); + + it('should indicate valid when isCvcValidValue is "valid"', () => { + const { result } = renderHook(() => useCardDetails('123', 'valid', true)); + expect(result.current[1]).toBe(true); + }); + + it('should indicate invalid when isCvcValidValue is "invalid"', () => { + const { result } = renderHook(() => useCardDetails('123', 'invalid', false)); + expect(result.current[2]).toBe(true); + }); + }); + + describe('getCardBrand - additional tests', () => { + it('should return RuPay for RuPay card number', () => { + expect(getCardBrand('6073841234567890')).toBe('RuPay'); + }); + + it('should return Mastercard for Mastercard 2-series', () => { + expect(getCardBrand('2221001234567890')).toBe('Mastercard'); + }); + }); +}); diff --git a/src/__tests__/ClickToPayCardEncryption.test.ts b/src/__tests__/ClickToPayCardEncryption.test.ts new file mode 100644 index 000000000..4ea10821d --- /dev/null +++ b/src/__tests__/ClickToPayCardEncryption.test.ts @@ -0,0 +1,101 @@ +import * as ClickToPayCardEncryption from '../Utilities/ClickToPayCardEncryption.bs.js'; + +jest.mock('../Utilities/ClickToPayCardEncryptionHelpers', () => ({ + encryptMessage: jest.fn(), +})); + +import { encryptMessage } from '../Utilities/ClickToPayCardEncryptionHelpers'; + +const mockEncryptMessage = encryptMessage as jest.MockedFunction; + +describe('ClickToPayCardEncryption', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('getEncryptedCard', () => { + it('returns encrypted card data on successful encryption', async () => { + mockEncryptMessage.mockResolvedValue('encrypted-data-123'); + + const cardPayloadJson = JSON.stringify({ + cardNumber: '4111111111111111', + expiryMonth: '12', + expiryYear: '2025', + }); + + const result = await ClickToPayCardEncryption.getEncryptedCard(cardPayloadJson); + + expect(result).toBe('encrypted-data-123'); + expect(mockEncryptMessage).toHaveBeenCalledWith(cardPayloadJson); + }); + + it('returns encrypted data for valid card payload', async () => { + mockEncryptMessage.mockResolvedValue('encrypted-payload-xyz'); + + const cardPayloadJson = JSON.stringify({ + cardNumber: '4242424242424242', + expiryMonth: '01', + expiryYear: '2026', + cvv: '123', + }); + + const result = await ClickToPayCardEncryption.getEncryptedCard(cardPayloadJson); + + expect(result).toBe('encrypted-payload-xyz'); + }); + + it('returns empty string when encryption helper throws an error', async () => { + mockEncryptMessage.mockRejectedValue(new Error('Encryption failed')); + + const cardPayloadJson = JSON.stringify({ + cardNumber: 'invalid', + expiryMonth: '12', + expiryYear: '2025', + }); + + const result = await ClickToPayCardEncryption.getEncryptedCard(cardPayloadJson); + + expect(result).toBe(''); + }); + + it('returns empty string when dynamic import fails', async () => { + mockEncryptMessage.mockRejectedValue(new Error('Import failed')); + + const result = await ClickToPayCardEncryption.getEncryptedCard(null); + + expect(result).toBe(''); + }); + + it('handles empty card payload', async () => { + mockEncryptMessage.mockResolvedValue(''); + + const result = await ClickToPayCardEncryption.getEncryptedCard(''); + + expect(typeof result).toBe('string'); + }); + + it('handles undefined payload gracefully', async () => { + mockEncryptMessage.mockRejectedValue(new Error('Invalid payload')); + + const result = await ClickToPayCardEncryption.getEncryptedCard(undefined as any); + + expect(result).toBe(''); + }); + + it('handles malformed JSON payload', async () => { + mockEncryptMessage.mockRejectedValue(new Error('Invalid JSON')); + + const result = await ClickToPayCardEncryption.getEncryptedCard('not valid json'); + + expect(result).toBe(''); + }); + + it('handles empty object payload', async () => { + mockEncryptMessage.mockResolvedValue('empty-encrypted'); + + const result = await ClickToPayCardEncryption.getEncryptedCard('{}'); + + expect(result).toBe('empty-encrypted'); + }); + }); +}); diff --git a/src/__tests__/ClickToPayHelpers.test.ts b/src/__tests__/ClickToPayHelpers.test.ts new file mode 100644 index 000000000..5b96474d0 --- /dev/null +++ b/src/__tests__/ClickToPayHelpers.test.ts @@ -0,0 +1,1130 @@ +import { + getIdentityType, + clickToPayCardItemToObjMapper, + clickToPayTokenItemToObjMapper, + urlToParamUrlItemToObjMapper, + getStrFromActionCode, + defaultProfile, + defaultCountry, + defaultParamUrl, + formatOrderId, + getVisaInitConfig, + closeWindow, + handleSuccessResponse, + scriptId, + srcUiKitScriptSrc, + srcUiKitCssHref, + recognitionTokenCookieName, + manualCardId, + savedCardId, + getScriptSrc, + orderIdRef, + clickToPayWindowRef, + SrcLoader, + SrcLearnMore, + setLocalStorage, + getLocalStorage, + deleteLocalStorage, + handleCloseClickToPayWindow, + handleOpenClickToPayWindow, + mcCheckoutService, + initializeMastercardCheckout, + getCards, + authenticate, + checkoutWithCard, + encryptCardForClickToPay, + checkoutWithNewCard, + signOut, + getCardsVisaUnified, + signOutVisaUnified, + loadVisaScript, + loadClickToPayUIScripts, + checkoutVisaUnified, + handleCheckoutWithCard, + handleProceedToPay, +} from '../Types/ClickToPayHelpers.bs.js'; + +const createMockLogger = () => ({ + setLogInfo: jest.fn(), + setLogError: jest.fn(), +}); + +describe('ClickToPayHelpers', () => { + describe('getIdentityType', () => { + it('should return "EMAIL_ADDRESS" for "EMAIL_ADDRESS"', () => { + expect(getIdentityType('EMAIL_ADDRESS')).toBe('EMAIL_ADDRESS'); + }); + + it('should return "MOBILE_PHONE_NUMBER" for any other value', () => { + expect(getIdentityType('MOBILE_PHONE_NUMBER')).toBe('MOBILE_PHONE_NUMBER'); + }); + + it('should return "MOBILE_PHONE_NUMBER" for unknown string', () => { + expect(getIdentityType('UNKNOWN')).toBe('MOBILE_PHONE_NUMBER'); + }); + + it('should return "MOBILE_PHONE_NUMBER" for empty string', () => { + expect(getIdentityType('')).toBe('MOBILE_PHONE_NUMBER'); + }); + }); + + describe('clickToPayCardItemToObjMapper', () => { + it('should map card item object to typed object', () => { + const jsonObj = { + srcDigitalCardId: 'card-123', + panLastFour: '4242', + panExpirationMonth: '12', + panExpirationYear: '2025', + paymentCardDescriptor: 'Visa', + digitalCardData: { + descriptorName: 'Visa Gold', + }, + panBin: '424242', + }; + const result = clickToPayCardItemToObjMapper(jsonObj); + expect(result.srcDigitalCardId).toBe('card-123'); + expect(result.panLastFour).toBe('4242'); + expect(result.panExpirationMonth).toBe('12'); + expect(result.panExpirationYear).toBe('2025'); + expect(result.paymentCardDescriptor).toBe('Visa'); + expect(result.digitalCardData.descriptorName).toBe('Visa Gold'); + expect(result.panBin).toBe('424242'); + }); + + it('should handle missing fields with defaults', () => { + const jsonObj = {}; + const result = clickToPayCardItemToObjMapper(jsonObj); + expect(result.srcDigitalCardId).toBe(''); + expect(result.panLastFour).toBe(''); + expect(result.panExpirationMonth).toBe(''); + expect(result.panExpirationYear).toBe(''); + expect(result.paymentCardDescriptor).toBe(''); + expect(result.panBin).toBe(''); + }); + + it('should handle partial card data', () => { + const jsonObj = { + srcDigitalCardId: 'card-456', + panLastFour: '1234', + }; + const result = clickToPayCardItemToObjMapper(jsonObj); + expect(result.srcDigitalCardId).toBe('card-456'); + expect(result.panLastFour).toBe('1234'); + expect(result.panExpirationMonth).toBe(''); + }); + }); + + describe('clickToPayTokenItemToObjMapper', () => { + it('should map token item object to typed object', () => { + const jsonObj = { + dpa_id: 'dpa-123', + dpa_name: 'Test Merchant', + locale: 'en_US', + transaction_amount: 100.5, + transaction_currency_code: 'USD', + acquirer_bin: '123456', + acquirer_merchant_id: 'merchant-123', + merchant_category_code: '5999', + merchant_country_code: 'US', + card_brands: ['VISA', 'MASTERCARD'], + email: 'test@example.com', + provider: 'mastercard', + }; + const result = clickToPayTokenItemToObjMapper(jsonObj); + expect(result.dpaId).toBe('dpa-123'); + expect(result.dpaName).toBe('Test Merchant'); + expect(result.locale).toBe('en_US'); + expect(result.transactionAmount).toBe(100.5); + expect(result.transactionCurrencyCode).toBe('USD'); + expect(result.acquirerBIN).toBe('123456'); + expect(result.acquirerMerchantId).toBe('merchant-123'); + expect(result.merchantCategoryCode).toBe('5999'); + expect(result.merchantCountryCode).toBe('US'); + expect(result.cardBrands).toEqual(['VISA', 'MASTERCARD']); + expect(result.email).toBe('test@example.com'); + expect(result.provider).toBe('mastercard'); + }); + + it('should handle missing fields with defaults', () => { + const jsonObj = {}; + const result = clickToPayTokenItemToObjMapper(jsonObj); + expect(result.dpaId).toBe(''); + expect(result.dpaName).toBe(''); + expect(result.locale).toBe(''); + expect(result.transactionAmount).toBe(0.0); + expect(result.transactionCurrencyCode).toBe(''); + expect(result.provider).toBe('mastercard'); + }); + + it('should handle empty card_brands array', () => { + const jsonObj = { + card_brands: [], + }; + const result = clickToPayTokenItemToObjMapper(jsonObj); + expect(result.cardBrands).toEqual([]); + }); + }); + + describe('urlToParamUrlItemToObjMapper', () => { + it('should parse URL parameters into key-value pairs', () => { + const url = '?key1=value1&key2=value2&key3=value3'; + const result = urlToParamUrlItemToObjMapper(url); + expect(result).toHaveLength(3); + expect(result[0]).toEqual({ key: 'key1', value: 'value1' }); + expect(result[1]).toEqual({ key: 'key2', value: 'value2' }); + expect(result[2]).toEqual({ key: 'key3', value: 'value3' }); + }); + + it('should handle URL without leading question mark', () => { + const url = 'key1=value1&key2=value2'; + const result = urlToParamUrlItemToObjMapper(url); + expect(result).toHaveLength(2); + expect(result[0]).toEqual({ key: 'key1', value: 'value1' }); + }); + + it('should handle empty string', () => { + const result = urlToParamUrlItemToObjMapper(''); + expect(result).toEqual([]); + }); + + it('should handle string with only question mark', () => { + const result = urlToParamUrlItemToObjMapper('?'); + expect(result).toEqual([]); + }); + + it('should handle empty values', () => { + const url = 'key1=&key2=value2'; + const result = urlToParamUrlItemToObjMapper(url); + expect(result).toHaveLength(2); + expect(result[0]).toEqual({ key: 'key1', value: '' }); + expect(result[1]).toEqual({ key: 'key2', value: 'value2' }); + }); + + it('should filter out empty parameters', () => { + const url = '?key1=value1&&key2=value2'; + const result = urlToParamUrlItemToObjMapper(url); + expect(result).toHaveLength(2); + }); + }); + + describe('getStrFromActionCode', () => { + it('should return "SUCCESS" for "SUCCESS"', () => { + expect(getStrFromActionCode('SUCCESS')).toBe('SUCCESS'); + }); + + it('should return "PENDING_CONSUMER_IDV" for "PENDING_CONSUMER_IDV"', () => { + expect(getStrFromActionCode('PENDING_CONSUMER_IDV')).toBe('PENDING_CONSUMER_IDV'); + }); + + it('should return "FAILED" for "FAILED"', () => { + expect(getStrFromActionCode('FAILED')).toBe('FAILED'); + }); + + it('should return "ERROR" for "ERROR"', () => { + expect(getStrFromActionCode('ERROR')).toBe('ERROR'); + }); + + it('should return "ADD_CARD" for "ADD_CARD"', () => { + expect(getStrFromActionCode('ADD_CARD')).toBe('ADD_CARD'); + }); + + it('should return undefined for unknown action code', () => { + expect(getStrFromActionCode('UNKNOWN')).toBeUndefined(); + }); + }); + + describe('defaultProfile', () => { + it('should have empty maskedCards array', () => { + expect(defaultProfile.maskedCards).toEqual([]); + }); + }); + + describe('defaultCountry', () => { + it('should have empty code', () => { + expect(defaultCountry.code).toBe(''); + }); + + it('should have empty countryISO', () => { + expect(defaultCountry.countryISO).toBe(''); + }); + }); + + describe('defaultParamUrl', () => { + it('should have empty key', () => { + expect(defaultParamUrl.key).toBe(''); + }); + + it('should have empty value', () => { + expect(defaultParamUrl.value).toBe(''); + }); + }); + + describe('formatOrderId', () => { + it('should extract order ID from payment secret format', () => { + const orderId = 'pay_abc123_secret_xyz789'; + const result = formatOrderId(orderId); + expect(result).toBe('abc123'); + }); + + it('should handle payment ID without secret suffix', () => { + const orderId = 'pay_test123'; + const result = formatOrderId(orderId); + expect(result).toBe('test123'); + }); + + it('should truncate to 40 characters max', () => { + const longId = 'pay_' + 'a'.repeat(50) + '_secret_xyz'; + const result = formatOrderId(longId); + expect(result.length).toBeLessThanOrEqual(40); + }); + + it('should handle empty string', () => { + const result = formatOrderId(''); + expect(result).toBe(''); + }); + + it('should handle string without pay_ prefix', () => { + const orderId = 'test123_secret_xyz'; + const result = formatOrderId(orderId); + expect(result).toBe('test123'); + }); + }); + + describe('getVisaInitConfig', () => { + it('should build Visa init config from token', () => { + const token = { + locale: 'en_US', + transactionAmount: 100.0, + transactionCurrencyCode: 'USD', + merchantCountryCode: 'US', + merchantCategoryCode: '5999', + acquirerBIN: '123456', + acquirerMerchantId: 'merchant-123', + }; + const clientSecret = 'pay_test123_secret_xyz'; + const result = getVisaInitConfig(token, clientSecret); + expect(result.dpaTransactionOptions.dpaLocale).toBe('en_US'); + expect(result.dpaTransactionOptions.dpaBillingPreference).toBe('NONE'); + expect(result.dpaTransactionOptions.payloadTypeIndicator).toBe('FULL'); + expect(result.dpaTransactionOptions.merchantCountryCode).toBe('US'); + expect(result.dpaTransactionOptions.merchantCategoryCode).toBe('5999'); + expect(result.dpaTransactionOptions.acquirerBIN).toBe('123456'); + expect(result.dpaTransactionOptions.acquirerMerchantId).toBe('merchant-123'); + }); + + it('should handle undefined clientSecret', () => { + const token = { + locale: 'en_US', + transactionAmount: 50.0, + transactionCurrencyCode: 'EUR', + merchantCountryCode: 'DE', + merchantCategoryCode: '5999', + acquirerBIN: '654321', + acquirerMerchantId: 'merchant-456', + }; + const result = getVisaInitConfig(token, undefined); + expect(result.dpaTransactionOptions.merchantOrderId).toBe(''); + }); + + it('should set payloadTypeIndicator to FULL', () => { + const token = { + locale: 'en_US', + transactionAmount: 100, + transactionCurrencyCode: 'USD', + merchantCountryCode: 'US', + merchantCategoryCode: '5999', + acquirerBIN: '123456', + acquirerMerchantId: 'merchant-123', + }; + const result = getVisaInitConfig(token, 'pay_test'); + expect(result.dpaTransactionOptions.payloadTypeIndicator).toBe('FULL'); + }); + + it('should set consumerNationalIdentifierRequested to false', () => { + const token = { + locale: 'en_US', + transactionAmount: 100, + transactionCurrencyCode: 'USD', + merchantCountryCode: 'US', + merchantCategoryCode: '5999', + acquirerBIN: '123456', + acquirerMerchantId: 'merchant-123', + }; + const result = getVisaInitConfig(token, 'pay_test'); + expect(result.dpaTransactionOptions.consumerNationalIdentifierRequested).toBe(false); + }); + }); + + describe('closeWindow', () => { + it('should return object with status and payload', () => { + const payload = { test: 'data' }; + const result = closeWindow('COMPLETE', payload); + expect(result.status).toBe('COMPLETE'); + expect(result.payload).toBe(payload); + }); + + it('should handle ERROR status', () => { + const result = closeWindow('ERROR', null); + expect(result.status).toBe('ERROR'); + expect(result.payload).toBeNull(); + }); + + it('should handle CANCEL status', () => { + const result = closeWindow('CANCEL', { reason: 'user_cancelled' }); + expect(result.status).toBe('CANCEL'); + expect(result.payload).toEqual({ reason: 'user_cancelled' }); + }); + + it('should handle null payload', () => { + const result = closeWindow('COMPLETE', null); + expect(result.status).toBe('COMPLETE'); + expect(result.payload).toBeNull(); + }); + }); + + describe('handleSuccessResponse', () => { + it('should return CANCEL status for checkoutActionCode "CANCEL"', () => { + const response = { checkoutActionCode: 'CANCEL' }; + const result = handleSuccessResponse(response); + expect(result.status).toBe('CANCEL'); + }); + + it('should return COMPLETE status for checkoutActionCode "COMPLETE"', () => { + const response = { checkoutActionCode: 'COMPLETE' }; + const result = handleSuccessResponse(response); + expect(result.status).toBe('COMPLETE'); + }); + + it('should return PAY_V3_CARD status for checkoutActionCode "PAY_V3_CARD"', () => { + const response = { checkoutActionCode: 'PAY_V3_CARD' }; + const result = handleSuccessResponse(response); + expect(result.status).toBe('PAY_V3_CARD'); + }); + + it('should return ERROR status for unknown checkoutActionCode', () => { + const response = { checkoutActionCode: 'UNKNOWN' }; + const result = handleSuccessResponse(response); + expect(result.status).toBe('ERROR'); + }); + + it('should return ERROR status for empty checkoutActionCode', () => { + const response = { checkoutActionCode: '' }; + const result = handleSuccessResponse(response); + expect(result.status).toBe('ERROR'); + }); + }); + + describe('constants', () => { + it('should have correct scriptId', () => { + expect(scriptId).toBe('mastercard-external-script'); + }); + + it('should have correct srcUiKitScriptSrc', () => { + expect(srcUiKitScriptSrc).toBe('https://src.mastercard.com/srci/integration/components/src-ui-kit/src-ui-kit.esm.js'); + }); + + it('should have correct srcUiKitCssHref', () => { + expect(srcUiKitCssHref).toBe('https://src.mastercard.com/srci/integration/components/src-ui-kit/src-ui-kit.css'); + }); + + it('should have correct recognitionTokenCookieName', () => { + expect(recognitionTokenCookieName).toBe('__mastercard_click_to_pay'); + }); + + it('should have correct manualCardId', () => { + expect(manualCardId).toBe('click_to_pay_manual_card'); + }); + + it('should have correct savedCardId prefix', () => { + expect(savedCardId).toBe('click_to_pay_saved_card_'); + }); + + it('should have orderIdRef with empty string contents', () => { + expect(orderIdRef).toHaveProperty('contents'); + expect(orderIdRef.contents).toBe(''); + }); + + it('should have clickToPayWindowRef with null contents', () => { + expect(clickToPayWindowRef).toHaveProperty('contents'); + expect(clickToPayWindowRef.contents).toBeNull(); + }); + + it('should have SrcLoader as an empty object', () => { + expect(SrcLoader).toEqual({}); + }); + + it('should have SrcLearnMore as an empty object', () => { + expect(SrcLearnMore).toEqual({}); + }); + }); + + describe('getScriptSrc', () => { + const originalIsProductionEnv = (globalThis as any).isProductionEnv; + + afterEach(() => { + (globalThis as any).isProductionEnv = originalIsProductionEnv; + }); + + it('should return sandbox URL when isProductionEnv is false', () => { + (globalThis as any).isProductionEnv = false; + expect(getScriptSrc()).toBe('https://sandbox.src.mastercard.com/srci/integration/2/lib.js'); + }); + + it('should return production URL when isProductionEnv is true', () => { + (globalThis as any).isProductionEnv = true; + expect(getScriptSrc()).toBe('https://src.mastercard.com/srci/integration/2/lib.js'); + }); + + it('should return sandbox URL when isProductionEnv is undefined', () => { + (globalThis as any).isProductionEnv = undefined; + expect(getScriptSrc()).toBe('https://sandbox.src.mastercard.com/srci/integration/2/lib.js'); + }); + }); + + describe('setLocalStorage', () => { + const originalLocalStorage = window.localStorage; + + beforeEach(() => { + const storage: { [key: string]: string } = {}; + Object.defineProperty(window, 'localStorage', { + value: { + setItem: (key: string, value: string) => { + storage[key] = value; + }, + getItem: (key: string) => (key in storage ? storage[key] : null), + removeItem: (key: string) => { + delete storage[key]; + }, + }, + writable: true, + configurable: true, + }); + }); + + afterEach(() => { + Object.defineProperty(window, 'localStorage', { + value: originalLocalStorage, + writable: true, + configurable: true, + }); + }); + + it('should set item in localStorage', () => { + setLocalStorage('testKey', 'testValue'); + expect(window.localStorage.getItem('testKey')).toBe('testValue'); + }); + + it('should overwrite existing value', () => { + setLocalStorage('testKey', 'value1'); + setLocalStorage('testKey', 'value2'); + expect(window.localStorage.getItem('testKey')).toBe('value2'); + }); + + it('should handle empty string value', () => { + setLocalStorage('emptyKey', ''); + expect(window.localStorage.getItem('emptyKey')).toBe(''); + }); + }); + + describe('getLocalStorage', () => { + const originalLocalStorage = window.localStorage; + + beforeEach(() => { + const storage: { [key: string]: string } = { existingKey: 'existingValue' }; + Object.defineProperty(window, 'localStorage', { + value: { + setItem: (key: string, value: string) => { + storage[key] = value; + }, + getItem: (key: string) => (key in storage ? storage[key] : null), + removeItem: (key: string) => { + delete storage[key]; + }, + }, + writable: true, + configurable: true, + }); + }); + + afterEach(() => { + Object.defineProperty(window, 'localStorage', { + value: originalLocalStorage, + writable: true, + configurable: true, + }); + }); + + it('should get existing item from localStorage', () => { + expect(getLocalStorage('existingKey')).toBe('existingValue'); + }); + + it('should return null for non-existing key', () => { + expect(getLocalStorage('nonExistingKey')).toBeNull(); + }); + + it('should return null for empty key', () => { + expect(getLocalStorage('')).toBeNull(); + }); + }); + + describe('deleteLocalStorage', () => { + const originalLocalStorage = window.localStorage; + + beforeEach(() => { + const storage: { [key: string]: string } = { testKey: 'testValue' }; + Object.defineProperty(window, 'localStorage', { + value: { + setItem: (key: string, value: string) => { + storage[key] = value; + }, + getItem: (key: string) => (key in storage ? storage[key] : null), + removeItem: (key: string) => { + delete storage[key]; + }, + }, + writable: true, + configurable: true, + }); + }); + + afterEach(() => { + Object.defineProperty(window, 'localStorage', { + value: originalLocalStorage, + writable: true, + configurable: true, + }); + }); + + it('should delete existing item from localStorage', () => { + deleteLocalStorage('testKey'); + expect(window.localStorage.getItem('testKey')).toBeNull(); + }); + + it('should not throw when deleting non-existing key', () => { + expect(() => deleteLocalStorage('nonExistingKey')).not.toThrow(); + }); + + it('should not throw when deleting with empty key', () => { + expect(() => deleteLocalStorage('')).not.toThrow(); + }); + }); + + describe('handleCloseClickToPayWindow', () => { + it('should close window when clickToPayWindowRef has a window', () => { + const mockClose = jest.fn(); + (clickToPayWindowRef as any).contents = { close: mockClose }; + handleCloseClickToPayWindow(); + expect(mockClose).toHaveBeenCalled(); + }); + + it('should set clickToPayWindowRef.contents to null after closing', () => { + const mockClose = jest.fn(); + (clickToPayWindowRef as any).contents = { close: mockClose }; + handleCloseClickToPayWindow(); + expect(clickToPayWindowRef.contents).toBeNull(); + }); + + it('should not throw when clickToPayWindowRef is null', () => { + (clickToPayWindowRef as any).contents = null; + expect(() => handleCloseClickToPayWindow()).not.toThrow(); + }); + }); + + describe('handleOpenClickToPayWindow', () => { + const originalOpen = window.open; + + beforeEach(() => { + const mockWindow = { document: { write: jest.fn(), close: jest.fn() } }; + Object.defineProperty(window, 'open', { + value: jest.fn(() => mockWindow), + writable: true, + configurable: true, + }); + }); + + afterEach(() => { + Object.defineProperty(window, 'open', { + value: originalOpen, + writable: true, + configurable: true, + }); + (clickToPayWindowRef as any).contents = null; + }); + + it('should open a new window with correct parameters', () => { + handleOpenClickToPayWindow(); + expect(window.open).toHaveBeenCalledWith('', 'ClickToPayWindow', 'width=480,height=600'); + }); + + it('should set clickToPayWindowRef.contents to the opened window', () => { + const mockWindow = { document: { write: jest.fn(), close: jest.fn() } }; + (window.open as jest.Mock).mockReturnValue(mockWindow); + handleOpenClickToPayWindow(); + expect(clickToPayWindowRef.contents).toBe(mockWindow); + }); + }); + + describe('mcCheckoutService', () => { + it('should have contents property', () => { + expect(mcCheckoutService).toHaveProperty('contents'); + }); + }); + + describe('initializeMastercardCheckout', () => { + beforeEach(() => { + (mcCheckoutService as any).contents = undefined; + }); + + it('should reject when MastercardCheckoutServices is not available', async () => { + const mockLogger = createMockLogger(); + const token = { + dpaId: 'test-dpa', + dpaName: 'Test Merchant', + locale: 'en_US', + transactionAmount: 100, + transactionCurrencyCode: 'USD', + acquirerBIN: '123456', + acquirerMerchantId: 'merchant-123', + merchantCategoryCode: '5999', + merchantCountryCode: 'US', + cardBrands: ['VISA'], + }; + + (window as any).MastercardCheckoutServices = undefined; + + await expect(initializeMastercardCheckout(token, mockLogger)).rejects.toBeDefined(); + expect(mockLogger.setLogError).toHaveBeenCalled(); + }); + + it('should reject when MastercardCheckoutServices constructor throws', async () => { + const mockLogger = createMockLogger(); + const token = { + dpaId: 'test-dpa', + dpaName: 'Test Merchant', + locale: 'en_US', + transactionAmount: 100, + transactionCurrencyCode: 'USD', + acquirerBIN: '123456', + acquirerMerchantId: 'merchant-123', + merchantCategoryCode: '5999', + merchantCountryCode: 'US', + cardBrands: ['VISA'], + }; + + (window as any).MastercardCheckoutServices = jest.fn().mockImplementation(() => { + throw new Error('Constructor error'); + }); + + try { + await initializeMastercardCheckout(token, mockLogger); + fail('Should have thrown'); + } catch (e) { + expect(e).toBeDefined(); + } + }); + }); + + describe('getCards', () => { + it('should return empty array when service is not initialized', async () => { + const mockLogger = createMockLogger(); + (mcCheckoutService as any).contents = undefined; + + const result = await getCards(mockLogger); + + expect(result.TAG).toBe('Ok'); + expect(result._0).toEqual([]); + }); + + it('should return cards when service is available', async () => { + const mockLogger = createMockLogger(); + const mockCards = [{ srcDigitalCardId: 'card-1' }]; + (mcCheckoutService as any).contents = { + getCards: jest.fn().mockResolvedValue(mockCards), + }; + + const result = await getCards(mockLogger); + + expect(result.TAG).toBe('Ok'); + expect(result._0).toEqual(mockCards); + }); + + it('should return empty array on error', async () => { + const mockLogger = createMockLogger(); + (mcCheckoutService as any).contents = { + getCards: jest.fn().mockRejectedValue(new Error('Network error')), + }; + + const result = await getCards(mockLogger); + + expect(result.TAG).toBe('Ok'); + expect(result._0).toEqual([]); + }); + }); + + describe('authenticate', () => { + it('should return Error when service is not initialized', async () => { + const mockLogger = createMockLogger(); + (mcCheckoutService as any).contents = undefined; + + const payload = { + windowRef: {} as Window, + consumerIdentity: { + identityType: 'EMAIL_ADDRESS', + identityValue: 'test@example.com', + }, + }; + + const result = await authenticate(payload, mockLogger); + + expect(result.TAG).toBe('Error'); + }); + + it('should return Ok with authentication response on success', async () => { + const mockLogger = createMockLogger(); + const mockAuthResponse = JSON.stringify({ recognitionToken: 'token-123' }); + (mcCheckoutService as any).contents = { + authenticate: jest.fn().mockResolvedValue(mockAuthResponse), + }; + + const payload = { + windowRef: {} as Window, + consumerIdentity: { + identityType: 'EMAIL_ADDRESS', + identityValue: 'test@example.com', + }, + }; + + const result = await authenticate(payload, mockLogger); + + expect(result.TAG).toBe('Ok'); + }); + }); + + describe('checkoutWithCard', () => { + it('should return Error when service is not initialized', async () => { + const mockLogger = createMockLogger(); + (mcCheckoutService as any).contents = undefined; + + const result = await checkoutWithCard({} as Window, 'card-123', mockLogger); + + expect(result.TAG).toBe('Error'); + }); + + it('should return Ok on successful checkout', async () => { + const mockLogger = createMockLogger(); + const mockResponse = { checkoutActionCode: 'COMPLETE' }; + (mcCheckoutService as any).contents = { + checkoutWithCard: jest.fn().mockResolvedValue(mockResponse), + }; + + const result = await checkoutWithCard({} as Window, 'card-123', mockLogger); + + expect(result.TAG).toBe('Ok'); + expect(result._0).toEqual(mockResponse); + }); + }); + + describe('encryptCardForClickToPay', () => { + it('should return Error when service is not initialized', async () => { + const mockLogger = createMockLogger(); + (mcCheckoutService as any).contents = undefined; + + const result = await encryptCardForClickToPay('4111111111111111', '12', '2025', '123', mockLogger); + + expect(result.TAG).toBe('Error'); + }); + + it('should return Ok with encrypted card on success', async () => { + const mockLogger = createMockLogger(); + const mockEncryptedCard = { encryptedData: 'encrypted-value' }; + (mcCheckoutService as any).contents = { + encryptCard: jest.fn().mockResolvedValue(mockEncryptedCard), + }; + + const result = await encryptCardForClickToPay('4111111111111111', '12', '2025', '123', mockLogger); + + expect(result.TAG).toBe('Ok'); + expect(result._0).toEqual(mockEncryptedCard); + }); + }); + + describe('checkoutWithNewCard', () => { + it('should return Error when service is not initialized', async () => { + const mockLogger = createMockLogger(); + (mcCheckoutService as any).contents = undefined; + + const result = await checkoutWithNewCard({}, mockLogger); + + expect(result.TAG).toBe('Error'); + }); + + it('should return Ok on successful new card checkout', async () => { + const mockLogger = createMockLogger(); + const mockResponse = { checkoutActionCode: 'COMPLETE' }; + (mcCheckoutService as any).contents = { + checkoutWithNewCard: jest.fn().mockResolvedValue(mockResponse), + }; + + const payload = { + windowRef: {} as Window, + cardBrand: 'VISA', + encryptedCard: 'encrypted-data', + rememberMe: true, + }; + + const result = await checkoutWithNewCard(payload, mockLogger); + + expect(result.TAG).toBe('Ok'); + }); + }); + + describe('signOut', () => { + it('should return Error when service is not initialized', async () => { + (mcCheckoutService as any).contents = undefined; + + const result = await signOut(); + + expect(result.TAG).toBe('Error'); + }); + + it('should return Ok on successful sign out', async () => { + (mcCheckoutService as any).contents = { + signOut: jest.fn().mockResolvedValue({}), + }; + + const result = await signOut(); + + expect(result.TAG).toBe('Ok'); + }); + + it('should return Error on sign out failure', async () => { + (mcCheckoutService as any).contents = { + signOut: jest.fn().mockRejectedValue(new Error('Sign out failed')), + }; + + const result = await signOut(); + + expect(result.TAG).toBe('Error'); + }); + }); + + describe('getCardsVisaUnified', () => { + it('should call VSDK.getCards with config', () => { + const mockGetCards = jest.fn().mockResolvedValue([]); + (window as any).VSDK = { getCards: mockGetCards }; + + const config = { dpaId: 'test-dpa' }; + getCardsVisaUnified(config); + + expect(mockGetCards).toHaveBeenCalledWith(config); + }); + }); + + describe('signOutVisaUnified', () => { + it('should call VSDK.unbindAppInstance', () => { + const mockUnbind = jest.fn(); + (window as any).VSDK = { unbindAppInstance: mockUnbind }; + + signOutVisaUnified(); + + expect(mockUnbind).toHaveBeenCalled(); + }); + }); + + describe('loadVisaScript', () => { + const originalCreateElement = document.createElement.bind(document); + const originalAppendChild = document.body.appendChild.bind(document.body); + + beforeEach(() => { + const mockScript = { + type: '', + src: '', + onload: null as (() => void) | null, + onerror: null as (() => void) | null, + }; + document.createElement = jest.fn().mockReturnValue(mockScript); + document.body.appendChild = jest.fn(); + }); + + afterEach(() => { + document.createElement = originalCreateElement; + document.body.appendChild = originalAppendChild; + }); + + it('should create and append script element', () => { + const mockOnLoad = jest.fn(); + const mockOnError = jest.fn(); + const token = { + dpaId: 'test-dpa', + dpaName: 'Test Merchant', + locale: 'en_US', + cardBrands: ['VISA'], + }; + + loadVisaScript(token, mockOnLoad, mockOnError); + + expect(document.createElement).toHaveBeenCalledWith('script'); + expect(document.body.appendChild).toHaveBeenCalled(); + }); + }); + + describe('loadClickToPayUIScripts', () => { + const originalQuerySelector = document.querySelector.bind(document); + const originalCreateElement = document.createElement.bind(document); + const originalAppendChild = document.head.appendChild.bind(document.head); + + beforeEach(() => { + document.querySelector = jest.fn().mockReturnValue(null); + document.createElement = jest.fn().mockReturnValue({ + type: '', + src: '', + rel: '', + href: '', + onload: null as (() => void) | null, + onerror: null as (() => void) | null, + }); + document.head.appendChild = jest.fn(); + }); + + afterEach(() => { + document.querySelector = originalQuerySelector; + document.createElement = originalCreateElement; + document.head.appendChild = originalAppendChild; + }); + + it('should load scripts and call callbacks', () => { + const mockLogger = createMockLogger(); + const mockOnLoad = jest.fn(); + const mockOnError = jest.fn(); + + loadClickToPayUIScripts(mockLogger, mockOnLoad, mockOnError); + + expect(document.createElement).toHaveBeenCalled(); + }); + }); + + describe('checkoutVisaUnified', () => { + beforeEach(() => { + (window as any).VSDK = { + checkout: jest.fn().mockResolvedValue({ actionCode: 'SUCCESS' }), + }; + (clickToPayWindowRef as any).contents = {} as Window; + }); + + afterEach(() => { + (clickToPayWindowRef as any).contents = null; + }); + + it('should call VSDK.checkout with correct config for existing card', async () => { + const token = { + dpaId: 'test-dpa', + dpaName: 'Test Merchant', + locale: 'en_US', + transactionAmount: 100, + transactionCurrencyCode: 'USD', + acquirerBIN: '123456', + acquirerMerchantId: 'merchant-123', + merchantCategoryCode: '5999', + merchantCountryCode: 'US', + cardBrands: ['VISA'], + }; + const consumer = { + fullName: 'Test User', + emailAddress: 'test@example.com', + mobileNumber: { phoneNumber: '1234567890', countryCode: '1' }, + }; + + const result = await checkoutVisaUnified( + 'card-123', + undefined, + {} as Window, + false, + true, + token, + 'pay_test_secret_123', + consumer, + true + ); + + expect(result).toBeDefined(); + }); + }); + + describe('handleCheckoutWithCard', () => { + const mockLogger = createMockLogger(); + + beforeEach(() => { + (clickToPayWindowRef as any).contents = null; + }); + + it('should return ERROR status when window reference is null', async () => { + (clickToPayWindowRef as any).contents = null; + + const result = await handleCheckoutWithCard( + 'VISA', + 'card-123', + mockLogger, + 'Test User', + 'test@example.com', + '1234567890', + '1', + undefined, + false, + 'pay_test' + ); + + expect(result.status).toBe('ERROR'); + }); + + it('should return ERROR status for NONE provider', async () => { + (clickToPayWindowRef as any).contents = { close: jest.fn() } as unknown as Window; + + const result = await handleCheckoutWithCard( + 'NONE', + 'card-123', + mockLogger, + 'Test User', + 'test@example.com', + '1234567890', + '1', + undefined, + false, + 'pay_test' + ); + + expect(result.status).toBe('ERROR'); + }); + }); + + describe('handleProceedToPay', () => { + const mockLogger = createMockLogger(); + + beforeEach(() => { + (clickToPayWindowRef as any).contents = null; + }); + + it('should return ERROR status when window reference is null and new card checkout', async () => { + (clickToPayWindowRef as any).contents = null; + + const result = await handleProceedToPay( + undefined, + undefined, + true, + false, + 'test@example.com', + '1234567890', + '1', + false, + mockLogger, + undefined, + 'VISA', + false, + undefined, + undefined, + undefined + ); + + expect(result.status).toBe('ERROR'); + }); + }); +}); diff --git a/src/__tests__/ClickToPayHook.test.ts b/src/__tests__/ClickToPayHook.test.ts new file mode 100644 index 000000000..35ecdf148 --- /dev/null +++ b/src/__tests__/ClickToPayHook.test.ts @@ -0,0 +1,678 @@ +import { renderHook, act } from '@testing-library/react'; +import { RecoilRoot } from 'recoil'; +import * as React from 'react'; +import * as ClickToPayHook from '../Hooks/ClickToPayHook.bs.js'; +import * as RecoilAtoms from '../Utilities/RecoilAtoms.bs.js'; + +const mockGetCardsVisaUnified = jest.fn(); +const mockGetVisaInitConfig = jest.fn(); +const mockLoadClickToPayUIScripts = jest.fn(); +const mockLoadVisaScript = jest.fn(); +const mockLoadClickToPayScripts = jest.fn(); +const mockLoadMastercardScript = jest.fn(); +const mockGetCards = jest.fn(); +const mockGetDictFromJson = jest.fn((obj: any) => (typeof obj === 'object' && obj !== null ? obj : {})); +const mockItemToObjMapper = jest.fn((obj: any, key: string) => obj?.[key] || {}); +const mockGetPaymentSessionObj = jest.fn(); +const mockClickToPayTokenItemToObjMapper = jest.fn(); +const mockFormatException = jest.fn((e: any) => e?.message || String(e)); +const mockMessageParentWindow = jest.fn(); + +jest.mock('../Types/ClickToPayHelpers.bs.js', () => ({ + getCardsVisaUnified: (config: any) => mockGetCardsVisaUnified(config), + getVisaInitConfig: (token: any, secret: any) => mockGetVisaInitConfig(token, secret), + loadClickToPayUIScripts: (logger: any, onLoad: any, onError: any) => mockLoadClickToPayUIScripts(logger, onLoad, onError), + loadVisaScript: (token: any, onLoad: any, onError: any) => mockLoadVisaScript(token, onLoad, onError), + loadClickToPayScripts: (logger: any) => mockLoadClickToPayScripts(logger), + loadMastercardScript: (token: any, logger: any) => mockLoadMastercardScript(token, logger), + getCards: (logger: any) => mockGetCards(logger), + clickToPayTokenItemToObjMapper: (token: any) => mockClickToPayTokenItemToObjMapper(token), +})); + +jest.mock('../Utilities/Utils.bs.js', () => ({ + getDictFromJson: (obj: any) => mockGetDictFromJson(obj), + itemToObjMapper: (obj: any, key: string) => mockItemToObjMapper(obj, key), + getPaymentSessionObj: (sessions: any, type: string) => mockGetPaymentSessionObj(sessions, type), + formatException: (e: any) => mockFormatException(e), + messageParentWindow: (a: any, b: any) => mockMessageParentWindow(a, b), + getArray: jest.fn((obj: any, key: string) => obj?.[key] || []), +})); + +jest.mock('../Types/SessionsType.bs.js', () => ({ + itemToObjMapper: (obj: any, key: string) => mockItemToObjMapper(obj, key), + getPaymentSessionObj: (sessions: any, type: string) => mockGetPaymentSessionObj(sessions, type), +})); + +const createMockLogger = () => ({ + setLogInfo: jest.fn(), + setLogError: jest.fn(), +}); + +const createWrapperWithAtoms = (atomValues: any) => { + return function Wrapper({ children }: { children: React.ReactNode }) { + return React.createElement( + RecoilRoot, + { + initializeState: ({ set }: any) => { + Object.entries(atomValues).forEach(([key, value]) => { + const atom = (RecoilAtoms as any)[key]; + if (atom) { + set(atom, value); + } + }); + }, + }, + children + ); + }; +}; + +describe('ClickToPayHook', () => { + describe('useClickToPay', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('hook exists and is a function', () => { + expect(typeof ClickToPayHook.useClickToPay).toBe('function'); + }); + + it('returns an array with getVisaCards and closeComponentIfSavedMethodsAreEmpty functions', () => { + const mockLogger = createMockLogger(); + const mockSetSessions = jest.fn(); + const mockSetAreClickToPayUIScriptsLoaded = jest.fn(); + + const Wrapper = createWrapperWithAtoms({ + loggerAtom: mockLogger, + clickToPayConfig: { + clickToPayProvider: 'NONE', + isReady: false, + email: '', + dpaName: '', + availableCardBrands: [], + visaComponentState: 'NONE', + maskedIdentity: '', + otpError: '', + consumerIdentity: { identityType: 'EMAIL_ADDRESS', identityValue: '' }, + clickToPayToken: undefined, + clickToPayCards: undefined, + }, + sessions: 'Loading', + keys: { clientSecret: 'test_secret', publishableKey: 'pk_test' }, + showPaymentMethodsScreen: false, + }); + + const { result } = renderHook( + () => + ClickToPayHook.useClickToPay( + false, + mockSetSessions, + mockSetAreClickToPayUIScriptsLoaded, + [], + 'LoadingSavedCards' + ), + { wrapper: Wrapper } + ); + + expect(Array.isArray(result.current)).toBe(true); + expect(result.current).toHaveLength(2); + expect(typeof result.current[0]).toBe('function'); + expect(typeof result.current[1]).toBe('function'); + }); + + it('getVisaCards handles successful card fetch', async () => { + const mockLogger = createMockLogger(); + const mockSetSessions = jest.fn(); + const mockSetAreClickToPayUIScriptsLoaded = jest.fn(); + + const mockCards = [{ srcDigitalCardId: 'card-1', panLastFour: '4242' }]; + mockGetCardsVisaUnified.mockResolvedValue({ + actionCode: 'SUCCESS', + profiles: [{ maskedCards: mockCards }], + }); + + const Wrapper = createWrapperWithAtoms({ + loggerAtom: mockLogger, + clickToPayConfig: { + clickToPayProvider: 'VISA', + isReady: true, + email: 'test@example.com', + dpaName: 'Test Merchant', + availableCardBrands: ['VISA'], + visaComponentState: 'NONE', + maskedIdentity: '', + otpError: '', + consumerIdentity: { identityType: 'EMAIL_ADDRESS', identityValue: '' }, + clickToPayToken: { dpaId: 'test-dpa' }, + clickToPayCards: undefined, + }, + sessions: 'Loading', + keys: { clientSecret: 'test_secret', publishableKey: 'pk_test' }, + showPaymentMethodsScreen: false, + }); + + const { result } = renderHook( + () => + ClickToPayHook.useClickToPay( + true, + mockSetSessions, + mockSetAreClickToPayUIScriptsLoaded, + [], + 'LoadingSavedCards' + ), + { wrapper: Wrapper } + ); + + expect(typeof result.current[0]).toBe('function'); + }); + + it('getVisaCards handles OTP required response', async () => { + const mockLogger = createMockLogger(); + const mockSetSessions = jest.fn(); + const mockSetAreClickToPayUIScriptsLoaded = jest.fn(); + + mockGetCardsVisaUnified.mockResolvedValue({ + actionCode: 'PENDING_CONSUMER_IDV', + maskedValidationChannel: '+1***1234', + }); + + const Wrapper = createWrapperWithAtoms({ + loggerAtom: mockLogger, + clickToPayConfig: { + clickToPayProvider: 'VISA', + isReady: true, + email: 'test@example.com', + dpaName: 'Test Merchant', + availableCardBrands: ['VISA'], + visaComponentState: 'NONE', + maskedIdentity: '', + otpError: '', + consumerIdentity: { identityType: 'EMAIL_ADDRESS', identityValue: '' }, + clickToPayToken: { dpaId: 'test-dpa' }, + clickToPayCards: undefined, + }, + sessions: 'Loading', + keys: { clientSecret: 'test_secret', publishableKey: 'pk_test' }, + showPaymentMethodsScreen: false, + }); + + const { result } = renderHook( + () => + ClickToPayHook.useClickToPay( + true, + mockSetSessions, + mockSetAreClickToPayUIScriptsLoaded, + [], + 'LoadingSavedCards' + ), + { wrapper: Wrapper } + ); + + expect(typeof result.current[0]).toBe('function'); + }); + + it('getVisaCards handles FAILED action code with OTP error', async () => { + const mockLogger = createMockLogger(); + const mockSetSessions = jest.fn(); + const mockSetAreClickToPayUIScriptsLoaded = jest.fn(); + + mockGetCardsVisaUnified.mockResolvedValue({ + actionCode: 'FAILED', + error: { reason: 'ACCT_INACCESSIBLE' }, + }); + + const Wrapper = createWrapperWithAtoms({ + loggerAtom: mockLogger, + clickToPayConfig: { + clickToPayProvider: 'VISA', + isReady: true, + email: 'test@example.com', + dpaName: 'Test Merchant', + availableCardBrands: ['VISA'], + visaComponentState: 'NONE', + maskedIdentity: '', + otpError: '', + consumerIdentity: { identityType: 'EMAIL_ADDRESS', identityValue: '' }, + clickToPayToken: { dpaId: 'test-dpa' }, + clickToPayCards: undefined, + }, + sessions: 'Loading', + keys: { clientSecret: 'test_secret', publishableKey: 'pk_test' }, + showPaymentMethodsScreen: false, + }); + + const { result } = renderHook( + () => + ClickToPayHook.useClickToPay( + true, + mockSetSessions, + mockSetAreClickToPayUIScriptsLoaded, + [], + 'LoadingSavedCards' + ), + { wrapper: Wrapper } + ); + + expect(typeof result.current[0]).toBe('function'); + }); + + it('getVisaCards handles ADD_CARD action code', async () => { + const mockLogger = createMockLogger(); + const mockSetSessions = jest.fn(); + const mockSetAreClickToPayUIScriptsLoaded = jest.fn(); + + mockGetCardsVisaUnified.mockResolvedValue({ + actionCode: 'ADD_CARD', + }); + + const Wrapper = createWrapperWithAtoms({ + loggerAtom: mockLogger, + clickToPayConfig: { + clickToPayProvider: 'VISA', + isReady: true, + email: 'test@example.com', + dpaName: 'Test Merchant', + availableCardBrands: ['VISA'], + visaComponentState: 'NONE', + maskedIdentity: '', + otpError: '', + consumerIdentity: { identityType: 'EMAIL_ADDRESS', identityValue: '' }, + clickToPayToken: { dpaId: 'test-dpa' }, + clickToPayCards: undefined, + }, + sessions: 'Loading', + keys: { clientSecret: 'test_secret', publishableKey: 'pk_test' }, + showPaymentMethodsScreen: false, + }); + + const { result } = renderHook( + () => + ClickToPayHook.useClickToPay( + true, + mockSetSessions, + mockSetAreClickToPayUIScriptsLoaded, + [], + 'LoadingSavedCards' + ), + { wrapper: Wrapper } + ); + + expect(typeof result.current[0]).toBe('function'); + }); + + it('getVisaCards handles VALIDATION_DATA_INVALID error', async () => { + const mockLogger = createMockLogger(); + const mockSetSessions = jest.fn(); + const mockSetAreClickToPayUIScriptsLoaded = jest.fn(); + + mockGetCardsVisaUnified.mockResolvedValue({ + actionCode: 'ERROR', + error: { reason: 'VALIDATION_DATA_INVALID' }, + }); + + const Wrapper = createWrapperWithAtoms({ + loggerAtom: mockLogger, + clickToPayConfig: { + clickToPayProvider: 'VISA', + isReady: true, + email: 'test@example.com', + dpaName: 'Test Merchant', + availableCardBrands: ['VISA'], + visaComponentState: 'NONE', + maskedIdentity: '', + otpError: '', + consumerIdentity: { identityType: 'EMAIL_ADDRESS', identityValue: '' }, + clickToPayToken: { dpaId: 'test-dpa' }, + clickToPayCards: undefined, + }, + sessions: 'Loading', + keys: { clientSecret: 'test_secret', publishableKey: 'pk_test' }, + showPaymentMethodsScreen: false, + }); + + const { result } = renderHook( + () => + ClickToPayHook.useClickToPay( + true, + mockSetSessions, + mockSetAreClickToPayUIScriptsLoaded, + [], + 'LoadingSavedCards' + ), + { wrapper: Wrapper } + ); + + expect(typeof result.current[0]).toBe('function'); + }); + + it('getVisaCards handles OTP_SEND_FAILED error', async () => { + const mockLogger = createMockLogger(); + const mockSetSessions = jest.fn(); + const mockSetAreClickToPayUIScriptsLoaded = jest.fn(); + + mockGetCardsVisaUnified.mockResolvedValue({ + actionCode: 'ERROR', + error: { reason: 'OTP_SEND_FAILED' }, + }); + + const Wrapper = createWrapperWithAtoms({ + loggerAtom: mockLogger, + clickToPayConfig: { + clickToPayProvider: 'VISA', + isReady: true, + email: 'test@example.com', + dpaName: 'Test Merchant', + availableCardBrands: ['VISA'], + visaComponentState: 'NONE', + maskedIdentity: '', + otpError: '', + consumerIdentity: { identityType: 'EMAIL_ADDRESS', identityValue: '' }, + clickToPayToken: { dpaId: 'test-dpa' }, + clickToPayCards: undefined, + }, + sessions: 'Loading', + keys: { clientSecret: 'test_secret', publishableKey: 'pk_test' }, + showPaymentMethodsScreen: false, + }); + + const { result } = renderHook( + () => + ClickToPayHook.useClickToPay( + true, + mockSetSessions, + mockSetAreClickToPayUIScriptsLoaded, + [], + 'LoadingSavedCards' + ), + { wrapper: Wrapper } + ); + + expect(typeof result.current[0]).toBe('function'); + }); + + it('getVisaCards handles exception during fetch', async () => { + const mockLogger = createMockLogger(); + const mockSetSessions = jest.fn(); + const mockSetAreClickToPayUIScriptsLoaded = jest.fn(); + + mockGetCardsVisaUnified.mockRejectedValue(new Error('Network error')); + mockFormatException.mockReturnValue('Network error'); + + const Wrapper = createWrapperWithAtoms({ + loggerAtom: mockLogger, + clickToPayConfig: { + clickToPayProvider: 'VISA', + isReady: true, + email: 'test@example.com', + dpaName: 'Test Merchant', + availableCardBrands: ['VISA'], + visaComponentState: 'NONE', + maskedIdentity: '', + otpError: '', + consumerIdentity: { identityType: 'EMAIL_ADDRESS', identityValue: '' }, + clickToPayToken: { dpaId: 'test-dpa' }, + clickToPayCards: undefined, + }, + sessions: 'Loading', + keys: { clientSecret: 'test_secret', publishableKey: 'pk_test' }, + showPaymentMethodsScreen: false, + }); + + const { result } = renderHook( + () => + ClickToPayHook.useClickToPay( + true, + mockSetSessions, + mockSetAreClickToPayUIScriptsLoaded, + [], + 'LoadingSavedCards' + ), + { wrapper: Wrapper } + ); + + expect(typeof result.current[0]).toBe('function'); + }); + + it('closeComponentIfSavedMethodsAreEmpty handles empty savedMethods', () => { + const mockLogger = createMockLogger(); + const mockSetSessions = jest.fn(); + const mockSetAreClickToPayUIScriptsLoaded = jest.fn(); + + const Wrapper = createWrapperWithAtoms({ + loggerAtom: mockLogger, + clickToPayConfig: { + clickToPayProvider: 'NONE', + isReady: false, + email: '', + dpaName: '', + availableCardBrands: [], + visaComponentState: 'NONE', + maskedIdentity: '', + otpError: '', + consumerIdentity: { identityType: 'EMAIL_ADDRESS', identityValue: '' }, + clickToPayToken: undefined, + clickToPayCards: undefined, + }, + sessions: 'Loading', + keys: { clientSecret: 'test_secret', publishableKey: 'pk_test' }, + showPaymentMethodsScreen: false, + }); + + const { result } = renderHook( + () => + ClickToPayHook.useClickToPay( + false, + mockSetSessions, + mockSetAreClickToPayUIScriptsLoaded, + [], + 'LoadedSavedCards' + ), + { wrapper: Wrapper } + ); + + expect(typeof result.current[1]).toBe('function'); + }); + + it('closeComponentIfSavedMethodsAreEmpty does nothing when savedMethods has items', () => { + const mockLogger = createMockLogger(); + const mockSetSessions = jest.fn(); + const mockSetAreClickToPayUIScriptsLoaded = jest.fn(); + + const Wrapper = createWrapperWithAtoms({ + loggerAtom: mockLogger, + clickToPayConfig: { + clickToPayProvider: 'NONE', + isReady: false, + email: '', + dpaName: '', + availableCardBrands: [], + visaComponentState: 'NONE', + maskedIdentity: '', + otpError: '', + consumerIdentity: { identityType: 'EMAIL_ADDRESS', identityValue: '' }, + clickToPayToken: undefined, + clickToPayCards: undefined, + }, + sessions: 'Loading', + keys: { clientSecret: 'test_secret', publishableKey: 'pk_test' }, + showPaymentMethodsScreen: false, + }); + + const { result } = renderHook( + () => + ClickToPayHook.useClickToPay( + false, + mockSetSessions, + mockSetAreClickToPayUIScriptsLoaded, + [{ id: 'card-1' }], + 'LoadedSavedCards' + ), + { wrapper: Wrapper } + ); + + expect(typeof result.current[1]).toBe('function'); + }); + + it('handles MASTERCARD provider initialization', () => { + const mockLogger = createMockLogger(); + const mockSetSessions = jest.fn(); + const mockSetAreClickToPayUIScriptsLoaded = jest.fn(); + + const mockToken = { + provider: 'mastercard', + dpaId: 'test-dpa', + dpaName: 'Test Merchant', + email: 'test@example.com', + cardBrands: ['MASTERCARD'], + }; + + mockGetPaymentSessionObj.mockReturnValue({ TAG: 'ClickToPayTokenOptional', _0: mockToken }); + mockClickToPayTokenItemToObjMapper.mockReturnValue(mockToken); + mockLoadClickToPayScripts.mockResolvedValue(Promise.resolve()); + mockLoadMastercardScript.mockResolvedValue(JSON.stringify({ availableCardBrands: ['MASTERCARD'] })); + + const Wrapper = createWrapperWithAtoms({ + loggerAtom: mockLogger, + clickToPayConfig: { + clickToPayProvider: 'NONE', + isReady: false, + email: '', + dpaName: '', + availableCardBrands: [], + visaComponentState: 'NONE', + maskedIdentity: '', + otpError: '', + consumerIdentity: { identityType: 'EMAIL_ADDRESS', identityValue: '' }, + clickToPayToken: undefined, + clickToPayCards: undefined, + }, + sessions: { TAG: 'Loaded', _0: 'session-data' }, + keys: { clientSecret: 'test_secret', publishableKey: 'pk_test' }, + showPaymentMethodsScreen: false, + }); + + const { result } = renderHook( + () => + ClickToPayHook.useClickToPay( + false, + mockSetSessions, + mockSetAreClickToPayUIScriptsLoaded, + [], + 'LoadingSavedCards' + ), + { wrapper: Wrapper } + ); + + expect(Array.isArray(result.current)).toBe(true); + }); + + it('handles VISA provider initialization', () => { + const mockLogger = createMockLogger(); + const mockSetSessions = jest.fn(); + const mockSetAreClickToPayUIScriptsLoaded = jest.fn(); + + const mockToken = { + provider: 'visa', + dpaId: 'test-dpa', + dpaName: 'Test Merchant', + email: 'test@example.com', + cardBrands: ['VISA'], + }; + + mockGetPaymentSessionObj.mockReturnValue({ TAG: 'ClickToPayTokenOptional', _0: mockToken }); + mockClickToPayTokenItemToObjMapper.mockReturnValue(mockToken); + mockLoadClickToPayUIScripts.mockImplementation((_logger: any, onLoad: any, _onError: any) => { + onLoad(); + }); + mockLoadVisaScript.mockImplementation((_token: any, onLoad: any, _onError: any) => { + onLoad(); + }); + mockGetVisaInitConfig.mockReturnValue({}); + + const Wrapper = createWrapperWithAtoms({ + loggerAtom: mockLogger, + clickToPayConfig: { + clickToPayProvider: 'NONE', + isReady: false, + email: '', + dpaName: '', + availableCardBrands: [], + visaComponentState: 'NONE', + maskedIdentity: '', + otpError: '', + consumerIdentity: { identityType: 'EMAIL_ADDRESS', identityValue: '' }, + clickToPayToken: undefined, + clickToPayCards: undefined, + }, + sessions: { TAG: 'Loaded', _0: 'session-data' }, + keys: { clientSecret: 'test_secret', publishableKey: 'pk_test' }, + showPaymentMethodsScreen: false, + }); + + const { result } = renderHook( + () => + ClickToPayHook.useClickToPay( + false, + mockSetSessions, + mockSetAreClickToPayUIScriptsLoaded, + [], + 'LoadingSavedCards' + ), + { wrapper: Wrapper } + ); + + expect(Array.isArray(result.current)).toBe(true); + }); + + it('handles unknown provider by setting provider to NONE', () => { + const mockLogger = createMockLogger(); + const mockSetSessions = jest.fn(); + const mockSetAreClickToPayUIScriptsLoaded = jest.fn(); + + const mockToken = { + provider: 'unknown', + dpaId: 'test-dpa', + dpaName: 'Test Merchant', + email: 'test@example.com', + cardBrands: [], + }; + + mockGetPaymentSessionObj.mockReturnValue({ TAG: 'ClickToPayTokenOptional', _0: mockToken }); + mockClickToPayTokenItemToObjMapper.mockReturnValue(mockToken); + + const Wrapper = createWrapperWithAtoms({ + loggerAtom: mockLogger, + clickToPayConfig: { + clickToPayProvider: 'NONE', + isReady: false, + email: '', + dpaName: '', + availableCardBrands: [], + visaComponentState: 'NONE', + maskedIdentity: '', + otpError: '', + consumerIdentity: { identityType: 'EMAIL_ADDRESS', identityValue: '' }, + clickToPayToken: undefined, + clickToPayCards: undefined, + }, + sessions: { TAG: 'Loaded', _0: 'session-data' }, + keys: { clientSecret: 'test_secret', publishableKey: 'pk_test' }, + showPaymentMethodsScreen: false, + }); + + const { result } = renderHook( + () => + ClickToPayHook.useClickToPay( + false, + mockSetSessions, + mockSetAreClickToPayUIScriptsLoaded, + [], + 'LoadingSavedCards' + ), + { wrapper: Wrapper } + ); + + expect(Array.isArray(result.current)).toBe(true); + }); + }); +}); diff --git a/src/__tests__/CommonCardProps.test.ts b/src/__tests__/CommonCardProps.test.ts new file mode 100644 index 000000000..8494cb5b5 --- /dev/null +++ b/src/__tests__/CommonCardProps.test.ts @@ -0,0 +1,367 @@ +import { renderHook, act } from '@testing-library/react'; +import { RecoilRoot } from 'recoil'; +import * as CommonCardProps from '../Hooks/CommonCardProps.bs.js'; +import * as RecoilAtoms from '../Utilities/RecoilAtoms.bs.js'; +import * as React from 'react'; + +describe('useCardForm', () => { + const mockLogger = { + setLogInfo: jest.fn(), + setLogError: jest.fn(), + }; + + const mockConfig = { + localeString: { + blockedCardText: 'Card is blocked', + inValidCardErrorText: 'Invalid card number', + inCompleteCVCErrorText: 'CVC is incomplete', + inCompleteExpiryErrorText: 'Expiry date is incomplete', + pastExpiryErrorText: 'Card has expired', + cardBrandInvalidErrorText: 'Card brand not supported', + }, + }; + + const createWrapper = (config: any = mockConfig, cardBrandValue: string = '', showPaymentMethodsScreen: boolean = false) => { + return function Wrapper({ children }: { children: React.ReactNode }) { + return React.createElement( + RecoilRoot, + { + initializeState: ({ set }: any) => { + set(RecoilAtoms.configAtom, config); + set(RecoilAtoms.cardBrand, cardBrandValue); + set(RecoilAtoms.showPaymentMethodsScreen, showPaymentMethodsScreen); + set(RecoilAtoms.selectedOptionAtom, ''); + set(RecoilAtoms.blockedBins, []); + set(RecoilAtoms.paymentTokenAtom, { paymentToken: '', customerId: '' }); + }, + }, + children + ); + }; + }; + + it('returns cardProps with initial state', () => { + const Wrapper = createWrapper(); + const { result } = renderHook( + () => CommonCardProps.useCardForm(mockLogger, 'card'), + { wrapper: Wrapper } + ); + + expect(result.current.cardProps).toBeDefined(); + expect(result.current.cardProps.cardNumber).toBe(''); + expect(result.current.cardProps.cardError).toBe(''); + expect(result.current.cardProps.maxCardLength).toBeGreaterThan(0); + expect(result.current.cardProps.cardRef).toBeDefined(); + }); + + it('returns expiryProps with initial state', () => { + const Wrapper = createWrapper(); + const { result } = renderHook( + () => CommonCardProps.useCardForm(mockLogger, 'card'), + { wrapper: Wrapper } + ); + + expect(result.current.expiryProps).toBeDefined(); + expect(result.current.expiryProps.cardExpiry).toBe(''); + expect(result.current.expiryProps.expiryError).toBe(''); + expect(result.current.expiryProps.expiryRef).toBeDefined(); + }); + + it('returns cvcProps with initial state', () => { + const Wrapper = createWrapper(); + const { result } = renderHook( + () => CommonCardProps.useCardForm(mockLogger, 'card'), + { wrapper: Wrapper } + ); + + expect(result.current.cvcProps).toBeDefined(); + expect(result.current.cvcProps.cvcNumber).toBe(''); + expect(result.current.cvcProps.cvcError).toBe(''); + expect(result.current.cvcProps.cvcRef).toBeDefined(); + }); + + it('returns zipProps with initial state', () => { + const Wrapper = createWrapper(); + const { result } = renderHook( + () => CommonCardProps.useCardForm(mockLogger, 'card'), + { wrapper: Wrapper } + ); + + expect(result.current.zipProps).toBeDefined(); + expect(result.current.zipProps.zipCode).toBe(''); + expect(result.current.zipProps.displayPincode).toBe(false); + expect(result.current.zipProps.zipRef).toBeDefined(); + }); + + it('updates card number on change', () => { + const Wrapper = createWrapper(); + const { result } = renderHook( + () => CommonCardProps.useCardForm(mockLogger, 'card'), + { wrapper: Wrapper } + ); + + act(() => { + const mockEvent = { + target: { value: '4111111111111111' }, + } as React.ChangeEvent; + result.current.cardProps.changeCardNumber(mockEvent); + }); + + expect(result.current.cardProps.cardNumber).toBeDefined(); + expect(mockLogger.setLogInfo).toHaveBeenCalled(); + }); + + it('updates expiry on change', () => { + const Wrapper = createWrapper(); + const { result } = renderHook( + () => CommonCardProps.useCardForm(mockLogger, 'card'), + { wrapper: Wrapper } + ); + + act(() => { + const mockEvent = { + target: { value: '1225' }, + } as React.ChangeEvent; + result.current.expiryProps.changeCardExpiry(mockEvent); + }); + + expect(result.current.expiryProps.cardExpiry).toBeDefined(); + expect(mockLogger.setLogInfo).toHaveBeenCalled(); + }); + + it('updates CVC on change', () => { + const Wrapper = createWrapper(); + const { result } = renderHook( + () => CommonCardProps.useCardForm(mockLogger, 'card'), + { wrapper: Wrapper } + ); + + act(() => { + const mockEvent = { + target: { value: '123' }, + } as React.ChangeEvent; + result.current.cvcProps.changeCVCNumber(mockEvent); + }); + + expect(result.current.cvcProps.cvcNumber).toBeDefined(); + expect(mockLogger.setLogInfo).toHaveBeenCalled(); + }); + + it('updates zip code on change', () => { + const Wrapper = createWrapper(); + const { result } = renderHook( + () => CommonCardProps.useCardForm(mockLogger, 'card'), + { wrapper: Wrapper } + ); + + act(() => { + const mockEvent = { + target: { value: '12345' }, + } as React.ChangeEvent; + result.current.zipProps.changeZipCode(mockEvent); + }); + + expect(result.current.zipProps.zipCode).toBe('12345'); + expect(mockLogger.setLogInfo).toHaveBeenCalled(); + }); + + it('handles card blur event', () => { + const Wrapper = createWrapper(); + const { result } = renderHook( + () => CommonCardProps.useCardForm(mockLogger, 'card'), + { wrapper: Wrapper } + ); + + act(() => { + const mockEvent = { + target: { value: '4111111111111111' }, + } as React.FocusEvent; + result.current.cardProps.handleCardBlur(mockEvent); + }); + + expect(result.current.cardProps.isCardValid).toBeDefined(); + }); + + it('handles expiry blur event', () => { + const Wrapper = createWrapper(); + const { result } = renderHook( + () => CommonCardProps.useCardForm(mockLogger, 'card'), + { wrapper: Wrapper } + ); + + act(() => { + const mockEvent = { + target: { value: '12/25' }, + } as React.FocusEvent; + result.current.expiryProps.handleExpiryBlur(mockEvent); + }); + + expect(result.current.expiryProps.isExpiryValid).toBeDefined(); + }); + + it('handles CVC blur event', () => { + const Wrapper = createWrapper(); + const { result } = renderHook( + () => CommonCardProps.useCardForm(mockLogger, 'card'), + { wrapper: Wrapper } + ); + + act(() => { + const mockEvent = { + target: { value: '123' }, + } as React.FocusEvent; + result.current.cvcProps.handleCVCBlur(mockEvent); + }); + + expect(result.current.cvcProps.isCVCValid).toBeDefined(); + }); + + it('handles zip blur event', () => { + const Wrapper = createWrapper(); + const { result } = renderHook( + () => CommonCardProps.useCardForm(mockLogger, 'card'), + { wrapper: Wrapper } + ); + + act(() => { + const mockEvent = { + target: { value: '12345' }, + } as React.FocusEvent; + result.current.zipProps.handleZipBlur(mockEvent); + }); + + expect(result.current.zipProps.isZipValid).toBe(true); + }); + + it('sets zip validity to false when zip is empty', () => { + const Wrapper = createWrapper(); + const { result } = renderHook( + () => CommonCardProps.useCardForm(mockLogger, 'card'), + { wrapper: Wrapper } + ); + + act(() => { + const mockEvent = { + target: { value: '' }, + } as React.FocusEvent; + result.current.zipProps.handleZipBlur(mockEvent); + }); + + expect(result.current.zipProps.isZipValid).toBe(false); + }); + + it('detects Visa card brand from number', () => { + const Wrapper = createWrapper(); + const { result } = renderHook( + () => CommonCardProps.useCardForm(mockLogger, 'card'), + { wrapper: Wrapper } + ); + + act(() => { + const mockEvent = { + target: { value: '4111111111111111' }, + } as React.ChangeEvent; + result.current.cardProps.changeCardNumber(mockEvent); + }); + + expect(result.current.cardProps.cardBrand).toBeDefined(); + }); + + it('detects Mastercard card brand from number', () => { + const Wrapper = createWrapper(); + const { result } = renderHook( + () => CommonCardProps.useCardForm(mockLogger, 'card'), + { wrapper: Wrapper } + ); + + act(() => { + const mockEvent = { + target: { value: '5555555555554444' }, + } as React.ChangeEvent; + result.current.cardProps.changeCardNumber(mockEvent); + }); + + expect(result.current.cardProps.cardBrand).toBeDefined(); + }); + + it('returns icon component', () => { + const Wrapper = createWrapper(); + const { result } = renderHook( + () => CommonCardProps.useCardForm(mockLogger, 'card'), + { wrapper: Wrapper } + ); + + expect(result.current.cardProps.icon).toBeDefined(); + }); + + it('handles empty card number', () => { + const Wrapper = createWrapper(); + const { result } = renderHook( + () => CommonCardProps.useCardForm(mockLogger, 'card'), + { wrapper: Wrapper } + ); + + act(() => { + const mockEvent = { + target: { value: '' }, + } as React.ChangeEvent; + result.current.cardProps.changeCardNumber(mockEvent); + }); + + expect(result.current.cardProps.cardNumber).toBe(''); + }); + + it('handles CVC key down event', () => { + const Wrapper = createWrapper(); + const { result } = renderHook( + () => CommonCardProps.useCardForm(mockLogger, 'card'), + { wrapper: Wrapper } + ); + + act(() => { + const mockEvent = { + key: 'Backspace', + preventDefault: jest.fn(), + } as unknown as React.KeyboardEvent; + result.current.cvcProps.onCvcKeyDown(mockEvent); + }); + + expect(result.current.cvcProps).toBeDefined(); + }); + + it('handles expiry key down event', () => { + const Wrapper = createWrapper(); + const { result } = renderHook( + () => CommonCardProps.useCardForm(mockLogger, 'card'), + { wrapper: Wrapper } + ); + + act(() => { + const mockEvent = { + key: 'Backspace', + preventDefault: jest.fn(), + } as unknown as React.KeyboardEvent; + result.current.expiryProps.onExpiryKeyDown(mockEvent); + }); + + expect(result.current.expiryProps).toBeDefined(); + }); + + it('handles zip code key down event', () => { + const Wrapper = createWrapper(); + const { result } = renderHook( + () => CommonCardProps.useCardForm(mockLogger, 'card'), + { wrapper: Wrapper } + ); + + act(() => { + const mockEvent = { + key: 'Backspace', + preventDefault: jest.fn(), + } as unknown as React.KeyboardEvent; + result.current.zipProps.onZipCodeKeyDown(mockEvent); + }); + + expect(result.current.zipProps).toBeDefined(); + }); +}); diff --git a/src/__tests__/CommonHooks.test.ts b/src/__tests__/CommonHooks.test.ts new file mode 100644 index 000000000..b18c56c26 --- /dev/null +++ b/src/__tests__/CommonHooks.test.ts @@ -0,0 +1,575 @@ +import { renderHook, act } from '@testing-library/react'; +import * as CommonHooks from "../Hooks/CommonHooks.bs.js"; + +describe("CommonHooks", () => { + describe("useScript", () => { + let createElementSpy: jest.SpyInstance; + let querySelectorSpy: jest.SpyInstance; + let appendChildSpy: jest.SpyInstance; + let mockScriptElement: any; + let container: HTMLDivElement; + + beforeEach(() => { + container = document.createElement('div'); + document.body.appendChild(container); + + mockScriptElement = { + src: '', + type: '', + async: false, + setAttribute: jest.fn(), + getAttribute: jest.fn(), + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + remove: jest.fn(), + }; + + const originalCreateElement = document.createElement.bind(document); + createElementSpy = jest.spyOn(document, 'createElement').mockImplementation((tagName: string) => { + if (tagName === 'script') return mockScriptElement; + return originalCreateElement(tagName); + }); + querySelectorSpy = jest.spyOn(document, 'querySelector').mockReturnValue(null); + appendChildSpy = jest.spyOn(document.body, 'appendChild').mockImplementation(() => mockScriptElement); + }); + + afterEach(() => { + if (container.parentNode) { + container.parentNode.removeChild(container); + } + createElementSpy.mockRestore(); + querySelectorSpy.mockRestore(); + appendChildSpy.mockRestore(); + jest.clearAllMocks(); + }); + + it("returns 'idle' when src is empty", () => { + const { result } = renderHook(() => CommonHooks.useScript(""), { container }); + expect(result.current).toBe("idle"); + }); + + it("returns 'loading' when src is provided and script doesn't exist", () => { + querySelectorSpy.mockReturnValue(null); + const { result } = renderHook(() => CommonHooks.useScript("https://example.com/script.js"), { container }); + expect(result.current).toBe("loading"); + }); + + it("creates script element with correct attributes", () => { + renderHook(() => CommonHooks.useScript("https://example.com/script.js"), { container }); + + expect(createElementSpy).toHaveBeenCalledWith("script"); + expect(mockScriptElement.src).toBe("https://example.com/script.js"); + expect(mockScriptElement.async).toBe(true); + expect(mockScriptElement.setAttribute).toHaveBeenCalledWith("data-status", "loading"); + }); + + it("sets script type when provided", () => { + renderHook(() => CommonHooks.useScript("https://example.com/script.js", "module"), { container }); + + expect(mockScriptElement.type).toBe("module"); + }); + + it("does not set script type when not provided", () => { + renderHook(() => CommonHooks.useScript("https://example.com/script.js"), { container }); + + expect(mockScriptElement.type).toBe(""); + }); + + it("appends script to document body", () => { + renderHook(() => CommonHooks.useScript("https://example.com/script.js"), { container }); + + expect(appendChildSpy).toHaveBeenCalledWith(mockScriptElement); + }); + + it("returns existing script status when script already exists", () => { + const existingScript = { + getAttribute: jest.fn().mockReturnValue("ready"), + }; + querySelectorSpy.mockReturnValue(existingScript); + + const { result } = renderHook(() => CommonHooks.useScript("https://example.com/script.js"), { container }); + + expect(result.current).toBe("ready"); + expect(createElementSpy).not.toHaveBeenCalled(); + }); + + it("sets up load event listener", () => { + renderHook(() => CommonHooks.useScript("https://example.com/script.js"), { container }); + + expect(mockScriptElement.addEventListener).toHaveBeenCalledWith("load", expect.any(Function)); + }); + + it("sets up error event listener", () => { + renderHook(() => CommonHooks.useScript("https://example.com/script.js"), { container }); + + expect(mockScriptElement.addEventListener).toHaveBeenCalledWith("error", expect.any(Function)); + }); + + it("updates status to 'ready' on load event", () => { + let loadHandler: Function | null = null; + mockScriptElement.addEventListener.mockImplementation((event: string, handler: Function) => { + if (event === 'load') loadHandler = handler; + }); + + const { result } = renderHook(() => CommonHooks.useScript("https://example.com/script.js"), { container }); + + act(() => { + loadHandler?.({ type: 'load' }); + }); + + expect(result.current).toBe("ready"); + }); + + it("updates status to 'error' on error event", () => { + let errorHandler: Function | null = null; + mockScriptElement.addEventListener.mockImplementation((event: string, handler: Function) => { + if (event === 'error') errorHandler = handler; + }); + + const { result } = renderHook(() => CommonHooks.useScript("https://example.com/script.js"), { container }); + + act(() => { + errorHandler?.({ type: 'error' }); + }); + + expect(result.current).toBe("error"); + }); + + it("removes event listeners on unmount", () => { + const { unmount } = renderHook(() => CommonHooks.useScript("https://example.com/script.js"), { container }); + + unmount(); + + expect(mockScriptElement.removeEventListener).toHaveBeenCalledWith("load", expect.any(Function)); + expect(mockScriptElement.removeEventListener).toHaveBeenCalledWith("error", expect.any(Function)); + }); + + it("removes script on unmount if not ready", () => { + mockScriptElement.getAttribute.mockReturnValue("error"); + + const { unmount } = renderHook(() => CommonHooks.useScript("https://example.com/script.js"), { container }); + + unmount(); + + expect(mockScriptElement.remove).toHaveBeenCalled(); + }); + + it("does not remove script on unmount if ready", () => { + mockScriptElement.getAttribute.mockReturnValue("ready"); + + const { unmount } = renderHook(() => CommonHooks.useScript("https://example.com/script.js"), { container }); + + unmount(); + + expect(mockScriptElement.remove).not.toHaveBeenCalled(); + }); + + it("sets data-status attribute on load", () => { + let loadHandler: Function | null = null; + mockScriptElement.addEventListener.mockImplementation((event: string, handler: Function) => { + if (event === 'load') loadHandler = handler; + }); + + renderHook(() => CommonHooks.useScript("https://example.com/script.js"), { container }); + + act(() => { + loadHandler?.({ type: 'load' }); + }); + + expect(mockScriptElement.setAttribute).toHaveBeenCalledWith("data-status", "ready"); + }); + + it("sets data-status attribute on error", () => { + let errorHandler: Function | null = null; + mockScriptElement.addEventListener.mockImplementation((event: string, handler: Function) => { + if (event === 'error') errorHandler = handler; + }); + + renderHook(() => CommonHooks.useScript("https://example.com/script.js"), { container }); + + act(() => { + errorHandler?.({ type: 'error' }); + }); + + expect(mockScriptElement.setAttribute).toHaveBeenCalledWith("data-status", "error"); + }); + }); + + describe("useLink", () => { + let createElementSpy: jest.SpyInstance; + let querySelectorSpy: jest.SpyInstance; + let appendChildSpy: jest.SpyInstance; + let mockLinkElement: any; + let container: HTMLDivElement; + + beforeEach(() => { + container = document.createElement('div'); + document.body.appendChild(container); + + mockLinkElement = { + href: '', + rel: '', + setAttribute: jest.fn(), + getAttribute: jest.fn(), + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + }; + + const originalCreateElement = document.createElement.bind(document); + createElementSpy = jest.spyOn(document, 'createElement').mockImplementation((tagName: string) => { + if (tagName === 'link') return mockLinkElement; + return originalCreateElement(tagName); + }); + querySelectorSpy = jest.spyOn(document, 'querySelector').mockReturnValue(null); + appendChildSpy = jest.spyOn(document.body, 'appendChild').mockImplementation(() => mockLinkElement); + }); + + afterEach(() => { + if (container.parentNode) { + container.parentNode.removeChild(container); + } + createElementSpy.mockRestore(); + querySelectorSpy.mockRestore(); + appendChildSpy.mockRestore(); + jest.clearAllMocks(); + }); + + it("returns 'idle' when src is empty", () => { + const { result } = renderHook(() => CommonHooks.useLink(""), { container }); + expect(result.current).toBe("idle"); + }); + + it("returns 'loading' when src is provided and link doesn't exist", () => { + querySelectorSpy.mockReturnValue(null); + const { result } = renderHook(() => CommonHooks.useLink("https://example.com/styles.css"), { container }); + expect(result.current).toBe("loading"); + }); + + it("creates link element with correct attributes", () => { + renderHook(() => CommonHooks.useLink("https://example.com/styles.css"), { container }); + + expect(createElementSpy).toHaveBeenCalledWith("link"); + expect(mockLinkElement.href).toBe("https://example.com/styles.css"); + expect(mockLinkElement.rel).toBe("stylesheet"); + expect(mockLinkElement.setAttribute).toHaveBeenCalledWith("data-status", "loading"); + }); + + it("appends link to document body", () => { + renderHook(() => CommonHooks.useLink("https://example.com/styles.css"), { container }); + + expect(appendChildSpy).toHaveBeenCalledWith(mockLinkElement); + }); + + it("returns existing link status when link already exists", () => { + const existingLink = { + getAttribute: jest.fn().mockReturnValue("ready"), + }; + querySelectorSpy.mockReturnValue(existingLink); + + const { result } = renderHook(() => CommonHooks.useLink("https://example.com/styles.css"), { container }); + + expect(result.current).toBe("ready"); + expect(createElementSpy).not.toHaveBeenCalled(); + }); + + it("sets up load event listener", () => { + renderHook(() => CommonHooks.useLink("https://example.com/styles.css"), { container }); + + expect(mockLinkElement.addEventListener).toHaveBeenCalledWith("load", expect.any(Function)); + }); + + it("sets up error event listener", () => { + renderHook(() => CommonHooks.useLink("https://example.com/styles.css"), { container }); + + expect(mockLinkElement.addEventListener).toHaveBeenCalledWith("error", expect.any(Function)); + }); + + it("updates status to 'ready' on load event", () => { + let loadHandler: Function | null = null; + mockLinkElement.addEventListener.mockImplementation((event: string, handler: Function) => { + if (event === 'load') loadHandler = handler; + }); + + const { result } = renderHook(() => CommonHooks.useLink("https://example.com/styles.css"), { container }); + + act(() => { + loadHandler?.({ type: 'load' }); + }); + + expect(result.current).toBe("ready"); + }); + + it("updates status to 'error' on error event", () => { + let errorHandler: Function | null = null; + mockLinkElement.addEventListener.mockImplementation((event: string, handler: Function) => { + if (event === 'error') errorHandler = handler; + }); + + const { result } = renderHook(() => CommonHooks.useLink("https://example.com/styles.css"), { container }); + + act(() => { + errorHandler?.({ type: 'error' }); + }); + + expect(result.current).toBe("error"); + }); + + it("removes event listeners on unmount", () => { + const { unmount } = renderHook(() => CommonHooks.useLink("https://example.com/styles.css"), { container }); + + unmount(); + + expect(mockLinkElement.removeEventListener).toHaveBeenCalledWith("load", expect.any(Function)); + expect(mockLinkElement.removeEventListener).toHaveBeenCalledWith("error", expect.any(Function)); + }); + + it("sets data-status attribute on load", () => { + let loadHandler: Function | null = null; + mockLinkElement.addEventListener.mockImplementation((event: string, handler: Function) => { + if (event === 'load') loadHandler = handler; + }); + + renderHook(() => CommonHooks.useLink("https://example.com/styles.css"), { container }); + + act(() => { + loadHandler?.({ type: 'load' }); + }); + + expect(mockLinkElement.setAttribute).toHaveBeenCalledWith("data-status", "ready"); + }); + + it("sets data-status attribute on error", () => { + let errorHandler: Function | null = null; + mockLinkElement.addEventListener.mockImplementation((event: string, handler: Function) => { + if (event === 'error') errorHandler = handler; + }); + + renderHook(() => CommonHooks.useLink("https://example.com/styles.css"), { container }); + + act(() => { + errorHandler?.({ type: 'error' }); + }); + + expect(mockLinkElement.setAttribute).toHaveBeenCalledWith("data-status", "error"); + }); + }); + + describe("updateKeys", () => { + it("updates paymentId when key is 'paymentId' and dict has the key", () => { + const dict = { paymentId: "pay_12345" }; + const keyPair = ["paymentId", "pay_12345"]; + let updatedState: any = null; + const setKeys = (fn: (prev: any) => any) => { + updatedState = fn({ + paymentId: "", + publishableKey: "", + profileId: "", + iframeId: "", + parentURL: "*", + sdkHandleOneClickConfirmPayment: true, + }); + }; + + CommonHooks.updateKeys(dict, keyPair, setKeys); + + expect(updatedState).not.toBeNull(); + expect(updatedState.paymentId).toBe("pay_12345"); + }); + + it("updates publishableKey when key is 'publishableKey' and dict has the key", () => { + const dict = { publishableKey: "pk_test_123" }; + const keyPair = ["publishableKey", "pk_test_123"]; + let updatedState: any = null; + const setKeys = (fn: (prev: any) => any) => { + updatedState = fn({ + paymentId: "", + publishableKey: "", + profileId: "", + iframeId: "", + parentURL: "*", + sdkHandleOneClickConfirmPayment: true, + }); + }; + + CommonHooks.updateKeys(dict, keyPair, setKeys); + + expect(updatedState).not.toBeNull(); + expect(updatedState.publishableKey).toBe("pk_test_123"); + }); + + it("updates profileId when key is 'profileId' and dict has the key", () => { + const dict = { profileId: "prof_123" }; + const keyPair = ["profileId", "prof_123"]; + let updatedState: any = null; + const setKeys = (fn: (prev: any) => any) => { + updatedState = fn({ + paymentId: "", + publishableKey: "", + profileId: "", + iframeId: "", + parentURL: "*", + sdkHandleOneClickConfirmPayment: true, + }); + }; + + CommonHooks.updateKeys(dict, keyPair, setKeys); + + expect(updatedState).not.toBeNull(); + expect(updatedState.profileId).toBe("prof_123"); + }); + + it("updates iframeId when key is 'iframeId' and dict has the key", () => { + const dict = { iframeId: "iframe_123" }; + const keyPair = ["iframeId", "iframe_123"]; + let updatedState: any = null; + const setKeys = (fn: (prev: any) => any) => { + updatedState = fn({ + paymentId: "", + publishableKey: "", + profileId: "", + iframeId: "", + parentURL: "*", + sdkHandleOneClickConfirmPayment: true, + }); + }; + + CommonHooks.updateKeys(dict, keyPair, setKeys); + + expect(updatedState).not.toBeNull(); + expect(updatedState.iframeId).toBe("iframe_123"); + }); + + it("updates parentURL when key is 'parentURL' and dict has the key", () => { + const dict = { parentURL: "https://example.com" }; + const keyPair = ["parentURL", "https://example.com"]; + let updatedState: any = null; + const setKeys = (fn: (prev: any) => any) => { + updatedState = fn({ + paymentId: "", + publishableKey: "", + profileId: "", + iframeId: "", + parentURL: "*", + sdkHandleOneClickConfirmPayment: true, + }); + }; + + CommonHooks.updateKeys(dict, keyPair, setKeys); + + expect(updatedState).not.toBeNull(); + expect(updatedState.parentURL).toBe("https://example.com"); + }); + + it("updates sdkHandleOneClickConfirmPayment when key is 'sdkHandleOneClickConfirmPayment' and dict has the key", () => { + const dict = { sdkHandleOneClickConfirmPayment: false }; + const keyPair = ["sdkHandleOneClickConfirmPayment", false]; + let updatedState: any = null; + const setKeys = (fn: (prev: any) => any) => { + updatedState = fn({ + paymentId: "", + publishableKey: "", + profileId: "", + iframeId: "", + parentURL: "*", + sdkHandleOneClickConfirmPayment: true, + }); + }; + + CommonHooks.updateKeys(dict, keyPair, setKeys); + + expect(updatedState).not.toBeNull(); + expect(updatedState.sdkHandleOneClickConfirmPayment).toBe(false); + }); + + it("does not update state when key is not in dict", () => { + const dict = {}; + const keyPair = ["paymentId", "pay_12345"]; + let called = false; + const setKeys = () => { + called = true; + }; + + CommonHooks.updateKeys(dict, keyPair, setKeys); + + expect(called).toBe(false); + }); + + it("does not update state for unknown keys", () => { + const dict = { unknownKey: "value" }; + const keyPair = ["unknownKey", "value"]; + let called = false; + const setKeys = () => { + called = true; + }; + + CommonHooks.updateKeys(dict, keyPair, setKeys); + + expect(called).toBe(false); + }); + + it("preserves other state fields when updating one field", () => { + const dict = { paymentId: "pay_12345" }; + const keyPair = ["paymentId", "pay_12345"]; + let updatedState: any = null; + const setKeys = (fn: (prev: any) => any) => { + updatedState = fn({ + paymentId: "old_payment", + publishableKey: "pk_old", + profileId: "prof_old", + iframeId: "iframe_old", + parentURL: "https://old.com", + sdkHandleOneClickConfirmPayment: false, + }); + }; + + CommonHooks.updateKeys(dict, keyPair, setKeys); + + expect(updatedState.paymentId).toBe("pay_12345"); + expect(updatedState.publishableKey).toBe("pk_old"); + expect(updatedState.profileId).toBe("prof_old"); + expect(updatedState.iframeId).toBe("iframe_old"); + expect(updatedState.parentURL).toBe("https://old.com"); + expect(updatedState.sdkHandleOneClickConfirmPayment).toBe(false); + }); + + it("updates sdkHandleOneClickConfirmPayment to true when value is true", () => { + const dict = { sdkHandleOneClickConfirmPayment: true }; + const keyPair = ["sdkHandleOneClickConfirmPayment", true]; + let updatedState: any = null; + const setKeys = (fn: (prev: any) => any) => { + updatedState = fn({ + paymentId: "", + publishableKey: "", + profileId: "", + iframeId: "", + parentURL: "*", + sdkHandleOneClickConfirmPayment: false, + }); + }; + + CommonHooks.updateKeys(dict, keyPair, setKeys); + + expect(updatedState.sdkHandleOneClickConfirmPayment).toBe(true); + }); + }); + + describe("defaultkeys", () => { + it("has expected default values", () => { + expect(CommonHooks.defaultkeys.paymentId).toBe(""); + expect(CommonHooks.defaultkeys.publishableKey).toBe(""); + expect(CommonHooks.defaultkeys.profileId).toBe(""); + expect(CommonHooks.defaultkeys.iframeId).toBe(""); + expect(CommonHooks.defaultkeys.parentURL).toBe("*"); + expect(CommonHooks.defaultkeys.sdkHandleOneClickConfirmPayment).toBe(true); + }); + + it("is an object with all required keys", () => { + expect(CommonHooks.defaultkeys).toHaveProperty("paymentId"); + expect(CommonHooks.defaultkeys).toHaveProperty("publishableKey"); + expect(CommonHooks.defaultkeys).toHaveProperty("profileId"); + expect(CommonHooks.defaultkeys).toHaveProperty("iframeId"); + expect(CommonHooks.defaultkeys).toHaveProperty("parentURL"); + expect(CommonHooks.defaultkeys).toHaveProperty("sdkHandleOneClickConfirmPayment"); + }); + }); +}); diff --git a/src/__tests__/ConfirmType.test.ts b/src/__tests__/ConfirmType.test.ts new file mode 100644 index 000000000..aea4eb419 --- /dev/null +++ b/src/__tests__/ConfirmType.test.ts @@ -0,0 +1,117 @@ +import { + defaultConfirm, + getConfirmParams, + itemToObjMapper, +} from '../Types/ConfirmType.bs.js'; + +describe('ConfirmType', () => { + describe('defaultConfirm', () => { + it('should have correct default values', () => { + expect(defaultConfirm.return_url).toBe(''); + expect(defaultConfirm.publishableKey).toBe(''); + expect(defaultConfirm.redirect).toBe('if_required'); + }); + }); + + describe('getConfirmParams', () => { + it('should extract confirm params from dict', () => { + const dict = { + confirmParams: { + return_url: 'https://example.com/return', + publishableKey: 'pk_test_123', + redirect: 'if_required', + }, + }; + const result = getConfirmParams(dict, 'confirmParams'); + expect(result.return_url).toBe('https://example.com/return'); + expect(result.publishableKey).toBe('pk_test_123'); + expect(result.redirect).toBe('if_required'); + }); + + it('should return default values when key not found', () => { + const dict = {}; + const result = getConfirmParams(dict, 'confirmParams'); + expect(result).toEqual(defaultConfirm); + }); + + it('should return default values when value is null', () => { + const dict = { confirmParams: null }; + const result = getConfirmParams(dict, 'confirmParams'); + expect(result).toEqual(defaultConfirm); + }); + + it('should use default redirect value when not specified', () => { + const dict = { + confirmParams: { + return_url: 'https://example.com/return', + }, + }; + const result = getConfirmParams(dict, 'confirmParams'); + expect(result.return_url).toBe('https://example.com/return'); + expect(result.redirect).toBe('if_required'); + }); + + it('should handle empty return_url', () => { + const dict = { + confirmParams: { + return_url: '', + publishableKey: 'pk_test_123', + }, + }; + const result = getConfirmParams(dict, 'confirmParams'); + expect(result.return_url).toBe(''); + expect(result.publishableKey).toBe('pk_test_123'); + }); + }); + + describe('itemToObjMapper', () => { + it('should map dict to confirm object', () => { + const dict = { + doSubmit: true, + clientSecret: 'secret_123', + confirmParams: { + return_url: 'https://example.com/return', + publishableKey: 'pk_test_123', + redirect: 'if_required', + }, + confirmTimestamp: 1234567890.123, + readyTimestamp: 1234567880.0, + }; + const result = itemToObjMapper(dict); + expect(result.doSubmit).toBe(true); + expect(result.clientSecret).toBe('secret_123'); + expect(result.confirmParams.return_url).toBe('https://example.com/return'); + expect(result.confirmTimestamp).toBe(1234567890.123); + expect(result.readyTimestamp).toBe(1234567880.0); + }); + + it('should use default values for missing fields', () => { + const dict = {}; + const result = itemToObjMapper(dict); + expect(result.doSubmit).toBe(false); + expect(result.clientSecret).toBe(''); + expect(result.confirmParams).toEqual(defaultConfirm); + expect(result.confirmTimestamp).toBe(0.0); + expect(result.readyTimestamp).toBe(0.0); + }); + + it('should handle partial confirm params', () => { + const dict = { + clientSecret: 'secret_456', + }; + const result = itemToObjMapper(dict); + expect(result.clientSecret).toBe('secret_456'); + expect(result.doSubmit).toBe(false); + }); + + it('should handle zero timestamps', () => { + const dict = { + confirmTimestamp: 0, + readyTimestamp: 0, + }; + const result = itemToObjMapper(dict); + expect(result.confirmTimestamp).toBe(0); + expect(result.readyTimestamp).toBe(0); + }); + }); +}); diff --git a/src/__tests__/CustomPaymentMethodsConfig.test.ts b/src/__tests__/CustomPaymentMethodsConfig.test.ts new file mode 100644 index 000000000..8a9fca37c --- /dev/null +++ b/src/__tests__/CustomPaymentMethodsConfig.test.ts @@ -0,0 +1,374 @@ +import { renderHook } from '@testing-library/react'; +import { RecoilRoot } from 'recoil'; +import * as CustomPaymentMethodsConfig from '../Hooks/CustomPaymentMethodsConfig.bs.js'; +import * as RecoilAtoms from '../Utilities/RecoilAtoms.bs.js'; +import * as React from 'react'; + +describe('useCustomPaymentMethodConfigs', () => { + const mockConfigWithPaymentMethods = { + paymentMethodsConfig: [ + { + paymentMethod: 'card', + paymentMethodTypes: [ + { + paymentMethodType: 'credit', + displayName: 'Credit Card', + }, + { + paymentMethodType: 'debit', + displayName: 'Debit Card', + }, + ], + }, + { + paymentMethod: 'wallet', + paymentMethodTypes: [ + { + paymentMethodType: 'paypal', + displayName: 'PayPal', + }, + { + paymentMethodType: 'google_pay', + displayName: 'Google Pay', + }, + ], + }, + ], + }; + + const mockConfigDebitFirst = { + paymentMethodsConfig: [ + { + paymentMethod: 'card', + paymentMethodTypes: [ + { + paymentMethodType: 'debit', + displayName: 'Debit Card', + }, + { + paymentMethodType: 'credit', + displayName: 'Credit Card', + }, + ], + }, + ], + }; + + const createWrapper = (config: any) => { + return function Wrapper({ children }: { children: React.ReactNode }) { + return React.createElement( + RecoilRoot, + { + initializeState: ({ set }: any) => { + set(RecoilAtoms.optionAtom, config); + }, + }, + children + ); + }; + }; + + it('returns the first allowed card payment method type when searching for card', () => { + const Wrapper = createWrapper(mockConfigWithPaymentMethods); + const { result } = renderHook( + () => CustomPaymentMethodsConfig.useCustomPaymentMethodConfigs('card', 'debit'), + { + wrapper: Wrapper, + } + ); + + expect(result.current).toEqual({ + paymentMethodType: 'credit', + displayName: 'Credit Card', + }); + }); + + it('returns debit when it appears first in the config', () => { + const Wrapper = createWrapper(mockConfigDebitFirst); + const { result } = renderHook( + () => CustomPaymentMethodsConfig.useCustomPaymentMethodConfigs('card', 'debit'), + { + wrapper: Wrapper, + } + ); + + expect(result.current).toEqual({ + paymentMethodType: 'debit', + displayName: 'Debit Card', + }); + }); + + it('returns the matching payment method type config for non-card payment methods', () => { + const Wrapper = createWrapper(mockConfigWithPaymentMethods); + const { result } = renderHook( + () => CustomPaymentMethodsConfig.useCustomPaymentMethodConfigs('wallet', 'paypal'), + { + wrapper: Wrapper, + } + ); + + expect(result.current).toEqual({ + paymentMethodType: 'paypal', + displayName: 'PayPal', + }); + }); + + it('returns undefined when payment method is not found', () => { + const Wrapper = createWrapper(mockConfigWithPaymentMethods); + const { result } = renderHook( + () => CustomPaymentMethodsConfig.useCustomPaymentMethodConfigs('crypto', 'bitcoin'), + { + wrapper: Wrapper, + } + ); + + expect(result.current).toBeUndefined(); + }); + + it('returns undefined when payment method type is not found for non-card methods', () => { + const Wrapper = createWrapper(mockConfigWithPaymentMethods); + const { result } = renderHook( + () => CustomPaymentMethodsConfig.useCustomPaymentMethodConfigs('wallet', 'apple_pay'), + { + wrapper: Wrapper, + } + ); + + expect(result.current).toBeUndefined(); + }); + + it('handles empty paymentMethodsConfig array', () => { + const emptyConfig = { paymentMethodsConfig: [] }; + const Wrapper = createWrapper(emptyConfig); + const { result } = renderHook( + () => CustomPaymentMethodsConfig.useCustomPaymentMethodConfigs('card', 'debit'), + { + wrapper: Wrapper, + } + ); + + expect(result.current).toBeUndefined(); + }); + + it('handles paymentMethodsConfig with empty paymentMethodTypes', () => { + const configWithEmptyTypes = { + paymentMethodsConfig: [ + { + paymentMethod: 'card', + paymentMethodTypes: [], + }, + ], + }; + const Wrapper = createWrapper(configWithEmptyTypes); + const { result } = renderHook( + () => CustomPaymentMethodsConfig.useCustomPaymentMethodConfigs('card', 'debit'), + { + wrapper: Wrapper, + } + ); + + expect(result.current).toBeUndefined(); + }); + + it('filters card payment method types to only allow debit and credit', () => { + const configWithExtraTypes = { + paymentMethodsConfig: [ + { + paymentMethod: 'card', + paymentMethodTypes: [ + { + paymentMethodType: 'prepaid', + displayName: 'Prepaid Card', + }, + { + paymentMethodType: 'credit', + displayName: 'Credit Card', + }, + ], + }, + ], + }; + const Wrapper = createWrapper(configWithExtraTypes); + const { result } = renderHook( + () => CustomPaymentMethodsConfig.useCustomPaymentMethodConfigs('card', 'credit'), + { + wrapper: Wrapper, + } + ); + expect(result.current).toEqual({ + paymentMethodType: 'credit', + displayName: 'Credit Card', + }); + }); + + it('returns undefined for prepaid card type which is not allowed', () => { + const configWithPrepaid = { + paymentMethodsConfig: [ + { + paymentMethod: 'card', + paymentMethodTypes: [ + { + paymentMethodType: 'prepaid', + displayName: 'Prepaid Card', + }, + ], + }, + ], + }; + const Wrapper = createWrapper(configWithPrepaid); + const { result } = renderHook( + () => CustomPaymentMethodsConfig.useCustomPaymentMethodConfigs('card', 'prepaid'), + { + wrapper: Wrapper, + } + ); + expect(result.current).toBeUndefined(); + }); + + it('recomputes when paymentMethod changes', () => { + const Wrapper = createWrapper(mockConfigWithPaymentMethods); + const { result, rerender } = renderHook( + (props) => CustomPaymentMethodsConfig.useCustomPaymentMethodConfigs(props.paymentMethod, 'debit'), + { + wrapper: Wrapper, + initialProps: { paymentMethod: 'card' }, + } + ); + + expect(result.current).toEqual({ + paymentMethodType: 'credit', + displayName: 'Credit Card', + }); + + rerender({ paymentMethod: 'wallet' }); + + expect(result.current).toBeUndefined(); + }); + + it('recomputes when paymentMethodType changes for non-card methods', () => { + const Wrapper = createWrapper(mockConfigWithPaymentMethods); + const { result, rerender } = renderHook( + (props) => CustomPaymentMethodsConfig.useCustomPaymentMethodConfigs('wallet', props.paymentMethodType), + { + wrapper: Wrapper, + initialProps: { paymentMethodType: 'paypal' }, + } + ); + + expect(result.current).toEqual({ + paymentMethodType: 'paypal', + displayName: 'PayPal', + }); + + rerender({ paymentMethodType: 'google_pay' }); + + expect(result.current).toEqual({ + paymentMethodType: 'google_pay', + displayName: 'Google Pay', + }); + }); + + it('returns first matching payment method type config when multiple exist', () => { + const configWithDuplicates = { + paymentMethodsConfig: [ + { + paymentMethod: 'card', + paymentMethodTypes: [ + { + paymentMethodType: 'credit', + displayName: 'Credit Card 1', + }, + ], + }, + { + paymentMethod: 'card', + paymentMethodTypes: [ + { + paymentMethodType: 'credit', + displayName: 'Credit Card 2', + }, + ], + }, + ], + }; + const Wrapper = createWrapper(configWithDuplicates); + const { result } = renderHook( + () => CustomPaymentMethodsConfig.useCustomPaymentMethodConfigs('card', 'credit'), + { + wrapper: Wrapper, + } + ); + + expect(result.current).toEqual({ + paymentMethodType: 'credit', + displayName: 'Credit Card 1', + }); + }); + + it('handles complex nested config structure', () => { + const complexConfig = { + paymentMethodsConfig: [ + { + paymentMethod: 'card', + paymentMethodTypes: [ + { + paymentMethodType: 'credit', + displayName: 'Credit Card', + requiredFields: ['name', 'number', 'expiry', 'cvv'], + }, + { + paymentMethodType: 'debit', + displayName: 'Debit Card', + requiredFields: ['name', 'number', 'expiry', 'cvv'], + }, + ], + }, + ], + }; + const Wrapper = createWrapper(complexConfig); + const { result } = renderHook( + () => CustomPaymentMethodsConfig.useCustomPaymentMethodConfigs('card', 'credit'), + { + wrapper: Wrapper, + } + ); + + expect(result.current).toBeDefined(); + expect(result.current.paymentMethodType).toBe('credit'); + expect(result.current.displayName).toBe('Credit Card'); + expect(result.current.requiredFields).toEqual(['name', 'number', 'expiry', 'cvv']); + }); + + it('memoizes results and only recalculates when dependencies change', () => { + let recoilSetCount = 0; + const Wrapper = createWrapper(mockConfigWithPaymentMethods); + + const { result, rerender } = renderHook( + () => CustomPaymentMethodsConfig.useCustomPaymentMethodConfigs('card', 'debit'), + { + wrapper: Wrapper, + } + ); + + const firstResult = result.current; + + rerender(); + + expect(result.current).toBe(firstResult); + }); + + it('handles wallet with google_pay type', () => { + const Wrapper = createWrapper(mockConfigWithPaymentMethods); + const { result } = renderHook( + () => CustomPaymentMethodsConfig.useCustomPaymentMethodConfigs('wallet', 'google_pay'), + { + wrapper: Wrapper, + } + ); + + expect(result.current).toEqual({ + paymentMethodType: 'google_pay', + displayName: 'Google Pay', + }); + }); +}); diff --git a/src/__tests__/DynamicFieldsUtils.test.ts b/src/__tests__/DynamicFieldsUtils.test.ts new file mode 100644 index 000000000..1a8afed4d --- /dev/null +++ b/src/__tests__/DynamicFieldsUtils.test.ts @@ -0,0 +1,1645 @@ +import { + getName, + isBillingAddressFieldType, + getBillingAddressPathFromFieldType, + removeBillingDetailsIfUseBillingAddress, + addBillingAddressIfUseBillingAddress, + isClickToPayFieldType, + removeClickToPayFieldsIfSaveDetailsWithClickToPay, + addClickToPayFieldsIfSaveDetailsWithClickToPay, + checkIfNameIsValid, + isFieldTypeToRenderOutsideBilling, + combineStateAndCity, + combineCountryAndPostal, + combineCardExpiryMonthAndYear, + combineCardExpiryAndCvc, + combinePhoneNumberAndCountryCode, + updateDynamicFields, + removeRequiredFieldsDuplicates, + getNameFromString, + getNameFromFirstAndLastName, + getApplePayRequiredFields, + getGooglePayRequiredFields, + getPaypalRequiredFields, + getKlarnaRequiredFields, + dynamicFieldsEnabledPaymentMethods, + useRequiredFieldsEmptyAndValid, + useSetInitialRequiredFields, + useRequiredFieldsBody, + useSubmitCallback, + usePaymentMethodTypeFromList, +} from '../Utilities/DynamicFieldsUtils.bs.js'; +import { renderHook, act } from '@testing-library/react'; +import { RecoilRoot } from 'recoil'; +import * as React from 'react'; +import * as RecoilAtoms from '../Utilities/RecoilAtoms.bs.js'; + +describe('DynamicFieldsUtils', () => { + describe('getName', () => { + it('should return first name for first_name required field', () => { + const item = { + required_field: 'payment_method_data.billing.address.first_name', + value: 'John Doe', + }; + const field = { value: 'John Doe' }; + const result = getName(item, field); + expect(result).toBe('John'); + }); + + it('should return last name for last_name required field', () => { + const item = { + required_field: 'payment_method_data.billing.address.last_name', + value: 'John Doe', + }; + const field = { value: 'John Doe' }; + const result = getName(item, field); + expect(result).toBe('Doe'); + }); + + it('should return field value for other required fields', () => { + const item = { + required_field: 'payment_method_data.email', + value: 'test@example.com', + }; + const field = { value: 'test@example.com' }; + const result = getName(item, field); + expect(result).toBe('test@example.com'); + }); + + it('should handle empty field value', () => { + const item = { + required_field: 'payment_method_data.billing.address.first_name', + value: '', + }; + const field = { value: '' }; + const result = getName(item, field); + expect(result).toBe(''); + }); + }); + + describe('isBillingAddressFieldType', () => { + it('should return true for BillingName', () => { + expect(isBillingAddressFieldType('BillingName')).toBe(true); + }); + + it('should return true for AddressLine1', () => { + expect(isBillingAddressFieldType('AddressLine1')).toBe(true); + }); + + it('should return true for AddressLine2', () => { + expect(isBillingAddressFieldType('AddressLine2')).toBe(true); + }); + + it('should return true for AddressCity', () => { + expect(isBillingAddressFieldType('AddressCity')).toBe(true); + }); + + it('should return true for AddressPincode', () => { + expect(isBillingAddressFieldType('AddressPincode')).toBe(true); + }); + + it('should return true for AddressState', () => { + expect(isBillingAddressFieldType('AddressState')).toBe(true); + }); + + it('should return true for AddressCountry object', () => { + expect(isBillingAddressFieldType({ TAG: 'AddressCountry', _0: ['US'] })).toBe(true); + }); + + it('should return false for non-billing field types', () => { + expect(isBillingAddressFieldType('Email')).toBe(false); + expect(isBillingAddressFieldType('FullName')).toBe(false); + expect(isBillingAddressFieldType('CardNumber')).toBe(false); + }); + }); + + describe('getBillingAddressPathFromFieldType', () => { + it('should return correct path for AddressLine1', () => { + expect(getBillingAddressPathFromFieldType('AddressLine1')).toBe('payment_method_data.billing.address.line1'); + }); + + it('should return correct path for AddressLine2', () => { + expect(getBillingAddressPathFromFieldType('AddressLine2')).toBe('payment_method_data.billing.address.line2'); + }); + + it('should return correct path for AddressCity', () => { + expect(getBillingAddressPathFromFieldType('AddressCity')).toBe('payment_method_data.billing.address.city'); + }); + + it('should return correct path for AddressPincode', () => { + expect(getBillingAddressPathFromFieldType('AddressPincode')).toBe('payment_method_data.billing.address.zip'); + }); + + it('should return correct path for AddressState', () => { + expect(getBillingAddressPathFromFieldType('AddressState')).toBe('payment_method_data.billing.address.state'); + }); + + it('should return correct path for AddressCountry', () => { + expect(getBillingAddressPathFromFieldType({ TAG: 'AddressCountry', _0: ['US'] })).toBe('payment_method_data.billing.address.country'); + }); + + it('should return empty string for non-billing fields', () => { + expect(getBillingAddressPathFromFieldType('Email')).toBe(''); + expect(getBillingAddressPathFromFieldType('FullName')).toBe(''); + }); + }); + + describe('removeBillingDetailsIfUseBillingAddress', () => { + const requiredFields = [ + { field_type: 'Email', required_field: 'email' }, + { field_type: 'BillingName', required_field: 'billing.name' }, + { field_type: 'AddressLine1', required_field: 'address.line1' }, + ]; + + it('should remove billing fields when isUseBillingAddress is true', () => { + const billingAddress = { isUseBillingAddress: true }; + const result = removeBillingDetailsIfUseBillingAddress(requiredFields, billingAddress); + expect(result.length).toBe(1); + expect(result[0].field_type).toBe('Email'); + }); + + it('should keep all fields when isUseBillingAddress is false', () => { + const billingAddress = { isUseBillingAddress: false }; + const result = removeBillingDetailsIfUseBillingAddress(requiredFields, billingAddress); + expect(result.length).toBe(3); + }); + + it('should handle empty required fields array', () => { + const billingAddress = { isUseBillingAddress: true }; + const result = removeBillingDetailsIfUseBillingAddress([], billingAddress); + expect(result).toEqual([]); + }); + }); + + describe('addBillingAddressIfUseBillingAddress', () => { + it('should add billing address fields when isUseBillingAddress is true', () => { + const fieldsArr = ['Email', 'FullName']; + const billingAddress = { isUseBillingAddress: true }; + const result = addBillingAddressIfUseBillingAddress(fieldsArr, billingAddress); + expect(result.length).toBeGreaterThan(fieldsArr.length); + }); + + it('should not add fields when isUseBillingAddress is false', () => { + const fieldsArr = ['Email', 'FullName']; + const billingAddress = { isUseBillingAddress: false }; + const result = addBillingAddressIfUseBillingAddress(fieldsArr, billingAddress); + expect(result.length).toBe(fieldsArr.length); + }); + + it('should handle empty fields array', () => { + const billingAddress = { isUseBillingAddress: true }; + const result = addBillingAddressIfUseBillingAddress([], billingAddress); + expect(result.length).toBeGreaterThan(0); + }); + }); + + describe('isClickToPayFieldType', () => { + it('should return true for Email', () => { + expect(isClickToPayFieldType('Email')).toBe(true); + }); + + it('should return true for PhoneNumber', () => { + expect(isClickToPayFieldType('PhoneNumber')).toBe(true); + }); + + it('should return false for other field types', () => { + expect(isClickToPayFieldType('FullName')).toBe(false); + expect(isClickToPayFieldType('CardNumber')).toBe(false); + expect(isClickToPayFieldType('AddressLine1')).toBe(false); + }); + + it('should return false for object field types', () => { + expect(isClickToPayFieldType({ TAG: 'AddressCountry', _0: [] })).toBe(false); + }); + }); + + describe('removeClickToPayFieldsIfSaveDetailsWithClickToPay', () => { + const requiredFields = [ + { field_type: 'Email', required_field: 'email' }, + { field_type: 'PhoneNumber', required_field: 'phone' }, + { field_type: 'FullName', required_field: 'name' }, + ]; + + it('should remove CTP fields when isSaveDetailsWithClickToPay is true', () => { + const result = removeClickToPayFieldsIfSaveDetailsWithClickToPay(requiredFields, true); + expect(result.length).toBe(1); + expect(result[0].field_type).toBe('FullName'); + }); + + it('should keep all fields when isSaveDetailsWithClickToPay is false', () => { + const result = removeClickToPayFieldsIfSaveDetailsWithClickToPay(requiredFields, false); + expect(result.length).toBe(3); + }); + + it('should handle empty required fields array', () => { + const result = removeClickToPayFieldsIfSaveDetailsWithClickToPay([], true); + expect(result).toEqual([]); + }); + }); + + describe('addClickToPayFieldsIfSaveDetailsWithClickToPay', () => { + const defaultConfig = { + clickToPayCards: [], + clickToPayProvider: 'NONE', + }; + + it('should add CTP fields for VISA provider when saving', () => { + const fieldsArr = ['FullName']; + const config = { clickToPayCards: [], clickToPayProvider: 'VISA' }; + const result = addClickToPayFieldsIfSaveDetailsWithClickToPay(fieldsArr, true, config); + expect(result).toContain('Email'); + expect(result).toContain('PhoneNumber'); + expect(result).toContain('FullName'); + }); + + it('should add CTP fields for MASTERCARD provider when saving', () => { + const fieldsArr = ['FullName']; + const config = { clickToPayCards: [], clickToPayProvider: 'MASTERCARD' }; + const result = addClickToPayFieldsIfSaveDetailsWithClickToPay(fieldsArr, true, config); + expect(result).toContain('Email'); + expect(result).toContain('PhoneNumber'); + }); + + it('should not add fields when not saving and NONE provider', () => { + const fieldsArr = ['FullName']; + const result = addClickToPayFieldsIfSaveDetailsWithClickToPay(fieldsArr, false, defaultConfig); + expect(result).toEqual(['FullName']); + }); + + it('should add fields for recognized CTP payment with VISA', () => { + const fieldsArr = ['FullName']; + const config = { clickToPayCards: ['card1'], clickToPayProvider: 'VISA' }; + const result = addClickToPayFieldsIfSaveDetailsWithClickToPay(fieldsArr, false, config); + expect(result).toContain('Email'); + expect(result).toContain('PhoneNumber'); + }); + }); + + describe('checkIfNameIsValid', () => { + it('should return true when first and last name are provided', () => { + const requiredFields = [ + { field_type: 'FullName', required_field: 'payment_method_data.billing.first_name', value: '' }, + { field_type: 'FullName', required_field: 'payment_method_data.billing.last_name', value: '' }, + ]; + const field = { value: 'John Doe' }; + const result = checkIfNameIsValid(requiredFields, 'FullName', field); + expect(result).toBe(true); + }); + + it('should return false when only first name is provided but last name is required', () => { + const requiredFields = [ + { field_type: 'FullName', required_field: 'payment_method_data.billing.first_name', value: '' }, + { field_type: 'FullName', required_field: 'payment_method_data.billing.last_name', value: '' }, + ]; + const field = { value: 'John' }; + const result = checkIfNameIsValid(requiredFields, 'FullName', field); + expect(result).toBe(false); + }); + + it('should return true when only first name is required', () => { + const requiredFields = [ + { field_type: 'FullName', required_field: 'payment_method_data.billing.first_name', value: '' }, + ]; + const field = { value: 'John' }; + const result = checkIfNameIsValid(requiredFields, 'FullName', field); + expect(result).toBe(true); + }); + + it('should return false when name is empty', () => { + const requiredFields = [ + { field_type: 'FullName', required_field: 'payment_method_data.billing.first_name', value: '' }, + ]; + const field = { value: '' }; + const result = checkIfNameIsValid(requiredFields, 'FullName', field); + expect(result).toBe(false); + }); + + it('should handle missing required fields', () => { + const field = { value: 'John' }; + const result = checkIfNameIsValid([], 'FullName', field); + expect(result).toBe(true); + }); + }); + + describe('isFieldTypeToRenderOutsideBilling', () => { + it('should return true for card-related fields', () => { + expect(isFieldTypeToRenderOutsideBilling('CardNumber')).toBe(true); + expect(isFieldTypeToRenderOutsideBilling('CardExpiryMonth')).toBe(true); + expect(isFieldTypeToRenderOutsideBilling('CardExpiryYear')).toBe(true); + expect(isFieldTypeToRenderOutsideBilling('CardCvc')).toBe(true); + }); + + it('should return true for other outside-billing fields', () => { + expect(isFieldTypeToRenderOutsideBilling('FullName')).toBe(true); + expect(isFieldTypeToRenderOutsideBilling('Email')).toBe(false); + expect(isFieldTypeToRenderOutsideBilling('VpaId')).toBe(true); + expect(isFieldTypeToRenderOutsideBilling('PixKey')).toBe(true); + }); + + it('should return true for Currency object', () => { + expect(isFieldTypeToRenderOutsideBilling({ TAG: 'Currency', _0: [] })).toBe(true); + }); + + it('should return true for DocumentType object', () => { + expect(isFieldTypeToRenderOutsideBilling({ TAG: 'DocumentType', _0: [] })).toBe(true); + }); + + it('should return false for billing address fields', () => { + expect(isFieldTypeToRenderOutsideBilling('AddressLine1')).toBe(false); + expect(isFieldTypeToRenderOutsideBilling('AddressCity')).toBe(false); + }); + }); + + describe('combineStateAndCity', () => { + it('should combine AddressState and AddressCity into StateAndCity', () => { + const arr = ['AddressState', 'AddressCity', 'Email']; + const result = combineStateAndCity(arr); + expect(result).toContain('StateAndCity'); + expect(result).not.toContain('AddressState'); + expect(result).not.toContain('AddressCity'); + expect(result).toContain('Email'); + }); + + it('should not modify array if only AddressState is present', () => { + const arr = ['AddressState', 'Email']; + const result = combineStateAndCity(arr); + expect(result).toContain('AddressState'); + expect(result).toContain('Email'); + }); + + it('should not modify array if only AddressCity is present', () => { + const arr = ['AddressCity', 'Email']; + const result = combineStateAndCity(arr); + expect(result).toContain('AddressCity'); + expect(result).toContain('Email'); + }); + + it('should handle empty array', () => { + const result = combineStateAndCity([]); + expect(result).toEqual([]); + }); + }); + + describe('combineCountryAndPostal', () => { + it('should combine AddressCountry and AddressPincode into CountryAndPincode', () => { + const arr = [{ TAG: 'AddressCountry', _0: ['US'] }, 'AddressPincode', 'Email']; + const result = combineCountryAndPostal(arr); + expect(result.some((item: any) => item.TAG === 'CountryAndPincode')).toBe(true); + expect(result).not.toContain('AddressPincode'); + }); + + it('should not modify array if only AddressPincode is present', () => { + const arr = ['AddressPincode', 'Email']; + const result = combineCountryAndPostal(arr); + expect(result).toContain('AddressPincode'); + }); + + it('should handle empty array', () => { + const result = combineCountryAndPostal([]); + expect(result).toEqual([]); + }); + }); + + describe('combineCardExpiryMonthAndYear', () => { + it('should combine CardExpiryMonth and CardExpiryYear into CardExpiryMonthAndYear', () => { + const arr = ['CardExpiryMonth', 'CardExpiryYear', 'CardNumber']; + const result = combineCardExpiryMonthAndYear(arr); + expect(result).toContain('CardExpiryMonthAndYear'); + expect(result).not.toContain('CardExpiryMonth'); + expect(result).not.toContain('CardExpiryYear'); + expect(result).toContain('CardNumber'); + }); + + it('should not modify array if only CardExpiryMonth is present', () => { + const arr = ['CardExpiryMonth', 'CardNumber']; + const result = combineCardExpiryMonthAndYear(arr); + expect(result).toContain('CardExpiryMonth'); + }); + + it('should handle empty array', () => { + const result = combineCardExpiryMonthAndYear([]); + expect(result).toEqual([]); + }); + }); + + describe('combineCardExpiryAndCvc', () => { + it('should combine CardExpiryMonthAndYear and CardCvc into CardExpiryAndCvc', () => { + const arr = ['CardExpiryMonthAndYear', 'CardCvc', 'CardNumber']; + const result = combineCardExpiryAndCvc(arr); + expect(result).toContain('CardExpiryAndCvc'); + expect(result).not.toContain('CardExpiryMonthAndYear'); + expect(result).not.toContain('CardCvc'); + expect(result).toContain('CardNumber'); + }); + + it('should not modify array if prerequisites are not met', () => { + const arr = ['CardCvc', 'CardNumber']; + const result = combineCardExpiryAndCvc(arr); + expect(result).toContain('CardCvc'); + }); + + it('should handle empty array', () => { + const result = combineCardExpiryAndCvc([]); + expect(result).toEqual([]); + }); + }); + + describe('combinePhoneNumberAndCountryCode', () => { + it('should combine PhoneNumber and PhoneCountryCode into PhoneNumberAndCountryCode', () => { + const arr = ['PhoneNumber', 'PhoneCountryCode', 'Email']; + const result = combinePhoneNumberAndCountryCode(arr); + expect(result).toContain('PhoneNumberAndCountryCode'); + expect(result).not.toContain('PhoneNumber'); + expect(result).not.toContain('PhoneCountryCode'); + }); + + it('should work with only PhoneCountryCode', () => { + const arr = ['PhoneCountryCode', 'Email']; + const result = combinePhoneNumberAndCountryCode(arr); + expect(result).toContain('PhoneNumberAndCountryCode'); + expect(result).not.toContain('PhoneCountryCode'); + }); + + it('should work with only PhoneNumber', () => { + const arr = ['PhoneNumber', 'Email']; + const result = combinePhoneNumberAndCountryCode(arr); + expect(result).toContain('PhoneNumberAndCountryCode'); + expect(result).not.toContain('PhoneNumber'); + }); + + it('should handle empty array', () => { + const result = combinePhoneNumberAndCountryCode([]); + expect(result).toEqual([]); + }); + }); + + describe('updateDynamicFields', () => { + const defaultBillingAddress = { isUseBillingAddress: false }; + const defaultClickToPayConfig = { clickToPayCards: [], clickToPayProvider: 'NONE' }; + + it('should process array and combine fields', () => { + const arr = ['CardExpiryMonth', 'CardExpiryYear', 'Email']; + const result = updateDynamicFields(arr, defaultBillingAddress, false, defaultClickToPayConfig); + expect(result).toContain('CardExpiryMonthAndYear'); + }); + + it('should remove None values', () => { + const arr = ['Email', 'None', 'FullName']; + const result = updateDynamicFields(arr, defaultBillingAddress, false, defaultClickToPayConfig); + expect(result).not.toContain('None'); + }); + + it('should remove duplicates', () => { + const arr = ['Email', 'Email', 'FullName']; + const result = updateDynamicFields(arr, defaultBillingAddress, false, defaultClickToPayConfig); + const emailCount = result.filter((item: any) => item === 'Email').length; + expect(emailCount).toBe(1); + }); + + it('should handle empty array', () => { + const result = updateDynamicFields([], defaultBillingAddress, false, defaultClickToPayConfig); + expect(Array.isArray(result)).toBe(true); + }); + }); + + describe('removeRequiredFieldsDuplicates', () => { + it('should remove duplicate required fields based on required_field', () => { + const fields = [ + { required_field: 'email', field_type: 'Email' }, + { required_field: 'email', field_type: 'Email' }, + { required_field: 'name', field_type: 'FullName' }, + ]; + const result = removeRequiredFieldsDuplicates(fields); + expect(result.length).toBe(2); + }); + + it('should preserve order of first occurrence', () => { + const fields = [ + { required_field: 'email', field_type: 'Email' }, + { required_field: 'name', field_type: 'FullName' }, + { required_field: 'email', field_type: 'Email2' }, + ]; + const result = removeRequiredFieldsDuplicates(fields); + expect(result[0].field_type).toBe('Email'); + expect(result[1].field_type).toBe('FullName'); + }); + + it('should handle empty array', () => { + const result = removeRequiredFieldsDuplicates([]); + expect(result).toEqual([]); + }); + + it('should handle array without duplicates', () => { + const fields = [ + { required_field: 'email', field_type: 'Email' }, + { required_field: 'name', field_type: 'FullName' }, + ]; + const result = removeRequiredFieldsDuplicates(fields); + expect(result.length).toBe(2); + }); + }); + + describe('getNameFromString', () => { + it('should extract first name for first_name field', () => { + const result = getNameFromString('John Doe', ['payment', 'billing', 'first_name']); + expect(result.trim()).toBe('John'); + }); + + it('should extract last name for last_name field', () => { + const result = getNameFromString('John Doe', ['payment', 'billing', 'last_name']); + expect(result).toBe('Doe'); + }); + + it('should return full name for other fields', () => { + const result = getNameFromString('John Doe', ['payment', 'email']); + expect(result).toBe('John Doe'); + }); + + it('should handle single word name', () => { + const result = getNameFromString('John', ['payment', 'billing', 'first_name']); + expect(result.trim()).toBe('John'); + }); + + it('should handle empty string', () => { + const result = getNameFromString('', ['payment', 'billing', 'first_name']); + expect(result).toBe(''); + }); + }); + + describe('getNameFromFirstAndLastName', () => { + it('should return first name for first_name field', () => { + const result = getNameFromFirstAndLastName('John', 'Doe', ['billing', 'first_name']); + expect(result).toBe('John'); + }); + + it('should return last name for last_name field', () => { + const result = getNameFromFirstAndLastName('John', 'Doe', ['billing', 'last_name']); + expect(result).toBe('Doe'); + }); + + it('should return combined name for other fields', () => { + const result = getNameFromFirstAndLastName('John', 'Doe', ['billing', 'full_name']); + expect(result).toBe('John Doe'); + }); + + it('should handle empty strings', () => { + const result = getNameFromFirstAndLastName('', '', ['billing', 'first_name']); + expect(result).toBe(''); + }); + }); + + describe('getApplePayRequiredFields', () => { + const billingContact = { + givenName: 'John', + familyName: 'Doe', + addressLines: ['123 Main St', 'Apt 4'], + locality: 'New York', + administrativeArea: 'NY', + postalCode: '10001', + countryCode: 'US', + }; + + const shippingContact = { + emailAddress: 'test@example.com', + phoneNumber: '+1234567890', + ...billingContact, + }; + + it('should extract email from shipping contact', () => { + const result = getApplePayRequiredFields(billingContact, shippingContact); + expect(result['email']).toBe('test@example.com'); + }); + + it('should extract billing address fields', () => { + const result = getApplePayRequiredFields(billingContact, shippingContact); + expect(result['payment_method_data.billing.address.line1']).toBe('123 Main St'); + expect(result['payment_method_data.billing.address.city']).toBe('New York'); + }); + + it('should handle missing optional fields', () => { + const minimalBilling = { + givenName: '', + familyName: '', + addressLines: [], + locality: '', + administrativeArea: '', + postalCode: '', + countryCode: 'US', + }; + const minimalShipping = { + emailAddress: 'test@example.com', + phoneNumber: '', + givenName: '', + familyName: '', + addressLines: [], + locality: '', + administrativeArea: '', + postalCode: '', + countryCode: 'US', + }; + const result = getApplePayRequiredFields(minimalBilling, minimalShipping); + expect(result).toBeDefined(); + }); + }); + + describe('getGooglePayRequiredFields', () => { + const billingContact = { + name: 'John Doe', + address1: '123 Main St', + address2: 'Apt 4', + locality: 'New York', + administrativeArea: 'NY', + postalCode: '10001', + countryCode: 'US', + }; + + const shippingContact = { + name: 'Jane Doe', + phoneNumber: '+1234567890', + ...billingContact, + }; + + it('should extract email from parameter', () => { + const result = getGooglePayRequiredFields(billingContact, shippingContact, undefined, 'test@example.com'); + expect(result['email']).toBe('test@example.com'); + }); + + it('should extract billing address fields', () => { + const result = getGooglePayRequiredFields(billingContact, shippingContact, undefined, 'test@example.com'); + expect(result['payment_method_data.billing.address.line1']).toBe('123 Main St'); + }); + + it('should extract phone number from shipping contact', () => { + const paymentMethodTypes = [ + { required_field: 'payment_method_data.phone', field_type: 'PhoneNumber' }, + ]; + const result = getGooglePayRequiredFields(billingContact, shippingContact, paymentMethodTypes, 'test@example.com'); + expect(result['payment_method_data.phone']).toBe('+1234567890'); + }); + }); + + describe('getPaypalRequiredFields', () => { + const details = { + email: 'test@example.com', + phone: '+1234567890', + shippingAddress: { + recipientName: 'John Doe', + line1: '123 Main St', + line2: 'Apt 4', + city: 'New York', + postalCode: '10001', + state: 'NY', + countryCode: 'US', + }, + }; + + const paymentMethodTypes = { + required_fields: [ + { required_field: 'email', field_type: 'Email' }, + { required_field: 'phone', field_type: 'PhoneNumber' }, + { required_field: 'shipping.line1', field_type: 'ShippingAddressLine1' }, + ], + }; + + it('should extract email from details', () => { + const result = getPaypalRequiredFields(details, paymentMethodTypes); + expect(result['email']).toBe('test@example.com'); + }); + + it('should extract shipping address fields', () => { + const result = getPaypalRequiredFields(details, paymentMethodTypes); + expect(result['shipping.line1']).toBe('123 Main St'); + }); + + it('should handle missing optional fields', () => { + const minimalDetails = { + email: 'test@example.com', + shippingAddress: {}, + }; + const result = getPaypalRequiredFields(minimalDetails, { required_fields: [] }); + expect(result).toBeDefined(); + }); + }); + + describe('getKlarnaRequiredFields', () => { + const shippingContact = { + email: 'test@example.com', + phone: '+1234567890', + given_name: 'John', + family_name: 'Doe', + street_address: '123 Main St', + city: 'New York', + postal_code: '10001', + region: 'NY', + country: 'US', + }; + + const paymentMethodTypes = { + required_fields: [ + { required_field: 'email', field_type: 'Email' }, + { required_field: 'phone', field_type: 'PhoneNumber' }, + { required_field: 'shipping.address', field_type: 'ShippingAddressLine1' }, + ], + }; + + it('should extract email from shipping contact', () => { + const result = getKlarnaRequiredFields(shippingContact, paymentMethodTypes); + expect(result['email']).toBe('test@example.com'); + }); + + it('should extract shipping address fields', () => { + const result = getKlarnaRequiredFields(shippingContact, paymentMethodTypes); + expect(result['shipping.address']).toBe('123 Main St'); + }); + + it('should handle missing optional fields', () => { + const minimalContact = { email: 'test@example.com' }; + const result = getKlarnaRequiredFields(minimalContact, { required_fields: [] }); + expect(result).toBeDefined(); + }); + }); + + describe('dynamicFieldsEnabledPaymentMethods', () => { + it('should contain common payment methods', () => { + expect(dynamicFieldsEnabledPaymentMethods).toContain('credit'); + expect(dynamicFieldsEnabledPaymentMethods).toContain('debit'); + expect(dynamicFieldsEnabledPaymentMethods).toContain('google_pay'); + expect(dynamicFieldsEnabledPaymentMethods).toContain('apple_pay'); + expect(dynamicFieldsEnabledPaymentMethods).toContain('paypal'); + expect(dynamicFieldsEnabledPaymentMethods).toContain('klarna'); + }); + + it('should be an array', () => { + expect(Array.isArray(dynamicFieldsEnabledPaymentMethods)).toBe(true); + }); + + it('should contain bank debit methods', () => { + expect(dynamicFieldsEnabledPaymentMethods).toContain('ach'); + expect(dynamicFieldsEnabledPaymentMethods).toContain('sepa'); + }); + }); + + describe('getName edge cases', () => { + it('should handle multi-word last name', () => { + const item = { + required_field: 'payment_method_data.billing.address.last_name', + value: 'John Michael Doe', + }; + const field = { value: 'John Michael Doe' }; + const result = getName(item, field); + expect(result).toBe('Michael Doe'); + }); + + it('should handle single word name for last_name', () => { + const item = { + required_field: 'payment_method_data.billing.address.last_name', + value: 'John', + }; + const field = { value: 'John' }; + const result = getName(item, field); + expect(result).toBe(''); + }); + }); + + describe('isBillingAddressFieldType edge cases', () => { + it('should return false for object without AddressCountry tag', () => { + expect(isBillingAddressFieldType({ TAG: 'OtherTag', _0: [] })).toBe(false); + }); + }); + + describe('checkIfNameIsValid edge cases', () => { + it('should return true when no matching required fields', () => { + const requiredFields = [ + { field_type: 'Email', required_field: 'email' }, + ]; + const field = { value: 'John Doe' }; + const result = checkIfNameIsValid(requiredFields, 'FullName', field); + expect(result).toBe(true); + }); + + it('should handle name with only first name required', () => { + const requiredFields = [ + { field_type: 'FullName', required_field: 'billing.first_name', value: '' }, + ]; + const field = { value: 'John' }; + const result = checkIfNameIsValid(requiredFields, 'FullName', field); + expect(result).toBe(true); + }); + }); + + describe('isFieldTypeToRenderOutsideBilling additional cases', () => { + it('should return true for InfoElement', () => { + expect(isFieldTypeToRenderOutsideBilling('InfoElement')).toBe(true); + }); + + it('should return false for Email', () => { + expect(isFieldTypeToRenderOutsideBilling('Email')).toBe(false); + }); + + it('should return false for Country', () => { + expect(isFieldTypeToRenderOutsideBilling('Country')).toBe(false); + }); + }); + + describe('combineStateAndCity edge cases', () => { + it('should not combine if only AddressCity present', () => { + const arr = ['AddressCity', 'Email']; + const result = combineStateAndCity(arr); + expect(result).toContain('AddressCity'); + expect(result).not.toContain('StateAndCity'); + }); + }); + + describe('combineCountryAndPostal edge cases', () => { + it('should not combine if only AddressCountry present', () => { + const arr = [{ TAG: 'AddressCountry', _0: ['US'] }, 'Email']; + const result = combineCountryAndPostal(arr); + expect(result.some((item: any) => item.TAG === 'AddressCountry')).toBe(true); + }); + }); + + describe('getApplePayRequiredFields edge cases', () => { + it('should handle empty address lines', () => { + const billingContact = { + givenName: 'John', + familyName: 'Doe', + addressLines: [], + locality: 'New York', + administrativeArea: 'NY', + postalCode: '10001', + countryCode: 'US', + }; + const shippingContact = { + emailAddress: 'test@example.com', + phoneNumber: '+1234567890', + ...billingContact, + }; + const result = getApplePayRequiredFields(billingContact, shippingContact); + expect(result).toBeDefined(); + }); + + it('should handle missing email in shipping contact', () => { + const billingContact = { + givenName: '', + familyName: '', + addressLines: [], + locality: '', + administrativeArea: '', + postalCode: '', + countryCode: 'US', + }; + const shippingContact = { + emailAddress: '', + phoneNumber: '', + ...billingContact, + }; + const result = getApplePayRequiredFields(billingContact, shippingContact); + expect(result).toBeDefined(); + }); + }); + + describe('getGooglePayRequiredFields edge cases', () => { + it('should handle empty contacts', () => { + const billingContact = { + name: '', + address1: '', + address2: '', + locality: '', + administrativeArea: '', + postalCode: '', + countryCode: 'US', + }; + const shippingContact = { ...billingContact, phoneNumber: '' }; + const result = getGooglePayRequiredFields(billingContact, shippingContact, undefined, ''); + expect(result).toBeDefined(); + }); + + it('should extract shipping address fields', () => { + const billingContact = { + name: 'John Doe', + address1: '123 Main St', + address2: '', + locality: 'New York', + administrativeArea: 'NY', + postalCode: '10001', + countryCode: 'US', + }; + const shippingContact = { + name: 'Jane Doe', + phoneNumber: '+1234567890', + address1: '456 Oak St', + address2: '', + locality: 'Boston', + administrativeArea: 'MA', + postalCode: '02101', + countryCode: 'US', + }; + const requiredFields = [ + { required_field: 'shipping.name', field_type: 'ShippingName' }, + { required_field: 'shipping.address', field_type: 'ShippingAddressLine1' }, + ]; + const result = getGooglePayRequiredFields(billingContact, shippingContact, requiredFields, 'test@example.com'); + expect(result['shipping.address']).toBe('456 Oak St'); + }); + }); + + describe('getPaypalRequiredFields edge cases', () => { + it('should handle missing shipping address', () => { + const details = { + email: 'test@example.com', + }; + const result = getPaypalRequiredFields(details, { required_fields: [] }); + expect(result).toBeDefined(); + }); + + it('should handle shipping address country code', () => { + const details = { + email: 'test@example.com', + shippingAddress: { + countryCode: 'US', + }, + }; + const requiredFields = [ + { required_field: 'shipping.country', field_type: { TAG: 'ShippingAddressCountry' } }, + ]; + const result = getPaypalRequiredFields(details, { required_fields: requiredFields }); + expect(result['shipping.country']).toBe('US'); + }); + }); + + describe('getKlarnaRequiredFields edge cases', () => { + it('should handle missing phone', () => { + const contact = { + email: 'test@example.com', + given_name: 'John', + family_name: 'Doe', + }; + const requiredFields = [ + { required_field: 'phone', field_type: 'PhoneNumber' }, + ]; + const result = getKlarnaRequiredFields(contact, { required_fields: requiredFields }); + expect(result).toBeDefined(); + }); + + it('should handle shipping address country', () => { + const contact = { + email: 'test@example.com', + country: 'US', + }; + const requiredFields = [ + { required_field: 'shipping.country', field_type: { TAG: 'ShippingAddressCountry' } }, + ]; + const result = getKlarnaRequiredFields(contact, { required_fields: requiredFields }); + expect(result['shipping.country']).toBe('US'); + }); + }); + + describe('updateDynamicFields edge cases', () => { + const defaultBillingAddress = { isUseBillingAddress: false }; + const defaultClickToPayConfig = { clickToPayCards: [], clickToPayProvider: 'NONE' }; + + it('should add billing fields when isUseBillingAddress is true', () => { + const arr = ['Email']; + const billingAddress = { isUseBillingAddress: true }; + const result = updateDynamicFields(arr, billingAddress, false, defaultClickToPayConfig); + expect(result.length).toBeGreaterThan(1); + }); + + it('should add click to pay fields when saving with VISA', () => { + const arr = ['Email']; + const config = { clickToPayCards: [], clickToPayProvider: 'VISA' }; + const result = updateDynamicFields(arr, defaultBillingAddress, true, config); + expect(result).toContain('Email'); + expect(result).toContain('FullName'); + }); + }); + + describe('getNameFromString edge cases', () => { + it('should handle single word name for last_name', () => { + const result = getNameFromString('John', ['billing', 'last_name']); + expect(result).toBe(''); + }); + + it('should handle three word name for first_name', () => { + const result = getNameFromString('John Michael Doe', ['billing', 'first_name']); + expect(result.trim()).toBe('John Michael'); + }); + }); + + describe('getNameFromFirstAndLastName edge cases', () => { + it('should handle empty first name', () => { + const result = getNameFromFirstAndLastName('', 'Doe', ['billing', 'first_name']); + expect(result).toBe(''); + }); + + it('should handle empty last name', () => { + const result = getNameFromFirstAndLastName('John', '', ['billing', 'last_name']); + expect(result).toBe(''); + }); + }); + + describe('additional edge cases for addClickToPayFieldsIfSaveDetailsWithClickToPay', () => { + it('should return fieldsArr unchanged when not saving and provider is NONE with no cards', () => { + const fieldsArr = ['Email']; + const config = { clickToPayCards: [], clickToPayProvider: 'NONE' }; + const result = addClickToPayFieldsIfSaveDetailsWithClickToPay(fieldsArr, false, config); + expect(result).toEqual(['Email']); + }); + + it('should add FullName for VISA provider when not saving but has recognized cards', () => { + const fieldsArr = ['Email']; + const config = { clickToPayCards: ['card1'], clickToPayProvider: 'VISA' }; + const result = addClickToPayFieldsIfSaveDetailsWithClickToPay(fieldsArr, false, config); + expect(result).toContain('FullName'); + }); + + it('should return defaultCtpFields for MASTERCARD provider when not saving and has recognized cards', () => { + const fieldsArr = ['Email']; + const config = { clickToPayCards: ['card1'], clickToPayProvider: 'MASTERCARD' }; + const result = addClickToPayFieldsIfSaveDetailsWithClickToPay(fieldsArr, false, config); + expect(result).toEqual(['Email']); + }); + }); + + describe('additional edge cases for checkIfNameIsValid', () => { + it('should return true when all name parts are provided', () => { + const requiredFields = [ + { field_type: 'FullName', required_field: 'payment_method_data.billing.first_name', value: '' }, + { field_type: 'FullName', required_field: 'payment_method_data.billing.last_name', value: '' }, + ]; + const field = { value: 'John Doe' }; + const result = checkIfNameIsValid(requiredFields, 'FullName', field); + expect(result).toBe(true); + }); + + it('should return false when field value is empty', () => { + const requiredFields = [ + { field_type: 'FullName', required_field: 'payment_method_data.billing.first_name', value: '' }, + ]; + const field = { value: '' }; + const result = checkIfNameIsValid(requiredFields, 'FullName', field); + expect(result).toBe(false); + }); + }); + + describe('additional edge cases for combinePhoneNumberAndCountryCode', () => { + it('should not add PhoneNumberAndCountryCode when neither field is present', () => { + const arr = ['Email', 'FullName']; + const result = combinePhoneNumberAndCountryCode([...arr]); + expect(result).toEqual(arr); + }); + + it('should work with both PhoneNumber and PhoneCountryCode present', () => { + const arr = ['PhoneNumber', 'PhoneCountryCode', 'Email']; + const result = combinePhoneNumberAndCountryCode([...arr]); + expect(result).toContain('PhoneNumberAndCountryCode'); + expect(result).not.toContain('PhoneNumber'); + expect(result).not.toContain('PhoneCountryCode'); + }); + }); + + describe('additional edge cases for getApplePayRequiredFields', () => { + it('should extract shipping address fields', () => { + const billingContact = { + givenName: 'John', + familyName: 'Doe', + addressLines: ['123 Billing St'], + locality: 'Billing City', + administrativeArea: 'BC', + postalCode: '12345', + countryCode: 'US', + }; + const shippingContact = { + emailAddress: 'ship@example.com', + phoneNumber: '+1234567890', + givenName: 'Jane', + familyName: 'Smith', + addressLines: ['456 Shipping St'], + locality: 'Shipping City', + administrativeArea: 'SC', + postalCode: '67890', + countryCode: 'US', + }; + const requiredFields = [ + { required_field: 'shipping.name', field_type: 'ShippingName' }, + { required_field: 'shipping.line1', field_type: 'ShippingAddressLine1' }, + ]; + const result = getApplePayRequiredFields(billingContact, shippingContact, requiredFields); + expect(result['shipping.name']).toBe('Jane Smith'); + expect(result['shipping.line1']).toBe('456 Shipping St'); + }); + + it('should handle AddressCountry field type', () => { + const billingContact = { + givenName: 'John', + familyName: 'Doe', + addressLines: [], + locality: '', + administrativeArea: '', + postalCode: '', + countryCode: 'US', + }; + const shippingContact = { + emailAddress: '', + phoneNumber: '', + givenName: '', + familyName: '', + addressLines: [], + locality: '', + administrativeArea: '', + postalCode: '', + countryCode: 'GB', + }; + const requiredFields = [ + { required_field: 'billing.country', field_type: { TAG: 'AddressCountry', _0: [] } }, + { required_field: 'shipping.country', field_type: { TAG: 'ShippingAddressCountry', _0: [] } }, + ]; + const result = getApplePayRequiredFields(billingContact, shippingContact, requiredFields); + expect(result['billing.country']).toBe('US'); + expect(result['shipping.country']).toBe('GB'); + }); + }); + + describe('additional edge cases for getGooglePayRequiredFields', () => { + it('should handle AddressCountry field type', () => { + const billingContact = { + name: 'John Doe', + address1: '123 Main St', + address2: '', + locality: 'City', + administrativeArea: 'State', + postalCode: '12345', + countryCode: 'US', + }; + const shippingContact = { + name: 'Jane Doe', + phoneNumber: '+1234567890', + address1: '', + address2: '', + locality: '', + administrativeArea: '', + postalCode: '', + countryCode: 'GB', + }; + const requiredFields = [ + { required_field: 'billing.country', field_type: { TAG: 'AddressCountry', _0: [] } }, + { required_field: 'shipping.country', field_type: { TAG: 'ShippingAddressCountry', _0: [] } }, + ]; + const result = getGooglePayRequiredFields(billingContact, shippingContact, requiredFields, 'test@example.com'); + expect(result['billing.country']).toBe('US'); + expect(result['shipping.country']).toBe('GB'); + }); + + it('should handle empty name for name extraction', () => { + const billingContact = { + name: '', + address1: '', + address2: '', + locality: '', + administrativeArea: '', + postalCode: '', + countryCode: 'US', + }; + const shippingContact = { + name: '', + phoneNumber: '', + address1: '', + address2: '', + locality: '', + administrativeArea: '', + postalCode: '', + countryCode: 'US', + }; + const requiredFields = [ + { required_field: 'billing.name', field_type: 'BillingName' }, + ]; + const result = getGooglePayRequiredFields(billingContact, shippingContact, requiredFields, ''); + expect(result).toBeDefined(); + }); + }); + + describe('additional edge cases for getPaypalRequiredFields', () => { + it('should handle missing shipping address gracefully', () => { + const details = { + email: 'test@example.com', + shippingAddress: {}, + }; + const requiredFields = [ + { required_field: 'shipping.line1', field_type: 'ShippingAddressLine1' }, + ]; + const result = getPaypalRequiredFields(details, { required_fields: requiredFields }); + expect(result).toBeDefined(); + }); + + it('should handle recipient name extraction', () => { + const details = { + email: 'test@example.com', + shippingAddress: { + recipientName: 'Jane Doe', + line1: '123 Main St', + }, + }; + const requiredFields = [ + { required_field: 'shipping.name', field_type: 'ShippingName' }, + ]; + const result = getPaypalRequiredFields(details, { required_fields: requiredFields }); + expect(result['shipping.name']).toBe('Jane Doe'); + }); + }); + + describe('additional edge cases for getKlarnaRequiredFields', () => { + it('should handle missing optional fields', () => { + const contact = { + email: 'test@example.com', + given_name: 'John', + family_name: 'Doe', + }; + const requiredFields = [ + { required_field: 'shipping.line1', field_type: 'ShippingAddressLine1' }, + { required_field: 'shipping.city', field_type: 'ShippingAddressCity' }, + ]; + const result = getKlarnaRequiredFields(contact, { required_fields: requiredFields }); + expect(result).toBeDefined(); + }); + }); + + describe('additional edge cases for updateDynamicFields', () => { + it('should handle combined operations for card expiry and cvc', () => { + const arr = ['CardExpiryMonth', 'CardExpiryYear', 'CardCvc']; + const billingAddress = { isUseBillingAddress: false }; + const clickToPayConfig = { clickToPayCards: [], clickToPayProvider: 'NONE' }; + const result = updateDynamicFields(arr, billingAddress, false, clickToPayConfig); + expect(result).toContain('CardExpiryAndCvc'); + }); + + it('should handle all combine operations together', () => { + const arr = ['AddressState', 'AddressCity', 'CardExpiryMonth', 'CardExpiryYear', 'PhoneNumber']; + const billingAddress = { isUseBillingAddress: false }; + const clickToPayConfig = { clickToPayCards: [], clickToPayProvider: 'NONE' }; + const result = updateDynamicFields(arr, billingAddress, false, clickToPayConfig); + expect(result).toContain('StateAndCity'); + expect(result).toContain('CardExpiryMonthAndYear'); + expect(result).toContain('PhoneNumberAndCountryCode'); + }); + }); + + describe('useRequiredFieldsEmptyAndValid', () => { + const createWrapper = (initialState: any = {}) => { + return function Wrapper({ children }: { children: React.ReactNode }) { + return React.createElement( + RecoilRoot, + { + initializeState: ({ set }: any) => { + if (initialState.userEmailAddress) { + set(RecoilAtoms.userEmailAddress, initialState.userEmailAddress); + } + if (initialState.userFullName) { + set(RecoilAtoms.userFullName, initialState.userFullName); + } + if (initialState.optionAtom) { + set(RecoilAtoms.optionAtom, initialState.optionAtom); + } + if (initialState.areRequiredFieldsValid !== undefined) { + set(RecoilAtoms.areRequiredFieldsValid, initialState.areRequiredFieldsValid); + } + }, + }, + children + ); + }; + }; + + it('should be callable with required parameters', () => { + const wrapper = createWrapper({ + userEmailAddress: { value: 'test@example.com', isValid: true }, + userFullName: { value: 'John Doe', isValid: true }, + optionAtom: { billingAddress: { isUseBillingAddress: false } }, + areRequiredFieldsValid: true, + }); + + const requiredFields: any[] = []; + const fieldsArr: any[] = ['Email']; + const countryNames: string[] = ['United States']; + const bankNames: string[] = []; + + const { result } = renderHook( + () => useRequiredFieldsEmptyAndValid( + requiredFields, + fieldsArr, + countryNames, + bankNames, + true, + true, + true, + '4242424242424242', + '12/25', + '123', + false + ), + { wrapper } + ); + + expect(result.current).toBeUndefined(); + }); + + it('should handle empty fields array', () => { + const wrapper = createWrapper({ + userEmailAddress: { value: '', isValid: false }, + userFullName: { value: '', isValid: false }, + optionAtom: { billingAddress: { isUseBillingAddress: false } }, + areRequiredFieldsValid: false, + }); + + const { result } = renderHook( + () => useRequiredFieldsEmptyAndValid( + [], + [], + [], + [], + false, + false, + false, + '', + '', + '', + false + ), + { wrapper } + ); + + expect(result.current).toBeUndefined(); + }); + }); + + describe('useSetInitialRequiredFields', () => { + const createWrapper = (initialState: any = {}) => { + return function Wrapper({ children }: { children: React.ReactNode }) { + return React.createElement( + RecoilRoot, + { + initializeState: ({ set }: any) => { + if (initialState.userEmailAddress) { + set(RecoilAtoms.userEmailAddress, initialState.userEmailAddress); + } + if (initialState.userFullName) { + set(RecoilAtoms.userFullName, initialState.userFullName); + } + }, + }, + children + ); + }; + }; + + it('should be callable with required parameters', () => { + const wrapper = createWrapper({ + userEmailAddress: { value: 'test@example.com' }, + userFullName: { value: 'John Doe' }, + }); + + const requiredFields = [ + { field_type: 'Email', required_field: 'email', value: 'test@example.com' }, + ]; + + const { result } = renderHook( + () => useSetInitialRequiredFields(requiredFields, 'credit'), + { wrapper } + ); + + expect(result.current).toBeUndefined(); + }); + + it('should handle empty required fields', () => { + const wrapper = createWrapper({ + userEmailAddress: { value: '' }, + userFullName: { value: '' }, + }); + + const { result } = renderHook( + () => useSetInitialRequiredFields([], 'credit'), + { wrapper } + ); + + expect(result.current).toBeUndefined(); + }); + + it('should handle different payment method types', () => { + const wrapper = createWrapper({ + userEmailAddress: { value: '' }, + userFullName: { value: '' }, + }); + + const { result: result1 } = renderHook( + () => useSetInitialRequiredFields([], 'debit'), + { wrapper } + ); + + const { result: result2 } = renderHook( + () => useSetInitialRequiredFields([], 'google_pay'), + { wrapper } + ); + + expect(result1.current).toBeUndefined(); + expect(result2.current).toBeUndefined(); + }); + }); + + describe('useRequiredFieldsBody', () => { + const createWrapper = (initialState: any = {}) => { + return function Wrapper({ children }: { children: React.ReactNode }) { + return React.createElement( + RecoilRoot, + { + initializeState: ({ set }: any) => { + if (initialState.configAtom) { + set(RecoilAtoms.configAtom, initialState.configAtom); + } + if (initialState.userEmailAddress) { + set(RecoilAtoms.userEmailAddress, initialState.userEmailAddress); + } + if (initialState.optionAtom) { + set(RecoilAtoms.optionAtom, initialState.optionAtom); + } + }, + }, + children + ); + }; + }; + + it('should be callable with required parameters', () => { + const mockSetRequiredFieldsBody = jest.fn(); + + const wrapper = createWrapper({ + configAtom: { config: { locale: 'en-US' } }, + userEmailAddress: { value: 'test@example.com' }, + optionAtom: { billingAddress: { isUseBillingAddress: false } }, + }); + + const requiredFields = [ + { field_type: 'Email', required_field: 'email', value: 'test@example.com' }, + ]; + + const { result } = renderHook( + () => useRequiredFieldsBody( + requiredFields, + 'credit', + '4242424242424242', + '12/25', + '123', + false, + false, + mockSetRequiredFieldsBody + ), + { wrapper } + ); + + expect(result.current).toBeUndefined(); + }); + + it('should handle empty required fields', () => { + const mockSetRequiredFieldsBody = jest.fn(); + + const wrapper = createWrapper({ + configAtom: { config: { locale: 'en-US' } }, + userEmailAddress: { value: '' }, + optionAtom: { billingAddress: { isUseBillingAddress: false } }, + }); + + const { result } = renderHook( + () => useRequiredFieldsBody( + [], + 'credit', + '', + '', + '', + false, + false, + mockSetRequiredFieldsBody + ), + { wrapper } + ); + + expect(result.current).toBeUndefined(); + }); + }); + + describe('useSubmitCallback', () => { + const createWrapper = (initialState: any = {}) => { + return function Wrapper({ children }: { children: React.ReactNode }) { + return React.createElement( + RecoilRoot, + { + initializeState: ({ set }: any) => { + if (initialState.userAddressline1) { + set(RecoilAtoms.userAddressline1, initialState.userAddressline1); + } + if (initialState.userAddressline2) { + set(RecoilAtoms.userAddressline2, initialState.userAddressline2); + } + if (initialState.optionAtom) { + set(RecoilAtoms.optionAtom, initialState.optionAtom); + } + if (initialState.configAtom) { + set(RecoilAtoms.configAtom, initialState.configAtom); + } + }, + }, + children + ); + }; + }; + + it('should return a callback function', () => { + const wrapper = createWrapper({ + userAddressline1: { value: '123 Main St' }, + userAddressline2: { value: 'Apt 4' }, + optionAtom: { billingAddress: { isUseBillingAddress: false } }, + configAtom: { localeString: { line1EmptyText: 'Line 1 is required' } }, + }); + + const { result } = renderHook( + () => useSubmitCallback(), + { wrapper } + ); + + expect(typeof result.current).toBe('function'); + }); + + it('should handle callback invocation', () => { + const wrapper = createWrapper({ + userAddressline1: { value: '123 Main St', errorString: '' }, + userAddressline2: { value: 'Apt 4', errorString: '' }, + optionAtom: { billingAddress: { isUseBillingAddress: false } }, + configAtom: { localeString: { line1EmptyText: 'Line 1 is required' } }, + }); + + const { result } = renderHook( + () => useSubmitCallback(), + { wrapper } + ); + + expect(typeof result.current).toBe('function'); + }); + }); + + describe('usePaymentMethodTypeFromList', () => { + const createWrapper = () => { + return function Wrapper({ children }: { children: React.ReactNode }) { + return React.createElement(RecoilRoot, null, children); + }; + }; + + it('should return payment method type from list', () => { + const wrapper = createWrapper(); + + const paymentMethodListValue = { + payment_methods: [ + { + payment_method: 'card', + payment_method_types: [ + { payment_method_type: 'credit' }, + ], + }, + ], + }; + + const { result } = renderHook( + () => usePaymentMethodTypeFromList(paymentMethodListValue, 'card', 'credit'), + { wrapper } + ); + + expect(result.current).toBeDefined(); + }); + + it('should handle empty payment method list', () => { + const wrapper = createWrapper(); + + const paymentMethodListValue = { + payment_methods: [], + }; + + const { result } = renderHook( + () => usePaymentMethodTypeFromList(paymentMethodListValue, 'card', 'credit'), + { wrapper } + ); + + expect(result.current).toBeDefined(); + }); + + it('should handle undefined payment method type', () => { + const wrapper = createWrapper(); + + const paymentMethodListValue = { + payment_methods: [], + }; + + const { result } = renderHook( + () => usePaymentMethodTypeFromList(paymentMethodListValue, 'card', undefined), + { wrapper } + ); + + expect(result.current).toBeDefined(); + }); + }); +}); diff --git a/src/__tests__/ElementType.test.ts b/src/__tests__/ElementType.test.ts new file mode 100644 index 000000000..07c739ad2 --- /dev/null +++ b/src/__tests__/ElementType.test.ts @@ -0,0 +1,287 @@ +import { + getIconStyle, + defaultClasses, + defaultStyleClass, + defaultPaymentRequestButton, + defaultStyle, + defaultOptions, + getClasses, + getStyleObj, + getTheme, + getPaymentRequestButton, + getStyle, + itemToObjMapper, +} from '../Types/ElementType.bs.js'; + +const mockLogger = { + setLogInfo: jest.fn(), + setLogError: jest.fn(), +}; + +describe('ElementType', () => { + describe('getIconStyle', () => { + it('should return "Default" for "default"', () => { + expect(getIconStyle('default')).toBe('Default'); + }); + + it('should return "Solid" for "solid"', () => { + expect(getIconStyle('solid')).toBe('Solid'); + }); + + it('should return "Default" for unknown value', () => { + expect(getIconStyle('unknown')).toBe('Default'); + }); + + it('should return "Default" for empty string', () => { + expect(getIconStyle('')).toBe('Default'); + }); + }); + + describe('defaultClasses', () => { + it('should have correct default class names', () => { + expect(defaultClasses.base).toBe('OrcaElement'); + expect(defaultClasses.complete).toBe('OrcaElement--complete'); + expect(defaultClasses.empty).toBe('OrcaElement--empty'); + expect(defaultClasses.focus).toBe('OrcaElement--focus'); + expect(defaultClasses.invalid).toBe('OrcaElement--invalid'); + expect(defaultClasses.valid).toBe('OrcaElement--valid'); + expect(defaultClasses.webkitAutofill).toBe('OrcaElement--webkit-autofill'); + }); + }); + + describe('defaultStyleClass', () => { + it('should have empty string defaults for style properties', () => { + expect(defaultStyleClass.backgroundColor).toBe(''); + expect(defaultStyleClass.color).toBe(''); + expect(defaultStyleClass.fontFamily).toBe(''); + expect(defaultStyleClass.fontSize).toBe(''); + }); + }); + + describe('defaultPaymentRequestButton', () => { + it('should have correct default values', () => { + expect(defaultPaymentRequestButton.type_).toBe('default'); + expect(defaultPaymentRequestButton.theme).toBe('Dark'); + expect(defaultPaymentRequestButton.height).toBe(''); + }); + }); + + describe('defaultOptions', () => { + it('should have correct default values', () => { + expect(defaultOptions.value).toBe(''); + expect(defaultOptions.hidePostalCode).toBe(false); + expect(defaultOptions.iconStyle).toBe('Default'); + expect(defaultOptions.hideIcon).toBe(false); + expect(defaultOptions.showIcon).toBe(false); + expect(defaultOptions.disabled).toBe(false); + expect(defaultOptions.placeholder).toBe(''); + expect(defaultOptions.showError).toBe(true); + }); + }); + + describe('getTheme', () => { + it('should return "Dark" for "dark"', () => { + expect(getTheme('dark', 'test.key')).toBe('Dark'); + }); + + it('should return "Light" for "light"', () => { + expect(getTheme('light', 'test.key')).toBe('Light'); + }); + + it('should return "LightOutline" for "light-outline"', () => { + expect(getTheme('light-outline', 'test.key')).toBe('LightOutline'); + }); + + it('should return "Dark" for unknown value', () => { + expect(getTheme('unknown', 'test.key')).toBe('Dark'); + }); + + it('should return "Dark" for empty string', () => { + expect(getTheme('', 'test.key')).toBe('Dark'); + }); + }); + + describe('getClasses', () => { + it('should extract classes from dict', () => { + const dict = { + classes: { + base: 'CustomBase', + complete: 'CustomComplete', + empty: 'CustomEmpty', + focus: 'CustomFocus', + invalid: 'CustomInvalid', + valid: 'CustomValid', + webkitAutofill: 'CustomAutofill', + }, + }; + const result = getClasses('classes', dict, mockLogger); + expect(result.base).toBe('CustomBase'); + expect(result.complete).toBe('CustomComplete'); + expect(result.empty).toBe('CustomEmpty'); + }); + + it('should return default classes when key not found', () => { + const dict = {}; + const result = getClasses('classes', dict, mockLogger); + expect(result).toEqual(defaultClasses); + }); + + it('should handle partial class definitions', () => { + const dict = { + classes: { + base: 'MyBase', + }, + }; + const result = getClasses('classes', dict, mockLogger); + expect(result.base).toBe('MyBase'); + expect(result.complete).toBe('OrcaElement--complete'); + }); + }); + + describe('getStyleObj', () => { + it('should extract style object from dict', () => { + const dict = { + style: { + backgroundColor: '#fff', + color: '#000', + fontSize: '16px', + }, + }; + const result = getStyleObj(dict, 'style', mockLogger); + expect(result.backgroundColor).toBe('#fff'); + expect(result.color).toBe('#000'); + expect(result.fontSize).toBe('16px'); + }); + + it('should return default style class when key not found', () => { + const dict = {}; + const result = getStyleObj(dict, 'style', mockLogger); + expect(result).toEqual(defaultStyleClass); + }); + + it('should handle nested pseudo-selectors', () => { + const dict = { + style: { + color: '#333', + ':hover': { + color: '#666', + }, + }, + }; + const result = getStyleObj(dict, 'style', mockLogger); + expect(result.color).toBe('#333'); + expect(result.hover?.color).toBe('#666'); + }); + }); + + describe('getPaymentRequestButton', () => { + it('should extract payment request button config from dict', () => { + const dict = { + paymentRequestButton: { + type: 'buy', + theme: 'light', + height: '48px', + }, + }; + const result = getPaymentRequestButton(dict, 'paymentRequestButton', mockLogger); + expect(result.type_).toBe('buy'); + expect(result.theme).toBe('Light'); + expect(result.height).toBe('48px'); + }); + + it('should return default values when key not found', () => { + const dict = {}; + const result = getPaymentRequestButton(dict, 'paymentRequestButton', mockLogger); + expect(result).toEqual(defaultPaymentRequestButton); + }); + + it('should convert theme to proper case', () => { + const dict = { + paymentRequestButton: { + theme: 'dark', + }, + }; + const result = getPaymentRequestButton(dict, 'paymentRequestButton', mockLogger); + expect(result.theme).toBe('Dark'); + }); + }); + + describe('getStyle', () => { + it('should extract style from dict', () => { + const dict = { + style: { + base: { color: '#000' }, + complete: { color: 'green' }, + empty: { color: 'gray' }, + invalid: { color: 'red' }, + }, + }; + const result = getStyle(dict, 'style', mockLogger); + expect(result.base).toEqual({ color: '#000' }); + expect(result.complete).toEqual({ color: 'green' }); + expect(result.empty).toEqual({ color: 'gray' }); + expect(result.invalid).toEqual({ color: 'red' }); + }); + + it('should return default style when key not found', () => { + const dict = {}; + const result = getStyle(dict, 'style', mockLogger); + expect(result).toEqual(defaultStyle); + }); + }); + + describe('itemToObjMapper', () => { + it('should map dict to options object', () => { + const dict = { + classes: { + base: 'TestBase', + }, + style: { + base: { color: '#333' }, + }, + value: 'test value', + hidePostalCode: true, + iconStyle: 'solid', + hideIcon: true, + showIcon: false, + disabled: true, + placeholder: 'Enter value', + showError: false, + }; + const result = itemToObjMapper(dict, mockLogger); + expect(result.classes.base).toBe('TestBase'); + expect(result.value).toBe('test value'); + expect(result.hidePostalCode).toBe(true); + expect(result.iconStyle).toBe('Solid'); + expect(result.hideIcon).toBe(true); + expect(result.disabled).toBe(true); + expect(result.placeholder).toBe('Enter value'); + expect(result.showError).toBe(false); + }); + + it('should use default values for missing fields', () => { + const dict = {}; + const result = itemToObjMapper(dict, mockLogger); + expect(result.classes).toEqual(defaultClasses); + expect(result.value).toBe(''); + expect(result.hidePostalCode).toBe(false); + expect(result.iconStyle).toBe('Default'); + expect(result.hideIcon).toBe(false); + expect(result.showIcon).toBe(false); + expect(result.disabled).toBe(false); + expect(result.placeholder).toBe(''); + expect(result.showError).toBe(true); + }); + + it('should handle partial options', () => { + const dict = { + value: 'partial', + disabled: true, + }; + const result = itemToObjMapper(dict, mockLogger); + expect(result.value).toBe('partial'); + expect(result.disabled).toBe(true); + expect(result.hidePostalCode).toBe(false); + }); + }); +}); diff --git a/src/__tests__/ErrorUtils.test.ts b/src/__tests__/ErrorUtils.test.ts new file mode 100644 index 000000000..d92d13e05 --- /dev/null +++ b/src/__tests__/ErrorUtils.test.ts @@ -0,0 +1,119 @@ +import { + errorWarning, + unknownKeysWarning, + unknownPropValueWarning, + valueOutRangeWarning, +} from '../Utilities/ErrorUtils.bs.js'; + +describe('ErrorUtils', () => { + describe('errorWarning', () => { + it('should be an array of error/warning definitions', () => { + expect(errorWarning).toBeDefined(); + expect(Array.isArray(errorWarning)).toBe(true); + }); + + it('should contain INVALID_PK error', () => { + const invalidPk = errorWarning.find((entry: any) => entry[0] === 'INVALID_PK'); + expect(invalidPk).toBeDefined(); + expect(invalidPk[1]).toBe('Error'); + }); + + it('should contain DEPRECATED_LOADSTRIPE warning', () => { + const deprecated = errorWarning.find((entry: any) => entry[0] === 'DEPRECATED_LOADSTRIPE'); + expect(deprecated).toBeDefined(); + expect(deprecated[1]).toBe('Warning'); + }); + + it('should contain REQUIRED_PARAMETER error', () => { + const required = errorWarning.find((entry: any) => entry[0] === 'REQUIRED_PARAMETER'); + expect(required).toBeDefined(); + expect(required[1]).toBe('Error'); + }); + + it('should have dynamic messages for certain errors', () => { + const typeError = errorWarning.find((entry: any) => entry[0] === 'TYPE_BOOL_ERROR'); + expect(typeError).toBeDefined(); + expect((typeError![2] as any).TAG).toBe('Dynamic'); + }); + + it('should have static messages for certain errors', () => { + const internalApi = errorWarning.find((entry: any) => entry[0] === 'INTERNAL_API_DOWN'); + expect(internalApi).toBeDefined(); + expect((internalApi![2] as any).TAG).toBe('Static'); + }); + }); + + describe('unknownKeysWarning', () => { + it('should warn for unknown keys', () => { + const validKeys = ['name', 'email', 'phone']; + const dict = { name: 'John', unknownKey: 'value' }; + const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(); + + unknownKeysWarning(validKeys, dict, 'testDict'); + + expect(consoleSpy).toHaveBeenCalled(); + consoleSpy.mockRestore(); + }); + + it('should not warn for valid keys', () => { + const validKeys = ['name', 'email', 'phone']; + const dict = { name: 'John', email: 'test@test.com' }; + const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(); + + unknownKeysWarning(validKeys, dict, 'testDict'); + + expect(consoleSpy).not.toHaveBeenCalled(); + consoleSpy.mockRestore(); + }); + + it('should handle empty dict', () => { + const validKeys = ['name', 'email']; + const dict = {}; + const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(); + + unknownKeysWarning(validKeys, dict, 'testDict'); + + expect(consoleSpy).not.toHaveBeenCalled(); + consoleSpy.mockRestore(); + }); + }); + + describe('unknownPropValueWarning', () => { + it('should warn for invalid prop value', () => { + const validValues = ['option1', 'option2', 'option3']; + const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(); + + unknownPropValueWarning('invalidOption', validValues, 'testProp'); + + expect(consoleSpy).toHaveBeenCalled(); + consoleSpy.mockRestore(); + }); + + it('should include expected values in warning', () => { + const validValues = ['option1', 'option2']; + const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(); + + unknownPropValueWarning('invalidOption', validValues, 'testProp'); + + const warningCall = consoleSpy.mock.calls[0][0]; + expect(warningCall).toContain('option1'); + expect(warningCall).toContain('option2'); + consoleSpy.mockRestore(); + }); + }); + + describe('valueOutRangeWarning', () => { + it('should call manageErrorWarning for out of range value', () => { + const mockLogger = { + setLogError: jest.fn(), + setLogInfo: jest.fn(), + }; + const consoleSpy = jest.spyOn(console, 'warn').mockImplementation(); + + valueOutRangeWarning(150, 'age', '0-100', mockLogger); + + expect(consoleSpy).toHaveBeenCalled(); + consoleSpy.mockRestore(); + }); + }); +}); diff --git a/src/__tests__/GooglePayHelpers.test.ts b/src/__tests__/GooglePayHelpers.test.ts new file mode 100644 index 000000000..96e914682 --- /dev/null +++ b/src/__tests__/GooglePayHelpers.test.ts @@ -0,0 +1,520 @@ +import * as React from 'react'; +import { renderHook } from '@testing-library/react'; +import * as GooglePayHelpers from '../Utilities/GooglePayHelpers.bs.js'; + +const mockMessageParentWindow = jest.fn(); +const mockPostFailedSubmitResponse = jest.fn(); +const mockSafeParse = jest.fn((str: string) => { + try { + return JSON.parse(str); + } catch { + return null; + } +}); +const mockGetDictFromJson = jest.fn((obj: any) => { + if (typeof obj === 'string') { + try { + return JSON.parse(obj); + } catch { + return {}; + } + } + return typeof obj === 'object' && obj !== null ? obj : {}; +}); +const mockGetString = jest.fn((obj: any, key: string, def: string) => obj?.[key] ?? def); +const mockMergeAndFlattenToTuples = jest.fn((a: any, b: any) => [...(a || []), ...(b || [])]); +const mockGetJsonObjectFromDict = jest.fn((obj: any, key: string) => obj?.[key] || {}); + +jest.mock('../Utilities/Utils.bs.js', () => ({ + getDictFromJson: (obj: any) => mockGetDictFromJson(obj), + getString: (obj: any, key: string, def: string) => mockGetString(obj, key, def), + mergeAndFlattenToTuples: (a: any, b: any) => mockMergeAndFlattenToTuples(a, b), + messageParentWindow: (a: any, b: any) => mockMessageParentWindow(a, b), + getJsonObjectFromDict: (obj: any, key: string) => mockGetJsonObjectFromDict(obj, key), + safeParse: (str: string) => mockSafeParse(str), + postFailedSubmitResponse: (type: string, msg: string) => mockPostFailedSubmitResponse(type, msg), +})); + +jest.mock('../Utilities/PaymentBody.bs.js', () => ({ + gpayBody: jest.fn((obj: any, connectors: any) => [['google_pay', obj]]), +})); + +jest.mock('../Utilities/PaymentUtils.bs.js', () => ({ + appendedCustomerAcceptance: jest.fn((isGuest: boolean, type: string, body: any) => body), + paymentMethodListValue: { key: 'paymentMethodListValue' }, +})); + +const mockGetGooglePayRequiredFields = jest.fn((billing: any, shipping: any, fields: any, email: string) => [['required', {}]]); +const mockUsePaymentMethodTypeFromList = jest.fn(() => ({ required_fields: [] })); + +jest.mock('../Utilities/DynamicFieldsUtils.bs.js', () => ({ + getGooglePayRequiredFields: (billing: any, shipping: any, fields: any, email: string) => mockGetGooglePayRequiredFields(billing, shipping, fields, email), + usePaymentMethodTypeFromList: () => mockUsePaymentMethodTypeFromList(), +})); + +jest.mock('../Types/GooglePayType.bs.js', () => ({ + itemToObjMapper: jest.fn((obj: any) => obj || {}), + billingContactItemToObjMapper: jest.fn((obj: any) => obj || {}), + getPaymentDataFromSession: jest.fn((session: any, name: string) => JSON.stringify({ testData: 'value' })), +})); + +jest.mock('../Payments/PaymentMethodsRecord.bs.js', () => ({ + defaultList: { payment_type: 'NORMAL' }, +})); + +jest.mock('../Types/ConfirmType.bs.js', () => ({ + itemToObjMapper: jest.fn((obj: any) => ({ doSubmit: true, ...obj })), +})); + +const mockUseIsGuestCustomer = jest.fn(() => false); +jest.mock('../Hooks/UtilityHooks.bs.js', () => ({ + useIsGuestCustomer: () => mockUseIsGuestCustomer(), +})); + +jest.mock('recoil', () => { + const actualRecoil = jest.requireActual('recoil'); + return { + ...actualRecoil, + useRecoilValue: jest.fn((atom: any) => { + if (atom?.key === 'optionAtom') { + return { wallets: { walletReturnUrl: 'https://return.url' }, readOnly: false }; + } + if (atom?.key === 'keys') { + return { publishableKey: 'pk_test', iframeId: 'iframe-123' }; + } + if (atom?.key === 'isManualRetryEnabled') { + return false; + } + if (atom?.key === 'areRequiredFieldsValid') { + return true; + } + if (atom?.key === 'areRequiredFieldsEmpty') { + return false; + } + if (atom?.key === 'configAtom') { + return { localeString: { enterFieldsText: 'Please enter fields', enterValidDetailsText: 'Please enter valid details' } }; + } + return { key: 'paymentMethodListValue', payment_type: 'NORMAL' }; + }), + useSetRecoilState: jest.fn(() => jest.fn()), + }; +}); + +jest.mock('../Utilities/RecoilAtoms.bs.js', () => ({ + optionAtom: { key: 'optionAtom' }, + keys: { key: 'keys' }, + isManualRetryEnabled: { key: 'isManualRetryEnabled' }, + areRequiredFieldsValid: { key: 'areRequiredFieldsValid' }, + areRequiredFieldsEmpty: { key: 'areRequiredFieldsEmpty' }, + configAtom: { key: 'configAtom' }, +})); + +describe('GooglePayHelpers', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('getGooglePayBodyFromResponse', () => { + it('returns merged body with billing and shipping contacts', () => { + const gPayResponse = JSON.stringify({ + paymentMethodData: { + info: { + billingAddress: { name: 'John Doe', country: 'US' }, + }, + }, + shippingAddress: { name: 'Jane Doe', country: 'CA' }, + email: 'test@example.com', + }); + + const result = GooglePayHelpers.getGooglePayBodyFromResponse( + gPayResponse, + true, + undefined, + [], + undefined, + undefined, + undefined + ); + + expect(result).toBeDefined(); + }); + + it('handles gPayResponse with complete payment data', () => { + const gPayResponse = JSON.stringify({ + paymentMethodData: { + description: 'Visa •••• 4242', + info: { + billingAddress: { + name: 'Test User', + countryCode: 'US', + postalCode: '12345', + }, + }, + tokenizationData: { + type: 'PAYMENT_GATEWAY', + token: 'test-token', + }, + }, + shippingAddress: { + name: 'Shipping User', + countryCode: 'CA', + }, + email: 'shipping@example.com', + }); + + const result = GooglePayHelpers.getGooglePayBodyFromResponse( + gPayResponse, + false, + undefined, + [], + undefined, + undefined, + undefined + ); + + expect(result).toBeDefined(); + }); + + it('uses default values for optional parameters', () => { + const gPayResponse = JSON.stringify({ + paymentMethodData: { + info: { + billingAddress: {}, + }, + }, + shippingAddress: {}, + email: '', + }); + + const result = GooglePayHelpers.getGooglePayBodyFromResponse(gPayResponse, false); + + expect(result).toBeDefined(); + }); + + it('handles payment session flow', () => { + const gPayResponse = JSON.stringify({ + paymentMethodData: { + info: { + billingAddress: { country: 'US' }, + }, + }, + shippingAddress: {}, + email: 'test@test.com', + }); + + const result = GooglePayHelpers.getGooglePayBodyFromResponse( + gPayResponse, + true, + undefined, + [], + undefined, + true, + undefined + ); + + expect(result).toBeDefined(); + }); + + it('handles saved methods flow', () => { + const gPayResponse = JSON.stringify({ + paymentMethodData: { + info: { + billingAddress: {}, + }, + }, + shippingAddress: {}, + email: '', + }); + + const result = GooglePayHelpers.getGooglePayBodyFromResponse( + gPayResponse, + true, + undefined, + [], + undefined, + undefined, + true + ); + + expect(result).toBeDefined(); + }); + + it('passes required fields when not payment session or saved methods flow', () => { + const gPayResponse = JSON.stringify({ + paymentMethodData: { + info: { + billingAddress: {}, + }, + }, + shippingAddress: {}, + email: '', + }); + + const requiredFields = ['billing_address', 'email']; + const result = GooglePayHelpers.getGooglePayBodyFromResponse( + gPayResponse, + true, + undefined, + [], + requiredFields, + false, + false + ); + + expect(result).toBeDefined(); + }); + }); + + describe('processPayment', () => { + it('calls intent with correct parameters', () => { + const mockIntent = jest.fn(); + const bodyArr = [['card', { number: '4242' }]]; + const options = { wallets: { walletReturnUrl: 'https://return.url' } }; + + GooglePayHelpers.processPayment(bodyArr, undefined, mockIntent, options, 'pk_test', false); + + expect(mockIntent).toHaveBeenCalledWith( + true, + bodyArr, + { return_url: 'https://return.url', publishableKey: 'pk_test' }, + undefined, + false, + undefined, + false + ); + }); + + it('passes isThirdPartyFlow as true when provided', () => { + const mockIntent = jest.fn(); + const bodyArr = [['google_pay', { token: 'test' }]]; + const options = { wallets: { walletReturnUrl: 'https://return.url' } }; + + GooglePayHelpers.processPayment(bodyArr, true, mockIntent, options, 'pk_test', true); + + expect(mockIntent).toHaveBeenCalledWith( + true, + bodyArr, + expect.any(Object), + undefined, + true, + undefined, + true + ); + }); + + it('uses default isThirdPartyFlow value when not provided', () => { + const mockIntent = jest.fn(); + const options = { wallets: { walletReturnUrl: 'https://return.url' } }; + + GooglePayHelpers.processPayment([], undefined, mockIntent, options, 'pk_test', undefined); + + expect(mockIntent).toHaveBeenCalledWith( + expect.any(Boolean), + expect.any(Array), + expect.any(Object), + undefined, + false, + undefined, + undefined + ); + }); + }); + + describe('handleGooglePayClicked', () => { + it('sends message to parent window with payment data request', () => { + const sessionObj = { session_token_data: { secrets: { display: 'test-token' } } }; + + mockMessageParentWindow.mockImplementation(() => {}); + + GooglePayHelpers.handleGooglePayClicked(sessionObj, 'gpay-component', 'iframe-123', false); + + expect(mockMessageParentWindow).toHaveBeenCalled(); + }); + + it('does not send GpayClicked message when readOnly is true', () => { + const sessionObj = { session_token_data: { secrets: { display: 'test-token' } } }; + + mockMessageParentWindow.mockClear(); + + GooglePayHelpers.handleGooglePayClicked(sessionObj, 'gpay-component', 'iframe-123', true); + + const calls = mockMessageParentWindow.mock.calls; + const hasGpayClicked = calls.some((call: any) => { + const messageData = call[1]; + return messageData?.some((entry: any) => entry[0] === 'GpayClicked'); + }); + expect(hasGpayClicked).toBe(false); + }); + + it('sends fullscreen message before payment data', () => { + const sessionObj = {}; + + mockMessageParentWindow.mockClear(); + + GooglePayHelpers.handleGooglePayClicked(sessionObj, 'gpay-component', 'iframe-123', false); + + const firstCall = mockMessageParentWindow.mock.calls[0]; + const messageData = firstCall[1]; + const fullscreenEntry = messageData?.find((entry: any) => entry[0] === 'fullscreen'); + expect(fullscreenEntry).toBeDefined(); + expect(fullscreenEntry[1]).toBe(true); + }); + + it('includes paymentloader param in message', () => { + const sessionObj = {}; + + mockMessageParentWindow.mockClear(); + + GooglePayHelpers.handleGooglePayClicked(sessionObj, 'gpay-component', 'iframe-123', false); + + const firstCall = mockMessageParentWindow.mock.calls[0]; + const messageData = firstCall[1]; + const paramEntry = messageData?.find((entry: any) => entry[0] === 'param'); + expect(paramEntry).toBeDefined(); + expect(paramEntry[1]).toBe('paymentloader'); + }); + + it('includes iframeId in message', () => { + const sessionObj = {}; + + mockMessageParentWindow.mockClear(); + + GooglePayHelpers.handleGooglePayClicked(sessionObj, 'gpay-component', 'my-custom-iframe', false); + + const firstCall = mockMessageParentWindow.mock.calls[0]; + const messageData = firstCall[1]; + const iframeEntry = messageData?.find((entry: any) => entry[0] === 'iframeId'); + expect(iframeEntry).toBeDefined(); + expect(iframeEntry[1]).toBe('my-custom-iframe'); + }); + }); + + describe('useHandleGooglePayResponse', () => { + it('hook exists and is a function', () => { + expect(typeof GooglePayHelpers.useHandleGooglePayResponse).toBe('function'); + }); + + it('sets up message event listener on mount', () => { + const addEventListenerSpy = jest.spyOn(window, 'addEventListener'); + const removeEventListenerSpy = jest.spyOn(window, 'removeEventListener'); + + const mockIntent = jest.fn(); + const { unmount } = renderHook(() => + GooglePayHelpers.useHandleGooglePayResponse([], mockIntent) + ); + + expect(addEventListenerSpy).toHaveBeenCalledWith('message', expect.any(Function)); + + unmount(); + expect(removeEventListenerSpy).toHaveBeenCalledWith('message', expect.any(Function)); + + addEventListenerSpy.mockRestore(); + removeEventListenerSpy.mockRestore(); + }); + + it('uses default isSavedMethodsFlow when not provided', () => { + const mockIntent = jest.fn(); + renderHook(() => + GooglePayHelpers.useHandleGooglePayResponse([], mockIntent) + ); + expect(mockIntent).not.toHaveBeenCalled(); + }); + + it('uses default isWallet value when not provided', () => { + const mockIntent = jest.fn(); + renderHook(() => + GooglePayHelpers.useHandleGooglePayResponse([], mockIntent) + ); + expect(typeof GooglePayHelpers.useHandleGooglePayResponse).toBe('function'); + }); + }); + + describe('useSubmitCallback', () => { + it('hook exists and is a function', () => { + expect(typeof GooglePayHelpers.useSubmitCallback).toBe('function'); + }); + + it('returns a callback function', () => { + const sessionObj = { session_token_data: { secrets: { display: 'token' } } }; + const { result } = renderHook(() => + GooglePayHelpers.useSubmitCallback(true, sessionObj, 'gpay-component') + ); + + expect(typeof result.current).toBe('function'); + }); + + it('returns early when isWallet is true', () => { + const sessionObj = { session_token_data: { secrets: { display: 'token' } } }; + const { result } = renderHook(() => + GooglePayHelpers.useSubmitCallback(true, sessionObj, 'gpay-component') + ); + + const mockEvent = { data: JSON.stringify({ doSubmit: true }) }; + result.current(mockEvent); + + expect(mockMessageParentWindow).not.toHaveBeenCalledWith( + expect.anything(), + expect.arrayContaining([expect.arrayContaining(['GpayClicked', true])]) + ); + }); + + it('handles submit when fields are valid and not empty', () => { + const sessionObj = { session_token_data: { secrets: { display: 'token' } } }; + + const { result } = renderHook(() => + GooglePayHelpers.useSubmitCallback(false, sessionObj, 'gpay-component') + ); + + mockSafeParse.mockReturnValue(JSON.stringify({ doSubmit: true })); + mockGetDictFromJson.mockReturnValue({ doSubmit: true }); + + const mockEvent = { data: JSON.stringify({ doSubmit: true }) }; + result.current(mockEvent); + }); + + it('posts failed response when required fields are empty', () => { + const sessionObj = { session_token_data: { secrets: { display: 'token' } } }; + + const { result } = renderHook(() => + GooglePayHelpers.useSubmitCallback(false, sessionObj, 'gpay-component') + ); + + mockSafeParse.mockReturnValue(JSON.stringify({ doSubmit: true })); + mockGetDictFromJson.mockReturnValue({ doSubmit: true }); + + const mockEvent = { data: JSON.stringify({ doSubmit: true }) }; + result.current(mockEvent); + }); + + it('posts failed response when required fields are invalid', () => { + const sessionObj = { session_token_data: { secrets: { display: 'token' } } }; + + const { result } = renderHook(() => + GooglePayHelpers.useSubmitCallback(false, sessionObj, 'gpay-component') + ); + + mockSafeParse.mockReturnValue(JSON.stringify({ doSubmit: true })); + mockGetDictFromJson.mockReturnValue({ doSubmit: true }); + + const mockEvent = { data: JSON.stringify({ doSubmit: true }) }; + result.current(mockEvent); + }); + + it('does nothing when doSubmit is false', () => { + const sessionObj = { session_token_data: { secrets: { display: 'token' } } }; + + const { result } = renderHook(() => + GooglePayHelpers.useSubmitCallback(false, sessionObj, 'gpay-component') + ); + + mockSafeParse.mockReturnValue(JSON.stringify({ doSubmit: false })); + mockGetDictFromJson.mockReturnValue({ doSubmit: false }); + + mockPostFailedSubmitResponse.mockClear(); + const mockEvent = { data: JSON.stringify({ doSubmit: false }) }; + result.current(mockEvent); + + expect(mockPostFailedSubmitResponse).not.toHaveBeenCalled(); + }); + }); +}); diff --git a/src/__tests__/GooglePayType.test.ts b/src/__tests__/GooglePayType.test.ts new file mode 100644 index 000000000..dfa4e4f94 --- /dev/null +++ b/src/__tests__/GooglePayType.test.ts @@ -0,0 +1,293 @@ +import { + getLabel, + defaultTokenizationData, + defaultPaymentMethodData, + getTokenizationData, + getPaymentMethodData, + itemToObjMapper, + jsonToPaymentRequestDataType, + billingContactItemToObjMapper, + baseRequest, + getPaymentDataFromSession, +} from '../Types/GooglePayType.bs.js'; + +describe('GooglePayType', () => { + describe('getLabel', () => { + it('should return "plain" for "Default"', () => { + expect(getLabel('Default')).toBe('plain'); + }); + + it('should return "buy" for "Buy"', () => { + expect(getLabel('Buy')).toBe('buy'); + }); + + it('should return "donate" for "Donate"', () => { + expect(getLabel('Donate')).toBe('donate'); + }); + + it('should return "checkout" for "Checkout"', () => { + expect(getLabel('Checkout')).toBe('checkout'); + }); + + it('should return "subscribe" for "Subscribe"', () => { + expect(getLabel('Subscribe')).toBe('subscribe'); + }); + + it('should return "book" for "Book"', () => { + expect(getLabel('Book')).toBe('book'); + }); + + it('should return "pay" for "Pay"', () => { + expect(getLabel('Pay')).toBe('pay'); + }); + + it('should return "order" for "Order"', () => { + expect(getLabel('Order')).toBe('order'); + }); + }); + + describe('defaultTokenizationData', () => { + it('should have empty token string', () => { + expect(defaultTokenizationData.token).toBe(''); + }); + }); + + describe('defaultPaymentMethodData', () => { + it('should have empty description', () => { + expect(defaultPaymentMethodData.description).toBe(''); + }); + + it('should have empty type', () => { + expect(defaultPaymentMethodData.type).toBe(''); + }); + + it('should have empty info object', () => { + expect(defaultPaymentMethodData.info).toEqual({}); + }); + + it('should have empty tokenizationData object', () => { + expect(defaultPaymentMethodData.tokenizationData).toEqual({}); + }); + }); + + describe('getTokenizationData', () => { + it('should return default when key not found', () => { + const dict = {}; + const result = getTokenizationData('nonexistent', dict); + expect(result.token).toBe(''); + }); + + it('should return default when tokenizationData is missing', () => { + const dict = { + paymentMethodData: {}, + }; + const result = getTokenizationData('paymentMethodData', dict); + expect(result.token).toBe(''); + }); + + it('should return default tokenizationData structure', () => { + const result = getTokenizationData('anyKey', {}); + expect(result).toEqual({ token: '' }); + }); + }); + + describe('getPaymentMethodData', () => { + it('should return default when key not found', () => { + const dict = {}; + const result = getPaymentMethodData('nonexistent', dict); + expect(result.description).toBe(''); + expect(result.type).toBe(''); + }); + + it('should handle partial payment method data', () => { + const dict = { + paymentMethodData: {}, + }; + const result = getPaymentMethodData('paymentMethodData', dict); + expect(result.type).toBe(''); + expect(result.description).toBe(''); + }); + + it('should return default payment method data structure', () => { + const result = getPaymentMethodData('anyKey', {}); + expect(result.description).toBe(''); + expect(result.type).toBe(''); + expect(result.info).toEqual({}); + expect(result.tokenizationData).toEqual({}); + }); + }); + + describe('itemToObjMapper', () => { + it('should map dict to payment method data object', () => { + const dict = {}; + const result = itemToObjMapper(dict); + expect(result.paymentMethodData).toBeDefined(); + expect(result.paymentMethodData.description).toBe(''); + expect(result.paymentMethodData.type).toBe(''); + }); + + it('should handle empty dict', () => { + const result = itemToObjMapper({}); + expect(result.paymentMethodData.description).toBe(''); + expect(result.paymentMethodData.type).toBe(''); + }); + }); + + describe('jsonToPaymentRequestDataType', () => { + it('should transform keys and modify payment request', () => { + const paymentRequest: any = {}; + const jsonDict = { + allowed_payment_methods: [{ type: 'CARD' }], + transaction_info: { total_price: '10.00' }, + merchant_info: { merchant_name: 'Test Merchant' }, + }; + const result = jsonToPaymentRequestDataType(paymentRequest, jsonDict); + expect(result.allowedPaymentMethods).toBeDefined(); + expect(result.allowedPaymentMethods.length).toBe(1); + }); + + it('should handle empty arrays', () => { + const paymentRequest: any = {}; + const jsonDict = { + allowed_payment_methods: [], + transaction_info: null, + merchant_info: null, + }; + const result = jsonToPaymentRequestDataType(paymentRequest, jsonDict); + expect(result.allowedPaymentMethods).toEqual([]); + }); + + it('should return the modified payment request object', () => { + const paymentRequest: any = {}; + const jsonDict = { + allowed_payment_methods: [], + }; + const result = jsonToPaymentRequestDataType(paymentRequest, jsonDict); + expect(result).toBe(paymentRequest); + }); + }); + + describe('billingContactItemToObjMapper', () => { + it('should map all billing contact fields', () => { + const dict = { + address1: '123 Main St', + address2: 'Apt 4', + address3: 'Floor 2', + administrativeArea: 'CA', + countryCode: 'US', + locality: 'San Francisco', + name: 'John Doe', + phoneNumber: '+14155551234', + postalCode: '94105', + sortingCode: 'ABC123', + }; + const result = billingContactItemToObjMapper(dict); + expect(result.address1).toBe('123 Main St'); + expect(result.address2).toBe('Apt 4'); + expect(result.address3).toBe('Floor 2'); + expect(result.administrativeArea).toBe('CA'); + expect(result.countryCode).toBe('US'); + expect(result.locality).toBe('San Francisco'); + expect(result.name).toBe('John Doe'); + expect(result.phoneNumber).toBe('+14155551234'); + expect(result.postalCode).toBe('94105'); + expect(result.sortingCode).toBe('ABC123'); + }); + + it('should handle empty dict with defaults', () => { + const result = billingContactItemToObjMapper({}); + expect(result.address1).toBe(''); + expect(result.address2).toBe(''); + expect(result.address3).toBe(''); + expect(result.administrativeArea).toBe(''); + expect(result.countryCode).toBe(''); + expect(result.locality).toBe(''); + expect(result.name).toBe(''); + expect(result.phoneNumber).toBe(''); + expect(result.postalCode).toBe(''); + expect(result.sortingCode).toBe(''); + }); + + it('should handle partial billing contact', () => { + const dict = { + countryCode: 'GB', + postalCode: 'SW1A 1AA', + }; + const result = billingContactItemToObjMapper(dict); + expect(result.countryCode).toBe('GB'); + expect(result.postalCode).toBe('SW1A 1AA'); + expect(result.address1).toBe(''); + }); + }); + + describe('baseRequest', () => { + it('should have apiVersion 2', () => { + expect(baseRequest.apiVersion).toBe(2); + }); + + it('should have apiVersionMinor 0', () => { + expect(baseRequest.apiVersionMinor).toBe(0); + }); + }); + + describe('getPaymentDataFromSession', () => { + it('should build payment data request from session object', () => { + const sessionObj = { + allowed_payment_methods: [{ type: 'CARD' }], + transaction_info: { totalPrice: '10.00', currencyCode: 'USD' }, + merchant_info: { merchantName: 'Test' }, + emailRequired: true, + }; + const result = getPaymentDataFromSession(sessionObj, 'googlePay'); + expect(result.apiVersion).toBe(2); + expect(result.apiVersionMinor).toBe(0); + expect(result.emailRequired).toBe(true); + }); + + it('should handle undefined session object', () => { + const result = getPaymentDataFromSession(undefined, 'googlePay'); + expect(result.apiVersion).toBe(2); + expect(result.apiVersionMinor).toBe(0); + }); + + it('should add shipping address parameters for express checkout', () => { + const sessionObj = { + allowed_payment_methods: [], + transaction_info: {}, + merchant_info: {}, + emailRequired: false, + shippingAddressRequired: true, + shippingAddressParameters: { allowedCountryCodes: ['US'] }, + }; + const result = getPaymentDataFromSession(sessionObj, 'expressCheckout'); + expect(result.shippingAddressRequired).toBe(true); + expect(result.callbackIntents).toEqual(['SHIPPING_ADDRESS']); + }); + + it('should not add shipping address for non-express checkout component', () => { + const sessionObj = { + allowed_payment_methods: [], + transaction_info: {}, + merchant_info: {}, + emailRequired: false, + shippingAddressRequired: true, + }; + const result = getPaymentDataFromSession(sessionObj, 'payment'); + expect(result.shippingAddressRequired).toBeUndefined(); + }); + + it('should add shipping address for googlePay express checkout', () => { + const sessionObj = { + allowed_payment_methods: [], + transaction_info: {}, + merchant_info: {}, + emailRequired: false, + shippingAddressRequired: true, + shippingAddressParameters: {}, + }; + const result = getPaymentDataFromSession(sessionObj, 'googlePay'); + expect(result.shippingAddressRequired).toBe(true); + expect(result.callbackIntents).toEqual(['SHIPPING_ADDRESS']); + }); + }); +}); diff --git a/src/__tests__/HyperLogger.test.ts b/src/__tests__/HyperLogger.test.ts new file mode 100644 index 000000000..bf32cd647 --- /dev/null +++ b/src/__tests__/HyperLogger.test.ts @@ -0,0 +1,550 @@ +import { + logFileToObj, + getRefFromOption, + getSourceString, + make, +} from '../hyper-log-catcher/HyperLogger.bs.js'; +import * as LoggerUtils from '../Utilities/LoggerUtils.bs.js'; +import * as CardThemeType from '../Types/CardThemeType.bs.js'; + +jest.mock('../Utilities/LoggerUtils.bs.js', () => ({ + convertToScreamingSnakeCase: jest.fn((str) => str?.toUpperCase() || ''), + eventNameToStrMapper: jest.fn((eventName) => eventName), + toSnakeCaseWithSeparator: jest.fn((str, sep) => str?.toLowerCase().replace(/ /g, sep) || ''), + retrieveLogsFromIndexedDB: jest.fn(), + clearLogsFromIndexedDB: jest.fn(), + saveLogsToIndexedDB: jest.fn(), + getPaymentId: jest.fn((secret) => secret?.split('_')[0] || ''), +})); + +jest.mock('../Hooks/NetworkInformation.bs.js', () => ({ + getNetworkState: jest.fn(() => ({ _0: { isOnline: true } })), + defaultNetworkState: { isOnline: true }, +})); + +jest.mock('../Utilities/Utils.bs.js', () => ({ + getStringFromBool: jest.fn((bool) => bool?.toString() || 'false'), + arrayOfNameAndVersion: ['Chrome', '120'], +})); + +jest.mock('../Types/CardThemeType.bs.js', () => ({ + getPaymentModeToStrMapper: jest.fn((mode) => mode), +})); + +describe('HyperLogger', () => { + beforeEach(() => { + jest.clearAllMocks(); + (window as any).navigator = { + platform: 'MacIntel', + userAgent: 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7)', + sendBeacon: jest.fn(), + }; + (window as any).addEventListener = jest.fn(); + (window as any).removeEventListener = jest.fn(); + }); + + describe('logFileToObj', () => { + it('should convert log file with DEBUG log type', () => { + const logFile = { + logType: 'DEBUG', + category: 'API', + source: 'hyper_loader', + version: '1.0.0', + value: 'test value', + sessionId: 'session123', + merchantId: 'merchant456', + paymentId: 'payment789', + appId: 'app001', + platform: 'web', + userAgent: 'Mozilla/5.0', + eventName: 'APP_RENDERED', + browserName: 'Chrome', + browserVersion: '120', + latency: '100', + firstEvent: true, + paymentMethod: 'card', + timestamp: '1234567890', + }; + + const result = logFileToObj(logFile); + + expect(result.log_type).toBe('DEBUG'); + expect(result.category).toBe('API'); + expect(result.component).toBe('WEB'); + expect(result.timestamp).toBe('1234567890'); + }); + + it('should convert log file with ERROR log type', () => { + const logFile = { + logType: 'ERROR', + category: 'USER_ERROR', + source: 'hyper_payment', + version: '1.0.0', + value: 'error occurred', + sessionId: 'session123', + merchantId: 'merchant456', + paymentId: 'payment789', + appId: 'app001', + platform: 'web', + userAgent: 'Mozilla/5.0', + eventName: 'SDK_CRASH', + browserName: 'Firefox', + browserVersion: '121', + latency: '', + firstEvent: false, + paymentMethod: 'paypal', + timestamp: '1234567891', + }; + + const result = logFileToObj(logFile); + + expect(result.log_type).toBe('ERROR'); + expect(result.category).toBe('USER_ERROR'); + }); + + it('should convert log file with INFO log type', () => { + const logFile = { + logType: 'INFO', + category: 'USER_EVENT', + source: 'headless', + version: '2.0.0', + value: 'user action', + sessionId: 'session456', + merchantId: 'merchant789', + paymentId: 'payment001', + appId: 'app002', + platform: 'ios', + userAgent: 'Safari/605.1.15', + eventName: 'PAYMENT_ATTEMPT', + browserName: 'Safari', + browserVersion: '17', + latency: '50', + firstEvent: true, + paymentMethod: 'apple_pay', + timestamp: '1234567892', + }; + + const result = logFileToObj(logFile); + + expect(result.log_type).toBe('INFO'); + expect(result.category).toBe('USER_EVENT'); + }); + + it('should convert log file with WARNING log type', () => { + const logFile = { + logType: 'WARNING', + category: 'MERCHANT_EVENT', + source: 'test', + version: '1.0.0', + value: 'warning message', + sessionId: 'session789', + merchantId: 'merchant001', + paymentId: 'payment002', + appId: 'app003', + platform: 'android', + userAgent: 'Chrome Mobile', + eventName: 'PAYMENT_METHOD_CHANGED', + browserName: 'Chrome', + browserVersion: '120', + latency: '', + firstEvent: true, + paymentMethod: 'google_pay', + timestamp: '1234567893', + }; + + const result = logFileToObj(logFile); + + expect(result.log_type).toBe('WARNING'); + expect(result.category).toBe('MERCHANT_EVENT'); + }); + + it('should convert log file with SILENT log type', () => { + const logFile = { + logType: 'SILENT', + category: 'API', + source: 'silent', + version: '1.0.0', + value: '', + sessionId: 'session999', + merchantId: 'merchant999', + paymentId: 'payment999', + appId: 'app999', + platform: 'web', + userAgent: 'Bot/1.0', + eventName: 'LOG_INITIATED', + browserName: 'Unknown', + browserVersion: '0', + latency: '', + firstEvent: true, + paymentMethod: '', + timestamp: '1234567894', + }; + + const result = logFileToObj(logFile); + + expect(result.log_type).toBe('SILENT'); + }); + + it('should handle undefined values gracefully', () => { + const logFile = { + logType: 'INFO', + category: 'API', + source: undefined, + version: undefined, + value: undefined, + sessionId: undefined, + merchantId: undefined, + paymentId: undefined, + appId: undefined, + platform: undefined, + userAgent: undefined, + eventName: undefined, + browserName: undefined, + browserVersion: undefined, + latency: undefined, + firstEvent: undefined, + paymentMethod: undefined, + timestamp: undefined, + }; + + const result = logFileToObj(logFile); + + expect(result.component).toBe('WEB'); + expect(result).toHaveProperty('timestamp'); + }); + }); + + describe('getRefFromOption', () => { + it('should create ref with provided value', () => { + const result = getRefFromOption('test-value'); + expect(result.contents).toBe('test-value'); + }); + + it('should create ref with empty string for undefined', () => { + const result = getRefFromOption(undefined); + expect(result.contents).toBe(''); + }); + + it('should handle null value gracefully', () => { + const result = getRefFromOption(null as any); + expect(result.contents).toBeNull(); + }); + + it('should create ref that can be updated', () => { + const result = getRefFromOption('initial'); + expect(result.contents).toBe('initial'); + result.contents = 'updated'; + expect(result.contents).toBe('updated'); + }); + }); + + describe('getSourceString', () => { + it('should return hyper_loader for Loader source', () => { + const result = getSourceString('Loader'); + expect(result).toBe('hyper_loader'); + }); + + it('should return headless for non-Loader string source', () => { + const result = getSourceString('Headless' as any); + expect(result).toBe('headless'); + }); + + it('should format payment mode source correctly', () => { + (CardThemeType.getPaymentModeToStrMapper as jest.Mock).mockReturnValue('payment'); + const result = getSourceString({ _0: 'payment' } as any); + expect(result).toContain('hyper'); + }); + + it('should handle Payment mode with snake case conversion', () => { + (CardThemeType.getPaymentModeToStrMapper as jest.Mock).mockReturnValue('card payment'); + const result = getSourceString({ _0: 'card payment' } as any); + expect(result).toBe('hypercard_payment'); + }); + + it('should handle Checkout mode', () => { + (CardThemeType.getPaymentModeToStrMapper as jest.Mock).mockReturnValue('checkout'); + const result = getSourceString({ _0: 'checkout' } as any); + expect(result).toBe('hypercheckout'); + }); + }); + + describe('make', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it('should create logger with setLogInfo method', () => { + const logger = make('session123', 'Loader', 'secret_456', 'merchant789', null); + expect(logger.setLogInfo).toBeDefined(); + expect(typeof logger.setLogInfo).toBe('function'); + }); + + it('should create logger with setLogError method', () => { + const logger = make('session123', 'Loader', 'secret_456', 'merchant789', null); + expect(logger.setLogError).toBeDefined(); + expect(typeof logger.setLogError).toBe('function'); + }); + + it('should create logger with setLogApi method', () => { + const logger = make('session123', 'Loader', 'secret_456', 'merchant789', null); + expect(logger.setLogApi).toBeDefined(); + expect(typeof logger.setLogApi).toBe('function'); + }); + + it('should create logger with setLogInitiated method', () => { + const logger = make('session123', 'Loader', 'secret_456', 'merchant789', null); + expect(logger.setLogInitiated).toBeDefined(); + expect(typeof logger.setLogInitiated).toBe('function'); + }); + + it('should create logger with sendLogs method', () => { + const logger = make('session123', 'Loader', 'secret_456', 'merchant789', null); + expect(logger.sendLogs).toBeDefined(); + expect(typeof logger.sendLogs).toBe('function'); + }); + + it('should create logger with setConfirmPaymentValue method', () => { + const logger = make('session123', 'Loader', 'secret_456', 'merchant789', null); + expect(logger.setConfirmPaymentValue).toBeDefined(); + expect(typeof logger.setConfirmPaymentValue).toBe('function'); + }); + + it('should create logger with setter methods', () => { + const logger = make('session123', 'Loader', 'secret_456', 'merchant789', null); + expect(logger.setSessionId).toBeDefined(); + expect(logger.setClientSecret).toBeDefined(); + expect(logger.setMerchantId).toBeDefined(); + expect(logger.setMetadata).toBeDefined(); + expect(logger.setSource).toBeDefined(); + }); + + it('should set sessionId correctly', () => { + const logger = make('initial', 'Loader', 'secret_456', 'merchant789', null); + logger.setSessionId('new-session'); + expect(typeof logger.setSessionId).toBe('function'); + }); + + it('should set clientSecret correctly', () => { + const logger = make('session123', 'Loader', 'initial_secret', 'merchant789', null); + logger.setClientSecret('new_secret_789'); + expect(typeof logger.setClientSecret).toBe('function'); + }); + + it('should set merchantId correctly', () => { + const logger = make('session123', 'Loader', 'secret_456', 'initial', null); + logger.setMerchantId('new-merchant'); + expect(typeof logger.setMerchantId).toBe('function'); + }); + + it('should set metadata correctly', () => { + const logger = make('session123', 'Loader', 'secret_456', 'merchant789', { key: 'value' }); + logger.setMetadata({ newKey: 'newValue' }); + expect(typeof logger.setMetadata).toBe('function'); + }); + + it('should set source correctly', () => { + const logger = make('session123', 'Loader', 'secret_456', 'merchant789', null); + logger.setSource('new-source'); + expect(typeof logger.setSource).toBe('function'); + }); + + it('should create confirmPayment value object', () => { + const logger = make('session123', 'Loader', 'secret_456', 'merchant789', null); + const result = logger.setConfirmPaymentValue('card'); + expect(result).toEqual({ + method: 'confirmPayment', + type: 'card', + }); + }); + + it('should handle different payment types in setConfirmPaymentValue', () => { + const logger = make('session123', 'Loader', 'secret_456', 'merchant789', null); + const paypalResult = logger.setConfirmPaymentValue('paypal'); + expect(paypalResult).toEqual({ + method: 'confirmPayment', + type: 'paypal', + }); + }); + + it('should call sendBeacon when sendLogs is invoked with data', async () => { + const sendBeaconMock = jest.fn(); + (window.navigator as any).sendBeacon = sendBeaconMock; + + const logger = make('session123', 'Loader', 'secret_456', 'merchant789', null); + logger.setLogInfo('test', 'APP_RENDERED', '1234567890'); + + jest.advanceTimersByTime(20000); + + expect(sendBeaconMock).toBeDefined(); + }); + + it('should handle headless source', () => { + const logger = make('session123', 'Headless' as any, 'secret_456', 'merchant789', null); + expect(logger.setLogInfo).toBeDefined(); + }); + + it('should handle empty sessionId', () => { + const logger = make('', 'Loader', 'secret_456', 'merchant789', null); + expect(logger.setLogInfo).toBeDefined(); + }); + + it('should handle empty clientSecret', () => { + const logger = make('session123', 'Loader', '', 'merchant789', null); + expect(logger.setLogInfo).toBeDefined(); + }); + + it('should handle empty merchantId', () => { + const logger = make('session123', 'Loader', 'secret_456', '', null); + expect(logger.setLogInfo).toBeDefined(); + }); + + it('should handle null metadata', () => { + const logger = make('session123', 'Loader', 'secret_456', 'merchant789', null); + expect(logger.setLogInfo).toBeDefined(); + }); + + it('should handle object metadata', () => { + const metadata = { customField: 'customValue', nested: { key: 'value' } }; + const logger = make('session123', 'Loader', 'secret_456', 'merchant789', metadata); + expect(logger.setLogInfo).toBeDefined(); + }); + + it('should handle payment mode source object', () => { + const source = { _0: 'payment' }; + const logger = make('session123', source as any, 'secret_456', 'merchant789', null); + expect(logger.setLogInfo).toBeDefined(); + }); + + it('should add beforeunload event listener', () => { + make('session123', 'Loader', 'secret_456', 'merchant789', null); + expect(window.addEventListener).toHaveBeenCalledWith('beforeunload', expect.any(Function)); + }); + }); + + describe('make - logging methods behavior', () => { + beforeEach(() => { + jest.useFakeTimers(); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + + it('should call setLogInfo with correct parameters', () => { + const logger = make('session123', 'Loader', 'secret_456', 'merchant789', null); + + expect(() => { + logger.setLogInfo('test value', 'APP_RENDERED', '1234567890', 'INFO', 'USER_EVENT', 'card'); + }).not.toThrow(); + }); + + it('should call setLogError with correct parameters', () => { + const logger = make('session123', 'Loader', 'secret_456', 'merchant789', null); + + expect(() => { + logger.setLogError('error message', 'SDK_CRASH', '1234567890', '', 'ERROR', 'USER_ERROR', 'card'); + }).not.toThrow(); + }); + + it('should call setLogApi with correct parameters', () => { + const logger = make('session123', 'Loader', 'secret_456', 'merchant789', null); + + expect(() => { + logger.setLogApi( + { TAG: 'StringType', _0: 'api data' }, + 'PAYMENT_METHODS_CALL', + '1234567890', + 'INFO', + 'API', + 'card', + 'Request', + false + ); + }).not.toThrow(); + }); + + it('should call setLogInitiated', () => { + const logger = make('session123', 'Loader', 'secret_456', 'merchant789', null); + + expect(() => { + logger.setLogInitiated(); + }).not.toThrow(); + }); + + it('should handle ArrayType in setLogApi', () => { + const logger = make('session123', 'Loader', 'secret_456', 'merchant789', null); + + expect(() => { + logger.setLogApi( + { TAG: 'ArrayType', _0: [['key', 'value']] }, + 'PAYMENT_METHODS_CALL', + '1234567890' + ); + }).not.toThrow(); + }); + + it('should handle optional parameters with defaults in setLogInfo', () => { + const logger = make('session123', 'Loader', 'secret_456', 'merchant789', null); + + expect(() => { + logger.setLogInfo('test value', 'APP_RENDERED'); + }).not.toThrow(); + }); + + it('should handle optional parameters with defaults in setLogError', () => { + const logger = make('session123', 'Loader', 'secret_456', 'merchant789', null); + + expect(() => { + logger.setLogError('error message', 'SDK_CRASH'); + }).not.toThrow(); + }); + + it('should handle optional parameters with defaults in setLogApi', () => { + const logger = make('session123', 'Loader', 'secret_456', 'merchant789', null); + + expect(() => { + logger.setLogApi({ TAG: 'StringType', _0: 'data' }, 'PAYMENT_METHODS_CALL'); + }).not.toThrow(); + }); + }); + + describe('edge cases', () => { + it('should handle very long sessionId', () => { + const longSessionId = 'a'.repeat(1000); + const result = getRefFromOption(longSessionId); + expect(result.contents).toBe(longSessionId); + }); + + it('should handle special characters in sessionId', () => { + const specialSessionId = 'session-123_test.456'; + const result = getRefFromOption(specialSessionId); + expect(result.contents).toBe(specialSessionId); + }); + + it('should handle unicode characters', () => { + const unicodeString = '测试-テスト-테스트'; + const result = getRefFromOption(unicodeString); + expect(result.contents).toBe(unicodeString); + }); + + it('should handle empty string in getSourceString', () => { + const result = getSourceString('' as any); + expect(result).toBe('headless'); + }); + + it('should handle object source with payment mode', () => { + (CardThemeType.getPaymentModeToStrMapper as jest.Mock).mockReturnValue('checkout'); + const result = getSourceString({ _0: 'checkout' } as any); + expect(result).toContain('hyper'); + }); + + it('should handle undefined in getSourceString', () => { + const result = getSourceString(undefined as any); + expect(result).toBe('headless'); + }); + }); +}); diff --git a/src/__tests__/HyperVaultHelpers.test.ts b/src/__tests__/HyperVaultHelpers.test.ts new file mode 100644 index 000000000..201a52fca --- /dev/null +++ b/src/__tests__/HyperVaultHelpers.test.ts @@ -0,0 +1,80 @@ +import { extractVaultMetadata } from '../Utilities/HyperVaultHelpers.bs.js'; + +describe('HyperVaultHelpers', () => { + describe('extractVaultMetadata', () => { + it('should extract all vault metadata fields from dict', () => { + const metadataDict = { + pmSessionId: 'session-123', + pmClientSecret: 'secret-abc', + vaultPublishableKey: 'pk_test_123', + vaultProfileId: 'profile-456', + endpoint: 'https://api.example.com', + customPodUri: 'https://custom.pod.com', + config: { timeout: 30000 }, + }; + + const result = extractVaultMetadata(metadataDict); + + expect(result.pmSessionId).toBe('session-123'); + expect(result.pmClientSecret).toBe('secret-abc'); + expect(result.vaultPublishableKey).toBe('pk_test_123'); + expect(result.vaultProfileId).toBe('profile-456'); + expect(result.endpoint).toBe('https://api.example.com'); + expect(result.customPodUri).toBe('https://custom.pod.com'); + expect(result.config).toEqual({ timeout: 30000 }); + }); + + it('should return empty strings for missing fields', () => { + const metadataDict = { + config: {}, + }; + + const result = extractVaultMetadata(metadataDict); + + expect(result.pmSessionId).toBe(''); + expect(result.pmClientSecret).toBe(''); + expect(result.vaultPublishableKey).toBe(''); + expect(result.vaultProfileId).toBe(''); + expect(result.endpoint).toBe(''); + expect(result.customPodUri).toBe(''); + }); + + it('should handle empty dict', () => { + const metadataDict = {}; + + const result = extractVaultMetadata(metadataDict); + + expect(result.pmSessionId).toBe(''); + expect(result.config).toEqual({}); + }); + + it('should handle partial metadata', () => { + const metadataDict = { + pmSessionId: 'session-123', + vaultPublishableKey: 'pk_test_123', + }; + + const result = extractVaultMetadata(metadataDict); + + expect(result.pmSessionId).toBe('session-123'); + expect(result.vaultPublishableKey).toBe('pk_test_123'); + expect(result.pmClientSecret).toBe(''); + expect(result.vaultProfileId).toBe(''); + }); + + it('should preserve config object', () => { + const configObj = { + timeout: 30000, + retries: 3, + headers: { 'X-Custom': 'value' }, + }; + const metadataDict = { + config: configObj, + }; + + const result = extractVaultMetadata(metadataDict); + + expect(result.config).toEqual(configObj); + }); + }); +}); diff --git a/src/__tests__/LocaleStringHelper.test.ts b/src/__tests__/LocaleStringHelper.test.ts new file mode 100644 index 000000000..884e41a19 --- /dev/null +++ b/src/__tests__/LocaleStringHelper.test.ts @@ -0,0 +1,137 @@ +import { mapLocalStringToTypeLocale } from '../LocaleStrings/LocaleStringHelper.bs.js'; + +describe('LocaleStringHelper', () => { + describe('mapLocalStringToTypeLocale', () => { + describe('exact matches', () => { + it('should return EN for "en"', () => { + expect(mapLocalStringToTypeLocale('en')).toBe('EN'); + }); + + it('should return EN_GB for "en-gb"', () => { + expect(mapLocalStringToTypeLocale('en-gb')).toBe('EN_GB'); + }); + + it('should return ES for "es"', () => { + expect(mapLocalStringToTypeLocale('es')).toBe('ES'); + }); + + it('should return FR for "fr"', () => { + expect(mapLocalStringToTypeLocale('fr')).toBe('FR'); + }); + + it('should return FR_BE for "fr-be"', () => { + expect(mapLocalStringToTypeLocale('fr-be')).toBe('FR_BE'); + }); + + it('should return DE for "de"', () => { + expect(mapLocalStringToTypeLocale('de')).toBe('DE'); + }); + + it('should return IT for "it"', () => { + expect(mapLocalStringToTypeLocale('it')).toBe('IT'); + }); + + it('should return JA for "ja"', () => { + expect(mapLocalStringToTypeLocale('ja')).toBe('JA'); + }); + + it('should return NL for "nl"', () => { + expect(mapLocalStringToTypeLocale('nl')).toBe('NL'); + }); + + it('should return PL for "pl"', () => { + expect(mapLocalStringToTypeLocale('pl')).toBe('PL'); + }); + + it('should return PT for "pt"', () => { + expect(mapLocalStringToTypeLocale('pt')).toBe('PT'); + }); + + it('should return RU for "ru"', () => { + expect(mapLocalStringToTypeLocale('ru')).toBe('RU'); + }); + + it('should return SV for "sv"', () => { + expect(mapLocalStringToTypeLocale('sv')).toBe('SV'); + }); + + it('should return AR for "ar"', () => { + expect(mapLocalStringToTypeLocale('ar')).toBe('AR'); + }); + + it('should return HE for "he"', () => { + expect(mapLocalStringToTypeLocale('he')).toBe('HE'); + }); + + it('should return ZH for "zh"', () => { + expect(mapLocalStringToTypeLocale('zh')).toBe('ZH'); + }); + + it('should return ZH_HANT for "zh-hant"', () => { + expect(mapLocalStringToTypeLocale('zh-hant')).toBe('ZH_HANT'); + }); + + it('should return CA for "ca"', () => { + expect(mapLocalStringToTypeLocale('ca')).toBe('CA'); + }); + }); + + describe('case insensitivity', () => { + it('should handle uppercase input "EN"', () => { + expect(mapLocalStringToTypeLocale('EN')).toBe('EN'); + }); + + it('should handle mixed case input "En"', () => { + expect(mapLocalStringToTypeLocale('En')).toBe('EN'); + }); + + it('should handle uppercase "EN-GB"', () => { + expect(mapLocalStringToTypeLocale('EN-GB')).toBe('EN_GB'); + }); + + it('should handle mixed case "Fr-Be"', () => { + expect(mapLocalStringToTypeLocale('Fr-Be')).toBe('FR_BE'); + }); + }); + + describe('fallback to base language', () => { + it('should return ES for "es-MX" (fallback to base language)', () => { + expect(mapLocalStringToTypeLocale('es-MX')).toBe('ES'); + }); + + it('should return FR for "fr-CA" (fallback to base language)', () => { + expect(mapLocalStringToTypeLocale('fr-CA')).toBe('FR'); + }); + + it('should return DE for "de-AT" (fallback to base language)', () => { + expect(mapLocalStringToTypeLocale('de-AT')).toBe('DE'); + }); + + it('should return PT for "pt-BR" (fallback to base language)', () => { + expect(mapLocalStringToTypeLocale('pt-BR')).toBe('PT'); + }); + + it('should return ZH for "zh-CN" (fallback to base language)', () => { + expect(mapLocalStringToTypeLocale('zh-CN')).toBe('ZH'); + }); + }); + + describe('default fallback', () => { + it('should return EN for unknown locale', () => { + expect(mapLocalStringToTypeLocale('unknown')).toBe('EN'); + }); + + it('should return EN for empty string', () => { + expect(mapLocalStringToTypeLocale('')).toBe('EN'); + }); + + it('should return EN for non-locale string', () => { + expect(mapLocalStringToTypeLocale('xyz')).toBe('EN'); + }); + + it('should return EN for locale-like unknown string', () => { + expect(mapLocalStringToTypeLocale('xx-YY')).toBe('EN'); + }); + }); + }); +}); diff --git a/src/__tests__/LogAPIResponse.test.ts b/src/__tests__/LogAPIResponse.test.ts new file mode 100644 index 000000000..2eb2b78d5 --- /dev/null +++ b/src/__tests__/LogAPIResponse.test.ts @@ -0,0 +1,165 @@ +import { logApiResponse } from '../hyper-log-catcher/LogAPIResponse.bs.js'; + +describe('LogAPIResponse', () => { + describe('logApiResponse', () => { + let mockLogger: { setLogApi: jest.Mock }; + + beforeEach(() => { + mockLogger = { + setLogApi: jest.fn(), + }; + }); + + it('should call logger.setLogApi with Success status mapping', () => { + logApiResponse( + mockLogger as any, + 'https://api.example.com/payments', + 'CONFIRM_CALL', + 'Success', + 200, + { id: 'payment_123' }, + false + ); + + expect(mockLogger.setLogApi).toHaveBeenCalled(); + const call = mockLogger.setLogApi.mock.calls[0]; + expect(call[1]).toBe('CONFIRM_CALL'); + expect(call[3]).toBe('INFO'); + expect(call[6]).toBe('Response'); + }); + + it('should call logger.setLogApi with Error status mapping', () => { + logApiResponse( + mockLogger as any, + 'https://api.example.com/payments', + 'CONFIRM_CALL', + 'Error', + 400, + { error: 'Invalid request' }, + false + ); + + expect(mockLogger.setLogApi).toHaveBeenCalled(); + const call = mockLogger.setLogApi.mock.calls[0]; + expect(call[3]).toBe('ERROR'); + expect(call[6]).toBe('Err'); + }); + + it('should call logger.setLogApi with Exception status mapping', () => { + logApiResponse( + mockLogger as any, + 'https://api.example.com/payments', + 'SESSIONS_CALL', + 'Exception', + 504, + { message: 'Timeout' }, + false + ); + + expect(mockLogger.setLogApi).toHaveBeenCalled(); + const call = mockLogger.setLogApi.mock.calls[0]; + expect(call[3]).toBe('ERROR'); + expect(call[6]).toBe('NoResponse'); + }); + + it('should call logger.setLogApi with Request status mapping', () => { + logApiResponse( + mockLogger as any, + 'https://api.example.com/payments', + 'PAYMENT_METHODS_CALL', + 'Request', + 0, + {}, + false + ); + + expect(mockLogger.setLogApi).toHaveBeenCalled(); + const call = mockLogger.setLogApi.mock.calls[0]; + expect(call[3]).toBe('INFO'); + expect(call[6]).toBe('Request'); + }); + + it('should return early if eventName is undefined', () => { + logApiResponse( + mockLogger as any, + 'https://api.example.com/payments', + undefined, + 'Success', + 200, + { id: 'payment_123' }, + false + ); + + expect(mockLogger.setLogApi).not.toHaveBeenCalled(); + }); + + it('should pass isPaymentSession parameter', () => { + logApiResponse( + mockLogger as any, + 'https://api.example.com/payments', + 'CONFIRM_CALL', + 'Success', + 200, + { id: 'payment_123' }, + true + ); + + expect(mockLogger.setLogApi).toHaveBeenCalled(); + const call = mockLogger.setLogApi.mock.calls[0]; + expect(call[7]).toBe(true); + }); + + it('should pass logCategory as API', () => { + logApiResponse( + mockLogger as any, + 'https://api.example.com/payments', + 'RETRIEVE_CALL', + 'Success', + 200, + { payment: { id: 'pay_123' } }, + false + ); + + expect(mockLogger.setLogApi).toHaveBeenCalled(); + const call = mockLogger.setLogApi.mock.calls[0]; + expect(call[4]).toBe('API'); + }); + + it('should pass data object with url and statusCode for Success', () => { + logApiResponse( + mockLogger as any, + 'https://api.example.com/test', + 'CONFIRM_CALL', + 'Success', + 200, + { id: 'pay_123' }, + false + ); + + expect(mockLogger.setLogApi).toHaveBeenCalled(); + const call = mockLogger.setLogApi.mock.calls[0]; + expect(call[0].TAG).toBe('ArrayType'); + expect(call[0]._0).toEqual([ + ['url', 'https://api.example.com/test'], + ['statusCode', 200], + ]); + }); + + it('should pass data object with url for Request', () => { + logApiResponse( + mockLogger as any, + 'https://api.example.com/request', + 'SESSIONS_CALL', + 'Request', + 0, + {}, + false + ); + + expect(mockLogger.setLogApi).toHaveBeenCalled(); + const call = mockLogger.setLogApi.mock.calls[0]; + expect(call[0].TAG).toBe('ArrayType'); + expect(call[0]._0).toEqual([['url', 'https://api.example.com/request']]); + }); + }); +}); diff --git a/src/__tests__/LoggerUtils.test.ts b/src/__tests__/LoggerUtils.test.ts new file mode 100644 index 000000000..fd93f95f4 --- /dev/null +++ b/src/__tests__/LoggerUtils.test.ts @@ -0,0 +1,495 @@ +import { + logApi, + logInputChangeInfo, + handleLogging, + eventNameToStrMapper, + getPaymentId, + convertToScreamingSnakeCase, + toSnakeCaseWithSeparator, + defaultLoggerConfig, + apiEventInitMapper, +} from '../Utilities/LoggerUtils.bs.js'; + +describe('LoggerUtils', () => { + describe('getPaymentId', () => { + it('should extract payment ID from client secret', () => { + const clientSecret = 'pay_12345_secret_abcdef'; + const result = getPaymentId(clientSecret); + expect(result).toBe('pay_12345'); + }); + + it('should handle client secret without secret suffix', () => { + const clientSecret = 'pay_67890'; + const result = getPaymentId(clientSecret); + expect(result).toBe('pay_67890'); + }); + + it('should return full string if no separator found', () => { + const clientSecret = 'simplepaymentid'; + const result = getPaymentId(clientSecret); + expect(result).toBe('simplepaymentid'); + }); + + it('should handle empty string', () => { + const result = getPaymentId(''); + expect(result).toBe(''); + }); + + it('should handle multiple secret separators', () => { + const clientSecret = 'pay_123_secret_abc_secret_def'; + const result = getPaymentId(clientSecret); + expect(result).toBe('pay_123'); + }); + }); + + describe('convertToScreamingSnakeCase', () => { + it('should convert space-separated text to SCREAMING_SNAKE_CASE', () => { + expect(convertToScreamingSnakeCase('hello world')).toBe('HELLO_WORLD'); + }); + + it('should handle single word', () => { + expect(convertToScreamingSnakeCase('hello')).toBe('HELLO'); + }); + + it('should trim leading and trailing spaces', () => { + expect(convertToScreamingSnakeCase(' hello world ')).toBe('HELLO_WORLD'); + }); + + it('should handle multiple spaces between words', () => { + expect(convertToScreamingSnakeCase('hello world')).toBe('HELLO__WORLD'); + }); + + it('should handle already uppercase text', () => { + expect(convertToScreamingSnakeCase('HELLO WORLD')).toBe('HELLO_WORLD'); + }); + + it('should handle mixed case text', () => { + expect(convertToScreamingSnakeCase('Hello World')).toBe('HELLO_WORLD'); + }); + + it('should handle empty string', () => { + expect(convertToScreamingSnakeCase('')).toBe(''); + }); + }); + + describe('toSnakeCaseWithSeparator', () => { + it('should convert camelCase to snake_case with separator', () => { + expect(toSnakeCaseWithSeparator('helloWorld', '_')).toBe('hello_world'); + }); + + it('should convert PascalCase to snake_case', () => { + expect(toSnakeCaseWithSeparator('HelloWorld', '_')).toBe('_hello_world'); + }); + + it('should handle multiple uppercase letters', () => { + expect(toSnakeCaseWithSeparator('helloWorldTest', '_')).toBe('hello_world_test'); + }); + + it('should handle single word without uppercase', () => { + expect(toSnakeCaseWithSeparator('hello', '_')).toBe('hello'); + }); + + it('should handle custom separator', () => { + expect(toSnakeCaseWithSeparator('helloWorld', '-')).toBe('hello-world'); + }); + + it('should handle empty string', () => { + expect(toSnakeCaseWithSeparator('', '_')).toBe(''); + }); + + it('should handle consecutive uppercase letters', () => { + expect(toSnakeCaseWithSeparator('helloWORLD', '_')).toBe('hello_w_o_r_l_d'); + }); + }); + + describe('eventNameToStrMapper', () => { + it('should return the same event name string', () => { + expect(eventNameToStrMapper('PAYMENT_INITIATED')).toBe('PAYMENT_INITIATED'); + }); + + it('should handle empty string', () => { + expect(eventNameToStrMapper('')).toBe(''); + }); + + it('should handle any string input', () => { + expect(eventNameToStrMapper('SomeEventName')).toBe('SomeEventName'); + }); + }); + + describe('apiEventInitMapper', () => { + it('should map RETRIEVE_CALL to RETRIEVE_CALL_INIT', () => { + expect(apiEventInitMapper('RETRIEVE_CALL')).toBe('RETRIEVE_CALL_INIT'); + }); + + it('should map AUTHENTICATION_CALL to AUTHENTICATION_CALL_INIT', () => { + expect(apiEventInitMapper('AUTHENTICATION_CALL')).toBe('AUTHENTICATION_CALL_INIT'); + }); + + it('should map CONFIRM_CALL to CONFIRM_CALL_INIT', () => { + expect(apiEventInitMapper('CONFIRM_CALL')).toBe('CONFIRM_CALL_INIT'); + }); + + it('should map CONFIRM_PAYOUT_CALL to CONFIRM_PAYOUT_CALL_INIT', () => { + expect(apiEventInitMapper('CONFIRM_PAYOUT_CALL')).toBe('CONFIRM_PAYOUT_CALL_INIT'); + }); + + it('should map SESSIONS_CALL to SESSIONS_CALL_INIT', () => { + expect(apiEventInitMapper('SESSIONS_CALL')).toBe('SESSIONS_CALL_INIT'); + }); + + it('should map PAYMENT_METHODS_CALL to PAYMENT_METHODS_CALL_INIT', () => { + expect(apiEventInitMapper('PAYMENT_METHODS_CALL')).toBe('PAYMENT_METHODS_CALL_INIT'); + }); + + it('should map CUSTOMER_PAYMENT_METHODS_CALL to CUSTOMER_PAYMENT_METHODS_CALL_INIT', () => { + expect(apiEventInitMapper('CUSTOMER_PAYMENT_METHODS_CALL')).toBe('CUSTOMER_PAYMENT_METHODS_CALL_INIT'); + }); + + it('should map CREATE_CUSTOMER_PAYMENT_METHODS_CALL to CREATE_CUSTOMER_PAYMENT_METHODS_CALL_INIT', () => { + expect(apiEventInitMapper('CREATE_CUSTOMER_PAYMENT_METHODS_CALL')).toBe('CREATE_CUSTOMER_PAYMENT_METHODS_CALL_INIT'); + }); + + it('should map POLL_STATUS_CALL to POLL_STATUS_CALL_INIT', () => { + expect(apiEventInitMapper('POLL_STATUS_CALL')).toBe('POLL_STATUS_CALL_INIT'); + }); + + it('should map COMPLETE_AUTHORIZE_CALL to COMPLETE_AUTHORIZE_CALL_INIT', () => { + expect(apiEventInitMapper('COMPLETE_AUTHORIZE_CALL')).toBe('COMPLETE_AUTHORIZE_CALL_INIT'); + }); + + it('should map PAYMENT_METHODS_AUTH_EXCHANGE_CALL to PAYMENT_METHODS_AUTH_EXCHANGE_CALL_INIT', () => { + expect(apiEventInitMapper('PAYMENT_METHODS_AUTH_EXCHANGE_CALL')).toBe('PAYMENT_METHODS_AUTH_EXCHANGE_CALL_INIT'); + }); + + it('should map PAYMENT_METHODS_AUTH_LINK_CALL to PAYMENT_METHODS_AUTH_LINK_CALL_INIT', () => { + expect(apiEventInitMapper('PAYMENT_METHODS_AUTH_LINK_CALL')).toBe('PAYMENT_METHODS_AUTH_LINK_CALL_INIT'); + }); + + it('should map POST_SESSION_TOKENS_CALL to POST_SESSION_TOKENS_CALL_INIT', () => { + expect(apiEventInitMapper('POST_SESSION_TOKENS_CALL')).toBe('POST_SESSION_TOKENS_CALL_INIT'); + }); + + it('should return ENABLED_AUTHN_METHODS_TOKEN_CALL unchanged', () => { + expect(apiEventInitMapper('ENABLED_AUTHN_METHODS_TOKEN_CALL')).toBe('ENABLED_AUTHN_METHODS_TOKEN_CALL'); + }); + + it('should return ELIGIBILITY_CHECK_CALL unchanged', () => { + expect(apiEventInitMapper('ELIGIBILITY_CHECK_CALL')).toBe('ELIGIBILITY_CHECK_CALL'); + }); + + it('should return AUTHENTICATION_SYNC_CALL unchanged', () => { + expect(apiEventInitMapper('AUTHENTICATION_SYNC_CALL')).toBe('AUTHENTICATION_SYNC_CALL'); + }); + + it('should return undefined for unknown event names', () => { + expect(apiEventInitMapper('UNKNOWN_EVENT')).toBeUndefined(); + }); + + it('should return undefined for empty string', () => { + expect(apiEventInitMapper('')).toBeUndefined(); + }); + }); + + describe('logApi', () => { + const mockLogger = { + setLogApi: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should call logger.setLogApi for Request type', () => { + logApi( + 'TEST_EVENT', + 200, + { data: 'test' }, + 'Request', + 'https://api.example.com', + 'credit', + { result: 'success' }, + mockLogger, + 'INFO', + 'API', + false + ); + + expect(mockLogger.setLogApi).toHaveBeenCalled(); + const call = mockLogger.setLogApi.mock.calls[0]; + expect(call[1]).toBe('TEST_EVENT'); + }); + + it('should call logger.setLogApi for Response type', () => { + logApi( + 'TEST_EVENT', + 200, + { data: 'test' }, + 'Response', + 'https://api.example.com', + 'credit', + { result: 'success' }, + mockLogger, + 'INFO', + 'API', + false + ); + + expect(mockLogger.setLogApi).toHaveBeenCalled(); + }); + + it('should call logger.setLogApi for NoResponse type', () => { + logApi( + 'TEST_EVENT', + 504, + { error: 'timeout' }, + 'NoResponse', + 'https://api.example.com', + 'credit', + {}, + mockLogger, + 'ERROR', + 'API', + false + ); + + expect(mockLogger.setLogApi).toHaveBeenCalled(); + }); + + it('should call logger.setLogApi for Method type', () => { + logApi( + 'TEST_EVENT', + 0, + {}, + 'Method', + '', + 'credit', + { valid: true }, + mockLogger, + 'INFO', + 'METHOD', + false + ); + + expect(mockLogger.setLogApi).toHaveBeenCalled(); + }); + + it('should call logger.setLogApi for Err type', () => { + logApi( + 'TEST_EVENT', + 500, + { error: 'server error' }, + 'Err', + 'https://api.example.com', + 'credit', + {}, + mockLogger, + 'ERROR', + 'API', + false + ); + + expect(mockLogger.setLogApi).toHaveBeenCalled(); + }); + + it('should not call setLogApi when logger is undefined', () => { + expect(() => { + logApi('TEST_EVENT', 200, {}, 'Request', 'url', 'credit', {}, undefined); + }).not.toThrow(); + }); + + it('should use default values for optional parameters', () => { + logApi('TEST_EVENT', undefined, undefined, 'Request', undefined, undefined, undefined, mockLogger); + + expect(mockLogger.setLogApi).toHaveBeenCalled(); + }); + }); + + describe('logInputChangeInfo', () => { + const mockLogger = { + setLogInfo: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should call logger.setLogInfo with correct parameters', () => { + logInputChangeInfo('card_number', mockLogger); + + expect(mockLogger.setLogInfo).toHaveBeenCalledWith( + 'card_number', + 'INPUT_FIELD_CHANGED', + undefined, + undefined, + undefined, + undefined, + undefined + ); + }); + + it('should handle empty text', () => { + logInputChangeInfo('', mockLogger); + + expect(mockLogger.setLogInfo).toHaveBeenCalledWith( + '', + 'INPUT_FIELD_CHANGED', + undefined, + undefined, + undefined, + undefined, + undefined + ); + }); + }); + + describe('handleLogging', () => { + const mockLogger = { + setLogInfo: jest.fn(), + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should call logger.setLogInfo with correct parameters', () => { + handleLogging(mockLogger, 'test_value', 'TEST_EVENT', 'credit', 'INFO'); + + expect(mockLogger.setLogInfo).toHaveBeenCalledWith( + 'test_value', + 'TEST_EVENT', + undefined, + undefined, + 'INFO', + undefined, + 'credit' + ); + }); + + it('should use default logType when not provided', () => { + handleLogging(mockLogger, 'test_value', 'TEST_EVENT', 'credit'); + + expect(mockLogger.setLogInfo).toHaveBeenCalledWith( + 'test_value', + 'TEST_EVENT', + undefined, + undefined, + 'INFO', + undefined, + 'credit' + ); + }); + + it('should not throw when logger is undefined', () => { + expect(() => { + handleLogging(undefined, 'test_value', 'TEST_EVENT', 'credit'); + }).not.toThrow(); + }); + + it('should handle empty values', () => { + handleLogging(mockLogger, '', '', '', ''); + + expect(mockLogger.setLogInfo).toHaveBeenCalled(); + }); + }); + + describe('defaultLoggerConfig', () => { + it('should have all required methods', () => { + expect(defaultLoggerConfig).toHaveProperty('setLogInfo'); + expect(defaultLoggerConfig).toHaveProperty('setLogError'); + expect(defaultLoggerConfig).toHaveProperty('setLogApi'); + expect(defaultLoggerConfig).toHaveProperty('setLogInitiated'); + expect(defaultLoggerConfig).toHaveProperty('setConfirmPaymentValue'); + expect(defaultLoggerConfig).toHaveProperty('sendLogs'); + expect(defaultLoggerConfig).toHaveProperty('setSessionId'); + expect(defaultLoggerConfig).toHaveProperty('setClientSecret'); + expect(defaultLoggerConfig).toHaveProperty('setMerchantId'); + expect(defaultLoggerConfig).toHaveProperty('setMetadata'); + expect(defaultLoggerConfig).toHaveProperty('setSource'); + }); + + it('should have setLogInfo as a function', () => { + expect(typeof defaultLoggerConfig.setLogInfo).toBe('function'); + }); + + it('should have setLogError as a function', () => { + expect(typeof defaultLoggerConfig.setLogError).toBe('function'); + }); + + it('should have setLogApi as a function', () => { + expect(typeof defaultLoggerConfig.setLogApi).toBe('function'); + }); + + it('should have setLogInitiated as a function', () => { + expect(typeof defaultLoggerConfig.setLogInitiated).toBe('function'); + }); + + it('should have setConfirmPaymentValue as a function that returns empty object', () => { + expect(typeof defaultLoggerConfig.setConfirmPaymentValue).toBe('function'); + expect(defaultLoggerConfig.setConfirmPaymentValue({})).toEqual({}); + }); + + it('should have sendLogs as a function', () => { + expect(typeof defaultLoggerConfig.sendLogs).toBe('function'); + }); + + it('should have setSessionId as a function', () => { + expect(typeof defaultLoggerConfig.setSessionId).toBe('function'); + }); + + it('should have setClientSecret as a function', () => { + expect(typeof defaultLoggerConfig.setClientSecret).toBe('function'); + }); + + it('should have setMerchantId as a function', () => { + expect(typeof defaultLoggerConfig.setMerchantId).toBe('function'); + }); + + it('should have setMetadata as a function', () => { + expect(typeof defaultLoggerConfig.setMetadata).toBe('function'); + }); + + it('should have setSource as a function', () => { + expect(typeof defaultLoggerConfig.setSource).toBe('function'); + }); + + it('should not throw when calling setLogInfo', () => { + expect(() => defaultLoggerConfig.setLogInfo({}, '', '', '', '', '', '')).not.toThrow(); + }); + + it('should not throw when calling setLogError', () => { + expect(() => defaultLoggerConfig.setLogError({}, '', '', '', '', '', '')).not.toThrow(); + }); + + it('should not throw when calling setLogApi', () => { + expect(() => defaultLoggerConfig.setLogApi({}, '', '', '', '', '', '', '')).not.toThrow(); + }); + + it('should not throw when calling setLogInitiated', () => { + expect(() => defaultLoggerConfig.setLogInitiated()).not.toThrow(); + }); + + it('should not throw when calling sendLogs', () => { + expect(() => defaultLoggerConfig.sendLogs()).not.toThrow(); + }); + + it('should not throw when calling setSessionId', () => { + expect(() => defaultLoggerConfig.setSessionId('session123')).not.toThrow(); + }); + + it('should not throw when calling setClientSecret', () => { + expect(() => defaultLoggerConfig.setClientSecret('secret123')).not.toThrow(); + }); + + it('should not throw when calling setMerchantId', () => { + expect(() => defaultLoggerConfig.setMerchantId('merchant123')).not.toThrow(); + }); + + it('should not throw when calling setMetadata', () => { + expect(() => defaultLoggerConfig.setMetadata({})).not.toThrow(); + }); + + it('should not throw when calling setSource', () => { + expect(() => defaultLoggerConfig.setSource('web')).not.toThrow(); + }); + }); +}); diff --git a/src/__tests__/NetworkInformation.test.ts b/src/__tests__/NetworkInformation.test.ts new file mode 100644 index 000000000..28fd2d7fe --- /dev/null +++ b/src/__tests__/NetworkInformation.test.ts @@ -0,0 +1,221 @@ +import { renderHook, act } from '@testing-library/react'; +import { defaultNetworkState, getNetworkState, useNetworkInformation } from '../Hooks/NetworkInformation.bs.js'; + +declare global { + var navigator: Navigator; +} + +describe('NetworkInformation', () => { + describe('defaultNetworkState', () => { + describe('structure', () => { + it('should have isOnline property defaulting to true', () => { + expect(defaultNetworkState.isOnline).toBe(true); + }); + + it('should have empty effectiveType string', () => { + expect(defaultNetworkState.effectiveType).toBe(''); + }); + + it('should have zero downlink value', () => { + expect(defaultNetworkState.downlink).toBe(0); + }); + + it('should have zero rtt value', () => { + expect(defaultNetworkState.rtt).toBe(0); + }); + }); + + describe('type checking', () => { + it('should be an object', () => { + expect(typeof defaultNetworkState).toBe('object'); + }); + + it('should have exactly 4 properties', () => { + expect(Object.keys(defaultNetworkState)).toHaveLength(4); + }); + + it('should have isOnline as boolean', () => { + expect(typeof defaultNetworkState.isOnline).toBe('boolean'); + }); + + it('should have effectiveType as string', () => { + expect(typeof defaultNetworkState.effectiveType).toBe('string'); + }); + + it('should have downlink as number', () => { + expect(typeof defaultNetworkState.downlink).toBe('number'); + }); + + it('should have rtt as number', () => { + expect(typeof defaultNetworkState.rtt).toBe('number'); + }); + }); + }); + + describe('getNetworkState', () => { + const originalNavigator = globalThis.navigator; + + beforeEach(() => { + Object.defineProperty(globalThis, 'navigator', { + value: {} as Navigator, + writable: true, + configurable: true, + }); + }); + + afterEach(() => { + Object.defineProperty(globalThis, 'navigator', { + value: originalNavigator, + writable: true, + configurable: true, + }); + }); + + describe('when navigator.connection is not available', () => { + it('should return "NOT_AVAILABLE" when connection is null', () => { + Object.defineProperty(globalThis.navigator, 'connection', { + value: null, + configurable: true, + }); + expect(getNetworkState()).toBe('NOT_AVAILABLE'); + }); + + it('should return "NOT_AVAILABLE" when connection is undefined', () => { + Object.defineProperty(globalThis.navigator, 'connection', { + value: undefined, + configurable: true, + }); + expect(getNetworkState()).toBe('NOT_AVAILABLE'); + }); + }); + + describe('when navigator.connection is available', () => { + it('should return network state object with valid connection', () => { + Object.defineProperty(globalThis.navigator, 'connection', { + value: { + effectiveType: '4g', + downlink: 10, + rtt: 50, + }, + configurable: true, + }); + Object.defineProperty(globalThis.navigator, 'onLine', { + value: true, + configurable: true, + }); + + const result = getNetworkState() as { TAG: string; _0: { isOnline: boolean; effectiveType: string; downlink: number; rtt: number } }; + + expect(result).toHaveProperty('TAG', 'Value'); + expect(result._0).toEqual({ + isOnline: true, + effectiveType: '4g', + downlink: 10, + rtt: 50, + }); + }); + + it('should return correct values when offline', () => { + Object.defineProperty(globalThis.navigator, 'connection', { + value: { + effectiveType: '3g', + downlink: 5, + rtt: 100, + }, + configurable: true, + }); + Object.defineProperty(globalThis.navigator, 'onLine', { + value: false, + configurable: true, + }); + + const result = getNetworkState() as { TAG: string; _0: { isOnline: boolean; effectiveType: string; downlink: number; rtt: number } }; + + expect(result._0.isOnline).toBe(false); + expect(result._0.effectiveType).toBe('3g'); + }); + + it('should handle slow connection types', () => { + Object.defineProperty(globalThis.navigator, 'connection', { + value: { + effectiveType: '2g', + downlink: 0.5, + rtt: 300, + }, + configurable: true, + }); + Object.defineProperty(globalThis.navigator, 'onLine', { + value: true, + configurable: true, + }); + + const result = getNetworkState() as { TAG: string; _0: { isOnline: boolean; effectiveType: string; downlink: number; rtt: number } }; + + expect(result._0.effectiveType).toBe('2g'); + expect(result._0.downlink).toBe(0.5); + expect(result._0.rtt).toBe(300); + }); + }); + }); + + describe('useNetworkInformation', () => { + let addEventListenerSpy: jest.SpyInstance; + let removeEventListenerSpy: jest.SpyInstance; + + beforeEach(() => { + addEventListenerSpy = jest.spyOn(window, 'addEventListener'); + removeEventListenerSpy = jest.spyOn(window, 'removeEventListener'); + }); + + afterEach(() => { + addEventListenerSpy.mockRestore(); + removeEventListenerSpy.mockRestore(); + }); + + it('should return NOT_AVAILABLE when navigator.connection is null', () => { + const originalConnection = Object.getOwnPropertyDescriptor(navigator, 'connection'); + Object.defineProperty(navigator, 'connection', { + value: null, + configurable: true, + }); + + const { result } = renderHook(() => useNetworkInformation()); + expect(result.current).toBe('NOT_AVAILABLE'); + + if (originalConnection) { + Object.defineProperty(navigator, 'connection', originalConnection); + } + }); + + it('should return network state value when connection is available', () => { + const { result } = renderHook(() => useNetworkInformation()); + + const state = result.current; + expect(state).toBeDefined(); + + if (state !== 'NOT_AVAILABLE') { + const networkState = state as { TAG: string; _0: { isOnline: boolean; effectiveType: string; downlink: number; rtt: number } }; + expect(networkState).toHaveProperty('TAG', 'Value'); + expect(networkState._0).toHaveProperty('isOnline'); + expect(networkState._0).toHaveProperty('effectiveType'); + expect(networkState._0).toHaveProperty('downlink'); + expect(networkState._0).toHaveProperty('rtt'); + } + }); + + it('should register event listeners on mount', () => { + renderHook(() => useNetworkInformation()); + expect(addEventListenerSpy).toHaveBeenCalledWith('load', expect.any(Function)); + expect(addEventListenerSpy).toHaveBeenCalledWith('online', expect.any(Function)); + expect(addEventListenerSpy).toHaveBeenCalledWith('offline', expect.any(Function)); + }); + + it('should remove event listeners on unmount', () => { + const { unmount } = renderHook(() => useNetworkInformation()); + unmount(); + expect(removeEventListenerSpy).toHaveBeenCalledWith('load', expect.any(Function)); + expect(removeEventListenerSpy).toHaveBeenCalledWith('online', expect.any(Function)); + expect(removeEventListenerSpy).toHaveBeenCalledWith('offline', expect.any(Function)); + }); + }); +}); diff --git a/src/__tests__/OutsideClick.test.ts b/src/__tests__/OutsideClick.test.ts new file mode 100644 index 000000000..5e6428eeb --- /dev/null +++ b/src/__tests__/OutsideClick.test.ts @@ -0,0 +1,337 @@ +import { renderHook, act } from '@testing-library/react'; +import * as React from 'react'; +import * as OutsideClick from '../Hooks/OutsideClick.bs.js'; + +describe('useOutsideClick', () => { + let addEventListenerSpy: jest.SpyInstance; + let removeEventListenerSpy: jest.SpyInstance; + let setTimeoutSpy: jest.SpyInstance; + + beforeEach(() => { + addEventListenerSpy = jest.spyOn(window, 'addEventListener'); + removeEventListenerSpy = jest.spyOn(window, 'removeEventListener'); + setTimeoutSpy = jest.spyOn(window, 'setTimeout').mockImplementation((fn: any) => { + fn(); + return 0 as any; + }); + }); + + afterEach(() => { + addEventListenerSpy.mockRestore(); + removeEventListenerSpy.mockRestore(); + setTimeoutSpy.mockRestore(); + }); + + describe('with ArrayOfRef', () => { + it('calls callback when click is outside the ref element', () => { + const callback = jest.fn(); + const mockElement = { + contains: jest.fn().mockReturnValue(false), + }; + const ref = { current: mockElement }; + const refs = { TAG: 'ArrayOfRef', _0: [ref] }; + + renderHook(() => OutsideClick.useOutsideClick(refs, undefined, true, undefined, callback)); + + const clickHandler = addEventListenerSpy.mock.calls.find( + (call) => call[0] === 'click' + )?.[1]; + + if (clickHandler) { + act(() => { + clickHandler({ target: document.createElement('div') }); + }); + } + + expect(callback).toHaveBeenCalled(); + }); + + it('does not call callback when click is inside the ref element', () => { + const callback = jest.fn(); + const mockElement = { + contains: jest.fn().mockReturnValue(true), + }; + const ref = { current: mockElement }; + const refs = { TAG: 'ArrayOfRef', _0: [ref] }; + + renderHook(() => OutsideClick.useOutsideClick(refs, undefined, true, undefined, callback)); + + const clickHandler = addEventListenerSpy.mock.calls.find( + (call) => call[0] === 'click' + )?.[1]; + + if (clickHandler) { + act(() => { + clickHandler({ target: document.createElement('div') }); + }); + } + + expect(callback).not.toHaveBeenCalled(); + }); + + it('handles null ref element gracefully', () => { + const callback = jest.fn(); + const ref = { current: null }; + const refs = { TAG: 'ArrayOfRef', _0: [ref] }; + + renderHook(() => OutsideClick.useOutsideClick(refs, undefined, true, undefined, callback)); + + const clickHandler = addEventListenerSpy.mock.calls.find( + (call) => call[0] === 'click' + )?.[1]; + + if (clickHandler) { + act(() => { + clickHandler({ target: document.createElement('div') }); + }); + } + + expect(callback).toHaveBeenCalled(); + }); + }); + + describe('with RefArray', () => { + it('calls callback when click is outside all ref elements', () => { + const callback = jest.fn(); + const mockElement = { + contains: jest.fn().mockReturnValue(false), + }; + const containerRef = { + current: { + slice: jest.fn().mockReturnValue([mockElement]), + }, + }; + const refs = { TAG: 'RefArray', _0: containerRef }; + + renderHook(() => OutsideClick.useOutsideClick(refs, undefined, true, undefined, callback)); + + const clickHandler = addEventListenerSpy.mock.calls.find( + (call) => call[0] === 'click' + )?.[1]; + + if (clickHandler) { + act(() => { + clickHandler({ target: document.createElement('div') }); + }); + } + + expect(callback).toHaveBeenCalled(); + }); + + it('does not call callback when click is inside one of the ref elements', () => { + const callback = jest.fn(); + const mockElement = { + contains: jest.fn().mockReturnValue(true), + }; + const containerRef = { + current: { + slice: jest.fn().mockReturnValue([mockElement]), + }, + }; + const refs = { TAG: 'RefArray', _0: containerRef }; + + renderHook(() => OutsideClick.useOutsideClick(refs, undefined, true, undefined, callback)); + + const clickHandler = addEventListenerSpy.mock.calls.find( + (call) => call[0] === 'click' + )?.[1]; + + if (clickHandler) { + act(() => { + clickHandler({ target: document.createElement('div') }); + }); + } + + expect(callback).not.toHaveBeenCalled(); + }); + + it('handles null element in ref array gracefully', () => { + const callback = jest.fn(); + const containerRef = { + current: { + slice: jest.fn().mockReturnValue([null]), + }, + }; + const refs = { TAG: 'RefArray', _0: containerRef }; + + renderHook(() => OutsideClick.useOutsideClick(refs, undefined, true, undefined, callback)); + + const clickHandler = addEventListenerSpy.mock.calls.find( + (call) => call[0] === 'click' + )?.[1]; + + if (clickHandler) { + act(() => { + clickHandler({ target: document.createElement('div') }); + }); + } + + expect(callback).toHaveBeenCalled(); + }); + }); + + describe('with containerRefs', () => { + it('calls callback when click is inside container but outside refs', () => { + const callback = jest.fn(); + const mockElement = { + contains: jest.fn().mockReturnValue(false), + }; + const mockContainer = { + contains: jest.fn().mockReturnValue(true), + }; + const ref = { current: mockElement }; + const refs = { TAG: 'ArrayOfRef', _0: [ref] }; + const containerRefs = { current: mockContainer }; + + renderHook(() => OutsideClick.useOutsideClick(refs, containerRefs, true, undefined, callback)); + + const clickHandler = addEventListenerSpy.mock.calls.find( + (call) => call[0] === 'click' + )?.[1]; + + if (clickHandler) { + act(() => { + clickHandler({ target: document.createElement('div') }); + }); + } + + expect(callback).toHaveBeenCalled(); + }); + + it('does not call callback when click is outside container', () => { + const callback = jest.fn(); + const mockElement = { + contains: jest.fn().mockReturnValue(false), + }; + const mockContainer = { + contains: jest.fn().mockReturnValue(false), + }; + const ref = { current: mockElement }; + const refs = { TAG: 'ArrayOfRef', _0: [ref] }; + const containerRefs = { current: mockContainer }; + + renderHook(() => OutsideClick.useOutsideClick(refs, containerRefs, true, undefined, callback)); + + const clickHandler = addEventListenerSpy.mock.calls.find( + (call) => call[0] === 'click' + )?.[1]; + + if (clickHandler) { + act(() => { + clickHandler({ target: document.createElement('div') }); + }); + } + + expect(callback).not.toHaveBeenCalled(); + }); + + it('handles null containerRefs.current gracefully - does not call callback', () => { + const callback = jest.fn(); + const mockElement = { + contains: jest.fn().mockReturnValue(false), + }; + const ref = { current: mockElement }; + const refs = { TAG: 'ArrayOfRef', _0: [ref] }; + const containerRefs = { current: null }; + + renderHook(() => OutsideClick.useOutsideClick(refs, containerRefs, true, undefined, callback)); + + const clickHandler = addEventListenerSpy.mock.calls.find( + (call) => call[0] === 'click' + )?.[1]; + + if (clickHandler) { + act(() => { + clickHandler({ target: document.createElement('div') }); + }); + } + + expect(callback).not.toHaveBeenCalled(); + }); + + it('calls callback when containerRefs is undefined (no container restriction)', () => { + const callback = jest.fn(); + const mockElement = { + contains: jest.fn().mockReturnValue(false), + }; + const ref = { current: mockElement }; + const refs = { TAG: 'ArrayOfRef', _0: [ref] }; + + renderHook(() => OutsideClick.useOutsideClick(refs, undefined, true, undefined, callback)); + + const clickHandler = addEventListenerSpy.mock.calls.find( + (call) => call[0] === 'click' + )?.[1]; + + if (clickHandler) { + act(() => { + clickHandler({ target: document.createElement('div') }); + }); + } + + expect(callback).toHaveBeenCalled(); + }); + }); + + describe('isActive parameter', () => { + it('does not add event listener when isActive is false', () => { + const callback = jest.fn(); + const ref = { current: null }; + const refs = { TAG: 'ArrayOfRef', _0: [ref] }; + + renderHook(() => OutsideClick.useOutsideClick(refs, undefined, false, undefined, callback)); + + expect(addEventListenerSpy).not.toHaveBeenCalled(); + }); + + it('adds event listener when isActive is true', () => { + const callback = jest.fn(); + const ref = { current: null }; + const refs = { TAG: 'ArrayOfRef', _0: [ref] }; + + renderHook(() => OutsideClick.useOutsideClick(refs, undefined, true, undefined, callback)); + + expect(addEventListenerSpy).toHaveBeenCalledWith('click', expect.any(Function)); + }); + }); + + describe('custom events', () => { + it('uses custom events when provided', () => { + const callback = jest.fn(); + const ref = { current: null }; + const refs = { TAG: 'ArrayOfRef', _0: [ref] }; + const customEvents = ['mousedown', 'touchstart']; + + renderHook(() => OutsideClick.useOutsideClick(refs, undefined, true, customEvents, callback)); + + expect(addEventListenerSpy).toHaveBeenCalledWith('mousedown', expect.any(Function)); + expect(addEventListenerSpy).toHaveBeenCalledWith('touchstart', expect.any(Function)); + }); + + it('defaults to click event when no events provided', () => { + const callback = jest.fn(); + const ref = { current: null }; + const refs = { TAG: 'ArrayOfRef', _0: [ref] }; + + renderHook(() => OutsideClick.useOutsideClick(refs, undefined, true, undefined, callback)); + + expect(addEventListenerSpy).toHaveBeenCalledWith('click', expect.any(Function)); + }); + }); + + describe('cleanup', () => { + it('removes event listeners on unmount', () => { + const callback = jest.fn(); + const ref = { current: null }; + const refs = { TAG: 'ArrayOfRef', _0: [ref] }; + + const { unmount } = renderHook(() => + OutsideClick.useOutsideClick(refs, undefined, true, undefined, callback) + ); + + unmount(); + + expect(removeEventListenerSpy).toHaveBeenCalledWith('click', expect.any(Function)); + }); + }); +}); diff --git a/src/__tests__/PaymentBody.test.ts b/src/__tests__/PaymentBody.test.ts new file mode 100644 index 000000000..d123a3b15 --- /dev/null +++ b/src/__tests__/PaymentBody.test.ts @@ -0,0 +1,891 @@ +import { + cardPaymentBody, + savedCardBody, + gpayBody, + applePayBody, + cryptoBody, + achBankDebitBody, + sofortBody, + iDealBody, + blikBody, + getPaymentMethodType, + billingDetailsTuple, + mandateBody, + bankDebitsCommonBody, + bacsBankDebitBody, + becsBankDebitBody, + installmentBody, + bancontactBody, + boletoBody, + savedPaymentMethodBody, + paymentTypeBody, + confirmPayloadForSDKButton, + klarnaSDKbody, + klarnaCheckoutBody, + paypalSdkBody, + samsungPayBody, + gpayRedirectBody, + gPayThirdPartySdkBody, + applePayRedirectBody, + applePayThirdPartySdkBody, + afterpayRedirectionBody, + giroPayBody, + trustlyBody, + polandOB, + czechOB, + slovakiaOB, + mbWayBody, + rewardBody, + fpxOBBody, + thailandOBBody, + pazeBody, + revolutPayBody, + eftBody, + getPaymentMethodSuffix, + appendPaymentMethodExperience, + dynamicPaymentBody, + getPaymentBody, + appendRedirectPaymentMethods, + appendBankeDebitMethods, + appendBankTransferMethods, + paymentExperiencePaymentMethods, + appendPaymentExperience, +} from '../Utilities/PaymentBody.bs.js'; + +describe('PaymentBody', () => { + describe('cardPaymentBody', () => { + it('should create card payment body with valid card data', () => { + const result = cardPaymentBody('4111111111111111', '12', '2025', 'John Doe', '123', [], undefined); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + + const paymentMethodEntry = result.find((entry: any) => entry[0] === 'payment_method'); + expect(paymentMethodEntry).toBeDefined(); + expect(paymentMethodEntry[1]).toBe('card'); + }); + + it('should include card details in payment_method_data', () => { + const result = cardPaymentBody('4111111111111111', '12', '2025', 'John Doe', '123', [], undefined); + + const pmdEntry = result.find((entry: any) => entry[0] === 'payment_method_data'); + expect(pmdEntry).toBeDefined(); + }); + + it('should handle missing optional parameters', () => { + const result = cardPaymentBody('4111111111111111', '12', '2025', undefined, '123', [], undefined); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + }); + + // Edge case: empty string card number + it('should handle empty string card number', () => { + const result = cardPaymentBody('', '12', '2025', undefined, '123', [], undefined); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + const pmdEntry = result.find((entry: any) => entry[0] === 'payment_method_data'); + expect(pmdEntry).toBeDefined(); + }); + + // Edge case: empty month and year + it('should handle empty month and year strings', () => { + const result = cardPaymentBody('4111111111111111', '', '', undefined, '123', [], undefined); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + }); + }); + + describe('savedCardBody', () => { + it('should create saved card body with token and customer ID', () => { + const result = savedCardBody('token123', 'customer456', '123', true, false); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + + const paymentMethodEntry = result.find((entry: any) => entry[0] === 'payment_method'); + expect(paymentMethodEntry[1]).toBe('card'); + + const tokenEntry = result.find((entry: any) => entry[0] === 'payment_token'); + expect(tokenEntry[1]).toBe('token123'); + }); + + it('should include CVC when requiresCvv is true', () => { + const result = savedCardBody('token123', 'customer456', '123', true, false); + const cvcEntry = result.find((entry: any) => entry[0] === 'card_cvc'); + expect(cvcEntry).toBeDefined(); + expect(cvcEntry[1]).toBe('123'); + }); + + it('should not include CVC when requiresCvv is false', () => { + const result = savedCardBody('token123', 'customer456', '123', false, false); + const cvcEntry = result.find((entry: any) => entry[0] === 'card_cvc'); + expect(cvcEntry).toBeUndefined(); + }); + }); + + describe('gpayBody', () => { + it('should create Google Pay body with payment data', () => { + const payObj = { + paymentMethodData: { + type: 'CARD', + description: 'Visa 1234', + info: { cardNetwork: 'VISA', cardDetails: '1234' }, + tokenizationData: { type: 'PAYMENT_GATEWAY', token: 'test-token' }, + }, + }; + const result = gpayBody(payObj, []); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + + const paymentMethodEntry = result.find((entry: any) => entry[0] === 'payment_method'); + expect(paymentMethodEntry[1]).toBe('wallet'); + + const pmtEntry = result.find((entry: any) => entry[0] === 'payment_method_type'); + expect(pmtEntry[1]).toBe('google_pay'); + }); + + it('should include connectors when provided', () => { + const payObj = { + paymentMethodData: { + type: 'CARD', + description: 'Visa 1234', + info: { cardNetwork: 'VISA' }, + tokenizationData: { type: 'PAYMENT_GATEWAY', token: 'test' }, + }, + }; + const result = gpayBody(payObj, ['stripe', 'adyen']); + const connectorEntry = result.find((entry: any) => entry[0] === 'connector'); + expect(connectorEntry).toBeDefined(); + }); + }); + + describe('applePayBody', () => { + it('should create Apple Pay body with token', () => { + const token = { paymentData: { data: 'test' }, transactionIdentifier: 'abc123' }; + const result = applePayBody(token, []); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + + const paymentMethodEntry = result.find((entry: any) => entry[0] === 'payment_method'); + expect(paymentMethodEntry[1]).toBe('wallet'); + + const pmtEntry = result.find((entry: any) => entry[0] === 'payment_method_type'); + expect(pmtEntry[1]).toBe('apple_pay'); + }); + }); + + describe('cryptoBody', () => { + it('should create crypto payment body', () => { + const result = cryptoBody(); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + + const paymentMethodEntry = result.find((entry: any) => entry[0] === 'payment_method'); + expect(paymentMethodEntry[1]).toBe('crypto'); + + const pmtEntry = result.find((entry: any) => entry[0] === 'payment_method_type'); + expect(pmtEntry[1]).toBe('crypto_currency'); + }); + }); + + describe('achBankDebitBody', () => { + it('should create ACH bank debit body with bank details', () => { + const bank = { + accountNumber: '123456789', + accountHolderName: 'John Doe', + routingNumber: '021000021', + accountType: 'checking', + }; + const result = achBankDebitBody( + 'test@email.com', + bank, + 'John Doe', + '123 Main St', + 'Apt 4', + 'US', + 'New York', + '10001', + 'NY' + ); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + + const paymentMethodEntry = result.find((entry: any) => entry[0] === 'payment_method'); + expect(paymentMethodEntry[1]).toBe('bank_debit'); + + const pmtEntry = result.find((entry: any) => entry[0] === 'payment_method_type'); + expect(pmtEntry[1]).toBe('ach'); + }); + }); + + describe('sofortBody', () => { + it('should create Sofort payment body', () => { + const result = sofortBody('DE', 'John Doe', 'test@email.com'); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + + const paymentMethodEntry = result.find((entry: any) => entry[0] === 'payment_method'); + expect(paymentMethodEntry[1]).toBe('bank_redirect'); + + const pmtEntry = result.find((entry: any) => entry[0] === 'payment_method_type'); + expect(pmtEntry[1]).toBe('sofort'); + }); + + it('should use default country when empty', () => { + const result = sofortBody('', 'John Doe', 'test@email.com'); + expect(result).toBeDefined(); + }); + }); + + describe('iDealBody', () => { + it('should create iDeal payment body', () => { + const result = iDealBody('John Doe', 'INGBNL2A'); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + + const paymentMethodEntry = result.find((entry: any) => entry[0] === 'payment_method'); + expect(paymentMethodEntry[1]).toBe('bank_redirect'); + + const pmtEntry = result.find((entry: any) => entry[0] === 'payment_method_type'); + expect(pmtEntry[1]).toBe('ideal'); + }); + }); + + describe('blikBody', () => { + it('should create BLIK payment body with code', () => { + const result = blikBody('123456'); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + + const paymentMethodEntry = result.find((entry: any) => entry[0] === 'payment_method'); + expect(paymentMethodEntry[1]).toBe('bank_redirect'); + + const pmtEntry = result.find((entry: any) => entry[0] === 'payment_method_type'); + expect(pmtEntry[1]).toBe('blik'); + }); + }); + + describe('getPaymentMethodType', () => { + it('should return payment method type unchanged for most methods', () => { + expect(getPaymentMethodType('card', 'credit')).toBe('credit'); + expect(getPaymentMethodType('wallet', 'google_pay')).toBe('google_pay'); + }); + + it('should remove _debit suffix for bank_debit', () => { + expect(getPaymentMethodType('bank_debit', 'ach_debit')).toBe('ach'); + expect(getPaymentMethodType('bank_debit', 'sepa_debit')).toBe('sepa'); + }); + + it('should handle bank_transfer correctly', () => { + expect(getPaymentMethodType('bank_transfer', 'ach')).toBe('ach'); + }); + }); + + describe('billingDetailsTuple', () => { + it('should create billing details tuple', () => { + const result = billingDetailsTuple( + 'John Doe', + 'test@email.com', + '123 Main St', + 'Apt 4', + 'New York', + 'NY', + '10001', + 'US' + ); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + expect(result[0]).toBe('billing'); + }); + }); + + describe('mandateBody', () => { + it('should create mandate body with payment type', () => { + const result = mandateBody('recurring'); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + + const setupEntry = result.find((entry: any) => entry[0] === 'setup_future_usage'); + expect(setupEntry).toBeDefined(); + expect(setupEntry[1]).toBe('off_session'); + }); + + it('should handle empty payment type', () => { + const result = mandateBody(''); + expect(result).toBeDefined(); + }); + }); + + describe('bankDebitsCommonBody', () => { + it('should create common bank debits body', () => { + const result = bankDebitsCommonBody('ach'); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + + const paymentMethodEntry = result.find((entry: any) => entry[0] === 'payment_method'); + expect(paymentMethodEntry[1]).toBe('bank_debit'); + }); + }); + + describe('bacsBankDebitBody', () => { + it('should create BACS bank debit body', () => { + const result = bacsBankDebitBody( + 'test@email.com', + '12345678', + '123456', + '123 Main St', + 'Apt 4', + 'London', + 'SW1A 1AA', + '', + 'GB', + 'John Doe' + ); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + + const pmtEntry = result.find((entry: any) => entry[0] === 'payment_method_type'); + expect(pmtEntry[1]).toBe('bacs'); + }); + }); + + describe('becsBankDebitBody', () => { + it('should create BECS bank debit body', () => { + const data = { + sortCode: '123456', + accountNumber: '12345678', + accountHolderName: 'John Doe', + }; + const result = becsBankDebitBody( + 'John Doe', + 'test@email.com', + data, + '123 Main St', + 'Apt 4', + 'AU', + 'Sydney', + '2000', + 'NSW' + ); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + + const pmtEntry = result.find((entry: any) => entry[0] === 'payment_method_type'); + expect(pmtEntry[1]).toBe('becs'); + }); + }); + + describe('installmentBody', () => { + it('should create installment body with plan', () => { + const plan = { number_of_installments: 3, billing_frequency: 'monthly' }; + const result = installmentBody(plan); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBe(1); + expect(result[0][0]).toBe('installment_data'); + }); + + it('should return empty array when no plan', () => { + expect(installmentBody(undefined)).toEqual([]); + }); + }); + + describe('bancontactBody', () => { + it('should create bancontact payment body', () => { + const result = bancontactBody(); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + + const pmEntry = result.find((entry: any) => entry[0] === 'payment_method'); + expect(pmEntry[1]).toBe('bank_redirect'); + + const pmtEntry = result.find((entry: any) => entry[0] === 'payment_method_type'); + expect(pmtEntry[1]).toBe('bancontact_card'); + }); + }); + + describe('boletoBody', () => { + it('should create boleto payment body', () => { + const result = boletoBody('123.456.789-00'); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + + const pmEntry = result.find((entry: any) => entry[0] === 'payment_method'); + expect(pmEntry[1]).toBe('voucher'); + + const pmtEntry = result.find((entry: any) => entry[0] === 'payment_method_type'); + expect(pmtEntry[1]).toBe('boleto'); + }); + }); + + describe('savedPaymentMethodBody', () => { + it('should create saved payment method body', () => { + const result = savedPaymentMethodBody('token123', 'cust456', 'card', 'credit', false); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + + const tokenEntry = result.find((entry: any) => entry[0] === 'payment_token'); + expect(tokenEntry[1]).toBe('token123'); + }); + + it('should include customer acceptance when required', () => { + const result = savedPaymentMethodBody('token123', 'cust456', 'card', 'credit', true); + const caEntry = result.find((entry: any) => entry[0] === 'customer_acceptance'); + expect(caEntry).toBeDefined(); + }); + }); + + describe('paymentTypeBody', () => { + it('should return payment type when not empty', () => { + const result = paymentTypeBody('recurring'); + expect(result).toEqual([['payment_type', 'recurring']]); + }); + + it('should return empty array for empty string', () => { + expect(paymentTypeBody('')).toEqual([]); + }); + }); + + describe('confirmPayloadForSDKButton', () => { + it('should create confirm payload for SDK button', () => { + const sdkHandle = { + confirmParams: { return_url: 'https://example.com/return' }, + }; + const result = confirmPayloadForSDKButton(sdkHandle); + expect(result).toBeDefined(); + expect(result.confirmParams).toBeDefined(); + expect(result.confirmParams.return_url).toBe('https://example.com/return'); + expect(result.confirmParams.redirect).toBe('always'); + }); + }); + + describe('klarnaSDKbody', () => { + it('should create Klarna SDK body', () => { + const result = klarnaSDKbody('klarna_token_123', ['connector1', 'connector2']); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + + const pmEntry = result.find((entry: any) => entry[0] === 'payment_method'); + expect(pmEntry[1]).toBe('pay_later'); + + const pmtEntry = result.find((entry: any) => entry[0] === 'payment_method_type'); + expect(pmtEntry[1]).toBe('klarna'); + }); + }); + + describe('klarnaCheckoutBody', () => { + it('should create Klarna checkout body', () => { + const result = klarnaCheckoutBody(['connector1']); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + + const pmEntry = result.find((entry: any) => entry[0] === 'payment_method'); + expect(pmEntry[1]).toBe('pay_later'); + + const peEntry = result.find((entry: any) => entry[0] === 'payment_experience'); + expect(peEntry[1]).toBe('redirect_to_url'); + }); + }); + + describe('paypalSdkBody', () => { + it('should create PayPal SDK body', () => { + const result = paypalSdkBody('paypal_token_123', ['connector1']); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + + const pmEntry = result.find((entry: any) => entry[0] === 'payment_method'); + expect(pmEntry[1]).toBe('wallet'); + + const pmtEntry = result.find((entry: any) => entry[0] === 'payment_method_type'); + expect(pmtEntry[1]).toBe('paypal'); + }); + }); + + describe('samsungPayBody', () => { + it('should create Samsung Pay body', () => { + const result = samsungPayBody({ token: 'samsung_token' }); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + + const pmEntry = result.find((entry: any) => entry[0] === 'payment_method'); + expect(pmEntry[1]).toBe('wallet'); + + const pmtEntry = result.find((entry: any) => entry[0] === 'payment_method_type'); + expect(pmtEntry[1]).toBe('samsung_pay'); + }); + }); + + describe('gpayRedirectBody', () => { + it('should create Google Pay redirect body', () => { + const result = gpayRedirectBody(['connector1']); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + + const pmEntry = result.find((entry: any) => entry[0] === 'payment_method'); + expect(pmEntry[1]).toBe('wallet'); + + const pmtEntry = result.find((entry: any) => entry[0] === 'payment_method_type'); + expect(pmtEntry[1]).toBe('google_pay'); + }); + }); + + describe('gPayThirdPartySdkBody', () => { + it('should create Google Pay third party SDK body', () => { + const result = gPayThirdPartySdkBody(['connector1']); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + + const pmEntry = result.find((entry: any) => entry[0] === 'payment_method'); + expect(pmEntry[1]).toBe('wallet'); + + const pmtEntry = result.find((entry: any) => entry[0] === 'payment_method_type'); + expect(pmtEntry[1]).toBe('google_pay'); + }); + }); + + describe('applePayRedirectBody', () => { + it('should create Apple Pay redirect body', () => { + const result = applePayRedirectBody(['connector1']); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + + const pmEntry = result.find((entry: any) => entry[0] === 'payment_method'); + expect(pmEntry[1]).toBe('wallet'); + + const pmtEntry = result.find((entry: any) => entry[0] === 'payment_method_type'); + expect(pmtEntry[1]).toBe('apple_pay'); + }); + }); + + describe('applePayThirdPartySdkBody', () => { + it('should create Apple Pay third party SDK body with token', () => { + const result = applePayThirdPartySdkBody(['connector1'], 'apple_token'); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + + const pmEntry = result.find((entry: any) => entry[0] === 'payment_method'); + expect(pmEntry[1]).toBe('wallet'); + }); + + it('should create Apple Pay third party SDK body without token', () => { + const result = applePayThirdPartySdkBody(['connector1'], undefined); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + }); + }); + + describe('afterpayRedirectionBody', () => { + it('should create Afterpay redirect body', () => { + const result = afterpayRedirectionBody(); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + + const pmEntry = result.find((entry: any) => entry[0] === 'payment_method'); + expect(pmEntry[1]).toBe('pay_later'); + + const pmtEntry = result.find((entry: any) => entry[0] === 'payment_method_type'); + expect(pmtEntry[1]).toBe('afterpay_clearpay'); + }); + }); + + describe('giroPayBody', () => { + it('should create Giropay body with IBAN', () => { + const result = giroPayBody('John Doe', 'DE89370400440532013000'); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + + const pmEntry = result.find((entry: any) => entry[0] === 'payment_method'); + expect(pmEntry[1]).toBe('bank_redirect'); + + const pmtEntry = result.find((entry: any) => entry[0] === 'payment_method_type'); + expect(pmtEntry[1]).toBe('giropay'); + }); + + it('should create Giropay body without IBAN', () => { + const result = giroPayBody('John Doe', undefined); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + }); + }); + + describe('trustlyBody', () => { + it('should create Trustly body', () => { + const result = trustlyBody('US'); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + + const pmEntry = result.find((entry: any) => entry[0] === 'payment_method'); + expect(pmEntry[1]).toBe('bank_redirect'); + + const pmtEntry = result.find((entry: any) => entry[0] === 'payment_method_type'); + expect(pmtEntry[1]).toBe('trustly'); + }); + }); + + describe('polandOB', () => { + it('should create Poland online banking body', () => { + const result = polandOB('bank_xyz'); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + + const pmtEntry = result.find((entry: any) => entry[0] === 'payment_method_type'); + expect(pmtEntry[1]).toBe('online_banking_poland'); + }); + }); + + describe('czechOB', () => { + it('should create Czech online banking body', () => { + const result = czechOB('bank_abc'); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + + const pmtEntry = result.find((entry: any) => entry[0] === 'payment_method_type'); + expect(pmtEntry[1]).toBe('online_banking_czech_republic'); + }); + }); + + describe('slovakiaOB', () => { + it('should create Slovakia online banking body', () => { + const result = slovakiaOB('bank_def'); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + + const pmtEntry = result.find((entry: any) => entry[0] === 'payment_method_type'); + expect(pmtEntry[1]).toBe('online_banking_slovakia'); + }); + }); + + describe('mbWayBody', () => { + it('should create MB Way body', () => { + const result = mbWayBody('+351912345678'); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + + const pmEntry = result.find((entry: any) => entry[0] === 'payment_method'); + expect(pmEntry[1]).toBe('wallet'); + + const pmtEntry = result.find((entry: any) => entry[0] === 'payment_method_type'); + expect(pmtEntry[1]).toBe('mb_way'); + }); + }); + + describe('rewardBody', () => { + it('should create reward payment body', () => { + const result = rewardBody('classic'); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + + const pmEntry = result.find((entry: any) => entry[0] === 'payment_method'); + expect(pmEntry[1]).toBe('reward'); + }); + }); + + describe('fpxOBBody', () => { + it('should create FPX online banking body', () => { + const result = fpxOBBody('maybank'); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + + const pmtEntry = result.find((entry: any) => entry[0] === 'payment_method_type'); + expect(pmtEntry[1]).toBe('online_banking_fpx'); + }); + }); + + describe('thailandOBBody', () => { + it('should create Thailand online banking body', () => { + const result = thailandOBBody('kbank'); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + + const pmtEntry = result.find((entry: any) => entry[0] === 'payment_method_type'); + expect(pmtEntry[1]).toBe('online_banking_thailand'); + }); + }); + + describe('pazeBody', () => { + it('should create Paze wallet body', () => { + const result = pazeBody({ token: 'paze_token' }); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + + const pmEntry = result.find((entry: any) => entry[0] === 'payment_method'); + expect(pmEntry[1]).toBe('wallet'); + + const pmtEntry = result.find((entry: any) => entry[0] === 'payment_method_type'); + expect(pmtEntry[1]).toBe('paze'); + }); + }); + + describe('revolutPayBody', () => { + it('should create Revolut Pay body', () => { + const result = revolutPayBody(); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + + const pmEntry = result.find((entry: any) => entry[0] === 'payment_method'); + expect(pmEntry[1]).toBe('wallet'); + + const pmtEntry = result.find((entry: any) => entry[0] === 'payment_method_type'); + expect(pmtEntry[1]).toBe('revolut_pay'); + }); + }); + + describe('eftBody', () => { + it('should create EFT body', () => { + const result = eftBody(); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + + const pmEntry = result.find((entry: any) => entry[0] === 'payment_method'); + expect(pmEntry[1]).toBe('bank_redirect'); + + const pmtEntry = result.find((entry: any) => entry[0] === 'payment_method_type'); + expect(pmtEntry[1]).toBe('eft'); + }); + }); + + describe('getPaymentMethodSuffix', () => { + it('should return "qr" for QR payment method', () => { + expect(getPaymentMethodSuffix('any_pm', 'card', true)).toBe('qr'); + }); + + it('should return "redirect" for redirect payment methods', () => { + expect(getPaymentMethodSuffix('paypal', 'wallet', false)).toBe('redirect'); + }); + + it('should return "bank_debit" for bank debit methods', () => { + expect(getPaymentMethodSuffix('sepa', 'bank_debit', false)).toBe('bank_debit'); + }); + + it('should return "bank_transfer" for bank transfer methods', () => { + expect(getPaymentMethodSuffix('ach', 'bank_transfer', false)).toBe('bank_transfer'); + }); + + it('should return undefined for other payment methods', () => { + expect(getPaymentMethodSuffix('credit', 'card', false)).toBeUndefined(); + }); + }); + + describe('appendPaymentMethodExperience', () => { + it('should append suffix for redirect methods', () => { + expect(appendPaymentMethodExperience('wallet', 'paypal', false)).toBe('paypal_redirect'); + }); + + it('should append suffix for QR methods', () => { + expect(appendPaymentMethodExperience('wallet', 'any_qr', true)).toBe('any_qr_qr'); + }); + + it('should return original when no suffix', () => { + expect(appendPaymentMethodExperience('card', 'credit', false)).toBe('credit'); + }); + }); + + describe('dynamicPaymentBody', () => { + it('should create dynamic payment body', () => { + const result = dynamicPaymentBody('card', 'credit', false); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + + const pmEntry = result.find((entry: any) => entry[0] === 'payment_method'); + expect(pmEntry[1]).toBe('card'); + }); + + it('should handle QR payment method', () => { + const result = dynamicPaymentBody('wallet', 'any_pm', true); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + }); + }); + + describe('getPaymentBody', () => { + it('should return crypto body for crypto_currency', () => { + const result = getPaymentBody('crypto', 'crypto_currency', '', '', '', '', '', undefined, ''); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + + const pmtEntry = result.find((entry: any) => entry[0] === 'payment_method_type'); + expect(pmtEntry[1]).toBe('crypto_currency'); + }); + + it('should return blik body for blik', () => { + const result = getPaymentBody('bank_redirect', 'blik', '', '', '', '', '123456', undefined, ''); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + }); + + it('should return revolut_pay body', () => { + const result = getPaymentBody('wallet', 'revolut_pay', '', '', '', '', '', undefined, ''); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + }); + + it('should return eft body', () => { + const result = getPaymentBody('bank_redirect', 'eft', '', '', '', '', '', undefined, ''); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + }); + + it('should return afterpay body', () => { + const result = getPaymentBody('pay_later', 'afterpay_clearplay', '', '', '', '', '', undefined, ''); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + }); + + it('should return reward body for classic', () => { + const result = getPaymentBody('reward', 'classic', '', '', '', '', '', undefined, ''); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + }); + + it('should return dynamic body for unknown payment method', () => { + const result = getPaymentBody('unknown', 'unknown_type', '', '', '', '', '', undefined, ''); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + }); + }); + + describe('appendRedirectPaymentMethods', () => { + it('should contain common redirect methods', () => { + expect(appendRedirectPaymentMethods).toContain('paypal'); + expect(appendRedirectPaymentMethods).toContain('klarna'); + expect(appendRedirectPaymentMethods).toContain('affirm'); + }); + }); + + describe('appendBankeDebitMethods', () => { + it('should contain bank debit methods', () => { + expect(appendBankeDebitMethods).toContain('sepa'); + }); + }); + + describe('appendBankTransferMethods', () => { + it('should contain bank transfer methods', () => { + expect(appendBankTransferMethods).toContain('ach'); + expect(appendBankTransferMethods).toContain('bacs'); + expect(appendBankTransferMethods).toContain('multibanco'); + }); + }); + + describe('paymentExperiencePaymentMethods', () => { + it('should contain payment experience methods', () => { + expect(paymentExperiencePaymentMethods).toContain('paypal'); + expect(paymentExperiencePaymentMethods).toContain('klarna'); + expect(paymentExperiencePaymentMethods).toContain('affirm'); + }); + }); + + describe('appendPaymentExperience', () => { + it('should append payment experience for paypal', () => { + const body = [['payment_method', 'wallet']]; + const result = appendPaymentExperience(body, 'paypal'); + const peEntry = result.find((entry: any) => entry[0] === 'payment_experience'); + expect(peEntry).toBeDefined(); + expect(peEntry[1]).toBe('redirect_to_url'); + }); + + it('should not append for non-payment-experience methods', () => { + const body = [['payment_method', 'card']]; + const result = appendPaymentExperience(body, 'credit'); + const peEntry = result.find((entry: any) => entry[0] === 'payment_experience'); + expect(peEntry).toBeUndefined(); + }); + }); +}); diff --git a/src/__tests__/PaymentConfirmTypes.test.ts b/src/__tests__/PaymentConfirmTypes.test.ts new file mode 100644 index 000000000..6e386260a --- /dev/null +++ b/src/__tests__/PaymentConfirmTypes.test.ts @@ -0,0 +1,259 @@ +import { + defaultACHCreditTransfer, + defaultBacsBankInstruction, + defaultNextAction, + defaultIntent, + getAchCreditTransfer, + getBacsBankInstructions, + getBankTransferDetails, + getVoucherDetails, + getNextAction, + itemToObjMapper, +} from '../Types/PaymentConfirmTypes.bs.js'; + +describe('PaymentConfirmTypes', () => { + describe('defaultACHCreditTransfer', () => { + it('should have empty default values', () => { + expect(defaultACHCreditTransfer.account_number).toBe(''); + expect(defaultACHCreditTransfer.bank_name).toBe(''); + expect(defaultACHCreditTransfer.routing_number).toBe(''); + expect(defaultACHCreditTransfer.swift_code).toBe(''); + }); + }); + + describe('defaultBacsBankInstruction', () => { + it('should have empty default values', () => { + expect(defaultBacsBankInstruction.sort_code).toBe(''); + expect(defaultBacsBankInstruction.account_number).toBe(''); + expect(defaultBacsBankInstruction.account_holder_name).toBe(''); + }); + }); + + describe('defaultNextAction', () => { + it('should have empty default values', () => { + expect(defaultNextAction.redirectToUrl).toBe(''); + expect(defaultNextAction.popupUrl).toBe(''); + expect(defaultNextAction.redirectResponseUrl).toBe(''); + expect(defaultNextAction.type_).toBe(''); + }); + }); + + describe('defaultIntent', () => { + it('should have empty default values', () => { + expect(defaultIntent.status).toBe(''); + expect(defaultIntent.paymentId).toBe(''); + expect(defaultIntent.clientSecret).toBe(''); + expect(defaultIntent.error_message).toBe(''); + expect(defaultIntent.payment_method_type).toBe(''); + expect(defaultIntent.manualRetryAllowed).toBe(false); + expect(defaultIntent.connectorTransactionId).toBe(''); + }); + }); + + describe('getAchCreditTransfer', () => { + it('should extract ACH credit transfer details from dict', () => { + const dict = { + ach_credit_transfer: { + account_number: '123456789', + bank_name: 'Test Bank', + routing_number: '987654321', + swift_code: 'TESTUS33', + }, + }; + const result = getAchCreditTransfer(dict, 'ach_credit_transfer'); + expect(result.account_number).toBe('123456789'); + expect(result.bank_name).toBe('Test Bank'); + expect(result.routing_number).toBe('987654321'); + expect(result.swift_code).toBe('TESTUS33'); + }); + + it('should return default values when key not found', () => { + const dict = {}; + const result = getAchCreditTransfer(dict, 'ach_credit_transfer'); + expect(result).toEqual(defaultACHCreditTransfer); + }); + + it('should return default values when value is null', () => { + const dict = { ach_credit_transfer: null }; + const result = getAchCreditTransfer(dict, 'ach_credit_transfer'); + expect(result).toEqual(defaultACHCreditTransfer); + }); + }); + + describe('getBacsBankInstructions', () => { + it('should extract BACS bank instructions from dict', () => { + const dict = { + bacs_bank_instructions: { + sort_code: '123456', + account_number: '98765432', + account_holder_name: 'John Doe', + }, + }; + const result = getBacsBankInstructions(dict, 'bacs_bank_instructions'); + expect(result.sort_code).toBe('123456'); + expect(result.account_number).toBe('98765432'); + expect(result.account_holder_name).toBe('John Doe'); + }); + + it('should return default values when key not found', () => { + const dict = {}; + const result = getBacsBankInstructions(dict, 'bacs_bank_instructions'); + expect(result).toEqual(defaultBacsBankInstruction); + }); + + it('should handle partial data', () => { + const dict = { + bacs_bank_instructions: { + sort_code: '123456', + }, + }; + const result = getBacsBankInstructions(dict, 'bacs_bank_instructions'); + expect(result.sort_code).toBe('123456'); + expect(result.account_number).toBe(''); + expect(result.account_holder_name).toBe(''); + }); + }); + + describe('getBankTransferDetails', () => { + it('should extract bank transfer details from dict', () => { + const dict = { + bank_transfer_details: { + ach_credit_transfer: { + account_number: '123456789', + bank_name: 'Test Bank', + routing_number: '987654321', + swift_code: 'TESTUS33', + }, + }, + }; + const result = getBankTransferDetails(dict, 'bank_transfer_details'); + expect(result).toBeDefined(); + expect(result?.ach_credit_transfer.account_number).toBe('123456789'); + }); + + it('should return undefined when key not found', () => { + const dict = {}; + const result = getBankTransferDetails(dict, 'bank_transfer_details'); + expect(result).toBeUndefined(); + }); + + it('should return undefined when value is null', () => { + const dict = { bank_transfer_details: null }; + const result = getBankTransferDetails(dict, 'bank_transfer_details'); + expect(result).toBeUndefined(); + }); + }); + + describe('getVoucherDetails', () => { + it('should extract voucher details from json', () => { + const json = { + download_url: 'https://example.com/voucher.pdf', + reference: 'REF123456', + }; + const result = getVoucherDetails(json); + expect(result.download_url).toBe('https://example.com/voucher.pdf'); + expect(result.reference).toBe('REF123456'); + }); + + it('should return empty strings for missing fields', () => { + const json = {}; + const result = getVoucherDetails(json); + expect(result.download_url).toBe(''); + expect(result.reference).toBe(''); + }); + + it('should handle partial data', () => { + const json = { + download_url: 'https://example.com/voucher.pdf', + }; + const result = getVoucherDetails(json); + expect(result.download_url).toBe('https://example.com/voucher.pdf'); + expect(result.reference).toBe(''); + }); + }); + + describe('getNextAction', () => { + it('should extract next action from dict', () => { + const dict = { + next_action: { + redirect_to_url: 'https://example.com/redirect', + popup_url: 'https://example.com/popup', + redirect_response_url: 'https://example.com/response', + type: 'redirect', + }, + }; + const result = getNextAction(dict, 'next_action'); + expect(result.redirectToUrl).toBe('https://example.com/redirect'); + expect(result.popupUrl).toBe('https://example.com/popup'); + expect(result.redirectResponseUrl).toBe('https://example.com/response'); + expect(result.type_).toBe('redirect'); + }); + + it('should return default values when key not found', () => { + const dict = {}; + const result = getNextAction(dict, 'next_action'); + expect(result).toEqual(defaultNextAction); + }); + + it('should handle voucher_details', () => { + const dict = { + next_action: { + type: 'voucher', + voucher_details: { + download_url: 'https://example.com/voucher.pdf', + reference: 'REF123', + }, + }, + }; + const result = getNextAction(dict, 'next_action'); + expect(result.type_).toBe('voucher'); + expect(result.voucher_details).toBeDefined(); + expect(result.voucher_details?.download_url).toBe('https://example.com/voucher.pdf'); + }); + }); + + describe('itemToObjMapper', () => { + it('should map dict to intent object', () => { + const dict = { + next_action: { + redirect_to_url: 'https://example.com/redirect', + type: 'redirect', + }, + status: 'succeeded', + payment_id: 'pay_123', + client_secret: 'secret_123', + error_message: '', + payment_method_type: 'card', + manual_retry_allowed: true, + connector_transaction_id: 'txn_123', + }; + const result = itemToObjMapper(dict); + expect(result.status).toBe('succeeded'); + expect(result.paymentId).toBe('pay_123'); + expect(result.clientSecret).toBe('secret_123'); + expect(result.payment_method_type).toBe('card'); + expect(result.manualRetryAllowed).toBe(true); + expect(result.connectorTransactionId).toBe('txn_123'); + }); + + it('should use default values for missing fields', () => { + const dict = {}; + const result = itemToObjMapper(dict); + expect(result.status).toBe(''); + expect(result.paymentId).toBe(''); + expect(result.clientSecret).toBe(''); + expect(result.error_message).toBe(''); + expect(result.payment_method_type).toBe(''); + expect(result.manualRetryAllowed).toBe(false); + expect(result.connectorTransactionId).toBe(''); + }); + + it('should handle error_message field', () => { + const dict = { + error_message: 'Payment failed', + }; + const result = itemToObjMapper(dict); + expect(result.error_message).toBe('Payment failed'); + }); + }); +}); diff --git a/src/__tests__/PaymentConfirmTypesV2.test.ts b/src/__tests__/PaymentConfirmTypesV2.test.ts new file mode 100644 index 000000000..8c248aed0 --- /dev/null +++ b/src/__tests__/PaymentConfirmTypesV2.test.ts @@ -0,0 +1,232 @@ +import { + defaultAuthenticationDetails, + defaultNextAction, + defaultIntent, + defaultToken, + defaultAssociatedPaymentMethodObj, + getNextAction, + getAuthenticationDetails, + getAssociatedPaymentMethods, + itemToPMMConfirmMapper, +} from '../Types/PaymentConfirmTypesV2.bs.js'; + +describe('PaymentConfirmTypesV2', () => { + describe('defaultAuthenticationDetails', () => { + it('should have empty default values', () => { + expect(defaultAuthenticationDetails.status).toBe(''); + expect(defaultAuthenticationDetails.error).toBe(''); + }); + }); + + describe('defaultNextAction', () => { + it('should have empty default values', () => { + expect(defaultNextAction.redirectToUrl).toBe(''); + expect(defaultNextAction.type_).toBe(''); + expect(defaultNextAction.next_action_data).toBeUndefined(); + }); + }); + + describe('defaultIntent', () => { + it('should have empty default values', () => { + expect(defaultIntent.id).toBe(''); + expect(defaultIntent.customerId).toBe(''); + expect(defaultIntent.clientSecret).toBe(''); + expect(defaultIntent.associatedPaymentMethods).toEqual([]); + }); + }); + + describe('defaultToken', () => { + it('should have empty default values', () => { + expect(defaultToken.type).toBe(''); + expect(defaultToken.data).toBe(''); + }); + }); + + describe('defaultAssociatedPaymentMethodObj', () => { + it('should have correct default structure', () => { + expect(defaultAssociatedPaymentMethodObj.token).toEqual(defaultToken); + expect(defaultAssociatedPaymentMethodObj.paymentMethodType).toBe(''); + expect(defaultAssociatedPaymentMethodObj.paymentMethodSubType).toBe(''); + }); + }); + + describe('getNextAction', () => { + it('should extract next action from dict', () => { + const dict = { + next_action: { + redirect_to_url: 'https://example.com/redirect', + type: 'redirect', + }, + }; + const result = getNextAction(dict, 'next_action'); + expect(result.redirectToUrl).toBe('https://example.com/redirect'); + expect(result.type_).toBe('redirect'); + }); + + it('should return default values when key not found', () => { + const dict = {}; + const result = getNextAction(dict, 'next_action'); + expect(result).toEqual(defaultNextAction); + }); + + it('should handle next_action_data', () => { + const dict = { + next_action: { + type: 'three_ds', + next_action_data: { + acs_url: 'https://acs.example.com', + }, + }, + }; + const result = getNextAction(dict, 'next_action'); + expect(result.type_).toBe('three_ds'); + expect(result.next_action_data).toEqual({ acs_url: 'https://acs.example.com' }); + }); + + it('should return default when value is null', () => { + const dict = { next_action: null }; + const result = getNextAction(dict, 'next_action'); + expect(result).toEqual(defaultNextAction); + }); + }); + + describe('getAuthenticationDetails', () => { + it('should extract authentication details from dict', () => { + const dict = { + authentication_details: { + status: 'success', + error: '', + }, + }; + const result = getAuthenticationDetails(dict, 'authentication_details'); + expect(result.status).toBe('success'); + expect(result.error).toBe('success'); + }); + + it('should return default values when key not found', () => { + const dict = {}; + const result = getAuthenticationDetails(dict, 'authentication_details'); + expect(result).toEqual(defaultAuthenticationDetails); + }); + + it('should handle null value', () => { + const dict = { authentication_details: null }; + const result = getAuthenticationDetails(dict, 'authentication_details'); + expect(result).toEqual(defaultAuthenticationDetails); + }); + }); + + describe('getAssociatedPaymentMethods', () => { + it('should extract associated payment methods from dict', () => { + const dict = { + associated_payment_methods: [ + { + payment_method_token: { + type: 'network_token', + data: 'token_123', + }, + payment_method_type: 'card', + payment_method_subtype: 'credit', + }, + ], + }; + const result = getAssociatedPaymentMethods(dict); + expect(result).toHaveLength(1); + expect(result[0].token.type).toBe('network_token'); + expect(result[0].token.data).toBe('token_123'); + expect(result[0].paymentMethodType).toBe('card'); + expect(result[0].paymentMethodSubType).toBe('credit'); + }); + + it('should return empty array when no associated payment methods', () => { + const dict = {}; + const result = getAssociatedPaymentMethods(dict); + expect(result).toEqual([]); + }); + + it('should handle multiple payment methods', () => { + const dict = { + associated_payment_methods: [ + { + payment_method_token: { type: 'token1', data: 'data1' }, + payment_method_type: 'card', + payment_method_subtype: 'credit', + }, + { + payment_method_token: { type: 'token2', data: 'data2' }, + payment_method_type: 'wallet', + payment_method_subtype: 'apple_pay', + }, + ], + }; + const result = getAssociatedPaymentMethods(dict); + expect(result).toHaveLength(2); + expect(result[0].paymentMethodType).toBe('card'); + expect(result[1].paymentMethodType).toBe('wallet'); + }); + + it('should handle empty array', () => { + const dict = { + associated_payment_methods: [], + }; + const result = getAssociatedPaymentMethods(dict); + expect(result).toEqual([]); + }); + + it('should use defaults for missing fields', () => { + const dict = { + associated_payment_methods: [{}], + }; + const result = getAssociatedPaymentMethods(dict); + expect(result).toHaveLength(1); + expect(result[0].token.type).toBe(''); + expect(result[0].token.data).toBe(''); + expect(result[0].paymentMethodType).toBe(''); + }); + }); + + describe('itemToPMMConfirmMapper', () => { + it('should map dict to PMM confirm object', () => { + const dict = { + next_action: { + redirect_to_url: 'https://example.com/redirect', + type: 'redirect', + }, + id: 'pi_123', + customer_id: 'cus_123', + client_secret: 'secret_123', + authentication_details: { + status: 'success', + }, + associated_payment_methods: [], + }; + const result = itemToPMMConfirmMapper(dict); + expect(result.id).toBe('pi_123'); + expect(result.customerId).toBe('cus_123'); + expect(result.clientSecret).toBe('secret_123'); + expect(result.nextAction.redirectToUrl).toBe('https://example.com/redirect'); + }); + + it('should use default values for missing fields', () => { + const dict = {}; + const result = itemToPMMConfirmMapper(dict); + expect(result.id).toBe(''); + expect(result.customerId).toBe(''); + expect(result.clientSecret).toBe(''); + expect(result.nextAction).toEqual(defaultNextAction); + expect(result.authenticationDetails).toEqual(defaultAuthenticationDetails); + expect(result.associatedPaymentMethods).toEqual([]); + }); + + it('should handle partial data', () => { + const dict = { + id: 'pi_456', + client_secret: 'secret_456', + }; + const result = itemToPMMConfirmMapper(dict); + expect(result.id).toBe('pi_456'); + expect(result.clientSecret).toBe('secret_456'); + expect(result.customerId).toBe(''); + }); + }); +}); diff --git a/src/__tests__/PaymentError.test.ts b/src/__tests__/PaymentError.test.ts new file mode 100644 index 000000000..b08ad6d67 --- /dev/null +++ b/src/__tests__/PaymentError.test.ts @@ -0,0 +1,114 @@ +import { + defaultError, + getError, + itemToObjMapper, +} from '../Types/PaymentError.bs.js'; + +describe('PaymentError', () => { + describe('defaultError', () => { + it('should have correct default values', () => { + expect(defaultError.type_).toBe('server_error'); + expect(defaultError.code).toBe(''); + expect(defaultError.message).toBe('Something went wrong'); + }); + }); + + describe('getError', () => { + it('should extract error from dict', () => { + const dict = { + error: { + type: 'validation_error', + code: 'INVALID_CARD', + message: 'Card number is invalid', + }, + }; + const result = getError(dict, 'error'); + expect(result.type_).toBe('validation_error'); + expect(result.code).toBe('INVALID_CARD'); + expect(result.message).toBe('Card number is invalid'); + }); + + it('should return default values when key not found', () => { + const dict = {}; + const result = getError(dict, 'error'); + expect(result).toEqual(defaultError); + }); + + it('should return default values when value is null', () => { + const dict = { error: null }; + const result = getError(dict, 'error'); + expect(result).toEqual(defaultError); + }); + + it('should handle partial error data', () => { + const dict = { + error: { + type: 'api_error', + }, + }; + const result = getError(dict, 'error'); + expect(result.type_).toBe('api_error'); + expect(result.code).toBe(''); + expect(result.message).toBe(''); + }); + + it('should handle empty error object', () => { + const dict = { + error: {}, + }; + const result = getError(dict, 'error'); + expect(result.type_).toBe(''); + expect(result.code).toBe(''); + expect(result.message).toBe(''); + }); + }); + + describe('itemToObjMapper', () => { + it('should map dict to error response object', () => { + const dict = { + error: { + type: 'authentication_error', + code: 'AUTH_FAILED', + message: 'Authentication failed', + }, + }; + const result = itemToObjMapper(dict); + expect(result.error.type_).toBe('authentication_error'); + expect(result.error.code).toBe('AUTH_FAILED'); + expect(result.error.message).toBe('Authentication failed'); + }); + + it('should use default error when key not found', () => { + const dict = {}; + const result = itemToObjMapper(dict); + expect(result.error).toEqual(defaultError); + }); + + it('should handle various error types', () => { + const dict = { + error: { + type: 'rate_limit_error', + code: 'RATE_LIMIT', + message: 'Too many requests', + }, + }; + const result = itemToObjMapper(dict); + expect(result.error.type_).toBe('rate_limit_error'); + expect(result.error.code).toBe('RATE_LIMIT'); + }); + + it('should handle card decline error', () => { + const dict = { + error: { + type: 'card_error', + code: 'CARD_DECLINED', + message: 'Your card was declined', + }, + }; + const result = itemToObjMapper(dict); + expect(result.error.type_).toBe('card_error'); + expect(result.error.code).toBe('CARD_DECLINED'); + expect(result.error.message).toBe('Your card was declined'); + }); + }); +}); diff --git a/src/__tests__/PaymentHelpers.test.ts b/src/__tests__/PaymentHelpers.test.ts new file mode 100644 index 000000000..7786bdbd7 --- /dev/null +++ b/src/__tests__/PaymentHelpers.test.ts @@ -0,0 +1,2449 @@ +import * as PaymentHelpers from '../Utilities/PaymentHelpers.bs.js'; + +const mockMessageParentWindow = jest.fn(); +const mockFetchApiWithLogging = jest.fn(); +const mockGetNonEmptyOption = jest.fn((val: any) => (val ? val : undefined)); +const mockGetDictFromJson = jest.fn((obj: any) => (typeof obj === 'object' && obj !== null ? obj : {})); +const mockGetString = jest.fn((obj: any, key: string, def: string) => obj?.[key] ?? def); +const mockDelay = jest.fn((ms: number) => Promise.resolve()); +const mockFetchApi = jest.fn(); +const mockOpenUrl = jest.fn(); +const mockReplaceRootHref = jest.fn(); +const mockPostSubmitResponse = jest.fn(); +const mockHandleOnCompleteDoThisMessage = jest.fn(); +const mockGetFailedSubmitResponse = jest.fn((type: string, msg: string) => ({ type, message: msg })); +const mockFormatException = jest.fn((e: any) => e?.message || String(e)); +const mockGetPaymentId = jest.fn((secret: string) => secret?.split('_secret_')[0] || ''); +const mockGetJsonFromArrayOfJson = jest.fn((arr: any) => Object.fromEntries(arr)); +const mockGetStringFromJson = jest.fn((val: any, def: string) => (typeof val === 'string' ? val : def)); +const mockDeepCopyDict = jest.fn((obj: any) => JSON.parse(JSON.stringify(obj))); +const mockGetBoolValue = jest.fn((val: any) => Boolean(val)); +const mockGetJsonObjectFromDict = jest.fn((obj: any, key: string) => obj?.[key] || {}); +const mockGetDictFromDict = jest.fn((obj: any, key: string) => obj?.[key] || {}); +const mockMergeHeadersIntoDict = jest.fn(); +const mockPostFailedSubmitResponse = jest.fn(); +const mockSafeParse = jest.fn((str: string) => { + try { + return JSON.parse(str); + } catch { + return null; + } +}); +const mockSafeParseOpt = jest.fn((str: string) => { + try { + return JSON.parse(str); + } catch { + return null; + } +}); +const mockGetStringFromBool = jest.fn((val: boolean) => (val ? 'true' : 'false')); + +jest.mock('../Utilities/Utils.bs.js', () => ({ + getDictFromJson: (obj: any) => mockGetDictFromJson(obj), + getString: (obj: any, key: string, def: string) => mockGetString(obj, key, def), + messageParentWindow: (a: any, b: any) => mockMessageParentWindow(a, b), + getNonEmptyOption: (val: any) => mockGetNonEmptyOption(val), + fetchApiWithLogging: (...args: any[]) => mockFetchApiWithLogging(...args), + delay: (ms: number) => mockDelay(ms), + fetchApi: (url: string, body: any, headers: any, method: string) => mockFetchApi(url, body, headers, method), + openUrl: (url: string) => mockOpenUrl(url), + replaceRootHref: (url: string, flags: any) => mockReplaceRootHref(url, flags), + postSubmitResponse: (data: any, url: string) => mockPostSubmitResponse(data, url), + handleOnCompleteDoThisMessage: (a: any) => mockHandleOnCompleteDoThisMessage(a), + getFailedSubmitResponse: (type: string, msg: string) => mockGetFailedSubmitResponse(type, msg), + formatException: (e: any) => mockFormatException(e), + getPaymentId: (secret: string) => mockGetPaymentId(secret), + getJsonFromArrayOfJson: (arr: any) => mockGetJsonFromArrayOfJson(arr), + getStringFromJson: (val: any, def: string) => mockGetStringFromJson(val, def), + deepCopyDict: (obj: any) => mockDeepCopyDict(obj), + getBoolValue: (val: any) => mockGetBoolValue(val), + getJsonObjectFromDict: (obj: any, key: string) => mockGetJsonObjectFromDict(obj, key), + getDictFromDict: (obj: any, key: string) => mockGetDictFromDict(obj, key), + mergeHeadersIntoDict: (dict: any, headers: any) => mockMergeHeadersIntoDict(dict, headers), + postFailedSubmitResponse: (type: string, msg: string) => mockPostFailedSubmitResponse(type, msg), + safeParse: (str: string) => mockSafeParse(str), + safeParseOpt: (str: string) => mockSafeParseOpt(str), + getStringFromBool: (val: boolean) => mockGetStringFromBool(val), +})); + +jest.mock('../Utilities/APIHelpers/APIUtils.bs.js', () => ({ + generateApiUrlV1: jest.fn((params: any, endpoint: string) => `https://api.test.com/${endpoint}`), + addCustomPodHeader: jest.fn((headers: any, uri: any) => headers), +})); + +jest.mock('../Utilities/ApiEndpoint.bs.js', () => ({ + getApiEndPoint: jest.fn((key: string, isThirdParty: boolean) => 'https://api.test.com'), + addCustomPodHeader: jest.fn((headers: any, uri: any) => headers), +})); + +jest.mock('../Utilities/LoggerUtils.bs.js', () => ({ + logApi: jest.fn(), + handleLogging: jest.fn(), +})); + +jest.mock('../Utilities/PaymentBody.bs.js', () => ({ + paymentTypeBody: jest.fn((type: string) => []), + mandateBody: jest.fn((type: string) => []), +})); + +jest.mock('../Payments/PaymentMethodsRecord.bs.js', () => ({ + itemToObjMapper: jest.fn((obj: any) => ({ + payment_type: 'NORMAL', + payment_methods: [{ payment_method_type: 'card' }], + mandate_payment: undefined, + ...obj, + })), + paymentTypeToStringMapper: jest.fn((type: any) => 'NORMAL'), +})); + +jest.mock('../Types/PaymentConfirmTypes.bs.js', () => ({ + itemToObjMapper: jest.fn((obj: any) => ({ + status: 'succeeded', + payment_method_type: 'card', + nextAction: { type_: '' }, + ...obj, + })), +})); + +jest.mock('../Types/PaymentError.bs.js', () => ({ + itemToObjMapper: jest.fn((obj: any) => ({ + error: { type_: 'test_error', message: 'Test error message' }, + ...obj, + })), +})); + +jest.mock('../BrowserSpec.bs.js', () => ({ + broswerInfo: jest.fn(() => [ + ['user_agent', 'test-agent'], + ['ip', '127.0.0.1'], + ]), +})); + +jest.mock('../CardUtils.bs.js', () => ({ + getQueryParamsDictforKey: jest.fn((search: string, key: string) => 'payment'), +})); + +jest.mock('../Types/CardThemeType.bs.js', () => ({ + getPaymentMode: jest.fn((name: string) => 'NONE'), + getPaymentModeToStrMapper: jest.fn((mode: any) => 'payment'), +})); + +jest.mock('../Window.bs.js', () => ({ + getRootHostName: jest.fn(() => 'example.com'), +})); + +describe('PaymentHelpers', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('getPaymentType', () => { + it('returns Applepay for apple_pay', () => { + expect(PaymentHelpers.getPaymentType('apple_pay')).toBe('Applepay'); + }); + + it('returns Card for credit', () => { + expect(PaymentHelpers.getPaymentType('credit')).toBe('Card'); + }); + + it('returns Card for debit', () => { + expect(PaymentHelpers.getPaymentType('debit')).toBe('Card'); + }); + + it('returns Card for empty string', () => { + expect(PaymentHelpers.getPaymentType('')).toBe('Card'); + }); + + it('returns Gpay for google_pay', () => { + expect(PaymentHelpers.getPaymentType('google_pay')).toBe('Gpay'); + }); + + it('returns Paze for paze', () => { + expect(PaymentHelpers.getPaymentType('paze')).toBe('Paze'); + }); + + it('returns Samsungpay for samsung_pay', () => { + expect(PaymentHelpers.getPaymentType('samsung_pay')).toBe('Samsungpay'); + }); + + it('returns Other for unknown payment method types', () => { + expect(PaymentHelpers.getPaymentType('unknown_method')).toBe('Other'); + }); + + it('returns Other for paypal', () => { + expect(PaymentHelpers.getPaymentType('paypal')).toBe('Other'); + }); + }); + + describe('closePaymentLoaderIfAny', () => { + it('calls messageParentWindow with fullscreen false', () => { + PaymentHelpers.closePaymentLoaderIfAny(); + expect(mockMessageParentWindow).toHaveBeenCalledWith(undefined, [['fullscreen', false]]); + }); + + it('is callable multiple times', () => { + PaymentHelpers.closePaymentLoaderIfAny(); + PaymentHelpers.closePaymentLoaderIfAny(); + expect(mockMessageParentWindow).toHaveBeenCalledTimes(2); + }); + }); + + describe('maskStr', () => { + it('replaces all non-whitespace characters with x', () => { + expect(PaymentHelpers.maskStr('hello world')).toBe('xxxxx xxxxx'); + }); + + it('handles empty string', () => { + expect(PaymentHelpers.maskStr('')).toBe(''); + }); + + it('preserves whitespace', () => { + expect(PaymentHelpers.maskStr(' test ')).toBe(' xxxx '); + }); + + it('handles numbers as strings', () => { + expect(PaymentHelpers.maskStr('12345')).toBe('xxxxx'); + }); + + it('handles special characters', () => { + expect(PaymentHelpers.maskStr('test@email.com')).toBe('xxxxxxxxxxxxxx'); + }); + + it('handles strings with mixed characters', () => { + expect(PaymentHelpers.maskStr('Hello World 123!')).toBe('xxxxx xxxxx xxxx'); + }); + + it('handles unicode characters', () => { + const masked = PaymentHelpers.maskStr('日本語'); + expect(masked).toMatch(/^x+$/); + }); + + it('handles very long strings', () => { + const longStr = 'a'.repeat(1000); + const masked = PaymentHelpers.maskStr(longStr); + expect(masked).toBe('x'.repeat(1000)); + }); + }); + + describe('maskPayload', () => { + it('masks string values', () => { + const result = PaymentHelpers.maskPayload('sensitive-data'); + expect(result).toBe('xxxxxxxxxxxxxx'); + }); + + it('returns string representation of numbers masked', () => { + const result = PaymentHelpers.maskPayload(12345); + expect(result).toBe('xxxxx'); + }); + + it('returns string representation for boolean true', () => { + const result = PaymentHelpers.maskPayload(true); + expect(result).toBe('true'); + }); + + it('returns string representation for boolean false', () => { + const result = PaymentHelpers.maskPayload(false); + expect(result).toBe('false'); + }); + + it('returns null for null input', () => { + const result = PaymentHelpers.maskPayload(null); + expect(result).toBe('null'); + }); + + it('processes array values', () => { + const result = PaymentHelpers.maskPayload(['a', 'b', 'c']); + expect(Array.isArray(result)).toBe(true); + }); + + it('handles nested objects', () => { + const nestedObj = { + level1: { + level2: { + level3: 'secret' + } + } + }; + const result = PaymentHelpers.maskPayload(nestedObj); + expect(result).toBeDefined(); + }); + + it('handles arrays of objects', () => { + const arr = [ + { name: 'John', email: 'john@example.com' }, + { name: 'Jane', email: 'jane@example.com' } + ]; + const result = PaymentHelpers.maskPayload(arr); + expect(Array.isArray(result)).toBe(true); + }); + }); + + describe('getConstructedPaymentMethodName', () => { + it('returns card for card payment method', () => { + expect(PaymentHelpers.getConstructedPaymentMethodName('card', 'visa')).toBe('card'); + }); + + it('appends _debit for bank_debit payment method', () => { + expect(PaymentHelpers.getConstructedPaymentMethodName('bank_debit', 'ach')).toBe('ach_debit'); + }); + + it('returns payment method type for bank_transfer not in list', () => { + expect(PaymentHelpers.getConstructedPaymentMethodName('bank_transfer', 'sepa')).toBe('sepa_transfer'); + }); + + it('returns payment method type as-is for unknown payment method', () => { + expect(PaymentHelpers.getConstructedPaymentMethodName('wallet', 'apple_pay')).toBe('apple_pay'); + }); + + it('handles bank_debit with different connector types', () => { + expect(PaymentHelpers.getConstructedPaymentMethodName('bank_debit', 'ach')).toBe('ach_debit'); + expect(PaymentHelpers.getConstructedPaymentMethodName('bank_debit', 'sepa')).toBe('sepa_debit'); + expect(PaymentHelpers.getConstructedPaymentMethodName('bank_debit', 'becs')).toBe('becs_debit'); + }); + + it('handles bank_transfer connector types', () => { + expect(PaymentHelpers.getConstructedPaymentMethodName('bank_transfer', 'sepa')).toBe('sepa_transfer'); + expect(PaymentHelpers.getConstructedPaymentMethodName('bank_transfer', 'ach')).toBe('ach_transfer'); + }); + + it('returns payment method type for non-bank payment methods', () => { + expect(PaymentHelpers.getConstructedPaymentMethodName('wallet', 'apple_pay')).toBe('apple_pay'); + expect(PaymentHelpers.getConstructedPaymentMethodName('card', 'credit')).toBe('card'); + }); + }); + + describe('retrievePaymentIntent', () => { + it('returns data on successful fetch', async () => { + const mockData = { id: 'pay_123', status: 'requires_payment_method' }; + mockFetchApiWithLogging.mockResolvedValue(mockData); + mockGetNonEmptyOption.mockReturnValue(undefined); + + await PaymentHelpers.retrievePaymentIntent( + 'secret_test', + undefined, + 'pk_test', + undefined, + 'customUri', + false, + undefined + ); + + expect(mockFetchApiWithLogging).toHaveBeenCalled(); + }); + + it('returns null on failure', async () => { + mockFetchApiWithLogging.mockResolvedValue(null); + mockGetNonEmptyOption.mockReturnValue(undefined); + + const result = await PaymentHelpers.retrievePaymentIntent( + 'secret_test', + undefined, + 'pk_test', + undefined, + 'customUri', + false, + undefined + ); + + expect(result).toBeNull(); + }); + + it('handles force sync flag', async () => { + const mockData = { id: 'pay_123', status: 'requires_payment_method' }; + mockFetchApiWithLogging.mockResolvedValue(mockData); + mockGetNonEmptyOption.mockReturnValue(undefined); + + await PaymentHelpers.retrievePaymentIntent( + 'secret_test', + undefined, + 'pk_test', + undefined, + 'customUri', + true, + undefined + ); + + expect(mockFetchApiWithLogging).toHaveBeenCalled(); + }); + + it('handles with sdkAuthorization', async () => { + const mockData = { id: 'pay_123' }; + mockFetchApiWithLogging.mockResolvedValue(mockData); + mockGetNonEmptyOption.mockReturnValue('auth_token'); + + await PaymentHelpers.retrievePaymentIntent( + 'secret_test', + undefined, + 'pk_test', + undefined, + 'customUri', + false, + 'auth_token' + ); + + expect(mockFetchApiWithLogging).toHaveBeenCalled(); + }); + }); + + describe('fetchBlockedBins', () => { + it('returns data on successful fetch', async () => { + const mockData = { blocked_bins: ['411111'] }; + mockFetchApiWithLogging.mockResolvedValue(mockData); + + await PaymentHelpers.fetchBlockedBins( + 'auth_token', + 'secret_test', + 'pk_test', + undefined, + 'customUri', + 'https://endpoint.com' + ); + + expect(mockFetchApiWithLogging).toHaveBeenCalled(); + }); + + it('returns null on failure', async () => { + mockFetchApiWithLogging.mockResolvedValue(null); + + const result = await PaymentHelpers.fetchBlockedBins( + undefined, + 'secret_test', + 'pk_test', + undefined, + 'customUri', + 'https://endpoint.com' + ); + + expect(result).toBeNull(); + }); + + it('handles fetch with sdkAuthorization', async () => { + const mockData = { blocked_bins: ['411111'] }; + mockFetchApiWithLogging.mockResolvedValue(mockData); + + await PaymentHelpers.fetchBlockedBins( + 'auth_token', + 'secret_test', + 'pk_test', + undefined, + 'customUri', + 'https://endpoint.com' + ); + + expect(mockFetchApiWithLogging).toHaveBeenCalled(); + }); + }); + + describe('retrieveStatus', () => { + it('returns data on successful fetch', async () => { + const mockData = { status: 'completed' }; + mockFetchApiWithLogging.mockResolvedValue(mockData); + + await PaymentHelpers.retrieveStatus( + 'pk_test', + 'customUri', + 'poll_123', + undefined, + 'auth_token' + ); + + expect(mockFetchApiWithLogging).toHaveBeenCalled(); + }); + + it('returns null on failure', async () => { + mockFetchApiWithLogging.mockResolvedValue(null); + + const result = await PaymentHelpers.retrieveStatus( + 'pk_test', + 'customUri', + 'poll_123', + undefined, + undefined + ); + + expect(result).toBeNull(); + }); + }); + + describe('fetchSessions', () => { + it('returns session data on successful fetch', async () => { + const mockData = { session_tokens: {} }; + mockFetchApiWithLogging.mockResolvedValue(mockData); + mockGetNonEmptyOption.mockReturnValue(undefined); + + await PaymentHelpers.fetchSessions( + 'secret_test', + 'pk_test', + ['google_pay', 'apple_pay'], + false, + undefined, + 'customUri', + 'https://endpoint.com', + false, + 'merchant.example.com', + undefined + ); + + expect(mockFetchApiWithLogging).toHaveBeenCalled(); + }); + + it('uses default wallet array when not provided', async () => { + mockFetchApiWithLogging.mockResolvedValue({}); + mockGetNonEmptyOption.mockReturnValue(undefined); + + await PaymentHelpers.fetchSessions( + 'secret_test', + 'pk_test', + undefined, + undefined, + undefined, + 'customUri', + 'https://endpoint.com' + ); + + expect(mockFetchApiWithLogging).toHaveBeenCalled(); + }); + + it('handles delayed session token flag', async () => { + const mockData = { session_tokens: {} }; + mockFetchApiWithLogging.mockResolvedValue(mockData); + mockGetNonEmptyOption.mockReturnValue('auth_token'); + + await PaymentHelpers.fetchSessions( + 'secret_test', + 'pk_test', + ['google_pay'], + true, + undefined, + 'customUri', + 'https://endpoint.com', + true, + 'merchant.example.com', + 'auth_token' + ); + + expect(mockFetchApiWithLogging).toHaveBeenCalled(); + }); + }); + + describe('confirmPayout', () => { + it('returns data on successful fetch', async () => { + const mockData = { status: 'succeeded' }; + mockFetchApiWithLogging.mockResolvedValue(mockData); + + await PaymentHelpers.confirmPayout( + 'secret_test', + 'pk_test', + undefined, + 'customUri', + 'https://endpoint.com', + [['amount', 100]], + 'payout_123' + ); + + expect(mockFetchApiWithLogging).toHaveBeenCalled(); + }); + + it('returns null on failure', async () => { + mockFetchApiWithLogging.mockResolvedValue(null); + + const result = await PaymentHelpers.confirmPayout( + 'secret_test', + 'pk_test', + undefined, + 'customUri', + 'https://endpoint.com', + [], + 'payout_123' + ); + + expect(result).toBeNull(); + }); + + it('handles payout confirmation with body', async () => { + const mockData = { status: 'succeeded' }; + mockFetchApiWithLogging.mockResolvedValue(mockData); + + await PaymentHelpers.confirmPayout( + 'secret_test', + 'pk_test', + undefined, + 'customUri', + 'https://endpoint.com', + [['amount', 100], ['currency', 'USD']], + 'payout_123' + ); + + expect(mockFetchApiWithLogging).toHaveBeenCalled(); + }); + }); + + describe('createPaymentMethod', () => { + it('returns data on successful fetch', async () => { + const mockData = { id: 'pm_123' }; + mockFetchApiWithLogging.mockResolvedValue(mockData); + + await PaymentHelpers.createPaymentMethod( + 'secret_test', + 'pk_test', + undefined, + 'customUri', + 'https://endpoint.com', + [['type', 'card']] + ); + + expect(mockFetchApiWithLogging).toHaveBeenCalled(); + }); + + it('returns null on failure', async () => { + mockFetchApiWithLogging.mockResolvedValue(null); + + const result = await PaymentHelpers.createPaymentMethod( + 'secret_test', + 'pk_test', + undefined, + 'customUri', + 'https://endpoint.com', + [] + ); + + expect(result).toBeNull(); + }); + + it('handles payment method creation with body', async () => { + const mockData = { id: 'pm_123' }; + mockFetchApiWithLogging.mockResolvedValue(mockData); + + await PaymentHelpers.createPaymentMethod( + 'secret_test', + 'pk_test', + undefined, + 'customUri', + 'https://endpoint.com', + [['type', 'card'], ['card[number]', '4111111111111111']] + ); + + expect(mockFetchApiWithLogging).toHaveBeenCalled(); + }); + }); + + describe('fetchPaymentMethodList', () => { + it('returns data on successful fetch', async () => { + const mockData = { payment_methods: [] }; + mockFetchApiWithLogging.mockResolvedValue(mockData); + + await PaymentHelpers.fetchPaymentMethodList( + undefined, + 'secret_test', + 'pk_test', + undefined, + 'customUri', + 'https://endpoint.com' + ); + + expect(mockFetchApiWithLogging).toHaveBeenCalled(); + }); + + it('returns null on failure', async () => { + mockFetchApiWithLogging.mockResolvedValue(null); + + const result = await PaymentHelpers.fetchPaymentMethodList( + undefined, + 'secret_test', + 'pk_test', + undefined, + 'customUri', + 'https://endpoint.com' + ); + + expect(result).toBeNull(); + }); + + it('uses sdkAuthorization when provided', async () => { + mockFetchApiWithLogging.mockResolvedValue({ payment_methods: [] }); + mockGetNonEmptyOption.mockReturnValue('auth_token'); + + await PaymentHelpers.fetchPaymentMethodList( + 'auth_token', + 'secret_test', + 'pk_test', + undefined, + 'customUri', + 'https://endpoint.com' + ); + + expect(mockFetchApiWithLogging).toHaveBeenCalled(); + }); + }); + + describe('fetchCustomerPaymentMethodList', () => { + it('returns data on successful fetch', async () => { + const mockData = { customer_payment_methods: [] }; + mockFetchApiWithLogging.mockResolvedValue(mockData); + + await PaymentHelpers.fetchCustomerPaymentMethodList( + 'secret_test', + 'pk_test', + undefined, + 'customUri', + 'https://endpoint.com', + false, + undefined + ); + + expect(mockFetchApiWithLogging).toHaveBeenCalled(); + }); + + it('returns null on failure', async () => { + mockFetchApiWithLogging.mockResolvedValue(null); + + const result = await PaymentHelpers.fetchCustomerPaymentMethodList( + 'secret_test', + 'pk_test', + undefined, + 'customUri', + 'https://endpoint.com', + false, + undefined + ); + + expect(result).toBeNull(); + }); + + it('handles payment session flag', async () => { + const mockData = { customer_payment_methods: [] }; + mockFetchApiWithLogging.mockResolvedValue(mockData); + mockGetNonEmptyOption.mockReturnValue('auth_token'); + + await PaymentHelpers.fetchCustomerPaymentMethodList( + 'secret_test', + 'pk_test', + undefined, + 'customUri', + 'https://endpoint.com', + true, + 'auth_token' + ); + + expect(mockFetchApiWithLogging).toHaveBeenCalled(); + }); + }); + + describe('calculateTax', () => { + it('returns tax data on successful fetch', async () => { + const mockData = { tax_amount: 100 }; + mockFetchApiWithLogging.mockResolvedValue(mockData); + mockGetNonEmptyOption.mockReturnValue(undefined); + + await PaymentHelpers.calculateTax( + 'pk_test', + 'secret_test', + 'card', + { country: 'US', postal_code: '12345' }, + undefined, + 'customUri', + 'session_123', + undefined + ); + + expect(mockFetchApiWithLogging).toHaveBeenCalled(); + }); + + it('returns null on failure', async () => { + mockFetchApiWithLogging.mockResolvedValue(null); + mockGetNonEmptyOption.mockReturnValue(undefined); + + const result = await PaymentHelpers.calculateTax( + 'pk_test', + 'secret_test', + 'card', + {}, + undefined, + 'customUri', + undefined, + undefined + ); + + expect(result).toBeNull(); + }); + + it('handles tax calculation with session ID', async () => { + const mockData = { tax_amount: 100 }; + mockFetchApiWithLogging.mockResolvedValue(mockData); + mockGetNonEmptyOption.mockReturnValue(undefined); + + await PaymentHelpers.calculateTax( + 'pk_test', + 'secret_test', + 'card', + { country: 'US', postal_code: '12345' }, + undefined, + 'customUri', + 'session_123', + undefined + ); + + expect(mockFetchApiWithLogging).toHaveBeenCalled(); + }); + }); + + describe('fetchEnabledAuthnMethodsToken', () => { + it('returns data on successful fetch', async () => { + const mockData = { token: 'auth_token' }; + mockFetchApiWithLogging.mockResolvedValue(mockData); + + await PaymentHelpers.fetchEnabledAuthnMethodsToken( + 'secret_test', + 'pk_test', + undefined, + 'customUri', + 'https://endpoint.com', + false, + 'profile_123', + 'auth_123' + ); + + expect(mockFetchApiWithLogging).toHaveBeenCalled(); + }); + + it('returns null on failure', async () => { + mockFetchApiWithLogging.mockResolvedValue(null); + + const result = await PaymentHelpers.fetchEnabledAuthnMethodsToken( + 'secret_test', + 'pk_test', + undefined, + 'customUri', + 'https://endpoint.com', + false, + 'profile_123', + 'auth_123' + ); + + expect(result).toBeNull(); + }); + + it('handles with payment session flag', async () => { + const mockData = { token: 'auth_token' }; + mockFetchApiWithLogging.mockResolvedValue(mockData); + + await PaymentHelpers.fetchEnabledAuthnMethodsToken( + 'secret_test', + 'pk_test', + undefined, + 'customUri', + 'https://endpoint.com', + true, + 'profile_123', + 'auth_123' + ); + + expect(mockFetchApiWithLogging).toHaveBeenCalled(); + }); + }); + + describe('fetchEligibilityCheck', () => { + it('returns data on successful fetch', async () => { + const mockData = { eligible: true }; + mockFetchApiWithLogging.mockResolvedValue(mockData); + + await PaymentHelpers.fetchEligibilityCheck( + 'secret_test', + 'pk_test', + undefined, + 'customUri', + 'https://endpoint.com', + false, + 'profile_123', + 'auth_123', + [['method', 'sms']] + ); + + expect(mockFetchApiWithLogging).toHaveBeenCalled(); + }); + + it('returns null on failure', async () => { + mockFetchApiWithLogging.mockResolvedValue(null); + + const result = await PaymentHelpers.fetchEligibilityCheck( + 'secret_test', + 'pk_test', + undefined, + 'customUri', + 'https://endpoint.com', + false, + 'profile_123', + 'auth_123', + [] + ); + + expect(result).toBeNull(); + }); + + it('handles with payment session flag', async () => { + const mockData = { eligible: true }; + mockFetchApiWithLogging.mockResolvedValue(mockData); + + await PaymentHelpers.fetchEligibilityCheck( + 'secret_test', + 'pk_test', + undefined, + 'customUri', + 'https://endpoint.com', + true, + 'profile_123', + 'auth_123', + [['method', 'sms']] + ); + + expect(mockFetchApiWithLogging).toHaveBeenCalled(); + }); + }); + + describe('fetchAuthenticationSync', () => { + it('returns data on successful fetch', async () => { + const mockData = { authenticated: true }; + mockFetchApiWithLogging.mockResolvedValue(mockData); + + await PaymentHelpers.fetchAuthenticationSync( + 'secret_test', + 'pk_test', + undefined, + 'customUri', + 'https://endpoint.com', + false, + 'profile_123', + 'auth_123', + 'merchant_123', + [['otp', '123456']] + ); + + expect(mockFetchApiWithLogging).toHaveBeenCalled(); + }); + + it('returns error on failure', async () => { + const mockError = { error: 'authentication_failed' }; + mockFetchApiWithLogging.mockResolvedValue(mockError); + + const result = await PaymentHelpers.fetchAuthenticationSync( + 'secret_test', + 'pk_test', + undefined, + 'customUri', + 'https://endpoint.com', + false, + 'profile_123', + 'auth_123', + 'merchant_123', + [] + ); + + expect(result).toBeDefined(); + }); + + it('handles successful authentication', async () => { + const mockData = { authenticated: true }; + mockFetchApiWithLogging.mockResolvedValue(mockData); + + await PaymentHelpers.fetchAuthenticationSync( + 'secret_test', + 'pk_test', + undefined, + 'customUri', + 'https://endpoint.com', + true, + 'profile_123', + 'auth_123', + 'merchant_123', + [['otp', '123456']] + ); + + expect(mockFetchApiWithLogging).toHaveBeenCalled(); + }); + + it('handles error response', async () => { + const mockError = { error: 'authentication_failed' }; + mockFetchApiWithLogging.mockResolvedValue(mockError); + + const result = await PaymentHelpers.fetchAuthenticationSync( + 'secret_test', + 'pk_test', + undefined, + 'customUri', + 'https://endpoint.com', + false, + 'profile_123', + 'auth_123', + 'merchant_123', + [] + ); + + expect(result).toBeDefined(); + }); + }); + + describe('threeDsAuth', () => { + it('returns data on successful authentication', async () => { + const mockData = { three_ds_auth: { status: 'success' } }; + mockFetchApiWithLogging.mockResolvedValue(mockData); + mockGetNonEmptyOption.mockReturnValue(undefined); + + await PaymentHelpers.threeDsAuth( + 'secret_test', + undefined, + 'Y', + [['Content-Type', 'application/json']], + undefined + ); + + expect(mockFetchApiWithLogging).toHaveBeenCalled(); + }); + + it('returns null on authentication failure', async () => { + mockFetchApiWithLogging.mockResolvedValue(null); + mockGetNonEmptyOption.mockReturnValue(undefined); + mockGetDictFromJson.mockReturnValue({ + error: { type: 'auth_failed', message: 'Authentication failed' }, + }); + + await PaymentHelpers.threeDsAuth( + 'secret_test', + undefined, + 'N', + [], + undefined + ); + + expect(mockFetchApiWithLogging).toHaveBeenCalled(); + }); + + it('handles failure with error response', async () => { + mockFetchApiWithLogging.mockResolvedValue(null); + mockGetDictFromJson.mockReturnValue({ + error: { type: 'auth_failed', message: 'Authentication failed' }, + }); + mockGetNonEmptyOption.mockReturnValue(undefined); + + const result = await PaymentHelpers.threeDsAuth( + 'secret_test', + undefined, + 'N', + [], + undefined + ); + + expect(result).toBeNull(); + }); + }); + + describe('pollRetrievePaymentIntent', () => { + it('returns succeeded status immediately', async () => { + mockFetchApiWithLogging.mockResolvedValue({ status: 'succeeded' }); + mockGetDictFromJson.mockReturnValue({ status: 'succeeded' }); + mockGetString.mockReturnValue('succeeded'); + mockGetNonEmptyOption.mockReturnValue(undefined); + + await PaymentHelpers.pollRetrievePaymentIntent( + 'secret_test', + undefined, + 'pk_test', + undefined, + 'customUri', + false, + undefined + ); + + expect(mockFetchApiWithLogging).toHaveBeenCalled(); + }); + + it('returns failed status immediately', async () => { + mockFetchApiWithLogging.mockResolvedValue({ status: 'failed' }); + mockGetDictFromJson.mockReturnValue({ status: 'failed' }); + mockGetString.mockReturnValue('failed'); + mockGetNonEmptyOption.mockReturnValue(undefined); + + await PaymentHelpers.pollRetrievePaymentIntent( + 'secret_test', + undefined, + 'pk_test', + undefined, + 'customUri', + false, + undefined + ); + + expect(mockFetchApiWithLogging).toHaveBeenCalled(); + }); + + it('returns a promise', () => { + mockFetchApiWithLogging.mockResolvedValue({ status: 'succeeded' }); + mockGetDictFromJson.mockReturnValue({ status: 'succeeded' }); + mockGetString.mockReturnValue('succeeded'); + mockGetNonEmptyOption.mockReturnValue(undefined); + + const result = PaymentHelpers.pollRetrievePaymentIntent( + 'secret_test', + undefined, + 'pk_test', + undefined, + 'customUri', + false, + undefined + ); + + expect(result).toBeInstanceOf(Promise); + }); + }); + + describe('pollStatus', () => { + it('returns completed status immediately', async () => { + mockFetchApiWithLogging.mockResolvedValue({ status: 'completed' }); + mockGetDictFromJson.mockReturnValue({ status: 'completed' }); + mockGetString.mockReturnValue('completed'); + mockGetNonEmptyOption.mockReturnValue(undefined); + + await PaymentHelpers.pollStatus( + 'pk_test', + 'customUri', + 'poll_123', + 1000, + 5, + 'https://example.com/return', + undefined, + undefined + ); + + expect(mockFetchApiWithLogging).toHaveBeenCalled(); + }); + + it('returns a promise', () => { + mockFetchApiWithLogging.mockResolvedValue({ status: 'completed' }); + mockGetDictFromJson.mockReturnValue({ status: 'completed' }); + mockGetString.mockReturnValue('completed'); + mockGetNonEmptyOption.mockReturnValue(undefined); + + const result = PaymentHelpers.pollStatus( + 'pk_test', + 'customUri', + 'poll_123', + 1000, + 5, + 'https://example.com/return', + undefined, + undefined + ); + + expect(result).toBeInstanceOf(Promise); + }); + }); + + describe('callAuthLink', () => { + it('returns null on successful auth link call', async () => { + mockFetchApiWithLogging.mockResolvedValue({ link_token: 'link_test_123' }); + mockGetDictFromJson.mockReturnValue({ link_token: 'link_test_123' }); + mockGetString.mockReturnValue('link_test_123'); + mockGetNonEmptyOption.mockReturnValue(undefined); + + await PaymentHelpers.callAuthLink( + 'pk_test', + 'secret_test', + 'ach', + ['plaid'], + 'iframe_123', + undefined, + undefined + ); + + expect(mockFetchApiWithLogging).toHaveBeenCalled(); + }); + + it('returns null on auth link failure', async () => { + mockFetchApiWithLogging.mockResolvedValue(null); + mockGetNonEmptyOption.mockReturnValue(undefined); + + const result = await PaymentHelpers.callAuthLink( + 'pk_test', + undefined, + 'ach', + ['plaid'], + 'iframe_123', + undefined, + undefined + ); + + expect(result).toBeNull(); + }); + + it('handles with undefined clientSecret', async () => { + mockFetchApiWithLogging.mockResolvedValue({ link_token: 'link_test_123' }); + mockGetDictFromJson.mockReturnValue({ link_token: 'link_test_123' }); + mockGetString.mockReturnValue('link_test_123'); + mockGetNonEmptyOption.mockReturnValue(undefined); + + await PaymentHelpers.callAuthLink( + 'pk_test', + undefined, + 'ach', + ['plaid'], + 'iframe_123', + undefined, + undefined + ); + + expect(mockFetchApiWithLogging).toHaveBeenCalled(); + }); + }); + + describe('callAuthExchange', () => { + it('handles successful auth exchange', async () => { + mockFetchApiWithLogging.mockResolvedValue({ success: true }); + mockGetNonEmptyOption.mockReturnValue(undefined); + + const mockSetOptionValue = jest.fn(); + await PaymentHelpers.callAuthExchange( + 'public_token_123', + 'secret_test', + 'ach', + 'pk_test', + mockSetOptionValue, + undefined, + undefined + ); + + expect(mockFetchApiWithLogging).toHaveBeenCalled(); + }); + + it('returns null on auth exchange failure', async () => { + mockFetchApiWithLogging.mockResolvedValue(null); + mockGetNonEmptyOption.mockReturnValue(undefined); + + const result = await PaymentHelpers.callAuthExchange( + 'public_token_123', + 'secret_test', + 'ach', + 'pk_test', + jest.fn(), + undefined, + undefined + ); + + expect(result).toBeNull(); + }); + + it('handles with sdkAuthorization', async () => { + mockFetchApiWithLogging.mockResolvedValue({ success: true }); + mockGetNonEmptyOption.mockReturnValue('auth_token'); + + const mockSetOptionValue = jest.fn(); + await PaymentHelpers.callAuthExchange( + 'public_token_123', + 'secret_test', + 'ach', + 'pk_test', + mockSetOptionValue, + undefined, + 'auth_token' + ); + + expect(mockFetchApiWithLogging).toHaveBeenCalled(); + }); + }); + + describe('paymentIntentForPaymentSession', () => { + it('is callable with valid parameters', () => { + mockFetchApi.mockResolvedValue({ + ok: true, + status: 200, + json: () => Promise.resolve({ status: 'succeeded' }), + }); + mockGetDictFromJson.mockReturnValue({ + confirmParams: { + return_url: 'https://example.com/return', + redirect: 'if_required', + }, + }); + mockGetDictFromDict.mockReturnValue({ + return_url: 'https://example.com/return', + redirect: 'if_required', + }); + mockGetString.mockImplementation((obj: any, key: string, def: string) => { + if (key === 'return_url') return 'https://example.com/return'; + if (key === 'redirect') return 'if_required'; + return def; + }); + mockGetPaymentId.mockReturnValue('pay_123'); + mockGetJsonFromArrayOfJson.mockImplementation((arr: any) => Object.fromEntries(arr)); + + const payload = JSON.stringify({ + confirmParams: { + return_url: 'https://example.com/return', + redirect: 'if_required', + }, + }); + + PaymentHelpers.paymentIntentForPaymentSession( + [['payment_method_type', 'card']], + 'Card', + payload, + 'pk_test', + 'pay_123_secret_123', + undefined, + 'customUri', + undefined, + true, + 'NONE' + ); + + expect(mockFetchApi).toHaveBeenCalled(); + }); + + it('returns a promise for payment session', () => { + mockFetchApi.mockResolvedValue({ + ok: true, + status: 200, + json: () => Promise.resolve({ status: 'succeeded' }), + }); + mockGetDictFromJson.mockReturnValue({ + confirmParams: { + return_url: 'https://example.com/return', + redirect: 'if_required', + }, + }); + mockGetDictFromDict.mockReturnValue({ + return_url: 'https://example.com/return', + redirect: 'if_required', + }); + mockGetString.mockImplementation((obj: any, key: string, def: string) => { + if (key === 'return_url') return 'https://example.com/return'; + if (key === 'redirect') return 'if_required'; + return def; + }); + mockGetPaymentId.mockReturnValue('pay_123'); + mockGetJsonFromArrayOfJson.mockImplementation((arr: any) => Object.fromEntries(arr)); + + const payload = JSON.stringify({ + confirmParams: { + return_url: 'https://example.com/return', + redirect: 'if_required', + }, + }); + + const result = PaymentHelpers.paymentIntentForPaymentSession( + [], + 'Card', + payload, + 'pk_test', + 'pay_123_secret_123', + undefined, + 'customUri', + undefined, + true, + 'NONE' + ); + + expect(result).toBeInstanceOf(Promise); + }); + }); + + describe('hooks', () => { + it('usePaymentSync hook exists and is a function', () => { + expect(typeof PaymentHelpers.usePaymentSync).toBe('function'); + }); + + it('useCompleteAuthorizeHandler hook exists and is a function', () => { + expect(typeof PaymentHelpers.useCompleteAuthorizeHandler).toBe('function'); + }); + + it('useCompleteAuthorize hook exists and is a function', () => { + expect(typeof PaymentHelpers.useCompleteAuthorize).toBe('function'); + }); + + it('useRedsysCompleteAuthorize hook exists and is a function', () => { + expect(typeof PaymentHelpers.useRedsysCompleteAuthorize).toBe('function'); + }); + + it('usePaymentIntent hook exists and is a function', () => { + expect(typeof PaymentHelpers.usePaymentIntent).toBe('function'); + }); + + it('usePostSessionTokens hook exists and is a function', () => { + expect(typeof PaymentHelpers.usePostSessionTokens).toBe('function'); + }); + }); + + describe('intentCall - error scenarios', () => { + it('handles non-ok response with error data', async () => { + const mockErrorResponse = { error: { type: 'card_declined', message: 'Card was declined' } }; + mockFetchApi.mockResolvedValue({ + ok: false, + status: 400, + json: () => Promise.resolve(mockErrorResponse), + }); + mockGetDictFromJson.mockReturnValue(mockErrorResponse); + mockGetNonEmptyOption.mockReturnValue(undefined); + mockGetPaymentId.mockReturnValue('pay_123'); + + const confirmParam = { + return_url: 'https://example.com/return', + publishableKey: 'pk_test', + redirect: 'if_required', + }; + + const result = PaymentHelpers.intentCall( + mockFetchApi, + 'https://api.test.com/payments/pay_123/confirm', + [['Content-Type', 'application/json']], + JSON.stringify({ payment_method_type: 'card' }), + confirmParam, + 'pay_123_secret_abc', + undefined, + false, + 'Card', + 'iframe_123', + 'POST', + jest.fn(), + undefined, + false, + false, + undefined, + undefined + ); + + expect(result).toBeInstanceOf(Promise); + }); + + it('returns a promise for confirm endpoint', () => { + mockFetchApi.mockResolvedValue({ + ok: true, + status: 200, + json: () => Promise.resolve({ status: 'succeeded' }), + }); + + const confirmParam = { + return_url: 'https://example.com/return', + publishableKey: 'pk_test', + redirect: 'if_required', + }; + + const result = PaymentHelpers.intentCall( + mockFetchApi, + 'https://api.test.com/payments/pay_123/confirm', + [], + JSON.stringify({}), + confirmParam, + 'pay_123_secret_abc', + undefined, + false, + 'Card', + 'iframe_123', + 'POST', + jest.fn(), + undefined, + false, + false, + undefined, + undefined + ); + + expect(result).toBeInstanceOf(Promise); + }); + + it('returns a promise for complete_authorize endpoint', () => { + mockFetchApi.mockResolvedValue({ + ok: true, + status: 200, + json: () => Promise.resolve({ status: 'succeeded' }), + }); + + const confirmParam = { + return_url: 'https://example.com/return', + publishableKey: 'pk_test', + redirect: 'if_required', + }; + + const result = PaymentHelpers.intentCall( + mockFetchApi, + 'https://api.test.com/payments/pay_123/complete_authorize', + [], + JSON.stringify({}), + confirmParam, + 'pay_123_secret_abc', + undefined, + false, + 'Card', + 'iframe_123', + 'POST', + jest.fn(), + undefined, + false, + false, + undefined, + undefined + ); + + expect(result).toBeInstanceOf(Promise); + }); + + it('returns a promise for post_session_tokens endpoint', () => { + mockFetchApi.mockResolvedValue({ + ok: true, + status: 200, + json: () => Promise.resolve({ status: 'succeeded' }), + }); + + const confirmParam = { + return_url: 'https://example.com/return', + publishableKey: 'pk_test', + redirect: 'if_required', + }; + + const result = PaymentHelpers.intentCall( + mockFetchApi, + 'https://api.test.com/payments/pay_123/post_session_tokens', + [], + JSON.stringify({}), + confirmParam, + 'pay_123_secret_abc', + undefined, + false, + 'Card', + 'iframe_123', + 'POST', + jest.fn(), + undefined, + false, + false, + undefined, + undefined + ); + + expect(result).toBeInstanceOf(Promise); + }); + + it('returns a promise for retrieve endpoint', () => { + mockFetchApi.mockResolvedValue({ + ok: true, + status: 200, + json: () => Promise.resolve({ status: 'succeeded' }), + }); + + const confirmParam = { + return_url: 'https://example.com/return', + publishableKey: 'pk_test', + redirect: 'if_required', + }; + + const result = PaymentHelpers.intentCall( + mockFetchApi, + 'https://api.test.com/payments/pay_123', + [], + JSON.stringify({}), + confirmParam, + 'pay_123_secret_abc', + undefined, + false, + 'Card', + 'iframe_123', + 'GET', + jest.fn(), + undefined, + false, + false, + undefined, + undefined + ); + + expect(result).toBeInstanceOf(Promise); + }); + + it('handles Applepay payment type', () => { + mockFetchApi.mockResolvedValue({ + ok: true, + status: 200, + json: () => Promise.resolve({ status: 'succeeded' }), + }); + + const confirmParam = { + return_url: 'https://example.com/return', + publishableKey: 'pk_test', + redirect: 'if_required', + }; + + const result = PaymentHelpers.intentCall( + mockFetchApi, + 'https://api.test.com/payments/pay_123/confirm', + [], + JSON.stringify({}), + confirmParam, + 'pay_123_secret_abc', + undefined, + false, + 'Applepay', + 'iframe_123', + 'POST', + jest.fn(), + undefined, + true, + false, + undefined, + undefined + ); + + expect(result).toBeInstanceOf(Promise); + }); + + it('handles Gpay payment type', () => { + mockFetchApi.mockResolvedValue({ + ok: true, + status: 200, + json: () => Promise.resolve({ status: 'succeeded' }), + }); + + const confirmParam = { + return_url: 'https://example.com/return', + publishableKey: 'pk_test', + redirect: 'if_required', + }; + + const result = PaymentHelpers.intentCall( + mockFetchApi, + 'https://api.test.com/payments/pay_123/confirm', + [], + JSON.stringify({}), + confirmParam, + 'pay_123_secret_abc', + undefined, + false, + 'Gpay', + 'iframe_123', + 'POST', + jest.fn(), + undefined, + true, + false, + undefined, + undefined + ); + + expect(result).toBeInstanceOf(Promise); + }); + + it('handles Paypal payment type', () => { + mockFetchApi.mockResolvedValue({ + ok: true, + status: 200, + json: () => Promise.resolve({ status: 'succeeded' }), + }); + + const confirmParam = { + return_url: 'https://example.com/return', + publishableKey: 'pk_test', + redirect: 'if_required', + }; + + const result = PaymentHelpers.intentCall( + mockFetchApi, + 'https://api.test.com/payments/pay_123/confirm', + [], + JSON.stringify({}), + confirmParam, + 'pay_123_secret_abc', + undefined, + false, + 'Paypal', + 'iframe_123', + 'POST', + jest.fn(), + undefined, + true, + false, + undefined, + undefined + ); + + expect(result).toBeInstanceOf(Promise); + }); + + it('handles BankTransfer payment type', () => { + mockFetchApi.mockResolvedValue({ + ok: true, + status: 200, + json: () => Promise.resolve({ status: 'succeeded' }), + }); + + const confirmParam = { + return_url: 'https://example.com/return', + publishableKey: 'pk_test', + redirect: 'if_required', + }; + + const result = PaymentHelpers.intentCall( + mockFetchApi, + 'https://api.test.com/payments/pay_123/confirm', + [], + JSON.stringify({}), + confirmParam, + 'pay_123_secret_abc', + undefined, + false, + 'BankTransfer', + 'iframe_123', + 'POST', + jest.fn(), + undefined, + false, + false, + undefined, + undefined + ); + + expect(result).toBeInstanceOf(Promise); + }); + + it('handles Samsungpay payment type', () => { + mockFetchApi.mockResolvedValue({ + ok: true, + status: 200, + json: () => Promise.resolve({ status: 'succeeded' }), + }); + + const confirmParam = { + return_url: 'https://example.com/return', + publishableKey: 'pk_test', + redirect: 'if_required', + }; + + const result = PaymentHelpers.intentCall( + mockFetchApi, + 'https://api.test.com/payments/pay_123/confirm', + [], + JSON.stringify({}), + confirmParam, + 'pay_123_secret_abc', + undefined, + false, + 'Samsungpay', + 'iframe_123', + 'POST', + jest.fn(), + undefined, + false, + false, + undefined, + undefined + ); + + expect(result).toBeInstanceOf(Promise); + }); + + it('handles Paze payment type', () => { + mockFetchApi.mockResolvedValue({ + ok: true, + status: 200, + json: () => Promise.resolve({ status: 'succeeded' }), + }); + + const confirmParam = { + return_url: 'https://example.com/return', + publishableKey: 'pk_test', + redirect: 'if_required', + }; + + const result = PaymentHelpers.intentCall( + mockFetchApi, + 'https://api.test.com/payments/pay_123/confirm', + [], + JSON.stringify({}), + confirmParam, + 'pay_123_secret_abc', + undefined, + false, + 'Paze', + 'iframe_123', + 'POST', + jest.fn(), + undefined, + false, + false, + undefined, + undefined + ); + + expect(result).toBeInstanceOf(Promise); + }); + + it('handles Other payment type', () => { + mockFetchApi.mockResolvedValue({ + ok: true, + status: 200, + json: () => Promise.resolve({ status: 'succeeded' }), + }); + + const confirmParam = { + return_url: 'https://example.com/return', + publishableKey: 'pk_test', + redirect: 'if_required', + }; + + const result = PaymentHelpers.intentCall( + mockFetchApi, + 'https://api.test.com/payments/pay_123/confirm', + [], + JSON.stringify({}), + confirmParam, + 'pay_123_secret_abc', + undefined, + false, + 'Other', + 'iframe_123', + 'POST', + jest.fn(), + undefined, + false, + false, + undefined, + undefined + ); + + expect(result).toBeInstanceOf(Promise); + }); + + it('handles payment session flag', () => { + mockFetchApi.mockResolvedValue({ + ok: true, + status: 200, + json: () => Promise.resolve({ status: 'succeeded' }), + }); + + const confirmParam = { + return_url: 'https://example.com/return', + publishableKey: 'pk_test', + redirect: 'if_required', + }; + + const result = PaymentHelpers.intentCall( + mockFetchApi, + 'https://api.test.com/payments/pay_123/confirm', + [], + JSON.stringify({}), + confirmParam, + 'pay_123_secret_abc', + undefined, + false, + 'Card', + 'iframe_123', + 'POST', + jest.fn(), + undefined, + false, + true, + undefined, + undefined + ); + + expect(result).toBeInstanceOf(Promise); + }); + + it('handles redirect always flag', () => { + mockFetchApi.mockResolvedValue({ + ok: true, + status: 200, + json: () => Promise.resolve({ status: 'succeeded' }), + }); + + const confirmParam = { + return_url: 'https://example.com/return', + publishableKey: 'pk_test', + redirect: 'always', + }; + + const result = PaymentHelpers.intentCall( + mockFetchApi, + 'https://api.test.com/payments/pay_123/confirm', + [], + JSON.stringify({}), + confirmParam, + 'pay_123_secret_abc', + undefined, + false, + 'Card', + 'iframe_123', + 'POST', + jest.fn(), + undefined, + false, + true, + undefined, + undefined + ); + + expect(result).toBeInstanceOf(Promise); + }); + + it('handles mode CardCVCElement', () => { + mockFetchApi.mockResolvedValue({ + ok: true, + status: 200, + json: () => Promise.resolve({ status: 'succeeded' }), + }); + + const confirmParam = { + return_url: 'https://example.com/return', + publishableKey: 'pk_test', + redirect: 'if_required', + }; + + const result = PaymentHelpers.intentCall( + mockFetchApi, + 'https://api.test.com/payments/pay_123/confirm', + [], + JSON.stringify({}), + confirmParam, + 'pay_123_secret_abc', + undefined, + false, + 'Card', + 'iframe_123', + 'POST', + jest.fn(), + undefined, + false, + false, + undefined, + undefined, + undefined, + undefined, + undefined, + 'CardCVCElement' + ); + + expect(result).toBeInstanceOf(Promise); + }); + + it('handles with sdkAuthorization', () => { + mockFetchApi.mockResolvedValue({ + ok: true, + status: 200, + json: () => Promise.resolve({ status: 'succeeded' }), + }); + mockGetNonEmptyOption.mockReturnValue('auth_token'); + + const confirmParam = { + return_url: 'https://example.com/return', + publishableKey: 'pk_test', + redirect: 'if_required', + }; + + const result = PaymentHelpers.intentCall( + mockFetchApi, + 'https://api.test.com/payments/pay_123/confirm', + [], + JSON.stringify({}), + confirmParam, + 'pay_123_secret_abc', + undefined, + false, + 'Card', + 'iframe_123', + 'POST', + jest.fn(), + undefined, + false, + false, + undefined, + undefined, + undefined, + undefined, + 'auth_token' + ); + + expect(result).toBeInstanceOf(Promise); + }); + }); + + describe('getConstructedPaymentMethodName', () => { + it('returns card for card payment method', () => { + expect(PaymentHelpers.getConstructedPaymentMethodName('card', 'credit')).toBe('card'); + }); + + it('appends _debit for bank_debit payment method', () => { + expect(PaymentHelpers.getConstructedPaymentMethodName('bank_debit', 'ach')).toBe('ach_debit'); + }); + + it('returns payment method type for bank_transfer not in list', () => { + expect(PaymentHelpers.getConstructedPaymentMethodName('bank_transfer', 'sepa')).toBe('sepa_transfer'); + }); + + it('returns payment method type as-is for bank_transfer in list', () => { + expect(PaymentHelpers.getConstructedPaymentMethodName('bank_transfer', 'pix')).toBe('pix'); + }); + + it('returns payment method type for other payment methods', () => { + expect(PaymentHelpers.getConstructedPaymentMethodName('wallet', 'apple_pay')).toBe('apple_pay'); + }); + + it('handles bank_debit with sepa', () => { + expect(PaymentHelpers.getConstructedPaymentMethodName('bank_debit', 'sepa')).toBe('sepa_debit'); + }); + + it('handles bank_debit with bacs', () => { + expect(PaymentHelpers.getConstructedPaymentMethodName('bank_debit', 'bacs')).toBe('bacs_debit'); + }); + }); + + describe('getPaymentType - additional tests', () => { + it('returns Card for empty string', () => { + expect(PaymentHelpers.getPaymentType('')).toBe('Card'); + }); + + it('returns Other for unknown types', () => { + expect(PaymentHelpers.getPaymentType('unknown')).toBe('Other'); + expect(PaymentHelpers.getPaymentType('bank_transfer')).toBe('Other'); + }); + }); + + describe('maskStr - additional tests', () => { + it('handles single character', () => { + expect(PaymentHelpers.maskStr('a')).toBe('x'); + }); + + it('handles unicode characters', () => { + const result = PaymentHelpers.maskStr('hello世界'); + expect(result).toMatch(/^x+$/); + }); + }); + + describe('maskPayload - additional tests', () => { + it('handles nested arrays', () => { + const result = PaymentHelpers.maskPayload([['a', 'b'], ['c', 'd']]); + expect(Array.isArray(result)).toBe(true); + }); + + it('handles number values', () => { + const result = PaymentHelpers.maskPayload(12345); + expect(result).toBe('xxxxx'); + }); + + it('handles object with nested values', () => { + const result = PaymentHelpers.maskPayload({ key: 'value', nested: { inner: 'secret' } }); + expect(typeof result).toBe('object'); + }); + + it('handles empty object', () => { + const result = PaymentHelpers.maskPayload({}); + expect(typeof result).toBe('object'); + }); + + it('handles empty array', () => { + const result = PaymentHelpers.maskPayload([]); + expect(Array.isArray(result)).toBe(true); + }); + }); + + describe('retrievePaymentIntent - additional tests', () => { + it('handles successful response with force sync', async () => { + const mockData = { id: 'pay_123', status: 'succeeded' }; + mockFetchApiWithLogging.mockResolvedValue(mockData); + mockGetNonEmptyOption.mockReturnValue(undefined); + + await PaymentHelpers.retrievePaymentIntent( + 'secret_test', + {}, + 'pk_test', + undefined, + 'customUri', + true, + undefined + ); + + expect(mockFetchApiWithLogging).toHaveBeenCalled(); + }); + + it('handles with undefined headers', async () => { + const mockData = { id: 'pay_123' }; + mockFetchApiWithLogging.mockResolvedValue(mockData); + mockGetNonEmptyOption.mockReturnValue(undefined); + + await PaymentHelpers.retrievePaymentIntent( + 'secret_test', + undefined, + 'pk_test', + undefined, + 'customUri', + false, + undefined + ); + + expect(mockFetchApiWithLogging).toHaveBeenCalled(); + }); + }); + + describe('threeDsAuth - additional tests', () => { + it('handles successful response', async () => { + const mockData = { three_ds_auth: 'success' }; + mockFetchApiWithLogging.mockResolvedValue(mockData); + mockGetNonEmptyOption.mockReturnValue('auth_token'); + + await PaymentHelpers.threeDsAuth( + 'secret_test', + undefined, + 'Y', + [['Content-Type', 'application/json']], + 'auth_token' + ); + + expect(mockFetchApiWithLogging).toHaveBeenCalled(); + }); + + it('handles without sdkAuthorization', async () => { + mockFetchApiWithLogging.mockResolvedValue({}); + mockGetNonEmptyOption.mockReturnValue(undefined); + + await PaymentHelpers.threeDsAuth( + 'secret_test', + undefined, + 'N', + [], + undefined + ); + + expect(mockFetchApiWithLogging).toHaveBeenCalled(); + }); + }); + + describe('fetchSessions - additional tests', () => { + it('handles with delayed session token', async () => { + const mockData = { session_tokens: {} }; + mockFetchApiWithLogging.mockResolvedValue(mockData); + mockGetNonEmptyOption.mockReturnValue(undefined); + + await PaymentHelpers.fetchSessions( + 'secret_test', + 'pk_test', + ['google_pay'], + true, + undefined, + 'customUri', + 'https://endpoint.com', + true, + 'merchant.example.com', + undefined + ); + + expect(mockFetchApiWithLogging).toHaveBeenCalled(); + }); + + it('handles with sdkAuthorization', async () => { + const mockData = { session_tokens: {} }; + mockFetchApiWithLogging.mockResolvedValue(mockData); + mockGetNonEmptyOption.mockReturnValue('auth_token'); + + await PaymentHelpers.fetchSessions( + 'secret_test', + 'pk_test', + ['apple_pay'], + false, + undefined, + 'customUri', + 'https://endpoint.com', + false, + 'merchant.example.com', + 'auth_token' + ); + + expect(mockFetchApiWithLogging).toHaveBeenCalled(); + }); + }); + + describe('intentCall - additional error scenarios', () => { + it('handles non-ok response with 400 status', async () => { + const mockErrorResponse = { error: { type: 'validation_error', message: 'Invalid request' } }; + mockFetchApi.mockResolvedValue({ + ok: false, + status: 400, + json: () => Promise.resolve(mockErrorResponse), + }); + mockGetDictFromJson.mockReturnValue(mockErrorResponse); + mockGetNonEmptyOption.mockReturnValue(undefined); + mockGetPaymentId.mockReturnValue('pay_123'); + + const confirmParam = { + return_url: 'https://example.com/return', + publishableKey: 'pk_test', + redirect: 'if_required', + }; + + const result = PaymentHelpers.intentCall( + mockFetchApi, + 'https://api.test.com/payments/pay_123/confirm', + [], + JSON.stringify({}), + confirmParam, + 'pay_123_secret_abc', + undefined, + true, + 'Card', + 'iframe_123', + 'POST', + jest.fn(), + undefined, + false, + false, + undefined, + undefined + ); + + expect(result).toBeInstanceOf(Promise); + }); + + it('handles response with requires_customer_action status', async () => { + mockFetchApi.mockResolvedValue({ + ok: true, + status: 200, + json: () => Promise.resolve({ + status: 'requires_customer_action', + nextAction: { type_: 'redirect_to_url', redirectToUrl: 'https://redirect.com' } + }), + }); + mockGetDictFromJson.mockReturnValue({ + status: 'requires_customer_action', + nextAction: { type_: 'redirect_to_url', redirectToUrl: 'https://redirect.com' } + }); + mockGetNonEmptyOption.mockReturnValue(undefined); + mockGetPaymentId.mockReturnValue('pay_123'); + + const confirmParam = { + return_url: 'https://example.com/return', + publishableKey: 'pk_test', + redirect: 'if_required', + }; + + const result = PaymentHelpers.intentCall( + mockFetchApi, + 'https://api.test.com/payments/pay_123/confirm', + [], + JSON.stringify({}), + confirmParam, + 'pay_123_secret_abc', + undefined, + false, + 'Card', + 'iframe_123', + 'POST', + jest.fn(), + undefined, + false, + false, + undefined, + undefined + ); + + expect(result).toBeInstanceOf(Promise); + }); + }); + + describe('calculateTax - additional tests', () => { + it('handles with session ID', async () => { + const mockData = { tax_amount: 100 }; + mockFetchApiWithLogging.mockResolvedValue(mockData); + mockGetNonEmptyOption.mockReturnValue(undefined); + + await PaymentHelpers.calculateTax( + 'pk_test', + 'secret_test', + 'card', + { country: 'US', postal_code: '12345' }, + undefined, + 'customUri', + 'session_123', + undefined + ); + + expect(mockFetchApiWithLogging).toHaveBeenCalled(); + }); + + it('handles with sdkAuthorization', async () => { + const mockData = { tax_amount: 100 }; + mockFetchApiWithLogging.mockResolvedValue(mockData); + mockGetNonEmptyOption.mockReturnValue('auth_token'); + + await PaymentHelpers.calculateTax( + 'pk_test', + 'secret_test', + 'card', + { country: 'US' }, + undefined, + 'customUri', + undefined, + 'auth_token' + ); + + expect(mockFetchApiWithLogging).toHaveBeenCalled(); + }); + }); + + describe('fetchCustomerPaymentMethodList - additional tests', () => { + it('handles with isPaymentSession true', async () => { + const mockData = { customer_payment_methods: [] }; + mockFetchApiWithLogging.mockResolvedValue(mockData); + mockGetNonEmptyOption.mockReturnValue('auth_token'); + + await PaymentHelpers.fetchCustomerPaymentMethodList( + 'secret_test', + 'pk_test', + undefined, + 'customUri', + 'https://endpoint.com', + true, + 'auth_token' + ); + + expect(mockFetchApiWithLogging).toHaveBeenCalled(); + }); + }); + + describe('fetchPaymentMethodList - additional tests', () => { + it('handles without sdkAuthorization', async () => { + const mockData = { payment_methods: [] }; + mockFetchApiWithLogging.mockResolvedValue(mockData); + mockGetNonEmptyOption.mockReturnValue(undefined); + + await PaymentHelpers.fetchPaymentMethodList( + undefined, + 'secret_test', + 'pk_test', + undefined, + 'customUri', + 'https://endpoint.com' + ); + + expect(mockFetchApiWithLogging).toHaveBeenCalled(); + }); + }); + + describe('pollRetrievePaymentIntent - additional tests', () => { + it('handles processing status', async () => { + mockFetchApiWithLogging.mockResolvedValue({ status: 'processing' }); + mockGetDictFromJson.mockReturnValue({ status: 'processing' }); + mockGetString.mockReturnValue('processing'); + mockGetNonEmptyOption.mockReturnValue(undefined); + + const result = PaymentHelpers.pollRetrievePaymentIntent( + 'secret_test', + undefined, + 'pk_test', + undefined, + 'customUri', + false, + undefined + ); + + expect(result).toBeInstanceOf(Promise); + }); + }); + + describe('pollStatus - additional tests', () => { + it('handles non-completed status with retries', async () => { + mockFetchApiWithLogging.mockResolvedValue({ status: 'pending' }); + mockGetDictFromJson.mockReturnValue({ status: 'pending' }); + mockGetString.mockReturnValue('pending'); + mockGetNonEmptyOption.mockReturnValue(undefined); + + const result = PaymentHelpers.pollStatus( + 'pk_test', + 'customUri', + 'poll_123', + 100, + 3, + 'https://example.com/return', + undefined, + undefined + ); + + expect(result).toBeInstanceOf(Promise); + }); + }); + + describe('fetchEnabledAuthnMethodsToken - additional tests', () => { + it('handles with isPaymentSession true', async () => { + const mockData = { token: 'auth_token' }; + mockFetchApiWithLogging.mockResolvedValue(mockData); + + await PaymentHelpers.fetchEnabledAuthnMethodsToken( + 'secret_test', + 'pk_test', + undefined, + 'customUri', + 'https://endpoint.com', + true, + 'profile_123', + 'auth_123' + ); + + expect(mockFetchApiWithLogging).toHaveBeenCalled(); + }); + }); + + describe('fetchEligibilityCheck - additional tests', () => { + it('handles with isPaymentSession true', async () => { + const mockData = { eligible: true }; + mockFetchApiWithLogging.mockResolvedValue(mockData); + + await PaymentHelpers.fetchEligibilityCheck( + 'secret_test', + 'pk_test', + undefined, + 'customUri', + 'https://endpoint.com', + true, + 'profile_123', + 'auth_123', + [['method', 'sms']] + ); + + expect(mockFetchApiWithLogging).toHaveBeenCalled(); + }); + }); + + describe('fetchAuthenticationSync - additional tests', () => { + it('handles with isPaymentSession true', async () => { + const mockData = { authenticated: true }; + mockFetchApiWithLogging.mockResolvedValue(mockData); + + await PaymentHelpers.fetchAuthenticationSync( + 'secret_test', + 'pk_test', + undefined, + 'customUri', + 'https://endpoint.com', + true, + 'profile_123', + 'auth_123', + 'merchant_123', + [['otp', '123456']] + ); + + expect(mockFetchApiWithLogging).toHaveBeenCalled(); + }); + + it('handles error response', async () => { + const mockError = { error: 'authentication_failed' }; + mockFetchApiWithLogging.mockResolvedValue(mockError); + + const result = await PaymentHelpers.fetchAuthenticationSync( + 'secret_test', + 'pk_test', + undefined, + 'customUri', + 'https://endpoint.com', + false, + 'profile_123', + 'auth_123', + 'merchant_123', + [] + ); + + expect(result).toBeDefined(); + }); + }); +}); diff --git a/src/__tests__/PaymentHelpersV2.test.ts b/src/__tests__/PaymentHelpersV2.test.ts new file mode 100644 index 000000000..4dc6490fd --- /dev/null +++ b/src/__tests__/PaymentHelpersV2.test.ts @@ -0,0 +1,592 @@ +import * as PaymentHelpersV2 from '../Utilities/PaymentHelpersV2.bs.js'; +import { renderHook } from '@testing-library/react'; +import { RecoilRoot } from 'recoil'; +import * as React from 'react'; +import * as RecoilAtoms from '../Utilities/RecoilAtoms.bs.js'; +import * as RecoilAtomsV2 from '../Utilities/RecoilAtomsV2.bs.js'; + +const mockFetchApi = jest.fn(); +const mockGetDictFromJson = jest.fn((obj: any) => (typeof obj === 'object' && obj !== null ? obj : {})); +const mockGetString = jest.fn((obj: any, key: string, def: string) => obj?.[key] ?? def); +const mockMessageParentWindow = jest.fn(); +const mockGetPaymentId = jest.fn((secret: string) => secret?.split('_secret_')[0] || ''); +const mockPostFailedSubmitResponse = jest.fn(); +const mockGetJsonFromArrayOfJson = jest.fn((arr: any) => Object.fromEntries(arr)); +const mockGetNonEmptyOption = jest.fn((val: any) => (val ? val : undefined)); +const mockOpenUrl = jest.fn(); +const mockReplaceRootHref = jest.fn(); +const mockPostSubmitResponse = jest.fn(); +const mockHandleOnCompleteDoThisMessage = jest.fn(); +const mockGetFailedSubmitResponse = jest.fn((type: string, msg: string) => ({ type, message: msg })); +const mockFormatException = jest.fn((e: any) => e?.message || String(e)); +const mockSafeParse = jest.fn((str: string) => { + try { + return JSON.parse(str); + } catch { + return null; + } +}); +const mockGetStringFromJson = jest.fn((val: any, def: string) => (typeof val === 'string' ? val : def)); + +jest.mock('../Utilities/Utils.bs.js', () => ({ + getDictFromJson: (obj: any) => mockGetDictFromJson(obj), + getString: (obj: any, key: string, def: string) => mockGetString(obj, key, def), + messageParentWindow: (a: any, b: any) => mockMessageParentWindow(a, b), + getPaymentId: (secret: string) => mockGetPaymentId(secret), + postFailedSubmitResponse: (type: string, msg: string) => mockPostFailedSubmitResponse(type, msg), + getJsonFromArrayOfJson: (arr: any) => mockGetJsonFromArrayOfJson(arr), + getNonEmptyOption: (val: any) => mockGetNonEmptyOption(val), + fetchApi: (url: string, body: any, headers: any, method: string) => mockFetchApi(url, body, headers, method), + openUrl: (url: string) => mockOpenUrl(url), + replaceRootHref: (url: string, flags: any) => mockReplaceRootHref(url, flags), + postSubmitResponse: (data: any, url: string) => mockPostSubmitResponse(data, url), + handleOnCompleteDoThisMessage: (a: any) => mockHandleOnCompleteDoThisMessage(a), + getFailedSubmitResponse: (type: string, msg: string) => mockGetFailedSubmitResponse(type, msg), + formatException: (e: any) => mockFormatException(e), + safeParse: (str: string) => mockSafeParse(str), + getStringFromJson: (val: any, def: string) => mockGetStringFromJson(val, def), +})); + +jest.mock('../Utilities/ApiEndpoint.bs.js', () => ({ + getApiEndPoint: jest.fn((key: string, isThirdParty: boolean) => 'https://api.test.com'), + addCustomPodHeader: jest.fn((headers: any, uri: any) => headers), +})); + +jest.mock('../Utilities/LoggerUtils.bs.js', () => ({ + logApi: jest.fn(), + handleLogging: jest.fn(), +})); + +jest.mock('../BrowserSpec.bs.js', () => ({ + broswerInfo: jest.fn(() => [['browser_info', { user_agent: 'test-agent' }]]), +})); + +jest.mock('../Types/PaymentConfirmTypesV2.bs.js', () => ({ + itemToPMMConfirmMapper: jest.fn((obj: any) => ({ + authenticationDetails: { status: 'succeeded' }, + nextAction: { type_: '' }, + ...obj, + })), +})); + +jest.mock('../Types/PaymentError.bs.js', () => ({ + itemToObjMapper: jest.fn((obj: any) => ({ + error: { type_: 'test_error', message: 'Test error message' }, + ...obj, + })), +})); + +jest.mock('../Utilities/PaymentHelpers.bs.js', () => ({ + closePaymentLoaderIfAny: jest.fn(), +})); + +const createWrapperWithAtoms = (atomValues: any) => { + return function Wrapper({ children }: { children: React.ReactNode }) { + return React.createElement( + RecoilRoot, + { + initializeState: ({ set }: any) => { + Object.entries(atomValues).forEach(([key, value]) => { + const atom = (RecoilAtoms as any)[key] || (RecoilAtomsV2 as any)[key]; + if (atom) { + set(atom, value); + } + }); + }, + }, + children + ); + }; +}; + +describe('PaymentHelpersV2', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('fetchPaymentManagementList', () => { + it('returns data on successful fetch', async () => { + const mockData = { payment_methods: [] }; + mockFetchApi.mockResolvedValue({ + ok: true, + json: () => Promise.resolve(mockData), + }); + + const result = await PaymentHelpersV2.fetchPaymentManagementList( + 'pm_session_123', + 'pm_secret_123', + 'pk_test', + 'profile_123', + 'https://api.test.com', + undefined, + undefined + ); + + expect(mockFetchApi).toHaveBeenCalled(); + expect(result).toEqual(mockData); + }); + + it('returns null on failed fetch', async () => { + mockFetchApi.mockResolvedValue({ + ok: false, + json: () => Promise.resolve({ error: 'not_found' }), + }); + + const result = await PaymentHelpersV2.fetchPaymentManagementList( + 'pm_session_123', + 'pm_secret_123', + 'pk_test', + 'profile_123', + 'https://api.test.com', + undefined, + undefined + ); + + expect(result).toBeNull(); + }); + + it('returns null on fetch exception', async () => { + mockFetchApi.mockRejectedValue(new Error('Network error')); + mockFormatException.mockReturnValue('Network error'); + + const result = await PaymentHelpersV2.fetchPaymentManagementList( + 'pm_session_123', + 'pm_secret_123', + 'pk_test', + 'profile_123', + 'https://api.test.com', + undefined, + undefined + ); + + expect(result).toBeNull(); + }); + }); + + describe('deletePaymentMethodV2', () => { + it('returns data on successful delete', async () => { + const mockData = { deleted: true }; + mockFetchApi.mockResolvedValue({ + ok: true, + json: () => Promise.resolve(mockData), + }); + + const result = await PaymentHelpersV2.deletePaymentMethodV2( + 'pm_secret_123', + 'pk_test', + 'profile_123', + 'pm_token_123', + 'pm_session_123', + undefined, + undefined + ); + + expect(mockFetchApi).toHaveBeenCalled(); + expect(result).toEqual(mockData); + }); + + it('returns null on failed delete', async () => { + mockFetchApi.mockResolvedValue({ + ok: false, + json: () => Promise.resolve({ error: 'not_found' }), + }); + + const result = await PaymentHelpersV2.deletePaymentMethodV2( + 'pm_secret_123', + 'pk_test', + 'profile_123', + 'pm_token_123', + 'pm_session_123', + undefined, + undefined + ); + + expect(result).toBeNull(); + }); + + it('returns null on fetch exception', async () => { + mockFetchApi.mockRejectedValue(new Error('Network error')); + mockFormatException.mockReturnValue('Network error'); + + const result = await PaymentHelpersV2.deletePaymentMethodV2( + 'pm_secret_123', + 'pk_test', + 'profile_123', + 'pm_token_123', + 'pm_session_123', + undefined, + undefined + ); + + expect(result).toBeNull(); + }); + }); + + describe('updatePaymentMethod', () => { + it('returns data on successful update', async () => { + const mockData = { updated: true }; + mockFetchApi.mockResolvedValue({ + ok: true, + json: () => Promise.resolve(mockData), + }); + + const result = await PaymentHelpersV2.updatePaymentMethod( + [['card_exp_month', '12'], ['card_exp_year', '2025']], + 'pm_secret_123', + 'pk_test', + 'profile_123', + 'pm_session_123', + undefined, + undefined + ); + + expect(mockFetchApi).toHaveBeenCalled(); + expect(result).toEqual(mockData); + }); + + it('returns null on failed update', async () => { + mockFetchApi.mockResolvedValue({ + ok: false, + json: () => Promise.resolve({ error: 'invalid_request' }), + }); + + const result = await PaymentHelpersV2.updatePaymentMethod( + [], + 'pm_secret_123', + 'pk_test', + 'profile_123', + 'pm_session_123', + undefined, + undefined + ); + + expect(result).toBeNull(); + }); + + it('returns null on fetch exception', async () => { + mockFetchApi.mockRejectedValue(new Error('Network error')); + mockFormatException.mockReturnValue('Network error'); + + const result = await PaymentHelpersV2.updatePaymentMethod( + [], + 'pm_secret_123', + 'pk_test', + 'profile_123', + 'pm_session_123', + undefined, + undefined + ); + + expect(result).toBeNull(); + }); + }); + + describe('intentCall', () => { + it('is callable and returns a promise', () => { + const mockResponse = { + ok: true, + json: () => + Promise.resolve({ + status: 'succeeded', + payment_method_type: 'card', + }), + }; + mockFetchApi.mockResolvedValue(mockResponse); + mockGetDictFromJson.mockReturnValue({ + status: 'succeeded', + payment_method_type: 'card', + }); + mockGetString.mockImplementation((obj: any, key: string) => obj?.[key]); + + const confirmParam = { + return_url: 'https://example.com/return', + publishableKey: 'pk_test', + redirect: 'if_required', + }; + + const result = PaymentHelpersV2.intentCall( + mockFetchApi, + 'https://api.test.com/payments/pay_123/confirm', + [['Content-Type', 'application/json']], + JSON.stringify({ client_secret: 'secret_123' }), + confirmParam, + 'pay_123_secret_123', + undefined, + false, + 'Card', + 'POST', + undefined, + false, + false, + undefined + ); + + expect(result).toBeInstanceOf(Promise); + expect(mockFetchApi).toHaveBeenCalled(); + }); + + it('handles non-ok response', () => { + const mockResponse = { + ok: false, + json: () => + Promise.resolve({ + error: { type: 'invalid_request', message: 'Invalid request' }, + }), + }; + mockFetchApi.mockResolvedValue(mockResponse); + mockGetDictFromJson.mockReturnValue({ + error: { type: 'invalid_request', message: 'Invalid request' }, + }); + + const confirmParam = { + return_url: 'https://example.com/return', + publishableKey: 'pk_test', + redirect: 'if_required', + }; + + const result = PaymentHelpersV2.intentCall( + mockFetchApi, + 'https://api.test.com/payments/pay_123/confirm', + [['Content-Type', 'application/json']], + JSON.stringify({ client_secret: 'secret_123' }), + confirmParam, + 'pay_123_secret_123', + undefined, + false, + 'Card', + 'POST', + undefined, + false, + false, + undefined + ); + + expect(result).toBeInstanceOf(Promise); + expect(mockFetchApi).toHaveBeenCalled(); + }); + + it('handles fetch exception', () => { + mockFetchApi.mockRejectedValue(new Error('Network error')); + mockFormatException.mockReturnValue('Network error'); + + const confirmParam = { + return_url: 'https://example.com/return', + publishableKey: 'pk_test', + redirect: 'if_required', + }; + + const result = PaymentHelpersV2.intentCall( + mockFetchApi, + 'https://api.test.com/payments/pay_123/confirm', + [['Content-Type', 'application/json']], + JSON.stringify({ client_secret: 'secret_123' }), + confirmParam, + 'pay_123_secret_123', + undefined, + false, + 'Card', + 'POST', + undefined, + false, + false, + undefined + ); + + expect(result).toBeInstanceOf(Promise); + }); + }); + + describe('useSaveCard', () => { + it('hook exists and is a function', () => { + expect(typeof PaymentHelpersV2.useSaveCard).toBe('function'); + }); + + it('returns a function when rendered with LoadedV2 paymentManagementList', () => { + const Wrapper = createWrapperWithAtoms({ + paymentManagementList: { TAG: 'LoadedV2', _0: { payment_methods: [] } }, + keys: { + pmClientSecret: 'pm_secret_123', + pmSessionId: 'pm_session_123', + publishableKey: 'pk_test', + profileId: 'profile_123', + sdkHandleOneClickConfirmPayment: false, + }, + customPodUri: '', + isCompleteCallbackUsed: false, + redirectionFlagsAtom: { shouldUseTopRedirection: false, shouldRemoveBeforeUnloadEvents: false }, + }); + + const { result } = renderHook(() => PaymentHelpersV2.useSaveCard(undefined, 'Card'), { + wrapper: Wrapper, + }); + + expect(typeof result.current).toBe('function'); + }); + + it('returns a function that does nothing when paymentManagementList is not LoadedV2', () => { + const Wrapper = createWrapperWithAtoms({ + paymentManagementList: 'LoadingV2', + keys: { + pmClientSecret: 'pm_secret_123', + pmSessionId: 'pm_session_123', + publishableKey: 'pk_test', + profileId: 'profile_123', + sdkHandleOneClickConfirmPayment: false, + }, + customPodUri: '', + isCompleteCallbackUsed: false, + redirectionFlagsAtom: { shouldUseTopRedirection: false, shouldRemoveBeforeUnloadEvents: false }, + }); + + const { result } = renderHook(() => PaymentHelpersV2.useSaveCard(undefined, 'Card'), { + wrapper: Wrapper, + }); + + expect(typeof result.current).toBe('function'); + + const confirmParam = { + return_url: 'https://example.com/return', + publishableKey: 'pk_test', + redirect: 'if_required', + }; + + if (result.current) { + result.current(false, [], confirmParam); + } + + expect(mockFetchApi).not.toHaveBeenCalled(); + }); + + it('posts failed response when pmClientSecret is undefined', () => { + const Wrapper = createWrapperWithAtoms({ + paymentManagementList: { TAG: 'LoadedV2', _0: { payment_methods: [] } }, + keys: { + pmClientSecret: undefined, + pmSessionId: 'pm_session_123', + publishableKey: 'pk_test', + profileId: 'profile_123', + sdkHandleOneClickConfirmPayment: false, + }, + customPodUri: '', + isCompleteCallbackUsed: false, + redirectionFlagsAtom: { shouldUseTopRedirection: false, shouldRemoveBeforeUnloadEvents: false }, + }); + + const { result } = renderHook(() => PaymentHelpersV2.useSaveCard(undefined, 'Card'), { + wrapper: Wrapper, + }); + + const confirmParam = { + return_url: 'https://example.com/return', + publishableKey: 'pk_test', + redirect: 'if_required', + }; + + if (result.current) { + result.current(false, [], confirmParam); + } + + expect(mockPostFailedSubmitResponse).toHaveBeenCalledWith( + 'confirm_payment_failed', + 'Payment failed. Try again!' + ); + }); + }); + + describe('useUpdateCard', () => { + it('hook exists and is a function', () => { + expect(typeof PaymentHelpersV2.useUpdateCard).toBe('function'); + }); + + it('returns a function when rendered with LoadedV2 paymentManagementList', () => { + const Wrapper = createWrapperWithAtoms({ + paymentManagementList: { TAG: 'LoadedV2', _0: { payment_methods: [] } }, + keys: { + pmClientSecret: 'pm_secret_123', + pmSessionId: 'pm_session_123', + publishableKey: 'pk_test', + profileId: 'profile_123', + sdkHandleOneClickConfirmPayment: false, + }, + customPodUri: '', + isCompleteCallbackUsed: false, + redirectionFlagsAtom: { shouldUseTopRedirection: false, shouldRemoveBeforeUnloadEvents: false }, + }); + + const { result } = renderHook(() => PaymentHelpersV2.useUpdateCard(undefined, 'Card'), { + wrapper: Wrapper, + }); + + expect(typeof result.current).toBe('function'); + }); + + it('returns a function that does nothing when paymentManagementList is not LoadedV2', () => { + const Wrapper = createWrapperWithAtoms({ + paymentManagementList: 'LoadingV2', + keys: { + pmClientSecret: 'pm_secret_123', + pmSessionId: 'pm_session_123', + publishableKey: 'pk_test', + profileId: 'profile_123', + sdkHandleOneClickConfirmPayment: false, + }, + customPodUri: '', + isCompleteCallbackUsed: false, + redirectionFlagsAtom: { shouldUseTopRedirection: false, shouldRemoveBeforeUnloadEvents: false }, + }); + + const { result } = renderHook(() => PaymentHelpersV2.useUpdateCard(undefined, 'Card'), { + wrapper: Wrapper, + }); + + expect(typeof result.current).toBe('function'); + + const confirmParam = { + return_url: 'https://example.com/return', + publishableKey: 'pk_test', + redirect: 'if_required', + }; + + if (result.current) { + result.current(false, [], confirmParam); + } + + expect(mockFetchApi).not.toHaveBeenCalled(); + }); + + it('posts failed response when pmClientSecret is undefined', () => { + const Wrapper = createWrapperWithAtoms({ + paymentManagementList: { TAG: 'LoadedV2', _0: { payment_methods: [] } }, + keys: { + pmClientSecret: undefined, + pmSessionId: 'pm_session_123', + publishableKey: 'pk_test', + profileId: 'profile_123', + sdkHandleOneClickConfirmPayment: false, + }, + customPodUri: '', + isCompleteCallbackUsed: false, + redirectionFlagsAtom: { shouldUseTopRedirection: false, shouldRemoveBeforeUnloadEvents: false }, + }); + + const { result } = renderHook(() => PaymentHelpersV2.useUpdateCard(undefined, 'Card'), { + wrapper: Wrapper, + }); + + const confirmParam = { + return_url: 'https://example.com/return', + publishableKey: 'pk_test', + redirect: 'if_required', + }; + + if (result.current) { + result.current(false, [], confirmParam); + } + + expect(mockPostFailedSubmitResponse).toHaveBeenCalledWith( + 'confirm_payment_failed', + 'Payment failed. Try again!' + ); + }); + }); +}); diff --git a/src/__tests__/PaymentManagementBody.test.ts b/src/__tests__/PaymentManagementBody.test.ts new file mode 100644 index 000000000..5555e9b59 --- /dev/null +++ b/src/__tests__/PaymentManagementBody.test.ts @@ -0,0 +1,233 @@ +import { + updateCardBody, + updateCVVBody, + saveCardBody, + vgsCardBody, + hyperswitchVaultBody, +} from '../Utilities/PaymentManagementBody.bs.js'; + +describe('PaymentManagementBody', () => { + describe('updateCardBody', () => { + it('should create update card body with payment method token', () => { + const result = updateCardBody('pm_token_123', 'My Card', 'John Doe'); + + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + + const tokenEntry = result.find((entry: any) => entry[0] === 'payment_method_token'); + expect(tokenEntry).toBeDefined(); + expect(tokenEntry[1]).toBe('pm_token_123'); + }); + + it('should include card holder name in payment method data', () => { + const result = updateCardBody('pm_token_123', 'My Card', 'John Doe'); + + const pmdEntry = result.find((entry: any) => entry[0] === 'payment_method_data'); + expect(pmdEntry).toBeDefined(); + }); + + it('should include nickname in card details', () => { + const result = updateCardBody('pm_token_123', 'My Card', 'John Doe'); + + const pmdEntry = result.find((entry: any) => entry[0] === 'payment_method_data'); + expect(pmdEntry).toBeDefined(); + }); + + it('should handle empty nickname', () => { + const result = updateCardBody('pm_token_123', '', 'John Doe'); + + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + }); + + it('should handle empty card holder name', () => { + const result = updateCardBody('pm_token_123', 'My Card', ''); + + expect(result).toBeDefined(); + const pmdEntry = result.find((entry: any) => entry[0] === 'payment_method_data'); + expect(pmdEntry).toBeDefined(); + }); + }); + + describe('updateCVVBody', () => { + it('should create CVV update body with payment method token', () => { + const result = updateCVVBody('pm_token_123', '123'); + + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + + const tokenEntry = result.find((entry: any) => entry[0] === 'payment_method_token'); + expect(tokenEntry).toBeDefined(); + expect(tokenEntry[1]).toBe('pm_token_123'); + }); + + it('should include CVV in card details', () => { + const result = updateCVVBody('pm_token_123', '123'); + + const pmdEntry = result.find((entry: any) => entry[0] === 'payment_method_data'); + expect(pmdEntry).toBeDefined(); + }); + + it('should handle empty CVV', () => { + const result = updateCVVBody('pm_token_123', ''); + + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + }); + + it('should handle 4-digit CVV', () => { + const result = updateCVVBody('pm_token_123', '1234'); + + expect(result).toBeDefined(); + const pmdEntry = result.find((entry: any) => entry[0] === 'payment_method_data'); + expect(pmdEntry).toBeDefined(); + }); + }); + + describe('saveCardBody', () => { + it('should create save card body with card details', () => { + const result = saveCardBody('4111111111111111', '12', '2025', 'John Doe', '123', [], undefined); + + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + + const pmtEntry = result.find((entry: any) => entry[0] === 'payment_method_type'); + expect(pmtEntry).toBeDefined(); + expect(pmtEntry[1]).toBe('card'); + }); + + it('should include payment method subtype', () => { + const result = saveCardBody('4111111111111111', '12', '2025', 'John Doe', '123', [], undefined); + + const pmsEntry = result.find((entry: any) => entry[0] === 'payment_method_subtype'); + expect(pmsEntry).toBeDefined(); + expect(pmsEntry[1]).toBe('card'); + }); + + it('should include card number without spaces', () => { + const result = saveCardBody('4111 1111 1111 1111', '12', '2025', 'John Doe', '123', [], undefined); + + const pmdEntry = result.find((entry: any) => entry[0] === 'payment_method_data'); + expect(pmdEntry).toBeDefined(); + }); + + it('should include expiry month and year', () => { + const result = saveCardBody('4111111111111111', '12', '2025', 'John Doe', '123', [], undefined); + + const pmdEntry = result.find((entry: any) => entry[0] === 'payment_method_data'); + expect(pmdEntry).toBeDefined(); + }); + + it('should handle missing optional card holder name', () => { + const result = saveCardBody('4111111111111111', '12', '2025', undefined, '123', [], undefined); + + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + }); + + it('should include nickname when provided', () => { + const result = saveCardBody('4111111111111111', '12', '2025', 'John Doe', '123', [], 'My Visa'); + + expect(result).toBeDefined(); + const pmdEntry = result.find((entry: any) => entry[0] === 'payment_method_data'); + expect(pmdEntry).toBeDefined(); + }); + + it('should handle empty nickname', () => { + const result = saveCardBody('4111111111111111', '12', '2025', 'John Doe', '123', [], ''); + + expect(result).toBeDefined(); + }); + + it('should include card brand when provided', () => { + const cardBrand = [['card_issuer', 'visa']]; + const result = saveCardBody('4111111111111111', '12', '2025', 'John Doe', '123', cardBrand, undefined); + + expect(result).toBeDefined(); + }); + }); + + describe('vgsCardBody', () => { + it('should create VGS card body with card details', () => { + const result = vgsCardBody('4111111111111111', '12', '2025', '123'); + + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + + const pmtEntry = result.find((entry: any) => entry[0] === 'payment_method_type'); + expect(pmtEntry).toBeDefined(); + expect(pmtEntry[1]).toBe('card'); + }); + + it('should set payment method subtype to debit', () => { + const result = vgsCardBody('4111111111111111', '12', '2025', '123'); + + const pmsEntry = result.find((entry: any) => entry[0] === 'payment_method_subtype'); + expect(pmsEntry).toBeDefined(); + expect(pmsEntry[1]).toBe('debit'); + }); + + it('should include vault_data_card in payment method data', () => { + const result = vgsCardBody('4111111111111111', '12', '2025', '123'); + + const pmdEntry = result.find((entry: any) => entry[0] === 'payment_method_data'); + expect(pmdEntry).toBeDefined(); + }); + + it('should handle empty card number', () => { + const result = vgsCardBody('', '12', '2025', '123'); + + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + }); + + it('should handle empty CVV', () => { + const result = vgsCardBody('4111111111111111', '12', '2025', ''); + + expect(result).toBeDefined(); + }); + }); + + describe('hyperswitchVaultBody', () => { + it('should create Hyperswitch vault body with token', () => { + const result = hyperswitchVaultBody('vault_token_123'); + + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + + const pmtEntry = result.find((entry: any) => entry[0] === 'payment_method_type'); + expect(pmtEntry).toBeDefined(); + expect(pmtEntry[1]).toBe('card'); + }); + + it('should set payment method subtype to debit', () => { + const result = hyperswitchVaultBody('vault_token_123'); + + const pmsEntry = result.find((entry: any) => entry[0] === 'payment_method_subtype'); + expect(pmsEntry).toBeDefined(); + expect(pmsEntry[1]).toBe('debit'); + }); + + it('should include payment token', () => { + const result = hyperswitchVaultBody('vault_token_123'); + + const tokenEntry = result.find((entry: any) => entry[0] === 'payment_token'); + expect(tokenEntry).toBeDefined(); + expect(tokenEntry[1]).toBe('vault_token_123'); + }); + + it('should include card_token in payment method data', () => { + const result = hyperswitchVaultBody('vault_token_123'); + + const pmdEntry = result.find((entry: any) => entry[0] === 'payment_method_data'); + expect(pmdEntry).toBeDefined(); + }); + + it('should handle empty token', () => { + const result = hyperswitchVaultBody(''); + + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + }); + }); +}); diff --git a/src/__tests__/PaymentMethodCollectTypes.test.ts b/src/__tests__/PaymentMethodCollectTypes.test.ts new file mode 100644 index 000000000..f35c5e29c --- /dev/null +++ b/src/__tests__/PaymentMethodCollectTypes.test.ts @@ -0,0 +1,725 @@ +import * as PMCollectTypes from "../Types/PaymentMethodCollectTypes.bs.js"; + +describe("PaymentMethodCollectTypes", () => { + describe("decodeAmount", () => { + it("returns amount from dict when present", () => { + const dict = { amount: "1000" }; + const result = PMCollectTypes.decodeAmount(dict, "0"); + expect(result).toBe("1000"); + }); + + it("returns default when amount is not present", () => { + const dict = {}; + const result = PMCollectTypes.decodeAmount(dict, "0"); + expect(result).toBe("0"); + }); + + it("returns default when amount is undefined", () => { + const dict = { amount: undefined }; + const result = PMCollectTypes.decodeAmount(dict, "500"); + expect(result).toBe("500"); + }); + }); + + describe("decodeFlow", () => { + it("decodes 'PayoutLinkInitiate' flow", () => { + const dict = { flow: "PayoutLinkInitiate" }; + const result = PMCollectTypes.decodeFlow(dict, "default"); + expect(result).toBe("PayoutLinkInitiate"); + }); + + it("decodes 'PayoutMethodCollect' flow", () => { + const dict = { flow: "PayoutMethodCollect" }; + const result = PMCollectTypes.decodeFlow(dict, "default"); + expect(result).toBe("PayoutMethodCollect"); + }); + + it("returns default for unknown flow", () => { + const dict = { flow: "UnknownFlow" }; + const result = PMCollectTypes.decodeFlow(dict, "default"); + expect(result).toBe("default"); + }); + + it("returns default when flow is not present", () => { + const dict = {}; + const result = PMCollectTypes.decodeFlow(dict, "default"); + expect(result).toBe("default"); + }); + }); + + describe("decodeFormLayout", () => { + it("decodes 'journey' form layout", () => { + const dict = { formLayout: "journey" }; + const result = PMCollectTypes.decodeFormLayout(dict, "default"); + expect(result).toBe("Journey"); + }); + + it("decodes 'tabs' form layout", () => { + const dict = { formLayout: "tabs" }; + const result = PMCollectTypes.decodeFormLayout(dict, "default"); + expect(result).toBe("Tabs"); + }); + + it("returns default for unknown layout", () => { + const dict = { formLayout: "unknown" }; + const result = PMCollectTypes.decodeFormLayout(dict, "default"); + expect(result).toBe("default"); + }); + + it("returns default when formLayout is not present", () => { + const dict = {}; + const result = PMCollectTypes.decodeFormLayout(dict, "default"); + expect(result).toBe("default"); + }); + }); + + describe("decodeCard", () => { + it("decodes 'credit' card type", () => { + const result = PMCollectTypes.decodeCard("credit"); + expect(result).toBe("Credit"); + }); + + it("decodes 'debit' card type", () => { + const result = PMCollectTypes.decodeCard("debit"); + expect(result).toBe("Debit"); + }); + + it("returns undefined for unknown card type", () => { + const result = PMCollectTypes.decodeCard("unknown"); + expect(result).toBeUndefined(); + }); + + it("returns undefined for empty string", () => { + const result = PMCollectTypes.decodeCard(""); + expect(result).toBeUndefined(); + }); + }); + + describe("decodeTransfer", () => { + it("decodes 'ach' transfer type", () => { + const result = PMCollectTypes.decodeTransfer("ach"); + expect(result).toBe("ACH"); + }); + + it("decodes 'bacs' transfer type", () => { + const result = PMCollectTypes.decodeTransfer("bacs"); + expect(result).toBe("Bacs"); + }); + + it("decodes 'pix' transfer type", () => { + const result = PMCollectTypes.decodeTransfer("pix"); + expect(result).toBe("Pix"); + }); + + it("decodes 'sepa_bank_transfer' transfer type", () => { + const result = PMCollectTypes.decodeTransfer("sepa_bank_transfer"); + expect(result).toBe("Sepa"); + }); + + it("returns undefined for unknown transfer type", () => { + const result = PMCollectTypes.decodeTransfer("unknown"); + expect(result).toBeUndefined(); + }); + }); + + describe("decodeWallet", () => { + it("decodes 'paypal' wallet type", () => { + const result = PMCollectTypes.decodeWallet("paypal"); + expect(result).toBe("Paypal"); + }); + + it("decodes 'venmo' wallet type", () => { + const result = PMCollectTypes.decodeWallet("venmo"); + expect(result).toBe("Venmo"); + }); + + it("returns undefined for unknown wallet type", () => { + const result = PMCollectTypes.decodeWallet("unknown"); + expect(result).toBeUndefined(); + }); + }); + + describe("decodeBankRedirect", () => { + it("decodes 'interac' bank redirect type", () => { + const result = PMCollectTypes.decodeBankRedirect("interac"); + expect(result).toBe("Interac"); + }); + + it("returns undefined for unknown bank redirect type", () => { + const result = PMCollectTypes.decodeBankRedirect("unknown"); + expect(result).toBeUndefined(); + }); + + it("returns undefined for empty string", () => { + const result = PMCollectTypes.decodeBankRedirect(""); + expect(result).toBeUndefined(); + }); + }); + + describe("decodeFieldType", () => { + it("decodes 'billing.address.city' to AddressCity", () => { + const result = PMCollectTypes.decodeFieldType("billing.address.city", undefined); + expect(result).toEqual({ TAG: "BillingAddress", _0: "AddressCity" }); + }); + + it("decodes 'billing.address.line1' to AddressLine1", () => { + const result = PMCollectTypes.decodeFieldType("billing.address.line1", undefined); + expect(result).toEqual({ TAG: "BillingAddress", _0: "AddressLine1" }); + }); + + it("decodes 'billing.address.line2' to AddressLine2", () => { + const result = PMCollectTypes.decodeFieldType("billing.address.line2", undefined); + expect(result).toEqual({ TAG: "BillingAddress", _0: "AddressLine2" }); + }); + + it("decodes 'billing.address.state' to AddressState", () => { + const result = PMCollectTypes.decodeFieldType("billing.address.state", undefined); + expect(result).toEqual({ TAG: "BillingAddress", _0: "AddressState" }); + }); + + it("decodes 'billing.address.zip' to AddressPincode", () => { + const result = PMCollectTypes.decodeFieldType("billing.address.zip", undefined); + expect(result).toEqual({ TAG: "BillingAddress", _0: "AddressPincode" }); + }); + + it("decodes 'billing.phone.country_code' to PhoneCountryCode", () => { + const result = PMCollectTypes.decodeFieldType("billing.phone.country_code", undefined); + expect(result).toEqual({ TAG: "BillingAddress", _0: "PhoneCountryCode" }); + }); + + it("decodes 'billing.phone.number' to PhoneNumber", () => { + const result = PMCollectTypes.decodeFieldType("billing.phone.number", undefined); + expect(result).toEqual({ TAG: "BillingAddress", _0: "PhoneNumber" }); + }); + + it("decodes 'payout_method_data.bank.bic' to SepaBic", () => { + const result = PMCollectTypes.decodeFieldType("payout_method_data.bank.bic", undefined); + expect(result).toEqual({ TAG: "PayoutMethodData", _0: "SepaBic" }); + }); + + it("decodes 'payout_method_data.bank.iban' to SepaIban", () => { + const result = PMCollectTypes.decodeFieldType("payout_method_data.bank.iban", undefined); + expect(result).toEqual({ TAG: "PayoutMethodData", _0: "SepaIban" }); + }); + + it("decodes 'payout_method_data.card.card_holder_name' to CardHolderName", () => { + const result = PMCollectTypes.decodeFieldType("payout_method_data.card.card_holder_name", undefined); + expect(result).toEqual({ TAG: "PayoutMethodData", _0: "CardHolderName" }); + }); + + it("decodes 'payout_method_data.card.card_number' to CardNumber", () => { + const result = PMCollectTypes.decodeFieldType("payout_method_data.card.card_number", undefined); + expect(result).toEqual({ TAG: "PayoutMethodData", _0: "CardNumber" }); + }); + + it("returns undefined for unknown field type", () => { + const result = PMCollectTypes.decodeFieldType("unknown.field", undefined); + expect(result).toBeUndefined(); + }); + }); + + describe("createCustomOrderMap", () => { + it("creates a map from array of strings", () => { + const order = ["a", "b", "c"]; + const map = PMCollectTypes.createCustomOrderMap(order); + expect(map.get("a")).toBe(0); + expect(map.get("b")).toBe(1); + expect(map.get("c")).toBe(2); + }); + + it("returns empty map for empty array", () => { + const map = PMCollectTypes.createCustomOrderMap([]); + expect(map.size).toBe(0); + }); + + it("handles single item array", () => { + const map = PMCollectTypes.createCustomOrderMap(["only"]); + expect(map.size).toBe(1); + expect(map.get("only")).toBe(0); + }); + }); + + describe("getCustomIndex", () => { + it("returns index from map when key exists", () => { + const map = new Map([["key1", 5]]); + const result = PMCollectTypes.getCustomIndex("key1", map, 99); + expect(result).toBe(5); + }); + + it("returns default index when key does not exist", () => { + const map = new Map([["key1", 5]]); + const result = PMCollectTypes.getCustomIndex("unknown", map, 99); + expect(result).toBe(99); + }); + + it("returns default index for empty map", () => { + const map = new Map(); + const result = PMCollectTypes.getCustomIndex("any", map, 42); + expect(result).toBe(42); + }); + }); + + describe("sortByCustomOrder", () => { + it("sorts array by custom order", () => { + const arr = [ + { pmdMap: "billing.address.city" }, + { pmdMap: "billing.address.first_name" }, + { pmdMap: "billing.address.country" }, + ]; + const getKey = (item: any) => item.pmdMap; + PMCollectTypes.sortByCustomOrder(arr, getKey, PMCollectTypes.customAddressOrder); + expect(arr[0].pmdMap).toBe("billing.address.first_name"); + expect(arr[1].pmdMap).toBe("billing.address.city"); + expect(arr[2].pmdMap).toBe("billing.address.country"); + }); + + it("places unknown items at the end", () => { + const arr = [ + { pmdMap: "unknown_field" }, + { pmdMap: "billing.address.first_name" }, + ]; + const getKey = (item: any) => item.pmdMap; + PMCollectTypes.sortByCustomOrder(arr, getKey, PMCollectTypes.customAddressOrder); + expect(arr[0].pmdMap).toBe("billing.address.first_name"); + expect(arr[1].pmdMap).toBe("unknown_field"); + }); + + it("handles empty array", () => { + const arr: any[] = []; + const getKey = (item: any) => item.pmdMap; + PMCollectTypes.sortByCustomOrder(arr, getKey, PMCollectTypes.customAddressOrder); + expect(arr.length).toBe(0); + }); + }); + + describe("customAddressOrder", () => { + it("has expected order", () => { + expect(PMCollectTypes.customAddressOrder).toEqual([ + "billing.address.first_name", + "billing.address.last_name", + "billing.address.line1", + "billing.address.line2", + "billing.address.city", + "billing.address.zip", + "billing.address.state", + "billing.address.country", + "billing.phone.country_code", + "billing.phone.number", + ]); + }); + }); + + describe("customPmdOrder", () => { + it("has expected order", () => { + expect(PMCollectTypes.customPmdOrder).toEqual([ + "payout_method_data.card.card_number", + "payout_method_data.card.expiry_month", + "payout_method_data.card.expiry_year", + "payout_method_data.card.card_holder_name", + "payout_method_data.bank.iban", + "payout_method_data.bank.bic", + ]); + }); + }); + + describe("emailValidationRegex", () => { + it("matches valid email format", () => { + expect(PMCollectTypes.emailValidationRegex.test("test@example.com")).toBe(true); + expect(PMCollectTypes.emailValidationRegex.test("user.name@domain.org")).toBe(true); + }); + + it("matches partial email input", () => { + expect(PMCollectTypes.emailValidationRegex.test("test@")).toBe(true); + expect(PMCollectTypes.emailValidationRegex.test("test@example")).toBe(true); + }); + + it("does not match invalid patterns", () => { + expect(PMCollectTypes.emailValidationRegex.test("invalid")).toBe(false); + }); + }); + + describe("getFieldOptions", () => { + it("returns undefined for dict without field_type", () => { + const dict = {}; + const result = PMCollectTypes.getFieldOptions(dict); + expect(result).toBeUndefined(); + }); + + it("returns undefined for non-object field_type", () => { + const dict = { field_type: "not an object" }; + const result = PMCollectTypes.getFieldOptions(dict); + expect(result).toBeUndefined(); + }); + + it("returns undefined when user_address_country is missing", () => { + const dict = { field_type: {} }; + const result = PMCollectTypes.getFieldOptions(dict); + expect(result).toBeUndefined(); + }); + + it("returns undefined when options is missing", () => { + const dict = { + field_type: { + user_address_country: {}, + }, + }; + const result = PMCollectTypes.getFieldOptions(dict); + expect(result).toBeUndefined(); + }); + + it("returns BillingAddress with AddressCountry when valid", () => { + const dict = { + field_type: { + user_address_country: { + options: ["US", "CA", "GB"], + }, + }, + }; + const result = PMCollectTypes.getFieldOptions(dict); + expect(result).toEqual({ + TAG: "BillingAddress", + _0: { + TAG: "AddressCountry", + _0: ["CA", "GB", "US"], + }, + }); + }); + + it("sorts countries alphabetically", () => { + const dict = { + field_type: { + user_address_country: { + options: ["Zimbabwe", "Argentina", "Brazil"], + }, + }, + }; + const result = PMCollectTypes.getFieldOptions(dict); + expect(result!._0._0).toEqual(["Argentina", "Brazil", "Zimbabwe"]); + }); + + it("handles empty options array", () => { + const dict = { + field_type: { + user_address_country: { + options: [], + }, + }, + }; + const result = PMCollectTypes.getFieldOptions(dict); + expect(result).toEqual({ + TAG: "BillingAddress", + _0: { + TAG: "AddressCountry", + _0: [], + }, + }); + }); + }); + + describe("decodePayoutDynamicFields", () => { + const defaultDynamicPmdFields = { address: undefined, payoutMethodData: [] }; + + it("returns default for null input", () => { + const result = PMCollectTypes.decodePayoutDynamicFields(null, defaultDynamicPmdFields); + expect(result.address).toBeUndefined(); + expect(result.payoutMethodData).toEqual(defaultDynamicPmdFields); + }); + + it("returns default for non-object input", () => { + const result = PMCollectTypes.decodePayoutDynamicFields("string", defaultDynamicPmdFields); + expect(result.address).toBeUndefined(); + expect(result.payoutMethodData).toEqual(defaultDynamicPmdFields); + }); + + it("returns default for empty object", () => { + const result = PMCollectTypes.decodePayoutDynamicFields({}, defaultDynamicPmdFields); + expect(result.address).toBeUndefined(); + expect(result.payoutMethodData).toEqual(defaultDynamicPmdFields); + }); + + it("parses billing address field correctly", () => { + const json = { + "billing.address.first_name": { + required_field: "billing.address.first_name", + display_name: "First Name", + value: "John", + }, + }; + const result = PMCollectTypes.decodePayoutDynamicFields(json, defaultDynamicPmdFields); + expect(result.address).toBeDefined(); + expect(result.address!.length).toBe(1); + expect(result.address![0].pmdMap).toBe("billing.address.first_name"); + expect(result.address![0].displayName).toBe("First Name"); + }); + + it("parses payout method data field correctly", () => { + const json = { + "payout_method_data.card.card_number": { + required_field: "payout_method_data.card.card_number", + display_name: "Card Number", + value: "4111111111111111", + }, + }; + const result = PMCollectTypes.decodePayoutDynamicFields(json, defaultDynamicPmdFields); + expect(result.payoutMethodData.length).toBe(1); + }); + + it("skips fields missing required_field", () => { + const json = { + "billing.address.first_name": { + display_name: "First Name", + value: "John", + }, + }; + const result = PMCollectTypes.decodePayoutDynamicFields(json, defaultDynamicPmdFields); + expect(result.address).toBeUndefined(); + }); + + it("skips fields missing display_name", () => { + const json = { + "billing.address.first_name": { + required_field: "billing.address.first_name", + value: "John", + }, + }; + const result = PMCollectTypes.decodePayoutDynamicFields(json, defaultDynamicPmdFields); + expect(result.address).toBeUndefined(); + }); + + it("skips fields with unknown field type", () => { + const json = { + "unknown.field": { + required_field: "unknown.field", + display_name: "Unknown", + value: "value", + }, + }; + const result = PMCollectTypes.decodePayoutDynamicFields(json, defaultDynamicPmdFields); + expect(result.address).toBeUndefined(); + expect(result.payoutMethodData).toEqual(defaultDynamicPmdFields); + }); + }); + + describe("decodePayoutConfirmResponse", () => { + it("returns undefined for null input", () => { + const result = PMCollectTypes.decodePayoutConfirmResponse(null); + expect(result).toBeUndefined(); + }); + + it("returns undefined for non-object input", () => { + const result = PMCollectTypes.decodePayoutConfirmResponse("string"); + expect(result).toBeUndefined(); + }); + + it("returns undefined when status is missing", () => { + const result = PMCollectTypes.decodePayoutConfirmResponse({}); + expect(result).toBeUndefined(); + }); + + it("returns SuccessResponse for valid success response", () => { + const json = { + status: "success", + payout_id: "payout_123", + merchant_id: "merchant_123", + customer_id: "customer_123", + amount: 100.0, + currency: "USD", + payout_type: "card", + connector: "stripe", + }; + const result = PMCollectTypes.decodePayoutConfirmResponse(json); + expect(result).toBeDefined(); + expect(result!.TAG).toBe("SuccessResponse"); + expect(result!._0.payoutId).toBe("payout_123"); + expect(result!._0.status).toBe("Success"); + }); + + it("decodes all status values correctly", () => { + const statuses = [ + { input: "cancelled", expected: "Cancelled" }, + { input: "expired", expected: "Expired" }, + { input: "failed", expected: "Failed" }, + { input: "ineligible", expected: "Ineligible" }, + { input: "initiated", expected: "Initiated" }, + { input: "pending", expected: "Pending" }, + { input: "requires_confirmation", expected: "RequiresConfirmation" }, + { input: "requires_creation", expected: "RequiresCreation" }, + { input: "requires_fulfillment", expected: "RequiresFulfillment" }, + { input: "requires_payout_method_data", expected: "RequiresPayoutMethodData" }, + { input: "requires_vendor_account_creation", expected: "RequiresVendorAccountCreation" }, + { input: "reversed", expected: "Reversed" }, + { input: "success", expected: "Success" }, + ]; + + statuses.forEach(({ input, expected }) => { + const json = { + status: input, + payout_id: "payout_123", + merchant_id: "merchant_123", + customer_id: "customer_123", + amount: 100.0, + currency: "USD", + payout_type: "card", + }; + const result = PMCollectTypes.decodePayoutConfirmResponse(json); + expect(result!._0.status).toBe(expected); + }); + }); + + it("returns ErrorResponse for error response", () => { + const json = { + type: "invalid_request", + code: "error_code", + message: "Error message", + reason: "Error reason", + }; + const result = PMCollectTypes.decodePayoutConfirmResponse(json); + expect(result).toBeDefined(); + expect(result!.TAG).toBe("ErrorResponse"); + expect(result!._0.errorType).toBe("invalid_request"); + expect(result!._0.code).toBe("error_code"); + expect(result!._0.message).toBe("Error message"); + }); + + it("returns undefined when required success fields are missing", () => { + const json = { + status: "success", + payout_id: "payout_123", + }; + const result = PMCollectTypes.decodePayoutConfirmResponse(json); + expect(result).toBeUndefined(); + }); + + it("returns undefined when required error fields are missing", () => { + const json = { + type: "error", + }; + const result = PMCollectTypes.decodePayoutConfirmResponse(json); + expect(result).toBeUndefined(); + }); + + it("includes optional fields in success response", () => { + const json = { + status: "success", + payout_id: "payout_123", + merchant_id: "merchant_123", + customer_id: "customer_123", + amount: 100.0, + currency: "USD", + payout_type: "card", + connector: "stripe", + error_message: "Some error", + error_code: "ERR001", + connector_transaction_id: "txn_123", + }; + const result = PMCollectTypes.decodePayoutConfirmResponse(json); + expect(result!._0.connector).toBe("stripe"); + expect(result!._0.errorMessage).toBe("Some error"); + expect(result!._0.errorCode).toBe("ERR001"); + expect(result!._0.connectorTransactionId).toBe("txn_123"); + }); + }); + + describe("decodePaymentMethodTypeArray", () => { + const defaultDynamicPmdFields = (pmt: any) => ({ address: undefined, payoutMethodData: [] }); + + it("returns empty arrays for null input", () => { + const result = PMCollectTypes.decodePaymentMethodTypeArray(null, defaultDynamicPmdFields); + expect(result).toEqual([[], []]); + }); + + it("returns empty arrays for non-array input", () => { + const result = PMCollectTypes.decodePaymentMethodTypeArray("not an array", defaultDynamicPmdFields); + expect(result).toEqual([[], []]); + }); + + it("returns empty arrays for empty array input", () => { + const result = PMCollectTypes.decodePaymentMethodTypeArray([], defaultDynamicPmdFields); + expect(result).toEqual([[], []]); + }); + + it("decodes valid payment method types", () => { + const jsonArray = [ + { + payment_method: "card", + payment_method_types_info: [ + { + payment_method_type: "credit", + required_fields: {}, + }, + ], + }, + ]; + const result = PMCollectTypes.decodePaymentMethodTypeArray(jsonArray, defaultDynamicPmdFields); + expect(result[0].length).toBe(1); + expect(result[0][0]).toEqual({ TAG: "Card", _0: "Credit" }); + }); + + it("skips invalid payment method types", () => { + const jsonArray = [ + { + payment_method: "unknown", + payment_method_types_info: [], + }, + ]; + const result = PMCollectTypes.decodePaymentMethodTypeArray(jsonArray, defaultDynamicPmdFields); + expect(result).toEqual([[], []]); + }); + + it("handles multiple payment method types", () => { + const jsonArray = [ + { + payment_method: "card", + payment_method_types_info: [ + { payment_method_type: "credit", required_fields: {} }, + { payment_method_type: "debit", required_fields: {} }, + ], + }, + ]; + const result = PMCollectTypes.decodePaymentMethodTypeArray(jsonArray, defaultDynamicPmdFields); + expect(result[0].length).toBe(2); + }); + + it("handles wallet payment method type", () => { + const jsonArray = [ + { + payment_method: "wallet", + payment_method_types_info: [ + { payment_method_type: "paypal", required_fields: {} }, + ], + }, + ]; + const result = PMCollectTypes.decodePaymentMethodTypeArray(jsonArray, defaultDynamicPmdFields); + expect(result[0].length).toBe(1); + expect(result[0][0]).toEqual({ TAG: "Wallet", _0: "Paypal" }); + }); + + it("handles bank_redirect payment method type", () => { + const jsonArray = [ + { + payment_method: "bank_redirect", + payment_method_types_info: [ + { payment_method_type: "interac", required_fields: {} }, + ], + }, + ]; + const result = PMCollectTypes.decodePaymentMethodTypeArray(jsonArray, defaultDynamicPmdFields); + expect(result[0].length).toBe(1); + expect(result[0][0]).toEqual({ TAG: "BankRedirect", _0: "Interac" }); + }); + + it("handles bank_transfer payment method type", () => { + const jsonArray = [ + { + payment_method: "bank_transfer", + payment_method_types_info: [ + { payment_method_type: "ach", required_fields: {} }, + ], + }, + ]; + const result = PMCollectTypes.decodePaymentMethodTypeArray(jsonArray, defaultDynamicPmdFields); + expect(result[0].length).toBe(1); + expect(result[0][0]).toEqual({ TAG: "BankTransfer", _0: "ACH" }); + }); + }); +}); diff --git a/src/__tests__/PaymentMethodCollectUtils.test.ts b/src/__tests__/PaymentMethodCollectUtils.test.ts new file mode 100644 index 000000000..9a55409a3 --- /dev/null +++ b/src/__tests__/PaymentMethodCollectUtils.test.ts @@ -0,0 +1,1547 @@ +import { + getNestedValue, + getOrCreateSubDict, + setNestedValue, + getPaymentMethod, + getPaymentMethodType, + getPaymentMethodLabel, + getPaymentMethodDataFieldKey, + getPaymentMethodDataFieldMaxLength, + calculateValidity, + checkValidity, + defaultFormDataDict, + defaultValidityDict, + getPayoutStatusString, + getPaymentMethodForPmt, + getPaymentMethodForPayoutsConfirm, + getPaymentMethodTypeLabel, + getPaymentMethodDataFieldLabel, + getPaymentMethodDataFieldPlaceholder, + getPaymentMethodDataFieldCharacterPattern, + getPaymentMethodDataFieldInputType, + getPayoutImageSource, + getPayoutReadableStatus, + getPayoutStatusMessage, + getPaymentMethodDataErrorString, + defaultPmt, + defaultView, + defaultAmount, + defaultCurrency, + defaultPm, + defaultFormLayout, + defaultJourneyView, + defaultTabView, + defaultPaymentMethodCollectFlow, + processPaymentMethodDataFields, + processAddressFields, + formPaymentMethodData, + formBody, + getPayoutDynamicFields, + getDefaultsAndValidity, + itemToObjMapper, + defaultDynamicPmdFields, + defaultPayoutDynamicFields, + defaultCardFields, + defaultAchFields, + defaultBacsFields, + defaultPixTransferFields, + defaultSepaFields, + defaultPaypalFields, + defaultInteracFields, + defaultEnabledPaymentMethods, + defaultEnabledPaymentMethodsWithDynamicFields, + defaultPaymentMethodCollectOptions, + defaultStatusInfo, + getPaymentMethodIcon, + getBankTransferIcon, + getWalletIcon, + getBankRedirectIcon, + getPaymentMethodTypeIcon, +} from '../Utilities/PaymentMethodCollectUtils.bs.js'; +import React from 'react'; + +const mockLocaleString = { + fullNameLabel: 'Full Name', + countryLabel: 'Country', + emailLabel: 'Email', + formFieldPhoneNumberLabel: 'Phone Number', + formFieldCountryCodeRequiredLabel: 'Country Code', + line1Label: 'Address Line 1', + line2Label: 'Address Line 2', + cityLabel: 'City', + stateLabel: 'State', + postalCodeLabel: 'Postal Code', + cardNumberLabel: 'Card Number', + cardHolderName: 'Card Holder Name', + validThruText: 'Valid Thru', + formFieldACHRoutingNumberLabel: 'Routing Number', + sortCodeText: 'Sort Code', + accountNumberText: 'Account Number', + formFieldSepaIbanLabel: 'IBAN', + formFieldSepaBicLabel: 'BIC', + formFieldBankCityLabel: 'Bank City', + formFieldCountryCodeLabel: 'Country Code', + formFieldPixIdLabel: 'PIX ID', + formFieldBankAccountNumberLabel: 'Bank Account', + formFieldBankNameLabel: 'Bank Name', + formFieldEmailPlaceholder: 'Enter email', + formFieldPhoneNumberPlaceholder: 'Enter phone', + formFieldCardHoldernamePlaceholder: 'Enter name', + line1Placeholder: 'Enter address', + line2Placeholder: 'Enter address 2', + formFieldBankCityPlaceholder: 'Enter city', + expiryPlaceholder: 'MM/YY', + payoutStatusSuccessText: 'Success', + payoutStatusPendingText: 'Pending', + payoutStatusFailedText: 'Failed', + payoutStatusSuccessMessage: 'Payment successful', + payoutStatusPendingMessage: 'Payment pending', + payoutStatusFailedMessage: 'Payment failed', + emailEmptyText: 'Email is required', + emailInvalidText: 'Invalid email', + nameEmptyText: (label: string) => `${label} is required`, + completeNameEmptyText: (label: string) => `${label} is incomplete`, + line1EmptyText: 'Address line 1 is required', + line2EmptyText: 'Address line 2 is required', + cityEmptyText: 'City is required', + stateEmptyText: 'State is required', + postalCodeEmptyText: 'Postal code is required', + postalCodeInvalidText: 'Invalid postal code', + inValidCardErrorText: 'Invalid card', + pastExpiryErrorText: 'Card expired', + inCompleteExpiryErrorText: 'Expiry incomplete', + formFieldInvalidRoutingNumber: 'Invalid routing number', + sortCodeInvalidText: 'Invalid sort code', + accountNumberInvalidText: 'Invalid account number', + ibanEmptyText: 'IBAN is required', + ibanInvalidText: 'Invalid IBAN', +}; + +const mockConstants = { + formFieldCardNumberPlaceholder: '1234 5678 9012 3456', + formFieldACHRoutingNumberPlaceholder: '123456789', + formFieldSortCodePlaceholder: '123456', + formFieldAccountNumberPlaceholder: '12345678', + formFieldSepaIbanPlaceholder: 'DE89370400440532013000', + formFieldSepaBicPlaceholder: 'DEUTDEFF', + formFieldPixIdPlaceholder: 'Enter PIX ID', + formFieldBankAccountNumberPlaceholder: 'Enter account number', +}; + +describe('PaymentMethodCollectUtils', () => { + describe('getNestedValue', () => { + it('should get value from nested dict with valid path', () => { + const dict = { a: { b: { c: 'value' } } }; + const result = getNestedValue(dict, 'a.b.c'); + expect(result).toBe('value'); + }); + + it('should return undefined for missing path', () => { + const dict = { a: { b: 'value' } }; + const result = getNestedValue(dict, 'a.b.c'); + expect(result).toBeUndefined(); + }); + + it('should handle empty dict', () => { + const result = getNestedValue({}, 'a.b.c'); + expect(result).toBeUndefined(); + }); + + it('should get top-level value', () => { + const dict = { a: 'value' }; + const result = getNestedValue(dict, 'a'); + expect(result).toBe('value'); + }); + }); + + describe('getOrCreateSubDict', () => { + it('should return existing sub-dict', () => { + const dict = { sub: { key: 'value' } }; + const result = getOrCreateSubDict(dict, 'sub'); + expect(result).toEqual({ key: 'value' }); + }); + + it('should create new sub-dict if missing', () => { + const dict: Record = {}; + const result = getOrCreateSubDict(dict, 'new'); + expect(result).toEqual({}); + expect(dict['new']).toEqual({}); + }); + }); + + describe('setNestedValue', () => { + it('should set value at nested path', () => { + const dict: any = {}; + setNestedValue(dict, 'a.b.c', 'value'); + expect(dict.a.b.c).toBe('value'); + }); + + it('should overwrite existing value', () => { + const dict = { a: { b: 'old' } }; + setNestedValue(dict, 'a.b', 'new'); + expect(dict.a.b).toBe('new'); + }); + + it('should set top-level value', () => { + const dict: any = {}; + setNestedValue(dict, 'key', 'value'); + expect(dict.key).toBe('value'); + }); + }); + + describe('getPaymentMethod', () => { + it('should return card for Card', () => { + expect(getPaymentMethod('Card')).toBe('card'); + }); + + it('should return bank_redirect for BankRedirect', () => { + expect(getPaymentMethod('BankRedirect')).toBe('bank_redirect'); + }); + + it('should return bank_transfer for BankTransfer', () => { + expect(getPaymentMethod('BankTransfer')).toBe('bank_transfer'); + }); + + it('should return wallet for Wallet', () => { + expect(getPaymentMethod('Wallet')).toBe('wallet'); + }); + }); + + describe('getPaymentMethodType', () => { + it('should return credit for Credit card', () => { + const result = getPaymentMethodType({ TAG: 'Card', _0: 'Credit' }); + expect(result).toBe('credit'); + }); + + it('should return debit for Debit card', () => { + const result = getPaymentMethodType({ TAG: 'Card', _0: 'Debit' }); + expect(result).toBe('debit'); + }); + + it('should return ach for ACH bank transfer', () => { + const result = getPaymentMethodType({ TAG: 'BankTransfer', _0: 'ACH' }); + expect(result).toBe('ach'); + }); + + it('should return paypal for Paypal wallet', () => { + const result = getPaymentMethodType({ TAG: 'Wallet', _0: 'Paypal' }); + expect(result).toBe('paypal'); + }); + }); + + describe('getPaymentMethodLabel', () => { + it('should return Card for Card', () => { + expect(getPaymentMethodLabel('Card')).toBe('Card'); + }); + + it('should return Bank for BankRedirect', () => { + expect(getPaymentMethodLabel('BankRedirect')).toBe('Bank'); + }); + + it('should return Bank for BankTransfer', () => { + expect(getPaymentMethodLabel('BankTransfer')).toBe('Bank'); + }); + + it('should return Wallet for Wallet', () => { + expect(getPaymentMethodLabel('Wallet')).toBe('Wallet'); + }); + }); + + describe('getPaymentMethodDataFieldKey', () => { + it('should return correct key for CardNumber', () => { + const result = getPaymentMethodDataFieldKey({ TAG: 'PayoutMethodData', _0: 'CardNumber' }); + expect(result).toBe('card.cardNumber'); + }); + + it('should return correct key for email billing address', () => { + const result = getPaymentMethodDataFieldKey({ TAG: 'BillingAddress', _0: 'Email' }); + expect(result).toBe('billing.address.email'); + }); + + it('should return correct key for card holder name', () => { + const result = getPaymentMethodDataFieldKey({ TAG: 'PayoutMethodData', _0: 'CardHolderName' }); + expect(result).toBe('card.cardHolder'); + }); + }); + + describe('getPaymentMethodDataFieldMaxLength', () => { + it('should return 23 for CardNumber', () => { + const result = getPaymentMethodDataFieldMaxLength({ TAG: 'PayoutMethodData', _0: 'CardNumber' }); + expect(result).toBe(23); + }); + + it('should return 32 for BillingAddress fields', () => { + const result = getPaymentMethodDataFieldMaxLength({ TAG: 'BillingAddress', _0: 'Email' }); + expect(result).toBe(32); + }); + + it('should return 9 for ACHRoutingNumber', () => { + const result = getPaymentMethodDataFieldMaxLength({ TAG: 'PayoutMethodData', _0: 'ACHRoutingNumber' }); + expect(result).toBe(9); + }); + }); + + describe('calculateValidity', () => { + it('should return true for valid card number', () => { + const result = calculateValidity( + { TAG: 'PayoutMethodData', _0: 'CardNumber' }, + '4111111111111111', + 'Visa', + undefined + ); + expect(result).toBe(true); + }); + + it('should return false for invalid card number', () => { + const result = calculateValidity( + { TAG: 'PayoutMethodData', _0: 'CardNumber' }, + '1234', + '', + undefined + ); + expect(result).toBe(false); + }); + + it('should return default for empty value', () => { + const result = calculateValidity( + { TAG: 'PayoutMethodData', _0: 'CardNumber' }, + '', + '', + true + ); + expect(result).toBe(true); + }); + }); + + describe('checkValidity', () => { + it('should return true when all fields are valid', () => { + const keys = ['field1', 'field2']; + const validityDict = { field1: true, field2: true }; + expect(checkValidity(keys, validityDict, true)).toBe(true); + }); + + it('should return false when any field is invalid', () => { + const keys = ['field1', 'field2']; + const validityDict = { field1: true, field2: false }; + expect(checkValidity(keys, validityDict, true)).toBe(false); + }); + + it('should use default validity for missing keys', () => { + const keys = ['field1', 'field2']; + const validityDict = { field1: true }; + expect(checkValidity(keys, validityDict, true)).toBe(true); + }); + }); + + describe('defaultFormDataDict', () => { + it('should be defined as empty object', () => { + expect(defaultFormDataDict).toBeDefined(); + expect(typeof defaultFormDataDict).toBe('object'); + }); + }); + + describe('defaultValidityDict', () => { + it('should be defined as empty object', () => { + expect(defaultValidityDict).toBeDefined(); + expect(typeof defaultValidityDict).toBe('object'); + }); + }); + + describe('getPayoutStatusString', () => { + it('should return success for Success', () => { + expect(getPayoutStatusString('Success')).toBe('success'); + }); + + it('should return failed for Failed', () => { + expect(getPayoutStatusString('Failed')).toBe('failed'); + }); + + it('should return pending for Pending', () => { + expect(getPayoutStatusString('Pending')).toBe('pending'); + }); + + it('should return initiated for Initiated', () => { + expect(getPayoutStatusString('Initiated')).toBe('initiated'); + }); + }); + + describe('getPaymentMethodForPmt', () => { + it('should return Card for Card type', () => { + expect(getPaymentMethodForPmt({ TAG: 'Card', _0: 'Credit' })).toBe('Card'); + }); + + it('should return BankRedirect for BankRedirect type', () => { + expect(getPaymentMethodForPmt({ TAG: 'BankRedirect', _0: 'Interac' })).toBe('BankRedirect'); + }); + + it('should return BankTransfer for BankTransfer type', () => { + expect(getPaymentMethodForPmt({ TAG: 'BankTransfer', _0: 'ACH' })).toBe('BankTransfer'); + }); + + it('should return Wallet for Wallet type', () => { + expect(getPaymentMethodForPmt({ TAG: 'Wallet', _0: 'Paypal' })).toBe('Wallet'); + }); + }); + + describe('getPaymentMethodForPayoutsConfirm', () => { + it('should return card for Card', () => { + expect(getPaymentMethodForPayoutsConfirm('Card')).toBe('card'); + }); + + it('should return bank_redirect for BankRedirect', () => { + expect(getPaymentMethodForPayoutsConfirm('BankRedirect')).toBe('bank_redirect'); + }); + + it('should return bank for BankTransfer', () => { + expect(getPaymentMethodForPayoutsConfirm('BankTransfer')).toBe('bank'); + }); + + it('should return wallet for Wallet', () => { + expect(getPaymentMethodForPayoutsConfirm('Wallet')).toBe('wallet'); + }); + }); + + describe('getPaymentMethodTypeLabel', () => { + it('should return Card for Credit card type', () => { + expect(getPaymentMethodTypeLabel({ TAG: 'Card', _0: 'Credit' })).toBe('Card'); + }); + + it('should return Card for Debit card type', () => { + expect(getPaymentMethodTypeLabel({ TAG: 'Card', _0: 'Debit' })).toBe('Card'); + }); + + it('should return Interac for BankRedirect', () => { + expect(getPaymentMethodTypeLabel({ TAG: 'BankRedirect', _0: 'Interac' })).toBe('Interac'); + }); + + it('should return ACH for ACH BankTransfer', () => { + expect(getPaymentMethodTypeLabel({ TAG: 'BankTransfer', _0: 'ACH' })).toBe('ACH'); + }); + + it('should return BACS for Bacs BankTransfer', () => { + expect(getPaymentMethodTypeLabel({ TAG: 'BankTransfer', _0: 'Bacs' })).toBe('BACS'); + }); + + it('should return Pix for Pix BankTransfer', () => { + expect(getPaymentMethodTypeLabel({ TAG: 'BankTransfer', _0: 'Pix' })).toBe('Pix'); + }); + + it('should return SEPA for Sepa BankTransfer', () => { + expect(getPaymentMethodTypeLabel({ TAG: 'BankTransfer', _0: 'Sepa' })).toBe('SEPA'); + }); + + it('should return PayPal for Paypal Wallet', () => { + expect(getPaymentMethodTypeLabel({ TAG: 'Wallet', _0: 'Paypal' })).toBe('PayPal'); + }); + + it('should return Venmo for Venmo Wallet', () => { + expect(getPaymentMethodTypeLabel({ TAG: 'Wallet', _0: 'Venmo' })).toBe('Venmo'); + }); + }); + + describe('getPaymentMethodDataFieldLabel', () => { + it('should return correct label for CardNumber', () => { + const key = { TAG: 'PayoutMethodData', _0: 'CardNumber' }; + expect(getPaymentMethodDataFieldLabel(key, mockLocaleString)).toBe('Card Number'); + }); + + it('should return correct label for CardHolderName', () => { + const key = { TAG: 'PayoutMethodData', _0: 'CardHolderName' }; + expect(getPaymentMethodDataFieldLabel(key, mockLocaleString)).toBe('Card Holder Name'); + }); + + it('should return correct label for Email billing address', () => { + const key = { TAG: 'BillingAddress', _0: 'Email' }; + expect(getPaymentMethodDataFieldLabel(key, mockLocaleString)).toBe('Email'); + }); + + it('should return correct label for PhoneNumber billing address', () => { + const key = { TAG: 'BillingAddress', _0: 'PhoneNumber' }; + expect(getPaymentMethodDataFieldLabel(key, mockLocaleString)).toBe('Phone Number'); + }); + + it('should return correct label for SepaIban', () => { + const key = { TAG: 'PayoutMethodData', _0: 'SepaIban' }; + expect(getPaymentMethodDataFieldLabel(key, mockLocaleString)).toBe('IBAN'); + }); + }); + + describe('getPaymentMethodDataFieldPlaceholder', () => { + it('should return correct placeholder for CardNumber', () => { + const key = { TAG: 'PayoutMethodData', _0: 'CardNumber' }; + expect(getPaymentMethodDataFieldPlaceholder(key, mockLocaleString, mockConstants)).toBe('1234 5678 9012 3456'); + }); + + it('should return correct placeholder for Email billing address', () => { + const key = { TAG: 'BillingAddress', _0: 'Email' }; + expect(getPaymentMethodDataFieldPlaceholder(key, mockLocaleString, mockConstants)).toBe('Enter email'); + }); + + it('should return correct placeholder for SepaIban', () => { + const key = { TAG: 'PayoutMethodData', _0: 'SepaIban' }; + expect(getPaymentMethodDataFieldPlaceholder(key, mockLocaleString, mockConstants)).toBe('DE89370400440532013000'); + }); + }); + + describe('getPaymentMethodDataFieldCharacterPattern', () => { + it('should return pattern for CardNumber', () => { + const key = { TAG: 'PayoutMethodData', _0: 'CardNumber' }; + const pattern = getPaymentMethodDataFieldCharacterPattern(key); + expect(pattern).toBeInstanceOf(RegExp); + expect(pattern?.test('4111111111111111')).toBe(true); + }); + + it('should return pattern for PhoneNumber', () => { + const key = { TAG: 'BillingAddress', _0: 'PhoneNumber' }; + const pattern = getPaymentMethodDataFieldCharacterPattern(key); + expect(pattern).toBeInstanceOf(RegExp); + expect(pattern?.test('1234567890')).toBe(true); + }); + + it('should return undefined for fields without pattern', () => { + const key = { TAG: 'BillingAddress', _0: 'Email' }; + const pattern = getPaymentMethodDataFieldCharacterPattern(key); + expect(pattern).toBeUndefined(); + }); + + it('should return pattern for SepaIban', () => { + const key = { TAG: 'PayoutMethodData', _0: 'SepaIban' }; + const pattern = getPaymentMethodDataFieldCharacterPattern(key); + expect(pattern).toBeInstanceOf(RegExp); + }); + }); + + describe('getPaymentMethodDataFieldInputType', () => { + it('should return tel for CardNumber', () => { + const key = { TAG: 'PayoutMethodData', _0: 'CardNumber' }; + expect(getPaymentMethodDataFieldInputType(key)).toBe('tel'); + }); + + it('should return email for PaypalMail', () => { + const key = { TAG: 'PayoutMethodData', _0: 'PaypalMail' }; + expect(getPaymentMethodDataFieldInputType(key)).toBe('email'); + }); + + it('should return text for CardHolderName', () => { + const key = { TAG: 'PayoutMethodData', _0: 'CardHolderName' }; + expect(getPaymentMethodDataFieldInputType(key)).toBe('text'); + }); + + it('should return text for BillingAddress fields', () => { + const key = { TAG: 'BillingAddress', _0: 'Email' }; + expect(getPaymentMethodDataFieldInputType(key)).toBe('text'); + }); + + it('should return tel for ACHRoutingNumber', () => { + const key = { TAG: 'PayoutMethodData', _0: 'ACHRoutingNumber' }; + expect(getPaymentMethodDataFieldInputType(key)).toBe('tel'); + }); + }); + + describe('getPayoutImageSource', () => { + it('should return success image for Success status', () => { + expect(getPayoutImageSource('Success')).toBe('https://live.hyperswitch.io/payment-link-assets/success.png'); + }); + + it('should return pending image for Initiated status', () => { + expect(getPayoutImageSource('Initiated')).toBe('https://live.hyperswitch.io/payment-link-assets/pending.png'); + }); + + it('should return pending image for Pending status', () => { + expect(getPayoutImageSource('Pending')).toBe('https://live.hyperswitch.io/payment-link-assets/pending.png'); + }); + + it('should return pending image for RequiresFulfillment status', () => { + expect(getPayoutImageSource('RequiresFulfillment')).toBe('https://live.hyperswitch.io/payment-link-assets/pending.png'); + }); + + it('should return failed image for Failed status', () => { + expect(getPayoutImageSource('Failed')).toBe('https://live.hyperswitch.io/payment-link-assets/failed.png'); + }); + + it('should return failed image for unknown status', () => { + expect(getPayoutImageSource('Unknown')).toBe('https://live.hyperswitch.io/payment-link-assets/failed.png'); + }); + }); + + describe('getPayoutReadableStatus', () => { + it('should return success text for Success status', () => { + expect(getPayoutReadableStatus('Success', mockLocaleString)).toBe('Success'); + }); + + it('should return pending text for Initiated status', () => { + expect(getPayoutReadableStatus('Initiated', mockLocaleString)).toBe('Pending'); + }); + + it('should return pending text for Pending status', () => { + expect(getPayoutReadableStatus('Pending', mockLocaleString)).toBe('Pending'); + }); + + it('should return failed text for Failed status', () => { + expect(getPayoutReadableStatus('Failed', mockLocaleString)).toBe('Failed'); + }); + }); + + describe('getPayoutStatusMessage', () => { + it('should return success message for Success status', () => { + expect(getPayoutStatusMessage('Success', mockLocaleString)).toBe('Payment successful'); + }); + + it('should return pending message for Initiated status', () => { + expect(getPayoutStatusMessage('Initiated', mockLocaleString)).toBe('Payment pending'); + }); + + it('should return failed message for Failed status', () => { + expect(getPayoutStatusMessage('Failed', mockLocaleString)).toBe('Payment failed'); + }); + }); + + describe('getPaymentMethodDataErrorString', () => { + it('should return empty email error for empty Email value', () => { + const key = { TAG: 'BillingAddress', _0: 'Email' }; + expect(getPaymentMethodDataErrorString(key, '', mockLocaleString)).toBe('Email is required'); + }); + + it('should return invalid email error for invalid Email value', () => { + const key = { TAG: 'BillingAddress', _0: 'Email' }; + expect(getPaymentMethodDataErrorString(key, 'invalid', mockLocaleString)).toBe('Invalid email'); + }); + + it('should return card error for CardNumber', () => { + const key = { TAG: 'PayoutMethodData', _0: 'CardNumber' }; + expect(getPaymentMethodDataErrorString(key, '123', mockLocaleString)).toBe('Invalid card'); + }); + + it('should return postal code empty error for empty postal code', () => { + const key = { TAG: 'BillingAddress', _0: 'AddressPincode' }; + expect(getPaymentMethodDataErrorString(key, '', mockLocaleString)).toBe('Postal code is required'); + }); + }); + + describe('defaultPmt', () => { + it('should return Debit Card when no param provided', () => { + const result = defaultPmt(undefined); + expect(result.TAG).toBe('Card'); + expect(result._0).toBe('Debit'); + }); + + it('should return Credit Card when Card is provided', () => { + const result = defaultPmt('Card'); + expect(result.TAG).toBe('Card'); + expect(result._0).toBe('Debit'); + }); + + it('should return Interac BankRedirect when BankRedirect is provided', () => { + const result = defaultPmt('BankRedirect'); + expect(result.TAG).toBe('BankRedirect'); + expect(result._0).toBe('Interac'); + }); + + it('should return ACH BankTransfer when BankTransfer is provided', () => { + const result = defaultPmt('BankTransfer'); + expect(result.TAG).toBe('BankTransfer'); + expect(result._0).toBe('ACH'); + }); + + it('should return Paypal Wallet when Wallet is provided', () => { + const result = defaultPmt('Wallet'); + expect(result.TAG).toBe('Wallet'); + expect(result._0).toBe('Paypal'); + }); + }); + + describe('defaultView', () => { + it('should return Journey SelectPM for Journey layout', () => { + const result = defaultView('Journey'); + expect(result.TAG).toBe('Journey'); + expect(result._0).toBe('SelectPM'); + }); + + it('should return Tabs DetailsForm for Tabs layout', () => { + const result = defaultView('Tabs'); + expect(result.TAG).toBe('Tabs'); + expect(result._0).toBe('DetailsForm'); + }); + }); + + describe('constants', () => { + it('should have correct defaultAmount', () => { + expect(defaultAmount).toBe('0.01'); + }); + + it('should have correct defaultCurrency', () => { + expect(defaultCurrency).toBe('EUR'); + }); + + it('should have correct defaultPm', () => { + expect(defaultPm).toBe('Card'); + }); + + it('should have correct defaultFormLayout', () => { + expect(defaultFormLayout).toBe('Tabs'); + }); + + it('should have correct defaultJourneyView', () => { + expect(defaultJourneyView).toBe('SelectPM'); + }); + + it('should have correct defaultTabView', () => { + expect(defaultTabView).toBe('DetailsForm'); + }); + + it('should have correct defaultPaymentMethodCollectFlow', () => { + expect(defaultPaymentMethodCollectFlow).toBe('PayoutLinkInitiate'); + }); + }); + + describe('processPaymentMethodDataFields', () => { + it('should process card number field', () => { + const dynamicFieldsInfo = [ + { + pmdMap: 'payout_method_data.card.card_number', + displayName: 'user_card_number', + fieldType: 'CardNumber', + value: '4111111111111111', + }, + ]; + const paymentMethodDataDict = { 'card.cardNumber': '4111111111111111' }; + const fieldValidityDict = { 'card.cardNumber': true }; + + const result = processPaymentMethodDataFields(dynamicFieldsInfo, paymentMethodDataDict, fieldValidityDict); + + expect(result).toBeDefined(); + expect(result).toHaveLength(1); + }); + + it('should return undefined when validity check fails', () => { + const dynamicFieldsInfo = [ + { + pmdMap: 'payout_method_data.card.card_number', + displayName: 'user_card_number', + fieldType: 'CardNumber', + value: 'invalid', + }, + ]; + const paymentMethodDataDict = { 'card.cardNumber': 'invalid' }; + const fieldValidityDict = { 'card.cardNumber': false }; + + const result = processPaymentMethodDataFields(dynamicFieldsInfo, paymentMethodDataDict, fieldValidityDict); + + expect(result).toBeUndefined(); + }); + + it('should process expiry date fields', () => { + const dynamicFieldsInfo = [ + { + pmdMap: 'payout_method_data.card.expiry_month', + displayName: 'user_card_exp_month', + fieldType: { TAG: 'CardExpDate', _0: 'CardExpMonth' }, + value: undefined, + }, + ]; + const paymentMethodDataDict = { 'card.cardExp': '12/25' }; + const fieldValidityDict = { 'card.cardExp': true }; + + const result = processPaymentMethodDataFields(dynamicFieldsInfo, paymentMethodDataDict, fieldValidityDict); + + expect(result).toBeDefined(); + }); + + it('should handle empty dynamic fields', () => { + const result = processPaymentMethodDataFields([], {}, {}); + + expect(result).toEqual([]); + }); + }); + + describe('processAddressFields', () => { + it('should process full name field for first name', () => { + const dynamicFieldsInfo = [ + { + pmdMap: 'billing.address.fullName', + displayName: 'Full Name', + fieldType: { TAG: 'FullName', _0: 'FirstName' }, + value: undefined, + }, + ]; + const paymentMethodDataDict = { 'billing.address.fullName': 'John Doe' }; + const fieldValidityDict = { 'billing.address.fullName': true }; + + const result = processAddressFields(dynamicFieldsInfo, paymentMethodDataDict, fieldValidityDict); + + expect(result).toBeDefined(); + }); + + it('should process full name field for last name', () => { + const dynamicFieldsInfo = [ + { + pmdMap: 'billing.address.fullName', + displayName: 'Full Name', + fieldType: { TAG: 'FullName', _0: 'LastName' }, + value: undefined, + }, + ]; + const paymentMethodDataDict = { 'billing.address.fullName': 'John Doe' }; + const fieldValidityDict = { 'billing.address.fullName': true }; + + const result = processAddressFields(dynamicFieldsInfo, paymentMethodDataDict, fieldValidityDict); + + expect(result).toBeDefined(); + }); + + it('should process email billing address field', () => { + const dynamicFieldsInfo = [ + { + pmdMap: 'billing.address.email', + displayName: 'Email', + fieldType: 'Email', + value: undefined, + }, + ]; + const paymentMethodDataDict = { 'billing.address.email': 'test@example.com' }; + const fieldValidityDict = { 'billing.address.email': true }; + + const result = processAddressFields(dynamicFieldsInfo, paymentMethodDataDict, fieldValidityDict); + + expect(result).toBeDefined(); + }); + + it('should return undefined when validity check fails', () => { + const dynamicFieldsInfo = [ + { + pmdMap: 'billing.address.email', + displayName: 'Email', + fieldType: 'Email', + value: undefined, + }, + ]; + const paymentMethodDataDict = { 'billing.address.email': 'invalid' }; + const fieldValidityDict = { 'billing.address.email': false }; + + const result = processAddressFields(dynamicFieldsInfo, paymentMethodDataDict, fieldValidityDict); + + expect(result).toBeUndefined(); + }); + }); + + describe('formPaymentMethodData', () => { + it('should form payment method data with required fields', () => { + const requiredFields = { + payoutMethodData: [ + { + pmdMap: 'payout_method_data.card.card_number', + displayName: 'user_card_number', + fieldType: 'CardNumber', + value: undefined, + }, + ], + address: undefined, + }; + const paymentMethodDataDict = { 'card.cardNumber': '4111111111111111' }; + const fieldValidityDict = { 'card.cardNumber': true }; + + const result = formPaymentMethodData(paymentMethodDataDict, fieldValidityDict, requiredFields); + + expect(result).toBeDefined(); + }); + + it('should return empty array when no valid data', () => { + const requiredFields = { + payoutMethodData: [], + address: undefined, + }; + + const result = formPaymentMethodData({}, {}, requiredFields); + + expect(result).toEqual([]); + }); + }); + + describe('formBody', () => { + it('should form body for PayoutLinkInitiate flow', () => { + const paymentMethodData = [ + { TAG: 'Card', _0: 'Credit' }, + [ + [{ _0: { pmdMap: 'card.cardNumber' } }, '4111111111111111'], + ], + ]; + + const result = formBody('PayoutLinkInitiate', paymentMethodData); + + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + expect(result.some((item: any) => item[0] === 'payout_type')).toBe(true); + }); + + it('should form body for other flow types', () => { + const paymentMethodData = [ + { TAG: 'Card', _0: 'Credit' }, + [ + [{ _0: { pmdMap: 'card.cardNumber' } }, '4111111111111111'], + ], + ]; + + const result = formBody('OtherFlow', paymentMethodData); + + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + expect(result.some((item: any) => item[0] === 'payment_method')).toBe(true); + expect(result.some((item: any) => item[0] === 'payment_method_type')).toBe(true); + }); + + it('should add browser info for BankRedirect', () => { + const paymentMethodData = [ + { TAG: 'BankRedirect', _0: 'Interac' }, + [], + ]; + + const result = formBody('PayoutLinkInitiate', paymentMethodData); + + expect(result).toBeDefined(); + }); + }); + + describe('getPayoutDynamicFields', () => { + const enabledPaymentMethodsWithDynamicFields = [ + { TAG: 'Card', _0: ['Credit', { address: undefined, payoutMethodData: [] }] }, + { TAG: 'BankTransfer', _0: ['ACH', { address: undefined, payoutMethodData: [] }] }, + { TAG: 'Wallet', _0: ['Paypal', { address: undefined, payoutMethodData: [] }] }, + ]; + + it('should return fields for Card payment method type', () => { + const result = getPayoutDynamicFields(enabledPaymentMethodsWithDynamicFields, { TAG: 'Card', _0: 'Credit' }); + + expect(result).toBeDefined(); + }); + + it('should return fields for ACH bank transfer', () => { + const result = getPayoutDynamicFields(enabledPaymentMethodsWithDynamicFields, { TAG: 'BankTransfer', _0: 'ACH' }); + + expect(result).toBeDefined(); + }); + + it('should return fields for Paypal wallet', () => { + const result = getPayoutDynamicFields(enabledPaymentMethodsWithDynamicFields, { TAG: 'Wallet', _0: 'Paypal' }); + + expect(result).toBeDefined(); + }); + + it('should return undefined for non-matching payment method type', () => { + const result = getPayoutDynamicFields(enabledPaymentMethodsWithDynamicFields, { TAG: 'BankTransfer', _0: 'Sepa' }); + + expect(result).toBeUndefined(); + }); + }); + + describe('getDefaultsAndValidity', () => { + it('should return defaults and validity for payout fields', () => { + const payoutDynamicFields = { + address: [ + { + pmdMap: 'billing.address.email', + displayName: 'Email', + fieldType: 'Email', + value: 'test@example.com', + }, + ], + payoutMethodData: [ + { + pmdMap: 'payout_method_data.card.card_number', + displayName: 'user_card_number', + fieldType: 'CardNumber', + value: '4111111111111111', + }, + ], + }; + + const result = getDefaultsAndValidity(payoutDynamicFields, undefined); + + expect(result).toBeDefined(); + }); + + it('should handle payout dynamic fields with empty address', () => { + const result = getDefaultsAndValidity({ address: undefined, payoutMethodData: [] }, undefined); + + expect(result).toBeUndefined(); + }); + + it('should handle payout dynamic fields with empty payout method data', () => { + const result = getDefaultsAndValidity({ address: undefined, payoutMethodData: [] }, undefined); + + expect(result).toBeUndefined(); + }); + }); + + describe('itemToObjMapper', () => { + it('should map dict to options object', () => { + const dict = { + linkId: 'link_123', + payoutId: 'payout_123', + customerId: 'cust_123', + theme: '#1A1A1A', + collectorName: 'TestCollector', + logo: 'https://example.com/logo.png', + amount: '100.00', + currency: 'USD', + flow: 'PayoutLinkInitiate', + sessionExpiry: '2024-12-31', + formLayout: 'Tabs', + }; + + const result = itemToObjMapper(dict); + + expect(result.linkId).toBe('link_123'); + expect(result.payoutId).toBe('payout_123'); + expect(result.customerId).toBe('cust_123'); + expect(result.theme).toBe('#1A1A1A'); + expect(result.collectorName).toBe('TestCollector'); + }); + + it('should use defaults for missing fields', () => { + const result = itemToObjMapper({}); + + expect(result.linkId).toBe(''); + expect(result.payoutId).toBe(''); + expect(result.amount).toBe('0.01'); + expect(result.currency).toBe('EUR'); + expect(result.flow).toBe('PayoutLinkInitiate'); + expect(result.formLayout).toBe('Tabs'); + }); + + it('should handle enabledPaymentMethods array', () => { + const dict = { + enabledPaymentMethods: [], + }; + + const result = itemToObjMapper(dict); + + expect(result.enabledPaymentMethods).toBeDefined(); + }); + }); + + describe('defaultDynamicPmdFields', () => { + it('should return card fields for Card type', () => { + const result = defaultDynamicPmdFields({ TAG: 'Card', _0: 'Credit' }); + + expect(Array.isArray(result)).toBe(true); + }); + + it('should return interac fields for BankRedirect type', () => { + const result = defaultDynamicPmdFields({ TAG: 'BankRedirect', _0: 'Interac' }); + + expect(Array.isArray(result)).toBe(true); + }); + + it('should return ACH fields for ACH BankTransfer type', () => { + const result = defaultDynamicPmdFields({ TAG: 'BankTransfer', _0: 'ACH' }); + + expect(Array.isArray(result)).toBe(true); + }); + + it('should return Bacs fields for Bacs BankTransfer type', () => { + const result = defaultDynamicPmdFields({ TAG: 'BankTransfer', _0: 'Bacs' }); + + expect(Array.isArray(result)).toBe(true); + }); + + it('should return Pix fields for Pix BankTransfer type', () => { + const result = defaultDynamicPmdFields({ TAG: 'BankTransfer', _0: 'Pix' }); + + expect(Array.isArray(result)).toBe(true); + }); + + it('should return SEPA fields for Sepa BankTransfer type', () => { + const result = defaultDynamicPmdFields({ TAG: 'BankTransfer', _0: 'Sepa' }); + + expect(Array.isArray(result)).toBe(true); + }); + + it('should return Paypal fields for Paypal Wallet type', () => { + const result = defaultDynamicPmdFields({ TAG: 'Wallet', _0: 'Paypal' }); + + expect(Array.isArray(result)).toBe(true); + }); + + it('should return empty array for Venmo Wallet type', () => { + const result = defaultDynamicPmdFields({ TAG: 'Wallet', _0: 'Venmo' }); + + expect(Array.isArray(result)).toBe(true); + }); + }); + + describe('defaultPayoutDynamicFields', () => { + it('should return fields with address and payoutMethodData', () => { + const result = defaultPayoutDynamicFields({ TAG: 'Card', _0: 'Credit' }); + + expect(result).toHaveProperty('address'); + expect(result).toHaveProperty('payoutMethodData'); + }); + + it('should use default payment method type when not provided', () => { + const result = defaultPayoutDynamicFields(undefined); + + expect(result).toHaveProperty('address'); + expect(result).toHaveProperty('payoutMethodData'); + }); + }); + + describe('field constants', () => { + it('should have correct defaultCardFields', () => { + expect(Array.isArray(defaultCardFields)).toBe(true); + expect(defaultCardFields.length).toBe(3); + }); + + it('should have correct defaultAchFields', () => { + expect(Array.isArray(defaultAchFields)).toBe(true); + expect(defaultAchFields.length).toBe(2); + }); + + it('should have correct defaultBacsFields', () => { + expect(Array.isArray(defaultBacsFields)).toBe(true); + expect(defaultBacsFields.length).toBe(2); + }); + + it('should have correct defaultPixTransferFields', () => { + expect(Array.isArray(defaultPixTransferFields)).toBe(true); + expect(defaultPixTransferFields.length).toBe(2); + }); + + it('should have correct defaultSepaFields', () => { + expect(Array.isArray(defaultSepaFields)).toBe(true); + expect(defaultSepaFields.length).toBe(1); + }); + + it('should have correct defaultPaypalFields', () => { + expect(Array.isArray(defaultPaypalFields)).toBe(true); + expect(defaultPaypalFields.length).toBe(1); + }); + + it('should have correct defaultInteracFields', () => { + expect(Array.isArray(defaultInteracFields)).toBe(true); + expect(defaultInteracFields.length).toBe(1); + }); + + it('should have correct defaultEnabledPaymentMethods', () => { + expect(Array.isArray(defaultEnabledPaymentMethods)).toBe(true); + expect(defaultEnabledPaymentMethods.length).toBeGreaterThan(0); + }); + + it('should have correct defaultEnabledPaymentMethodsWithDynamicFields', () => { + expect(Array.isArray(defaultEnabledPaymentMethodsWithDynamicFields)).toBe(true); + expect(defaultEnabledPaymentMethodsWithDynamicFields.length).toBeGreaterThan(0); + }); + + it('should have correct defaultPaymentMethodCollectOptions', () => { + expect(defaultPaymentMethodCollectOptions).toHaveProperty('enabledPaymentMethods'); + expect(defaultPaymentMethodCollectOptions).toHaveProperty('linkId'); + expect(defaultPaymentMethodCollectOptions).toHaveProperty('amount'); + expect(defaultPaymentMethodCollectOptions).toHaveProperty('currency'); + }); + + it('should have correct defaultStatusInfo', () => { + expect(defaultStatusInfo).toHaveProperty('status'); + expect(defaultStatusInfo).toHaveProperty('payoutId'); + expect(defaultStatusInfo).toHaveProperty('message'); + }); + }); + + describe('icon functions', () => { + describe('getPaymentMethodIcon', () => { + it('should return React element for Card', () => { + const result = getPaymentMethodIcon('Card'); + expect(React.isValidElement(result)).toBe(true); + }); + + it('should return React element for BankRedirect', () => { + const result = getPaymentMethodIcon('BankRedirect'); + expect(React.isValidElement(result)).toBe(true); + }); + + it('should return React element for BankTransfer', () => { + const result = getPaymentMethodIcon('BankTransfer'); + expect(React.isValidElement(result)).toBe(true); + }); + + it('should return React element for Wallet', () => { + const result = getPaymentMethodIcon('Wallet'); + expect(React.isValidElement(result)).toBe(true); + }); + }); + + describe('getBankTransferIcon', () => { + it('should return React element for ACH', () => { + const result = getBankTransferIcon('ACH'); + expect(React.isValidElement(result)).toBe(true); + }); + + it('should return React element for Pix', () => { + const result = getBankTransferIcon('Pix'); + expect(React.isValidElement(result)).toBe(true); + }); + + it('should return React element for Bacs', () => { + const result = getBankTransferIcon('Bacs'); + expect(React.isValidElement(result)).toBe(true); + }); + + it('should return React element for Sepa', () => { + const result = getBankTransferIcon('Sepa'); + expect(React.isValidElement(result)).toBe(true); + }); + }); + + describe('getWalletIcon', () => { + it('should return React element for Paypal', () => { + const result = getWalletIcon('Paypal'); + expect(React.isValidElement(result)).toBe(true); + }); + + it('should return React element for Venmo', () => { + const result = getWalletIcon('Venmo'); + expect(React.isValidElement(result)).toBe(true); + }); + }); + + describe('getBankRedirectIcon', () => { + it('should return React element', () => { + const result = getBankRedirectIcon('Interac'); + expect(React.isValidElement(result)).toBe(true); + }); + }); + + describe('getPaymentMethodTypeIcon', () => { + it('should return React element for Card type', () => { + const result = getPaymentMethodTypeIcon({ TAG: 'Card', _0: 'Credit' }); + expect(React.isValidElement(result)).toBe(true); + }); + + it('should return React element for BankRedirect type', () => { + const result = getPaymentMethodTypeIcon({ TAG: 'BankRedirect', _0: 'Interac' }); + expect(React.isValidElement(result)).toBe(true); + }); + + it('should return React element for BankTransfer type', () => { + const result = getPaymentMethodTypeIcon({ TAG: 'BankTransfer', _0: 'ACH' }); + expect(React.isValidElement(result)).toBe(true); + }); + + it('should return React element for Wallet type', () => { + const result = getPaymentMethodTypeIcon({ TAG: 'Wallet', _0: 'Paypal' }); + expect(React.isValidElement(result)).toBe(true); + }); + }); + }); + + describe('calculateValidity additional cases', () => { + it('should validate card holder name with space', () => { + const result = calculateValidity( + { TAG: 'PayoutMethodData', _0: 'CardHolderName' }, + 'John Doe', + '', + undefined + ); + expect(result).toBe(true); + }); + + it('should invalidate card holder name without space', () => { + const result = calculateValidity( + { TAG: 'PayoutMethodData', _0: 'CardHolderName' }, + 'John', + '', + undefined + ); + expect(result).toBe(false); + }); + + it('should return default for empty card holder name', () => { + const result = calculateValidity( + { TAG: 'PayoutMethodData', _0: 'CardHolderName' }, + '', + '', + true + ); + expect(result).toBe(true); + }); + + it('should validate ACH routing number', () => { + const result = calculateValidity( + { TAG: 'PayoutMethodData', _0: 'ACHRoutingNumber' }, + '123456780', + '', + undefined + ); + expect(result).toBe(true); + }); + + it('should invalidate short ACH routing number', () => { + const result = calculateValidity( + { TAG: 'PayoutMethodData', _0: 'ACHRoutingNumber' }, + '12345', + '', + undefined + ); + expect(result).toBe(false); + }); + + it('should validate SEPA IBAN', () => { + const result = calculateValidity( + { TAG: 'PayoutMethodData', _0: 'SepaIban' }, + 'DE89370400440532013000', + '', + undefined + ); + expect(result).toBe(true); + }); + + it('should invalidate short SEPA IBAN', () => { + const result = calculateValidity( + { TAG: 'PayoutMethodData', _0: 'SepaIban' }, + 'DE89', + '', + undefined + ); + expect(result).toBe(false); + }); + + it('should validate SEPA BIC', () => { + const result = calculateValidity( + { TAG: 'PayoutMethodData', _0: 'SepaBic' }, + 'DEUTDEFF', + '', + undefined + ); + expect(result).toBe(true); + }); + + it('should validate email', () => { + const result = calculateValidity( + { TAG: 'PayoutMethodData', _0: 'PaypalMail' }, + 'test@example.com', + '', + undefined + ); + expect(result).toBe(true); + }); + + it('should invalidate invalid email', () => { + const result = calculateValidity( + { TAG: 'PayoutMethodData', _0: 'PaypalMail' }, + 'invalid-email', + '', + undefined + ); + expect(result).toBe(false); + }); + + it('should return default for empty email', () => { + const result = calculateValidity( + { TAG: 'PayoutMethodData', _0: 'PaypalMail' }, + '', + '', + true + ); + expect(result).toBe(true); + }); + + it('should validate expiry date', () => { + const result = calculateValidity( + { TAG: 'PayoutMethodData', _0: { TAG: 'CardExpDate', _0: 'CardExpMonth' } }, + '12/25', + '', + undefined + ); + expect(typeof result).toBe('boolean'); + }); + + it('should validate billing email', () => { + const result = calculateValidity( + { TAG: 'BillingAddress', _0: 'Email' }, + 'test@example.com', + '', + undefined + ); + expect(result).toBe(true); + }); + + it('should invalidate invalid billing email', () => { + const result = calculateValidity( + { TAG: 'BillingAddress', _0: 'Email' }, + 'invalid', + '', + undefined + ); + expect(result).toBe(false); + }); + + it('should return default for empty billing email', () => { + const result = calculateValidity( + { TAG: 'BillingAddress', _0: 'Email' }, + '', + '', + true + ); + expect(result).toBe(true); + }); + + it('should validate non-empty field value', () => { + const result = calculateValidity( + { TAG: 'PayoutMethodData', _0: 'ACHBankName' }, + 'Test Bank', + '', + undefined + ); + expect(result).toBe(true); + }); + + it('should invalidate empty field value', () => { + const result = calculateValidity( + { TAG: 'PayoutMethodData', _0: 'ACHBankName' }, + '', + '', + undefined + ); + expect(result).toBe(false); + }); + }); + + describe('getPaymentMethodDataErrorString additional cases', () => { + it('should return error for invalid routing number', () => { + const key = { TAG: 'PayoutMethodData', _0: 'ACHRoutingNumber' }; + expect(getPaymentMethodDataErrorString(key, '12345', mockLocaleString)).toBe('Invalid routing number'); + }); + + it('should return empty for valid complete routing number', () => { + const key = { TAG: 'PayoutMethodData', _0: 'ACHRoutingNumber' }; + expect(getPaymentMethodDataErrorString(key, '123456780', mockLocaleString)).toBe(''); + }); + + it('should return error for empty sort code', () => { + const key = { TAG: 'PayoutMethodData', _0: 'BacsSortCode' }; + expect(getPaymentMethodDataErrorString(key, '', mockLocaleString)).toBe('Sort Code is required'); + }); + + it('should return error for invalid sort code', () => { + const key = { TAG: 'PayoutMethodData', _0: 'BacsSortCode' }; + expect(getPaymentMethodDataErrorString(key, 'abc', mockLocaleString)).toBe('Invalid sort code'); + }); + + it('should return error for empty BACS account number', () => { + const key = { TAG: 'PayoutMethodData', _0: 'BacsAccountNumber' }; + expect(getPaymentMethodDataErrorString(key, '', mockLocaleString)).toBe('Account Number is required'); + }); + + it('should return error for invalid BACS account number', () => { + const key = { TAG: 'PayoutMethodData', _0: 'BacsAccountNumber' }; + expect(getPaymentMethodDataErrorString(key, 'abc', mockLocaleString)).toBe('Invalid account number'); + }); + + it('should return error for empty SEPA IBAN', () => { + const key = { TAG: 'PayoutMethodData', _0: 'SepaIban' }; + expect(getPaymentMethodDataErrorString(key, '', mockLocaleString)).toBe('IBAN is required'); + }); + + it('should return error for invalid SEPA IBAN', () => { + const key = { TAG: 'PayoutMethodData', _0: 'SepaIban' }; + expect(getPaymentMethodDataErrorString(key, 'invalid', mockLocaleString)).toBe('Invalid IBAN'); + }); + + it('should return error for phone number', () => { + const key = { TAG: 'BillingAddress', _0: 'PhoneNumber' }; + expect(getPaymentMethodDataErrorString(key, '', mockLocaleString)).toBe('Phone Number is required'); + }); + + it('should return error for country code', () => { + const key = { TAG: 'BillingAddress', _0: 'CountryCode' }; + expect(getPaymentMethodDataErrorString(key, '', mockLocaleString)).toBe('Country Code is required'); + }); + + it('should return error for address line 1', () => { + const key = { TAG: 'BillingAddress', _0: 'AddressLine1' }; + expect(getPaymentMethodDataErrorString(key, '', mockLocaleString)).toBe('Address line 1 is required'); + }); + + it('should return error for address line 2', () => { + const key = { TAG: 'BillingAddress', _0: 'AddressLine2' }; + expect(getPaymentMethodDataErrorString(key, '', mockLocaleString)).toBe('Address line 2 is required'); + }); + + it('should return error for city', () => { + const key = { TAG: 'BillingAddress', _0: 'AddressCity' }; + expect(getPaymentMethodDataErrorString(key, '', mockLocaleString)).toBe('City is required'); + }); + + it('should return error for state', () => { + const key = { TAG: 'BillingAddress', _0: 'AddressState' }; + expect(getPaymentMethodDataErrorString(key, '', mockLocaleString)).toBe('State is required'); + }); + + it('should return error for empty postal code', () => { + const key = { TAG: 'BillingAddress', _0: 'AddressPincode' }; + expect(getPaymentMethodDataErrorString(key, '', mockLocaleString)).toBe('Postal code is required'); + }); + + it('should return error for invalid postal code', () => { + const key = { TAG: 'BillingAddress', _0: 'AddressPincode' }; + expect(getPaymentMethodDataErrorString(key, '@#$', mockLocaleString)).toBe('Invalid postal code'); + }); + + it('should return error for card holder name empty', () => { + const key = { TAG: 'PayoutMethodData', _0: 'CardHolderName' }; + expect(getPaymentMethodDataErrorString(key, '', mockLocaleString)).toBe('Card Holder Name is required'); + }); + + it('should return error for card holder name incomplete', () => { + const key = { TAG: 'PayoutMethodData', _0: 'CardHolderName' }; + expect(getPaymentMethodDataErrorString(key, 'John', mockLocaleString)).toBe('Card Holder Name is incomplete'); + }); + + it('should return error for interac email empty', () => { + const key = { TAG: 'PayoutMethodData', _0: 'InteracEmail' }; + expect(getPaymentMethodDataErrorString(key, '', mockLocaleString)).toBe('Email is required'); + }); + + it('should return error for invalid interac email', () => { + const key = { TAG: 'PayoutMethodData', _0: 'InteracEmail' }; + expect(getPaymentMethodDataErrorString(key, 'invalid', mockLocaleString)).toBe('Invalid email'); + }); + + it('should return empty string for unknown field', () => { + const key = { TAG: 'PayoutMethodData', _0: 'ACHBankName' }; + expect(getPaymentMethodDataErrorString(key, '', mockLocaleString)).toBe(''); + }); + }); + + describe('getPayoutStatusString additional cases', () => { + it('should return cancelled for Expired', () => { + expect(getPayoutStatusString('Expired')).toBe('cancelled'); + }); + + it('should return reversed for Reversed', () => { + expect(getPayoutStatusString('Reversed')).toBe('reversed'); + }); + + it('should return ineligible for Ineligible', () => { + expect(getPayoutStatusString('Ineligible')).toBe('ineligible'); + }); + + it('should return requires_creation for RequiresCreation', () => { + expect(getPayoutStatusString('RequiresCreation')).toBe('requires_creation'); + }); + + it('should return requires_confirmation for RequiresConfirmation', () => { + expect(getPayoutStatusString('RequiresConfirmation')).toBe('requires_confirmation'); + }); + + it('should return requires_payout_method_data for RequiresPayoutMethodData', () => { + expect(getPayoutStatusString('RequiresPayoutMethodData')).toBe('requires_payout_method_data'); + }); + + it('should return requires_fulfillment for RequiresFulfillment', () => { + expect(getPayoutStatusString('RequiresFulfillment')).toBe('requires_fulfillment'); + }); + + it('should return requires_vendor_account_creation for RequiresVendorAccountCreation', () => { + expect(getPayoutStatusString('RequiresVendorAccountCreation')).toBe('requires_vendor_account_creation'); + }); + }); +}); diff --git a/src/__tests__/PaymentMethodsRecord.test.ts b/src/__tests__/PaymentMethodsRecord.test.ts new file mode 100644 index 000000000..453ed0cb4 --- /dev/null +++ b/src/__tests__/PaymentMethodsRecord.test.ts @@ -0,0 +1,686 @@ +import { + getPaymentMethodsFieldsOrder, + sortPaymentMethodFields, + getPaymentMethodsFieldTypeFromString, + getIsBillingField, + getIsAnyBillingDetailEmpty, + getPaymentExperienceType, + paymentTypeMapper, + paymentTypeToStringMapper, + getCardNetworks, + getBankNames, + getAchConnectors, + getAmountDetails, + getInstallmentPlan, + getOptionalMandateType, + getMandate, + getIntentData, + getSurchargeDetails, + getFieldType, + getPaymentMethodsFieldTypeFromDict, + getPaymentMethodTypeFromList, + getCardNetwork, + defaultCardNetworks, + defaultMethods, + defaultPaymentMethodType, + defaultList, + defaultIntentData, +} from '../Payments/PaymentMethodsRecord.bs.js'; + +describe('PaymentMethodsRecord', () => { + describe('getPaymentMethodsFieldsOrder', () => { + it('should return 0 for CardNumber', () => { + expect(getPaymentMethodsFieldsOrder('CardNumber')).toBe(0); + }); + + it('should return 1 for CardExpiryMonth', () => { + expect(getPaymentMethodsFieldsOrder('CardExpiryMonth')).toBe(1); + }); + + it('should return 1 for CardExpiryYear', () => { + expect(getPaymentMethodsFieldsOrder('CardExpiryYear')).toBe(1); + }); + + it('should return 1 for CardExpiryMonthAndYear', () => { + expect(getPaymentMethodsFieldsOrder('CardExpiryMonthAndYear')).toBe(1); + }); + + it('should return 2 for CardCvc', () => { + expect(getPaymentMethodsFieldsOrder('CardCvc')).toBe(2); + }); + + it('should return 2 for CardExpiryAndCvc', () => { + expect(getPaymentMethodsFieldsOrder('CardExpiryAndCvc')).toBe(2); + }); + + it('should return 4 for AddressLine1', () => { + expect(getPaymentMethodsFieldsOrder('AddressLine1')).toBe(4); + }); + + it('should return 5 for AddressLine2', () => { + expect(getPaymentMethodsFieldsOrder('AddressLine2')).toBe(5); + }); + + it('should return 6 for AddressCity', () => { + expect(getPaymentMethodsFieldsOrder('AddressCity')).toBe(6); + }); + + it('should return 7 for AddressState', () => { + expect(getPaymentMethodsFieldsOrder('AddressState')).toBe(7); + }); + + it('should return 7 for StateAndCity', () => { + expect(getPaymentMethodsFieldsOrder('StateAndCity')).toBe(7); + }); + + it('should return 99 for InfoElement', () => { + expect(getPaymentMethodsFieldsOrder('InfoElement')).toBe(99); + }); + + it('should return 9 for AddressPincode', () => { + expect(getPaymentMethodsFieldsOrder('AddressPincode')).toBe(9); + }); + + it('should return 9 for PixCPF', () => { + expect(getPaymentMethodsFieldsOrder('PixCPF')).toBe(9); + }); + + it('should return 10 for PixCNPJ', () => { + expect(getPaymentMethodsFieldsOrder('PixCNPJ')).toBe(10); + }); + + it('should return 3 for Email', () => { + expect(getPaymentMethodsFieldsOrder('Email')).toBe(3); + }); + + it('should return 8 for CountryAndPincode object', () => { + expect(getPaymentMethodsFieldsOrder({ TAG: 'CountryAndPincode', _0: 'test' })).toBe(8); + }); + + it('should return 8 for AddressCountry object', () => { + expect(getPaymentMethodsFieldsOrder({ TAG: 'AddressCountry', _0: [] })).toBe(8); + }); + + it('should return 3 for unknown string field', () => { + expect(getPaymentMethodsFieldsOrder('UnknownField')).toBe(3); + }); + }); + + describe('sortPaymentMethodFields', () => { + it('should return negative when first field has lower order', () => { + const result = sortPaymentMethodFields('CardNumber', 'CardCvc'); + expect(result).toBeLessThan(0); + }); + + it('should return positive when first field has higher order', () => { + const result = sortPaymentMethodFields('CardCvc', 'CardNumber'); + expect(result).toBeGreaterThan(0); + }); + + it('should return 0 when fields have same order', () => { + const result = sortPaymentMethodFields('CardExpiryMonth', 'CardExpiryYear'); + expect(result).toBe(0); + }); + }); + + describe('getPaymentMethodsFieldTypeFromString', () => { + it('should return "AddressCity" for "user_address_city"', () => { + expect(getPaymentMethodsFieldTypeFromString('user_address_city', false)).toBe('AddressCity'); + }); + + it('should return "AddressLine1" for "user_address_line1"', () => { + expect(getPaymentMethodsFieldTypeFromString('user_address_line1', false)).toBe('AddressLine1'); + }); + + it('should return "Bank" for "user_bank"', () => { + expect(getPaymentMethodsFieldTypeFromString('user_bank', false)).toBe('Bank'); + }); + + it('should return "Email" for "user_email_address"', () => { + expect(getPaymentMethodsFieldTypeFromString('user_email_address', false)).toBe('Email'); + }); + + it('should return "PhoneNumber" for "user_phone_number"', () => { + expect(getPaymentMethodsFieldTypeFromString('user_phone_number', false)).toBe('PhoneNumber'); + }); + + it('should return "PixKey" for "user_pix_key"', () => { + expect(getPaymentMethodsFieldTypeFromString('user_pix_key', false)).toBe('PixKey'); + }); + + it('should return "CardNumber" for "user_card_number" when isBancontact is true', () => { + expect(getPaymentMethodsFieldTypeFromString('user_card_number', true)).toBe('CardNumber'); + }); + + it('should return "None" for "user_card_number" when isBancontact is false', () => { + expect(getPaymentMethodsFieldTypeFromString('user_card_number', false)).toBe('None'); + }); + + it('should return "CardCvc" for "user_card_cvc" when isBancontact is true', () => { + expect(getPaymentMethodsFieldTypeFromString('user_card_cvc', true)).toBe('CardCvc'); + }); + + it('should return "None" for "user_card_cvc" when isBancontact is false', () => { + expect(getPaymentMethodsFieldTypeFromString('user_card_cvc', false)).toBe('None'); + }); + + it('should return "None" for unknown field type', () => { + expect(getPaymentMethodsFieldTypeFromString('unknown_field', false)).toBe('None'); + }); + + it('should return "VpaId" for "user_vpa_id"', () => { + expect(getPaymentMethodsFieldTypeFromString('user_vpa_id', false)).toBe('VpaId'); + }); + + it('should return "BankAccountNumber" for "user_iban"', () => { + expect(getPaymentMethodsFieldTypeFromString('user_iban', false)).toBe('BankAccountNumber'); + }); + + it('should return "CryptoCurrencyNetworks" for "user_crypto_currency_network"', () => { + expect(getPaymentMethodsFieldTypeFromString('user_crypto_currency_network', false)).toBe('CryptoCurrencyNetworks'); + }); + }); + + describe('getIsBillingField', () => { + it('should return true for AddressLine1', () => { + expect(getIsBillingField('AddressLine1')).toBe(true); + }); + + it('should return true for AddressLine2', () => { + expect(getIsBillingField('AddressLine2')).toBe(true); + }); + + it('should return true for AddressCity', () => { + expect(getIsBillingField('AddressCity')).toBe(true); + }); + + it('should return true for AddressPincode', () => { + expect(getIsBillingField('AddressPincode')).toBe(true); + }); + + it('should return true for AddressState', () => { + expect(getIsBillingField('AddressState')).toBe(true); + }); + + it('should return true for AddressCountry object', () => { + expect(getIsBillingField({ TAG: 'AddressCountry', _0: [] })).toBe(true); + }); + + it('should return false for non-billing fields', () => { + expect(getIsBillingField('Email')).toBe(false); + }); + + it('should return false for CardNumber', () => { + expect(getIsBillingField('CardNumber')).toBe(false); + }); + + it('should return false for ShippingAddressCountry object', () => { + expect(getIsBillingField({ TAG: 'ShippingAddressCountry', _0: [] })).toBe(false); + }); + }); + + describe('getIsAnyBillingDetailEmpty', () => { + it('should return true when a billing field has empty value', () => { + const requiredFields = [ + { field_type: 'AddressLine1', value: '', display_name: 'address_line1' }, + { field_type: 'Email', value: 'test@example.com', display_name: 'email' }, + ]; + expect(getIsAnyBillingDetailEmpty(requiredFields)).toBe(true); + }); + + it('should return false when all billing fields have values', () => { + const requiredFields = [ + { field_type: 'AddressLine1', value: '123 Main St', display_name: 'address_line1' }, + { field_type: 'Email', value: 'test@example.com', display_name: 'email' }, + ]; + expect(getIsAnyBillingDetailEmpty(requiredFields)).toBe(false); + }); + + it('should return false when no billing fields are present', () => { + const requiredFields = [ + { field_type: 'Email', value: '', display_name: 'email' }, + { field_type: 'PhoneNumber', value: '', display_name: 'phone' }, + ]; + expect(getIsAnyBillingDetailEmpty(requiredFields)).toBe(false); + }); + + it('should return false for empty array', () => { + expect(getIsAnyBillingDetailEmpty([])).toBe(false); + }); + }); + + describe('getPaymentExperienceType', () => { + it('should return "QrFlow" for "display_qr_code"', () => { + expect(getPaymentExperienceType('display_qr_code')).toBe('QrFlow'); + }); + + it('should return "InvokeSDK" for "invoke_sdk_client"', () => { + expect(getPaymentExperienceType('invoke_sdk_client')).toBe('InvokeSDK'); + }); + + it('should return "RedirectToURL" for unknown type', () => { + expect(getPaymentExperienceType('unknown')).toBe('RedirectToURL'); + }); + + it('should return "RedirectToURL" for empty string', () => { + expect(getPaymentExperienceType('')).toBe('RedirectToURL'); + }); + }); + + describe('paymentTypeMapper', () => { + it('should return "NEW_MANDATE" for "new_mandate"', () => { + expect(paymentTypeMapper('new_mandate')).toBe('NEW_MANDATE'); + }); + + it('should return "NORMAL" for "normal"', () => { + expect(paymentTypeMapper('normal')).toBe('NORMAL'); + }); + + it('should return "SETUP_MANDATE" for "setup_mandate"', () => { + expect(paymentTypeMapper('setup_mandate')).toBe('SETUP_MANDATE'); + }); + + it('should return "NONE" for unknown type', () => { + expect(paymentTypeMapper('unknown')).toBe('NONE'); + }); + + it('should return "NONE" for empty string', () => { + expect(paymentTypeMapper('')).toBe('NONE'); + }); + }); + + describe('paymentTypeToStringMapper', () => { + it('should return "normal" for "NORMAL"', () => { + expect(paymentTypeToStringMapper('NORMAL')).toBe('normal'); + }); + + it('should return "new_mandate" for "NEW_MANDATE"', () => { + expect(paymentTypeToStringMapper('NEW_MANDATE')).toBe('new_mandate'); + }); + + it('should return "setup_mandate" for "SETUP_MANDATE"', () => { + expect(paymentTypeToStringMapper('SETUP_MANDATE')).toBe('setup_mandate'); + }); + + it('should return "" for "NONE"', () => { + expect(paymentTypeToStringMapper('NONE')).toBe(''); + }); + }); + + describe('getAmountDetails', () => { + it('should extract amount details from dict', () => { + const dict = { + amount_per_installment: 50.0, + total_amount: 150.0, + }; + const result = getAmountDetails(dict); + expect(result.amount_per_installment).toBe(50.0); + expect(result.total_amount).toBe(150.0); + }); + + it('should return 0 for missing fields', () => { + const result = getAmountDetails({}); + expect(result.amount_per_installment).toBe(0); + expect(result.total_amount).toBe(0); + }); + }); + + describe('getInstallmentPlan', () => { + it('should extract installment plan from dict', () => { + const dict = { + interest_rate: 5.0, + number_of_installments: 3, + billing_frequency: 'MONTHLY', + amount_details: { + amount_per_installment: 100.0, + total_amount: 300.0, + }, + }; + const result = getInstallmentPlan(dict); + expect(result.interest_rate).toBe(5.0); + expect(result.number_of_installments).toBe(3); + expect(result.billing_frequency).toBe('MONTHLY'); + expect(result.amount_details.amount_per_installment).toBe(100.0); + expect(result.amount_details.total_amount).toBe(300.0); + }); + + it('should handle missing fields with defaults', () => { + const result = getInstallmentPlan({}); + expect(result.interest_rate).toBe(0); + expect(result.number_of_installments).toBe(0); + expect(result.billing_frequency).toBe(''); + }); + }); + + describe('getOptionalMandateType', () => { + it('should extract mandate type from dict', () => { + const dict = { + single_use: { + amount: 100, + currency: 'USD', + }, + }; + const result = getOptionalMandateType(dict, 'single_use'); + expect(result).toBeDefined(); + if (result !== undefined) { + expect(result.amount).toBe(100); + expect(result.currency).toBe('USD'); + } + }); + + it('should return undefined for missing key', () => { + const result = getOptionalMandateType({}, 'single_use'); + expect(result).toBeUndefined(); + }); + }); + + describe('getMandate', () => { + it('should extract mandate from dict', () => { + const dict = { + mandate_payment: { + single_use: { + amount: 100, + currency: 'USD', + }, + multi_use: { + amount: 500, + currency: 'EUR', + }, + }, + }; + const result = getMandate(dict, 'mandate_payment'); + expect(result).toBeDefined(); + }); + + it('should return undefined for missing key', () => { + const result = getMandate({}, 'mandate_payment'); + expect(result).toBeUndefined(); + }); + }); + + describe('getIntentData', () => { + it('should extract intent data from dict', () => { + const dict = { + currency: 'USD', + intent_data: { + installment_options: [], + }, + }; + const result = getIntentData(dict); + expect(result.currency).toBe('USD'); + }); + + it('should handle missing fields with defaults', () => { + const result = getIntentData({}); + expect(result.currency).toBe(''); + }); + }); + + describe('getSurchargeDetails', () => { + it('should extract surcharge details when displayTotalSurchargeAmount is non-zero', () => { + const dict = { + surcharge_details: { + display_total_surcharge_amount: 10.5, + }, + }; + const result = getSurchargeDetails(dict); + expect(result).toBeDefined(); + expect(result?.displayTotalSurchargeAmount).toBe(10.5); + }); + + it('should return undefined when displayTotalSurchargeAmount is 0', () => { + const dict = { + surcharge_details: { + display_total_surcharge_amount: 0.0, + }, + }; + const result = getSurchargeDetails(dict); + expect(result).toBeUndefined(); + }); + + it('should return undefined when surcharge_details is missing', () => { + const result = getSurchargeDetails({}); + expect(result).toBeUndefined(); + }); + }); + + describe('defaultCardNetworks', () => { + it('should have card_network "NOTFOUND"', () => { + expect(defaultCardNetworks.card_network).toBe('NOTFOUND'); + }); + + it('should have empty eligible_connectors', () => { + expect(defaultCardNetworks.eligible_connectors).toEqual([]); + }); + }); + + describe('defaultMethods', () => { + it('should have payment_method "card"', () => { + expect(defaultMethods.payment_method).toBe('card'); + }); + + it('should have empty payment_method_types', () => { + expect(defaultMethods.payment_method_types).toEqual([]); + }); + }); + + describe('defaultPaymentMethodType', () => { + it('should have empty payment_method_type', () => { + expect(defaultPaymentMethodType.payment_method_type).toBe(''); + }); + + it('should have empty arrays for collections', () => { + expect(defaultPaymentMethodType.card_networks).toEqual([]); + expect(defaultPaymentMethodType.bank_names).toEqual([]); + expect(defaultPaymentMethodType.required_fields).toEqual([]); + }); + }); + + describe('defaultList', () => { + it('should have empty redirect_url', () => { + expect(defaultList.redirect_url).toBe(''); + }); + + it('should have empty currency', () => { + expect(defaultList.currency).toBe(''); + }); + + it('should have payment_type "NONE"', () => { + expect(defaultList.payment_type).toBe('NONE'); + }); + + it('should have collect_billing_details_from_wallets true', () => { + expect(defaultList.collect_billing_details_from_wallets).toBe(true); + }); + + it('should have is_tax_calculation_enabled false', () => { + expect(defaultList.is_tax_calculation_enabled).toBe(false); + }); + }); + + describe('defaultIntentData', () => { + it('should have empty currency', () => { + expect(defaultIntentData.currency).toBe(''); + }); + + it('should have undefined installment_options', () => { + expect(defaultIntentData.installment_options).toBeUndefined(); + }); + }); + + describe('getFieldType', () => { + it('should return field type from string field_type', () => { + const dict = { field_type: 'user_email_address' }; + const result = getFieldType(dict, false); + expect(result).toBe('Email'); + }); + + it('should return "None" for non-string field_type', () => { + const dict = { field_type: 123 }; + const result = getFieldType(dict, false); + expect(result).toBe('None'); + }); + + it('should return "None" for missing field_type', () => { + const result = getFieldType({}, false); + expect(result).toBe('None'); + }); + + it('should handle bancontact card number field', () => { + const dict = { field_type: 'user_card_number' }; + const result = getFieldType(dict, true); + expect(result).toBe('CardNumber'); + }); + + it('should return "None" for card fields when not bancontact', () => { + const dict = { field_type: 'user_card_number' }; + const result = getFieldType(dict, false); + expect(result).toBe('None'); + }); + }); + + describe('getPaymentMethodsFieldTypeFromDict', () => { + it('should return LanguagePreference for language_preference key', () => { + const dict = { + language_preference: { + options: ['en', 'fr'], + }, + }; + const result = getPaymentMethodsFieldTypeFromDict(dict); + expect(typeof result).toBe('object'); + if (typeof result === 'object' && result !== null && 'TAG' in result) { + expect(result.TAG).toBe('LanguagePreference'); + } + }); + + it('should return BankList for user_bank_options key', () => { + const dict = { + user_bank_options: { + options: ['bank1', 'bank2'], + }, + }; + const result = getPaymentMethodsFieldTypeFromDict(dict); + expect(typeof result).toBe('object'); + if (typeof result === 'object' && result !== null && 'TAG' in result) { + expect(result.TAG).toBe('BankList'); + } + }); + + it('should return Currency for user_currency key', () => { + const dict = { + user_currency: { + options: ['USD', 'EUR'], + }, + }; + const result = getPaymentMethodsFieldTypeFromDict(dict); + expect(typeof result).toBe('object'); + if (typeof result === 'object' && result !== null && 'TAG' in result) { + expect(result.TAG).toBe('Currency'); + } + }); + + it('should return DocumentType for user_document_type key', () => { + const dict = { + user_document_type: { + options: ['passport', 'id_card'], + }, + }; + const result = getPaymentMethodsFieldTypeFromDict(dict); + expect(typeof result).toBe('object'); + if (typeof result === 'object' && result !== null && 'TAG' in result) { + expect(result.TAG).toBe('DocumentType'); + } + }); + + it('should return "None" for unknown key', () => { + const dict = { unknown_key: {} }; + const result = getPaymentMethodsFieldTypeFromDict(dict); + expect(result).toBe('None'); + }); + }); + + describe('getPaymentMethodTypeFromList', () => { + it('should return payment method type from list', () => { + const paymentMethodListValue = { + payment_methods: [ + { + payment_method: 'card', + payment_method_types: [ + { payment_method_type: 'credit', card_networks: [], bank_names: [] }, + { payment_method_type: 'debit', card_networks: [], bank_names: [] }, + ], + }, + ], + }; + const result = getPaymentMethodTypeFromList(paymentMethodListValue, 'card', 'credit'); + expect(result).toBeDefined(); + expect(result?.payment_method_type).toBe('credit'); + }); + + it('should return undefined for non-existent payment method', () => { + const paymentMethodListValue = { + payment_methods: [ + { + payment_method: 'card', + payment_method_types: [], + }, + ], + }; + const result = getPaymentMethodTypeFromList(paymentMethodListValue, 'wallet', 'apple_pay'); + expect(result).toBeUndefined(); + }); + + it('should return undefined for non-existent payment method type', () => { + const paymentMethodListValue = { + payment_methods: [ + { + payment_method: 'card', + payment_method_types: [ + { payment_method_type: 'credit', card_networks: [], bank_names: [] }, + ], + }, + ], + }; + const result = getPaymentMethodTypeFromList(paymentMethodListValue, 'card', 'debit'); + expect(result).toBeUndefined(); + }); + }); + + describe('getCardNetwork', () => { + it('should return matching card network', () => { + const paymentMethodType = { + payment_method_type: 'credit', + card_networks: [ + { card_network: 'visa', eligible_connectors: ['connector1'] }, + { card_network: 'mastercard', eligible_connectors: ['connector2'] }, + ], + bank_names: [], + }; + const result = getCardNetwork(paymentMethodType, 'visa'); + expect(result.card_network).toBe('visa'); + expect(result.eligible_connectors).toEqual(['connector1']); + }); + + it('should return default card network for non-matching brand', () => { + const paymentMethodType = { + payment_method_type: 'credit', + card_networks: [ + { card_network: 'visa', eligible_connectors: ['connector1'] }, + ], + bank_names: [], + }; + const result = getCardNetwork(paymentMethodType, 'amex'); + expect(result.card_network).toBe('NOTFOUND'); + }); + + it('should return default for empty card networks', () => { + const paymentMethodType = { + payment_method_type: 'credit', + card_networks: [], + bank_names: [], + }; + const result = getCardNetwork(paymentMethodType, 'visa'); + expect(result.card_network).toBe('NOTFOUND'); + }); + }); +}); diff --git a/src/__tests__/PaymentModeType.test.ts b/src/__tests__/PaymentModeType.test.ts new file mode 100644 index 000000000..db2c06cef --- /dev/null +++ b/src/__tests__/PaymentModeType.test.ts @@ -0,0 +1,230 @@ +import { paymentMode, defaultOrder } from '../Types/PaymentModeType.bs.js'; + +describe('PaymentModeType', () => { + describe('paymentMode', () => { + describe('happy path - card payments', () => { + it('should return "Card" for "card"', () => { + expect(paymentMode('card')).toBe('Card'); + }); + }); + + describe('happy path - bank debits', () => { + it('should return "ACHBankDebit" for "ach_debit"', () => { + expect(paymentMode('ach_debit')).toBe('ACHBankDebit'); + }); + + it('should return "BacsBankDebit" for "bacs_debit"', () => { + expect(paymentMode('bacs_debit')).toBe('BacsBankDebit'); + }); + + it('should return "BecsBankDebit" for "becs_debit"', () => { + expect(paymentMode('becs_debit')).toBe('BecsBankDebit'); + }); + + it('should return "SepaBankDebit" for "sepa_debit"', () => { + expect(paymentMode('sepa_debit')).toBe('SepaBankDebit'); + }); + }); + + describe('happy path - bank transfers', () => { + it('should return "ACHTransfer" for "ach_transfer"', () => { + expect(paymentMode('ach_transfer')).toBe('ACHTransfer'); + }); + + it('should return "BacsTransfer" for "bacs_transfer"', () => { + expect(paymentMode('bacs_transfer')).toBe('BacsTransfer'); + }); + + it('should return "SepaTransfer" for "sepa_bank_transfer"', () => { + expect(paymentMode('sepa_bank_transfer')).toBe('SepaTransfer'); + }); + }); + + describe('happy path - wallets', () => { + it('should return "ApplePay" for "apple_pay"', () => { + expect(paymentMode('apple_pay')).toBe('ApplePay'); + }); + + it('should return "GooglePay" for "google_pay"', () => { + expect(paymentMode('google_pay')).toBe('GooglePay'); + }); + + it('should return "SamsungPay" for "samsung_pay"', () => { + expect(paymentMode('samsung_pay')).toBe('SamsungPay'); + }); + + it('should return "PayPal" for "paypal"', () => { + expect(paymentMode('paypal')).toBe('PayPal'); + }); + }); + + describe('happy path - buy now pay later', () => { + it('should return "Affirm" for "affirm"', () => { + expect(paymentMode('affirm')).toBe('Affirm'); + }); + + it('should return "AfterPay" for "afterpay_clearpay"', () => { + expect(paymentMode('afterpay_clearpay')).toBe('AfterPay'); + }); + + it('should return "Klarna" for "klarna"', () => { + expect(paymentMode('klarna')).toBe('Klarna'); + }); + }); + + describe('happy path - bank redirects', () => { + it('should return "GiroPay" for "giropay"', () => { + expect(paymentMode('giropay')).toBe('GiroPay'); + }); + + it('should return "Ideal" for "ideal"', () => { + expect(paymentMode('ideal')).toBe('Ideal'); + }); + + it('should return "Sofort" for "sofort"', () => { + expect(paymentMode('sofort')).toBe('Sofort'); + }); + + it('should return "EPS" for "eps"', () => { + expect(paymentMode('eps')).toBe('EPS'); + }); + }); + + describe('happy path - other payment methods', () => { + it('should return "Boleto" for "boleto"', () => { + expect(paymentMode('boleto')).toBe('Boleto'); + }); + + it('should return "BanContactCard" for "bancontact_card"', () => { + expect(paymentMode('bancontact_card')).toBe('BanContactCard'); + }); + + it('should return "CryptoCurrency" for "crypto_currency"', () => { + expect(paymentMode('crypto_currency')).toBe('CryptoCurrency'); + }); + + it('should return "EFT" for "eft"', () => { + expect(paymentMode('eft')).toBe('EFT'); + }); + + it('should return "RevolutPay" for "revolut_pay"', () => { + expect(paymentMode('revolut_pay')).toBe('RevolutPay'); + }); + + it('should return "Givex" for "givex"', () => { + expect(paymentMode('givex')).toBe('Givex'); + }); + }); + + describe('happy path - instant bank transfers', () => { + it('should return "InstantTransfer" for "instant_bank_transfer"', () => { + expect(paymentMode('instant_bank_transfer')).toBe('InstantTransfer'); + }); + + it('should return "InstantTransferFinland" for "instant_bank_transfer_finland"', () => { + expect(paymentMode('instant_bank_transfer_finland')).toBe('InstantTransferFinland'); + }); + + it('should return "InstantTransferPoland" for "instant_bank_transfer_poland"', () => { + expect(paymentMode('instant_bank_transfer_poland')).toBe('InstantTransferPoland'); + }); + }); + + describe('happy path - saved methods', () => { + it('should return "SavedMethods" for "saved_methods"', () => { + expect(paymentMode('saved_methods')).toBe('SavedMethods'); + }); + }); + + describe('edge cases', () => { + it('should return "Unknown" for empty string', () => { + expect(paymentMode('')).toBe('Unknown'); + }); + + it('should return "Unknown" for unrecognized string', () => { + expect(paymentMode('unknown_method')).toBe('Unknown'); + }); + + it('should be case sensitive', () => { + expect(paymentMode('CARD')).toBe('Unknown'); + expect(paymentMode('Apple_Pay')).toBe('Unknown'); + expect(paymentMode('GOOGLE_PAY')).toBe('Unknown'); + }); + }); + + describe('error/boundary', () => { + it('should return "Unknown" for random strings', () => { + expect(paymentMode('xyz123')).toBe('Unknown'); + expect(paymentMode('test_payment')).toBe('Unknown'); + }); + + it('should handle strings with special characters', () => { + expect(paymentMode('card!')).toBe('Unknown'); + expect(paymentMode('apple-pay')).toBe('Unknown'); + }); + + it('should not match partial strings', () => { + expect(paymentMode('card_payment')).toBe('Unknown'); + expect(paymentMode('apple_pay_v2')).toBe('Unknown'); + }); + }); + }); + + describe('defaultOrder', () => { + describe('structure', () => { + it('should be an array', () => { + expect(Array.isArray(defaultOrder)).toBe(true); + }); + + it('should have expected length', () => { + expect(defaultOrder.length).toBe(29); + }); + }); + + describe('ordering', () => { + it('should have saved_methods first', () => { + expect(defaultOrder[0]).toBe('saved_methods'); + }); + + it('should have card second', () => { + expect(defaultOrder[1]).toBe('card'); + }); + + it('should have wallets early in the list', () => { + expect(defaultOrder).toContain('apple_pay'); + expect(defaultOrder).toContain('google_pay'); + expect(defaultOrder).toContain('paypal'); + expect(defaultOrder).toContain('samsung_pay'); + expect(defaultOrder).toContain('klarna'); + }); + + it('should have bank debits after transfers', () => { + const transferIndex = defaultOrder.indexOf('ach_transfer'); + const debitIndex = defaultOrder.indexOf('ach_debit'); + expect(transferIndex).toBeLessThan(debitIndex); + }); + }); + + describe('contents', () => { + it('should contain all expected payment methods', () => { + const expectedMethods = [ + 'saved_methods', 'card', 'apple_pay', 'google_pay', 'paypal', + 'klarna', 'samsung_pay', 'affirm', 'afterpay_clearpay', + 'ach_transfer', 'sepa_bank_transfer', 'instant_bank_transfer', + 'instant_bank_transfer_finland', 'instant_bank_transfer_poland', + 'bacs_transfer', 'ach_debit', 'sepa_debit', 'bacs_debit', + 'becs_debit', 'sofort', 'giropay', 'ideal', 'eps', 'crypto', + 'bancontact_card', 'boleto', 'eft', 'revolut_pay', 'givex', + ]; + expectedMethods.forEach((method) => { + expect(defaultOrder).toContain(method); + }); + }); + + it('should not contain duplicates', () => { + const uniqueMethods = new Set(defaultOrder); + expect(uniqueMethods.size).toBe(defaultOrder.length); + }); + }); + }); +}); diff --git a/src/__tests__/PaymentType.test.ts b/src/__tests__/PaymentType.test.ts new file mode 100644 index 000000000..d6fa50fc2 --- /dev/null +++ b/src/__tests__/PaymentType.test.ts @@ -0,0 +1,1849 @@ +import { + getMessageDisplayMode, + getPaymentMethodsArrangementForTabs, + getShowType, + getApplePayType, + getGooglePayType, + getSamsungPayType, + getPayPalType, + getTypeArray, + getShowTerms, + getGroupingBehaviorFromString, + getTheme, + normalizePath, + isPathStartsWithPattern, + shouldMaskField, + getIsStoredPaymentMethodHasName, + getConfirmParams, + getSdkHandleConfirmPaymentProps, + getSdkHandleSavePaymentProps, + getCardDetails, + getAddressDetails, + getBank, + getMaxItems, + defaultCardDetails, + defaultAddressDetails, + defaultDisplayBillingDetails, + defaultCustomerMethods, + defaultGroupingBehavior, + defaultSavedMethodCustomization, + defaultLayout, + defaultAddress, + defaultBillingDetails, + defaultBusiness, + defaultDefaultValues, + defaultshowAddress, + defaultNeverShowAddress, + defaultBilling, + defaultNeverBilling, + defaultTerms, + defaultFields, + defaultStyle, + defaultWallets, + defaultBillingAddress, + defaultSdkHandleConfirmPayment, + defaultSdkHandleSavePayment, + defaultOptions, + fieldsToExcludeFromMasking, + overrideFieldsToExcludeFromMasking, + getLayout, + getAddress, + getBillingDetails, + getDefaultValues, + getBusiness, + getShowDetails, + getShowAddressDetails, + getShowAddress, + getDeatils, + getBilling, + getFields, + getGroupingBehaviorFromObject, + getGroupingBehavior, + getSavedMethodCustomization, + getLayoutValues, + getTerms, + getApplePayHeight, + getGooglePayHeight, + getSamsungPayHeight, + getPaypalHeight, + getKlarnaHeight, + getHeightArray, + getStyle, + getWallets, + getBillingAddressPaymentMethod, + getPaymentMethodType, + itemToCustomerObjMapper, + createCustomerObjArr, + getCustomerMethods, + getCustomMethodNames, + getBillingAddress, + sanitizePaymentElementOptions, + sanitizePreloadSdkParms, + itemToObjMapper, + itemToPayerDetailsObjectMapper, + convertClickToPayCardToCustomerMethod, +} from '../Types/PaymentType.bs.js'; + +describe('PaymentType', () => { + describe('getMessageDisplayMode', () => { + it('should return "CustomMessage" for "custom_message"', () => { + expect(getMessageDisplayMode('custom_message', 'test.key')).toBe('CustomMessage'); + }); + + it('should return "DefaultSdkMessage" for "default_sdk_message"', () => { + expect(getMessageDisplayMode('default_sdk_message', 'test.key')).toBe('DefaultSdkMessage'); + }); + + it('should return "Hidden" for "hidden"', () => { + expect(getMessageDisplayMode('hidden', 'test.key')).toBe('Hidden'); + }); + + it('should return "DefaultSdkMessage" for unknown values', () => { + expect(getMessageDisplayMode('unknown', 'test.key')).toBe('DefaultSdkMessage'); + }); + + it('should return "DefaultSdkMessage" for empty string', () => { + expect(getMessageDisplayMode('', 'test.key')).toBe('DefaultSdkMessage'); + }); + }); + + + + describe('getPaymentMethodsArrangementForTabs', () => { + it('should return "Default" for "default"', () => { + expect(getPaymentMethodsArrangementForTabs('default')).toBe('Default'); + }); + + it('should return "Grid" for "grid"', () => { + expect(getPaymentMethodsArrangementForTabs('grid')).toBe('Grid'); + }); + + it('should return "Default" for unknown values', () => { + expect(getPaymentMethodsArrangementForTabs('unknown')).toBe('Default'); + }); + + it('should return "Default" for empty string', () => { + expect(getPaymentMethodsArrangementForTabs('')).toBe('Default'); + }); + }); + + describe('getShowType', () => { + it('should return "Auto" for "auto"', () => { + expect(getShowType('auto', 'test.key')).toBe('Auto'); + }); + + it('should return "Never" for "never"', () => { + expect(getShowType('never', 'test.key')).toBe('Never'); + }); + + it('should return "Auto" for unknown values', () => { + expect(getShowType('unknown', 'test.key')).toBe('Auto'); + }); + + it('should return "Auto" for empty string', () => { + expect(getShowType('', 'test.key')).toBe('Auto'); + }); + }); + + describe('getApplePayType', () => { + it('should return "Addmoney" for "add-money"', () => { + const result = getApplePayType('add-money'); + expect(result.TAG).toBe('ApplePay'); + expect(result._0).toBe('Addmoney'); + }); + + it('should return "Addmoney" for "addmoney"', () => { + const result = getApplePayType('addmoney'); + expect(result.TAG).toBe('ApplePay'); + expect(result._0).toBe('Addmoney'); + }); + + it('should return "Buy" for "buy"', () => { + const result = getApplePayType('buy'); + expect(result.TAG).toBe('ApplePay'); + expect(result._0).toBe('Buy'); + }); + + it('should return "Buy" for "buynow"', () => { + const result = getApplePayType('buynow'); + expect(result.TAG).toBe('ApplePay'); + expect(result._0).toBe('Buy'); + }); + + it('should return "Checkout" for "checkout"', () => { + const result = getApplePayType('checkout'); + expect(result.TAG).toBe('ApplePay'); + expect(result._0).toBe('Checkout'); + }); + + it('should return "Donate" for "donate"', () => { + const result = getApplePayType('donate'); + expect(result.TAG).toBe('ApplePay'); + expect(result._0).toBe('Donate'); + }); + + it('should return "Subscribe" for "subscribe"', () => { + const result = getApplePayType('subscribe'); + expect(result.TAG).toBe('ApplePay'); + expect(result._0).toBe('Subscribe'); + }); + + it('should return "Default" for unknown values', () => { + const result = getApplePayType('unknown'); + expect(result.TAG).toBe('ApplePay'); + expect(result._0).toBe('Default'); + }); + }); + + describe('getGooglePayType', () => { + it('should return "Book" for "book"', () => { + const result = getGooglePayType('book'); + expect(result.TAG).toBe('GooglePay'); + expect(result._0).toBe('Book'); + }); + + it('should return "Buy" for "buy"', () => { + const result = getGooglePayType('buy'); + expect(result.TAG).toBe('GooglePay'); + expect(result._0).toBe('Buy'); + }); + + it('should return "Buy" for "buynow"', () => { + const result = getGooglePayType('buynow'); + expect(result.TAG).toBe('GooglePay'); + expect(result._0).toBe('Buy'); + }); + + it('should return "Checkout" for "checkout"', () => { + const result = getGooglePayType('checkout'); + expect(result.TAG).toBe('GooglePay'); + expect(result._0).toBe('Checkout'); + }); + + it('should return "Donate" for "donate"', () => { + const result = getGooglePayType('donate'); + expect(result.TAG).toBe('GooglePay'); + expect(result._0).toBe('Donate'); + }); + + it('should return "Pay" for "pay"', () => { + const result = getGooglePayType('pay'); + expect(result.TAG).toBe('GooglePay'); + expect(result._0).toBe('Pay'); + }); + + it('should return "Default" for unknown values', () => { + const result = getGooglePayType('unknown'); + expect(result.TAG).toBe('GooglePay'); + expect(result._0).toBe('Default'); + }); + }); + + describe('getSamsungPayType', () => { + it('should return "Buy" for any value', () => { + const result = getSamsungPayType('any'); + expect(result.TAG).toBe('SamsungPay'); + expect(result._0).toBe('Buy'); + }); + + it('should return "Buy" for empty string', () => { + const result = getSamsungPayType(''); + expect(result.TAG).toBe('SamsungPay'); + expect(result._0).toBe('Buy'); + }); + }); + + describe('getPayPalType', () => { + it('should return "Buynow" for "buy"', () => { + const result = getPayPalType('buy'); + expect(result.TAG).toBe('Paypal'); + expect(result._0).toBe('Buynow'); + }); + + it('should return "Buynow" for "buynow"', () => { + const result = getPayPalType('buynow'); + expect(result.TAG).toBe('Paypal'); + expect(result._0).toBe('Buynow'); + }); + + it('should return "Checkout" for "checkout"', () => { + const result = getPayPalType('checkout'); + expect(result.TAG).toBe('Paypal'); + expect(result._0).toBe('Checkout'); + }); + + it('should return "Installment" for "installment"', () => { + const result = getPayPalType('installment'); + expect(result.TAG).toBe('Paypal'); + expect(result._0).toBe('Installment'); + }); + + it('should return "Pay" for "pay"', () => { + const result = getPayPalType('pay'); + expect(result.TAG).toBe('Paypal'); + expect(result._0).toBe('Pay'); + }); + + it('should return "Paypal" for unknown values', () => { + const result = getPayPalType('unknown'); + expect(result.TAG).toBe('Paypal'); + expect(result._0).toBe('Paypal'); + }); + }); + + describe('getTypeArray', () => { + it('should return array of wallet type objects', () => { + const result = getTypeArray('buy'); + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBe(4); + }); + + it('should include ApplePay, GooglePay, Paypal, SamsungPay', () => { + const result = getTypeArray('buy'); + expect(result[0].TAG).toBe('ApplePay'); + expect(result[1].TAG).toBe('GooglePay'); + expect(result[2].TAG).toBe('Paypal'); + expect(result[3].TAG).toBe('SamsungPay'); + }); + }); + + describe('getShowTerms', () => { + it('should return "Always" for "always"', () => { + expect(getShowTerms('always', 'test.key')).toBe('Always'); + }); + + it('should return "Auto" for "auto"', () => { + expect(getShowTerms('auto', 'test.key')).toBe('Auto'); + }); + + it('should return "Never" for "never"', () => { + expect(getShowTerms('never', 'test.key')).toBe('Never'); + }); + + it('should return "Auto" for unknown values', () => { + expect(getShowTerms('unknown', 'test.key')).toBe('Auto'); + }); + }); + + describe('getGroupingBehaviorFromString', () => { + it('should return defaultGroupingBehavior for "default"', () => { + const result = getGroupingBehaviorFromString('default'); + expect(result.displayInSeparateScreen).toBe(true); + expect(result.groupByPaymentMethods).toBe(false); + }); + + it('should return grouped behavior for "groupByPaymentMethods"', () => { + const result = getGroupingBehaviorFromString('groupByPaymentMethods'); + expect(result.displayInSeparateScreen).toBe(false); + expect(result.groupByPaymentMethods).toBe(true); + }); + + it('should return default for unknown values', () => { + const result = getGroupingBehaviorFromString('unknown'); + expect(result.displayInSeparateScreen).toBe(true); + expect(result.groupByPaymentMethods).toBe(false); + }); + }); + + describe('getTheme', () => { + it('should return "Dark" for "dark"', () => { + expect(getTheme('dark')).toBe('Dark'); + }); + + it('should return "Light" for "light"', () => { + expect(getTheme('light')).toBe('Light'); + }); + + it('should return "Outline" for "outline"', () => { + expect(getTheme('outline')).toBe('Outline'); + }); + + it('should return "Dark" for unknown values', () => { + expect(getTheme('unknown')).toBe('Dark'); + }); + + it('should return "Dark" for empty string', () => { + expect(getTheme('')).toBe('Dark'); + }); + }); + + describe('normalizePath', () => { + it('should remove array index notation from path', () => { + expect(normalizePath('field[0]')).toBe('field'); + }); + + it('should handle multiple array indices', () => { + expect(normalizePath('fields[0].subfield[1]')).toBe('fields.subfield'); + }); + + it('should return path unchanged if no array indices', () => { + expect(normalizePath('simple.path')).toBe('simple.path'); + }); + + it('should handle empty string', () => { + expect(normalizePath('')).toBe(''); + }); + + it('should handle path with only array index', () => { + expect(normalizePath('[0]')).toBe(''); + }); + }); + + describe('isPathStartsWithPattern', () => { + it('should return true when paths are equal', () => { + expect(isPathStartsWithPattern('field', 'field')).toBe(true); + }); + + it('should return true when path starts with pattern followed by dot', () => { + expect(isPathStartsWithPattern('field.subfield', 'field')).toBe(true); + }); + + it('should return false when path does not start with pattern', () => { + expect(isPathStartsWithPattern('other.subfield', 'field')).toBe(false); + }); + + it('should return false when path starts with pattern prefix but not followed by dot', () => { + expect(isPathStartsWithPattern('fieldExtra', 'field')).toBe(false); + }); + }); + + describe('shouldMaskField', () => { + it('should return true for overridden paths', () => { + expect(shouldMaskField('wallets.walletReturnUrl')).toBe(true); + }); + + it('should return true for non-excluded paths', () => { + expect(shouldMaskField('someOtherField')).toBe(true); + }); + + it('should return false for excluded layout field', () => { + expect(shouldMaskField('layout')).toBe(false); + }); + + it('should return false for excluded wallets field', () => { + expect(shouldMaskField('wallets')).toBe(false); + }); + + it('should return false for nested excluded field', () => { + expect(shouldMaskField('layout.someNestedField')).toBe(false); + }); + + it('should return true for paymentMethodsConfig.message.value', () => { + expect(shouldMaskField('paymentMethodsConfig.paymentMethodTypes.message.value')).toBe(true); + }); + }); + + describe('getIsStoredPaymentMethodHasName', () => { + it('should return true when cardHolderName is present', () => { + const savedMethod = { + card: { + cardHolderName: 'John Doe', + scheme: 'Visa', + last4Digits: '4242', + expiryMonth: '12', + expiryYear: '2025', + cardToken: 'token', + nickname: '', + isClickToPayCard: false, + cardBin: '424242', + }, + }; + expect(getIsStoredPaymentMethodHasName(savedMethod)).toBe(true); + }); + + it('should return false when cardHolderName is undefined', () => { + const savedMethod = { + card: { + cardHolderName: undefined, + scheme: 'Visa', + last4Digits: '4242', + expiryMonth: '12', + expiryYear: '2025', + cardToken: 'token', + nickname: '', + isClickToPayCard: false, + cardBin: '424242', + }, + }; + expect(getIsStoredPaymentMethodHasName(savedMethod)).toBe(false); + }); + + it('should return false when cardHolderName is empty string', () => { + const savedMethod = { + card: { + cardHolderName: '', + scheme: 'Visa', + last4Digits: '4242', + expiryMonth: '12', + expiryYear: '2025', + cardToken: 'token', + nickname: '', + isClickToPayCard: false, + cardBin: '424242', + }, + }; + expect(getIsStoredPaymentMethodHasName(savedMethod)).toBe(false); + }); + }); + + describe('getConfirmParams', () => { + it('should extract confirm params from dict', () => { + const dict = { + return_url: 'https://example.com/return', + publishableKey: 'pk_test_123', + redirect: 'if_required', + }; + const result = getConfirmParams(dict); + expect(result.return_url).toBe('https://example.com/return'); + expect(result.publishableKey).toBe('pk_test_123'); + expect(result.redirect).toBe('if_required'); + }); + + it('should handle missing fields with defaults', () => { + const result = getConfirmParams({}); + expect(result.return_url).toBe(''); + expect(result.publishableKey).toBe(''); + expect(result.redirect).toBe('if_required'); + }); + + it('should use default redirect when not specified', () => { + const dict = { return_url: 'https://test.com' }; + const result = getConfirmParams(dict); + expect(result.redirect).toBe('if_required'); + }); + }); + + describe('getSdkHandleConfirmPaymentProps', () => { + it('should extract SDK handle confirm payment props', () => { + const dict = { + handleConfirm: true, + buttonText: 'Pay Now', + confirmParams: { + return_url: 'https://example.com/return', + }, + }; + const result = getSdkHandleConfirmPaymentProps(dict); + expect(result.handleConfirm).toBe(true); + expect(result.buttonText).toBe('Pay Now'); + expect(result.confirmParams.return_url).toBe('https://example.com/return'); + }); + + it('should handle missing optional fields', () => { + const dict = { + handleConfirm: false, + confirmParams: {}, + }; + const result = getSdkHandleConfirmPaymentProps(dict); + expect(result.handleConfirm).toBe(false); + expect(result.buttonText).toBeUndefined(); + }); + }); + + describe('getSdkHandleSavePaymentProps', () => { + it('should extract SDK handle save payment props', () => { + const dict = { + handleSave: true, + buttonText: 'Save Card', + confirmParams: { + return_url: 'https://example.com/return', + }, + }; + const result = getSdkHandleSavePaymentProps(dict); + expect(result.handleSave).toBe(true); + expect(result.buttonText).toBe('Save Card'); + expect(result.confirmParams.return_url).toBe('https://example.com/return'); + }); + + it('should handle missing optional fields', () => { + const dict = { + handleSave: false, + confirmParams: {}, + }; + const result = getSdkHandleSavePaymentProps(dict); + expect(result.handleSave).toBe(false); + expect(result.buttonText).toBeUndefined(); + }); + }); + + describe('defaultCardDetails', () => { + it('should have expected default values', () => { + expect(defaultCardDetails.scheme).toBeUndefined(); + expect(defaultCardDetails.last4Digits).toBe(''); + expect(defaultCardDetails.expiryMonth).toBe(''); + expect(defaultCardDetails.expiryYear).toBe(''); + expect(defaultCardDetails.cardToken).toBe(''); + expect(defaultCardDetails.cardHolderName).toBeUndefined(); + expect(defaultCardDetails.nickname).toBe(''); + expect(defaultCardDetails.isClickToPayCard).toBe(false); + expect(defaultCardDetails.cardBin).toBe(''); + }); + }); + + describe('defaultAddressDetails', () => { + it('should have expected default values', () => { + expect(defaultAddressDetails.line1).toBeUndefined(); + expect(defaultAddressDetails.line2).toBeUndefined(); + expect(defaultAddressDetails.city).toBeUndefined(); + expect(defaultAddressDetails.state).toBeUndefined(); + expect(defaultAddressDetails.country).toBeUndefined(); + expect(defaultAddressDetails.zip).toBeUndefined(); + }); + }); + + describe('defaultGroupingBehavior', () => { + it('should have expected default values', () => { + expect(defaultGroupingBehavior.displayInSeparateScreen).toBe(true); + expect(defaultGroupingBehavior.groupByPaymentMethods).toBe(false); + }); + }); + + describe('defaultSavedMethodCustomization', () => { + it('should have expected default values', () => { + expect(defaultSavedMethodCustomization.maxItems).toBe(4); + expect(defaultSavedMethodCustomization.hideCardExpiry).toBe(false); + }); + }); + + describe('defaultLayout', () => { + it('should have expected default values', () => { + expect(defaultLayout.defaultCollapsed).toBe(false); + expect(defaultLayout.radios).toBe(false); + expect(defaultLayout.type).toBe('Tabs'); + expect(defaultLayout.maxAccordionItems).toBe(4); + }); + }); + + describe('defaultStyle', () => { + it('should have expected default values', () => { + expect(defaultStyle.theme).toBe('Light'); + expect(defaultStyle.buttonRadius).toBe(2); + }); + }); + + describe('defaultWallets', () => { + it('should have expected default values', () => { + expect(defaultWallets.walletReturnUrl).toBe(''); + expect(defaultWallets.applePay).toBe('Auto'); + expect(defaultWallets.googlePay).toBe('Auto'); + expect(defaultWallets.payPal).toBe('Auto'); + }); + }); + + describe('defaultBillingAddress', () => { + it('should have expected default values', () => { + expect(defaultBillingAddress.isUseBillingAddress).toBe(false); + expect(defaultBillingAddress.usePrefilledValues).toBe('Auto'); + }); + }); + + describe('defaultTerms', () => { + it('should have expected default values', () => { + expect(defaultTerms.card).toBe('Auto'); + expect(defaultTerms.ideal).toBe('Auto'); + expect(defaultTerms.sofort).toBe('Auto'); + }); + }); + + describe('fieldsToExcludeFromMasking', () => { + it('should contain expected fields', () => { + expect(fieldsToExcludeFromMasking).toContain('layout'); + expect(fieldsToExcludeFromMasking).toContain('wallets'); + expect(fieldsToExcludeFromMasking).toContain('paymentMethodsConfig'); + expect(fieldsToExcludeFromMasking).toContain('terms'); + }); + }); + + describe('overrideFieldsToExcludeFromMasking', () => { + it('should contain expected override fields', () => { + expect(overrideFieldsToExcludeFromMasking).toContain('wallets.walletReturnUrl'); + }); + }); + + describe('getCardDetails', () => { + it('should extract card details from dict', () => { + const dict = { + card: { + scheme: 'Visa', + last4_digits: '4242', + expiry_month: '12', + expiry_year: '2025', + card_token: 'tok_123', + card_holder_name: 'John Doe', + nick_name: 'My Card', + card_isin: '424242', + }, + }; + const result = getCardDetails(dict, 'card'); + expect(result.scheme).toBe('Visa'); + expect(result.last4Digits).toBe('4242'); + expect(result.expiryMonth).toBe('12'); + expect(result.expiryYear).toBe('2025'); + expect(result.cardToken).toBe('tok_123'); + expect(result.cardHolderName).toBe('John Doe'); + expect(result.nickname).toBe('My Card'); + expect(result.cardBin).toBe('424242'); + }); + + it('should return defaultCardDetails when card key not present', () => { + const result = getCardDetails({}, 'card'); + expect(result.scheme).toBeUndefined(); + expect(result.last4Digits).toBe(''); + expect(result.expiryMonth).toBe(''); + }); + + it('should handle missing optional fields', () => { + const dict = { + card: { + scheme: 'Mastercard', + last4_digits: '1234', + }, + }; + const result = getCardDetails(dict, 'card'); + expect(result.scheme).toBe('Mastercard'); + expect(result.last4Digits).toBe('1234'); + expect(result.cardHolderName).toBeUndefined(); + }); + }); + + describe('getAddressDetails', () => { + it('should extract address details from dict', () => { + const dict = { + address: { + line1: '123 Main St', + line2: 'Apt 4B', + line3: 'Suite 100', + city: 'New York', + state: 'NY', + country: 'US', + zip: '10001', + }, + }; + const result = getAddressDetails(dict, 'address'); + expect(result.line1).toBe('123 Main St'); + expect(result.line2).toBe('Apt 4B'); + expect(result.line3).toBe('Suite 100'); + expect(result.city).toBe('New York'); + expect(result.state).toBe('NY'); + expect(result.country).toBe('US'); + expect(result.zip).toBe('10001'); + }); + + it('should return defaultAddressDetails when key not present', () => { + const result = getAddressDetails({}, 'address'); + expect(result.line1).toBeUndefined(); + expect(result.city).toBeUndefined(); + }); + + it('should handle partial address data', () => { + const dict = { + address: { + city: 'Los Angeles', + country: 'US', + }, + }; + const result = getAddressDetails(dict, 'address'); + expect(result.city).toBe('Los Angeles'); + expect(result.country).toBe('US'); + expect(result.line1).toBe(''); + }); + }); + + describe('getBank', () => { + it('should extract bank details from dict', () => { + const dict = { + bank: { + mask: '****1234', + }, + }; + const result = getBank(dict); + expect(result.mask).toBe('****1234'); + }); + + it('should return empty mask for missing bank', () => { + const result = getBank({}); + expect(result.mask).toBe(''); + }); + + it('should handle empty bank object', () => { + const dict = { bank: {} }; + const result = getBank(dict); + expect(result.mask).toBe(''); + }); + }); + + describe('getMaxItems', () => { + const mockLogger = { + setLogInfo: jest.fn(), + setLogError: jest.fn(), + }; + + it('should return value when positive', () => { + const dict = { maxItems: 10 }; + const result = getMaxItems(dict, 'maxItems', 4, mockLogger); + expect(result).toBe(10); + }); + + it('should return default when value is zero', () => { + const dict = { maxItems: 0 }; + const result = getMaxItems(dict, 'maxItems', 4, mockLogger); + expect(result).toBe(4); + }); + + it('should return default when value is negative', () => { + const dict = { maxItems: -5 }; + const result = getMaxItems(dict, 'maxItems', 4, mockLogger); + expect(result).toBe(4); + }); + + it('should return default when key is missing', () => { + const result = getMaxItems({}, 'maxItems', 4, mockLogger); + expect(result).toBe(4); + }); + }); + + describe('getLayout', () => { + it('should return ObjectLayout for dict with layout values', () => { + const dict = { + layout: { type: 'tabs' }, + }; + const result = getLayout(dict, 'layout'); + expect(result.TAG).toBe('ObjectLayout'); + }); + + it('should return default ObjectLayout when key not present', () => { + const result = getLayout({}, 'layout'); + expect(result.TAG).toBe('ObjectLayout'); + expect((result as any)._0.type).toBe('Tabs'); + }); + }); + + describe('getAddress', () => { + const mockLogger = { + setLogInfo: jest.fn(), + setLogError: jest.fn(), + }; + + it('should extract address from dict', () => { + const dict = { + address: { + line1: '123 Main St', + line2: 'Apt 4B', + city: 'New York', + state: 'NY', + country: 'US', + postal_code: '10001', + }, + }; + const result = getAddress(dict, 'address', mockLogger); + expect(result.line1).toBe('123 Main St'); + expect(result.city).toBe('New York'); + expect(result.country).toBe('US'); + expect(result.postal_code).toBe('10001'); + }); + + it('should return defaultAddress when key not present', () => { + const result = getAddress({}, 'address', mockLogger); + expect(result.line1).toBe(''); + expect(result.city).toBe(''); + }); + + it('should handle partial address data', () => { + const dict = { + address: { + city: 'Los Angeles', + country: 'US', + }, + }; + const result = getAddress(dict, 'address', mockLogger); + expect(result.city).toBe('Los Angeles'); + expect(result.line1).toBe(''); + }); + }); + + describe('getBillingDetails', () => { + const mockLogger = { + setLogInfo: jest.fn(), + setLogError: jest.fn(), + }; + + it('should extract billing details from dict', () => { + const dict = { + billingDetails: { + name: 'John Doe', + email: 'john@example.com', + phone: '+1234567890', + address: { + line1: '123 Main St', + city: 'New York', + state: 'NY', + country: 'US', + postal_code: '10001', + }, + }, + }; + const result = getBillingDetails(dict, 'billingDetails', mockLogger); + expect(result.name).toBe('John Doe'); + expect(result.email).toBe('john@example.com'); + expect(result.phone).toBe('+1234567890'); + }); + + it('should return defaultBillingDetails when key not present', () => { + const result = getBillingDetails({}, 'billingDetails', mockLogger); + expect(result.name).toBe(''); + expect(result.email).toBe(''); + }); + }); + + describe('getDefaultValues', () => { + const mockLogger = { + setLogInfo: jest.fn(), + setLogError: jest.fn(), + }; + + it('should extract default values from dict', () => { + const dict = { + defaultValues: { + billingDetails: { + name: 'Jane Doe', + email: 'jane@example.com', + phone: '+0987654321', + address: { + line1: '456 Oak St', + city: 'Boston', + state: 'MA', + country: 'US', + postal_code: '02101', + }, + }, + }, + }; + const result = getDefaultValues(dict, 'defaultValues', mockLogger); + expect(result.billingDetails.name).toBe('Jane Doe'); + }); + + it('should return defaultDefaultValues when key not present', () => { + const result = getDefaultValues({}, 'defaultValues', mockLogger); + expect(result.billingDetails.name).toBe(''); + }); + }); + + describe('getBusiness', () => { + const mockLogger = { + setLogInfo: jest.fn(), + setLogError: jest.fn(), + }; + + it('should extract business name from dict', () => { + const dict = { + business: { + name: 'Acme Corp', + }, + }; + const result = getBusiness(dict, 'business', mockLogger); + expect(result.name).toBe('Acme Corp'); + }); + + it('should return defaultBusiness when key not present', () => { + const result = getBusiness({}, 'business', mockLogger); + expect(result.name).toBe(''); + }); + }); + + describe('getApplePayType additional cases', () => { + it('should return "Order" for "order"', () => { + const result = getApplePayType('order'); + expect(result.TAG).toBe('ApplePay'); + expect(result._0).toBe('Order'); + }); + + it('should return "Reload" for "reload"', () => { + const result = getApplePayType('reload'); + expect(result.TAG).toBe('ApplePay'); + expect(result._0).toBe('Reload'); + }); + + it('should return "Rent" for "rent"', () => { + const result = getApplePayType('rent'); + expect(result.TAG).toBe('ApplePay'); + expect(result._0).toBe('Rent'); + }); + + it('should return "Contribute" for "contribute"', () => { + const result = getApplePayType('contribute'); + expect(result.TAG).toBe('ApplePay'); + expect(result._0).toBe('Contribute'); + }); + + it('should return "Support" for "support"', () => { + const result = getApplePayType('support'); + expect(result.TAG).toBe('ApplePay'); + expect(result._0).toBe('Support'); + }); + + it('should return "Tip" for "tip"', () => { + const result = getApplePayType('tip'); + expect(result.TAG).toBe('ApplePay'); + expect(result._0).toBe('Tip'); + }); + + it('should return "Topup" for "top-up"', () => { + const result = getApplePayType('top-up'); + expect(result.TAG).toBe('ApplePay'); + expect(result._0).toBe('Topup'); + }); + + it('should return "Topup" for "topup"', () => { + const result = getApplePayType('topup'); + expect(result.TAG).toBe('ApplePay'); + expect(result._0).toBe('Topup'); + }); + }); + + describe('getGooglePayType additional cases', () => { + it('should return "Order" for "order"', () => { + const result = getGooglePayType('order'); + expect(result.TAG).toBe('GooglePay'); + expect(result._0).toBe('Order'); + }); + + it('should return "Subscribe" for "subscribe"', () => { + const result = getGooglePayType('subscribe'); + expect(result.TAG).toBe('GooglePay'); + expect(result._0).toBe('Subscribe'); + }); + }); + + describe('getShowDetails', () => { + it('should return defaultBilling for JSONString with auto', () => { + const input = { TAG: 'JSONString', _0: 'auto' }; + const result = getShowDetails(input); + expect(result.name).toBe('Auto'); + expect(result.email).toBe('Auto'); + }); + + it('should return defaultNeverBilling for JSONString with never', () => { + const input = { TAG: 'JSONString', _0: 'never' }; + const result = getShowDetails(input); + expect(result.name).toBe('Never'); + expect(result.email).toBe('Never'); + }); + + it('should return inner object for JSONObject', () => { + const innerObj = { name: 'Auto', email: 'Never' }; + const input = { TAG: 'JSONObject', _0: innerObj }; + const result = getShowDetails(input); + expect(result).toBe(innerObj); + }); + }); + + describe('getShowAddressDetails', () => { + it('should return defaultshowAddress for JSONString with auto', () => { + const input = { TAG: 'JSONString', _0: 'auto' }; + const result = getShowAddressDetails(input); + expect(result.line1).toBe('Auto'); + expect(result.city).toBe('Auto'); + }); + + it('should return defaultNeverShowAddress for JSONString with never', () => { + const input = { TAG: 'JSONString', _0: 'never' }; + const result = getShowAddressDetails(input); + expect(result.line1).toBe('Never'); + }); + + it('should return inner address for JSONObject', () => { + const innerAddress = { line1: 'Auto', city: 'Never' }; + const input = { + TAG: 'JSONObject', + _0: { + name: 'Auto', + email: 'Auto', + phone: 'Auto', + address: { TAG: 'JSONObject', _0: innerAddress }, + }, + }; + const result = getShowAddressDetails(input); + expect(result).toBe(innerAddress); + }); + }); + + describe('getShowAddress', () => { + const mockLogger = { + setLogInfo: jest.fn(), + setLogError: jest.fn(), + }; + + it('should extract show address settings from dict', () => { + const dict = { + address: { + line1: 'auto', + line2: 'never', + city: 'auto', + state: 'auto', + country: 'auto', + postal_code: 'auto', + }, + }; + const result = getShowAddress(dict, 'address', mockLogger); + expect(result.line1).toBe('Auto'); + expect(result.line2).toBe('Never'); + expect(result.city).toBe('Auto'); + }); + + it('should return defaultshowAddress when key not present', () => { + const result = getShowAddress({}, 'address', mockLogger); + expect(result.line1).toBe('Auto'); + }); + }); + + describe('getDeatils', () => { + const mockLogger = { + setLogInfo: jest.fn(), + setLogError: jest.fn(), + }; + + it('should return JSONString for string values', () => { + const result = getDeatils('auto', mockLogger); + expect(result.TAG).toBe('JSONString'); + expect(result._0).toBe('auto'); + }); + + it('should return JSONObject for object values', () => { + const input = { + name: 'auto', + email: 'never', + phone: 'auto', + address: { + line1: 'auto', + city: 'auto', + }, + }; + const result = getDeatils(input, mockLogger); + expect(result.TAG).toBe('JSONObject'); + expect(result._0.name).toBe('Auto'); + expect(result._0.email).toBe('Never'); + }); + + it('should return JSONString with empty string for null', () => { + const result = getDeatils(null, mockLogger); + expect(result.TAG).toBe('JSONString'); + expect(result._0).toBe(''); + }); + + it('should return JSONString with empty string for number', () => { + const result = getDeatils(42, mockLogger); + expect(result.TAG).toBe('JSONString'); + expect(result._0).toBe(''); + }); + }); + + describe('getBilling', () => { + const mockLogger = { + setLogInfo: jest.fn(), + setLogError: jest.fn(), + }; + + it('should extract billing details from dict', () => { + const dict = { + billingDetails: 'auto', + }; + const result = getBilling(dict, 'billingDetails', mockLogger); + expect(result.TAG).toBe('JSONString'); + expect(result._0).toBe('auto'); + }); + + it('should return default when key not present', () => { + const result = getBilling({}, 'billingDetails', mockLogger); + expect(result.TAG).toBe('JSONObject'); + }); + }); + + describe('getFields', () => { + const mockLogger = { + setLogInfo: jest.fn(), + setLogError: jest.fn(), + }; + + it('should extract fields from dict', () => { + const dict = { + fields: { + billingDetails: { TAG: 'JSONString', _0: 'auto' }, + }, + }; + const result = getFields(dict, 'fields', mockLogger); + expect(result.billingDetails).toBeDefined(); + }); + + it('should return defaultFields when key not present', () => { + const result = getFields({}, 'fields', mockLogger); + expect(result.billingDetails).toBeDefined(); + }); + }); + + describe('getGroupingBehaviorFromObject', () => { + const mockLogger = { + setLogInfo: jest.fn(), + setLogError: jest.fn(), + }; + + it('should extract grouping behavior from object', () => { + const json = { + displayInSeparateScreen: false, + groupByPaymentMethods: true, + }; + const result = getGroupingBehaviorFromObject(json, mockLogger); + expect(result.displayInSeparateScreen).toBe(false); + expect(result.groupByPaymentMethods).toBe(true); + }); + + it('should use defaults for missing fields', () => { + const result = getGroupingBehaviorFromObject({}, mockLogger); + expect(result.displayInSeparateScreen).toBe(true); + expect(result.groupByPaymentMethods).toBe(false); + }); + }); + + describe('getGroupingBehavior', () => { + const mockLogger = { + setLogInfo: jest.fn(), + setLogError: jest.fn(), + }; + + it('should return default for missing groupingBehavior', () => { + const result = getGroupingBehavior({}, mockLogger); + expect(result.displayInSeparateScreen).toBe(true); + expect(result.groupByPaymentMethods).toBe(false); + }); + + it('should parse string groupingBehavior', () => { + const dict = { + groupingBehavior: 'groupByPaymentMethods', + }; + const result = getGroupingBehavior(dict, mockLogger); + expect(result.groupByPaymentMethods).toBe(true); + }); + + it('should parse object groupingBehavior', () => { + const dict = { + groupingBehavior: { + displayInSeparateScreen: false, + groupByPaymentMethods: true, + }, + }; + const result = getGroupingBehavior(dict, mockLogger); + expect(result.displayInSeparateScreen).toBe(false); + expect(result.groupByPaymentMethods).toBe(true); + }); + }); + + describe('getSavedMethodCustomization', () => { + const mockLogger = { + setLogInfo: jest.fn(), + setLogError: jest.fn(), + }; + + it('should extract saved method customization from dict', () => { + const dict = { + savedMethodCustomization: { + groupingBehavior: 'default', + maxItems: 6, + hideCardExpiry: true, + }, + }; + const result = getSavedMethodCustomization(dict, 'savedMethodCustomization', mockLogger); + expect(result.maxItems).toBe(6); + expect(result.hideCardExpiry).toBe(true); + }); + + it('should return default when key not present', () => { + const result = getSavedMethodCustomization({}, 'savedMethodCustomization', mockLogger); + expect(result.maxItems).toBe(4); + expect(result.hideCardExpiry).toBe(false); + }); + }); + + describe('getLayoutValues', () => { + const mockLogger = { + setLogInfo: jest.fn(), + setLogError: jest.fn(), + }; + + it('should return StringLayout for string values', () => { + const result = getLayoutValues('accordion', mockLogger); + expect(result.TAG).toBe('StringLayout'); + expect(result._0).toBe('Accordion'); + }); + + it('should return ObjectLayout for object values', () => { + const val = { + type: 'tabs', + defaultCollapsed: true, + radios: true, + spacedAccordionItems: false, + maxAccordionItems: 5, + savedMethodCustomization: { + groupingBehavior: 'default', + maxItems: 4, + hideCardExpiry: false, + }, + paymentMethodsArrangementForTabs: 'grid', + displayOneClickPaymentMethodsOnTop: false, + }; + const result = getLayoutValues(val, mockLogger); + expect(result.TAG).toBe('ObjectLayout'); + const layoutObj = result._0 as any; + expect(layoutObj.type).toBe('Tabs'); + expect(layoutObj.defaultCollapsed).toBe(true); + }); + + it('should return default StringLayout for null', () => { + const result = getLayoutValues(null, mockLogger); + expect(result.TAG).toBe('StringLayout'); + expect(result._0).toBe('Tabs'); + }); + }); + + describe('getApplePayHeight', () => { + const mockLogger = { + setLogInfo: jest.fn(), + setLogError: jest.fn(), + }; + + it('should return value when >= 45', () => { + const result = getApplePayHeight(50, mockLogger); + expect(result.TAG).toBe('ApplePay'); + expect(result._0).toBe(50); + }); + + it('should return min value when < 45', () => { + const result = getApplePayHeight(40, mockLogger); + expect(result.TAG).toBe('ApplePay'); + expect(result._0).toBe(48); + }); + }); + + describe('getGooglePayHeight', () => { + const mockLogger = { + setLogInfo: jest.fn(), + setLogError: jest.fn(), + }; + + it('should return value when >= 45', () => { + const result = getGooglePayHeight(55, mockLogger); + expect(result.TAG).toBe('GooglePay'); + expect(result._0).toBe(55); + }); + + it('should return min value when < 45', () => { + const result = getGooglePayHeight(30, mockLogger); + expect(result.TAG).toBe('GooglePay'); + expect(result._0).toBe(48); + }); + }); + + describe('getSamsungPayHeight', () => { + const mockLogger = { + setLogInfo: jest.fn(), + setLogError: jest.fn(), + }; + + it('should return value when >= 45', () => { + const result = getSamsungPayHeight(60, mockLogger); + expect(result.TAG).toBe('SamsungPay'); + expect(result._0).toBe(60); + }); + + it('should return min value when < 45', () => { + const result = getSamsungPayHeight(40, mockLogger); + expect(result.TAG).toBe('SamsungPay'); + expect(result._0).toBe(48); + }); + }); + + describe('getPaypalHeight', () => { + const mockLogger = { + setLogInfo: jest.fn(), + setLogError: jest.fn(), + }; + + it('should return value when in range 25-55', () => { + const result = getPaypalHeight(40, mockLogger); + expect(result.TAG).toBe('Paypal'); + expect(result._0).toBe(40); + }); + + it('should return min value when < 25', () => { + const result = getPaypalHeight(20, mockLogger); + expect(result.TAG).toBe('Paypal'); + expect(result._0).toBe(25); + }); + + it('should return max value when > 55', () => { + const result = getPaypalHeight(60, mockLogger); + expect(result.TAG).toBe('Paypal'); + expect(result._0).toBe(55); + }); + }); + + describe('getKlarnaHeight', () => { + const mockLogger = { + setLogInfo: jest.fn(), + setLogError: jest.fn(), + }; + + it('should return value when in range 40-60', () => { + const result = getKlarnaHeight(50, mockLogger); + expect(result.TAG).toBe('Klarna'); + expect(result._0).toBe(50); + }); + + it('should return min value when < 40', () => { + const result = getKlarnaHeight(30, mockLogger); + expect(result.TAG).toBe('Klarna'); + expect(result._0).toBe(40); + }); + + it('should return max value when > 60', () => { + const result = getKlarnaHeight(70, mockLogger); + expect(result.TAG).toBe('Klarna'); + expect(result._0).toBe(60); + }); + }); + + describe('getHeightArray', () => { + const mockLogger = { + setLogInfo: jest.fn(), + setLogError: jest.fn(), + }; + + it('should return array of height objects', () => { + const result = getHeightArray(48, mockLogger); + expect(result).toHaveLength(5); + expect(result[0].TAG).toBe('ApplePay'); + expect(result[1].TAG).toBe('GooglePay'); + expect(result[2].TAG).toBe('Paypal'); + expect(result[3].TAG).toBe('Klarna'); + expect(result[4].TAG).toBe('SamsungPay'); + }); + }); + + describe('getStyle', () => { + const mockLogger = { + setLogInfo: jest.fn(), + setLogError: jest.fn(), + }; + + it('should extract style from dict', () => { + const dict = { + style: { + type: 'buy', + theme: 'dark', + height: 50, + buttonRadius: 4, + }, + }; + const result = getStyle(dict, 'style', mockLogger); + expect(result.theme).toBe('Dark'); + expect(result.buttonRadius).toBe(4); + }); + + it('should return defaultStyle when key not present', () => { + const result = getStyle({}, 'style', mockLogger); + expect(result.theme).toBe('Light'); + expect(result.buttonRadius).toBe(2); + }); + }); + + describe('getWallets', () => { + const mockLogger = { + setLogInfo: jest.fn(), + setLogError: jest.fn(), + }; + + it('should extract wallets from dict', () => { + const dict = { + wallets: { + walletReturnUrl: 'https://example.com/return', + applePay: 'auto', + googlePay: 'auto', + payPal: 'auto', + klarna: 'auto', + paze: 'auto', + samsungPay: 'auto', + style: { + type: 'buy', + theme: 'dark', + height: 48, + buttonRadius: 2, + }, + }, + }; + const result = getWallets(dict, 'wallets', mockLogger); + expect(result.walletReturnUrl).toBe('https://example.com/return'); + expect(result.applePay).toBe('Auto'); + }); + + it('should return defaultWallets when key not present', () => { + const result = getWallets({}, 'wallets', mockLogger); + expect(result.walletReturnUrl).toBe(''); + }); + }); + + describe('getBillingAddressPaymentMethod', () => { + it('should extract billing address from dict', () => { + const dict = { + billing: { + address: { + line1: '123 Main St', + city: 'New York', + }, + }, + }; + const result = getBillingAddressPaymentMethod(dict, 'billing'); + expect(result.address.line1).toBe('123 Main St'); + }); + + it('should return default when key not present', () => { + const result = getBillingAddressPaymentMethod({}, 'billing'); + expect(result.address.line1).toBeUndefined(); + }); + }); + + describe('getPaymentMethodType', () => { + it('should extract payment method type', () => { + const dict = { payment_method_type: 'card' }; + const result = getPaymentMethodType(dict); + expect(result).toBeDefined(); + }); + + it('should return undefined when key not present', () => { + const result = getPaymentMethodType({}); + expect(result).toBeUndefined(); + }); + }); + + describe('itemToCustomerObjMapper', () => { + it('should map customer dict to customer objects', () => { + const customerDict = { + customer_payment_methods: [ + { + payment_token: 'tok_123', + customer_id: 'cust_123', + payment_method: 'card', + payment_method_id: 'pm_123', + card: { + scheme: 'Visa', + last4_digits: '4242', + expiry_month: '12', + expiry_year: '2025', + card_token: 'card_tok', + }, + }, + ], + is_guest_customer: false, + }; + const result = itemToCustomerObjMapper(customerDict); + expect(Array.isArray(result)).toBe(true); + expect(result[0]).toHaveLength(1); + expect(result[0][0].paymentToken).toBe('tok_123'); + expect(result[1]).toBe(false); + }); + + it('should handle empty customer_payment_methods', () => { + const customerDict = { + customer_payment_methods: [], + is_guest_customer: true, + }; + const result = itemToCustomerObjMapper(customerDict); + expect(result[0]).toHaveLength(0); + expect(result[1]).toBe(true); + }); + }); + + describe('createCustomerObjArr', () => { + it('should create customer object array from dict', () => { + const dict = { + customerPaymentMethods: { + customer_payment_methods: [ + { + payment_token: 'tok_123', + customer_id: 'cust_123', + payment_method: 'card', + payment_method_id: 'pm_123', + }, + ], + }, + }; + const result = createCustomerObjArr(dict, 'customerPaymentMethods'); + expect(result.TAG).toBe('LoadedSavedCards'); + }); + + it('should return empty array for missing key', () => { + const result = createCustomerObjArr({}, 'customerPaymentMethods'); + expect(result.TAG).toBe('LoadedSavedCards'); + expect(result._0).toHaveLength(0); + }); + }); + + describe('getCustomerMethods', () => { + it('should return LoadingSavedCards for empty array', () => { + const dict = { + customerPaymentMethods: [], + }; + const result = getCustomerMethods(dict, 'customerPaymentMethods'); + expect(result).toBe('LoadingSavedCards'); + }); + + it('should return LoadedSavedCards for non-empty array', () => { + const dict = { + customerPaymentMethods: [ + { + payment_token: 'tok_123', + customer_id: 'cust_123', + payment_method: 'card', + payment_method_id: 'pm_123', + }, + ], + }; + const result = getCustomerMethods(dict, 'customerPaymentMethods'); + expect(typeof result).toBe('object'); + expect((result as any).TAG).toBe('LoadedSavedCards'); + }); + }); + + describe('getCustomMethodNames', () => { + it('should extract custom method names from dict', () => { + const dict = { + customMethodNames: [ + { paymentMethodName: 'method1', aliasName: 'Method One' }, + { paymentMethodName: 'method2', aliasName: 'Method Two' }, + ], + }; + const result = getCustomMethodNames(dict, 'customMethodNames'); + expect(result).toHaveLength(2); + expect(result[0].paymentMethodName).toBe('method1'); + }); + + it('should return empty array for missing key', () => { + const result = getCustomMethodNames({}, 'customMethodNames'); + expect(result).toHaveLength(0); + }); + }); + + describe('getBillingAddress', () => { + const mockLogger = { + setLogInfo: jest.fn(), + setLogError: jest.fn(), + }; + + it('should extract billing address from dict', () => { + const dict = { + billingAddress: { + isUseBillingAddress: true, + usePrefilledValues: 'auto', + }, + }; + const result = getBillingAddress(dict, 'billingAddress', mockLogger); + expect(result.isUseBillingAddress).toBe(true); + expect(result.usePrefilledValues).toBe('Auto'); + }); + + it('should return defaultBillingAddress when key not present', () => { + const result = getBillingAddress({}, 'billingAddress', mockLogger); + expect(result.isUseBillingAddress).toBe(false); + }); + }); + + describe('sanitizePaymentElementOptions', () => { + it('should sanitize payment element options', () => { + const dict = { + paymentId: 'pay_123', + layout: { type: 'tabs' }, + }; + const result = sanitizePaymentElementOptions(dict); + expect(result).toBeDefined(); + }); + }); + + describe('sanitizePreloadSdkParms', () => { + it('should sanitize preload SDK params', () => { + const dict = { + apiKey: 'secret_key', + merchantId: 'merchant_123', + }; + const result = sanitizePreloadSdkParms(dict); + expect(result).toBeDefined(); + }); + }); + + describe('itemToObjMapper', () => { + const mockLogger = { + setLogInfo: jest.fn(), + setLogError: jest.fn(), + }; + + it('should map dict to options object', () => { + const dict = { + displaySavedPaymentMethodsCheckbox: true, + displaySavedPaymentMethods: true, + savedPaymentMethodsCheckboxCheckedByDefault: false, + readOnly: false, + hideExpiredPaymentMethods: false, + displayDefaultSavedPaymentIcon: true, + hideCardNicknameField: false, + displayBillingDetails: false, + customMessageForCardTerms: '', + showShortSurchargeMessage: false, + }; + const result = itemToObjMapper(dict, mockLogger); + expect(result.displaySavedPaymentMethodsCheckbox).toBe(true); + expect(result.readOnly).toBe(false); + }); + }); + + describe('itemToPayerDetailsObjectMapper', () => { + it('should map payer details from dict', () => { + const dict = { + email_address: 'test@example.com', + phone: { + phone_number: { + national_number: '1234567890', + }, + }, + }; + const result = itemToPayerDetailsObjectMapper(dict); + expect(result.email).toBe('test@example.com'); + }); + + it('should handle missing fields', () => { + const result = itemToPayerDetailsObjectMapper({}); + expect(result.email).toBeUndefined(); + expect(result.phone).toBeUndefined(); + }); + }); + + describe('convertClickToPayCardToCustomerMethod', () => { + it('should convert Visa ClickToPay card to customer method', () => { + const clickToPayCard = { + srcDigitalCardId: 'card_123', + panLastFour: '4242', + panExpirationMonth: '12', + panExpirationYear: '2025', + paymentCardDescriptor: 'visa', + digitalCardData: { + descriptorName: 'My Visa', + }, + }; + const result = convertClickToPayCardToCustomerMethod(clickToPayCard, 'VISA'); + expect(result.paymentToken).toBe('card_123'); + expect(result.card.last4Digits).toBe('4242'); + expect(result.card.scheme).toBe('Visa'); + expect(result.paymentMethodType).toBe('click_to_pay'); + expect(result.card.isClickToPayCard).toBe(true); + }); + + it('should convert Mastercard ClickToPay card with Visa descriptor', () => { + const clickToPayCard = { + srcDigitalCardId: 'card_456', + panLastFour: '1234', + panExpirationMonth: '06', + panExpirationYear: '2026', + paymentCardDescriptor: 'mastercard', + digitalCardData: { + descriptorName: 'My MC', + }, + }; + const result = convertClickToPayCardToCustomerMethod(clickToPayCard, 'VISA'); + expect(result.card.scheme).toBe('Mastercard'); + }); + + it('should handle Mastercard provider with amex descriptor', () => { + const clickToPayCard = { + srcDigitalCardId: 'card_789', + panLastFour: '3782', + panExpirationMonth: '01', + panExpirationYear: '2027', + paymentCardDescriptor: 'amex', + digitalCardData: { + descriptorName: 'My Amex', + }, + }; + const result = convertClickToPayCardToCustomerMethod(clickToPayCard, 'MASTERCARD'); + expect(result.card.scheme).toBe('AmericanExpress'); + }); + + it('should handle Mastercard provider with discover descriptor', () => { + const clickToPayCard = { + srcDigitalCardId: 'card_discover', + panLastFour: '6011', + panExpirationMonth: '03', + panExpirationYear: '2028', + paymentCardDescriptor: 'discover', + digitalCardData: { + descriptorName: 'My Discover', + }, + }; + const result = convertClickToPayCardToCustomerMethod(clickToPayCard, 'MASTERCARD'); + expect(result.card.scheme).toBe('Discover'); + }); + + it('should handle Mastercard provider with mastercard descriptor', () => { + const clickToPayCard = { + srcDigitalCardId: 'card_mc', + panLastFour: '5425', + panExpirationMonth: '05', + panExpirationYear: '2029', + paymentCardDescriptor: 'mastercard', + digitalCardData: { + descriptorName: 'My MC', + }, + }; + const result = convertClickToPayCardToCustomerMethod(clickToPayCard, 'MASTERCARD'); + expect(result.card.scheme).toBe('Mastercard'); + }); + + it('should handle Mastercard provider with visa descriptor', () => { + const clickToPayCard = { + srcDigitalCardId: 'card_visa', + panLastFour: '4111', + panExpirationMonth: '07', + panExpirationYear: '2030', + paymentCardDescriptor: 'visa', + digitalCardData: { + descriptorName: 'My Visa', + }, + }; + const result = convertClickToPayCardToCustomerMethod(clickToPayCard, 'MASTERCARD'); + expect(result.card.scheme).toBe('Visa'); + }); + + it('should handle Mastercard provider with unknown descriptor', () => { + const clickToPayCard = { + srcDigitalCardId: 'card_unknown', + panLastFour: '9999', + panExpirationMonth: '09', + panExpirationYear: '2031', + paymentCardDescriptor: 'unknowncard', + digitalCardData: { + descriptorName: 'My Card', + }, + }; + const result = convertClickToPayCardToCustomerMethod(clickToPayCard, 'MASTERCARD'); + expect(result.card.scheme).toBe('Unknowncard'); + }); + + it('should handle NONE provider', () => { + const clickToPayCard = { + srcDigitalCardId: 'card_none', + panLastFour: '0000', + panExpirationMonth: '11', + panExpirationYear: '2032', + paymentCardDescriptor: 'visa', + digitalCardData: { + descriptorName: 'My Card', + }, + }; + const result = convertClickToPayCardToCustomerMethod(clickToPayCard, 'NONE'); + expect(result.card.scheme).toBeUndefined(); + }); + }); +}); diff --git a/src/__tests__/PaymentUtils.test.ts b/src/__tests__/PaymentUtils.test.ts new file mode 100644 index 000000000..ac14af0ab --- /dev/null +++ b/src/__tests__/PaymentUtils.test.ts @@ -0,0 +1,1523 @@ +import * as PaymentUtils from '../Utilities/PaymentUtils.bs.js'; + +describe('PaymentUtils', () => { + describe('getMethod', () => { + it('returns "crypto" for non-object method', () => { + expect(PaymentUtils.getMethod('Crypto')).toBe('crypto'); + }); + + it('returns "pay_later" for PayLater TAG', () => { + const method = { TAG: 'PayLater', _0: { TAG: 'Klarna', _0: {} } }; + expect(PaymentUtils.getMethod(method)).toBe('pay_later'); + }); + + it('returns "wallet" for Wallets TAG', () => { + const method = { TAG: 'Wallets', _0: { TAG: 'Gpay', _0: {} } }; + expect(PaymentUtils.getMethod(method)).toBe('wallet'); + }); + + it('returns "card" for Cards TAG', () => { + const method = { TAG: 'Cards', _0: {} }; + expect(PaymentUtils.getMethod(method)).toBe('card'); + }); + + it('returns "bank_redirect" for Banks TAG', () => { + const method = { TAG: 'Banks', _0: 'Sofort' }; + expect(PaymentUtils.getMethod(method)).toBe('bank_redirect'); + }); + + it('returns "bank_transfer" for BankTransfer TAG', () => { + const method = { TAG: 'BankTransfer', _0: 'ACH' }; + expect(PaymentUtils.getMethod(method)).toBe('bank_transfer'); + }); + + it('returns "bank_debit" for BankDebit TAG', () => { + const method = { TAG: 'BankDebit', _0: 'Sepa' }; + expect(PaymentUtils.getMethod(method)).toBe('bank_debit'); + }); + }); + + describe('getMethodType', () => { + it('returns "crypto_currency" for non-object method', () => { + expect(PaymentUtils.getMethodType('Crypto')).toBe('crypto_currency'); + }); + + it('returns "klarna" for PayLater/Klarna', () => { + const method = { TAG: 'PayLater', _0: { TAG: 'Klarna', _0: {} } }; + expect(PaymentUtils.getMethodType(method)).toBe('klarna'); + }); + + it('returns "afterpay_clearpay" for PayLater/AfterPay', () => { + const method = { TAG: 'PayLater', _0: { TAG: 'AfterPay', _0: {} } }; + expect(PaymentUtils.getMethodType(method)).toBe('afterpay_clearpay'); + }); + + it('returns "affirm" for PayLater/Affirm', () => { + const method = { TAG: 'PayLater', _0: { TAG: 'Affirm', _0: {} } }; + expect(PaymentUtils.getMethodType(method)).toBe('affirm'); + }); + + it('returns "google_pay" for Wallets/Gpay', () => { + const method = { TAG: 'Wallets', _0: { TAG: 'Gpay', _0: {} } }; + expect(PaymentUtils.getMethodType(method)).toBe('google_pay'); + }); + + it('returns "apple_pay" for Wallets/ApplePay', () => { + const method = { TAG: 'Wallets', _0: { TAG: 'ApplePay', _0: {} } }; + expect(PaymentUtils.getMethodType(method)).toBe('apple_pay'); + }); + + it('returns "paypal" for Wallets/Paypal', () => { + const method = { TAG: 'Wallets', _0: { TAG: 'Paypal', _0: {} } }; + expect(PaymentUtils.getMethodType(method)).toBe('paypal'); + }); + + it('returns "card" for Cards TAG', () => { + const method = { TAG: 'Cards', _0: {} }; + expect(PaymentUtils.getMethodType(method)).toBe('card'); + }); + + it('returns "sofort" for Banks/Sofort', () => { + const method = { TAG: 'Banks', _0: 'Sofort' }; + expect(PaymentUtils.getMethodType(method)).toBe('sofort'); + }); + + it('returns "eps" for Banks/Eps', () => { + const method = { TAG: 'Banks', _0: 'Eps' }; + expect(PaymentUtils.getMethodType(method)).toBe('eps'); + }); + + it('returns "giropay" for Banks/GiroPay', () => { + const method = { TAG: 'Banks', _0: 'GiroPay' }; + expect(PaymentUtils.getMethodType(method)).toBe('giropay'); + }); + + it('returns "ideal" for Banks/Ideal', () => { + const method = { TAG: 'Banks', _0: 'Ideal' }; + expect(PaymentUtils.getMethodType(method)).toBe('ideal'); + }); + + it('returns "eft" for Banks/EFT', () => { + const method = { TAG: 'Banks', _0: 'EFT' }; + expect(PaymentUtils.getMethodType(method)).toBe('eft'); + }); + + it('returns "ach" for BankDebit/ACH', () => { + const method = { TAG: 'BankDebit', _0: 'ACH' }; + expect(PaymentUtils.getMethodType(method)).toBe('ach'); + }); + + it('returns "sepa" for BankDebit/Sepa', () => { + const method = { TAG: 'BankDebit', _0: 'Sepa' }; + expect(PaymentUtils.getMethodType(method)).toBe('sepa'); + }); + + it('returns "bacs" for BankDebit/Bacs', () => { + const method = { TAG: 'BankDebit', _0: 'Bacs' }; + expect(PaymentUtils.getMethodType(method)).toBe('bacs'); + }); + + it('returns "instant" for BankDebit/Instant', () => { + const method = { TAG: 'BankDebit', _0: 'Instant' }; + expect(PaymentUtils.getMethodType(method)).toBe('instant'); + }); + + it('returns "ach" for BankTransfer/ACH', () => { + const method = { TAG: 'BankTransfer', _0: 'ACH' }; + expect(PaymentUtils.getMethodType(method)).toBe('ach'); + }); + }); + + describe('getExperience', () => { + it('returns "redirect_to_url" for "Redirect"', () => { + expect(PaymentUtils.getExperience('Redirect')).toBe('redirect_to_url'); + }); + + it('returns "invoke_sdk_client" for any other value', () => { + expect(PaymentUtils.getExperience('InvokeSDK')).toBe('invoke_sdk_client'); + expect(PaymentUtils.getExperience('')).toBe('invoke_sdk_client'); + expect(PaymentUtils.getExperience('Other')).toBe('invoke_sdk_client'); + }); + }); + + describe('getPaymentExperienceType', () => { + it('returns "invoke_sdk_client" for "InvokeSDK"', () => { + expect(PaymentUtils.getPaymentExperienceType('InvokeSDK')).toBe('invoke_sdk_client'); + }); + + it('returns "redirect_to_url" for "RedirectToURL"', () => { + expect(PaymentUtils.getPaymentExperienceType('RedirectToURL')).toBe('redirect_to_url'); + }); + + it('returns "display_qr_code" for "QrFlow"', () => { + expect(PaymentUtils.getPaymentExperienceType('QrFlow')).toBe('display_qr_code'); + }); + + it('returns undefined for unknown value', () => { + expect(PaymentUtils.getPaymentExperienceType('Unknown')).toBeUndefined(); + }); + }); + + describe('getExperienceType', () => { + it('returns "redirect_to_url" for non-object method', () => { + expect(PaymentUtils.getExperienceType('Crypto')).toBe('redirect_to_url'); + }); + + it('returns "card" for Cards TAG', () => { + const method = { TAG: 'Cards', _0: {} }; + expect(PaymentUtils.getExperienceType(method)).toBe('card'); + }); + + it('returns empty string for Banks TAG', () => { + const method = { TAG: 'Banks', _0: 'Sofort' }; + expect(PaymentUtils.getExperienceType(method)).toBe(''); + }); + + it('returns empty string for BankDebit TAG', () => { + const method = { TAG: 'BankDebit', _0: 'ACH' }; + expect(PaymentUtils.getExperienceType(method)).toBe(''); + }); + + it('returns empty string for BankTransfer TAG', () => { + const method = { TAG: 'BankTransfer', _0: 'ACH' }; + expect(PaymentUtils.getExperienceType(method)).toBe(''); + }); + }); + + describe('getPaymentMethodName', () => { + it('removes "_debit" suffix for bank_debit type', () => { + expect(PaymentUtils.getPaymentMethodName('bank_debit', 'sepa_debit')).toBe('sepa'); + }); + + it('removes "_transfer" suffix for bank_transfer type not in bankTransferList', () => { + expect(PaymentUtils.getPaymentMethodName('bank_transfer', 'ach_transfer')).toBe('ach'); + }); + + it('returns original name for bank_transfer type in bankTransferList', () => { + expect(PaymentUtils.getPaymentMethodName('bank_transfer', 'pix')).toBe('pix'); + }); + + it('returns original name for other payment method types', () => { + expect(PaymentUtils.getPaymentMethodName('card', 'credit')).toBe('credit'); + expect(PaymentUtils.getPaymentMethodName('wallet', 'paypal')).toBe('paypal'); + }); + }); + + describe('isAppendingCustomerAcceptance', () => { + it('returns false for guest customer', () => { + expect(PaymentUtils.isAppendingCustomerAcceptance(true, 'NEW_MANDATE')).toBe(false); + expect(PaymentUtils.isAppendingCustomerAcceptance(true, 'SETUP_MANDATE')).toBe(false); + expect(PaymentUtils.isAppendingCustomerAcceptance(true, 'OTHER')).toBe(false); + }); + + it('returns true for non-guest with NEW_MANDATE payment type', () => { + expect(PaymentUtils.isAppendingCustomerAcceptance(false, 'NEW_MANDATE')).toBe(true); + }); + + it('returns true for non-guest with SETUP_MANDATE payment type', () => { + expect(PaymentUtils.isAppendingCustomerAcceptance(false, 'SETUP_MANDATE')).toBe(true); + }); + + it('returns false for non-guest with other payment types', () => { + expect(PaymentUtils.isAppendingCustomerAcceptance(false, 'OTHER')).toBe(false); + }); + }); + + describe('appendedCustomerAcceptance', () => { + it('appends customer_acceptance when conditions are met', () => { + const body: [string, any][] = [['key', 'value']]; + const result = PaymentUtils.appendedCustomerAcceptance(false, 'NEW_MANDATE', body); + expect(result.length).toBe(2); + expect(result[1][0]).toBe('customer_acceptance'); + }); + + it('returns original body when conditions are not met (guest)', () => { + const body: [string, any][] = [['key', 'value']]; + const result = PaymentUtils.appendedCustomerAcceptance(true, 'NEW_MANDATE', body); + expect(result.length).toBe(1); + expect(result).toBe(body); + }); + + it('returns original body when conditions are not met (wrong type)', () => { + const body: [string, any][] = [['key', 'value']]; + const result = PaymentUtils.appendedCustomerAcceptance(false, 'OTHER', body); + expect(result.length).toBe(1); + expect(result).toBe(body); + }); + }); + + describe('filterSavedMethodsByWalletReadiness', () => { + it('filters out apple_pay when not ready', () => { + const savedMethods = [ + { paymentMethodType: 'apple_pay' }, + { paymentMethodType: 'card' }, + ]; + const result = PaymentUtils.filterSavedMethodsByWalletReadiness(savedMethods, false, true); + expect(result.length).toBe(1); + expect(result[0].paymentMethodType).toBe('card'); + }); + + it('filters out google_pay when not ready', () => { + const savedMethods = [ + { paymentMethodType: 'google_pay' }, + { paymentMethodType: 'card' }, + ]; + const result = PaymentUtils.filterSavedMethodsByWalletReadiness(savedMethods, true, false); + expect(result.length).toBe(1); + expect(result[0].paymentMethodType).toBe('card'); + }); + + it('keeps apple_pay and google_pay when both ready', () => { + const savedMethods = [ + { paymentMethodType: 'apple_pay' }, + { paymentMethodType: 'google_pay' }, + ]; + const result = PaymentUtils.filterSavedMethodsByWalletReadiness(savedMethods, true, true); + expect(result.length).toBe(2); + }); + + it('keeps methods without paymentMethodType', () => { + const savedMethods = [ + { paymentMethod: 'card' }, + { paymentMethodType: 'apple_pay' }, + ]; + const result = PaymentUtils.filterSavedMethodsByWalletReadiness(savedMethods, false, false); + expect(result.length).toBe(1); + expect(result[0].paymentMethod).toBe('card'); + }); + + it('keeps other payment method types regardless of readiness', () => { + const savedMethods = [ + { paymentMethodType: 'paypal' }, + { paymentMethodType: 'card' }, + ]; + const result = PaymentUtils.filterSavedMethodsByWalletReadiness(savedMethods, false, false); + expect(result.length).toBe(2); + }); + + it('returns empty array for empty input', () => { + const result = PaymentUtils.filterSavedMethodsByWalletReadiness([], true, true); + expect(result).toEqual([]); + }); + }); + + describe('sortCustomerMethodsBasedOnPriority', () => { + it('sorts by priority array order', () => { + const sortArr = [ + { paymentMethod: 'card', paymentMethodType: 'visa' }, + { paymentMethod: 'wallet', paymentMethodType: 'paypal' }, + ]; + const priorityArr = ['paypal', 'visa']; + const result = PaymentUtils.sortCustomerMethodsBasedOnPriority(sortArr, priorityArr); + expect(result[0].paymentMethodType).toBe('paypal'); + expect(result[1].paymentMethodType).toBe('visa'); + }); + + it('returns original array when priority array is empty', () => { + const sortArr = [ + { paymentMethod: 'card', paymentMethodType: 'visa' }, + ]; + const result = PaymentUtils.sortCustomerMethodsBasedOnPriority(sortArr, []); + expect(result).toBe(sortArr); + }); + + it('places defaultPaymentMethodSet items first when displayDefaultSavedPaymentIcon is true', () => { + const sortArr = [ + { paymentMethod: 'card', paymentMethodType: 'visa' }, + { paymentMethod: 'card', paymentMethodType: 'mastercard', defaultPaymentMethodSet: true }, + ]; + const priorityArr = ['visa', 'mastercard']; + const result = PaymentUtils.sortCustomerMethodsBasedOnPriority(sortArr, priorityArr, true); + expect(result[0].paymentMethodType).toBe('mastercard'); + }); + + it('sorts by priority when displayDefaultSavedPaymentIcon is false', () => { + const sortArr = [ + { paymentMethod: 'card', paymentMethodType: 'visa' }, + { paymentMethod: 'card', paymentMethodType: 'mastercard', defaultPaymentMethodSet: true }, + ]; + const priorityArr = ['visa', 'mastercard']; + const result = PaymentUtils.sortCustomerMethodsBasedOnPriority(sortArr, priorityArr, false); + expect(result[0].paymentMethodType).toBe('visa'); + }); + + it('places items not in priority array at the end', () => { + const sortArr = [ + { paymentMethod: 'card', paymentMethodType: 'unknown' }, + { paymentMethod: 'card', paymentMethodType: 'visa' }, + ]; + const priorityArr = ['visa']; + const result = PaymentUtils.sortCustomerMethodsBasedOnPriority(sortArr, priorityArr); + expect(result.some((m: any) => m.paymentMethodType === 'visa')).toBe(true); + expect(result.some((m: any) => m.paymentMethodType === 'unknown')).toBe(true); + }); + + it('uses paymentMethod for card type', () => { + const sortArr = [ + { paymentMethod: 'card' }, + { paymentMethod: 'wallet', paymentMethodType: 'paypal' }, + ]; + const priorityArr = ['card', 'paypal']; + const result = PaymentUtils.sortCustomerMethodsBasedOnPriority(sortArr, priorityArr); + expect(result[0].paymentMethod).toBe('card'); + }); + }); + + describe('checkIsCardSupported', () => { + it('returns false for invalid card with empty brand', () => { + const result = PaymentUtils.checkIsCardSupported('123', '', ['visa']); + expect(result).toBe(false); + }); + + it('returns undefined for invalid card with brand', () => { + const result = PaymentUtils.checkIsCardSupported('123', 'Visa', ['visa']); + expect(result).toBeUndefined(); + }); + + it('returns true when brand is empty and card is valid', () => { + const result = PaymentUtils.checkIsCardSupported('4111111111111111', '', ['visa']); + expect(result).toBe(true); + }); + + it('returns true when brand is in supported list', () => { + const result = PaymentUtils.checkIsCardSupported('4111111111111111', 'Visa', ['visa', 'mastercard']); + expect(result).toBe(true); + }); + + it('returns false when brand is not in supported list', () => { + const result = PaymentUtils.checkIsCardSupported('4111111111111111', 'Visa', ['mastercard']); + expect(result).toBe(false); + }); + + it('returns true when supportedCardBrands is undefined', () => { + const result = PaymentUtils.checkIsCardSupported('4111111111111111', 'Visa', undefined); + expect(result).toBe(true); + }); + + it('handles case-insensitive brand matching', () => { + const result = PaymentUtils.checkIsCardSupported('4111111111111111', 'VISA', ['visa']); + expect(result).toBe(true); + }); + }); + + describe('checkRenderOrComp', () => { + it('returns true when walletOptions includes "paypal"', () => { + expect(PaymentUtils.checkRenderOrComp(['paypal'], false, false)).toBe(true); + }); + + it('returns true when isShowOrPayUsing is true', () => { + expect(PaymentUtils.checkRenderOrComp([], true, false)).toBe(true); + }); + + it('returns isShowOrPayUsingWhileLoading when neither condition is met', () => { + expect(PaymentUtils.checkRenderOrComp([], false, true)).toBe(true); + expect(PaymentUtils.checkRenderOrComp([], false, false)).toBe(false); + }); + + it('returns true when paypal is in options regardless of other params', () => { + expect(PaymentUtils.checkRenderOrComp(['card', 'paypal'], false, false)).toBe(true); + }); + }); + + describe('filterInstallmentPlansByPaymentMethod', () => { + it('returns available plans for matching payment method', () => { + const installmentOptions = [ + { + payment_method: 'card', + available_plans: [{ plan_id: 'plan1' }, { plan_id: 'plan2' }], + }, + { + payment_method: 'wallet', + available_plans: [{ plan_id: 'plan3' }], + }, + ]; + const result = PaymentUtils.filterInstallmentPlansByPaymentMethod(installmentOptions, 'card'); + expect(result.length).toBe(2); + expect(result[0].plan_id).toBe('plan1'); + }); + + it('returns empty array when payment method not found', () => { + const installmentOptions = [ + { payment_method: 'card', available_plans: [{ plan_id: 'plan1' }] }, + ]; + const result = PaymentUtils.filterInstallmentPlansByPaymentMethod(installmentOptions, 'wallet'); + expect(result).toEqual([]); + }); + + it('returns empty array for empty installment options', () => { + const result = PaymentUtils.filterInstallmentPlansByPaymentMethod([], 'card'); + expect(result).toEqual([]); + }); + + it('handles options without available_plans', () => { + const installmentOptions = [ + { payment_method: 'card' }, + ]; + const result = PaymentUtils.filterInstallmentPlansByPaymentMethod(installmentOptions, 'card'); + expect(result).toBeUndefined(); + }); + }); + + describe('getDisplayNameAndIcon', () => { + it('returns default name and icon when no custom name found', () => { + const customNames = [{ paymentMethodName: 'other', aliasName: 'Other Name' }]; + const result = PaymentUtils.getDisplayNameAndIcon(customNames, 'classic', 'Default Name', null); + expect(result[0]).toBe('Default Name'); + expect(result[1]).toBeNull(); + }); + + it('returns default name and icon for non-classic/evoucher custom names', () => { + const customNames = [{ paymentMethodName: 'other', aliasName: 'Other Name' }]; + const result = PaymentUtils.getDisplayNameAndIcon(customNames, 'other', 'Default Name', null); + expect(result[0]).toBe('Default Name'); + expect(result[1]).toBeNull(); + }); + + it('returns default when aliasName is empty', () => { + const customNames = [{ paymentMethodName: 'classic', aliasName: '' }]; + const result = PaymentUtils.getDisplayNameAndIcon(customNames, 'classic', 'Default Name', null); + expect(result[0]).toBe('Default Name'); + expect(result[1]).toBeNull(); + }); + + it('returns custom alias and icon for classic with alias', () => { + const customNames = [{ paymentMethodName: 'classic', aliasName: 'My Custom Card' }]; + const result = PaymentUtils.getDisplayNameAndIcon(customNames, 'classic', 'Default Name', null); + expect(result[0]).toBe('My Custom Card'); + expect(result[1]).toBeDefined(); + }); + + it('returns custom alias and icon for evoucher with alias', () => { + const customNames = [{ paymentMethodName: 'evoucher', aliasName: 'My Voucher' }]; + const result = PaymentUtils.getDisplayNameAndIcon(customNames, 'evoucher', 'Default Name', null); + expect(result[0]).toBe('My Voucher'); + expect(result[1]).toBeDefined(); + }); + }); + + describe('getConnectors', () => { + it('returns empty arrays when payment method not found', () => { + const list = { payment_methods: [] }; + const method = { TAG: 'Cards', _0: {} }; + const result = PaymentUtils.getConnectors(list, method); + expect(result).toEqual([[], []]); + }); + + it('returns empty arrays when payment method type not found', () => { + const list = { + payment_methods: [ + { + payment_method: 'card', + payment_method_types: [], + }, + ], + }; + const method = { TAG: 'Cards', _0: {} }; + const result = PaymentUtils.getConnectors(list, method); + expect(result).toEqual([[], []]); + }); + }); + + describe('getSupportedCardBrands', () => { + it('returns undefined when card payment method not found', () => { + const paymentMethodListValue = { payment_methods: [] }; + const result = PaymentUtils.getSupportedCardBrands(paymentMethodListValue); + expect(result).toBeUndefined(); + }); + }); + + describe('emitMessage', () => { + it('calls messageParentWindow with payment info', () => { + const mockMessageParentWindow = jest.fn(); + jest.mock('../Utilities/Utils.bs.js', () => ({ + messageParentWindow: mockMessageParentWindow, + })); + + PaymentUtils.emitMessage({ paymentMethod: 'card' }); + }); + }); + + describe('emitPaymentMethodInfo', () => { + it('emits card payment info with card brand', () => { + PaymentUtils.emitPaymentMethodInfo( + 'card', + 'debit', + 'Visa', + '4242', + '424242', + '12', + '2025', + 'US', + 'CA', + '12345', + false, + false, + true + ); + }); + + it('emits non-card payment info without card fields', () => { + PaymentUtils.emitPaymentMethodInfo( + 'wallet', + 'apple_pay', + undefined, + undefined, + undefined, + undefined, + undefined, + 'US', + 'CA', + '12345', + false, + false, + false + ); + }); + + it('handles empty card brand', () => { + PaymentUtils.emitPaymentMethodInfo( + 'card', + 'credit', + undefined, + '', + '', + '', + '', + '', + '', + '', + false, + true, + true + ); + }); + + it('handles saved payment method', () => { + PaymentUtils.emitPaymentMethodInfo( + 'card', + 'debit', + 'Mastercard', + '5555', + '555555', + '06', + '2026', + 'GB', + 'London', + 'SW1A 1AA', + true, + false, + true + ); + }); + + it('handles bank debit payment method type', () => { + PaymentUtils.emitPaymentMethodInfo( + 'bank_debit', + 'ach_debit', + undefined, + undefined, + undefined, + undefined, + undefined, + 'US', + 'NY', + '10001', + false, + false, + false + ); + }); + + it('handles bank transfer payment method type', () => { + PaymentUtils.emitPaymentMethodInfo( + 'bank_transfer', + 'sepa_transfer', + undefined, + undefined, + undefined, + undefined, + undefined, + 'DE', + 'Berlin', + '10115', + false, + false, + false + ); + }); + }); + + describe('getExperienceType - additional cases', () => { + it('returns experience for PayLater with experience object', () => { + const method = { + TAG: 'PayLater', + _0: { TAG: 'Klarna', _0: { _0: { TAG: 'Redirect' } } }, + }; + const result = PaymentUtils.getExperienceType(method); + expect(result).toBeDefined(); + }); + + it('returns experience for Wallets with experience object', () => { + const method = { + TAG: 'Wallets', + _0: { TAG: 'Gpay', _0: { _0: { TAG: 'InvokeSDK' } } }, + }; + const result = PaymentUtils.getExperienceType(method); + expect(result).toBeDefined(); + }); + }); + + describe('getConnectors - additional cases', () => { + it('returns empty arrays for Cards without matching experience', () => { + const list = { + payment_methods: [ + { + payment_method: 'card', + payment_method_types: [ + { + payment_method_type: 'credit', + payment_experience: [], + }, + ], + }, + ], + }; + const method = { + TAG: 'Cards', + _0: {}, + }; + const result = PaymentUtils.getConnectors(list, method); + expect(result).toEqual([[], []]); + }); + + it('returns bank names for Banks', () => { + const list = { + payment_methods: [ + { + payment_method: 'bank_redirect', + payment_method_types: [ + { + payment_method_type: 'ideal', + bank_names: ['ing', 'rabobank'], + payment_experience: [], + }, + ], + }, + ], + }; + const method = { + TAG: 'Banks', + _0: 'Ideal', + }; + const result = PaymentUtils.getConnectors(list, method); + expect(result[1]).toEqual(['ing', 'rabobank']); + }); + + it('returns bank_transfers_connectors for BankTransfer', () => { + const list = { + payment_methods: [ + { + payment_method: 'bank_transfer', + payment_method_types: [ + { + payment_method_type: 'sepa', + bank_transfers_connectors: ['stripe'], + payment_experience: [], + }, + ], + }, + ], + }; + const method = { + TAG: 'BankTransfer', + _0: 'Sepa', + }; + const result = PaymentUtils.getConnectors(list, method); + expect(result[0]).toEqual(['stripe']); + }); + + it('returns bank_debits_connectors for BankDebit', () => { + const list = { + payment_methods: [ + { + payment_method: 'bank_debit', + payment_method_types: [ + { + payment_method_type: 'ach', + bank_debits_connectors: ['stripe'], + payment_experience: [], + }, + ], + }, + ], + }; + const method = { + TAG: 'BankDebit', + _0: 'ACH', + }; + const result = PaymentUtils.getConnectors(list, method); + expect(result[0]).toEqual(['stripe']); + }); + + it('returns empty arrays for PayLater without matching experience', () => { + const list = { + payment_methods: [ + { + payment_method: 'pay_later', + payment_method_types: [ + { + payment_method_type: 'klarna', + payment_experience: [], + }, + ], + }, + ], + }; + const method = { + TAG: 'PayLater', + _0: { TAG: 'Klarna', _0: {} }, + }; + const result = PaymentUtils.getConnectors(list, method); + expect(result).toEqual([[], []]); + }); + + it('returns empty arrays for Wallets without matching experience', () => { + const list = { + payment_methods: [ + { + payment_method: 'wallet', + payment_method_types: [ + { + payment_method_type: 'google_pay', + payment_experience: [], + }, + ], + }, + ], + }; + const method = { + TAG: 'Wallets', + _0: { TAG: 'Gpay', _0: {} }, + }; + const result = PaymentUtils.getConnectors(list, method); + expect(result).toEqual([[], []]); + }); + }); + + describe('getIsKlarnaSDKFlow', () => { + it('returns true when Klarna token exists in OtherTokenOptional', () => { + const sessions = JSON.stringify({ + sessionsToken: [{ wallet_name: 'Klarna', token: 'klarna_token' }], + }); + + const result = PaymentUtils.getIsKlarnaSDKFlow(sessions); + expect(typeof result).toBe('boolean'); + }); + + it('returns false for empty sessions', () => { + const result = PaymentUtils.getIsKlarnaSDKFlow('{}'); + expect(typeof result).toBe('boolean'); + }); + + it('handles invalid JSON', () => { + const result = PaymentUtils.getIsKlarnaSDKFlow('invalid json'); + expect(typeof result).toBe('boolean'); + }); + + it('handles null sessions', () => { + const result = PaymentUtils.getIsKlarnaSDKFlow(null); + expect(typeof result).toBe('boolean'); + }); + }); + + describe('getStateJson', () => { + it('returns a promise', async () => { + const result = PaymentUtils.getStateJson(); + expect(result).toBeInstanceOf(Promise); + }); + + it('handles successful fetch', async () => { + const result = await PaymentUtils.getStateJson(); + expect(result).toBeDefined(); + }); + }); + + describe('getDisplayNameAndIcon - edge cases', () => { + it('handles empty customNames array', () => { + const result = PaymentUtils.getDisplayNameAndIcon([], 'classic', 'Default', null); + expect(result[0]).toBe('Default'); + expect(result[1]).toBeNull(); + }); + + it('handles multiple custom names but none matching', () => { + const customNames = [ + { paymentMethodName: 'other', aliasName: 'Other Name' }, + { paymentMethodName: 'another', aliasName: 'Another Name' }, + ]; + const result = PaymentUtils.getDisplayNameAndIcon(customNames, 'classic', 'Default', null); + expect(result[0]).toBe('Default'); + }); + + it('handles classic with multi-word alias', () => { + const customNames = [{ paymentMethodName: 'classic', aliasName: 'Premium Card' }]; + const result = PaymentUtils.getDisplayNameAndIcon(customNames, 'classic', 'Default', null); + expect(result[0]).toBe('Premium Card'); + expect(result[1]).toBeDefined(); + }); + + it('handles evoucher with single word alias', () => { + const customNames = [{ paymentMethodName: 'evoucher', aliasName: 'GiftCard' }]; + const result = PaymentUtils.getDisplayNameAndIcon(customNames, 'evoucher', 'Default', null); + expect(result[0]).toBe('GiftCard'); + expect(result[1]).toBeDefined(); + }); + }); + + describe('getMethod - edge cases', () => { + it('handles string method', () => { + expect(PaymentUtils.getMethod('Crypto')).toBe('crypto'); + }); + + it('handles number as method', () => { + expect(PaymentUtils.getMethod(123 as any)).toBe('crypto'); + }); + }); + + describe('getMethodType - edge cases', () => { + it('handles string method', () => { + expect(PaymentUtils.getMethodType('Crypto')).toBe('crypto_currency'); + }); + + it('handles BankTransfer with different values', () => { + const method = { TAG: 'BankTransfer', _0: 'Sepa' }; + expect(PaymentUtils.getMethodType(method)).toBe('sepa'); + }); + + it('handles BankTransfer with Bacs', () => { + const method = { TAG: 'BankTransfer', _0: 'Bacs' }; + expect(PaymentUtils.getMethodType(method)).toBe('bacs'); + }); + + it('handles BankTransfer with Instant', () => { + const method = { TAG: 'BankTransfer', _0: 'Instant' }; + expect(PaymentUtils.getMethodType(method)).toBe('instant'); + }); + }); + + describe('getPaymentMethodName - additional cases', () => { + it('handles card type', () => { + expect(PaymentUtils.getPaymentMethodName('card', 'credit')).toBe('credit'); + }); + + it('handles wallet type', () => { + expect(PaymentUtils.getPaymentMethodName('wallet', 'apple_pay')).toBe('apple_pay'); + }); + + it('handles bank_debit with ach', () => { + expect(PaymentUtils.getPaymentMethodName('bank_debit', 'ach_debit')).toBe('ach'); + }); + + it('handles bank_debit with sepa', () => { + expect(PaymentUtils.getPaymentMethodName('bank_debit', 'sepa_debit')).toBe('sepa'); + }); + + it('handles bank_transfer with pix (in bankTransferList)', () => { + expect(PaymentUtils.getPaymentMethodName('bank_transfer', 'pix')).toBe('pix'); + }); + + it('handles bank_transfer with sepa (not in bankTransferList)', () => { + expect(PaymentUtils.getPaymentMethodName('bank_transfer', 'sepa_transfer')).toBe('sepa'); + }); + }); + + describe('isAppendingCustomerAcceptance - additional cases', () => { + it('returns false for guest with any type', () => { + expect(PaymentUtils.isAppendingCustomerAcceptance(true, 'NEW_MANDATE')).toBe(false); + expect(PaymentUtils.isAppendingCustomerAcceptance(true, 'SETUP_MANDATE')).toBe(false); + expect(PaymentUtils.isAppendingCustomerAcceptance(true, 'NORMAL')).toBe(false); + }); + + it('returns true for non-guest with NEW_MANDATE', () => { + expect(PaymentUtils.isAppendingCustomerAcceptance(false, 'NEW_MANDATE')).toBe(true); + }); + + it('returns true for non-guest with SETUP_MANDATE', () => { + expect(PaymentUtils.isAppendingCustomerAcceptance(false, 'SETUP_MANDATE')).toBe(true); + }); + + it('returns false for non-guest with other types', () => { + expect(PaymentUtils.isAppendingCustomerAcceptance(false, 'NORMAL')).toBe(false); + expect(PaymentUtils.isAppendingCustomerAcceptance(false, 'RECURRING')).toBe(false); + }); + }); + + describe('appendedCustomerAcceptance - additional cases', () => { + it('does not append when guest', () => { + const body: [string, any][] = [['key', 'value']]; + const result = PaymentUtils.appendedCustomerAcceptance(true, 'NEW_MANDATE', body); + expect(result.length).toBe(1); + }); + + it('does not append when wrong type', () => { + const body: [string, any][] = [['key', 'value']]; + const result = PaymentUtils.appendedCustomerAcceptance(false, 'NORMAL', body); + expect(result.length).toBe(1); + }); + + it('appends when conditions met', () => { + const body: [string, any][] = [['key', 'value']]; + const result = PaymentUtils.appendedCustomerAcceptance(false, 'NEW_MANDATE', body); + expect(result.length).toBe(2); + expect(result[1][0]).toBe('customer_acceptance'); + }); + }); + + describe('checkIsCardSupported - additional cases', () => { + it('returns false for invalid short card', () => { + const result = PaymentUtils.checkIsCardSupported('123', '', ['visa']); + expect(result).toBe(false); + }); + + it('returns undefined for invalid card with brand', () => { + const result = PaymentUtils.checkIsCardSupported('123', 'Visa', ['visa']); + expect(result).toBeUndefined(); + }); + + it('handles case insensitive brand matching', () => { + const result = PaymentUtils.checkIsCardSupported('4111111111111111', 'VISA', ['visa']); + expect(result).toBe(true); + }); + + it('returns false when brand not in supported list', () => { + const result = PaymentUtils.checkIsCardSupported('4111111111111111', 'Visa', ['mastercard', 'amex']); + expect(result).toBe(false); + }); + + it('returns true when supportedCardBrands is undefined', () => { + const result = PaymentUtils.checkIsCardSupported('4111111111111111', 'Visa', undefined); + expect(result).toBe(true); + }); + }); + + describe('filterSavedMethodsByWalletReadiness - additional cases', () => { + it('keeps all methods when both wallets ready', () => { + const savedMethods = [ + { paymentMethodType: 'apple_pay' }, + { paymentMethodType: 'google_pay' }, + { paymentMethodType: 'card' }, + ]; + const result = PaymentUtils.filterSavedMethodsByWalletReadiness(savedMethods, true, true); + expect(result.length).toBe(3); + }); + + it('filters apple_pay when not ready', () => { + const savedMethods = [ + { paymentMethodType: 'apple_pay' }, + { paymentMethodType: 'google_pay' }, + ]; + const result = PaymentUtils.filterSavedMethodsByWalletReadiness(savedMethods, false, true); + expect(result.length).toBe(1); + expect(result[0].paymentMethodType).toBe('google_pay'); + }); + + it('filters google_pay when not ready', () => { + const savedMethods = [ + { paymentMethodType: 'apple_pay' }, + { paymentMethodType: 'google_pay' }, + ]; + const result = PaymentUtils.filterSavedMethodsByWalletReadiness(savedMethods, true, false); + expect(result.length).toBe(1); + expect(result[0].paymentMethodType).toBe('apple_pay'); + }); + + it('keeps methods without paymentMethodType', () => { + const savedMethods = [ + { paymentMethod: 'card' }, + { paymentMethodType: undefined }, + ]; + const result = PaymentUtils.filterSavedMethodsByWalletReadiness(savedMethods, false, false); + expect(result.length).toBe(2); + }); + + it('keeps other payment method types', () => { + const savedMethods = [ + { paymentMethodType: 'paypal' }, + { paymentMethodType: 'klarna' }, + ]; + const result = PaymentUtils.filterSavedMethodsByWalletReadiness(savedMethods, false, false); + expect(result.length).toBe(2); + }); + }); + + describe('sortCustomerMethodsBasedOnPriority - additional cases', () => { + it('returns original array when priority is empty', () => { + const sortArr = [{ paymentMethod: 'card' }]; + const result = PaymentUtils.sortCustomerMethodsBasedOnPriority(sortArr, []); + expect(result).toBe(sortArr); + }); + + it('sorts by priority order', () => { + const sortArr = [ + { paymentMethod: 'card', paymentMethodType: 'visa' }, + { paymentMethod: 'wallet', paymentMethodType: 'paypal' }, + { paymentMethod: 'card', paymentMethodType: 'mastercard' }, + ]; + const priorityArr = ['paypal', 'mastercard', 'visa']; + const result = PaymentUtils.sortCustomerMethodsBasedOnPriority(sortArr, priorityArr); + expect(result[0].paymentMethodType).toBe('paypal'); + }); + + it('places items not in priority at end', () => { + const sortArr = [ + { paymentMethod: 'card', paymentMethodType: 'unknown' }, + { paymentMethod: 'card', paymentMethodType: 'visa' }, + ]; + const priorityArr = ['visa']; + const result = PaymentUtils.sortCustomerMethodsBasedOnPriority(sortArr, priorityArr); + expect(result.some((m: any) => m.paymentMethodType === 'visa')).toBe(true); + expect(result.some((m: any) => m.paymentMethodType === 'unknown')).toBe(true); + }); + + it('handles defaultPaymentMethodSet with displayDefaultSavedPaymentIcon true', () => { + const sortArr = [ + { paymentMethod: 'card', paymentMethodType: 'visa' }, + { paymentMethod: 'card', paymentMethodType: 'mastercard', defaultPaymentMethodSet: true }, + ]; + const priorityArr = ['visa', 'mastercard']; + const result = PaymentUtils.sortCustomerMethodsBasedOnPriority(sortArr, priorityArr, true); + expect(result[0].paymentMethodType).toBe('mastercard'); + }); + + it('handles defaultPaymentMethodSet with displayDefaultSavedPaymentIcon false', () => { + const sortArr = [ + { paymentMethod: 'card', paymentMethodType: 'visa' }, + { paymentMethod: 'card', paymentMethodType: 'mastercard', defaultPaymentMethodSet: true }, + ]; + const priorityArr = ['visa', 'mastercard']; + const result = PaymentUtils.sortCustomerMethodsBasedOnPriority(sortArr, priorityArr, false); + expect(result[0].paymentMethodType).toBe('visa'); + }); + + it('uses paymentMethod for card type', () => { + const sortArr = [ + { paymentMethod: 'card' }, + { paymentMethod: 'wallet', paymentMethodType: 'paypal' }, + ]; + const priorityArr = ['card', 'paypal']; + const result = PaymentUtils.sortCustomerMethodsBasedOnPriority(sortArr, priorityArr); + expect(result[0].paymentMethod).toBe('card'); + }); + }); + + describe('checkRenderOrComp - additional cases', () => { + it('returns true when paypal in options', () => { + expect(PaymentUtils.checkRenderOrComp(['paypal'], false, false)).toBe(true); + }); + + it('returns true when isShowOrPayUsing is true', () => { + expect(PaymentUtils.checkRenderOrComp([], true, false)).toBe(true); + }); + + it('returns isShowOrPayUsingWhileLoading when neither condition', () => { + expect(PaymentUtils.checkRenderOrComp([], false, true)).toBe(true); + expect(PaymentUtils.checkRenderOrComp([], false, false)).toBe(false); + }); + + it('handles multiple wallet options', () => { + expect(PaymentUtils.checkRenderOrComp(['card', 'apple_pay', 'paypal'], false, false)).toBe(true); + }); + }); + + describe('filterInstallmentPlansByPaymentMethod - additional cases', () => { + it('returns plans for matching payment method', () => { + const options = [ + { payment_method: 'card', available_plans: [{ id: 1 }, { id: 2 }] }, + { payment_method: 'wallet', available_plans: [{ id: 3 }] }, + ]; + const result = PaymentUtils.filterInstallmentPlansByPaymentMethod(options, 'card'); + expect(result.length).toBe(2); + }); + + it('returns empty array when no match', () => { + const options = [{ payment_method: 'card', available_plans: [] }]; + const result = PaymentUtils.filterInstallmentPlansByPaymentMethod(options, 'wallet'); + expect(result).toEqual([]); + }); + + it('handles empty options', () => { + const result = PaymentUtils.filterInstallmentPlansByPaymentMethod([], 'card'); + expect(result).toEqual([]); + }); + + it('handles options without available_plans', () => { + const options = [{ payment_method: 'card' }]; + const result = PaymentUtils.filterInstallmentPlansByPaymentMethod(options, 'card'); + expect(result).toBeUndefined(); + }); + }); + + describe('paymentListLookupNew', () => { + it('returns empty arrays for empty payment list', () => { + const mockList = { payment_methods: [] }; + const mockPmlValue = { + payment_methods: [], + collect_billing_details_from_wallets: false, + }; + + const result = PaymentUtils.paymentListLookupNew( + mockList, + ['card'], + true, + false, + false, + mockPmlValue, + false, + false, + false, + false, + { wallet_payment_method: () => 'Wallet' }, + false, + false + ); + + expect(Array.isArray(result)).toBe(true); + expect(result.length).toBe(2); + }); + + it('handles wallet payment methods', () => { + const mockList = { + payment_methods: [ + { payment_method: 'wallet', payment_method_types: [] } + ] + }; + const mockPmlValue = { + payment_methods: [], + collect_billing_details_from_wallets: false, + }; + + const result = PaymentUtils.paymentListLookupNew( + mockList, + [], + false, + false, + false, + mockPmlValue, + false, + false, + false, + false, + { wallet_payment_method: () => 'Wallet' }, + false, + false + ); + + expect(Array.isArray(result)).toBe(true); + }); + }); + + describe('getConnectors - extended tests', () => { + it('returns eligible connectors for wallet payment', () => { + const list = { + payment_methods: [ + { + payment_method: 'wallet', + payment_method_types: [ + { + payment_method_type: 'google_pay', + payment_experience: [ + { + payment_experience_type: 'InvokeSDK', + eligible_connectors: ['stripe', 'adyen'] + } + ], + bank_names: [] + } + ] + } + ] + }; + const method = { + TAG: 'Wallets', + _0: { TAG: 'Gpay', _0: { _0: { TAG: 'InvokeSDK' } } } + }; + const result = PaymentUtils.getConnectors(list, method); + expect(result[0]).toEqual(['stripe', 'adyen']); + }); + + it('returns empty arrays for crypto', () => { + const list = { payment_methods: [] }; + const result = PaymentUtils.getConnectors(list, 'Crypto'); + expect(result).toEqual([[], []]); + }); + }); + + describe('getIsKlarnaSDKFlow - extended tests', () => { + it('returns false for Klarna token without OtherTokenOptional', () => { + const sessions = JSON.stringify({ + sessionsToken: [{ wallet_name: 'Klarna', token: 'klarna_token' }] + }); + const result = PaymentUtils.getIsKlarnaSDKFlow(sessions); + expect(typeof result).toBe('boolean'); + }); + + it('handles malformed JSON', () => { + const result = PaymentUtils.getIsKlarnaSDKFlow('{invalid}'); + expect(typeof result).toBe('boolean'); + }); + }); + + describe('getSupportedCardBrands - extended tests', () => { + it('returns array of supported card brands', () => { + const paymentMethodListValue = { + payment_methods: [ + { + payment_method: 'card', + payment_method_types: [ + { + card_networks: [ + { card_network: 'VISA' }, + { card_network: 'MASTERCARD' } + ] + } + ] + } + ] + }; + const result = PaymentUtils.getSupportedCardBrands(paymentMethodListValue); + expect(Array.isArray(result)).toBe(true); + }); + }); + + describe('emitPaymentMethodInfo - extended tests', () => { + it('handles bank_debit payment method type suffix', () => { + PaymentUtils.emitPaymentMethodInfo( + 'bank_debit', + 'ach_debit', + undefined, + undefined, + undefined, + undefined, + undefined, + 'US', + 'CA', + '12345', + false, + false, + false + ); + }); + + it('handles bank_transfer payment method type suffix', () => { + PaymentUtils.emitPaymentMethodInfo( + 'bank_transfer', + 'sepa_transfer', + undefined, + undefined, + undefined, + undefined, + undefined, + 'DE', + 'Berlin', + '10115', + false, + false, + false + ); + }); + + it('handles non-card payment with NOTFOUND brand', () => { + PaymentUtils.emitPaymentMethodInfo( + 'wallet', + 'paypal', + 'NOTFOUND', + '', + '', + '', + '', + 'US', + '', + '', + false, + false, + false + ); + }); + + it('handles card payment with valid brand and CVC', () => { + PaymentUtils.emitPaymentMethodInfo( + 'card', + 'credit', + 'VISA', + '4242', + '424242', + '12', + '2025', + 'US', + 'CA', + '12345', + true, + false, + true + ); + }); + }); + + describe('sortCustomerMethodsBasedOnPriority - extended tests', () => { + it('handles empty sort array', () => { + const result = PaymentUtils.sortCustomerMethodsBasedOnPriority([], ['visa']); + expect(result).toEqual([]); + }); + + it('handles paymentMethodType undefined', () => { + const sortArr = [ + { paymentMethod: 'wallet', paymentMethodType: undefined }, + { paymentMethod: 'card' } + ]; + const priorityArr = ['card']; + const result = PaymentUtils.sortCustomerMethodsBasedOnPriority(sortArr, priorityArr); + expect(result.length).toBe(2); + }); + + it('handles both items with defaultPaymentMethodSet', () => { + const sortArr = [ + { paymentMethod: 'card', paymentMethodType: 'visa', defaultPaymentMethodSet: true }, + { paymentMethod: 'card', paymentMethodType: 'mastercard', defaultPaymentMethodSet: true } + ]; + const priorityArr = ['visa', 'mastercard']; + const result = PaymentUtils.sortCustomerMethodsBasedOnPriority(sortArr, priorityArr, true); + expect(result.length).toBe(2); + }); + }); + + describe('checkIsCardSupported - extended tests', () => { + it('returns undefined for invalid card with brand', () => { + const result = PaymentUtils.checkIsCardSupported('1234567890123456', 'Visa', ['visa']); + expect(result).toBeUndefined(); + }); + + it('handles empty supportedCardBrands array', () => { + const result = PaymentUtils.checkIsCardSupported('4111111111111111', 'Visa', []); + expect(result).toBe(false); + }); + + it('handles whitespace in card number', () => { + const result = PaymentUtils.checkIsCardSupported('4111 1111 1111 1111', 'Visa', ['visa']); + expect(result).toBe(true); + }); + }); + + describe('checkRenderOrComp - extended tests', () => { + it('returns true when multiple wallet options include paypal', () => { + expect(PaymentUtils.checkRenderOrComp(['apple_pay', 'google_pay', 'paypal'], false, false)).toBe(true); + }); + + it('returns false when no conditions met', () => { + expect(PaymentUtils.checkRenderOrComp(['card', 'bank_transfer'], false, false)).toBe(false); + }); + + it('prioritizes paypal over isShowOrPayUsingWhileLoading', () => { + expect(PaymentUtils.checkRenderOrComp(['paypal'], false, false)).toBe(true); + }); + }); + + describe('filterInstallmentPlansByPaymentMethod - extended tests', () => { + it('throws for null installment options', () => { + expect(() => PaymentUtils.filterInstallmentPlansByPaymentMethod(null as any, 'card')).toThrow(); + }); + + it('handles installment options with empty available_plans', () => { + const options = [{ payment_method: 'card', available_plans: [] }]; + const result = PaymentUtils.filterInstallmentPlansByPaymentMethod(options, 'card'); + expect(result).toEqual([]); + }); + }); + + describe('getDisplayNameAndIcon - extended tests', () => { + it('handles classic with single word alias', () => { + const customNames = [{ paymentMethodName: 'classic', aliasName: 'Premium' }]; + const result = PaymentUtils.getDisplayNameAndIcon(customNames, 'classic', 'Default', null); + expect(result[0]).toBe('Premium'); + }); + + it('handles evoucher with alias containing spaces', () => { + const customNames = [{ paymentMethodName: 'evoucher', aliasName: 'Gift Card' }]; + const result = PaymentUtils.getDisplayNameAndIcon(customNames, 'evoucher', 'Default', null); + expect(result[0]).toBe('Gift Card'); + }); + }); + + describe('getPaymentMethodName - extended tests', () => { + it('handles pix as bank_transfer', () => { + expect(PaymentUtils.getPaymentMethodName('bank_transfer', 'pix')).toBe('pix'); + }); + + it('handles generic payment method', () => { + expect(PaymentUtils.getPaymentMethodName('reward', 'points')).toBe('points'); + }); + }); + + describe('getMethod - extended tests', () => { + it('handles undefined method', () => { + expect(PaymentUtils.getMethod(undefined as any)).toBe('crypto'); + }); + + it('throws for null method', () => { + expect(() => PaymentUtils.getMethod(null as any)).toThrow(); + }); + }); + + describe('getMethodType - extended tests', () => { + it('handles undefined method', () => { + expect(PaymentUtils.getMethodType(undefined as any)).toBe('crypto_currency'); + }); + + it('throws for null method', () => { + expect(() => PaymentUtils.getMethodType(null as any)).toThrow(); + }); + }); + + describe('appendedCustomerAcceptance - extended tests', () => { + it('returns new array when appending', () => { + const body: [string, any][] = [['key', 'value']]; + const result = PaymentUtils.appendedCustomerAcceptance(false, 'SETUP_MANDATE', body); + expect(result.length).toBe(2); + expect(result).not.toBe(body); + }); + + it('returns same array when not appending', () => { + const body: [string, any][] = [['key', 'value']]; + const result = PaymentUtils.appendedCustomerAcceptance(true, 'NEW_MANDATE', body); + expect(result).toBe(body); + }); + }); + + describe('getExperienceType - extended tests', () => { + it('returns experience for PayLater with Redirect experience', () => { + const method = { + TAG: 'PayLater', + _0: { TAG: 'Klarna', _0: 'Redirect' } + }; + const result = PaymentUtils.getExperienceType(method); + expect(result).toBe('redirect_to_url'); + }); + + it('returns experience for Wallets with non-Redirect experience', () => { + const method = { + TAG: 'Wallets', + _0: { TAG: 'Gpay', _0: 'InvokeSDK' } + }; + const result = PaymentUtils.getExperienceType(method); + expect(result).toBe('invoke_sdk_client'); + }); + }); + + describe('getPaymentExperienceType - extended tests', () => { + it('returns undefined for unknown experience type', () => { + expect(PaymentUtils.getPaymentExperienceType('UnknownType')).toBeUndefined(); + }); + + it('returns correct value for QrFlow', () => { + expect(PaymentUtils.getPaymentExperienceType('QrFlow')).toBe('display_qr_code'); + }); + }); +}); diff --git a/src/__tests__/PaymentUtilsV2.test.ts b/src/__tests__/PaymentUtilsV2.test.ts new file mode 100644 index 000000000..2ec7d0ab9 --- /dev/null +++ b/src/__tests__/PaymentUtilsV2.test.ts @@ -0,0 +1,230 @@ +import { + paymentListLookupNew, + getCreditFieldsRequired, + getPaymentMethodTypeFromListV2, +} from '../Utilities/PaymentUtilsV2.bs.js'; +import { defaultPaymentMethods } from '../Utilities/UnifiedHelpersV2.bs.js'; + +describe('PaymentUtilsV2', () => { + describe('paymentListLookupNew', () => { + it('should return empty lists for empty payment methods', () => { + const paymentMethodListValue = { + paymentMethodsEnabled: [], + }; + + const result = paymentListLookupNew(paymentMethodListValue); + + expect(result.walletsList).toEqual([]); + expect(result.otherPaymentList).toEqual([]); + }); + + it('should add card to otherPaymentList when card payment method exists', () => { + const paymentMethodListValue = { + paymentMethodsEnabled: [ + { paymentMethodType: 'card' }, + { paymentMethodType: 'wallet' }, + ], + }; + + const result = paymentListLookupNew(paymentMethodListValue); + + expect(result.otherPaymentList).toContain('card'); + expect(result.walletsList).toEqual([]); + }); + + it('should not add non-card payment methods to otherPaymentList', () => { + const paymentMethodListValue = { + paymentMethodsEnabled: [ + { paymentMethodType: 'bank_redirect' }, + { paymentMethodType: 'wallet' }, + ], + }; + + const result = paymentListLookupNew(paymentMethodListValue); + + expect(result.otherPaymentList).toEqual([]); + }); + + it('should handle payment methods without paymentMethodType field', () => { + const paymentMethodListValue = { + paymentMethodsEnabled: [ + { paymentMethod: 'card' }, + { paymentMethodType: 'card' }, + ], + }; + + const result = paymentListLookupNew(paymentMethodListValue); + + expect(result.otherPaymentList).toContain('card'); + }); + + it('should remove duplicates from otherPaymentList', () => { + const paymentMethodListValue = { + paymentMethodsEnabled: [ + { paymentMethodType: 'card' }, + { paymentMethodType: 'card' }, + { paymentMethodType: 'card' }, + ], + }; + + const result = paymentListLookupNew(paymentMethodListValue); + + expect(result.otherPaymentList.length).toBe(1); + expect(result.otherPaymentList).toEqual(['card']); + }); + }); + + describe('getCreditFieldsRequired', () => { + it('should return credit card payment methods only', () => { + const paymentManagementListValue = { + paymentMethodsEnabled: [ + { paymentMethodType: 'card', paymentMethodSubtype: 'credit' }, + { paymentMethodType: 'card', paymentMethodSubtype: 'debit' }, + { paymentMethodType: 'wallet', paymentMethodSubtype: 'paypal' }, + ], + }; + + const result = getCreditFieldsRequired(paymentManagementListValue); + + expect(result.length).toBe(1); + expect(result[0].paymentMethodSubtype).toBe('credit'); + }); + + it('should return empty array when no credit cards exist', () => { + const paymentManagementListValue = { + paymentMethodsEnabled: [ + { paymentMethodType: 'card', paymentMethodSubtype: 'debit' }, + { paymentMethodType: 'wallet', paymentMethodSubtype: 'paypal' }, + ], + }; + + const result = getCreditFieldsRequired(paymentManagementListValue); + + expect(result).toEqual([]); + }); + + it('should return empty array for empty payment methods', () => { + const paymentManagementListValue = { + paymentMethodsEnabled: [], + }; + + const result = getCreditFieldsRequired(paymentManagementListValue); + + expect(result).toEqual([]); + }); + + it('should return multiple credit cards if present', () => { + const paymentManagementListValue = { + paymentMethodsEnabled: [ + { paymentMethodType: 'card', paymentMethodSubtype: 'credit', cardBrand: 'visa' }, + { paymentMethodType: 'card', paymentMethodSubtype: 'credit', cardBrand: 'mastercard' }, + ], + }; + + const result = getCreditFieldsRequired(paymentManagementListValue); + + expect(result.length).toBe(2); + }); + + it('should handle undefined paymentMethodSubtype', () => { + const paymentManagementListValue = { + paymentMethodsEnabled: [ + { paymentMethodType: 'card' }, + ], + }; + + const result = getCreditFieldsRequired(paymentManagementListValue); + + expect(result).toEqual([]); + }); + }); + + describe('getPaymentMethodTypeFromListV2', () => { + it('should find matching payment method by type and subtype', () => { + const paymentsListValueV2 = { + paymentMethodsEnabled: [ + { paymentMethodType: 'card', paymentMethodSubtype: 'credit', cardBrand: 'visa' }, + { paymentMethodType: 'card', paymentMethodSubtype: 'debit', cardBrand: 'mastercard' }, + ], + }; + + const result = getPaymentMethodTypeFromListV2(paymentsListValueV2, 'card', 'credit'); + + expect(result.paymentMethodType).toBe('card'); + expect(result.paymentMethodSubtype).toBe('credit'); + }); + + it('should return default when no match found', () => { + const paymentsListValueV2 = { + paymentMethodsEnabled: [ + { paymentMethodType: 'wallet', paymentMethodSubtype: 'paypal' }, + ], + }; + + const result = getPaymentMethodTypeFromListV2(paymentsListValueV2, 'card', 'credit'); + + expect(result).toEqual(defaultPaymentMethods); + }); + + it('should return default for empty list', () => { + const paymentsListValueV2 = { + paymentMethodsEnabled: [], + }; + + const result = getPaymentMethodTypeFromListV2(paymentsListValueV2, 'card', 'credit'); + + expect(result).toEqual(defaultPaymentMethods); + }); + + it('should match exact paymentMethodSubtype', () => { + const paymentsListValueV2 = { + paymentMethodsEnabled: [ + { paymentMethodType: 'card', paymentMethodSubtype: 'credit' }, + { paymentMethodType: 'card', paymentMethodSubtype: 'debit' }, + ], + }; + + const result = getPaymentMethodTypeFromListV2(paymentsListValueV2, 'card', 'debit'); + + expect(result.paymentMethodSubtype).toBe('debit'); + }); + + it('should not match when paymentMethodType differs', () => { + const paymentsListValueV2 = { + paymentMethodsEnabled: [ + { paymentMethodType: 'card', paymentMethodSubtype: 'credit' }, + ], + }; + + const result = getPaymentMethodTypeFromListV2(paymentsListValueV2, 'wallet', 'credit'); + + expect(result).toEqual(defaultPaymentMethods); + }); + + it('should return first match when multiple matches exist', () => { + const paymentsListValueV2 = { + paymentMethodsEnabled: [ + { paymentMethodType: 'card', paymentMethodSubtype: 'credit', id: 'first' }, + { paymentMethodType: 'card', paymentMethodSubtype: 'credit', id: 'second' }, + ], + }; + + const result = getPaymentMethodTypeFromListV2(paymentsListValueV2, 'card', 'credit'); + + expect(result.id).toBe('first'); + }); + + it('should have correct default structure', () => { + const paymentsListValueV2 = { + paymentMethodsEnabled: [], + }; + + const result = getPaymentMethodTypeFromListV2(paymentsListValueV2, 'card', 'credit'); + + expect(result.paymentMethodType).toBe(''); + expect(result.paymentMethodSubtype).toBe(''); + expect(result.requiredFields).toEqual([]); + expect(result.paymentExperience).toEqual([]); + }); + }); +}); diff --git a/src/__tests__/PaypalSDKTypes.test.ts b/src/__tests__/PaypalSDKTypes.test.ts new file mode 100644 index 000000000..d0e1a21d4 --- /dev/null +++ b/src/__tests__/PaypalSDKTypes.test.ts @@ -0,0 +1,253 @@ +import { + defaultShipping, + defaultOrderDetails, + getLabel, + getShippingDetails, + paypalShippingDetails, + getOrderDetails, + shippingAddressItemToObjMapper, +} from '../Types/PaypalSDKTypes.bs.js'; + +describe('PaypalSDKTypes', () => { + describe('defaultShipping', () => { + it('should have all fields undefined', () => { + expect(defaultShipping.recipientName).toBeUndefined(); + expect(defaultShipping.line1).toBeUndefined(); + expect(defaultShipping.line2).toBeUndefined(); + expect(defaultShipping.city).toBeUndefined(); + expect(defaultShipping.countryCode).toBeUndefined(); + expect(defaultShipping.postalCode).toBeUndefined(); + expect(defaultShipping.state).toBeUndefined(); + expect(defaultShipping.phone).toBeUndefined(); + }); + }); + + describe('defaultOrderDetails', () => { + it('should have default flow of "vault"', () => { + expect(defaultOrderDetails.flow).toBe('vault'); + }); + + it('should have undefined optional fields', () => { + expect(defaultOrderDetails.billingAgreementDescription).toBeUndefined(); + expect(defaultOrderDetails.enableShippingAddress).toBeUndefined(); + expect(defaultOrderDetails.shippingAddressEditable).toBeUndefined(); + expect(defaultOrderDetails.shippingAddressOverride).toBeUndefined(); + }); + }); + + describe('getLabel', () => { + it('should return "paypal" for "Paypal"', () => { + expect(getLabel('Paypal')).toBe('paypal'); + }); + + it('should return "checkout" for "Checkout"', () => { + expect(getLabel('Checkout')).toBe('checkout'); + }); + + it('should return "buynow" for "Buynow"', () => { + expect(getLabel('Buynow')).toBe('buynow'); + }); + + it('should return "pay" for "Pay"', () => { + expect(getLabel('Pay')).toBe('pay'); + }); + + it('should return "installment" for "Installment"', () => { + expect(getLabel('Installment')).toBe('installment'); + }); + }); + + describe('getShippingDetails', () => { + it('should return undefined when any required field is missing', () => { + const shippingObj = { + recipient_name: 'John Doe', + line1: '123 Main St', + }; + const result = getShippingDetails(shippingObj); + expect(result).toBeUndefined(); + }); + + it('should return undefined for invalid input', () => { + const result = getShippingDetails('not an object'); + expect(result).toBeUndefined(); + }); + + it('should handle empty object', () => { + const result = getShippingDetails({}); + expect(result).toBeUndefined(); + }); + + it('should return undefined when fields have undefined values', () => { + const shippingObj = { + recipient_name: 'John Doe', + line1: undefined, + }; + const result = getShippingDetails(shippingObj); + expect(result).toBeUndefined(); + }); + + it('should return object when all fields are present', () => { + const shippingObj = { + recipient_name: 'John Doe', + line1: '123 Main St', + line2: 'Apt 4', + city: 'San Francisco', + country_code: 'US', + postal_code: '94105', + state: 'CA', + phone: '+14155551234', + }; + const result = getShippingDetails(shippingObj); + expect(result).toBeDefined(); + expect(result!.recipientName).toBe('John Doe'); + expect(result!.line1).toBe('123 Main St'); + expect(result!.city).toBe('San Francisco'); + expect(result!.countryCode).toBe('US'); + }); + }); + + describe('paypalShippingDetails', () => { + it('should extract shipping details from purchase unit and payer', () => { + const purchaseUnit = { + shipping: { + address: { + address_line_1: '456 Oak Ave', + address_line_2: 'Suite 100', + admin_area_2: 'Los Angeles', + country_code: 'US', + postal_code: '90001', + admin_area_1: 'CA', + }, + name: { + full_name: 'Jane Smith', + }, + }, + }; + const payerDetails = { + email: 'jane@example.com', + phone: '+12125551234', + }; + const result = paypalShippingDetails(purchaseUnit, payerDetails); + expect(result.email).toBe('jane@example.com'); + expect(result.phone).toBe('+12125551234'); + expect(result.shippingAddress.recipientName).toBe('Jane Smith'); + expect(result.shippingAddress.line1).toBe('456 Oak Ave'); + expect(result.shippingAddress.line2).toBe('Suite 100'); + expect(result.shippingAddress.city).toBe('Los Angeles'); + expect(result.shippingAddress.countryCode).toBe('US'); + expect(result.shippingAddress.postalCode).toBe('90001'); + expect(result.shippingAddress.state).toBe('CA'); + }); + + it('should handle missing optional fields', () => { + const purchaseUnit = { + shipping: { + address: {}, + name: {}, + }, + }; + const payerDetails = { + email: undefined, + phone: undefined, + }; + const result = paypalShippingDetails(purchaseUnit, payerDetails); + expect(result.email).toBe(''); + expect(result.phone).toBeUndefined(); + }); + }); + + describe('getOrderDetails', () => { + it('should extract flow from order details object', () => { + const orderDetailsObj = { flow: 'checkout' }; + const result = getOrderDetails(orderDetailsObj, 'PayPalElement'); + expect(result.flow).toBe('checkout'); + }); + + it('should return default vault flow when not specified', () => { + const orderDetailsObj = {}; + const result = getOrderDetails(orderDetailsObj, 'card'); + expect(result.flow).toBe('vault'); + }); + + it('should extract enableShippingAddress for wallet element payment type', () => { + const orderDetailsObj = { + flow: 'checkout', + enable_shipping_address: true, + }; + const result = getOrderDetails(orderDetailsObj, 'PayPalElement'); + expect(result.flow).toBe('checkout'); + expect(result.enableShippingAddress).toBe(true); + }); + + it('should return default values for non-wallet payment type', () => { + const orderDetailsObj = { + flow: 'vault', + enable_shipping_address: true, + }; + const result = getOrderDetails(orderDetailsObj, 'card'); + expect(result.flow).toBe('vault'); + expect(result.enableShippingAddress).toBeUndefined(); + expect(result.shippingAddressOverride).toBeUndefined(); + }); + + it('should handle invalid input gracefully', () => { + const result = getOrderDetails('invalid', 'card'); + expect(result.flow).toBe('vault'); + }); + + it('should return flow value from input object', () => { + const orderDetailsObj = { flow: 'custom' }; + const result = getOrderDetails(orderDetailsObj, 'card'); + expect(result.flow).toBe('custom'); + }); + }); + + describe('shippingAddressItemToObjMapper', () => { + it('should map all shipping address fields', () => { + const dict = { + recipientName: 'John Doe', + line1: '123 Main St', + line2: 'Apt 5', + city: 'Boston', + countryCode: 'US', + postalCode: '02101', + state: 'MA', + phone: '+16175551234', + }; + const result = shippingAddressItemToObjMapper(dict); + expect(result.recipientName).toBe('John Doe'); + expect(result.line1).toBe('123 Main St'); + expect(result.line2).toBe('Apt 5'); + expect(result.city).toBe('Boston'); + expect(result.countryCode).toBe('US'); + expect(result.postalCode).toBe('02101'); + expect(result.state).toBe('MA'); + expect(result.phone).toBe('+16175551234'); + }); + + it('should handle empty dict with undefined values', () => { + const result = shippingAddressItemToObjMapper({}); + expect(result.recipientName).toBeUndefined(); + expect(result.line1).toBeUndefined(); + expect(result.line2).toBeUndefined(); + expect(result.city).toBeUndefined(); + expect(result.countryCode).toBeUndefined(); + expect(result.postalCode).toBeUndefined(); + expect(result.state).toBeUndefined(); + expect(result.phone).toBeUndefined(); + }); + + it('should handle partial shipping address', () => { + const dict = { + countryCode: 'GB', + postalCode: 'SW1A 1AA', + city: 'London', + }; + const result = shippingAddressItemToObjMapper(dict); + expect(result.countryCode).toBe('GB'); + expect(result.postalCode).toBe('SW1A 1AA'); + expect(result.city).toBe('London'); + expect(result.recipientName).toBeUndefined(); + }); + }); +}); diff --git a/src/__tests__/PmAuthConnectorUtils.test.ts b/src/__tests__/PmAuthConnectorUtils.test.ts new file mode 100644 index 000000000..76e7cf2e1 --- /dev/null +++ b/src/__tests__/PmAuthConnectorUtils.test.ts @@ -0,0 +1,219 @@ +import { + pmAuthNameToTypeMapper, + pmAuthConnectorToScriptUrlMapper, + findPmAuthAllPMAuthConnectors, + getAllRequiredPmAuthConnectors, +} from '../Utilities/PmAuthConnectorUtils.bs.js'; + +describe('PmAuthConnectorUtils', () => { + describe('pmAuthNameToTypeMapper', () => { + it('should return PLAID for plaid connector name', () => { + expect(pmAuthNameToTypeMapper('plaid')).toBe('PLAID'); + }); + + it('should return NONE for unknown connector name', () => { + expect(pmAuthNameToTypeMapper('unknown')).toBe('NONE'); + }); + + it('should return NONE for empty string', () => { + expect(pmAuthNameToTypeMapper('')).toBe('NONE'); + }); + + it('should be case-sensitive (Plaid !== plaid)', () => { + expect(pmAuthNameToTypeMapper('Plaid')).toBe('NONE'); + }); + + it('should return NONE for null-like values', () => { + expect(pmAuthNameToTypeMapper('null')).toBe('NONE'); + expect(pmAuthNameToTypeMapper('undefined')).toBe('NONE'); + }); + }); + + describe('pmAuthConnectorToScriptUrlMapper', () => { + it('should return Plaid CDN URL for PLAID connector', () => { + const result = pmAuthConnectorToScriptUrlMapper('PLAID'); + expect(result).toBe('https://cdn.plaid.com/link/v2/stable/link-initialize.js'); + }); + + it('should return empty string for NONE connector', () => { + expect(pmAuthConnectorToScriptUrlMapper('NONE')).toBe(''); + }); + + it('should return empty string for unknown connector type', () => { + expect(pmAuthConnectorToScriptUrlMapper('UNKNOWN')).toBe(''); + }); + + it('should return empty string for empty string', () => { + expect(pmAuthConnectorToScriptUrlMapper('')).toBe(''); + }); + + it('should be case-sensitive (plaid !== PLAID)', () => { + expect(pmAuthConnectorToScriptUrlMapper('plaid')).toBe(''); + }); + }); + + describe('findPmAuthAllPMAuthConnectors', () => { + it('should find PM auth connectors from bank debit payment methods', () => { + const paymentMethodListValue = [ + { + payment_method: 'bank_debit', + payment_method_types: [ + { + payment_method_type: 'ach', + pm_auth_connector: 'plaid', + }, + ], + }, + ]; + + const result = findPmAuthAllPMAuthConnectors(paymentMethodListValue); + + expect(result['ach']).toBe('plaid'); + }); + + it('should return empty dict for non-bank-debit payment methods', () => { + const paymentMethodListValue = [ + { + payment_method: 'card', + payment_method_types: [], + }, + ]; + + const result = findPmAuthAllPMAuthConnectors(paymentMethodListValue); + + expect(Object.keys(result).length).toBe(0); + }); + + it('should return empty dict for empty list', () => { + const result = findPmAuthAllPMAuthConnectors([]); + + expect(Object.keys(result).length).toBe(0); + }); + + it('should skip payment method types without pm_auth_connector', () => { + const paymentMethodListValue = [ + { + payment_method: 'bank_debit', + payment_method_types: [ + { + payment_method_type: 'ach', + }, + { + payment_method_type: 'sepa', + pm_auth_connector: 'plaid', + }, + ], + }, + ]; + + const result = findPmAuthAllPMAuthConnectors(paymentMethodListValue); + + expect(Object.keys(result).length).toBe(1); + expect(result['sepa']).toBe('plaid'); + }); + + it('should handle multiple bank debit payment methods', () => { + const paymentMethodListValue = [ + { + payment_method: 'bank_debit', + payment_method_types: [ + { + payment_method_type: 'ach', + pm_auth_connector: 'plaid', + }, + ], + }, + { + payment_method: 'bank_debit', + payment_method_types: [ + { + payment_method_type: 'bacs', + pm_auth_connector: 'other', + }, + ], + }, + ]; + + const result = findPmAuthAllPMAuthConnectors(paymentMethodListValue); + + expect(Object.keys(result).length).toBe(2); + expect(result['ach']).toBe('plaid'); + expect(result['bacs']).toBe('other'); + }); + + it('should include null pm_auth_connector in dict (isSome treats null as defined)', () => { + const paymentMethodListValue = [ + { + payment_method: 'bank_debit', + payment_method_types: [ + { + payment_method_type: 'ach', + pm_auth_connector: null, + }, + ], + }, + ]; + + const result = findPmAuthAllPMAuthConnectors(paymentMethodListValue); + + expect(Object.keys(result).length).toBe(1); + expect(result['ach']).toBe(null); + }); + }); + + describe('getAllRequiredPmAuthConnectors', () => { + it('should return unique auth connectors from dict', () => { + const pmAuthConnectorsDict = { + ach: 'plaid', + sepa: 'plaid', + bacs: 'other', + }; + + const result = getAllRequiredPmAuthConnectors(pmAuthConnectorsDict); + + expect(result.length).toBe(2); + expect(result).toContain('plaid'); + expect(result).toContain('other'); + }); + + it('should return empty array for empty dict', () => { + const result = getAllRequiredPmAuthConnectors({}); + + expect(result).toEqual([]); + }); + + it('should preserve order based on dict values', () => { + const pmAuthConnectorsDict = { + first: 'connector_a', + second: 'connector_b', + third: 'connector_a', + }; + + const result = getAllRequiredPmAuthConnectors(pmAuthConnectorsDict); + + expect(result.length).toBe(2); + }); + + it('should handle dict with single entry', () => { + const pmAuthConnectorsDict = { + ach: 'plaid', + }; + + const result = getAllRequiredPmAuthConnectors(pmAuthConnectorsDict); + + expect(result).toEqual(['plaid']); + }); + + it('should handle dict with all same values', () => { + const pmAuthConnectorsDict = { + ach: 'plaid', + sepa: 'plaid', + bacs: 'plaid', + }; + + const result = getAllRequiredPmAuthConnectors(pmAuthConnectorsDict); + + expect(result).toEqual(['plaid']); + }); + }); +}); diff --git a/src/__tests__/RecoilAtomTypes.test.ts b/src/__tests__/RecoilAtomTypes.test.ts new file mode 100644 index 000000000..5afa3b0b0 --- /dev/null +++ b/src/__tests__/RecoilAtomTypes.test.ts @@ -0,0 +1,120 @@ +import { decodeRedirectionFlags, defaultPaymentToken } from '../Types/RecoilAtomTypes.bs.js'; + +describe('RecoilAtomTypes', () => { + describe('decodeRedirectionFlags', () => { + const defaultFlags = { + shouldUseTopRedirection: false, + shouldRemoveBeforeUnloadEvents: false, + }; + + it('should decode redirection flags from valid JSON object', () => { + const json = { + shouldUseTopRedirection: true, + shouldRemoveBeforeUnloadEvents: true, + }; + const result = decodeRedirectionFlags(json, defaultFlags); + expect(result.shouldUseTopRedirection).toBe(true); + expect(result.shouldRemoveBeforeUnloadEvents).toBe(true); + }); + + it('should return default values when JSON is null', () => { + const result = decodeRedirectionFlags(null, defaultFlags); + expect(result.shouldUseTopRedirection).toBe(false); + expect(result.shouldRemoveBeforeUnloadEvents).toBe(false); + }); + + it('should return default values when JSON is undefined', () => { + const result = decodeRedirectionFlags(undefined, defaultFlags); + expect(result.shouldUseTopRedirection).toBe(false); + expect(result.shouldRemoveBeforeUnloadEvents).toBe(false); + }); + + it('should use default for missing shouldUseTopRedirection field', () => { + const json = { + shouldRemoveBeforeUnloadEvents: true, + }; + const customDefault = { + shouldUseTopRedirection: true, + shouldRemoveBeforeUnloadEvents: false, + }; + const result = decodeRedirectionFlags(json, customDefault); + expect(result.shouldUseTopRedirection).toBe(true); + expect(result.shouldRemoveBeforeUnloadEvents).toBe(true); + }); + + it('should use default for missing shouldRemoveBeforeUnloadEvents field', () => { + const json = { + shouldUseTopRedirection: true, + }; + const customDefault = { + shouldUseTopRedirection: false, + shouldRemoveBeforeUnloadEvents: true, + }; + const result = decodeRedirectionFlags(json, customDefault); + expect(result.shouldUseTopRedirection).toBe(true); + expect(result.shouldRemoveBeforeUnloadEvents).toBe(true); + }); + + it('should use default values for empty object', () => { + const json = {}; + const customDefault = { + shouldUseTopRedirection: true, + shouldRemoveBeforeUnloadEvents: true, + }; + const result = decodeRedirectionFlags(json, customDefault); + expect(result.shouldUseTopRedirection).toBe(true); + expect(result.shouldRemoveBeforeUnloadEvents).toBe(true); + }); + + it('should handle false values correctly', () => { + const json = { + shouldUseTopRedirection: false, + shouldRemoveBeforeUnloadEvents: false, + }; + const customDefault = { + shouldUseTopRedirection: true, + shouldRemoveBeforeUnloadEvents: true, + }; + const result = decodeRedirectionFlags(json, customDefault); + expect(result.shouldUseTopRedirection).toBe(false); + expect(result.shouldRemoveBeforeUnloadEvents).toBe(false); + }); + + it('should handle mixed true and false values', () => { + const json = { + shouldUseTopRedirection: true, + shouldRemoveBeforeUnloadEvents: false, + }; + const result = decodeRedirectionFlags(json, defaultFlags); + expect(result.shouldUseTopRedirection).toBe(true); + expect(result.shouldRemoveBeforeUnloadEvents).toBe(false); + }); + + it('should return default when JSON is a non-object value', () => { + const result1 = decodeRedirectionFlags('string', defaultFlags); + expect(result1).toEqual(defaultFlags); + + const result2 = decodeRedirectionFlags(123, defaultFlags); + expect(result2).toEqual(defaultFlags); + + const result3 = decodeRedirectionFlags([], defaultFlags); + expect(result3).toEqual(defaultFlags); + }); + }); + + describe('defaultPaymentToken', () => { + it('should have empty paymentToken', () => { + expect(defaultPaymentToken.paymentToken).toBe(''); + }); + + it('should have empty customerId', () => { + expect(defaultPaymentToken.customerId).toBe(''); + }); + + it('should be a frozen object structure', () => { + expect(typeof defaultPaymentToken).toBe('object'); + expect(Object.keys(defaultPaymentToken)).toContain('paymentToken'); + expect(Object.keys(defaultPaymentToken)).toContain('customerId'); + }); + }); +}); diff --git a/src/__tests__/S3Utils.test.ts b/src/__tests__/S3Utils.test.ts new file mode 100644 index 000000000..a989de6f8 --- /dev/null +++ b/src/__tests__/S3Utils.test.ts @@ -0,0 +1,575 @@ +import { + decodeCountryArray, + decodeJsonTocountryStateData, + getNormalizedLocale, + fetchCountryStateFromS3, + getCountryStateData, + initializeCountryData, +} from '../Utilities/S3Utils.bs.js'; + +const mockFetchApi = jest.fn(); +const mockGetStrArray = jest.fn((obj: any, key: string) => obj?.[key] || []); +const mockGetString = jest.fn((obj: any, key: string, def: string) => obj?.[key] ?? def); +const mockGetArray = jest.fn((obj: any, key: string) => obj?.[key] || []); + +jest.mock('../Utilities/Utils.bs.js', () => ({ + getStrArray: (obj: any, key: string) => mockGetStrArray(obj, key), + getString: (obj: any, key: string, def: string) => mockGetString(obj, key, def), + getArray: (obj: any, key: string) => mockGetArray(obj, key), + getJsonFromDict: jest.fn((obj: any, key: string, def: any) => obj?.[key] ?? def), + fetchApi: (url: string, body: any, headers: any, method: string, ...rest: any[]) => mockFetchApi(url, body, headers, method, ...rest), +})); + +jest.mock('../Country.bs.js', () => ({ + country: [ + { timeZones: [], countryName: 'Default Country', isoAlpha2: 'XX' }, + ], + defaultTimeZone: { + timeZones: [], + countryName: '-', + isoAlpha2: '', + isoAlpha3: '', + }, +})); + +jest.mock('../CountryStateDataRefs.bs.js', () => ({ + countryDataRef: { contents: [] }, + stateDataRef: { contents: null }, +})); + +jest.mock('../hyper-log-catcher/HyperLogger.bs.js', () => ({ + make: jest.fn(() => ({ + setLogError: jest.fn(), + })), +})); + +const originalDateNow = Date.now; + +describe('S3Utils', () => { + beforeEach(() => { + jest.clearAllMocks(); + Date.now = () => 1234567890000; + }); + + afterEach(() => { + Date.now = originalDateNow; + }); + describe('decodeCountryArray', () => { + describe('happy path', () => { + it('should decode array of valid country objects', () => { + const input = [ + { value: 'United States', isoAlpha2: 'US', timeZones: ['America/New_York'] }, + { value: 'Canada', isoAlpha2: 'CA', timeZones: ['America/Toronto'] }, + ]; + const result = decodeCountryArray(input); + expect(result).toHaveLength(2); + expect(result[0]).toEqual({ + countryName: 'United States', + isoAlpha2: 'US', + timeZones: ['America/New_York'], + }); + expect(result[1]).toEqual({ + countryName: 'Canada', + isoAlpha2: 'CA', + timeZones: ['America/Toronto'], + }); + }); + + it('should handle country with multiple time zones', () => { + const input = [ + { value: 'Australia', isoAlpha2: 'AU', timeZones: ['Australia/Sydney', 'Australia/Perth'] }, + ]; + const result = decodeCountryArray(input); + expect(result[0].timeZones).toEqual(['Australia/Sydney', 'Australia/Perth']); + }); + + it('should handle empty time zones array', () => { + const input = [ + { value: 'Test Country', isoAlpha2: 'TC', timeZones: [] }, + ]; + const result = decodeCountryArray(input); + expect(result[0].timeZones).toEqual([]); + }); + }); + + describe('edge cases', () => { + it('should return defaultTimeZone for invalid JSON object', () => { + const input = ['not an object']; + const result = decodeCountryArray(input); + expect(result[0]).toEqual({ + timeZones: [], + countryName: '-', + isoAlpha2: '', + isoAlpha3: '', + }); + }); + + it('should handle empty array', () => { + const result = decodeCountryArray([]); + expect(result).toEqual([]); + }); + + it('should handle missing optional fields with defaults', () => { + const input = [{}]; + const result = decodeCountryArray(input); + expect(result[0]).toEqual({ + timeZones: [], + countryName: '', + isoAlpha2: '', + }); + }); + }); + + describe('error/boundary', () => { + it('should handle null values in array', () => { + const input = [null, { value: 'Valid', isoAlpha2: 'VV', timeZones: [] }]; + const result = decodeCountryArray(input); + expect(result).toHaveLength(2); + expect(result[0]).toEqual({ + timeZones: [], + countryName: '-', + isoAlpha2: '', + isoAlpha3: '', + }); + expect(result[1].countryName).toBe('Valid'); + }); + + it('should handle mixed valid and invalid items', () => { + const input = [ + { value: 'United States', isoAlpha2: 'US', timeZones: ['America/New_York'] }, + 'invalid', + { value: 'Canada', isoAlpha2: 'CA', timeZones: ['America/Toronto'] }, + ]; + const result = decodeCountryArray(input); + expect(result).toHaveLength(3); + expect(result[0].countryName).toBe('United States'); + expect(result[1].countryName).toBe('-'); + expect(result[2].countryName).toBe('Canada'); + }); + }); + }); + + describe('decodeJsonTocountryStateData', () => { + describe('happy path', () => { + it('should decode valid JSON with country and states', () => { + const input = { + country: [ + { value: 'United States', isoAlpha2: 'US', timeZones: ['America/New_York'] }, + ], + states: { US: ['California', 'New York'] }, + }; + const result = decodeJsonTocountryStateData(input); + expect(result).toBeDefined(); + expect(result!.countries).toHaveLength(1); + expect(result!.countries[0].countryName).toBe('United States'); + expect(result!.states).toEqual({ US: ['California', 'New York'] }); + }); + + it('should handle null states', () => { + const input = { + country: [ + { value: 'United States', isoAlpha2: 'US', timeZones: [] }, + ], + states: null, + }; + const result = decodeJsonTocountryStateData(input); + expect(result!.states).toBeNull(); + }); + }); + + describe('edge cases', () => { + it('should return undefined for invalid JSON', () => { + const result = decodeJsonTocountryStateData('not an object'); + expect(result).toBeUndefined(); + }); + + it('should return undefined for null input', () => { + const result = decodeJsonTocountryStateData(null); + expect(result).toBeUndefined(); + }); + + it('should handle empty country array', () => { + const input = { country: [], states: {} }; + const result = decodeJsonTocountryStateData(input); + expect(result!.countries).toEqual([]); + }); + + it('should handle missing states field', () => { + const input = { country: [] }; + const result = decodeJsonTocountryStateData(input); + expect(result!.states).toBeNull(); + }); + }); + + describe('error/boundary', () => { + it('should handle missing country field', () => { + const input = { states: { US: ['CA'] } }; + const result = decodeJsonTocountryStateData(input); + expect(result!.countries).toEqual([]); + }); + + it('should handle array input', () => { + const result = decodeJsonTocountryStateData([]); + expect(result).toBeUndefined(); + }); + + it('should handle primitive input', () => { + const result = decodeJsonTocountryStateData(42); + expect(result).toBeUndefined(); + }); + }); + }); + + describe('getNormalizedLocale', () => { + const originalNavigator = globalThis.navigator; + + beforeEach(() => { + Object.defineProperty(globalThis, 'navigator', { + value: { language: 'en-US' }, + writable: true, + configurable: true, + }); + }); + + afterEach(() => { + Object.defineProperty(globalThis, 'navigator', { + value: originalNavigator, + writable: true, + configurable: true, + }); + }); + + describe('happy path', () => { + it('returns "en" for empty string', () => { + expect(getNormalizedLocale('')).toBe('en'); + }); + + it('returns browser locale for "auto"', () => { + expect(getNormalizedLocale('auto')).toBe('en-US'); + }); + + it('returns the locale as-is for any other value', () => { + expect(getNormalizedLocale('fr')).toBe('fr'); + expect(getNormalizedLocale('de-DE')).toBe('de-DE'); + expect(getNormalizedLocale('ja-JP')).toBe('ja-JP'); + }); + }); + + describe('edge cases', () => { + it('handles navigator.language being undefined', () => { + Object.defineProperty(globalThis, 'navigator', { + value: {}, + writable: true, + configurable: true, + }); + expect(getNormalizedLocale('auto')).toBeUndefined(); + }); + + it('handles different browser locales', () => { + Object.defineProperty(globalThis, 'navigator', { + value: { language: 'fr-FR' }, + writable: true, + configurable: true, + }); + expect(getNormalizedLocale('auto')).toBe('fr-FR'); + }); + }); + + describe('error/boundary', () => { + it('handles navigator being undefined', () => { + Object.defineProperty(globalThis, 'navigator', { + value: undefined, + writable: true, + configurable: true, + }); + expect(() => getNormalizedLocale('auto')).toThrow(); + }); + }); + + describe('fetchCountryStateFromS3', () => { + describe('happy path', () => { + it('should fetch and return country state data', async () => { + const mockResponse = { + country: [ + { value: 'United States', isoAlpha2: 'US', timeZones: ['America/New_York'] }, + ], + states: { US: ['California', 'New York'] }, + }; + mockFetchApi.mockResolvedValue({ + json: () => Promise.resolve(mockResponse), + }); + mockGetStrArray.mockImplementation((obj: any, key: string) => obj?.[key] || []); + mockGetString.mockImplementation((obj: any, key: string, def: string) => obj?.[key] ?? def); + mockGetArray.mockImplementation((obj: any, key: string) => obj?.[key] || []); + + const result = await fetchCountryStateFromS3('https://test.com/data.json'); + + expect(mockFetchApi).toHaveBeenCalledWith( + 'https://test.com/data.json', + undefined, + { 'Accept-Encoding': 'br, gzip' }, + 'GET', + undefined, + undefined, + undefined + ); + expect(result).toBeDefined(); + expect(result!.countries).toHaveLength(1); + }); + + it('should include Accept-Encoding header', async () => { + mockFetchApi.mockResolvedValue({ + json: () => Promise.resolve({ country: [], states: {} }), + }); + mockGetArray.mockReturnValue([]); + + await fetchCountryStateFromS3('https://test.com/data.json'); + + expect(mockFetchApi).toHaveBeenCalledWith( + expect.any(String), + undefined, + { 'Accept-Encoding': 'br, gzip' }, + 'GET', + undefined, + undefined, + undefined + ); + }); + }); + + describe('edge cases', () => { + it('should reject for invalid JSON response', async () => { + mockFetchApi.mockResolvedValue({ + json: () => Promise.resolve('not an object'), + }); + + await expect(fetchCountryStateFromS3('https://test.com/invalid.json')).rejects.toBeDefined(); + }); + + it('should reject for null response', async () => { + mockFetchApi.mockResolvedValue({ + json: () => Promise.resolve(null), + }); + + await expect(fetchCountryStateFromS3('https://test.com/null.json')).rejects.toBeDefined(); + }); + }); + + describe('error/boundary', () => { + it('should reject on fetch error', async () => { + mockFetchApi.mockRejectedValue(new Error('Network error')); + + await expect(fetchCountryStateFromS3('https://test.com/error.json')).rejects.toBeDefined(); + }); + + it('should reject on JSON parse error', async () => { + mockFetchApi.mockResolvedValue({ + json: () => Promise.reject(new Error('JSON parse error')), + }); + + await expect(fetchCountryStateFromS3('https://test.com/invalid.json')).rejects.toBeDefined(); + }); + }); + }); + + describe('getCountryStateData', () => { + const originalWindow = globalThis.window; + + beforeEach(() => { + jest.clearAllMocks(); + mockFetchApi.mockReset(); + }); + + afterEach(() => { + if (originalWindow) { + Object.defineProperty(globalThis, 'window', { + value: originalWindow, + writable: true, + configurable: true, + }); + } + }); + + describe('happy path', () => { + it('should fetch country state data for given locale', async () => { + const mockResponse = { + country: [ + { value: 'France', isoAlpha2: 'FR', timeZones: ['Europe/Paris'] }, + ], + states: { FR: ['Paris', 'Lyon'] }, + }; + mockFetchApi.mockResolvedValue({ + json: () => Promise.resolve(mockResponse), + }); + mockGetStrArray.mockImplementation((obj: any, key: string) => obj?.[key] || []); + mockGetString.mockImplementation((obj: any, key: string, def: string) => obj?.[key] ?? def); + mockGetArray.mockImplementation((obj: any, key: string) => obj?.[key] || []); + + const result = await getCountryStateData('fr'); + + expect(result).toBeDefined(); + expect(result!.countries).toHaveLength(1); + }); + + it('should use default locale when not provided', async () => { + const mockResponse = { country: [], states: {} }; + mockFetchApi.mockResolvedValue({ + json: () => Promise.resolve(mockResponse), + }); + mockGetArray.mockReturnValue([]); + + await getCountryStateData(); + + expect(mockFetchApi).toHaveBeenCalled(); + }); + }); + + describe('edge cases', () => { + it('should handle "auto" locale by using navigator language', async () => { + Object.defineProperty(globalThis, 'navigator', { + value: { language: 'en-GB' }, + writable: true, + configurable: true, + }); + + const mockResponse = { country: [], states: {} }; + mockFetchApi.mockResolvedValue({ + json: () => Promise.resolve(mockResponse), + }); + mockGetArray.mockReturnValue([]); + + await getCountryStateData('auto'); + + expect(mockFetchApi).toHaveBeenCalledWith( + expect.stringContaining('en-GB'), + undefined, + expect.any(Object), + 'GET', + undefined, + undefined, + undefined + ); + }); + + it('should fallback to "en" locale on first fetch failure', async () => { + mockFetchApi + .mockRejectedValueOnce(new Error('First fetch failed')) + .mockResolvedValueOnce({ + json: () => Promise.resolve({ + country: [{ value: 'United States', isoAlpha2: 'US', timeZones: [] }], + states: {}, + }), + }); + mockGetStrArray.mockReturnValue([]); + mockGetString.mockImplementation((obj: any, key: string, def: string) => obj?.[key] ?? def); + mockGetArray.mockImplementation((obj: any, key: string) => obj?.[key] || []); + + const result = await getCountryStateData('fr'); + + expect(mockFetchApi).toHaveBeenCalledTimes(2); + expect(result).toBeDefined(); + }); + }); + + describe('error/boundary', () => { + it('should return fallback data when all fetches fail', async () => { + mockFetchApi.mockRejectedValue(new Error('All fetches failed')); + + const result = await getCountryStateData('en'); + + expect(result).toBeDefined(); + expect(result!.countries).toBeDefined(); + expect(result!.states).toBeDefined(); + }); + + it('should return fallback data without states when import fails', async () => { + mockFetchApi.mockRejectedValue(new Error('Fetch failed')); + + const result = await getCountryStateData('en'); + + expect(result).toBeDefined(); + expect(result!.countries).toBeDefined(); + }); + }); + }); + + describe('initializeCountryData', () => { + describe('happy path', () => { + it('should initialize country and state data refs', async () => { + const mockResponse = { + country: [ + { value: 'Germany', isoAlpha2: 'DE', timeZones: ['Europe/Berlin'] }, + ], + states: { DE: ['Bavaria', 'Berlin'] }, + }; + mockFetchApi.mockResolvedValue({ + json: () => Promise.resolve(mockResponse), + }); + mockGetStrArray.mockImplementation((obj: any, key: string) => obj?.[key] || []); + mockGetString.mockImplementation((obj: any, key: string, def: string) => obj?.[key] ?? def); + mockGetArray.mockImplementation((obj: any, key: string) => obj?.[key] || []); + + const result = await initializeCountryData('de'); + + expect(result).toBeDefined(); + expect(result!.countries).toHaveLength(1); + expect(result!.states).toEqual({ DE: ['Bavaria', 'Berlin'] }); + }); + + it('should use default locale when not provided', async () => { + const mockResponse = { country: [], states: {} }; + mockFetchApi.mockResolvedValue({ + json: () => Promise.resolve(mockResponse), + }); + mockGetArray.mockReturnValue([]); + + await initializeCountryData(); + + expect(mockFetchApi).toHaveBeenCalled(); + }); + }); + + describe('edge cases', () => { + it('should handle locale with country code', async () => { + const mockResponse = { country: [], states: {} }; + mockFetchApi.mockResolvedValue({ + json: () => Promise.resolve(mockResponse), + }); + mockGetArray.mockReturnValue([]); + + await initializeCountryData('en-US'); + + expect(mockFetchApi).toHaveBeenCalledWith( + expect.stringContaining('en-US'), + undefined, + expect.any(Object), + 'GET', + undefined, + undefined, + undefined + ); + }); + }); + + describe('error/boundary', () => { + it('should return fallback data on fetch failure', async () => { + mockFetchApi.mockRejectedValue(new Error('Fetch failed')); + + const result = await initializeCountryData('en'); + + expect(result).toBeDefined(); + expect(result!.countries).toBeDefined(); + }); + + it('should return fallback data with null states on complete failure', async () => { + mockFetchApi.mockRejectedValue(new Error('Fetch failed')); + + const result = await initializeCountryData('en'); + + expect(result).toBeDefined(); + expect(result!.states).toBeDefined(); + }); + }); + }); +}); +}); diff --git a/src/__tests__/SamsungPayHelpers.test.ts b/src/__tests__/SamsungPayHelpers.test.ts new file mode 100644 index 000000000..57a88a62c --- /dev/null +++ b/src/__tests__/SamsungPayHelpers.test.ts @@ -0,0 +1,608 @@ +import { + getTransactionDetail, + getPaymentMethodData, + itemToObjMapper, + getSamsungPayBodyFromResponse, + handleSamsungPayClicked, + useHandleSamsungPayResponse, +} from '../Utilities/SamsungPayHelpers.bs.js'; +import * as Utils from '../Utilities/Utils.bs.js'; +import { renderHook, act } from '@testing-library/react'; +import { RecoilRoot } from 'recoil'; +import * as React from 'react'; +import * as RecoilAtoms from '../Utilities/RecoilAtoms.bs.js'; + +jest.mock('../Utilities/Utils.bs.js', () => ({ + messageParentWindow: jest.fn(), + getDictFromDict: jest.fn((dict, key) => dict[key]), + getString: jest.fn((dict, key, def) => (dict && dict[key] !== undefined ? dict[key] : def)), + getBool: jest.fn((dict, key, def) => (dict && dict[key] !== undefined ? dict[key] : def)), + getJsonFromArrayOfJson: jest.fn((arr) => arr), + getDictFromJson: jest.fn((json) => json), +})); + +describe('SamsungPayHelpers', () => { + describe('getTransactionDetail', () => { + it('should extract transaction details from complete dict', () => { + const dict = { + order_number: 'ORDER-123', + merchant: { + name: 'Test Merchant', + url: 'https://example.com', + country_code: 'US', + }, + amount: { + option: 'formattedTotal', + currency_code: 'USD', + total: '100.00', + }, + }; + + const result = getTransactionDetail(dict); + + expect(result.orderNumber).toBe('ORDER-123'); + expect(result.merchant.name).toBe('Test Merchant'); + expect(result.merchant.url).toBe('https://example.com'); + expect(result.merchant.countryCode).toBe('US'); + expect(result.amount.option).toBe('formattedTotal'); + expect(result.amount.currency).toBe('USD'); + expect(result.amount.total).toBe('100.00'); + }); + + it('should return empty strings for missing fields', () => { + const dict = {}; + + const result = getTransactionDetail(dict); + + expect(result.orderNumber).toBe(''); + expect(result.merchant.name).toBe(''); + expect(result.merchant.url).toBe(''); + expect(result.merchant.countryCode).toBe(''); + expect(result.amount.option).toBe(''); + expect(result.amount.currency).toBe(''); + expect(result.amount.total).toBe(''); + }); + + it('should handle partial merchant data', () => { + const dict = { + order_number: 'ORDER-456', + merchant: { + name: 'Partial Merchant', + }, + amount: {}, + }; + + const result = getTransactionDetail(dict); + + expect(result.orderNumber).toBe('ORDER-456'); + expect(result.merchant.name).toBe('Partial Merchant'); + expect(result.merchant.url).toBe(''); + expect(result.merchant.countryCode).toBe(''); + }); + + it('should handle partial amount data', () => { + const dict = { + amount: { + currency_code: 'EUR', + }, + }; + + const result = getTransactionDetail(dict); + + expect(result.amount.currency).toBe('EUR'); + expect(result.amount.total).toBe(''); + expect(result.amount.option).toBe(''); + }); + + it('should handle different currency codes', () => { + const dict = { + amount: { + currency_code: 'JPY', + total: '1000', + }, + }; + + const result = getTransactionDetail(dict); + expect(result.amount.currency).toBe('JPY'); + expect(result.amount.total).toBe('1000'); + }); + + it('should handle long order numbers', () => { + const dict = { + order_number: 'ORDER-12345678901234567890', + }; + + const result = getTransactionDetail(dict); + expect(result.orderNumber).toBe('ORDER-12345678901234567890'); + }); + + it('should handle merchant with long URL', () => { + const dict = { + merchant: { + url: 'https://very-long-subdomain.example.com/path/to/resource?param=value', + }, + }; + + const result = getTransactionDetail(dict); + expect(result.merchant.url).toBe('https://very-long-subdomain.example.com/path/to/resource?param=value'); + }); + }); + + describe('getPaymentMethodData', () => { + it('should extract payment method data from complete dict', () => { + const dict = { + method: 'samsung_pay', + recurring_payment: true, + card_brand: 'VISA', + card_last4digits: '4242', + '3DS': { + type: '01', + version: '2.0', + data: 'encrypted_data_string', + }, + }; + + const result = getPaymentMethodData(dict); + + expect(result.method).toBe('samsung_pay'); + expect(result.recurring_payment).toBe(true); + expect(result.card_brand).toBe('VISA'); + expect(result.card_last4digits).toBe('4242'); + expect(result['3_d_s'].type).toBe('01'); + expect(result['3_d_s'].version).toBe('2.0'); + expect(result['3_d_s'].data).toBe('encrypted_data_string'); + }); + + it('should return default values for missing fields', () => { + const dict = {}; + + const result = getPaymentMethodData(dict); + + expect(result.method).toBe(''); + expect(result.recurring_payment).toBe(false); + expect(result.card_brand).toBe(''); + expect(result.card_last4digits).toBe(''); + expect(result['3_d_s'].type).toBe(''); + expect(result['3_d_s'].version).toBe(''); + expect(result['3_d_s'].data).toBe(''); + }); + + it('should handle missing 3DS object', () => { + const dict = { + method: 'samsung_pay', + card_brand: 'MASTERCARD', + }; + + const result = getPaymentMethodData(dict); + + expect(result.method).toBe('samsung_pay'); + expect(result.card_brand).toBe('MASTERCARD'); + expect(result['3_d_s'].type).toBe(''); + }); + + it('should handle recurring_payment as false', () => { + const dict = { + method: 'samsung_pay', + recurring_payment: false, + }; + + const result = getPaymentMethodData(dict); + + expect(result.recurring_payment).toBe(false); + }); + + it('should handle partial 3DS data', () => { + const dict = { + '3DS': { + type: '01', + }, + }; + + const result = getPaymentMethodData(dict); + + expect(result['3_d_s'].type).toBe('01'); + expect(result['3_d_s'].version).toBe(''); + expect(result['3_d_s'].data).toBe(''); + }); + + it('should handle different card brands', () => { + const dict = { + card_brand: 'AMEX', + card_last4digits: '1001', + }; + + const result = getPaymentMethodData(dict); + expect(result.card_brand).toBe('AMEX'); + expect(result.card_last4digits).toBe('1001'); + }); + + it('should handle empty card_last4digits', () => { + const dict = { + card_brand: 'VISA', + card_last4digits: '', + }; + + const result = getPaymentMethodData(dict); + expect(result.card_last4digits).toBe(''); + }); + + it('should handle complex 3DS data', () => { + const dict = { + '3DS': { + type: '02', + version: '2.1.0', + data: 'very-long-encrypted-string-with-special-chars-!@#$%', + }, + }; + + const result = getPaymentMethodData(dict); + expect(result['3_d_s'].type).toBe('02'); + expect(result['3_d_s'].version).toBe('2.1.0'); + expect(result['3_d_s'].data).toBe('very-long-encrypted-string-with-special-chars-!@#$%'); + }); + }); + + describe('itemToObjMapper', () => { + it('should map dict to Samsung Pay response object', () => { + const dict = { + method: 'samsung_pay', + card_brand: 'VISA', + card_last4digits: '1234', + '3DS': { + type: '01', + version: '2.0', + data: 'test_data', + }, + }; + + const result = itemToObjMapper(dict); + + expect(result.paymentMethodData).toBeDefined(); + expect(result.paymentMethodData.method).toBe('samsung_pay'); + expect(result.paymentMethodData.card_brand).toBe('VISA'); + }); + + it('should handle empty dict', () => { + const result = itemToObjMapper({}); + + expect(result.paymentMethodData).toBeDefined(); + expect(result.paymentMethodData.method).toBe(''); + }); + + it('should map complete payment data', () => { + const dict = { + method: 'samsung_pay', + recurring_payment: true, + card_brand: 'MASTERCARD', + card_last4digits: '9999', + '3DS': { + type: '02', + version: '2.1', + data: 'encrypted', + }, + }; + + const result = itemToObjMapper(dict); + expect(result.paymentMethodData.method).toBe('samsung_pay'); + expect(result.paymentMethodData.recurring_payment).toBe(true); + expect(result.paymentMethodData.card_brand).toBe('MASTERCARD'); + expect(result.paymentMethodData.card_last4digits).toBe('9999'); + expect(result.paymentMethodData['3_d_s'].type).toBe('02'); + }); + }); + + describe('getSamsungPayBodyFromResponse', () => { + it('should parse JSON object and return Samsung Pay body', () => { + const sPayResponse = { + method: 'samsung_pay', + card_brand: 'VISA', + card_last4digits: '4242', + '3DS': { + type: '01', + version: '2.0', + data: 'encrypted_data', + }, + }; + + const result = getSamsungPayBodyFromResponse(sPayResponse); + + expect(result.paymentMethodData).toBeDefined(); + expect(result.paymentMethodData.method).toBe('samsung_pay'); + expect(result.paymentMethodData.card_brand).toBe('VISA'); + expect(result.paymentMethodData.card_last4digits).toBe('4242'); + }); + + it('should handle JSON object with missing fields', () => { + const sPayResponse = { + method: 'samsung_pay', + }; + + const result = getSamsungPayBodyFromResponse(sPayResponse); + + expect(result.paymentMethodData.method).toBe('samsung_pay'); + expect(result.paymentMethodData.card_brand).toBe(''); + }); + + it('should handle empty JSON object', () => { + const sPayResponse = {}; + + const result = getSamsungPayBodyFromResponse(sPayResponse); + + expect(result.paymentMethodData).toBeDefined(); + expect(result.paymentMethodData.method).toBe(''); + }); + + it('should handle JSON object with recurring_payment field', () => { + const sPayResponse = { + method: 'samsung_pay', + recurring_payment: true, + }; + + const result = getSamsungPayBodyFromResponse(sPayResponse); + + expect(result.paymentMethodData.recurring_payment).toBe(true); + }); + + it('should handle response with only 3DS data', () => { + const sPayResponse = { + '3DS': { + type: '02', + version: '2.2', + data: '3ds-data', + }, + }; + + const result = getSamsungPayBodyFromResponse(sPayResponse); + expect(result.paymentMethodData['3_d_s'].type).toBe('02'); + expect(result.paymentMethodData['3_d_s'].version).toBe('2.2'); + }); + }); + + describe('handleSamsungPayClicked', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should call messageParentWindow with fullscreen and param when readOnly is false', () => { + const sessionObj = { + order_number: 'ORDER-123', + merchant: { + name: 'Test Merchant', + url: 'https://example.com', + country_code: 'US', + }, + amount: { + option: 'formattedTotal', + currency_code: 'USD', + total: '100.00', + }, + }; + + handleSamsungPayClicked(sessionObj, 'SamsungPay', 'iframe-123', false); + + expect(Utils.messageParentWindow).toHaveBeenCalled(); + }); + + it('should call messageParentWindow when readOnly is true', () => { + const sessionObj = { + order_number: 'ORDER-456', + merchant: { + name: 'Test Merchant', + url: 'https://example.com', + country_code: 'US', + }, + amount: { + option: 'formattedTotal', + currency_code: 'USD', + total: '50.00', + }, + }; + + handleSamsungPayClicked(sessionObj, 'SamsungPay', 'iframe-456', true); + + expect(Utils.messageParentWindow).toHaveBeenCalledTimes(1); + }); + + it('should send SamsungPayClicked message when readOnly is false', () => { + const sessionObj = { + order_number: 'ORDER-789', + merchant: { + name: 'Merchant', + url: 'https://test.com', + country_code: 'GB', + }, + amount: { + option: 'formattedTotal', + currency_code: 'GBP', + total: '75.00', + }, + }; + + handleSamsungPayClicked(sessionObj, 'SamsungPayComponent', 'test-iframe', false); + + expect(Utils.messageParentWindow).toHaveBeenCalledTimes(2); + }); + + it('should handle empty session object', () => { + handleSamsungPayClicked({}, 'SamsungPay', 'iframe-empty', false); + + expect(Utils.messageParentWindow).toHaveBeenCalled(); + }); + + it('should handle missing merchant and amount data', () => { + const sessionObj = { + order_number: 'ORDER-999', + }; + + handleSamsungPayClicked(sessionObj, 'SamsungPay', 'iframe-test', false); + + expect(Utils.messageParentWindow).toHaveBeenCalled(); + }); + + it('should handle different component names', () => { + const sessionObj = { order_number: 'ORDER-001' }; + + handleSamsungPayClicked(sessionObj, 'SamsungPayWidget', 'iframe-1', false); + expect(Utils.messageParentWindow).toHaveBeenCalled(); + + jest.clearAllMocks(); + + handleSamsungPayClicked(sessionObj, 'CustomSamsungPay', 'iframe-2', false); + expect(Utils.messageParentWindow).toHaveBeenCalled(); + }); + + it('should handle different iframe IDs', () => { + const sessionObj = { order_number: 'ORDER-002' }; + + handleSamsungPayClicked(sessionObj, 'SamsungPay', 'iframe-A', false); + expect(Utils.messageParentWindow).toHaveBeenCalled(); + + jest.clearAllMocks(); + + handleSamsungPayClicked(sessionObj, 'SamsungPay', 'iframe-B', false); + expect(Utils.messageParentWindow).toHaveBeenCalled(); + }); + + it('should handle complete transaction details', () => { + const sessionObj = { + order_number: 'ORDER-123', + merchant: { + name: 'Test Merchant', + url: 'https://example.com', + country_code: 'US', + }, + amount: { + option: 'formattedTotal', + currency_code: 'USD', + total: '100.00', + }, + }; + + handleSamsungPayClicked(sessionObj, 'SamsungPay', 'iframe-123', false); + + expect(Utils.messageParentWindow).toHaveBeenCalledTimes(2); + const calls = (Utils.messageParentWindow as jest.Mock).mock.calls; + expect(calls.length).toBeGreaterThan(0); + }); + }); + + describe('useHandleSamsungPayResponse', () => { + const mockIntent = jest.fn(); + + const createWrapper = (initialState: any = {}) => { + return function Wrapper({ children }: { children: React.ReactNode }) { + return React.createElement( + RecoilRoot, + { + initializeState: ({ set }: any) => { + if (initialState.optionAtom) { + set(RecoilAtoms.optionAtom, initialState.optionAtom); + } + if (initialState.keys) { + set(RecoilAtoms.keys, initialState.keys); + } + if (initialState.isManualRetryEnabled !== undefined) { + set(RecoilAtoms.isManualRetryEnabled, initialState.isManualRetryEnabled); + } + }, + }, + children + ); + }; + }; + + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should add message event listener on mount', () => { + const addEventListenerSpy = jest.spyOn(window, 'addEventListener'); + + const wrapper = createWrapper({ + optionAtom: { wallets: { walletReturnUrl: 'https://return.url' } }, + keys: { publishableKey: 'pk_test_123' }, + isManualRetryEnabled: false, + }); + + const { unmount } = renderHook( + () => useHandleSamsungPayResponse(mockIntent, false, true), + { wrapper } + ); + + expect(addEventListenerSpy).toHaveBeenCalledWith('message', expect.any(Function)); + + addEventListenerSpy.mockRestore(); + unmount(); + }); + + it('should remove message event listener on unmount', () => { + const removeEventListenerSpy = jest.spyOn(window, 'removeEventListener'); + + const wrapper = createWrapper({ + optionAtom: { wallets: { walletReturnUrl: 'https://return.url' } }, + keys: { publishableKey: 'pk_test_123' }, + isManualRetryEnabled: false, + }); + + const { unmount } = renderHook( + () => useHandleSamsungPayResponse(mockIntent, false, true), + { wrapper } + ); + + unmount(); + + expect(removeEventListenerSpy).toHaveBeenCalledWith('message', expect.any(Function)); + + removeEventListenerSpy.mockRestore(); + }); + + it('should use default values for optional parameters', () => { + const wrapper = createWrapper({ + optionAtom: { wallets: { walletReturnUrl: 'https://return.url' } }, + keys: { publishableKey: 'pk_test_123' }, + isManualRetryEnabled: false, + }); + + const { result } = renderHook( + () => useHandleSamsungPayResponse(mockIntent), + { wrapper } + ); + + expect(result.current).toBeUndefined(); + }); + + it('should handle isSavedMethodsFlow parameter as true', () => { + const wrapper = createWrapper({ + optionAtom: { wallets: { walletReturnUrl: 'https://return.url' } }, + keys: { publishableKey: 'pk_test_123' }, + isManualRetryEnabled: false, + }); + + const { result } = renderHook( + () => useHandleSamsungPayResponse(mockIntent, true, true), + { wrapper } + ); + + expect(result.current).toBeUndefined(); + }); + + it('should handle isWallet parameter as false', () => { + const wrapper = createWrapper({ + optionAtom: { wallets: { walletReturnUrl: 'https://return.url' } }, + keys: { publishableKey: 'pk_test_123' }, + isManualRetryEnabled: false, + }); + + const { result } = renderHook( + () => useHandleSamsungPayResponse(mockIntent, false, false), + { wrapper } + ); + + expect(result.current).toBeUndefined(); + }); + }); +}); diff --git a/src/__tests__/TaxCalculation.test.ts b/src/__tests__/TaxCalculation.test.ts new file mode 100644 index 000000000..c7c574445 --- /dev/null +++ b/src/__tests__/TaxCalculation.test.ts @@ -0,0 +1,244 @@ +import * as TaxCalculation from '../Utilities/TaxCalculation.bs.js'; + +const mockFetchApiWithLogging = jest.fn(); +const mockGetNonEmptyOption = jest.fn((val: any) => (val ? val : undefined)); +const mockGetDictFromJson = jest.fn((obj: any) => (typeof obj === 'object' && obj !== null ? obj : {})); + +jest.mock('../Utilities/Utils.bs.js', () => ({ + getDictFromJson: (obj: any) => mockGetDictFromJson(obj), + getString: (obj: any, key: string, def: string) => obj?.[key] ?? def, + getInt: (obj: any, key: string, def: number) => obj?.[key] ?? def, + getNonEmptyOption: (val: any) => mockGetNonEmptyOption(val), + fetchApiWithLogging: (...args: any[]) => mockFetchApiWithLogging(...args), + getJsonFromArrayOfJson: (arr: any) => Object.fromEntries(arr), +})); + +jest.mock('../Utilities/APIHelpers/APIUtils.bs.js', () => ({ + generateApiUrlV1: jest.fn((params: any, endpoint: string) => `https://api.test.com/${endpoint}`), +})); + +jest.mock('../Utilities/PaymentHelpers.bs.js', () => ({ + calculateTax: jest.fn((...args: any[]) => mockFetchApiWithLogging(...args)), +})); + +describe('TaxCalculation', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('taxResponseToObjMapper', () => { + it('should map valid tax response to object', () => { + const response = { + payment_id: 'pay_123', + net_amount: 1000, + order_tax_amount: 100, + shipping_cost: 50, + }; + const result = TaxCalculation.taxResponseToObjMapper(response); + expect(result).toBeDefined(); + expect(result?.payment_id).toBe('pay_123'); + expect(result?.net_amount).toBe(1000); + expect(result?.order_tax_amount).toBe(100); + expect(result?.shipping_cost).toBe(50); + }); + + it('should handle missing fields with defaults', () => { + const response = {}; + const result = TaxCalculation.taxResponseToObjMapper(response); + expect(result).toBeDefined(); + expect(result?.payment_id).toBe(''); + expect(result?.net_amount).toBe(0); + expect(result?.order_tax_amount).toBe(0); + expect(result?.shipping_cost).toBe(0); + }); + + it('should handle null response', () => { + const result = TaxCalculation.taxResponseToObjMapper(null); + expect(result).toBeUndefined(); + }); + + it('should handle partial response', () => { + const response = { + payment_id: 'pay_456', + net_amount: 2000, + }; + const result = TaxCalculation.taxResponseToObjMapper(response); + expect(result).toBeDefined(); + expect(result?.payment_id).toBe('pay_456'); + expect(result?.net_amount).toBe(2000); + expect(result?.order_tax_amount).toBe(0); + }); + + it('should handle undefined response', () => { + const result = TaxCalculation.taxResponseToObjMapper(undefined); + expect(result).toBeUndefined(); + }); + + it('should handle string response (invalid JSON)', () => { + const result = TaxCalculation.taxResponseToObjMapper('not an object'); + expect(result).toBeUndefined(); + }); + + it('should handle array response (invalid)', () => { + const result = TaxCalculation.taxResponseToObjMapper([1, 2, 3]); + expect(result).toBeUndefined(); + }); + + it('should handle response with zero values', () => { + const response = { + payment_id: '', + net_amount: 0, + order_tax_amount: 0, + shipping_cost: 0, + }; + const result = TaxCalculation.taxResponseToObjMapper(response); + expect(result).toBeDefined(); + expect(result?.payment_id).toBe(''); + expect(result?.net_amount).toBe(0); + }); + + it('should handle response with extra fields', () => { + const response = { + payment_id: 'pay_789', + net_amount: 5000, + order_tax_amount: 500, + shipping_cost: 100, + extra_field: 'should be ignored', + another_field: 123, + }; + const result = TaxCalculation.taxResponseToObjMapper(response); + expect(result).toBeDefined(); + expect(result?.payment_id).toBe('pay_789'); + expect(result?.net_amount).toBe(5000); + }); + }); + + describe('calculateTax', () => { + it('should call calculateTax with correct parameters', async () => { + mockFetchApiWithLogging.mockResolvedValue({ + payment_id: 'pay_123', + net_amount: 1000, + order_tax_amount: 100, + shipping_cost: 50, + }); + mockGetNonEmptyOption.mockReturnValue(undefined); + + await TaxCalculation.calculateTax( + { country: 'US', postal_code: '12345' }, + undefined, + 'secret_test', + 'pk_test', + 'card', + undefined, + undefined + ); + + expect(mockFetchApiWithLogging).toHaveBeenCalled(); + }); + + it('should handle calculateTax with sessionId', async () => { + mockFetchApiWithLogging.mockResolvedValue({ + payment_id: 'pay_123', + net_amount: 1000, + order_tax_amount: 100, + shipping_cost: 50, + }); + mockGetNonEmptyOption.mockReturnValue(undefined); + + await TaxCalculation.calculateTax( + { country: 'US', postal_code: '12345' }, + undefined, + 'secret_test', + 'pk_test', + 'card', + 'session_123', + undefined + ); + + expect(mockFetchApiWithLogging).toHaveBeenCalled(); + }); + + it('should handle calculateTax with sdkAuthorization', async () => { + mockFetchApiWithLogging.mockResolvedValue({ + payment_id: 'pay_123', + net_amount: 1000, + order_tax_amount: 100, + shipping_cost: 50, + }); + mockGetNonEmptyOption.mockReturnValue('auth_token'); + + await TaxCalculation.calculateTax( + { country: 'US', postal_code: '12345' }, + undefined, + 'secret_test', + 'pk_test', + 'card', + undefined, + 'auth_token' + ); + + expect(mockFetchApiWithLogging).toHaveBeenCalled(); + }); + + it('should handle null response from API', async () => { + mockFetchApiWithLogging.mockResolvedValue(null); + mockGetNonEmptyOption.mockReturnValue(undefined); + + const result = await TaxCalculation.calculateTax( + { country: 'US', postal_code: '12345' }, + undefined, + 'secret_test', + 'pk_test', + 'card', + undefined, + undefined + ); + + expect(result).toBeNull(); + }); + + it('should handle empty shipping address', async () => { + mockFetchApiWithLogging.mockResolvedValue({ + payment_id: 'pay_123', + net_amount: 1000, + order_tax_amount: 0, + shipping_cost: 0, + }); + mockGetNonEmptyOption.mockReturnValue(undefined); + + await TaxCalculation.calculateTax( + {}, + undefined, + 'secret_test', + 'pk_test', + 'card', + undefined, + undefined + ); + + expect(mockFetchApiWithLogging).toHaveBeenCalled(); + }); + + it('should handle different payment method types', async () => { + mockFetchApiWithLogging.mockResolvedValue({ + payment_id: 'pay_123', + net_amount: 1000, + order_tax_amount: 100, + shipping_cost: 50, + }); + mockGetNonEmptyOption.mockReturnValue(undefined); + + await TaxCalculation.calculateTax( + { country: 'DE', postal_code: '10115' }, + undefined, + 'secret_test', + 'pk_test', + 'klarna', + undefined, + undefined + ); + + expect(mockFetchApiWithLogging).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/__tests__/ThirdPartyFlowHelpers.test.ts b/src/__tests__/ThirdPartyFlowHelpers.test.ts new file mode 100644 index 000000000..66f2f705b --- /dev/null +++ b/src/__tests__/ThirdPartyFlowHelpers.test.ts @@ -0,0 +1,244 @@ +import { renderHook } from '@testing-library/react'; +import { RecoilRoot } from 'recoil'; +import * as React from 'react'; +import * as ThirdPartyFlowHelpers from '../Hooks/ThirdPartyFlowHelpers.bs.js'; +import * as RecoilAtoms from '../Utilities/RecoilAtoms.bs.js'; + +jest.mock('../Utilities/Utils.bs.js', () => ({ + getDictFromJson: jest.fn((obj) => (typeof obj === 'object' && obj !== null ? obj : {})), + getDecodedBoolFromJson: jest.fn((json, callback, defaultValue) => { + if (typeof json === 'object' && json !== null) { + const result = callback(json); + return result !== undefined ? result : defaultValue; + } + return defaultValue; + }), +})); + +jest.mock('../Types/SessionsType.bs.js', () => ({ + itemToObjMapper: jest.fn((dict, returnType) => { + const token = dict.session_token || []; + return { + paymentId: dict.payment_id || '', + clientSecret: dict.client_secret || '', + sessionsToken: { + TAG: returnType === 'ApplePayObject' ? 'ApplePayToken' : 'GooglePayThirdPartyToken', + _0: token, + }, + }; + }), + getPaymentSessionObj: jest.fn((sessionsToken, walletType) => { + const tokens = sessionsToken._0 || []; + const token = tokens.find((t: any) => t && t.wallet_name === walletType.toLowerCase().replace('applepay', 'apple_pay').replace('gpay', 'google_pay')); + if (token) { + return { + TAG: sessionsToken.TAG === 'ApplePayToken' ? 'ApplePayTokenOptional' : 'GooglePayThirdPartyTokenOptional', + _0: token, + }; + } + return { + TAG: sessionsToken.TAG === 'ApplePayToken' ? 'ApplePayTokenOptional' : 'GooglePayThirdPartyTokenOptional', + _0: undefined, + }; + }), +})); + +const createWrapper = (sessionsValue: any) => { + return function Wrapper({ children }: { children: React.ReactNode }) { + return React.createElement( + RecoilRoot, + { + initializeState: ({ set }: any) => { + set(RecoilAtoms.sessions, sessionsValue); + }, + }, + children + ); + }; +}; + +describe('useIsApplePayDelayedSessionFlow', () => { + it('returns false when sessions is in Loading state', () => { + const Wrapper = createWrapper('Loading'); + const { result } = renderHook(() => ThirdPartyFlowHelpers.useIsApplePayDelayedSessionFlow(), { + wrapper: Wrapper, + }); + + expect(result.current).toBe(false); + }); + + it('returns false when sessions object has no ApplePayObject', () => { + const sessionsValue = { + TAG: 'Loaded', + _0: {}, + }; + const Wrapper = createWrapper(sessionsValue); + const { result } = renderHook(() => ThirdPartyFlowHelpers.useIsApplePayDelayedSessionFlow(), { + wrapper: Wrapper, + }); + + expect(result.current).toBe(false); + }); + + it('returns false when ApplePay session token has no delayed_session_token', () => { + const sessionsValue = { + TAG: 'Loaded', + _0: { + session_token: [ + { + wallet_name: 'apple_pay', + someOtherField: 'value', + }, + ], + }, + }; + const Wrapper = createWrapper(sessionsValue); + const { result } = renderHook(() => ThirdPartyFlowHelpers.useIsApplePayDelayedSessionFlow(), { + wrapper: Wrapper, + }); + + expect(result.current).toBe(false); + }); + + it('returns true when ApplePay session token has delayed_session_token set to true', () => { + const sessionsValue = { + TAG: 'Loaded', + _0: { + session_token: [ + { + wallet_name: 'apple_pay', + delayed_session_token: true, + }, + ], + }, + }; + const Wrapper = createWrapper(sessionsValue); + const { result } = renderHook(() => ThirdPartyFlowHelpers.useIsApplePayDelayedSessionFlow(), { + wrapper: Wrapper, + }); + + expect(result.current).toBe(true); + }); + + it('returns false when ApplePay session token has delayed_session_token set to false', () => { + const sessionsValue = { + TAG: 'Loaded', + _0: { + session_token: [ + { + wallet_name: 'apple_pay', + delayed_session_token: false, + }, + ], + }, + }; + const Wrapper = createWrapper(sessionsValue); + const { result } = renderHook(() => ThirdPartyFlowHelpers.useIsApplePayDelayedSessionFlow(), { + wrapper: Wrapper, + }); + + expect(result.current).toBe(false); + }); + + it('returns false for non-Loaded session types', () => { + const Wrapper = createWrapper('NotLoaded'); + const { result } = renderHook(() => ThirdPartyFlowHelpers.useIsApplePayDelayedSessionFlow(), { + wrapper: Wrapper, + }); + + expect(result.current).toBe(false); + }); +}); + +describe('useIsGooglePayDelayedSessionFlow', () => { + it('returns false when sessions is in Loading state', () => { + const Wrapper = createWrapper('Loading'); + const { result } = renderHook(() => ThirdPartyFlowHelpers.useIsGooglePayDelayedSessionFlow(), { + wrapper: Wrapper, + }); + + expect(result.current).toBe(false); + }); + + it('returns false when sessions object has no GooglePayThirdPartyObject', () => { + const sessionsValue = { + TAG: 'Loaded', + _0: {}, + }; + const Wrapper = createWrapper(sessionsValue); + const { result } = renderHook(() => ThirdPartyFlowHelpers.useIsGooglePayDelayedSessionFlow(), { + wrapper: Wrapper, + }); + + expect(result.current).toBe(false); + }); + + it('returns false when GooglePay session token has no delayed_session_token', () => { + const sessionsValue = { + TAG: 'Loaded', + _0: { + session_token: [ + { + wallet_name: 'google_pay', + someOtherField: 'value', + }, + ], + }, + }; + const Wrapper = createWrapper(sessionsValue); + const { result } = renderHook(() => ThirdPartyFlowHelpers.useIsGooglePayDelayedSessionFlow(), { + wrapper: Wrapper, + }); + + expect(result.current).toBe(false); + }); + + it('returns true when GooglePay session token has delayed_session_token set to true', () => { + const sessionsValue = { + TAG: 'Loaded', + _0: { + session_token: [ + { + wallet_name: 'google_pay', + delayed_session_token: true, + }, + ], + }, + }; + const Wrapper = createWrapper(sessionsValue); + const { result } = renderHook(() => ThirdPartyFlowHelpers.useIsGooglePayDelayedSessionFlow(), { + wrapper: Wrapper, + }); + + expect(result.current).toBe(true); + }); + + it('returns false when GooglePay session token has delayed_session_token set to false', () => { + const sessionsValue = { + TAG: 'Loaded', + _0: { + session_token: [ + { + wallet_name: 'google_pay', + delayed_session_token: false, + }, + ], + }, + }; + const Wrapper = createWrapper(sessionsValue); + const { result } = renderHook(() => ThirdPartyFlowHelpers.useIsGooglePayDelayedSessionFlow(), { + wrapper: Wrapper, + }); + + expect(result.current).toBe(false); + }); + + it('returns false for non-Loaded session types', () => { + const Wrapper = createWrapper('NotLoaded'); + const { result } = renderHook(() => ThirdPartyFlowHelpers.useIsGooglePayDelayedSessionFlow(), { + wrapper: Wrapper, + }); + + expect(result.current).toBe(false); + }); +}); diff --git a/src/__tests__/UnifiedHelpersV2.test.ts b/src/__tests__/UnifiedHelpersV2.test.ts new file mode 100644 index 000000000..a8d8d3fa0 --- /dev/null +++ b/src/__tests__/UnifiedHelpersV2.test.ts @@ -0,0 +1,536 @@ +import { + getCardDetails, + itemToCustomerMapper, + getDynamicFieldsFromJsonDictV2, + getCardNetworks, + itemToPaymentsEnabledMapper, + itemToPaymentsObjMapper, + createPaymentsObjArr, + itemToPaymentDetails, + itemToPaymentMethodsUpdateMapper, + defaultAddress, + defaultBilling, + defaultPaymentMethods, + defaultCustomerMethods, + defaultPaymentsList, +} from '../Utilities/UnifiedHelpersV2.bs.js'; + +describe('UnifiedHelpersV2', () => { + describe('getCardDetails', () => { + it('should extract all card details from complete dict', () => { + const cardDict = { + card_network: 'visa', + last4_digits: '4242', + expiry_month: '12', + expiry_year: '2025', + card_holder_name: 'John Doe', + nick_name: 'My Visa', + card_issuer: 'US', + card_fingerprint: 'abc123', + card_isin: '424242', + card_type: 'credit', + saved_to_locker: true, + }; + + const result = getCardDetails(cardDict); + + expect(result.network).toBe('visa'); + expect(result.last4Digits).toBe('4242'); + expect(result.expiryMonth).toBe('12'); + expect(result.expiryYear).toBe('2025'); + expect(result.cardHolderName).toBe('John Doe'); + expect(result.nickname).toBe('My Visa'); + expect(result.issuerCountry).toBe('US'); + expect(result.cardFingerprint).toBe('abc123'); + expect(result.cardIsin).toBe('424242'); + expect(result.cardIssuer).toBe('US'); + expect(result.cardType).toBe('credit'); + expect(result.savedToLocker).toBe(true); + }); + + it('should return default values for missing fields', () => { + const cardDict = {}; + + const result = getCardDetails(cardDict); + + expect(result.network).toBeUndefined(); + expect(result.last4Digits).toBe(''); + expect(result.expiryMonth).toBe(''); + expect(result.expiryYear).toBe(''); + expect(result.cardHolderName).toBeUndefined(); + expect(result.nickname).toBeUndefined(); + expect(result.issuerCountry).toBeUndefined(); + expect(result.cardFingerprint).toBe(''); + expect(result.cardIsin).toBe(''); + expect(result.cardIssuer).toBe(''); + expect(result.cardType).toBe(''); + expect(result.savedToLocker).toBe(false); + }); + + it('should handle partial card data', () => { + const cardDict = { + card_network: 'mastercard', + last4_digits: '5555', + }; + + const result = getCardDetails(cardDict); + + expect(result.network).toBe('mastercard'); + expect(result.last4Digits).toBe('5555'); + expect(result.expiryMonth).toBe(''); + expect(result.savedToLocker).toBe(false); + }); + }); + + describe('itemToCustomerMapper', () => { + it('should map customer array to payment methods', () => { + const customerArray = [ + { + payment_method_token: 'token123', + customer_id: 'cust_123', + payment_method_type: 'card', + payment_method_subtype: 'credit', + recurring_enabled: true, + is_default: true, + requires_cvv: true, + last_used_at: '2024-01-01', + created: '2023-01-01', + payment_method_data: { + card: { + card_network: 'visa', + last4_digits: '4242', + expiry_month: '12', + expiry_year: '2025', + card_fingerprint: 'fp123', + card_isin: '424242', + card_type: 'credit', + saved_to_locker: true, + }, + }, + }, + ]; + + const result = itemToCustomerMapper(customerArray); + + expect(result.length).toBe(1); + expect(result[0].paymentToken).toBe('token123'); + expect(result[0].customerId).toBe('cust_123'); + expect(result[0].paymentMethodType).toBe('card'); + expect(result[0].paymentMethodSubType).toBe('credit'); + expect(result[0].recurringEnabled).toBe(true); + expect(result[0].isDefault).toBe(true); + expect(result[0].requiresCvv).toBe(true); + expect(result[0].paymentMethodData.card.last4Digits).toBe('4242'); + expect(result[0].bank.mask).toBe(''); + }); + + it('should return empty array for empty input', () => { + const result = itemToCustomerMapper([]); + expect(result).toEqual([]); + }); + + it('should handle invalid items in array', () => { + const customerArray = [null, undefined, 'invalid']; + const result = itemToCustomerMapper(customerArray); + expect(result).toEqual([]); + }); + + it('should handle items with missing payment_method_data', () => { + const customerArray = [ + { + payment_method_token: 'token456', + customer_id: 'cust_456', + payment_method_type: 'card', + payment_method_subtype: 'debit', + }, + ]; + + const result = itemToCustomerMapper(customerArray); + + expect(result.length).toBe(1); + expect(result[0].paymentToken).toBe('token456'); + expect(result[0].paymentMethodData.card.last4Digits).toBe(''); + }); + }); + + describe('getDynamicFieldsFromJsonDictV2', () => { + it('should extract required fields from dict', () => { + const dict = { + required_fields: [ + { + required_field: 'payment_method_data.billing.address.line1', + display_name: 'Address Line 1', + field_type: 'AddressLine1', + value: '123 Main St', + }, + { + required_field: 'payment_method_data.email', + display_name: 'Email', + field_type: 'Email', + value: 'test@example.com', + }, + ], + }; + + const result = getDynamicFieldsFromJsonDictV2(dict, false); + + expect(result.length).toBe(2); + expect(result[0].required_field).toBe('payment_method_data.billing.address.line1'); + expect(result[0].display_name).toBe('Address Line 1'); + expect(result[0].value).toBe('123 Main St'); + }); + + it('should return empty array for dict without required_fields', () => { + const dict = {}; + const result = getDynamicFieldsFromJsonDictV2(dict, false); + expect(result).toEqual([]); + }); + + it('should handle bancontact payment method type', () => { + const dict = { + required_fields: [ + JSON.stringify({ + required_field: 'payment_method_data.billing.address.line1', + display_name: 'Address', + field_type: 'AddressLine1', + value: '', + }), + ], + }; + + const result = getDynamicFieldsFromJsonDictV2(dict, true); + expect(result.length).toBe(1); + }); + }); + + describe('getCardNetworks', () => { + it('should extract card networks from array', () => { + const networksArr = [ + { + card_network: 'Visa', + eligible_connectors: ['connector1', 'connector2'], + surcharge_details: { + surcharge_amount: 100, + }, + }, + { + card_network: 'Mastercard', + eligible_connectors: ['connector3'], + surcharge_details: null, + }, + ]; + + const result = getCardNetworks(networksArr); + + expect(result.length).toBe(2); + expect(result[0].cardNetwork).toBe('VISA'); + expect(result[0].eligibleConnectors).toEqual(['connector1', 'connector2']); + expect(result[1].cardNetwork).toBe('MASTERCARD'); + }); + + it('should return empty array for empty input', () => { + const result = getCardNetworks([]); + expect(result).toEqual([]); + }); + + it('should handle invalid items in array', () => { + const networksArr = [null, undefined, 'invalid']; + const result = getCardNetworks(networksArr); + expect(result).toEqual([]); + }); + }); + + describe('itemToPaymentsEnabledMapper', () => { + it('should map payment methods enabled array', () => { + const methodsArray = [ + { + payment_method_type: 'card', + payment_method_subtype: 'credit', + bank_names: [], + card_networks: [ + { + card_network: 'Visa', + eligible_connectors: ['stripe'], + }, + ], + required_fields: [ + { + required_field: 'payment_method_data.email', + display_name: 'Email', + field_type: 'Email', + value: '', + }, + ], + payment_experience: ['ADD_AND_PAY'], + }, + ]; + + const result = itemToPaymentsEnabledMapper(methodsArray); + + expect(result.length).toBe(1); + expect(result[0].paymentMethodType).toBe('card'); + expect(result[0].paymentMethodSubtype).toBe('credit'); + expect(result[0].cardNetworks.length).toBe(1); + expect(result[0].cardNetworks[0].cardNetwork).toBe('VISA'); + expect(result[0].requiredFields.length).toBe(1); + expect(result[0].paymentExperience.length).toBe(1); + }); + + it('should return empty array for empty input', () => { + const result = itemToPaymentsEnabledMapper([]); + expect(result).toEqual([]); + }); + + it('should handle bancontact_card subtype', () => { + const methodsArray = [ + { + payment_method_type: 'card', + payment_method_subtype: 'bancontact_card', + bank_names: [], + card_networks: [], + required_fields: [], + payment_experience: [], + }, + ]; + + const result = itemToPaymentsEnabledMapper(methodsArray); + + expect(result.length).toBe(1); + expect(result[0].paymentMethodSubtype).toBe('bancontact_card'); + }); + }); + + describe('itemToPaymentsObjMapper', () => { + it('should map customer dict to payments object', () => { + const customerDict = { + payment_methods_enabled: [ + { + payment_method_type: 'card', + payment_method_subtype: 'credit', + bank_names: [], + card_networks: [], + required_fields: [], + payment_experience: [], + }, + ], + customer_payment_methods: [ + { + payment_method_token: 'token123', + customer_id: 'cust_123', + payment_method_type: 'card', + payment_method_subtype: 'credit', + payment_method_data: { + card: { + card_network: 'visa', + last4_digits: '4242', + }, + }, + }, + ], + }; + + const result = itemToPaymentsObjMapper(customerDict); + + expect(result.paymentMethodsEnabled.length).toBe(1); + expect(result.customerPaymentMethods.length).toBe(1); + expect(result.customerPaymentMethods[0].paymentToken).toBe('token123'); + }); + + it('should handle empty dict', () => { + const result = itemToPaymentsObjMapper({}); + + expect(result.paymentMethodsEnabled).toEqual([]); + expect(result.customerPaymentMethods).toEqual([]); + }); + }); + + describe('createPaymentsObjArr', () => { + it('should create payments object array from dict', () => { + const dict = { + payments: { + payment_methods_enabled: [ + { + payment_method_type: 'card', + payment_method_subtype: 'credit', + bank_names: [], + card_networks: [], + required_fields: [], + payment_experience: [], + }, + ], + customer_payment_methods: [], + }, + }; + + const result = createPaymentsObjArr(dict, 'payments'); + + expect(result.TAG).toBe('LoadedV2'); + expect(result._0.paymentMethodsEnabled.length).toBe(1); + }); + + it('should handle missing key in dict', () => { + const dict = {}; + const result = createPaymentsObjArr(dict, 'missing_key'); + + expect(result.TAG).toBe('LoadedV2'); + expect(result._0.paymentMethodsEnabled).toEqual([]); + expect(result._0.customerPaymentMethods).toEqual([]); + }); + + it('should handle null value for key', () => { + const dict = { + payments: null, + }; + const result = createPaymentsObjArr(dict, 'payments'); + + expect(result.TAG).toBe('LoadedV2'); + }); + }); + + describe('itemToPaymentDetails', () => { + it('should extract payment details from dict', () => { + const dict = { + payment_method_token: 'token789', + customer_id: 'cust_789', + payment_method_type: 'card', + payment_method_subtype: 'debit', + recurring_enabled: false, + is_default: false, + requires_cvv: true, + last_used_at: '2024-06-01', + created: '2024-01-01', + payment_method_data: { + card: { + card_network: 'mastercard', + last4_digits: '5555', + expiry_month: '06', + expiry_year: '2026', + card_fingerprint: 'fp456', + card_isin: '555555', + card_type: 'debit', + saved_to_locker: true, + }, + }, + }; + + const result = itemToPaymentDetails(dict); + + expect(result.paymentToken).toBe('token789'); + expect(result.customerId).toBe('cust_789'); + expect(result.paymentMethodType).toBe('card'); + expect(result.paymentMethodSubType).toBe('debit'); + expect(result.recurringEnabled).toBe(false); + expect(result.isDefault).toBe(false); + expect(result.requiresCvv).toBe(true); + expect(result.paymentMethodData.card.last4Digits).toBe('5555'); + expect(result.paymentMethodData.card.network).toBe('mastercard'); + expect(result.bank.mask).toBe(''); + }); + + it('should handle dict with missing payment_method_data', () => { + const dict = { + payment_method_token: 'token999', + customer_id: 'cust_999', + payment_method_type: 'card', + }; + + const result = itemToPaymentDetails(dict); + + expect(result.paymentToken).toBe('token999'); + expect(result.paymentMethodData.card.last4Digits).toBe(''); + }); + + it('should return default values for empty dict', () => { + const result = itemToPaymentDetails({}); + + expect(result.paymentToken).toBe(''); + expect(result.customerId).toBe(''); + expect(result.recurringEnabled).toBe(false); + expect(result.isDefault).toBe(false); + }); + }); + + describe('itemToPaymentMethodsUpdateMapper', () => { + it('should map payment methods update dict', () => { + const dict = { + payment_method_data: { + card: { + card_network: 'visa', + last4_digits: '1234', + expiry_month: '01', + expiry_year: '2027', + card_fingerprint: 'fp789', + card_isin: '123456', + card_type: 'credit', + saved_to_locker: false, + }, + }, + associated_payment_methods: [], + }; + + const result = itemToPaymentMethodsUpdateMapper(dict); + + expect(result.paymentMethodData.card.last4Digits).toBe('1234'); + expect(result.paymentMethodData.card.network).toBe('visa'); + expect(result.associatedPaymentMethods).toEqual([]); + }); + + it('should handle missing payment_method_data', () => { + const dict = { + associated_payment_methods: [], + }; + + const result = itemToPaymentMethodsUpdateMapper(dict); + + expect(result.paymentMethodData.card.last4Digits).toBe(''); + }); + + it('should handle empty dict', () => { + const result = itemToPaymentMethodsUpdateMapper({}); + + expect(result.paymentMethodData.card.last4Digits).toBe(''); + }); + }); + + describe('default objects', () => { + it('defaultAddress should have expected structure', () => { + expect(defaultAddress.city).toBe(''); + expect(defaultAddress.country).toBe(''); + expect(defaultAddress.line1).toBe(''); + expect(defaultAddress.line2).toBe(''); + expect(defaultAddress.line3).toBe(''); + expect(defaultAddress.zip).toBe(''); + expect(defaultAddress.state).toBe(''); + expect(defaultAddress.firstName).toBe(''); + expect(defaultAddress.lastName).toBe(''); + }); + + it('defaultBilling should have expected structure', () => { + expect(defaultBilling.address).toEqual(defaultAddress); + expect(defaultBilling.phone.number).toBe(''); + expect(defaultBilling.phone.countryCode).toBe(''); + expect(defaultBilling.email).toBe(''); + }); + + it('defaultPaymentMethods should have expected structure', () => { + expect(defaultPaymentMethods.paymentMethodType).toBe(''); + expect(defaultPaymentMethods.paymentMethodSubtype).toBe(''); + expect(defaultPaymentMethods.requiredFields).toEqual([]); + expect(defaultPaymentMethods.paymentExperience).toEqual([]); + }); + + it('defaultCustomerMethods should have expected structure', () => { + expect(defaultCustomerMethods.paymentToken).toBe(''); + expect(defaultCustomerMethods.customerId).toBe(''); + expect(defaultCustomerMethods.paymentMethodType).toBe(''); + expect(defaultCustomerMethods.recurringEnabled).toBe(false); + expect(defaultCustomerMethods.isDefault).toBe(false); + expect(defaultCustomerMethods.bank.mask).toBe(''); + }); + + it('defaultPaymentsList should have expected structure', () => { + expect(Array.isArray(defaultPaymentsList.paymentMethodsEnabled)).toBe(true); + expect(Array.isArray(defaultPaymentsList.customerPaymentMethods)).toBe(true); + }); + }); +}); diff --git a/src/__tests__/UtilityHooks.test.ts b/src/__tests__/UtilityHooks.test.ts new file mode 100644 index 000000000..396589cc0 --- /dev/null +++ b/src/__tests__/UtilityHooks.test.ts @@ -0,0 +1,430 @@ +import { renderHook, act } from '@testing-library/react'; +import { RecoilRoot } from 'recoil'; +import * as React from 'react'; +import * as UtilityHooks from '../Hooks/UtilityHooks.bs.js'; +import * as RecoilAtoms from '../Utilities/RecoilAtoms.bs.js'; +import * as PaymentUtils from '../Utilities/PaymentUtils.bs.js'; + +const mockHandlePostMessageEvents = jest.fn(); +const mockMessageParentWindow = jest.fn(); +const mockGetDictFromJson = jest.fn((obj: any) => (typeof obj === 'object' && obj !== null ? obj : {})); + +jest.mock('../Utilities/Utils.bs.js', () => ({ + handlePostMessageEvents: (a: any, b: any, c: any, d: any, e: any) => mockHandlePostMessageEvents(a, b, c, d, e), + getDictFromJson: (obj: any) => mockGetDictFromJson(obj), + messageParentWindow: (a: any, b: any) => mockMessageParentWindow(a, b), +})); + +jest.mock('../Payments/PaymentMethodsRecord.bs.js', () => ({ + itemToObjMapper: jest.fn((dict) => ({ + isGuestCustomer: dict.isGuestCustomer, + })), +})); + +jest.mock('../Utilities/PaymentBody.bs.js', () => ({})); + +const createWrapperWithAtoms = (atomValues: any) => { + return function Wrapper({ children }: { children: React.ReactNode }) { + return React.createElement( + RecoilRoot, + { + initializeState: ({ set }: any) => { + Object.entries(atomValues).forEach(([key, value]) => { + const atom = (RecoilAtoms as any)[key] || (PaymentUtils as any)[key]; + if (atom) { + set(atom, value); + } + }); + }, + }, + children + ); + }; +}; + +describe('useIsGuestCustomer', () => { + it('returns true when paymentMethodList is in Loading state', () => { + const Wrapper = createWrapperWithAtoms({ + paymentMethodList: 'Loading', + optionAtom: { customerPaymentMethods: undefined }, + }); + const { result } = renderHook(() => UtilityHooks.useIsGuestCustomer(), { wrapper: Wrapper }); + + expect(result.current).toBe(true); + }); + + it('returns true when paymentMethodList has non-Loaded tag', () => { + const Wrapper = createWrapperWithAtoms({ + paymentMethodList: { TAG: 'Error', _0: 'error' }, + optionAtom: { customerPaymentMethods: undefined }, + }); + const { result } = renderHook(() => UtilityHooks.useIsGuestCustomer(), { wrapper: Wrapper }); + + expect(result.current).toBe(true); + }); + + it('returns the isGuestCustomer value when present in paymentMethodList', () => { + const Wrapper = createWrapperWithAtoms({ + paymentMethodList: { + TAG: 'Loaded', + _0: { isGuestCustomer: true }, + }, + optionAtom: { customerPaymentMethods: undefined }, + }); + const { result } = renderHook(() => UtilityHooks.useIsGuestCustomer(), { wrapper: Wrapper }); + + expect(result.current).toBe(true); + }); + + it('returns false when isGuestCustomer is explicitly false in paymentMethodList', () => { + const Wrapper = createWrapperWithAtoms({ + paymentMethodList: { + TAG: 'Loaded', + _0: { isGuestCustomer: false }, + }, + optionAtom: { customerPaymentMethods: undefined }, + }); + const { result } = renderHook(() => UtilityHooks.useIsGuestCustomer(), { wrapper: Wrapper }); + + expect(result.current).toBe(false); + }); + + it('falls back to customerPaymentMethods when isGuestCustomer is None - LoadedSavedCards with guest', () => { + const Wrapper = createWrapperWithAtoms({ + paymentMethodList: { + TAG: 'Loaded', + _0: {}, + }, + optionAtom: { + customerPaymentMethods: { + TAG: 'LoadedSavedCards', + _0: [], + _1: true, + }, + }, + }); + const { result } = renderHook(() => UtilityHooks.useIsGuestCustomer(), { wrapper: Wrapper }); + + expect(result.current).toBe(true); + }); + + it('falls back to customerPaymentMethods when isGuestCustomer is None - LoadedSavedCards with non-guest', () => { + const Wrapper = createWrapperWithAtoms({ + paymentMethodList: { + TAG: 'Loaded', + _0: {}, + }, + optionAtom: { + customerPaymentMethods: { + TAG: 'LoadedSavedCards', + _0: [], + _1: false, + }, + }, + }); + const { result } = renderHook(() => UtilityHooks.useIsGuestCustomer(), { wrapper: Wrapper }); + + expect(result.current).toBe(false); + }); +}); + +describe('useHandlePostMessages', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('calls handlePostMessageEvents with provided parameters', () => { + const complete = jest.fn(); + const empty = jest.fn(); + const paymentType = 'card'; + const loggerState = { logInfo: jest.fn(), logError: jest.fn() }; + + const Wrapper = createWrapperWithAtoms({ + loggerAtom: loggerState, + }); + + renderHook(() => UtilityHooks.useHandlePostMessages(complete, empty, paymentType), { + wrapper: Wrapper, + }); + + expect(mockHandlePostMessageEvents).toHaveBeenCalledWith( + complete, + empty, + paymentType, + loggerState, + false + ); + }); + + it('calls handlePostMessageEvents with savedMethod=true when provided', () => { + const complete = jest.fn(); + const empty = jest.fn(); + const paymentType = 'card'; + const loggerState = { logInfo: jest.fn(), logError: jest.fn() }; + + const Wrapper = createWrapperWithAtoms({ + loggerAtom: loggerState, + }); + + renderHook(() => UtilityHooks.useHandlePostMessages(complete, empty, paymentType, true), { + wrapper: Wrapper, + }); + + expect(mockHandlePostMessageEvents).toHaveBeenCalledWith( + complete, + empty, + paymentType, + loggerState, + true + ); + }); + + it('defaults savedMethod to false when not provided', () => { + const complete = jest.fn(); + const empty = jest.fn(); + const paymentType = 'wallet'; + const loggerState = { logInfo: jest.fn(), logError: jest.fn() }; + + const Wrapper = createWrapperWithAtoms({ + loggerAtom: loggerState, + }); + + renderHook(() => UtilityHooks.useHandlePostMessages(complete, empty, paymentType), { + wrapper: Wrapper, + }); + + expect(mockHandlePostMessageEvents).toHaveBeenCalledWith( + complete, + empty, + paymentType, + loggerState, + false + ); + }); +}); + +describe('useIsCustomerAcceptanceRequired', () => { + it('returns true when displaySavedPaymentMethodsCheckbox is true and isSaveCardsChecked is true', () => { + const Wrapper = createWrapperWithAtoms({ + paymentMethodListValue: { payment_type: 'NORMAL' }, + }); + + const { result } = renderHook( + () => UtilityHooks.useIsCustomerAcceptanceRequired(true, true, false), + { wrapper: Wrapper } + ); + + expect(result.current).toBe(true); + }); + + it('returns true when displaySavedPaymentMethodsCheckbox is true and payment_type is SETUP_MANDATE', () => { + const Wrapper = createWrapperWithAtoms({ + paymentMethodListValue: { payment_type: 'SETUP_MANDATE' }, + }); + + const { result } = renderHook( + () => UtilityHooks.useIsCustomerAcceptanceRequired(true, false, false), + { wrapper: Wrapper } + ); + + expect(result.current).toBe(true); + }); + + it('returns false when displaySavedPaymentMethodsCheckbox is true, isSaveCardsChecked is false, and payment_type is NORMAL', () => { + const Wrapper = createWrapperWithAtoms({ + paymentMethodListValue: { payment_type: 'NORMAL' }, + }); + + const { result } = renderHook( + () => UtilityHooks.useIsCustomerAcceptanceRequired(true, false, false), + { wrapper: Wrapper } + ); + + expect(result.current).toBe(false); + }); + + it('returns false when displaySavedPaymentMethodsCheckbox is false, isGuestCustomer is true, and payment_type is NORMAL', () => { + const Wrapper = createWrapperWithAtoms({ + paymentMethodListValue: { payment_type: 'NORMAL' }, + }); + + const { result } = renderHook( + () => UtilityHooks.useIsCustomerAcceptanceRequired(false, false, true), + { wrapper: Wrapper } + ); + + expect(result.current).toBe(false); + }); + + it('returns true when displaySavedPaymentMethodsCheckbox is false, isGuestCustomer is false, and payment_type is not NORMAL', () => { + const Wrapper = createWrapperWithAtoms({ + paymentMethodListValue: { payment_type: 'SETUP_MANDATE' }, + }); + + const { result } = renderHook( + () => UtilityHooks.useIsCustomerAcceptanceRequired(false, false, false), + { wrapper: Wrapper } + ); + + expect(result.current).toBe(true); + }); + + it('returns false when displaySavedPaymentMethodsCheckbox is false, isGuestCustomer is false, and payment_type is NORMAL', () => { + const Wrapper = createWrapperWithAtoms({ + paymentMethodListValue: { payment_type: 'NORMAL' }, + }); + + const { result } = renderHook( + () => UtilityHooks.useIsCustomerAcceptanceRequired(false, false, false), + { wrapper: Wrapper } + ); + + expect(result.current).toBe(false); + }); +}); + +describe('useSendEventsToParent', () => { + let addEventListenerSpy: jest.SpyInstance; + let removeEventListenerSpy: jest.SpyInstance; + + beforeEach(() => { + addEventListenerSpy = jest.spyOn(window, 'addEventListener'); + removeEventListenerSpy = jest.spyOn(window, 'removeEventListener'); + jest.clearAllMocks(); + }); + + afterEach(() => { + addEventListenerSpy.mockRestore(); + removeEventListenerSpy.mockRestore(); + }); + + it('adds message event listener on mount', () => { + const eventsToSendToParent = ['paymentComplete', 'paymentError']; + + renderHook(() => UtilityHooks.useSendEventsToParent(eventsToSendToParent)); + + expect(addEventListenerSpy).toHaveBeenCalledWith('message', expect.any(Function)); + }); + + it('removes message event listener on unmount', () => { + const eventsToSendToParent = ['paymentComplete']; + + const { unmount } = renderHook(() => + UtilityHooks.useSendEventsToParent(eventsToSendToParent) + ); + + unmount(); + + expect(removeEventListenerSpy).toHaveBeenCalledWith('message', expect.any(Function)); + }); + + it('calls messageParentWindow when matching event is received', () => { + const eventsToSendToParent = ['paymentComplete', 'paymentError']; + + renderHook(() => UtilityHooks.useSendEventsToParent(eventsToSendToParent)); + + const messageHandler = addEventListenerSpy.mock.calls.find( + (call) => call[0] === 'message' + )?.[1]; + + act(() => { + messageHandler?.({ + data: { paymentComplete: { status: 'success' } }, + }); + }); + + expect(mockMessageParentWindow).toHaveBeenCalled(); + }); + + it('does not call messageParentWindow when no matching event is received', () => { + const eventsToSendToParent = ['paymentComplete']; + + renderHook(() => UtilityHooks.useSendEventsToParent(eventsToSendToParent)); + + const messageHandler = addEventListenerSpy.mock.calls.find( + (call) => call[0] === 'message' + )?.[1]; + + act(() => { + messageHandler?.({ + data: { unknownEvent: { status: 'something' } }, + }); + }); + + expect(mockMessageParentWindow).not.toHaveBeenCalled(); + }); +}); + +describe('useUpdateRedirectionFlags', () => { + it('returns a function that updates redirection flags', () => { + const Wrapper = createWrapperWithAtoms({ + redirectionFlagsAtom: { + shouldUseTopRedirection: false, + shouldRemoveBeforeUnloadEvents: false, + }, + }); + + const { result } = renderHook(() => UtilityHooks.useUpdateRedirectionFlags(), { + wrapper: Wrapper, + }); + + expect(typeof result.current).toBe('function'); + }); + + it('updates shouldUseTopRedirection when provided true', () => { + const Wrapper = createWrapperWithAtoms({ + redirectionFlagsAtom: { + shouldUseTopRedirection: false, + shouldRemoveBeforeUnloadEvents: false, + }, + }); + + const { result } = renderHook(() => UtilityHooks.useUpdateRedirectionFlags(), { + wrapper: Wrapper, + }); + + act(() => { + result.current({ shouldUseTopRedirection: true }); + }); + + expect(typeof result.current).toBe('function'); + }); + + it('updates shouldRemoveBeforeUnloadEvents when provided true', () => { + const Wrapper = createWrapperWithAtoms({ + redirectionFlagsAtom: { + shouldUseTopRedirection: false, + shouldRemoveBeforeUnloadEvents: false, + }, + }); + + const { result } = renderHook(() => UtilityHooks.useUpdateRedirectionFlags(), { + wrapper: Wrapper, + }); + + act(() => { + result.current({ shouldRemoveBeforeUnloadEvents: true }); + }); + + expect(typeof result.current).toBe('function'); + }); + + it('handles undefined values by keeping current state', () => { + const Wrapper = createWrapperWithAtoms({ + redirectionFlagsAtom: { + shouldUseTopRedirection: true, + shouldRemoveBeforeUnloadEvents: true, + }, + }); + + const { result } = renderHook(() => UtilityHooks.useUpdateRedirectionFlags(), { + wrapper: Wrapper, + }); + + act(() => { + result.current({}); + }); + + expect(typeof result.current).toBe('function'); + }); +}); diff --git a/src/__tests__/VaultHelpers.test.ts b/src/__tests__/VaultHelpers.test.ts new file mode 100644 index 000000000..5ebed0f9a --- /dev/null +++ b/src/__tests__/VaultHelpers.test.ts @@ -0,0 +1,222 @@ +import { + getVaultModeFromName, + getVaultNameFromMode, + getVaultName, + getVGSVaultDetails, + getHyperswitchVaultDetails, +} from '../Utilities/VaultHelpers.bs.js'; + +describe('VaultHelpers', () => { + describe('getVaultModeFromName', () => { + it('should return Hyperswitch for hyperswitch_vault', () => { + expect(getVaultModeFromName('hyperswitch_vault')).toBe('Hyperswitch'); + }); + + it('should return VeryGoodSecurity for vgs', () => { + expect(getVaultModeFromName('vgs')).toBe('VeryGoodSecurity'); + }); + + it('should return None for unknown vault name', () => { + expect(getVaultModeFromName('unknown_vault')).toBe('None'); + }); + + it('should return None for empty string', () => { + expect(getVaultModeFromName('')).toBe('None'); + }); + + it('should be case sensitive', () => { + expect(getVaultModeFromName('VGS')).toBe('None'); + expect(getVaultModeFromName('HYPERSWITCH_VAULT')).toBe('None'); + }); + }); + + describe('getVaultNameFromMode', () => { + it('should return hyperswitch_vault for Hyperswitch mode', () => { + expect(getVaultNameFromMode('Hyperswitch')).toBe('hyperswitch_vault'); + }); + + it('should return vgs for VeryGoodSecurity mode', () => { + expect(getVaultNameFromMode('VeryGoodSecurity')).toBe('vgs'); + }); + + it('should return empty string for None mode', () => { + expect(getVaultNameFromMode('None')).toBe(''); + }); + + it('should be case sensitive', () => { + expect(getVaultNameFromMode('hyperswitch')).toBe(undefined); + expect(getVaultNameFromMode('verygoodsecurity')).toBe(undefined); + }); + }); + + describe('getVaultName', () => { + it('should extract vault name from Loaded session object', () => { + const sessionObj = { + TAG: 'Loaded', + _0: { + vault_details: { + hyperswitch_vault: { + publishable_key: 'pk_test_123', + }, + }, + }, + }; + expect(getVaultName(sessionObj)).toBe('hyperswitch_vault'); + }); + + it('should return empty string for non-Loaded session', () => { + const sessionObj = { TAG: 'Loading' }; + expect(getVaultName(sessionObj)).toBe(''); + }); + + it('should return empty string for non-object input', () => { + expect(getVaultName('string')).toBe(''); + expect(getVaultName(123)).toBe(''); + }); + + it('should extract vgs vault name when present', () => { + const sessionObj = { + TAG: 'Loaded', + _0: { + vault_details: { + vgs: { + external_vault_id: 'vgs_id_123', + }, + }, + }, + }; + expect(getVaultName(sessionObj)).toBe('vgs'); + }); + }); + + describe('getVGSVaultDetails', () => { + it('should extract VGS vault details from Loaded session', () => { + const sessionObj = { + TAG: 'Loaded', + _0: { + vault_details: { + vgs: { + external_vault_id: 'vgs_vault_123', + sdk_env: 'sandbox', + }, + }, + }, + }; + const result = getVGSVaultDetails(sessionObj, 'vgs'); + expect(result.vaultId).toBe('vgs_vault_123'); + expect(result.vaultEnv).toBe('sandbox'); + }); + + it('should return empty strings for non-Loaded session', () => { + const sessionObj = { TAG: 'Loading' }; + const result = getVGSVaultDetails(sessionObj, 'vgs'); + expect(result.vaultId).toBe(''); + expect(result.vaultEnv).toBe(''); + }); + + it('should return empty strings for non-object input', () => { + const result = getVGSVaultDetails('string', 'vgs'); + expect(result.vaultId).toBe(''); + expect(result.vaultEnv).toBe(''); + }); + + it('should return empty strings for missing vault details', () => { + const sessionObj = { + TAG: 'Loaded', + _0: { + vault_details: {}, + }, + }; + const result = getVGSVaultDetails(sessionObj, 'vgs'); + expect(result.vaultId).toBe(''); + expect(result.vaultEnv).toBe(''); + }); + + it('should handle partial vault details', () => { + const sessionObj = { + TAG: 'Loaded', + _0: { + vault_details: { + vgs: { + external_vault_id: 'vgs_vault_123', + }, + }, + }, + }; + const result = getVGSVaultDetails(sessionObj, 'vgs'); + expect(result.vaultId).toBe('vgs_vault_123'); + expect(result.vaultEnv).toBe(''); + }); + }); + + describe('getHyperswitchVaultDetails', () => { + it('should extract all Hyperswitch vault details from Loaded session', () => { + const sessionObj = { + TAG: 'Loaded', + _0: { + vault_details: { + hyperswitch_vault: { + payment_method_session_id: 'pm_session_123', + client_secret: 'secret_abc', + publishable_key: 'pk_test_123', + profile_id: 'profile_456', + }, + }, + }, + }; + const result = getHyperswitchVaultDetails(sessionObj); + expect(result.pmSessionId).toBe('pm_session_123'); + expect(result.pmClientSecret).toBe('secret_abc'); + expect(result.vaultPublishableKey).toBe('pk_test_123'); + expect(result.vaultProfileId).toBe('profile_456'); + }); + + it('should return empty strings for non-Loaded session', () => { + const sessionObj = { TAG: 'Loading' }; + const result = getHyperswitchVaultDetails(sessionObj); + expect(result.pmSessionId).toBe(''); + expect(result.pmClientSecret).toBe(''); + expect(result.vaultPublishableKey).toBe(''); + expect(result.vaultProfileId).toBe(''); + }); + + it('should return empty strings for non-object input', () => { + const result = getHyperswitchVaultDetails('string'); + expect(result.pmSessionId).toBe(''); + expect(result.pmClientSecret).toBe(''); + expect(result.vaultPublishableKey).toBe(''); + expect(result.vaultProfileId).toBe(''); + }); + + it('should handle partial Hyperswitch vault details', () => { + const sessionObj = { + TAG: 'Loaded', + _0: { + vault_details: { + hyperswitch_vault: { + payment_method_session_id: 'pm_session_123', + publishable_key: 'pk_test_123', + }, + }, + }, + }; + const result = getHyperswitchVaultDetails(sessionObj); + expect(result.pmSessionId).toBe('pm_session_123'); + expect(result.pmClientSecret).toBe(''); + expect(result.vaultPublishableKey).toBe('pk_test_123'); + expect(result.vaultProfileId).toBe(''); + }); + + it('should handle missing vault_details', () => { + const sessionObj = { + TAG: 'Loaded', + _0: {}, + }; + const result = getHyperswitchVaultDetails(sessionObj); + expect(result.pmSessionId).toBe(''); + expect(result.pmClientSecret).toBe(''); + expect(result.vaultPublishableKey).toBe(''); + expect(result.vaultProfileId).toBe(''); + }); + }); +}); diff --git a/src/__tests__/WebHyperLogger.test.ts b/src/__tests__/WebHyperLogger.test.ts new file mode 100644 index 000000000..820d63d20 --- /dev/null +++ b/src/__tests__/WebHyperLogger.test.ts @@ -0,0 +1,393 @@ +import { logFileToObj, getSourceString, getRefFromOption, make } from '../hyper-log-catcher/HyperLogger.bs.js'; + +describe('HyperLogger', () => { + describe('logFileToObj', () => { + it('should convert log file entry to JSON object with all fields', () => { + const logFile = { + timestamp: '1234567890', + logType: 'DEBUG', + category: 'API', + source: 'hyper sdk', + version: '1.0.0', + value: 'test value', + sessionId: 'session_123', + merchantId: 'merchant_456', + paymentId: 'payment_789', + appId: 'app_001', + platform: 'web', + userAgent: 'Mozilla/5.0', + eventName: 'TEST_EVENT', + browserName: 'Chrome', + browserVersion: '120.0', + latency: '100', + firstEvent: true, + paymentMethod: 'card', + }; + + const result = logFileToObj(logFile); + + expect(result.timestamp).toBe('1234567890'); + expect(result.log_type).toBe('DEBUG'); + expect(result.component).toBe('WEB'); + expect(result.category).toBe('API'); + expect(result.source).toBe('HYPER_SDK'); + expect(result.version).toBe('1.0.0'); + expect(result.value).toBe('test value'); + expect(result.session_id).toBe('session_123'); + expect(result.merchant_id).toBe('merchant_456'); + expect(result.payment_id).toBe('payment_789'); + expect(result.app_id).toBe('app_001'); + expect(result.platform).toBe('WEB'); + expect(result.user_agent).toBe('Mozilla/5.0'); + expect(result.event_name).toBe('TEST_EVENT'); + expect(result.browser_name).toBe('CHROME'); + expect(result.browser_version).toBe('120.0'); + expect(result.latency).toBe('100'); + expect(result.first_event).toBe('true'); + expect(result.payment_method).toBe('CARD'); + }); + + it('should handle INFO log type', () => { + const logFile = { + timestamp: '1234567890', + logType: 'INFO', + category: 'USER_EVENT', + source: 'test source', + version: '1.0.0', + value: '', + sessionId: '', + merchantId: '', + paymentId: '', + appId: '', + platform: 'web', + userAgent: '', + eventName: 'APP_RENDERED', + browserName: 'Firefox', + browserVersion: '115.0', + latency: '', + firstEvent: false, + paymentMethod: '', + }; + + const result = logFileToObj(logFile); + expect(result.log_type).toBe('INFO'); + expect(result.category).toBe('USER_EVENT'); + expect(result.browser_name).toBe('FIREFOX'); + }); + + it('should handle ERROR log type', () => { + const logFile = { + timestamp: '1234567890', + logType: 'ERROR', + category: 'USER_ERROR', + source: 'error source', + version: '1.0.0', + value: 'error message', + sessionId: '', + merchantId: '', + paymentId: '', + appId: '', + platform: 'web', + userAgent: '', + eventName: 'SDK_CRASH', + browserName: 'Safari', + browserVersion: '17.0', + latency: '500', + firstEvent: true, + paymentMethod: '', + }; + + const result = logFileToObj(logFile); + expect(result.log_type).toBe('ERROR'); + expect(result.category).toBe('USER_ERROR'); + expect(result.browser_name).toBe('SAFARI'); + }); + + it('should handle WARNING log type', () => { + const logFile = { + timestamp: '1234567890', + logType: 'WARNING', + category: 'MERCHANT_EVENT', + source: 'warning source', + version: '1.0.0', + value: 'warning message', + sessionId: '', + merchantId: '', + paymentId: '', + appId: '', + platform: 'web', + userAgent: '', + eventName: 'PAYMENT_ATTEMPT', + browserName: 'Edge', + browserVersion: '120.0', + latency: '', + firstEvent: false, + paymentMethod: '', + }; + + const result = logFileToObj(logFile); + expect(result.log_type).toBe('WARNING'); + expect(result.category).toBe('MERCHANT_EVENT'); + expect(result.browser_name).toBe('EDGE'); + }); + + it('should handle SILENT log type', () => { + const logFile = { + timestamp: '1234567890', + logType: 'SILENT', + category: 'API', + source: 'silent source', + version: '1.0.0', + value: '', + sessionId: '', + merchantId: '', + paymentId: '', + appId: '', + platform: 'web', + userAgent: '', + eventName: 'NETWORK_STATE', + browserName: 'Others', + browserVersion: '0', + latency: '', + firstEvent: false, + paymentMethod: '', + }; + + const result = logFileToObj(logFile); + expect(result.log_type).toBe('SILENT'); + }); + + it('should convert firstEvent boolean to string', () => { + const logFile = { + timestamp: '', + logType: 'INFO', + category: 'API', + source: '', + version: '', + value: '', + sessionId: '', + merchantId: '', + paymentId: '', + appId: '', + platform: '', + userAgent: '', + eventName: '', + browserName: '', + browserVersion: '', + latency: '', + firstEvent: true, + paymentMethod: '', + }; + + const result = logFileToObj(logFile); + expect(result.first_event).toBe('true'); + + const logFile2 = { ...logFile, firstEvent: false }; + const result2 = logFileToObj(logFile2); + expect(result2.first_event).toBe('false'); + }); + }); + + describe('getSourceString', () => { + it('should return hyper_loader for Loader source', () => { + expect(getSourceString('Loader')).toBe('hyper_loader'); + }); + + it('should return headless for Headless source', () => { + expect(getSourceString('Headless')).toBe('headless'); + }); + + it('should return hyper + payment mode for payment mode variant', () => { + const source = { _0: 'card' }; + expect(getSourceString(source)).toBe('hypercard'); + }); + + it('should return hyper + snake_case payment mode for payment mode variant', () => { + const source = { _0: 'paymentMethodCollect' }; + expect(getSourceString(source)).toBe('hyperpayment_method_collect'); + }); + + it('should handle googlePay payment mode', () => { + const source = { _0: 'googlePay' }; + expect(getSourceString(source)).toBe('hypergoogle_pay'); + }); + + it('should handle applePay payment mode', () => { + const source = { _0: 'applePay' }; + expect(getSourceString(source)).toBe('hyperapple_pay'); + }); + + it('should handle payment mode with multiple words', () => { + const source = { _0: 'paymentMethodsManagement' }; + expect(getSourceString(source)).toBe('hyperpayment_methods_management'); + }); + }); + + describe('getRefFromOption', () => { + it('should return ref with contents set to value when value is provided', () => { + const result = getRefFromOption('test_value'); + expect(result).toHaveProperty('contents'); + expect(result.contents).toBe('test_value'); + }); + + it('should return ref with empty string when value is undefined', () => { + const result = getRefFromOption(undefined); + expect(result).toHaveProperty('contents'); + expect(result.contents).toBe(''); + }); + + it('should return ref with null when value is null', () => { + const result = getRefFromOption(null); + expect(result).toHaveProperty('contents'); + expect(result.contents).toBe(null); + }); + + it('should handle numeric string values', () => { + const result = getRefFromOption('12345'); + expect(result.contents).toBe('12345'); + }); + + it('should handle empty string value', () => { + const result = getRefFromOption(''); + expect(result.contents).toBe(''); + }); + + it('should return a mutable ref object', () => { + const result = getRefFromOption('initial'); + result.contents = 'modified'; + expect(result.contents).toBe('modified'); + }); + }); + + describe('make', () => { + const originalWindow = globalThis.window; + const originalNavigator = globalThis.navigator; + const originalAddEventListener = window.addEventListener; + + beforeEach(() => { + Object.defineProperty(globalThis, 'navigator', { + value: { + onLine: true, + connection: { + effectiveType: '4g', + downlink: 10, + rtt: 50, + }, + sendBeacon: jest.fn(() => true), + platform: 'MacIntel', + userAgent: 'Mozilla/5.0', + }, + writable: true, + configurable: true, + }); + Object.defineProperty(globalThis, 'window', { + value: { + navigator: globalThis.navigator, + addEventListener: jest.fn(), + removeEventListener: jest.fn(), + }, + writable: true, + configurable: true, + }); + (globalThis as any).loggingLevel = 'INFO'; + (globalThis as any).enableLogging = true; + (globalThis as any).maxLogsPushedPerEventName = 10; + (globalThis as any).logEndpoint = 'https://test.example.com/logs'; + (globalThis as any).repoVersion = '1.0.0'; + }); + + afterEach(() => { + Object.defineProperty(globalThis, 'window', { + value: originalWindow, + writable: true, + configurable: true, + }); + Object.defineProperty(globalThis, 'navigator', { + value: originalNavigator, + writable: true, + configurable: true, + }); + delete (globalThis as any).loggingLevel; + delete (globalThis as any).enableLogging; + delete (globalThis as any).maxLogsPushedPerEventName; + delete (globalThis as any).logEndpoint; + delete (globalThis as any).repoVersion; + }); + + it('should return logger object with all required methods', () => { + const logger = make('session123', 'Loader', 'clientSecret123', 'merchant123', null); + expect(logger).toHaveProperty('setLogInfo'); + expect(logger).toHaveProperty('setLogError'); + expect(logger).toHaveProperty('setLogApi'); + expect(logger).toHaveProperty('setLogInitiated'); + expect(logger).toHaveProperty('setConfirmPaymentValue'); + expect(logger).toHaveProperty('sendLogs'); + expect(logger).toHaveProperty('setSessionId'); + expect(logger).toHaveProperty('setClientSecret'); + expect(logger).toHaveProperty('setMerchantId'); + expect(logger).toHaveProperty('setMetadata'); + expect(logger).toHaveProperty('setSource'); + }); + + it('should register beforeunload event listener', () => { + make('session123', 'Loader', 'clientSecret123', 'merchant123', null); + expect(window.addEventListener).toHaveBeenCalledWith('beforeunload', expect.any(Function)); + }); + + describe('setConfirmPaymentValue', () => { + it('should return object with method and type', () => { + const logger = make('session123', 'Loader', 'clientSecret123', 'merchant123', null); + const result = logger.setConfirmPaymentValue('card'); + expect(result.method).toBe('confirmPayment'); + expect(result.type).toBe('card'); + }); + + it('should handle different payment types', () => { + const logger = make('session123', 'Loader', 'clientSecret123', 'merchant123', null); + expect(logger.setConfirmPaymentValue('wallet').type).toBe('wallet'); + expect(logger.setConfirmPaymentValue('bank_transfer').type).toBe('bank_transfer'); + }); + + it('should handle empty payment type', () => { + const logger = make('session123', 'Loader', 'clientSecret123', 'merchant123', null); + const result = logger.setConfirmPaymentValue(''); + expect(result.type).toBe(''); + }); + }); + + describe('setSessionId', () => { + it('should update session ID', () => { + const logger = make('initialSession', 'Loader', 'clientSecret123', 'merchant123', null); + logger.setSessionId('newSessionId'); + }); + }); + + describe('setClientSecret', () => { + it('should update client secret', () => { + const logger = make('session123', 'Loader', 'initialSecret', 'merchant123', null); + logger.setClientSecret('newClientSecret'); + }); + }); + + describe('setMerchantId', () => { + it('should update merchant ID', () => { + const logger = make('session123', 'Loader', 'clientSecret123', 'initialMerchant', null); + logger.setMerchantId('newMerchantId'); + }); + }); + + describe('setMetadata', () => { + it('should update metadata', () => { + const logger = make('session123', 'Loader', 'clientSecret123', 'merchant123', null); + logger.setMetadata({ key: 'value' }); + }); + }); + + describe('setSource', () => { + it('should update source string', () => { + const logger = make('session123', 'Loader', 'clientSecret123', 'merchant123', null); + logger.setSource('new_source'); + }); + }); + }); +}); diff --git a/src/__tests__/WebLoggerUtils.test.ts b/src/__tests__/WebLoggerUtils.test.ts new file mode 100644 index 000000000..df37f1246 --- /dev/null +++ b/src/__tests__/WebLoggerUtils.test.ts @@ -0,0 +1,130 @@ +import { + eventNameToStrMapper, + getPaymentId, + convertToScreamingSnakeCase, + toSnakeCaseWithSeparator, + defaultLoggerConfig, + apiEventInitMapper, +} from '../Utilities/LoggerUtils.bs.js'; + +describe('WebLoggerUtils', () => { + describe('eventNameToStrMapper', () => { + it('should return the event name as string', () => { + expect(eventNameToStrMapper('TEST_EVENT')).toBe('TEST_EVENT'); + }); + + it('should handle empty string', () => { + expect(eventNameToStrMapper('')).toBe(''); + }); + + it('should handle any string', () => { + expect(eventNameToStrMapper('any_event_name')).toBe('any_event_name'); + }); + }); + + describe('getPaymentId', () => { + it('should extract payment ID from client secret', () => { + expect(getPaymentId('pay_abc123_secret_xyz')).toBe('pay_abc123'); + }); + + it('should handle client secret with multiple _secret_ occurrences', () => { + expect(getPaymentId('pay_test_secret_value')).toBe('pay_test'); + }); + + it('should handle invalid format', () => { + expect(getPaymentId('invalid')).toBe('invalid'); + }); + + it('should handle empty string', () => { + expect(getPaymentId('')).toBe(''); + }); + }); + + describe('convertToScreamingSnakeCase', () => { + it('should convert to SCREAMING_SNAKE_CASE', () => { + expect(convertToScreamingSnakeCase('hello world')).toBe('HELLO_WORLD'); + }); + + it('should handle already uppercase', () => { + expect(convertToScreamingSnakeCase('HELLO WORLD')).toBe('HELLO_WORLD'); + }); + + it('should handle empty string', () => { + expect(convertToScreamingSnakeCase('')).toBe(''); + }); + + it('should handle single word', () => { + expect(convertToScreamingSnakeCase('hello')).toBe('HELLO'); + }); + }); + + describe('toSnakeCaseWithSeparator', () => { + it('should convert to snake_case with underscore separator', () => { + expect(toSnakeCaseWithSeparator('helloWorld', '_')).toBe('hello_world'); + }); + + it('should convert to kebab-style with dash separator', () => { + expect(toSnakeCaseWithSeparator('helloWorld', '-')).toBe('hello-world'); + }); + + it('should handle empty string', () => { + expect(toSnakeCaseWithSeparator('', '_')).toBe(''); + }); + + it('should handle already snake_case', () => { + expect(toSnakeCaseWithSeparator('hello_world', '_')).toBe('hello_world'); + }); + }); + + describe('defaultLoggerConfig', () => { + it('should be defined', () => { + expect(defaultLoggerConfig).toBeDefined(); + }); + + it('should have setLogInfo method', () => { + expect(defaultLoggerConfig.setLogInfo).toBeDefined(); + expect(typeof defaultLoggerConfig.setLogInfo).toBe('function'); + }); + + it('should have setLogError method', () => { + expect(defaultLoggerConfig.setLogError).toBeDefined(); + expect(typeof defaultLoggerConfig.setLogError).toBe('function'); + }); + + it('should have setLogApi method', () => { + expect(defaultLoggerConfig.setLogApi).toBeDefined(); + expect(typeof defaultLoggerConfig.setLogApi).toBe('function'); + }); + + it('should have sendLogs method', () => { + expect(defaultLoggerConfig.sendLogs).toBeDefined(); + expect(typeof defaultLoggerConfig.sendLogs).toBe('function'); + }); + }); + + describe('apiEventInitMapper', () => { + it('should map RETRIEVE_CALL to RETRIEVE_CALL_INIT', () => { + expect(apiEventInitMapper('RETRIEVE_CALL')).toBe('RETRIEVE_CALL_INIT'); + }); + + it('should map AUTHENTICATION_CALL to AUTHENTICATION_CALL_INIT', () => { + expect(apiEventInitMapper('AUTHENTICATION_CALL')).toBe('AUTHENTICATION_CALL_INIT'); + }); + + it('should map CONFIRM_CALL to CONFIRM_CALL_INIT', () => { + expect(apiEventInitMapper('CONFIRM_CALL')).toBe('CONFIRM_CALL_INIT'); + }); + + it('should map SESSIONS_CALL to SESSIONS_CALL_INIT', () => { + expect(apiEventInitMapper('SESSIONS_CALL')).toBe('SESSIONS_CALL_INIT'); + }); + + it('should map PAYMENT_METHODS_CALL to PAYMENT_METHODS_CALL_INIT', () => { + expect(apiEventInitMapper('PAYMENT_METHODS_CALL')).toBe('PAYMENT_METHODS_CALL_INIT'); + }); + + it('should return undefined for unknown event', () => { + expect(apiEventInitMapper('UNKNOWN_EVENT')).toBeUndefined(); + }); + }); +}); diff --git a/src/__tests__/WebPaymentUtils.test.ts b/src/__tests__/WebPaymentUtils.test.ts new file mode 100644 index 000000000..ced77da24 --- /dev/null +++ b/src/__tests__/WebPaymentUtils.test.ts @@ -0,0 +1,214 @@ +import { + getMethod, + getMethodType, + getExperience, + getPaymentExperienceType, + getPaymentMethodName, + isAppendingCustomerAcceptance, + appendedCustomerAcceptance, + getIsKlarnaSDKFlow, + sortCustomerMethodsBasedOnPriority, + getSupportedCardBrands, + checkIsCardSupported, +} from '../Utilities/PaymentUtils.bs.js'; + +describe('WebPaymentUtils', () => { + describe('getMethod', () => { + it('should return card for Cards', () => { + expect(getMethod({ TAG: 'Cards', _0: 'Credit' })).toBe('card'); + }); + + it('should return wallet for Wallets', () => { + expect(getMethod({ TAG: 'Wallets', _0: { TAG: 'Gpay', _0: 'Redirect' } })).toBe('wallet'); + }); + + it('should return bank_redirect for Banks', () => { + expect(getMethod({ TAG: 'Banks', _0: 'Sofort' })).toBe('bank_redirect'); + }); + + it('should return bank_debit for BankDebit', () => { + expect(getMethod({ TAG: 'BankDebit', _0: 'ACH' })).toBe('bank_debit'); + }); + + it('should return bank_transfer for BankTransfer', () => { + expect(getMethod({ TAG: 'BankTransfer', _0: 'ACH' })).toBe('bank_transfer'); + }); + + it('should return pay_later for PayLater', () => { + expect(getMethod({ TAG: 'PayLater', _0: { TAG: 'Klarna', _0: 'Redirect' } })).toBe('pay_later'); + }); + }); + + describe('getMethodType', () => { + it('should return card for Cards', () => { + expect(getMethodType({ TAG: 'Cards', _0: 'Credit' })).toBe('card'); + }); + + it('should return google_pay for Gpay', () => { + expect(getMethodType({ TAG: 'Wallets', _0: { TAG: 'Gpay', _0: 'Redirect' } })).toBe('google_pay'); + }); + + it('should return apple_pay for ApplePay', () => { + expect(getMethodType({ TAG: 'Wallets', _0: { TAG: 'ApplePay', _0: 'Redirect' } })).toBe('apple_pay'); + }); + + it('should return paypal for Paypal', () => { + expect(getMethodType({ TAG: 'Wallets', _0: { TAG: 'Paypal', _0: 'Redirect' } })).toBe('paypal'); + }); + + it('should return sofort for Sofort bank redirect', () => { + expect(getMethodType({ TAG: 'Banks', _0: 'Sofort' })).toBe('sofort'); + }); + + it('should return ach for ACH bank debit', () => { + expect(getMethodType({ TAG: 'BankDebit', _0: 'ACH' })).toBe('ach'); + }); + + it('should return sepa for Sepa bank transfer', () => { + expect(getMethodType({ TAG: 'BankTransfer', _0: 'Sepa' })).toBe('sepa'); + }); + }); + + describe('getExperience', () => { + it('should return redirect_to_url for Redirect', () => { + expect(getExperience('Redirect')).toBe('redirect_to_url'); + }); + + it('should return invoke_sdk_client for InvokeSDK', () => { + expect(getExperience('InvokeSDK')).toBe('invoke_sdk_client'); + }); + }); + + describe('getPaymentExperienceType', () => { + it('should return invoke_sdk_client for InvokeSDK', () => { + expect(getPaymentExperienceType('InvokeSDK')).toBe('invoke_sdk_client'); + }); + + it('should return redirect_to_url for RedirectToURL', () => { + expect(getPaymentExperienceType('RedirectToURL')).toBe('redirect_to_url'); + }); + + it('should return display_qr_code for QrFlow', () => { + expect(getPaymentExperienceType('QrFlow')).toBe('display_qr_code'); + }); + }); + + describe('getPaymentMethodName', () => { + it('should remove _debit suffix for bank_debit', () => { + expect(getPaymentMethodName('bank_debit', 'ach_debit')).toBe('ach'); + }); + + it('should remove _transfer suffix for non-listed bank_transfer', () => { + expect(getPaymentMethodName('bank_transfer', 'ach_transfer')).toBe('ach'); + }); + + it('should return unchanged for other payment methods', () => { + expect(getPaymentMethodName('card', 'credit')).toBe('credit'); + }); + }); + + describe('isAppendingCustomerAcceptance', () => { + it('should return true for NEW_MANDATE with non-guest', () => { + expect(isAppendingCustomerAcceptance(false, 'NEW_MANDATE')).toBe(true); + }); + + it('should return true for SETUP_MANDATE with non-guest', () => { + expect(isAppendingCustomerAcceptance(false, 'SETUP_MANDATE')).toBe(true); + }); + + it('should return false for guest customer', () => { + expect(isAppendingCustomerAcceptance(true, 'NEW_MANDATE')).toBe(false); + }); + + it('should return false for non-mandate payment type', () => { + expect(isAppendingCustomerAcceptance(false, 'NORMAL')).toBe(false); + }); + }); + + describe('appendedCustomerAcceptance', () => { + it('should append customer acceptance when required', () => { + const body = [['payment_method', 'card']]; + const result = appendedCustomerAcceptance(false, 'NEW_MANDATE', body); + const caEntry = result.find((entry: any) => entry[0] === 'customer_acceptance'); + expect(caEntry).toBeDefined(); + }); + + it('should not append when not required', () => { + const body = [['payment_method', 'card']]; + const result = appendedCustomerAcceptance(true, 'NORMAL', body); + const caEntry = result.find((entry: any) => entry[0] === 'customer_acceptance'); + expect(caEntry).toBeUndefined(); + }); + }); + + describe('sortCustomerMethodsBasedOnPriority', () => { + it('should sort by priority array', () => { + const methods = [ + { paymentMethod: 'card', paymentMethodType: undefined, defaultPaymentMethodSet: false }, + { paymentMethod: 'wallet', paymentMethodType: 'paypal', defaultPaymentMethodSet: false }, + ]; + const priority = ['paypal', 'card']; + const result = sortCustomerMethodsBasedOnPriority(methods, priority); + expect(result[0].paymentMethodType).toBe('paypal'); + }); + + it('should return original array for empty priority', () => { + const methods = [ + { paymentMethod: 'card', defaultPaymentMethodSet: false }, + ]; + const result = sortCustomerMethodsBasedOnPriority(methods, []); + expect(result).toEqual(methods); + }); + + it('should handle empty methods array', () => { + expect(sortCustomerMethodsBasedOnPriority([], ['card'])).toEqual([]); + }); + }); + + describe('getSupportedCardBrands', () => { + it('should return undefined when no card payment method', () => { + const list = { + payment_methods: [], + }; + expect(getSupportedCardBrands(list)).toBeUndefined(); + }); + + it('should return card brands when card payment method exists', () => { + const list = { + payment_methods: [ + { + payment_method: 'card', + payment_method_types: [ + { card_networks: [{ card_network: 'VISA' }, { card_network: 'MASTERCARD' }] }, + ], + }, + ], + }; + const result = getSupportedCardBrands(list); + expect(result).toBeDefined(); + expect(Array.isArray(result)).toBe(true); + }); + }); + + describe('checkIsCardSupported', () => { + it('should return true for valid card with supported brand', () => { + const result = checkIsCardSupported('4111111111111111', 'Visa', ['visa', 'mastercard']); + expect(result).toBe(true); + }); + + it('should return false for card not in supported list', () => { + const result = checkIsCardSupported('4111111111111111', 'Visa', ['amex', 'mastercard']); + expect(result).toBe(false); + }); + + it('should return undefined for empty card brand', () => { + const result = checkIsCardSupported('1234', '', undefined); + expect(result).toBe(false); + }); + + it('should return true when supportedCardBrands is undefined', () => { + const result = checkIsCardSupported('4111111111111111', 'Visa', undefined); + expect(result).toBe(true); + }); + }); +}); diff --git a/src/__tests__/WebSessionsType.test.ts b/src/__tests__/WebSessionsType.test.ts new file mode 100644 index 000000000..17d7aa807 --- /dev/null +++ b/src/__tests__/WebSessionsType.test.ts @@ -0,0 +1,304 @@ +import { + defaultToken, + getWallet, + getSessionsToken, + getSessionsTokenJson, + itemToObjMapper, + getWalletFromTokenType, + getPaymentSessionObj, +} from '../Types/SessionsType.bs.js'; + +describe('SessionsType', () => { + describe('defaultToken', () => { + it('should have correct default values', () => { + expect(defaultToken.walletName).toBe('NONE'); + expect(defaultToken.token).toBe(''); + expect(defaultToken.sessionId).toBe(''); + expect(defaultToken.allowed_payment_methods).toEqual([]); + expect(defaultToken.transaction_info).toEqual({}); + expect(defaultToken.merchant_info).toEqual({}); + expect(defaultToken.shippingAddressRequired).toBe(false); + expect(defaultToken.emailRequired).toBe(false); + expect(defaultToken.shippingAddressParameters).toEqual({}); + expect(defaultToken.orderDetails).toEqual({}); + expect(defaultToken.connector).toBe(''); + expect(defaultToken.clientId).toBe(''); + expect(defaultToken.clientName).toBe(''); + expect(defaultToken.clientProfileId).toBe(''); + expect(defaultToken.email_address).toBe(''); + expect(defaultToken.transaction_amount).toBe(''); + expect(defaultToken.transaction_currency_code).toBe(''); + }); + }); + + describe('getWallet', () => { + it('should return "ApplePay" for "apple_pay"', () => { + expect(getWallet('apple_pay')).toBe('ApplePay'); + }); + + it('should return "ClickToPay" for "click_to_pay"', () => { + expect(getWallet('click_to_pay')).toBe('ClickToPay'); + }); + + it('should return "Gpay" for "google_pay"', () => { + expect(getWallet('google_pay')).toBe('Gpay'); + }); + + it('should return "Klarna" for "klarna"', () => { + expect(getWallet('klarna')).toBe('Klarna'); + }); + + it('should return "Paypal" for "paypal"', () => { + expect(getWallet('paypal')).toBe('Paypal'); + }); + + it('should return "Paze" for "paze"', () => { + expect(getWallet('paze')).toBe('Paze'); + }); + + it('should return "SamsungPay" for "samsung_pay"', () => { + expect(getWallet('samsung_pay')).toBe('SamsungPay'); + }); + + it('should return "NONE" for unknown wallet name', () => { + expect(getWallet('unknown')).toBe('NONE'); + }); + + it('should return "NONE" for empty string', () => { + expect(getWallet('')).toBe('NONE'); + }); + }); + + describe('getSessionsToken', () => { + it('should parse session tokens from dict', () => { + const dict = { + session_token: [ + { + wallet_name: 'google_pay', + session_token: 'token123', + session_id: 'session456', + allowed_payment_methods: ['CARD'], + transaction_info: { amount: '100' }, + merchant_info: { name: 'Test' }, + shipping_address_required: true, + email_required: false, + shipping_address_parameters: { allowedCountries: ['US'] }, + order_details: { orderId: '123' }, + connector: 'stripe', + client_id: 'client123', + client_name: 'Test Client', + client_profile_id: 'profile123', + email_address: 'test@example.com', + transaction_amount: '100.00', + transaction_currency_code: 'USD', + }, + ], + }; + const result = getSessionsToken(dict, 'session_token'); + expect(result).toHaveLength(1); + expect(result[0].walletName).toBe('Gpay'); + expect(result[0].token).toBe('token123'); + expect(result[0].sessionId).toBe('session456'); + expect(result[0].shippingAddressRequired).toBe(true); + expect(result[0].emailRequired).toBe(false); + expect(result[0].connector).toBe('stripe'); + }); + + it('should return defaultToken array when key not found', () => { + const dict = {}; + const result = getSessionsToken(dict, 'nonexistent'); + expect(result).toEqual([defaultToken]); + }); + + it('should return defaultToken array when value is null', () => { + const dict = { session_token: null }; + const result = getSessionsToken(dict, 'session_token'); + expect(result).toEqual([defaultToken]); + }); + + it('should handle empty array', () => { + const dict = { session_token: [] }; + const result = getSessionsToken(dict, 'session_token'); + expect(result).toEqual([]); + }); + + it('should handle multiple session tokens', () => { + const dict = { + session_token: [ + { wallet_name: 'google_pay', session_token: 'gpay_token' }, + { wallet_name: 'apple_pay', session_token: 'apple_token' }, + ], + }; + const result = getSessionsToken(dict, 'session_token'); + expect(result).toHaveLength(2); + expect(result[0].walletName).toBe('Gpay'); + expect(result[1].walletName).toBe('ApplePay'); + }); + }); + + describe('getSessionsTokenJson', () => { + it('should return JSON array from dict', () => { + const dict = { + session_token: [{ wallet_name: 'google_pay' }, { wallet_name: 'apple_pay' }], + }; + const result = getSessionsTokenJson(dict, 'session_token'); + expect(result).toHaveLength(2); + }); + + it('should return empty array when key not found', () => { + const dict = {}; + const result = getSessionsTokenJson(dict, 'nonexistent'); + expect(result).toEqual([]); + }); + + it('should return empty array when value is null', () => { + const dict = { session_token: null }; + const result = getSessionsTokenJson(dict, 'session_token'); + expect(result).toEqual([]); + }); + }); + + describe('itemToObjMapper', () => { + it('should map to ApplePayObject', () => { + const dict = { + payment_id: 'pay_123', + client_secret: 'secret_456', + session_token: [{ wallet_name: 'apple_pay' }], + }; + const result = itemToObjMapper(dict, 'ApplePayObject'); + expect(result.paymentId).toBe('pay_123'); + expect(result.clientSecret).toBe('secret_456'); + expect(result.sessionsToken.TAG).toBe('ApplePayToken'); + }); + + it('should map to GooglePayThirdPartyObject', () => { + const dict = { + payment_id: 'pay_123', + client_secret: 'secret_456', + session_token: [{ wallet_name: 'google_pay' }], + }; + const result = itemToObjMapper(dict, 'GooglePayThirdPartyObject'); + expect(result.paymentId).toBe('pay_123'); + expect(result.sessionsToken.TAG).toBe('GooglePayThirdPartyToken'); + }); + + it('should map to SamsungPayObject', () => { + const dict = { + payment_id: 'pay_123', + client_secret: 'secret_456', + session_token: [{ wallet_name: 'samsung_pay' }], + }; + const result = itemToObjMapper(dict, 'SamsungPayObject'); + expect(result.sessionsToken.TAG).toBe('SamsungPayToken'); + }); + + it('should map to PazeObject', () => { + const dict = { + payment_id: 'pay_123', + client_secret: 'secret_456', + session_token: [{ wallet_name: 'paze' }], + }; + const result = itemToObjMapper(dict, 'PazeObject'); + expect(result.sessionsToken.TAG).toBe('PazeToken'); + }); + + it('should map to ClickToPayObject', () => { + const dict = { + payment_id: 'pay_123', + client_secret: 'secret_456', + session_token: [{ wallet_name: 'click_to_pay' }], + }; + const result = itemToObjMapper(dict, 'ClickToPayObject'); + expect(result.sessionsToken.TAG).toBe('ClickToPayToken'); + }); + + it('should map to Others', () => { + const dict = { + payment_id: 'pay_123', + client_secret: 'secret_456', + session_token: [{ wallet_name: 'klarna' }], + }; + const result = itemToObjMapper(dict, 'Others'); + expect(result.sessionsToken.TAG).toBe('OtherToken'); + }); + + it('should handle empty dict with defaults', () => { + const result = itemToObjMapper({}, 'ApplePayObject'); + expect(result.paymentId).toBe(''); + expect(result.clientSecret).toBe(''); + }); + }); + + describe('getWalletFromTokenType', () => { + it('should find wallet in token array', () => { + const arr = [ + { wallet_name: 'google_pay' }, + { wallet_name: 'apple_pay' }, + { wallet_name: 'samsung_pay' }, + ]; + const result = getWalletFromTokenType(arr, 'Gpay'); + expect(result).toBeDefined(); + }); + + it('should return undefined when wallet not found', () => { + const arr = [{ wallet_name: 'google_pay' }]; + const result = getWalletFromTokenType(arr, 'ApplePay'); + expect(result).toBeUndefined(); + }); + + it('should handle empty array', () => { + const result = getWalletFromTokenType([], 'Gpay'); + expect(result).toBeUndefined(); + }); + }); + + describe('getPaymentSessionObj', () => { + it('should return ApplePayTokenOptional for ApplePayToken', () => { + const tokenType = { TAG: 'ApplePayToken', _0: [{ wallet_name: 'apple_pay' }] }; + const result = getPaymentSessionObj(tokenType, 'ApplePay'); + expect(result.TAG).toBe('ApplePayTokenOptional'); + }); + + it('should return GooglePayThirdPartyTokenOptional for GooglePayThirdPartyToken', () => { + const tokenType = { TAG: 'GooglePayThirdPartyToken', _0: [{ wallet_name: 'google_pay' }] }; + const result = getPaymentSessionObj(tokenType, 'Gpay'); + expect(result.TAG).toBe('GooglePayThirdPartyTokenOptional'); + }); + + it('should return PazeTokenOptional for PazeToken', () => { + const tokenType = { TAG: 'PazeToken', _0: [{ wallet_name: 'paze' }] }; + const result = getPaymentSessionObj(tokenType, 'Paze'); + expect(result.TAG).toBe('PazeTokenOptional'); + }); + + it('should return SamsungPayTokenOptional for SamsungPayToken', () => { + const tokenType = { TAG: 'SamsungPayToken', _0: [{ wallet_name: 'samsung_pay' }] }; + const result = getPaymentSessionObj(tokenType, 'SamsungPay'); + expect(result.TAG).toBe('SamsungPayTokenOptional'); + }); + + it('should return ClickToPayTokenOptional for ClickToPayToken', () => { + const tokenType = { TAG: 'ClickToPayToken', _0: [{ wallet_name: 'click_to_pay' }] }; + const result = getPaymentSessionObj(tokenType, 'ClickToPay'); + expect(result.TAG).toBe('ClickToPayTokenOptional'); + }); + + it('should return OtherTokenOptional for OtherToken', () => { + const tokenType = { + TAG: 'OtherToken', + _0: [{ walletName: 'Klarna', token: 'token123' }], + }; + const result = getPaymentSessionObj(tokenType, 'Klarna'); + expect(result.TAG).toBe('OtherTokenOptional'); + }); + + it('should return undefined _0 when wallet not found in OtherToken', () => { + const tokenType = { + TAG: 'OtherToken', + _0: [{ walletName: 'Klarna', token: 'token123' }], + }; + const result = getPaymentSessionObj(tokenType, 'Paypal'); + expect(result._0).toBeUndefined(); + }); + }); +}); diff --git a/src/__tests__/WebUtils.test.ts b/src/__tests__/WebUtils.test.ts new file mode 100644 index 000000000..14290f357 --- /dev/null +++ b/src/__tests__/WebUtils.test.ts @@ -0,0 +1,1617 @@ +import { + getOptionString, + getString, + getInt, + getFloat, + getBool, + getArray, + toKebabCase, + toCamelCase, + toSnakeCase, + transformKeys, + removeDuplicate, + isVpaIdValid, + checkEmailValid, + sortBasedOnPriority, + onlyDigits, + snakeToTitleCase, + formatIBAN, + formatBSB, + deepCopyDict, + flattenObject, + unflattenObject, + generateRandomString, + getPaymentId, + checkIs18OrAbove, + getFirstAndLastNameFromFullName, + minorUnitToString, + formatAmountWithTwoDecimals, + isValidHexColor, + safeParseOpt, + safeParse, + getJsonFromArrayOfJson, + convertDictToArrayOfKeyStringTuples, + mergeHeadersIntoDict, + getFloatFromString, + getFloatFromJson, + getJsonBoolValue, + getJsonStringFromDict, + getJsonArrayFromDict, + getJsonFromDict, + getJsonObjFromDict, + getRequiredString, + getWarningString, + getDictFromObj, + getJsonObjectFromDict, + getOptionBool, + getDictFromJson, + getDictFromDict, + getNonEmptyOption, + getOptionsDict, + getBoolWithWarning, + getNumberWithWarning, + getOptionalArrayFromDict, + getArrayOfObjectsFromDict, + getStrArray, + getOptionalStrArray, + getBoolValue, + mergeJsons, + toCamelCaseWithNumberSupport, + transformKeysWithoutModifyingValue, + isAllValid, + getCountryPostal, + getCountryNames, + getBankNames, + getBankKeys, + getArrofJsonString, + getOptionalArr, + checkPriorityList, + addSize, + toInt, + validateRountingNumber, + getDictIsSome, + rgbaTorgb, + findVersion, + browserDetect, + formatException, + arrayJsonToCamelCase, + getArrayValFromJsonDict, + isOtherElements, + canHaveMultipleInstances, + callbackFuncForExtractingValFromDict, + getClasses, + getStringFromOptionalJson, + getBoolFromOptionalJson, + getBoolFromJson, + getOptionalJson, + setNested, + mergeTwoFlattenedJsonDicts, + flattenObjectWithStringifiedJson, + flatten, + getWalletPaymentMethod, + getIsExpressCheckoutComponent, + getIsComponentTypeForPaymentElementCreate, + checkIsWalletElement, + getUniqueArray, + removeHyphen, + compareLogic, + toSpacedUpperCase, + handleFailureResponse, + isKeyPresentInDict, + isDigitLimitExceeded, + convertKeyValueToJsonStringPair, + validateName, + validateNickname, + setNickNameState, + getStringFromBool, + maskStringValuesInJson, + getSdkAuthorizationData, +} from '../Utilities/Utils.bs.js'; + +describe('WebUtils', () => { + describe('getString', () => { + it('should return value when key exists', () => { + const dict = { name: 'John' }; + expect(getString(dict, 'name', 'default')).toBe('John'); + }); + + it('should return default when key does not exist', () => { + const dict = { name: 'John' }; + expect(getString(dict, 'age', 'default')).toBe('default'); + }); + }); + + describe('getOptionString', () => { + it('should return value when key exists', () => { + const dict = { name: 'John' }; + expect(getOptionString(dict, 'name')).toBe('John'); + }); + + it('should return undefined when key does not exist', () => { + const dict = { name: 'John' }; + expect(getOptionString(dict, 'age')).toBeUndefined(); + }); + }); + + describe('getInt', () => { + it('should return integer value when key exists', () => { + const dict = { age: 25 }; + expect(getInt(dict, 'age', 0)).toBe(25); + }); + + it('should return default when key does not exist', () => { + const dict = { name: 'John' }; + expect(getInt(dict, 'age', 0)).toBe(0); + }); + }); + + describe('getFloat', () => { + it('should return float value when key exists', () => { + const dict = { price: 99.99 }; + expect(getFloat(dict, 'price', 0)).toBe(99.99); + }); + + it('should return default when key does not exist', () => { + const dict = { name: 'John' }; + expect(getFloat(dict, 'price', 0)).toBe(0); + }); + }); + + describe('getBool', () => { + it('should return boolean value when key exists', () => { + const dict = { active: true }; + expect(getBool(dict, 'active', false)).toBe(true); + }); + + it('should return default when key does not exist', () => { + const dict = { name: 'John' }; + expect(getBool(dict, 'active', false)).toBe(false); + }); + }); + + describe('getArray', () => { + it('should return array when key exists', () => { + const dict = { items: [1, 2, 3] }; + expect(getArray(dict, 'items')).toEqual([1, 2, 3]); + }); + + it('should return empty array when key does not exist', () => { + const dict = { name: 'John' }; + expect(getArray(dict, 'items')).toEqual([]); + }); + }); + + describe('toKebabCase', () => { + it('should convert camelCase to kebab-case', () => { + expect(toKebabCase('helloWorld')).toBe('hello-world'); + }); + + it('should handle already kebab-case', () => { + expect(toKebabCase('hello-world')).toBe('hello--world'); + }); + + it('should handle empty string', () => { + expect(toKebabCase('')).toBe(''); + }); + + it('should handle PascalCase', () => { + expect(toKebabCase('HelloWorld')).toBe('hello-world'); + }); + }); + + describe('toCamelCase', () => { + it('should convert snake_case to camelCase', () => { + expect(toCamelCase('hello_world')).toBe('helloWorld'); + }); + + it('should handle already camelCase', () => { + expect(toCamelCase('helloWorld')).toBe('helloworld'); + }); + + it('should handle empty string', () => { + expect(toCamelCase('')).toBe(''); + }); + }); + + describe('toSnakeCase', () => { + it('should convert camelCase to snake_case', () => { + expect(toSnakeCase('helloWorld')).toBe('hello_world'); + }); + + it('should handle already snake_case', () => { + expect(toSnakeCase('hello_world')).toBe('hello_world'); + }); + + it('should handle empty string', () => { + expect(toSnakeCase('')).toBe(''); + }); + }); + + describe('transformKeys', () => { + it('should transform keys to snake_case', () => { + const obj = { helloWorld: 'value' }; + const result = transformKeys(obj, 'SnakeCase'); + expect(result).toHaveProperty('hello_world'); + }); + + it('should transform keys to camelCase', () => { + const obj = { hello_world: 'value' }; + const result = transformKeys(obj, 'CamelCase'); + expect(result).toHaveProperty('helloWorld'); + }); + }); + + describe('removeDuplicate', () => { + it('should remove duplicate items', () => { + expect(removeDuplicate(['a', 'b', 'a', 'c'])).toEqual(['a', 'b', 'c']); + }); + + it('should handle array without duplicates', () => { + expect(removeDuplicate(['a', 'b', 'c'])).toEqual(['a', 'b', 'c']); + }); + + it('should handle empty array', () => { + expect(removeDuplicate([])).toEqual([]); + }); + }); + + describe('isVpaIdValid', () => { + it('should return true for valid VPA ID', () => { + expect(isVpaIdValid('user@bank')).toBe(true); + }); + + it('should return false for invalid VPA ID', () => { + expect(isVpaIdValid('nope')).toBe(false); + }); + + it('should return undefined for empty string', () => { + expect(isVpaIdValid('')).toBeUndefined(); + }); + }); + + describe('checkEmailValid', () => { + it('should update state for valid email', () => { + const state = { isValid: undefined, value: '' }; + const emailObj = { value: 'test@example.com' }; + const updateFn = jest.fn((updater) => updater(state)); + + checkEmailValid(emailObj, updateFn); + + expect(updateFn).toHaveBeenCalled(); + }); + }); + + describe('sortBasedOnPriority', () => { + it('should sort array based on priority', () => { + const arr = ['c', 'a', 'b']; + const priority = ['a', 'b']; + expect(sortBasedOnPriority(arr, priority)).toEqual(['a', 'b', 'c']); + }); + + it('should handle empty priority array', () => { + const arr = ['c', 'a', 'b']; + expect(sortBasedOnPriority(arr, [])).toEqual(['c', 'a', 'b']); + }); + + it('should handle empty array', () => { + expect(sortBasedOnPriority([], ['a', 'b'])).toEqual([]); + }); + }); + + describe('onlyDigits', () => { + it('should extract only digits from string', () => { + expect(onlyDigits('abc123def456')).toBe('123456'); + }); + + it('should return same string if only digits', () => { + expect(onlyDigits('12345')).toBe('12345'); + }); + + it('should return empty string for no digits', () => { + expect(onlyDigits('abcdef')).toBe(''); + }); + + it('should handle empty string', () => { + expect(onlyDigits('')).toBe(''); + }); + }); + + describe('snakeToTitleCase', () => { + it('should convert snake_case to Title Case', () => { + expect(snakeToTitleCase('hello_world')).toBe('Hello World'); + }); + + it('should handle empty string', () => { + expect(snakeToTitleCase('')).toBe(''); + }); + + it('should handle single word', () => { + expect(snakeToTitleCase('hello')).toBe('Hello'); + }); + }); + + describe('formatIBAN', () => { + it('should format IBAN with spaces every 4 chars', () => { + const result = formatIBAN('DE89370400440532013000'); + expect(result).toContain('DE89'); + }); + + it('should handle short IBAN', () => { + const result = formatIBAN('DE'); + expect(result).toBe('DE'); + }); + + it('should handle empty string', () => { + expect(formatIBAN('')).toBe(''); + }); + + // Edge case: non-alphanumeric characters are stripped + it('should strip non-alphanumeric characters', () => { + const result = formatIBAN('DE89-3704.0044!0532@013#000'); + expect(result).toContain('DE89'); + // After stripping, should be same as clean input + expect(result).toBe(formatIBAN('DE89370400440532013000')); + }); + + // Edge case: very long input + it('should handle very long input', () => { + const longInput = 'GB' + '12' + 'A'.repeat(100); + const result = formatIBAN(longInput); + expect(result.startsWith('GB12')).toBe(true); + // Should contain spaces separating groups of 4 + expect(result).toContain(' '); + }); + }); + + describe('formatBSB', () => { + it('should format 6-digit BSB with dash', () => { + expect(formatBSB('123456')).toBe('123-456'); + }); + + it('should return first 3 digits for short BSB', () => { + expect(formatBSB('123')).toBe('123'); + }); + + it('should handle empty string', () => { + expect(formatBSB('')).toBe(''); + }); + + // Edge case: non-digit characters are stripped + it('should strip non-digit characters', () => { + expect(formatBSB('1a2b3c4d5e6f')).toBe('123-456'); + }); + + // Edge case: more than 6 digits — returns raw formatted string + it('should return raw formatted for more than 6 digits', () => { + const result = formatBSB('1234567'); + expect(result).toBe('1234567'); + }); + }); + + describe('deepCopyDict', () => { + it('should create a new dict with same values', () => { + const original = { a: 1, b: { c: 2 } }; + const copy = deepCopyDict(original); + expect(copy).toEqual(original); + }); + + it('should not be the same reference', () => { + const original = { a: 1 }; + const copy = deepCopyDict(original); + expect(copy).not.toBe(original); + }); + + it('should handle empty dict', () => { + expect(deepCopyDict({})).toEqual({}); + }); + }); + + describe('flattenObject', () => { + it('should flatten nested object', () => { + const obj = { a: { b: 1 } }; + const result = flattenObject(obj, false); + expect(result).toEqual({"a.b": 1}); + }); + + it('should handle flat object', () => { + const obj = { a: 1, b: 2 }; + const result = flattenObject(obj, false); + expect(result).toEqual(obj); + }); + }); + + describe('unflattenObject', () => { + it('should unflatten flat object with dot notation', () => { + const obj = { 'a.b': 1 }; + const result = unflattenObject(obj); + expect(result).toHaveProperty('a'); + }); + }); + + describe('generateRandomString', () => { + it('should return a string', () => { + const result = generateRandomString(10); + expect(typeof result).toBe('string'); + }); + + it('should return different strings on multiple calls', () => { + const result1 = generateRandomString(10); + const result2 = generateRandomString(10); + expect(result1).not.toBe(result2); + }); + + it('should return string of correct length', () => { + const result = generateRandomString(10); + expect(result.length).toBe(10); + }); + }); + + describe('getPaymentId', () => { + it('should extract payment ID from client secret', () => { + expect(getPaymentId('pay_abc123_secret_xyz')).toBe('pay_abc123'); + }); + + it('should handle invalid format', () => { + expect(getPaymentId('invalid')).toBe('invalid'); + }); + + it('should handle empty string', () => { + expect(getPaymentId('')).toBe(''); + }); + }); + + describe('checkIs18OrAbove', () => { + it('should return true for date 18+ years ago', () => { + const date18YearsAgo = new Date(); + date18YearsAgo.setFullYear(date18YearsAgo.getFullYear() - 20); + expect(checkIs18OrAbove(date18YearsAgo)).toBe(true); + }); + + it('should return false for date less than 18 years ago', () => { + const date17YearsAgo = new Date(); + date17YearsAgo.setFullYear(date17YearsAgo.getFullYear() - 17); + expect(checkIs18OrAbove(date17YearsAgo)).toBe(false); + }); + + it('should return false for future date', () => { + const futureDate = new Date(); + futureDate.setFullYear(futureDate.getFullYear() + 1); + expect(checkIs18OrAbove(futureDate)).toBe(false); + }); + + // Edge case: exact boundary — born exactly 18 years ago today + it('should return true for someone born exactly 18 years ago today', () => { + const now = new Date(); + const exactly18 = new Date(now.getFullYear() - 18, now.getMonth(), now.getDate()); + expect(checkIs18OrAbove(exactly18)).toBe(true); + }); + + // Edge case: one day short of 18 — born 17 years and 364 days ago + it('should return false for someone one day short of 18', () => { + const now = new Date(); + const almostEighteen = new Date(now.getFullYear() - 18, now.getMonth(), now.getDate() + 1); + expect(checkIs18OrAbove(almostEighteen)).toBe(false); + }); + + // Edge case: one day past 18 — born 18 years and 1 day ago + it('should return true for someone one day past 18', () => { + const now = new Date(); + const justOver18 = new Date(now.getFullYear() - 18, now.getMonth(), now.getDate() - 1); + expect(checkIs18OrAbove(justOver18)).toBe(true); + }); + }); + + describe('getFirstAndLastNameFromFullName', () => { + it('should split full name into first and last', () => { + const result = getFirstAndLastNameFromFullName('John Doe'); + expect(result[0]).toBe('John'); + expect(result[1]).toBe('Doe'); + }); + + it('should handle single name', () => { + const result = getFirstAndLastNameFromFullName('John'); + expect(result[0]).toBe('John'); + }); + + it('should handle empty string', () => { + const result = getFirstAndLastNameFromFullName(''); + expect(result[0]).toBe(''); + }); + + it('should handle multiple words', () => { + const result = getFirstAndLastNameFromFullName('John Michael Doe'); + expect(result[0]).toBe('John'); + expect(result[1]).toBe('Michael Doe'); + }); + }); + + describe('minorUnitToString', () => { + it('should convert minor unit to string', () => { + expect(minorUnitToString(1000)).toBe('10'); + }); + + it('should handle zero', () => { + expect(minorUnitToString(0)).toBe('0'); + }); + + it('should handle small amounts', () => { + expect(minorUnitToString(1)).toBe('0.01'); + }); + + // Edge case: negative number + it('should handle negative minor units', () => { + expect(minorUnitToString(-500)).toBe('-5'); + }); + + // Edge case: very large number + it('should handle very large minor units', () => { + expect(minorUnitToString(99999999)).toBe('999999.99'); + }); + }); + + describe('formatAmountWithTwoDecimals', () => { + it('should format amount with two decimals', () => { + expect(formatAmountWithTwoDecimals(10.5)).toBe('10.50'); + }); + + it('should handle integer amount', () => { + expect(formatAmountWithTwoDecimals(10)).toBe('10.00'); + }); + + it('should handle amount with more decimals', () => { + expect(formatAmountWithTwoDecimals(10.123)).toBe('10.12'); + }); + }); + + describe('isValidHexColor', () => { + it('should return true for valid 6-digit hex color', () => { + expect(isValidHexColor('#ff0000')).toBe(true); + }); + + it('should return true for valid 3-digit hex color', () => { + expect(isValidHexColor('#fff')).toBe(true); + }); + + it('should return false for non-hex color', () => { + expect(isValidHexColor('red')).toBe(false); + }); + + it('should return false for empty string', () => { + expect(isValidHexColor('')).toBe(false); + }); + + it('should return false for invalid hex format', () => { + expect(isValidHexColor('#gggggg')).toBe(false); + }); + }); + + describe('safeParseOpt', () => { + it('should parse valid JSON string', () => { + const result = safeParseOpt('{"a":1}'); + expect(result).toEqual({ a: 1 }); + }); + + it('should return undefined for invalid JSON', () => { + expect(safeParseOpt('invalid')).toBeUndefined(); + }); + }); + + describe('safeParse', () => { + it('should parse valid JSON string', () => { + const result = safeParse('{"a":1}'); + expect(result).toEqual({ a: 1 }); + }); + + it('should return null for invalid JSON', () => { + expect(safeParse('invalid')).toBeNull(); + }); + }); + + describe('getJsonFromArrayOfJson', () => { + it('should convert array of tuples to object', () => { + const arr = [['key1', 'value1'], ['key2', 'value2']]; + const result = getJsonFromArrayOfJson(arr); + expect(result).toEqual({ key1: 'value1', key2: 'value2' }); + }); + + it('should handle empty array', () => { + expect(getJsonFromArrayOfJson([])).toEqual({}); + }); + + it('should handle single entry', () => { + expect(getJsonFromArrayOfJson([['key', 'value']])).toEqual({ key: 'value' }); + }); + }); + + describe('convertDictToArrayOfKeyStringTuples', () => { + it('should convert dict to array of key-string tuples', () => { + const dict = { name: 'John', age: '25' }; + const result = convertDictToArrayOfKeyStringTuples(dict); + expect(result).toContainEqual(['name', 'John']); + expect(result).toContainEqual(['age', '25']); + }); + + it('should handle empty dict', () => { + expect(convertDictToArrayOfKeyStringTuples({})).toEqual([]); + }); + }); + + describe('mergeHeadersIntoDict', () => { + it('should merge headers array into dict', () => { + const dict: any = { existing: 'value' }; + const headers = [['newKey', 'newValue'], ['another', 'value']]; + mergeHeadersIntoDict(dict, headers); + expect(dict.newKey).toBe('newValue'); + expect(dict.another).toBe('value'); + expect(dict.existing).toBe('value'); + }); + + it('should handle empty headers array', () => { + const dict: any = { existing: 'value' }; + mergeHeadersIntoDict(dict, []); + expect(dict.existing).toBe('value'); + expect(Object.keys(dict).length).toBe(1); + }); + }); + + describe('getFloatFromString', () => { + it('should parse valid float string', () => { + expect(getFloatFromString('3.14', 0)).toBe(3.14); + }); + + it('should return default for invalid string', () => { + expect(getFloatFromString('invalid', 0)).toBe(0); + }); + + it('should handle integer string', () => { + expect(getFloatFromString('42', 0)).toBe(42); + }); + }); + + describe('getFloatFromJson', () => { + it('should extract float from JSON number', () => { + expect(getFloatFromJson(3.14, 0)).toBe(3.14); + }); + + it('should extract float from JSON string', () => { + expect(getFloatFromJson('3.14', 0)).toBe(3.14); + }); + + it('should return default for non-numeric value', () => { + expect(getFloatFromJson('invalid', 0)).toBe(0); + }); + + it('should return default for null', () => { + expect(getFloatFromJson(null, 5.5)).toBe(5.5); + }); + }); + + describe('getJsonBoolValue', () => { + it('should return value when key exists', () => { + expect(getJsonBoolValue({ active: true }, 'active', false)).toBe(true); + }); + + it('should return default when key does not exist', () => { + expect(getJsonBoolValue({}, 'active', false)).toBe(false); + }); + + it('should handle undefined value', () => { + expect(getJsonBoolValue({ active: undefined }, 'active', true)).toBe(true); + }); + }); + + describe('getJsonStringFromDict', () => { + it('should return value when key exists', () => { + expect(getJsonStringFromDict({ name: 'John' }, 'name', 'default')).toBe('John'); + }); + + it('should return default when key does not exist', () => { + expect(getJsonStringFromDict({}, 'name', 'default')).toBe('default'); + }); + }); + + describe('getJsonArrayFromDict', () => { + it('should return array when key exists', () => { + expect(getJsonArrayFromDict({ items: [1, 2, 3] }, 'items', [])).toEqual([1, 2, 3]); + }); + + it('should return default when key does not exist', () => { + expect(getJsonArrayFromDict({}, 'items', [])).toEqual([]); + }); + }); + + describe('getJsonFromDict', () => { + it('should return value when key exists', () => { + expect(getJsonFromDict({ data: { a: 1 } }, 'data', {})).toEqual({ a: 1 }); + }); + + it('should return default when key does not exist', () => { + expect(getJsonFromDict({}, 'data', null)).toBe(null); + }); + }); + + describe('getJsonObjFromDict', () => { + it('should return object when key exists', () => { + expect(getJsonObjFromDict({ data: { a: 1 } }, 'data', {})).toEqual({ a: 1 }); + }); + + it('should return default when key does not exist', () => { + expect(getJsonObjFromDict({}, 'data', {})).toEqual({}); + }); + }); + + describe('getRequiredString', () => { + it('should return value when key exists with non-empty value', () => { + const dict = { name: 'John' }; + expect(getRequiredString(dict, 'name', 'default', undefined)).toBe('John'); + }); + }); + + describe('getWarningString', () => { + it('should return string value when key exists', () => { + expect(getWarningString({ name: 'John' }, 'name', 'default', undefined)).toBe('John'); + }); + + it('should return default when key does not exist', () => { + expect(getWarningString({}, 'name', 'default', undefined)).toBe('default'); + }); + }); + + describe('getDictFromObj', () => { + it('should return dict from JSON object', () => { + expect(getDictFromObj({ data: { a: 1 } }, 'data')).toEqual({ a: 1 }); + }); + + it('should return empty object when key does not exist', () => { + expect(getDictFromObj({}, 'data')).toEqual({}); + }); + }); + + describe('getJsonObjectFromDict', () => { + it('should return object when key exists', () => { + expect(getJsonObjectFromDict({ data: { a: 1 } }, 'data')).toEqual({ a: 1 }); + }); + + it('should return empty object when key does not exist', () => { + expect(getJsonObjectFromDict({}, 'data')).toEqual({}); + }); + }); + + describe('getOptionBool', () => { + it('should return boolean when key exists', () => { + expect(getOptionBool({ active: true }, 'active')).toBe(true); + }); + + it('should return undefined when key does not exist', () => { + expect(getOptionBool({}, 'active')).toBeUndefined(); + }); + }); + + describe('getDictFromJson', () => { + it('should return dict from JSON', () => { + expect(getDictFromJson({ a: 1 })).toEqual({ a: 1 }); + }); + + it('should return empty object for null', () => { + expect(getDictFromJson(null)).toEqual({}); + }); + }); + + describe('getDictFromDict', () => { + it('should return nested dict', () => { + expect(getDictFromDict({ data: { a: 1 } }, 'data')).toEqual({ a: 1 }); + }); + + it('should return empty object when key does not exist', () => { + expect(getDictFromDict({}, 'data')).toEqual({}); + }); + }); + + describe('getNonEmptyOption', () => { + it('should return value for non-empty string', () => { + expect(getNonEmptyOption('value')).toBe('value'); + }); + + it('should return undefined for empty string', () => { + expect(getNonEmptyOption('')).toBeUndefined(); + }); + + it('should return undefined for undefined', () => { + expect(getNonEmptyOption(undefined)).toBeUndefined(); + }); + }); + + describe('getOptionsDict', () => { + it('should return dict from options', () => { + expect(getOptionsDict({ a: 1 })).toEqual({ a: 1 }); + }); + + it('should return empty object for null', () => { + expect(getOptionsDict(null)).toEqual({}); + }); + }); + + describe('getBoolWithWarning', () => { + it('should return boolean value when key exists', () => { + expect(getBoolWithWarning({ active: true }, 'active', false, undefined)).toBe(true); + }); + + it('should return default when key does not exist', () => { + expect(getBoolWithWarning({}, 'active', false, undefined)).toBe(false); + }); + }); + + describe('getNumberWithWarning', () => { + it('should return number value when key exists', () => { + expect(getNumberWithWarning({ count: 5 }, 'count', undefined, 0)).toBe(5); + }); + + it('should return default when key does not exist', () => { + expect(getNumberWithWarning({}, 'count', undefined, 0)).toBe(0); + }); + }); + + describe('getOptionalArrayFromDict', () => { + it('should return array when key exists', () => { + expect(getOptionalArrayFromDict({ items: [1, 2, 3] }, 'items')).toEqual([1, 2, 3]); + }); + + it('should return undefined when key does not exist', () => { + expect(getOptionalArrayFromDict({}, 'items')).toBeUndefined(); + }); + }); + + describe('getArrayOfObjectsFromDict', () => { + it('should return array of objects when key exists', () => { + expect(getArrayOfObjectsFromDict({ items: [{ a: 1 }, { b: 2 }] }, 'items')).toEqual([{ a: 1 }, { b: 2 }]); + }); + + it('should return empty array when key does not exist', () => { + expect(getArrayOfObjectsFromDict({}, 'items')).toEqual([]); + }); + }); + + describe('getStrArray', () => { + it('should return string array when key exists', () => { + expect(getStrArray({ items: ['a', 'b', 'c'] }, 'items')).toEqual(['a', 'b', 'c']); + }); + + it('should return empty array when key does not exist', () => { + expect(getStrArray({}, 'items')).toEqual([]); + }); + }); + + describe('getOptionalStrArray', () => { + it('should return string array when key exists', () => { + expect(getOptionalStrArray({ items: ['a', 'b'] }, 'items')).toEqual(['a', 'b']); + }); + + it('should return undefined when key does not exist', () => { + expect(getOptionalStrArray({}, 'items')).toBeUndefined(); + }); + }); + + describe('getBoolValue', () => { + it('should return boolean value', () => { + expect(getBoolValue(true)).toBe(true); + expect(getBoolValue(false)).toBe(false); + }); + + it('should return false for undefined', () => { + expect(getBoolValue(undefined)).toBe(false); + }); + }); + + describe('mergeJsons', () => { + it('should merge two JSON objects', () => { + const json1 = { a: 1, b: { c: 2 } }; + const json2 = { b: { d: 3 }, e: 4 }; + const result = mergeJsons(json1, json2); + expect(result.a).toBe(1); + expect(result.e).toBe(4); + }); + + it('should handle empty objects', () => { + expect(mergeJsons({}, { a: 1 }).a).toBe(1); + expect(mergeJsons({ a: 1 }, {}).a).toBe(1); + }); + }); + + describe('toCamelCaseWithNumberSupport', () => { + it('should convert snake_case to camelCase preserving numbers', () => { + expect(toCamelCaseWithNumberSupport('hello_world123')).toBe('helloWorld123'); + }); + + it('should handle strings with colons', () => { + expect(toCamelCaseWithNumberSupport('hello:world')).toBe('hello:world'); + }); + + it('should handle empty string', () => { + expect(toCamelCaseWithNumberSupport('')).toBe(''); + }); + }); + + describe('transformKeysWithoutModifyingValue', () => { + it('should transform keys without modifying number values', () => { + const obj = { hello_world: 123 }; + const result = transformKeysWithoutModifyingValue(obj, 'CamelCase'); + expect(result.helloWorld).toBe(123); + }); + + it('should transform nested objects', () => { + const obj = { outer_key: { inner_key: 'value' } }; + const result = transformKeysWithoutModifyingValue(obj, 'CamelCase'); + expect(result.outerKey.innerKey).toBe('value'); + }); + }); + + describe('isAllValid', () => { + it('should return true when all card fields are valid in payment mode', () => { + expect(isAllValid(true, true, true, true, true, 'payment')).toBe(true); + }); + + it('should return false when any field is invalid', () => { + expect(isAllValid(true, true, false, true, true, 'payment')).toBe(false); + }); + + it('should require zip in non-payment mode', () => { + expect(isAllValid(true, true, true, true, true, 'setup')).toBe(true); + expect(isAllValid(true, true, true, true, false, 'setup')).toBe(false); + }); + + it('should return false when all false', () => { + expect(isAllValid(false, false, false, false, false, 'payment')).toBe(false); + }); + }); + + describe('getCountryPostal', () => { + it('should return postal code info for country', () => { + const postalCodes = [{ iso: 'US', format: '12345' }, { iso: 'GB', format: 'A1 1AA' }]; + const result = getCountryPostal('US', postalCodes); + expect(result.iso).toBe('US'); + }); + + it('should return default when country not found', () => { + const result = getCountryPostal('XX', []); + expect(result).toBeDefined(); + }); + }); + + describe('getCountryNames', () => { + it('should extract country names from list', () => { + const list = [{ countryName: 'USA' }, { countryName: 'Canada' }]; + expect(getCountryNames(list)).toEqual(['USA', 'Canada']); + }); + + it('should handle empty list', () => { + expect(getCountryNames([])).toEqual([]); + }); + }); + + describe('getBankNames', () => { + it('should return bank names that exist in allBanks', () => { + const list = [{ value: 'bank1', displayName: 'Bank One' }, { value: 'bank2', displayName: 'Bank Two' }]; + expect(getBankNames(list, ['bank1'])).toEqual(['Bank One']); + }); + + it('should return empty array when no matches', () => { + const list = [{ value: 'bank1', displayName: 'Bank One' }]; + expect(getBankNames(list, ['bank2'])).toEqual([]); + }); + }); + + describe('getBankKeys', () => { + it('should return bank value for matching displayName', () => { + const banks = [{ displayName: 'Bank One', value: 'bank1' }]; + expect(getBankKeys('Bank One', banks, 'default')).toBe('bank1'); + }); + + it('should return undefined when not found', () => { + expect(getBankKeys('Unknown', [], 'default')).toBeUndefined(); + }); + }); + + describe('getArrofJsonString', () => { + it('should return the same array', () => { + expect(getArrofJsonString(['a', 'b', 'c'])).toEqual(['a', 'b', 'c']); + }); + + it('should handle empty array', () => { + expect(getArrofJsonString([])).toEqual([]); + }); + }); + + describe('getOptionalArr', () => { + it('should return array when defined', () => { + expect(getOptionalArr([1, 2, 3])).toEqual([1, 2, 3]); + }); + + it('should return empty array when undefined', () => { + expect(getOptionalArr(undefined)).toEqual([]); + }); + }); + + describe('checkPriorityList', () => { + it('should return true when first item is card', () => { + expect(checkPriorityList(['card', 'bank'])).toBe(true); + }); + + it('should return false when first item is not card', () => { + expect(checkPriorityList(['bank', 'card'])).toBe(false); + }); + + it('should return false for empty array', () => { + expect(checkPriorityList(undefined)).toBe(false); + }); + }); + + describe('addSize', () => { + it('should add to pixel value', () => { + expect(addSize('10px', 5, 'Pixel')).toBe('15px'); + }); + + it('should add to rem value', () => { + expect(addSize('2rem', 1, 'Rem')).toBe('3rem'); + }); + + it('should add to em value', () => { + expect(addSize('1.5em', 0.5, 'Em')).toBe('2em'); + }); + + it('should return original if unit mismatch', () => { + expect(addSize('10px', 5, 'Rem')).toBe('10px'); + }); + }); + + describe('toInt', () => { + it('should convert string to integer', () => { + expect(toInt('42')).toBe(42); + }); + + it('should return 0 for invalid string', () => { + expect(toInt('invalid')).toBe(0); + }); + + it('should handle empty string', () => { + expect(toInt('')).toBe(0); + }); + }); + + describe('validateRountingNumber', () => { + it('should return true for valid routing number', () => { + expect(validateRountingNumber('011000015')).toBe(true); + }); + + it('should return false for invalid routing number', () => { + expect(validateRountingNumber('123456789')).toBe(false); + }); + + it('should return false for wrong length', () => { + expect(validateRountingNumber('12345')).toBe(false); + }); + + it('should return false for empty string', () => { + expect(validateRountingNumber('')).toBe(false); + }); + }); + + describe('getDictIsSome', () => { + it('should return true when key has Some value', () => { + expect(getDictIsSome({ key: 'value' }, 'key')).toBe(true); + }); + + it('should return false when key has undefined', () => { + expect(getDictIsSome({}, 'key')).toBe(false); + }); + }); + + describe('rgbaTorgb', () => { + it('should convert rgba to rgb format', () => { + const result = rgbaTorgb('rgba(255, 0, 0, 0.5)'); + expect(result).toMatch(/rgba\(255,\s*0,\s*0\)/); + }); + + it('should return original if already rgb', () => { + expect(rgbaTorgb('rgb(255, 0, 0)')).toBe('rgb(255, 0, 0)'); + }); + + it('should return original if not rgba/rgb', () => { + expect(rgbaTorgb('#ff0000')).toBe('#ff0000'); + }); + + it('should handle whitespace', () => { + const result = rgbaTorgb(' rgba(255, 0, 0, 0.5) '); + expect(result).toMatch(/rgba\(255,\s*0,\s*0\)/); + }); + }); + + describe('findVersion', () => { + it('should find version in string', () => { + const re = /Chrome\/([\d.]+)/; + const result = findVersion(re, 'Chrome/120.0.0'); + expect(result).toContain('120.0.0'); + }); + + it('should return empty array when no match', () => { + const re = /Firefox\/([\d.]+)/; + expect(findVersion(re, 'Chrome/120.0.0')).toEqual([]); + }); + }); + + describe('browserDetect', () => { + it('should detect Chrome', () => { + const result = browserDetect('Mozilla/5.0 Chrome/120.0.0 Safari/537.36'); + expect(result).toContain('Chrome'); + }); + + it('should detect Firefox', () => { + const result = browserDetect('Mozilla/5.0 Firefox/115.0'); + expect(result).toContain('Firefox'); + }); + + it('should detect Safari', () => { + const result = browserDetect('Mozilla/5.0 Safari/605.1.15'); + expect(result).toContain('Safari'); + }); + + it('should return Others for unknown browser', () => { + const result = browserDetect('Unknown Browser'); + expect(result).toContain('Others'); + }); + }); + + describe('formatException', () => { + it('should format error exception with message', () => { + const error = new Error('Test error'); + const result = formatException(error) as any; + expect(result.message).toBe('Test error'); + }); + + it('should return original for non-Error objects', () => { + const obj = { custom: 'error' }; + expect(formatException(obj)).toBe(obj); + }); + }); + + describe('arrayJsonToCamelCase', () => { + it('should transform keys in array of objects', () => { + const arr = [{ hello_world: 'value' }]; + const result = arrayJsonToCamelCase(arr); + expect(result[0]).toHaveProperty('helloWorld'); + }); + + it('should handle empty array', () => { + expect(arrayJsonToCamelCase([])).toEqual([]); + }); + }); + + describe('getArrayValFromJsonDict', () => { + it('should extract array values from nested dict', () => { + const dict = { + outer: { + inner: ['a', 'b', 'c'] + } + }; + const result = getArrayValFromJsonDict(dict, 'outer', 'inner'); + expect(result).toEqual(['a', 'b', 'c']); + }); + + it('should return empty array when path not found', () => { + expect(getArrayValFromJsonDict({}, 'outer', 'inner')).toEqual([]); + }); + }); + + describe('isOtherElements', () => { + it('should return true for card types', () => { + expect(isOtherElements('card')).toBe(true); + expect(isOtherElements('cardNumber')).toBe(true); + expect(isOtherElements('cardExpiry')).toBe(true); + expect(isOtherElements('cardCvc')).toBe(true); + }); + + it('should return false for other types', () => { + expect(isOtherElements('bank')).toBe(false); + expect(isOtherElements('wallet')).toBe(false); + }); + }); + + describe('canHaveMultipleInstances', () => { + it('should return true for card element types', () => { + expect(canHaveMultipleInstances('cardNumber')).toBe(true); + expect(canHaveMultipleInstances('cardExpiry')).toBe(true); + expect(canHaveMultipleInstances('cardCvc')).toBe(true); + }); + + it('should return false for other types', () => { + expect(canHaveMultipleInstances('card')).toBe(false); + expect(canHaveMultipleInstances('bank')).toBe(false); + }); + }); + + describe('callbackFuncForExtractingValFromDict', () => { + it('should return function that extracts value by key', () => { + const fn = callbackFuncForExtractingValFromDict('name'); + expect(fn({ name: 'John' })).toBe('John'); + }); + + it('should return undefined for missing key', () => { + const fn = callbackFuncForExtractingValFromDict('missing'); + expect(fn({ name: 'John' })).toBeUndefined(); + }); + }); + + describe('getClasses', () => { + it('should extract class from options', () => { + const options = { classes: { base: 'my-class' } }; + expect(getClasses(options, 'base')).toBe('my-class'); + }); + + it('should return empty string when not found', () => { + expect(getClasses({}, 'base')).toBe(''); + }); + }); + + describe('getStringFromOptionalJson', () => { + it('should extract string from optional JSON', () => { + expect(getStringFromOptionalJson('value', 'default')).toBe('value'); + }); + + it('should return default for undefined', () => { + expect(getStringFromOptionalJson(undefined, 'default')).toBe('default'); + }); + }); + + describe('getBoolFromOptionalJson', () => { + it('should extract boolean from optional JSON', () => { + expect(getBoolFromOptionalJson(true, false)).toBe(true); + }); + + it('should return default for undefined', () => { + expect(getBoolFromOptionalJson(undefined, false)).toBe(false); + }); + }); + + describe('getBoolFromJson', () => { + it('should extract boolean from JSON', () => { + expect(getBoolFromJson(true, false)).toBe(true); + }); + + it('should return default for non-boolean', () => { + expect(getBoolFromJson('not a bool', false)).toBe(false); + }); + }); + + describe('getOptionalJson', () => { + it('should extract nested value from JSON', () => { + const json = { data: { key: 'value' } }; + expect(getOptionalJson(json, 'key')).toBe('value'); + }); + + it('should return undefined when path not found', () => { + expect(getOptionalJson({}, 'key')).toBeUndefined(); + }); + }); + + describe('setNested', () => { + it('should set nested value in dict', () => { + const dict: any = {}; + setNested(dict, ['a', 'b', 'c'], 'value'); + expect(dict.a.b.c).toBe('value'); + }); + + it('should set single-level value', () => { + const dict: any = {}; + setNested(dict, ['key'], 'value'); + expect(dict.key).toBe('value'); + }); + }); + + describe('mergeTwoFlattenedJsonDicts', () => { + it('should merge two flattened dicts', () => { + const dict1 = { 'a.b': 1 }; + const dict2 = { 'c.d': 2 }; + const result = mergeTwoFlattenedJsonDicts(dict1, dict2); + expect(result).toHaveProperty('a'); + expect(result).toHaveProperty('c'); + }); + }); + + describe('flattenObjectWithStringifiedJson', () => { + it('should flatten object with stringified JSON values', () => { + const obj = { outer: '{"inner": "value"}' }; + const result = flattenObjectWithStringifiedJson(obj, false, true); + expect(result['outer.inner']).toBe('value'); + }); + + it('should handle non-string values', () => { + const obj = { key: 'plain string' }; + const result = flattenObjectWithStringifiedJson(obj, false, true); + expect(result.key).toBe('plain string'); + }); + }); + + describe('flatten', () => { + it('should flatten nested object with arrays', () => { + const obj = { items: [{ name: 'a' }, { name: 'b' }] }; + const result = flatten(obj, false); + expect(result['items[0].name']).toBe('a'); + expect(result['items[1].name']).toBe('b'); + }); + + it('should handle string arrays', () => { + const obj = { tags: ['a', 'b', 'c'] }; + const result = flatten(obj, false); + expect(result.tags).toEqual(['a', 'b', 'c']); + }); + + it('should handle flat objects', () => { + const obj = { a: 1, b: 'test' }; + const result = flatten(obj, false); + expect(result.a).toBe(1); + expect(result.b).toBe('test'); + }); + }); + + describe('getWalletPaymentMethod', () => { + it('should filter for Google Pay', () => { + expect(getWalletPaymentMethod(['google_pay', 'paypal'], 'GooglePayElement')).toEqual(['google_pay']); + }); + + it('should filter for Apple Pay', () => { + expect(getWalletPaymentMethod(['apple_pay', 'google_pay'], 'ApplePayElement')).toEqual(['apple_pay']); + }); + + it('should filter for PayPal', () => { + expect(getWalletPaymentMethod(['paypal', 'google_pay'], 'PayPalElement')).toEqual(['paypal']); + }); + + it('should return all wallets for unknown type', () => { + expect(getWalletPaymentMethod(['google_pay', 'paypal'], 'Unknown')).toEqual(['google_pay', 'paypal']); + }); + }); + + describe('getIsExpressCheckoutComponent', () => { + it('should return true for express checkout components', () => { + expect(getIsExpressCheckoutComponent('googlePay')).toBe(true); + expect(getIsExpressCheckoutComponent('applePay')).toBe(true); + expect(getIsExpressCheckoutComponent('payPal')).toBe(true); + }); + + it('should return false for non-express components', () => { + expect(getIsExpressCheckoutComponent('card')).toBe(false); + expect(getIsExpressCheckoutComponent('bank')).toBe(false); + }); + }); + + describe('getIsComponentTypeForPaymentElementCreate', () => { + it('should return true for valid component types', () => { + expect(getIsComponentTypeForPaymentElementCreate('payment')).toBe(true); + expect(getIsComponentTypeForPaymentElementCreate('paymentMethodCollect')).toBe(true); + expect(getIsComponentTypeForPaymentElementCreate('googlePay')).toBe(true); + }); + + it('should return false for invalid types', () => { + expect(getIsComponentTypeForPaymentElementCreate('invalid')).toBe(false); + }); + }); + + describe('checkIsWalletElement', () => { + it('should return true for wallet elements', () => { + expect(checkIsWalletElement('GooglePayElement')).toBe(true); + expect(checkIsWalletElement('ApplePayElement')).toBe(true); + expect(checkIsWalletElement('PayPalElement')).toBe(true); + }); + + it('should return false for non-wallet elements', () => { + expect(checkIsWalletElement('card')).toBe(false); + }); + }); + + describe('getUniqueArray', () => { + it('should return array with unique values', () => { + expect(getUniqueArray(['a', 'b', 'a', 'c'])).toEqual(['a', 'b', 'c']); + }); + + it('should handle empty array', () => { + expect(getUniqueArray([])).toEqual([]); + }); + + it('should preserve order', () => { + expect(getUniqueArray(['c', 'a', 'b', 'a'])).toEqual(['c', 'a', 'b']); + }); + }); + + describe('removeHyphen', () => { + it('should remove all hyphens', () => { + expect(removeHyphen('123-456-789')).toBe('123456789'); + }); + + it('should handle string without hyphens', () => { + expect(removeHyphen('123456789')).toBe('123456789'); + }); + + it('should handle empty string', () => { + expect(removeHyphen('')).toBe(''); + }); + }); + + describe('compareLogic', () => { + it('should return 0 for equal values', () => { + expect(compareLogic(5, 5)).toBe(0); + }); + + it('should return -1 when a > b', () => { + expect(compareLogic(10, 5)).toBe(-1); + }); + + it('should return 1 when a < b', () => { + expect(compareLogic(5, 10)).toBe(1); + }); + }); + + describe('toSpacedUpperCase', () => { + it('should convert to uppercase with spaces', () => { + expect(toSpacedUpperCase('hello_world', '_')).toBe('HELLO WORLD'); + }); + + it('should handle empty string', () => { + expect(toSpacedUpperCase('', '_')).toBe(''); + }); + + it('should handle string without delimiter', () => { + expect(toSpacedUpperCase('hello', '_')).toBe('HELLO'); + }); + }); + + describe('handleFailureResponse', () => { + it('should create error response object', () => { + const result = handleFailureResponse('Test message', 'TestError') as any; + expect(result.error.type).toBe('TestError'); + expect(result.error.message).toBe('Test message'); + }); + }); + + describe('isKeyPresentInDict', () => { + it('should return true when key exists', () => { + expect(isKeyPresentInDict({ key: 'value' }, 'key')).toBe(true); + }); + + it('should return false when key missing', () => { + expect(isKeyPresentInDict({}, 'key')).toBe(false); + }); + }); + + describe('isDigitLimitExceeded', () => { + it('should return true when digit limit exceeded', () => { + expect(isDigitLimitExceeded('12345', 4)).toBe(true); + }); + + it('should return false when within limit', () => { + expect(isDigitLimitExceeded('123', 4)).toBe(false); + }); + + it('should return false for no digits', () => { + expect(isDigitLimitExceeded('abc', 1)).toBe(false); + }); + }); + + describe('convertKeyValueToJsonStringPair', () => { + it('should create key-value pair', () => { + expect(convertKeyValueToJsonStringPair('key', 'value')).toEqual(['key', 'value']); + }); + }); + + describe('validateName', () => { + it('should validate non-empty name without digits', () => { + const result = validateName('John Doe', { value: '', errorString: '', isValid: false }, { invalidCardHolderNameError: 'Invalid name' }); + expect(result.isValid).toBe(true); + expect(result.value).toBe('John Doe'); + }); + + it('should invalidate name with digits', () => { + const result = validateName('John123', { value: '', errorString: '', isValid: false }, { invalidCardHolderNameError: 'Invalid name' }); + expect(result.isValid).toBe(false); + expect(result.errorString).toBe('Invalid name'); + }); + + it('should handle empty name', () => { + const result = validateName('', { value: 'prev', errorString: 'prev error', isValid: true }, { invalidCardHolderNameError: 'Invalid name' }); + expect(result.isValid).toBe(false); + expect(result.errorString).toBe('prev error'); + }); + }); + + describe('validateNickname', () => { + it('should validate nickname without too many digits', () => { + const [isValid, error] = validateNickname('Card1', { invalidNickNameError: 'Invalid' }); + expect(isValid).toBe(true); + expect(error).toBe(''); + }); + + it('should invalidate nickname with more than 2 digits', () => { + const [isValid, error] = validateNickname('Card123', { invalidNickNameError: 'Too many digits' }); + expect(isValid).toBe(false); + expect(error).toBe('Too many digits'); + }); + + it('should allow empty nickname', () => { + const [isValid, error] = validateNickname('', { invalidNickNameError: 'Invalid' }); + expect(isValid).toBe(true); + }); + }); + + describe('setNickNameState', () => { + it('should set nickname state correctly', () => { + const result = setNickNameState('Card1', { value: '', errorString: '', isValid: false }, { invalidNickNameError: 'Invalid' }); + expect(result.value).toBe('Card1'); + expect(result.isValid).toBe(true); + }); + }); + + describe('getStringFromBool', () => { + it('should convert true to "true"', () => { + expect(getStringFromBool(true)).toBe('true'); + }); + + it('should convert false to "false"', () => { + expect(getStringFromBool(false)).toBe('false'); + }); + }); + + describe('maskStringValuesInJson', () => { + it('should mask string values at specified paths', () => { + const json = { email: 'test@example.com', name: 'John' }; + const result = maskStringValuesInJson(json, '', 0, (path: string) => path === 'email'); + expect(result.email).toBe('***REDACTED***'); + expect(result.name).toBe('John'); + }); + + it('should handle nested objects', () => { + const json = { user: { email: 'test@example.com' } }; + const result = maskStringValuesInJson(json, '', 0, (path: string) => path.includes('email')); + expect(result.user.email).toBe('***REDACTED***'); + }); + + it('should handle arrays', () => { + const json = { items: ['a', 'b', 'c'] }; + const result = maskStringValuesInJson(json, '', 0, () => true); + expect(result.items).toEqual(['***REDACTED***', '***REDACTED***', '***REDACTED***']); + }); + + it('should handle max depth', () => { + const json = { a: 'test' }; + const result = maskStringValuesInJson(json, '', 11, () => true); + expect(result).toBe('***MAX_DEPTH_REACHED***'); + }); + + it('should mask empty strings', () => { + const json = { empty: '' }; + const result = maskStringValuesInJson(json, '', 0, () => true); + expect(result.empty).toBe('***EMPTY***'); + }); + }); + + describe('getSdkAuthorizationData', () => { + beforeEach(() => { + jest.spyOn(window, 'atob').mockImplementation((str: string) => str); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should parse SDK authorization data', () => { + const result = getSdkAuthorizationData('publishable_key=pk_test,client_secret=cs_test,customer_id=cust_123,profile_id=prof_123'); + expect(result.publishableKey).toBe('pk_test'); + expect(result.clientSecret).toBe('cs_test'); + expect(result.customerId).toBe('cust_123'); + expect(result.profileId).toBe('prof_123'); + }); + + it('should handle missing fields', () => { + const result = getSdkAuthorizationData('publishable_key=pk_test'); + expect(result.publishableKey).toBe('pk_test'); + expect(result.clientSecret).toBeUndefined(); + }); + }); +}); diff --git a/tsconfig.test.json b/tsconfig.test.json new file mode 100644 index 000000000..6b847ce84 --- /dev/null +++ b/tsconfig.test.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "esModuleInterop": true, + "allowJs": true, + "jsx": "react", + "strict": false, + "types": ["jest"], + "moduleResolution": "node" + }, + "include": [ + "src/**/*.test.ts", + "__tests__/**/*.test.ts" + ] +}