From 3613b2e80e541eceed924ad8d3b21a9a743f3291 Mon Sep 17 00:00:00 2001 From: arekkubaczkowski Date: Wed, 29 Jun 2022 15:33:06 +0200 Subject: [PATCH] unit tests (#353) * unit tests * test functions * functions tests * android permissions and b64 tests * ts fix, improve tests * handle clearCachedCredentials exceptional method * useStripeTerminala more unit tests * typo fix --- package.json | 2 + .../__snapshots__/functions.test.ts.snap | 42 + .../__snapshots__/index.test.tsx.snap | 0 src/__tests__/functions.test.ts | 752 ++++++++++++++++++ src/{ => __tests__}/index.test.tsx | 4 +- src/components/StripeTerminalProvider.tsx | 1 - .../__tests__/StripeTerminalProvider.test.tsx | 59 ++ .../StripeTerminalProvider.test.tsx.snap | 7 + .../withStripeTerminal.test.tsx.snap | 25 + .../__tests__/withStripeTerminal.test.tsx | 28 + src/functions.ts | 2 +- .../useStripeTerminal.test.tsx.snap | 48 ++ .../__tests__/useStripeTerminal.test.tsx | 524 ++++++++++++ src/hooks/useStripeTerminal.tsx | 24 +- .../__tests__/androidPermissionsUtils.test.ts | 160 ++++ src/utils/__tests__/b64EncodeDecode.test.ts | 45 ++ yarn.lock | 50 +- 17 files changed, 1750 insertions(+), 23 deletions(-) create mode 100644 src/__tests__/__snapshots__/functions.test.ts.snap rename src/{ => __tests__}/__snapshots__/index.test.tsx.snap (100%) create mode 100644 src/__tests__/functions.test.ts rename src/{ => __tests__}/index.test.tsx (65%) create mode 100644 src/components/__tests__/StripeTerminalProvider.test.tsx create mode 100644 src/components/__tests__/__snapshots__/StripeTerminalProvider.test.tsx.snap create mode 100644 src/components/__tests__/__snapshots__/withStripeTerminal.test.tsx.snap create mode 100644 src/components/__tests__/withStripeTerminal.test.tsx create mode 100644 src/hooks/__tests__/__snapshots__/useStripeTerminal.test.tsx.snap create mode 100644 src/hooks/__tests__/useStripeTerminal.test.tsx create mode 100644 src/utils/__tests__/androidPermissionsUtils.test.ts create mode 100644 src/utils/__tests__/b64EncodeDecode.test.ts diff --git a/package.json b/package.json index 318148fb..0d9e2e5e 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "@expo/config-plugins": "^4.0.18", "@react-native-community/bob": "^0.16.2", "@react-native-community/eslint-config": "^2.0.0", + "@testing-library/react-native": "^10.0.0", "@types/base-64": "^1.0.0", "@types/jest": "^27.0.2", "@types/react": "^16.9.19", @@ -73,6 +74,7 @@ "prettier": "^2.0.5", "react": "17.0.2", "react-native": "0.68.2", + "react-test-renderer": "17.0.2", "stripe": "^7.14.0", "typedoc": "^0.22.15", "typescript": "^4.6.2" diff --git a/src/__tests__/__snapshots__/functions.test.ts.snap b/src/__tests__/__snapshots__/functions.test.ts.snap new file mode 100644 index 00000000..30bd6c96 --- /dev/null +++ b/src/__tests__/__snapshots__/functions.test.ts.snap @@ -0,0 +1,42 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`functions.test.ts Functions snapshot ensure there are no unexpected changes to the functions exports 1`] = ` +Object { + "cancelCollectPaymentMethod": [Function], + "cancelCollectRefundPaymentMethod": [Function], + "cancelCollectSetupIntent": [Function], + "cancelDiscovering": [Function], + "cancelInstallingUpdate": [Function], + "cancelPaymentIntent": [Function], + "cancelReadReusableCard": [Function], + "cancelSetupIntent": [Function], + "clearCachedCredentials": [Function], + "clearReaderDisplay": [Function], + "collectPaymentMethod": [Function], + "collectRefundPaymentMethod": [Function], + "collectSetupIntentPaymentMethod": [Function], + "confirmSetupIntent": [Function], + "connectBluetoothReader": [Function], + "connectEmbeddedReader": [Function], + "connectHandoffReader": [Function], + "connectInternetReader": [Function], + "connectLocalMobileReader": [Function], + "connectUsbReader": [Function], + "createPaymentIntent": [Function], + "createSetupIntent": [Function], + "disconnectReader": [Function], + "discoverReaders": [Function], + "getLocations": [Function], + "initialize": [Function], + "installAvailableUpdate": [Function], + "processPayment": [Function], + "processRefund": [Function], + "readReusableCard": [Function], + "retrievePaymentIntent": [Function], + "retrieveSetupIntent": [Function], + "setConnectionToken": [Function], + "setReaderDisplay": [Function], + "setSimulatedCard": [Function], + "simulateReaderUpdate": [Function], +} +`; diff --git a/src/__snapshots__/index.test.tsx.snap b/src/__tests__/__snapshots__/index.test.tsx.snap similarity index 100% rename from src/__snapshots__/index.test.tsx.snap rename to src/__tests__/__snapshots__/index.test.tsx.snap diff --git a/src/__tests__/functions.test.ts b/src/__tests__/functions.test.ts new file mode 100644 index 00000000..a7c49d07 --- /dev/null +++ b/src/__tests__/functions.test.ts @@ -0,0 +1,752 @@ +jest.mock('../logger', () => ({ + traceSdkMethod: (fn: (...args: any[]) => any | Promise) => { + return function (this: any, ...args: any[]) { + const response = fn.apply(this, args); + return response; + }; + }, +})); + +const mockReader = { + id: 1, + label: '_reader', + batteryLevel: 99, + serialNumber: '_serial', +}; + +const mockPaymentIntent = { + id: 1, + amount: 33, + currency: 'USD', +}; + +const mockSetupIntent = { + id: 2, + status: 'succeeded', +}; + +const mockPaymentMethod = { + id: 3, + customer: '_cus', +}; + +const mockRefund = { + id: 4, + amount: 502, + chargeId: '_chargeId', +}; + +const mockLocations = [ + { + id: 5, + displayName: 'loc_01', + }, + { + id: 6, + displayName: 'loc_02', + }, +]; + +describe('functions.test.ts', () => { + describe('Functions snapshot', () => { + it('ensure there are no unexpected changes to the functions exports', () => { + expect(require('../functions')).toMatchSnapshot(); + }); + }); + + describe('Functions success results', () => { + beforeAll(() => { + jest.resetModules(); + jest.mock('../StripeTerminalSdk', () => ({ + initialize: jest.fn().mockImplementation(() => ({ + reader: mockReader, + })), + setConnectionToken: jest.fn(), + simulateReaderUpdate: jest.fn(), + disconnectReader: jest.fn(), + clearCachedCredentials: jest.fn(), + + discoverReaders: jest.fn().mockImplementation(() => ({})), + cancelDiscovering: jest.fn().mockImplementation(() => ({})), + connectBluetoothReader: jest + .fn() + .mockImplementation(() => ({ reader: mockReader })), + connectHandoffReader: jest + .fn() + .mockImplementation(() => ({ reader: mockReader })), + connectInternetReader: jest + .fn() + .mockImplementation(() => ({ reader: mockReader })), + connectUsbReader: jest + .fn() + .mockImplementation(() => ({ reader: mockReader })), + connectLocalMobileReader: jest + .fn() + .mockImplementation(() => ({ reader: mockReader })), + createPaymentIntent: jest + .fn() + .mockImplementation(() => ({ paymentIntent: mockPaymentIntent })), + collectPaymentMethod: jest + .fn() + .mockImplementation(() => ({ paymentIntent: mockPaymentIntent })), + retrievePaymentIntent: jest + .fn() + .mockImplementation(() => ({ paymentIntent: mockPaymentIntent })), + getLocations: jest.fn().mockImplementation(() => ({ + locations: mockLocations, + hasMore: true, + })), + processPayment: jest + .fn() + .mockImplementation(() => ({ paymentIntent: mockPaymentIntent })), + createSetupIntent: jest + .fn() + .mockImplementation(() => ({ setupIntent: mockSetupIntent })), + cancelPaymentIntent: jest + .fn() + .mockImplementation(() => ({ paymentIntent: mockPaymentIntent })), + installAvailableUpdate: jest.fn().mockImplementation(() => ({})), + cancelInstallingUpdate: jest.fn().mockImplementation(() => ({})), + setReaderDisplay: jest.fn().mockImplementation(() => ({})), + clearReaderDisplay: jest.fn().mockImplementation(() => ({})), + retrieveSetupIntent: jest + .fn() + .mockImplementation(() => ({ setupIntent: mockSetupIntent })), + collectSetupIntentPaymentMethod: jest + .fn() + .mockImplementation(() => ({ setupIntent: mockSetupIntent })), + cancelSetupIntent: jest + .fn() + .mockImplementation(() => ({ setupIntent: mockSetupIntent })), + confirmSetupIntent: jest + .fn() + .mockImplementation(() => ({ setupIntent: mockSetupIntent })), + collectRefundPaymentMethod: jest.fn().mockImplementation(() => ({})), + processRefund: jest + .fn() + .mockImplementation(() => ({ refund: mockRefund })), + readReusableCard: jest + .fn() + .mockImplementation(() => ({ paymentMethod: mockPaymentMethod })), + cancelCollectPaymentMethod: jest.fn().mockImplementation(() => ({})), + cancelCollectRefundPaymentMethod: jest + .fn() + .mockImplementation(() => ({})), + cancelCollectSetupIntent: jest.fn().mockImplementation(() => ({})), + cancelReadReusableCard: jest.fn().mockImplementation(() => ({})), + connectEmbeddedReader: jest + .fn() + .mockImplementation(() => ({ reader: mockReader })), + setSimulatedCard: jest.fn(), + })); + }); + + it('initialize returns a proper value', async () => { + const functions = require('../functions'); + await expect(functions.initialize()).resolves.toEqual({ + reader: mockReader, + }); + }); + + it('setConnectionToken returns a proper value', async () => { + const functions = require('../functions'); + await expect(functions.setConnectionToken()).resolves.toEqual(undefined); + }); + + it('discoverReaders returns a proper value', async () => { + const functions = require('../functions'); + await expect(functions.discoverReaders({} as any)).resolves.toEqual({ + error: undefined, + }); + }); + + it('cancelDiscovering returns a proper value', async () => { + const functions = require('../functions'); + await expect(functions.cancelDiscovering()).resolves.toEqual({ + error: undefined, + }); + }); + + it('connectBluetoothReader returns a proper value', async () => { + const functions = require('../functions'); + await expect( + functions.connectBluetoothReader({} as any) + ).resolves.toEqual({ + error: undefined, + reader: mockReader, + }); + }); + + it('connectHandoffReader returns a proper value', async () => { + const functions = require('../functions'); + await expect(functions.connectHandoffReader({} as any)).resolves.toEqual({ + error: undefined, + reader: mockReader, + }); + }); + + it('connectInternetReader returns a proper value', async () => { + const functions = require('../functions'); + await expect(functions.connectInternetReader({} as any)).resolves.toEqual( + { + error: undefined, + reader: mockReader, + } + ); + }); + + it('connectUsbReader returns a proper value', async () => { + const functions = require('../functions'); + await expect(functions.connectUsbReader({} as any)).resolves.toEqual({ + error: undefined, + reader: mockReader, + }); + }); + + it('createPaymentIntent returns a proper value', async () => { + const functions = require('../functions'); + await expect(functions.createPaymentIntent({} as any)).resolves.toEqual({ + error: undefined, + paymentIntent: mockPaymentIntent, + }); + }); + + it('collectPaymentMethod returns a proper value', async () => { + const functions = require('../functions'); + await expect(functions.collectPaymentMethod({} as any)).resolves.toEqual({ + error: undefined, + paymentIntent: mockPaymentIntent, + }); + }); + + it('retrievePaymentIntent returns a proper value', async () => { + const functions = require('../functions'); + await expect(functions.retrievePaymentIntent({} as any)).resolves.toEqual( + { + error: undefined, + paymentIntent: mockPaymentIntent, + } + ); + }); + + it('getLocations returns a proper value', async () => { + const functions = require('../functions'); + await expect(functions.getLocations({} as any)).resolves.toEqual({ + locations: mockLocations, + hasMore: true, + error: undefined, + }); + }); + + it('processPayment returns a proper value', async () => { + const functions = require('../functions'); + await expect(functions.processPayment({} as any)).resolves.toEqual({ + error: undefined, + paymentIntent: mockPaymentIntent, + }); + }); + + it('createSetupIntent returns a proper value', async () => { + const functions = require('../functions'); + await expect(functions.createSetupIntent({} as any)).resolves.toEqual({ + error: undefined, + setupIntent: mockSetupIntent, + }); + }); + + it('cancelPaymentIntent returns a proper value', async () => { + const functions = require('../functions'); + await expect(functions.cancelPaymentIntent('_id')).resolves.toEqual({ + error: undefined, + paymentIntent: mockPaymentIntent, + }); + }); + + it('installAvailableUpdate returns a proper value', async () => { + const functions = require('../functions'); + await expect(functions.installAvailableUpdate()).resolves.toEqual({}); + }); + + it('cancelInstallingUpdate returns a proper value', async () => { + const functions = require('../functions'); + await expect(functions.cancelInstallingUpdate()).resolves.toEqual({}); + }); + + it('setReaderDisplay returns a proper value', async () => { + const functions = require('../functions'); + await expect(functions.setReaderDisplay({} as any)).resolves.toEqual({ + error: undefined, + }); + }); + + it('clearReaderDisplay returns a proper value', async () => { + const functions = require('../functions'); + await expect(functions.clearReaderDisplay()).resolves.toEqual({ + error: undefined, + }); + }); + + it('retrieveSetupIntent returns a proper value', async () => { + const functions = require('../functions'); + await expect(functions.retrieveSetupIntent('')).resolves.toEqual({ + error: undefined, + setupIntent: mockSetupIntent, + }); + }); + + it('collectSetupIntentPaymentMethod returns a proper value', async () => { + const functions = require('../functions'); + await expect( + functions.collectSetupIntentPaymentMethod({} as any) + ).resolves.toEqual({ + error: undefined, + setupIntent: mockSetupIntent, + }); + }); + + it('cancelSetupIntent returns a proper value', async () => { + const functions = require('../functions'); + await expect(functions.cancelSetupIntent('')).resolves.toEqual({ + error: undefined, + setupIntent: mockSetupIntent, + }); + }); + + it('confirmSetupIntent returns a proper value', async () => { + const functions = require('../functions'); + await expect(functions.confirmSetupIntent('_secret')).resolves.toEqual({ + error: undefined, + setupIntent: mockSetupIntent, + }); + }); + + it('simulateReaderUpdate returns a proper value', async () => { + const functions = require('../functions'); + await expect(functions.simulateReaderUpdate({} as any)).resolves.toEqual({ + error: undefined, + }); + }); + + it('collectRefundPaymentMethod returns a proper value', async () => { + const functions = require('../functions'); + await expect( + functions.collectRefundPaymentMethod({} as any) + ).resolves.toEqual({ + error: undefined, + }); + }); + + it('processRefund returns a proper value', async () => { + const functions = require('../functions'); + await expect(functions.processRefund()).resolves.toEqual({ + error: undefined, + refund: mockRefund, + }); + }); + + it('clearCachedCredentials returns a proper value', async () => { + const functions = require('../functions'); + await expect(functions.clearCachedCredentials()).resolves.toEqual({}); + }); + + it('readReusableCard returns a proper value', async () => { + const functions = require('../functions'); + await expect(functions.readReusableCard({} as any)).resolves.toEqual({ + error: undefined, + paymentMethod: mockPaymentMethod, + }); + }); + + it('cancelCollectPaymentMethod returns a proper value', async () => { + const functions = require('../functions'); + await expect(functions.cancelCollectPaymentMethod()).resolves.toEqual({}); + }); + + it('cancelCollectRefundPaymentMethod returns a proper value', async () => { + const functions = require('../functions'); + await expect( + functions.cancelCollectRefundPaymentMethod() + ).resolves.toEqual({}); + }); + + it('cancelCollectSetupIntent returns a proper value', async () => { + const functions = require('../functions'); + await expect(functions.cancelCollectSetupIntent()).resolves.toEqual({}); + }); + + it('cancelReadReusableCard returns a proper value', async () => { + const functions = require('../functions'); + await expect(functions.cancelReadReusableCard()).resolves.toEqual({}); + }); + + it('connectEmbeddedReader returns a proper value', async () => { + const functions = require('../functions'); + await expect(functions.connectEmbeddedReader({} as any)).resolves.toEqual( + { + error: undefined, + reader: mockReader, + } + ); + }); + + it('connectLocalMobileReader returns a proper value', async () => { + const functions = require('../functions'); + await expect( + functions.connectLocalMobileReader({} as any) + ).resolves.toEqual({ + error: undefined, + reader: mockReader, + }); + }); + + it('setSimulatedCard returns a proper value', async () => { + const functions = require('../functions'); + await expect(functions.setSimulatedCard('_number')).resolves.toEqual({}); + }); + }); + + describe('Functions error results', () => { + beforeAll(() => { + jest.resetModules(); + jest.mock('../StripeTerminalSdk', () => ({ + discoverReaders: jest + .fn() + .mockImplementation(() => ({ error: '_error' })), + cancelDiscovering: jest + .fn() + .mockImplementation(() => ({ error: '_error' })), + connectBluetoothReader: jest + .fn() + .mockImplementation(() => ({ error: '_error' })), + connectHandoffReader: jest + .fn() + .mockImplementation(() => ({ error: '_error' })), + disconnectReader: jest + .fn() + .mockImplementation(() => ({ error: '_error' })), + connectInternetReader: jest + .fn() + .mockImplementation(() => ({ error: '_error' })), + connectUsbReader: jest + .fn() + .mockImplementation(() => ({ error: '_error' })), + connectLocalMobileReader: jest + .fn() + .mockImplementation(() => ({ error: '_error' })), + createPaymentIntent: jest + .fn() + .mockImplementation(() => ({ error: '_error' })), + collectPaymentMethod: jest + .fn() + .mockImplementation(() => ({ error: '_error' })), + retrievePaymentIntent: jest + .fn() + .mockImplementation(() => ({ error: '_error' })), + getLocations: jest.fn().mockImplementation(() => ({ + error: '_error', + })), + processPayment: jest + .fn() + .mockImplementation(() => ({ error: '_error' })), + createSetupIntent: jest + .fn() + .mockImplementation(() => ({ error: '_error' })), + cancelPaymentIntent: jest + .fn() + .mockImplementation(() => ({ error: '_error' })), + + setReaderDisplay: jest + .fn() + .mockImplementation(() => ({ error: '_error' })), + clearReaderDisplay: jest + .fn() + .mockImplementation(() => ({ error: '_error' })), + retrieveSetupIntent: jest + .fn() + .mockImplementation(() => ({ error: '_error' })), + collectSetupIntentPaymentMethod: jest + .fn() + .mockImplementation(() => ({ error: '_error' })), + cancelSetupIntent: jest + .fn() + .mockImplementation(() => ({ error: '_error' })), + confirmSetupIntent: jest + .fn() + .mockImplementation(() => ({ error: '_error' })), + collectRefundPaymentMethod: jest + .fn() + .mockImplementation(() => ({ error: '_error' })), + processRefund: jest + .fn() + .mockImplementation(() => ({ error: '_error' })), + readReusableCard: jest + .fn() + .mockImplementation(() => ({ error: '_error' })), + + cancelCollectSetupIntent: jest + .fn() + .mockImplementation(() => ({ error: '_error' })), + cancelReadReusableCard: jest + .fn() + .mockImplementation(() => ({ error: '_error' })), + connectEmbeddedReader: jest + .fn() + .mockImplementation(() => ({ error: '_error' })), + + simulateReaderUpdate: jest.fn().mockRejectedValue('_error'), + clearCachedCredentials: jest.fn().mockRejectedValue('_error'), + cancelCollectRefundPaymentMethod: jest.fn().mockRejectedValue('_error'), + cancelCollectPaymentMethod: jest.fn().mockRejectedValue('_error'), + setSimulatedCard: jest.fn().mockRejectedValue('_error'), + cancelInstallingUpdate: jest.fn().mockRejectedValue('_error'), + installAvailableUpdate: jest.fn().mockRejectedValue('_error'), + initialize: jest.fn().mockImplementation(() => ({ error: '_error' })), + })); + }); + + it('initialize returns a proper value', async () => { + const functions = require('../functions'); + await expect(functions.initialize()).resolves.toEqual({ + error: '_error', + }); + }); + + it('discoverReaders returns a proper value', async () => { + const functions = require('../functions'); + await expect(functions.discoverReaders({} as any)).resolves.toEqual({ + error: '_error', + }); + }); + + it('cancelDiscovering returns a proper value', async () => { + const functions = require('../functions'); + await expect(functions.cancelDiscovering()).resolves.toEqual({ + error: '_error', + }); + }); + + it('connectBluetoothReader returns a proper value', async () => { + const functions = require('../functions'); + await expect( + functions.connectBluetoothReader({} as any) + ).resolves.toEqual({ + error: '_error', + }); + }); + + it('connectHandoffReader returns a proper value', async () => { + const functions = require('../functions'); + await expect(functions.connectHandoffReader({} as any)).resolves.toEqual({ + error: '_error', + }); + }); + + it('connectInternetReader returns a proper value', async () => { + const functions = require('../functions'); + await expect(functions.connectInternetReader({} as any)).resolves.toEqual( + { + error: '_error', + } + ); + }); + + it('connectUsbReader returns a proper value', async () => { + const functions = require('../functions'); + await expect(functions.connectUsbReader({} as any)).resolves.toEqual({ + error: '_error', + }); + }); + + it('createPaymentIntent returns a proper value', async () => { + const functions = require('../functions'); + await expect(functions.createPaymentIntent({} as any)).resolves.toEqual({ + error: '_error', + }); + }); + + it('collectPaymentMethod returns a proper value', async () => { + const functions = require('../functions'); + await expect(functions.collectPaymentMethod({} as any)).resolves.toEqual({ + error: '_error', + }); + }); + + it('retrievePaymentIntent returns a proper value', async () => { + const functions = require('../functions'); + await expect(functions.retrievePaymentIntent({} as any)).resolves.toEqual( + { + error: '_error', + } + ); + }); + + it('getLocations returns a proper value', async () => { + const functions = require('../functions'); + await expect(functions.getLocations({} as any)).resolves.toEqual({ + locations: undefined, + hasMore: undefined, + error: '_error', + }); + }); + + it('processPayment returns a proper value', async () => { + const functions = require('../functions'); + await expect(functions.processPayment({} as any)).resolves.toEqual({ + error: '_error', + }); + }); + + it('createSetupIntent returns a proper value', async () => { + const functions = require('../functions'); + await expect(functions.createSetupIntent({} as any)).resolves.toEqual({ + error: '_error', + }); + }); + + it('cancelPaymentIntent returns a proper value', async () => { + const functions = require('../functions'); + await expect(functions.cancelPaymentIntent('_id')).resolves.toEqual({ + error: '_error', + }); + }); + + it('setReaderDisplay returns a proper value', async () => { + const functions = require('../functions'); + await expect(functions.setReaderDisplay({} as any)).resolves.toEqual({ + error: '_error', + }); + }); + + it('clearReaderDisplay returns a proper value', async () => { + const functions = require('../functions'); + await expect(functions.clearReaderDisplay()).resolves.toEqual({ + error: '_error', + }); + }); + + it('retrieveSetupIntent returns a proper value', async () => { + const functions = require('../functions'); + await expect(functions.retrieveSetupIntent('')).resolves.toEqual({ + error: '_error', + setupIntent: undefined, + }); + }); + + it('collectSetupIntentPaymentMethod returns a proper value', async () => { + const functions = require('../functions'); + await expect( + functions.collectSetupIntentPaymentMethod({} as any) + ).resolves.toEqual({ + error: '_error', + setupIntent: undefined, + }); + }); + + it('cancelSetupIntent returns a proper value', async () => { + const functions = require('../functions'); + await expect(functions.cancelSetupIntent('')).resolves.toEqual({ + error: '_error', + setupIntent: undefined, + }); + }); + + it('confirmSetupIntent returns a proper value', async () => { + const functions = require('../functions'); + await expect(functions.confirmSetupIntent('_secret')).resolves.toEqual({ + error: '_error', + setupIntent: undefined, + }); + }); + + it('collectRefundPaymentMethod returns a proper value', async () => { + const functions = require('../functions'); + await expect( + functions.collectRefundPaymentMethod({} as any) + ).resolves.toEqual({ + error: '_error', + }); + }); + + it('processRefund returns a proper value', async () => { + const functions = require('../functions'); + await expect(functions.processRefund()).resolves.toEqual({ + error: '_error', + refund: undefined, + }); + }); + + it('readReusableCard returns a proper value', async () => { + const functions = require('../functions'); + await expect(functions.readReusableCard({} as any)).resolves.toEqual({ + error: '_error', + paymentMethod: undefined, + }); + }); + + it('connectEmbeddedReader returns a proper value', async () => { + const functions = require('../functions'); + await expect(functions.connectEmbeddedReader({} as any)).resolves.toEqual( + { + error: '_error', + reader: undefined, + } + ); + }); + + it('connectLocalMobileReader returns a proper value', async () => { + const functions = require('../functions'); + await expect( + functions.connectLocalMobileReader({} as any) + ).resolves.toEqual({ + error: '_error', + reader: undefined, + }); + }); + + it('simulateReaderUpdate returns a proper value', async () => { + const functions = require('../functions'); + await expect(functions.simulateReaderUpdate({} as any)).resolves.toEqual({ + error: '_error', + }); + }); + + it('clearCachedCredentials returns a proper value', async () => { + const functions = require('../functions'); + await expect(functions.clearCachedCredentials()).resolves.toEqual({ + error: '_error', + }); + }); + + it('cancelCollectRefundPaymentMethod returns a proper value', async () => { + const functions = require('../functions'); + await expect( + functions.cancelCollectRefundPaymentMethod() + ).resolves.toEqual({ error: '_error' }); + }); + + it('cancelInstallingUpdate returns a proper value', async () => { + const functions = require('../functions'); + await expect(functions.cancelInstallingUpdate()).resolves.toEqual({ + error: '_error', + }); + }); + + it('setSimulatedCard returns a proper value', async () => { + const functions = require('../functions'); + await expect(functions.setSimulatedCard('_number')).resolves.toEqual({ + error: '_error', + }); + }); + + it('installAvailableUpdate returns a proper value', async () => { + const functions = require('../functions'); + await expect(functions.installAvailableUpdate()).resolves.toEqual({ + error: '_error', + }); + }); + }); +}); + +// workaround to avoiding producing d.ts files that introduces types conflicts +// https://backbencher.dev/articles/typescript-solved-cannot-redeclare-block-scoped-variable-name +export {}; diff --git a/src/index.test.tsx b/src/__tests__/index.test.tsx similarity index 65% rename from src/index.test.tsx rename to src/__tests__/index.test.tsx index c46391ec..b1c086dd 100644 --- a/src/index.test.tsx +++ b/src/__tests__/index.test.tsx @@ -1,7 +1,7 @@ -import * as StripeTerminal from './index'; +import * as StripeTerminal from '../index'; jest.mock( - '../node_modules/react-native/Libraries/EventEmitter/NativeEventEmitter' + '../../node_modules/react-native/Libraries/EventEmitter/NativeEventEmitter' ); describe('index.ts', () => { diff --git a/src/components/StripeTerminalProvider.tsx b/src/components/StripeTerminalProvider.tsx index aee848f2..d78ae320 100644 --- a/src/components/StripeTerminalProvider.tsx +++ b/src/components/StripeTerminalProvider.tsx @@ -11,7 +11,6 @@ import { StripeTerminalContext } from './StripeTerminalContext'; import { initialize, setConnectionToken } from '../functions'; import { useListener } from '../hooks/useListener'; import { NativeModules } from 'react-native'; -// @ts-ignore import EventEmitter from 'react-native/Libraries/vendor/emitter/EventEmitter'; const { diff --git a/src/components/__tests__/StripeTerminalProvider.test.tsx b/src/components/__tests__/StripeTerminalProvider.test.tsx new file mode 100644 index 00000000..67f431e0 --- /dev/null +++ b/src/components/__tests__/StripeTerminalProvider.test.tsx @@ -0,0 +1,59 @@ +import * as React from 'react'; +import { StripeTerminalProvider } from '../StripeTerminalProvider'; +import { fireEvent, render } from '@testing-library/react-native'; +import { Text, TouchableOpacity } from 'react-native'; +import { useStripeTerminal } from '../../hooks/useStripeTerminal'; + +jest.mock( + '../../../node_modules/react-native/Libraries/EventEmitter/NativeEventEmitter' +); + +describe('StripeTerminalProvider.tsx', () => { + it('renders children correctly', () => { + const tokenProvider = jest.fn(); + const { toJSON, findByText } = render( + + test text + + ); + const childText = findByText('test text'); + expect(toJSON()).toMatchSnapshot(); + expect(childText).toBeTruthy(); + }); + + it('ensure if tokenProvider is not called on render', () => { + const tokenProvider = jest.fn(); + render( + } + /> + ); + expect(tokenProvider).not.toBeCalled(); + }); + + it('trigger tokenProvider on init', () => { + const tokenProvider = jest.fn().mockReturnValue('_token'); + + const ChildImpl = () => { + const { initialize } = useStripeTerminal(); + + return ( + initialize()}> + init + + ); + }; + + const { getByText } = render( + + + + ); + + fireEvent.press(getByText('init')); + expect(tokenProvider).toBeCalled(); + expect(tokenProvider).toReturnWith('_token'); + }); +}); diff --git a/src/components/__tests__/__snapshots__/StripeTerminalProvider.test.tsx.snap b/src/components/__tests__/__snapshots__/StripeTerminalProvider.test.tsx.snap new file mode 100644 index 00000000..c3cd34bf --- /dev/null +++ b/src/components/__tests__/__snapshots__/StripeTerminalProvider.test.tsx.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`StripeTerminalProvider.tsx renders children correctly 1`] = ` + + test text + +`; diff --git a/src/components/__tests__/__snapshots__/withStripeTerminal.test.tsx.snap b/src/components/__tests__/__snapshots__/withStripeTerminal.test.tsx.snap new file mode 100644 index 00000000..76ad15fa --- /dev/null +++ b/src/components/__tests__/__snapshots__/withStripeTerminal.test.tsx.snap @@ -0,0 +1,25 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`withStripeTerminal.tsx renders correctly 1`] = ` + + + test + + +`; diff --git a/src/components/__tests__/withStripeTerminal.test.tsx b/src/components/__tests__/withStripeTerminal.test.tsx new file mode 100644 index 00000000..43783fb2 --- /dev/null +++ b/src/components/__tests__/withStripeTerminal.test.tsx @@ -0,0 +1,28 @@ +import * as React from 'react'; +import { withStripeTerminal } from '../withStripeTerminal'; +import { render } from '@testing-library/react-native'; +import { Text, TouchableOpacity } from 'react-native'; + +jest.mock( + '../../../node_modules/react-native/Libraries/EventEmitter/NativeEventEmitter' +); + +describe('withStripeTerminal.tsx', () => { + it('renders correctly', () => { + const MockComponent = ({}) => { + return ( + + test + + ); + }; + + const WithStripeTerminalComponent = withStripeTerminal(MockComponent); + + const { toJSON, findByText } = render(); + + const childText = findByText('test'); + expect(childText).toBeTruthy(); + expect(toJSON()).toMatchSnapshot(); + }); +}); diff --git a/src/functions.ts b/src/functions.ts index 096a8d74..83bf0dc0 100644 --- a/src/functions.ts +++ b/src/functions.ts @@ -404,7 +404,7 @@ export async function getLocations( } return { locations: locations!, - hasMore: hasMore!, + hasMore: hasMore || false, error: undefined, }; } catch (error) { diff --git a/src/hooks/__tests__/__snapshots__/useStripeTerminal.test.tsx.snap b/src/hooks/__tests__/__snapshots__/useStripeTerminal.test.tsx.snap new file mode 100644 index 00000000..072186a0 --- /dev/null +++ b/src/hooks/__tests__/__snapshots__/useStripeTerminal.test.tsx.snap @@ -0,0 +1,48 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`useStripeTerminal.test.tsx Public API snapshot ensure there are no unexpected changes to the hook exports 1`] = ` +Object { + "current": Object { + "cancelCollectPaymentMethod": [Function], + "cancelCollectRefundPaymentMethod": [Function], + "cancelCollectSetupIntent": [Function], + "cancelDiscovering": [Function], + "cancelInstallingUpdate": [Function], + "cancelPaymentIntent": [Function], + "cancelReadReusableCard": [Function], + "cancelSetupIntent": [Function], + "clearCachedCredentials": [Function], + "clearReaderDisplay": [Function], + "collectPaymentMethod": [Function], + "collectRefundPaymentMethod": [Function], + "collectSetupIntentPaymentMethod": [Function], + "confirmSetupIntent": [Function], + "connectBluetoothReader": [Function], + "connectEmbeddedReader": [Function], + "connectHandoffReader": [Function], + "connectInternetReader": [Function], + "connectLocalMobileReader": [Function], + "connectUsbReader": [Function], + "connectedReader": null, + "createPaymentIntent": [Function], + "createSetupIntent": [Function], + "disconnectReader": [Function], + "discoverReaders": [Function], + "discoveredReaders": Array [], + "emitter": undefined, + "getLocations": [Function], + "initialize": [Function], + "installAvailableUpdate": [Function], + "isInitialized": false, + "loading": false, + "processPayment": [Function], + "processRefund": [Function], + "readReusableCard": [Function], + "retrievePaymentIntent": [Function], + "retrieveSetupIntent": [Function], + "setReaderDisplay": [Function], + "setSimulatedCard": [Function], + "simulateReaderUpdate": [Function], + }, +} +`; diff --git a/src/hooks/__tests__/useStripeTerminal.test.tsx b/src/hooks/__tests__/useStripeTerminal.test.tsx new file mode 100644 index 00000000..a6212fdb --- /dev/null +++ b/src/hooks/__tests__/useStripeTerminal.test.tsx @@ -0,0 +1,524 @@ +import * as React from 'react'; +import { useStripeTerminal } from '../useStripeTerminal'; +import { act, renderHook } from '@testing-library/react-native'; +import { StripeTerminalContext } from '../../components/StripeTerminalContext'; +import * as functions from '../../functions'; + +jest.mock( + '../../../node_modules/react-native/Libraries/EventEmitter/NativeEventEmitter' +); + +function spyAllFunctions({ returnWith = null }: { returnWith?: any } = {}) { + const createSetupIntent = jest.fn(() => returnWith); + jest + .spyOn(functions, 'createSetupIntent') + .mockImplementation(createSetupIntent); + // + const connectBluetoothReader = jest.fn(() => returnWith); + jest + .spyOn(functions, 'connectBluetoothReader') + .mockImplementation(connectBluetoothReader); + // + const discoverReaders = jest.fn(() => returnWith); + jest.spyOn(functions, 'discoverReaders').mockImplementation(discoverReaders); + // + const cancelDiscovering = jest.fn(() => returnWith); + jest + .spyOn(functions, 'cancelDiscovering') + .mockImplementation(cancelDiscovering); + // + const connectInternetReader = jest.fn(() => returnWith); + jest + .spyOn(functions, 'connectInternetReader') + .mockImplementation(connectInternetReader); + // + const connectUsbReader = jest.fn(() => returnWith); + jest + .spyOn(functions, 'connectUsbReader') + .mockImplementation(connectUsbReader); + // + const createPaymentIntent = jest.fn(() => returnWith); + jest + .spyOn(functions, 'createPaymentIntent') + .mockImplementation(createPaymentIntent); + // + const collectPaymentMethod = jest.fn(() => returnWith); + jest + .spyOn(functions, 'collectPaymentMethod') + .mockImplementation(collectPaymentMethod); + // + const retrievePaymentIntent = jest.fn(() => returnWith); + jest + .spyOn(functions, 'retrievePaymentIntent') + .mockImplementation(retrievePaymentIntent); + // + const getLocations = jest.fn(() => returnWith); + jest.spyOn(functions, 'getLocations').mockImplementation(getLocations); + // + const processPayment = jest.fn(() => returnWith); + jest.spyOn(functions, 'processPayment').mockImplementation(processPayment); + // + const cancelPaymentIntent = jest.fn(() => returnWith); + jest + .spyOn(functions, 'cancelPaymentIntent') + .mockImplementation(cancelPaymentIntent); + // + const disconnectReader = jest.fn(() => returnWith); + jest + .spyOn(functions, 'disconnectReader') + .mockImplementation(disconnectReader); + // + + const installAvailableUpdate = jest.fn(() => returnWith); + jest + .spyOn(functions, 'installAvailableUpdate') + .mockImplementation(installAvailableUpdate); + // + const cancelInstallingUpdate = jest.fn(() => returnWith); + jest + .spyOn(functions, 'cancelInstallingUpdate') + .mockImplementation(cancelInstallingUpdate); + // + const setReaderDisplay = jest.fn(() => returnWith); + jest + .spyOn(functions, 'setReaderDisplay') + .mockImplementation(setReaderDisplay); + + // + const clearReaderDisplay = jest.fn(() => returnWith); + jest + .spyOn(functions, 'clearReaderDisplay') + .mockImplementation(clearReaderDisplay); + + // + const retrieveSetupIntent = jest.fn(() => returnWith); + jest + .spyOn(functions, 'retrieveSetupIntent') + .mockImplementation(retrieveSetupIntent); + + // + const collectSetupIntentPaymentMethod = jest.fn(() => returnWith); + jest + .spyOn(functions, 'collectSetupIntentPaymentMethod') + .mockImplementation(collectSetupIntentPaymentMethod); + + // + const cancelSetupIntent = jest.fn(() => returnWith); + jest + .spyOn(functions, 'cancelSetupIntent') + .mockImplementation(cancelSetupIntent); + + // + const confirmSetupIntent = jest.fn(() => returnWith); + jest + .spyOn(functions, 'confirmSetupIntent') + .mockImplementation(confirmSetupIntent); + + // + const simulateReaderUpdate = jest.fn(() => returnWith); + jest + .spyOn(functions, 'simulateReaderUpdate') + .mockImplementation(simulateReaderUpdate); + + // + const collectRefundPaymentMethod = jest.fn(() => returnWith); + jest + .spyOn(functions, 'collectRefundPaymentMethod') + .mockImplementation(collectRefundPaymentMethod); + + // + const processRefund = jest.fn(() => returnWith); + jest.spyOn(functions, 'processRefund').mockImplementation(processRefund); + + // + const readReusableCard = jest.fn(() => returnWith); + jest + .spyOn(functions, 'readReusableCard') + .mockImplementation(readReusableCard); + + // + const cancelCollectPaymentMethod = jest.fn(() => returnWith); + jest + .spyOn(functions, 'cancelCollectPaymentMethod') + .mockImplementation(cancelCollectPaymentMethod); + + // + const cancelCollectRefundPaymentMethod = jest.fn(() => returnWith); + jest + .spyOn(functions, 'cancelCollectRefundPaymentMethod') + .mockImplementation(cancelCollectRefundPaymentMethod); + + // + const cancelCollectSetupIntent = jest.fn(() => returnWith); + jest + .spyOn(functions, 'cancelCollectSetupIntent') + .mockImplementation(cancelCollectSetupIntent); + + // + const cancelReadReusableCard = jest.fn(() => returnWith); + jest + .spyOn(functions, 'cancelReadReusableCard') + .mockImplementation(cancelReadReusableCard); + + // + const connectEmbeddedReader = jest.fn(() => returnWith); + jest + .spyOn(functions, 'connectEmbeddedReader') + .mockImplementation(connectEmbeddedReader); + + // + const connectHandoffReader = jest.fn(() => returnWith); + jest + .spyOn(functions, 'connectHandoffReader') + .mockImplementation(connectHandoffReader); + + // + const connectLocalMobileReader = jest.fn(() => returnWith); + jest + .spyOn(functions, 'connectLocalMobileReader') + .mockImplementation(connectLocalMobileReader); + + // + const setSimulatedCard = jest.fn(() => returnWith); + jest + .spyOn(functions, 'setSimulatedCard') + .mockImplementation(setSimulatedCard); + + return { + discoverReaders, + cancelDiscovering, + connectBluetoothReader, + disconnectReader, + connectInternetReader, + connectUsbReader, + createPaymentIntent, + collectPaymentMethod, + retrievePaymentIntent, + getLocations, + processPayment, + createSetupIntent, + cancelPaymentIntent, + installAvailableUpdate, + cancelInstallingUpdate, + setReaderDisplay, + clearReaderDisplay, + retrieveSetupIntent, + collectSetupIntentPaymentMethod, + cancelSetupIntent, + confirmSetupIntent, + simulateReaderUpdate, + collectRefundPaymentMethod, + processRefund, + readReusableCard, + cancelCollectPaymentMethod, + cancelCollectRefundPaymentMethod, + cancelCollectSetupIntent, + cancelReadReusableCard, + connectEmbeddedReader, + connectHandoffReader, + connectLocalMobileReader, + setSimulatedCard, + }; +} + +const createContextWrapper = + (providerProps: any): React.FC => + ({ children }) => + ( + + {children} + + ); + +describe('useStripeTerminal.test.tsx', () => { + describe('Public API snapshot', () => { + it('ensure there are no unexpected changes to the hook exports', () => { + const { result } = renderHook(() => useStripeTerminal(), { + wrapper: createContextWrapper({}), + }); + + expect(result).toMatchSnapshot(); + }); + }); + + it('should use context values', () => { + const { result } = renderHook(() => useStripeTerminal(), { + wrapper: createContextWrapper({ + isInitialized: true, + loading: false, + connectedReader: { id: 12 }, + discoveredReaders: [{ id: 12 }, { id: 15 }], + }), + }); + + const { isInitialized, loading, connectedReader, discoveredReaders } = + result.current; + + expect(isInitialized).toEqual(true); + expect(loading).toEqual(loading); + expect(connectedReader).toMatchObject({ id: 12 }); + expect(discoveredReaders).toMatchObject([{ id: 12 }, { id: 15 }]); + }); + + describe('Public methods are called properly', () => { + it('clearCachedCredentials is called', () => { + const clearCachedCredentials = jest.fn(); + jest + .spyOn(functions, 'clearCachedCredentials') + .mockImplementation(clearCachedCredentials); + + const ContextWrapper = createContextWrapper({}); + const { result } = renderHook(() => useStripeTerminal(), { + wrapper: ContextWrapper, + }); + + act(() => { + result.current.clearCachedCredentials(); + }); + + expect(clearCachedCredentials).toBeCalled(); + }); + + it('initialized method is called', () => { + const initializeFn = jest.fn(); + const ContextWrapper = createContextWrapper({ initialize: initializeFn }); + const { result } = renderHook(() => useStripeTerminal(), { + wrapper: ContextWrapper, + }); + + act(() => { + result.current.initialize(); + }); + + expect(initializeFn).toBeCalled(); + }); + + it('public methods are called when it is initialized', () => { + const fns = spyAllFunctions(); + + const ContextWrapper = createContextWrapper({ isInitialized: true }); + const { result } = renderHook(() => useStripeTerminal(), { + wrapper: ContextWrapper, + }); + + act(() => { + result.current.connectBluetoothReader({} as any); + result.current.discoverReaders({} as any); + result.current.cancelCollectPaymentMethod(); + result.current.cancelDiscovering(); + result.current.cancelCollectRefundPaymentMethod(); + result.current.cancelInstallingUpdate(); + result.current.cancelPaymentIntent(''); + result.current.cancelReadReusableCard(); + result.current.cancelSetupIntent(''); + result.current.clearCachedCredentials(); + result.current.clearReaderDisplay(); + result.current.collectPaymentMethod({} as any); + result.current.collectRefundPaymentMethod({} as any); + result.current.collectSetupIntentPaymentMethod({} as any); + result.current.confirmSetupIntent(''); + result.current.connectBluetoothReader({} as any); + result.current.connectEmbeddedReader({} as any); + result.current.connectHandoffReader({} as any); + result.current.connectInternetReader({} as any); + result.current.connectLocalMobileReader({} as any); + result.current.connectUsbReader({} as any); + result.current.createPaymentIntent({} as any); + result.current.createSetupIntent({} as any); + result.current.disconnectReader(); + result.current.retrievePaymentIntent(''); + result.current.getLocations({} as any); + result.current.processPayment(''); + result.current.retrieveSetupIntent(''); + result.current.simulateReaderUpdate({} as any); + result.current.readReusableCard({} as any); + result.current.setSimulatedCard(''); + result.current.installAvailableUpdate(); + result.current.setReaderDisplay({} as any); + result.current.processRefund(); + result.current.cancelCollectSetupIntent(); + }); + + Object.values(fns).forEach((fn) => { + expect(fn).toBeCalled(); + }); + }); + + it('public methods are not called when it is not initialized', () => { + const fns = spyAllFunctions(); + console.error = jest.fn(); + + const ContextWrapper = createContextWrapper({ isInitialized: false }); + const { result } = renderHook(() => useStripeTerminal(), { + wrapper: ContextWrapper, + }); + + act(() => { + result.current.connectBluetoothReader({} as any); + result.current.discoverReaders({} as any); + result.current.cancelCollectPaymentMethod(); + result.current.cancelDiscovering(); + result.current.cancelCollectRefundPaymentMethod(); + result.current.cancelInstallingUpdate(); + result.current.cancelPaymentIntent(''); + result.current.cancelReadReusableCard(); + result.current.cancelSetupIntent(''); + result.current.clearReaderDisplay(); + result.current.collectPaymentMethod({} as any); + result.current.collectRefundPaymentMethod({} as any); + result.current.collectSetupIntentPaymentMethod({} as any); + result.current.confirmSetupIntent(''); + result.current.connectBluetoothReader({} as any); + result.current.connectEmbeddedReader({} as any); + result.current.connectHandoffReader({} as any); + result.current.connectInternetReader({} as any); + result.current.connectLocalMobileReader({} as any); + result.current.connectUsbReader({} as any); + result.current.createPaymentIntent({} as any); + result.current.createSetupIntent({} as any); + result.current.disconnectReader(); + result.current.retrievePaymentIntent(''); + result.current.getLocations({} as any); + result.current.processPayment(''); + result.current.retrieveSetupIntent(''); + result.current.simulateReaderUpdate({} as any); + result.current.readReusableCard({} as any); + result.current.setSimulatedCard(''); + result.current.installAvailableUpdate(); + result.current.setReaderDisplay({} as any); + result.current.processRefund(); + result.current.cancelCollectSetupIntent(); + }); + + Object.values(fns).forEach((fn) => { + expect(fn).not.toBeCalled(); + }); + expect(console.error).toBeCalledWith( + 'First initialize the Stripe Terminal SDK before performing any action' + ); + expect(console.error).toBeCalledTimes(34); + }); + + it('public methods are returns with mocked value', async () => { + spyAllFunctions({ returnWith: '_value' }); + + const ContextWrapper = createContextWrapper({ isInitialized: true }); + const { result } = renderHook(() => useStripeTerminal(), { + wrapper: ContextWrapper, + }); + + await expect( + result.current.connectBluetoothReader({} as any) + ).resolves.toEqual('_value'); + await expect(result.current.discoverReaders({} as any)).resolves.toEqual( + '_value' + ); + await expect( + result.current.cancelCollectPaymentMethod() + ).resolves.toEqual('_value'); + await expect(result.current.cancelDiscovering()).resolves.toEqual( + '_value' + ); + await expect( + result.current.cancelCollectRefundPaymentMethod() + ).resolves.toEqual('_value'); + await expect(result.current.cancelInstallingUpdate()).resolves.toEqual( + '_value' + ); + await expect( + result.current.cancelPaymentIntent({} as any) + ).resolves.toEqual('_value'); + await expect(result.current.cancelReadReusableCard()).resolves.toEqual( + '_value' + ); + await expect( + result.current.cancelSetupIntent({} as any) + ).resolves.toEqual('_value'); + await expect(result.current.clearReaderDisplay()).resolves.toEqual( + '_value' + ); + await expect( + result.current.collectPaymentMethod({} as any) + ).resolves.toEqual('_value'); + await expect( + result.current.collectRefundPaymentMethod({} as any) + ).resolves.toEqual('_value'); + await expect( + result.current.collectSetupIntentPaymentMethod({} as any) + ).resolves.toEqual('_value'); + await expect( + result.current.confirmSetupIntent({} as any) + ).resolves.toEqual('_value'); + await expect( + result.current.connectEmbeddedReader({} as any) + ).resolves.toEqual('_value'); + await expect( + result.current.connectHandoffReader({} as any) + ).resolves.toEqual('_value'); + await expect( + result.current.connectInternetReader({} as any) + ).resolves.toEqual('_value'); + await expect( + result.current.connectLocalMobileReader({} as any) + ).resolves.toEqual('_value'); + await expect(result.current.connectUsbReader({} as any)).resolves.toEqual( + '_value' + ); + await expect( + result.current.createPaymentIntent({} as any) + ).resolves.toEqual('_value'); + await expect( + result.current.createSetupIntent({} as any) + ).resolves.toEqual('_value'); + await expect(result.current.disconnectReader()).resolves.toEqual( + '_value' + ); + await expect( + result.current.retrievePaymentIntent({} as any) + ).resolves.toEqual('_value'); + await expect(result.current.getLocations({} as any)).resolves.toEqual( + '_value' + ); + await expect(result.current.processPayment({} as any)).resolves.toEqual( + '_value' + ); + await expect( + result.current.retrieveSetupIntent({} as any) + ).resolves.toEqual('_value'); + await expect( + result.current.simulateReaderUpdate({} as any) + ).resolves.toEqual('_value'); + await expect(result.current.readReusableCard({} as any)).resolves.toEqual( + '_value' + ); + await expect(result.current.setSimulatedCard({} as any)).resolves.toEqual( + '_value' + ); + await expect(result.current.installAvailableUpdate()).resolves.toEqual( + '_value' + ); + await expect(result.current.setReaderDisplay({} as any)).resolves.toEqual( + '_value' + ); + await expect(result.current.processRefund()).resolves.toEqual('_value'); + await expect(result.current.cancelCollectSetupIntent()).resolves.toEqual( + '_value' + ); + }); + }); +}); diff --git a/src/hooks/useStripeTerminal.tsx b/src/hooks/useStripeTerminal.tsx index 0371e305..6b0c62f8 100644 --- a/src/hooks/useStripeTerminal.tsx +++ b/src/hooks/useStripeTerminal.tsx @@ -357,6 +357,10 @@ export function useStripeTerminal(props?: Props) { const _connectEmbeddedReader = useCallback( async (params: ConnectEmbeddedParams) => { + if (!_isInitialized()) { + console.error(NOT_INITIALIZED_ERROR_MESSAGE); + throw Error(NOT_INITIALIZED_ERROR_MESSAGE); + } setLoading(true); const response = await connectEmbeddedReader(params); @@ -368,11 +372,15 @@ export function useStripeTerminal(props?: Props) { return response; }, - [setConnectedReader, setLoading] + [_isInitialized, setConnectedReader, setLoading] ); const _connectLocalMobileReader = useCallback( async (params: ConnectLocalMobileParams) => { + if (!_isInitialized()) { + console.error(NOT_INITIALIZED_ERROR_MESSAGE); + throw Error(NOT_INITIALIZED_ERROR_MESSAGE); + } setLoading(true); const response = await connectLocalMobileReader(params); @@ -384,11 +392,15 @@ export function useStripeTerminal(props?: Props) { return response; }, - [setConnectedReader, setLoading] + [_isInitialized, setConnectedReader, setLoading] ); const _connectHandoffReader = useCallback( async (params: ConnectEmbeddedParams) => { + if (!_isInitialized()) { + console.error(NOT_INITIALIZED_ERROR_MESSAGE); + throw Error(NOT_INITIALIZED_ERROR_MESSAGE); + } setLoading(true); const response = await connectHandoffReader(params); @@ -400,7 +412,7 @@ export function useStripeTerminal(props?: Props) { return response; }, - [setConnectedReader, setLoading] + [_isInitialized, setConnectedReader, setLoading] ); const _disconnectReader = useCallback(async () => { @@ -667,6 +679,10 @@ export function useStripeTerminal(props?: Props) { const _setSimulatedCard = useCallback( async (cardNumber: string) => { + if (!_isInitialized()) { + console.error(NOT_INITIALIZED_ERROR_MESSAGE); + throw Error(NOT_INITIALIZED_ERROR_MESSAGE); + } setLoading(true); const response = await setSimulatedCard(cardNumber); @@ -674,7 +690,7 @@ export function useStripeTerminal(props?: Props) { return response; }, - [setLoading] + [_isInitialized, setLoading] ); const _simulateReaderUpdate = useCallback( diff --git a/src/utils/__tests__/androidPermissionsUtils.test.ts b/src/utils/__tests__/androidPermissionsUtils.test.ts new file mode 100644 index 00000000..ebf120c8 --- /dev/null +++ b/src/utils/__tests__/androidPermissionsUtils.test.ts @@ -0,0 +1,160 @@ +import { requestNeededAndroidPermissions } from '../androidPermissionsUtils'; + +const permissionsConstantsMock = { + PERMISSIONS: { + ACCESS_FINE_LOCATION: 'ACCESS_FINE_LOCATION', + BLUETOOTH_CONNECT: 'BLUETOOTH_CONNECT', + BLUETOOTH_SCAN: 'BLUETOOTH_SCAN', + }, + RESULTS: { + GRANTED: 'granted', + }, +}; + +describe('androidPermissionsUtils.ts', () => { + it('access is granted', async () => { + const permissionsMock = { + ACCESS_FINE_LOCATION: 'granted', + BLUETOOTH_CONNECT: 'granted', + BLUETOOTH_SCAN: 'granted', + }; + jest.resetModules(); + + jest.mock('react-native/Libraries/Utilities/Platform', () => ({ + OS: 'android', + Version: 31, + })); + + jest.doMock( + 'react-native/Libraries/PermissionsAndroid/PermissionsAndroid', + () => ({ + request: (status: keyof typeof permissionsMock) => { + return permissionsMock[status]; + }, + ...permissionsConstantsMock, + }) + ); + + await expect(requestNeededAndroidPermissions()).resolves.toEqual({ + error: null, + }); + }); + + it('access fine location is not granted', async () => { + const permissionsMock = { + ACCESS_FINE_LOCATION: 'denied', + BLUETOOTH_CONNECT: 'granted', + BLUETOOTH_SCAN: 'granted', + }; + jest.resetModules(); + + jest.mock('react-native/Libraries/Utilities/Platform', () => ({ + OS: 'android', + Version: 31, + })); + + jest.doMock( + 'react-native/Libraries/PermissionsAndroid/PermissionsAndroid', + () => ({ + request: (permission: keyof typeof permissionsMock) => { + return permissionsMock[permission]; + }, + ...permissionsConstantsMock, + }) + ); + + await expect(requestNeededAndroidPermissions()).resolves.toEqual({ + error: { + ACCESS_FINE_LOCATION: 'denied', + }, + }); + }); + + it('bluetooth connect is not granted', async () => { + const permissionsMock = { + ACCESS_FINE_LOCATION: 'granted', + BLUETOOTH_CONNECT: 'denied', + BLUETOOTH_SCAN: 'granted', + }; + jest.resetModules(); + + jest.mock('react-native/Libraries/Utilities/Platform', () => ({ + OS: 'android', + Version: 31, + })); + + jest.doMock( + 'react-native/Libraries/PermissionsAndroid/PermissionsAndroid', + () => ({ + request: (permission: keyof typeof permissionsMock) => { + return permissionsMock[permission]; + }, + ...permissionsConstantsMock, + }) + ); + + await expect(requestNeededAndroidPermissions()).resolves.toEqual({ + error: { + BLUETOOTH_CONNECT: 'denied', + }, + }); + }); + + it('bluetooth scan is not granted', async () => { + const permissionsMock = { + ACCESS_FINE_LOCATION: 'granted', + BLUETOOTH_CONNECT: 'granted', + BLUETOOTH_SCAN: 'denied', + }; + jest.resetModules(); + + jest.mock('react-native/Libraries/Utilities/Platform', () => ({ + OS: 'android', + Version: 31, + })); + + jest.doMock( + 'react-native/Libraries/PermissionsAndroid/PermissionsAndroid', + () => ({ + request: (permission: keyof typeof permissionsMock) => { + return permissionsMock[permission]; + }, + ...permissionsConstantsMock, + }) + ); + + await expect(requestNeededAndroidPermissions()).resolves.toEqual({ + error: { + BLUETOOTH_SCAN: 'denied', + }, + }); + }); + + it('grants permissions on android lower api level', async () => { + const permissionsMock = { + ACCESS_FINE_LOCATION: 'granted', + BLUETOOTH_CONNECT: 'denied', + BLUETOOTH_SCAN: 'denied', + }; + jest.resetModules(); + + jest.mock('react-native/Libraries/Utilities/Platform', () => ({ + OS: 'android', + Version: 29, + })); + + jest.doMock( + 'react-native/Libraries/PermissionsAndroid/PermissionsAndroid', + () => ({ + request: (permission: keyof typeof permissionsMock) => { + return permissionsMock[permission]; + }, + ...permissionsConstantsMock, + }) + ); + + await expect(requestNeededAndroidPermissions()).resolves.toEqual({ + error: null, + }); + }); +}); diff --git a/src/utils/__tests__/b64EncodeDecode.test.ts b/src/utils/__tests__/b64EncodeDecode.test.ts new file mode 100644 index 00000000..fe2eb399 --- /dev/null +++ b/src/utils/__tests__/b64EncodeDecode.test.ts @@ -0,0 +1,45 @@ +import { b64EncodeUnicode, b64DecodeUnicode } from '../b64EncodeDecode'; + +describe('b64EncodeDecode.ts', () => { + describe('Base64 encode/decode', () => { + it('encodes unicode properly', () => { + expect(b64EncodeUnicode('test-encode-1')).toEqual('dGVzdC1lbmNvZGUtMQ=='); + + expect(b64EncodeUnicode('🍩 – doughnut')).toEqual( + '8J+NqSDigJMgZG91Z2hudXQ=' + ); + + expect(b64EncodeUnicode('🍫 – chocolate')).toEqual( + '8J+NqyDigJMgY2hvY29sYXRl' + ); + + expect(b64EncodeUnicode('🍿 – popcorn')).toEqual( + '8J+NvyDigJMgcG9wY29ybg==' + ); + + expect(b64EncodeUnicode('🍪 🥧 🍬 🍪')).toEqual( + '8J+NqiDwn6WnIPCfjawg8J+Nqg==' + ); + }); + + it('decodes unicode properly', () => { + expect(b64DecodeUnicode('dGVzdC1lbmNvZGUtMQ==')).toEqual('test-encode-1'); + + expect(b64DecodeUnicode('8J+NqSDigJMgZG91Z2hudXQ=')).toEqual( + '🍩 – doughnut' + ); + + expect(b64DecodeUnicode('8J+NqyDigJMgY2hvY29sYXRl')).toEqual( + '🍫 – chocolate' + ); + + expect(b64DecodeUnicode('8J+NvyDigJMgcG9wY29ybg==')).toEqual( + '🍿 – popcorn' + ); + + expect(b64DecodeUnicode('8J+NqiDwn6WnIPCfjawg8J+Nqg==')).toEqual( + '🍪 🥧 🍬 🍪' + ); + }); + }); +}); diff --git a/yarn.lock b/yarn.lock index 415a571c..1420cb91 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2467,6 +2467,13 @@ dependencies: defer-to-connect "^1.0.1" +"@testing-library/react-native@^10.0.0": + version "10.0.0" + resolved "https://registry.yarnpkg.com/@testing-library/react-native/-/react-native-10.0.0.tgz#fe8539c09c62df522b21528a5796dbecad3e34a2" + integrity sha512-aFq2BQReP9ijZsQ83XMLJthGyffFS7vv0YN3ZAvgIgxEfAQ78v0GOMLyoTNyZEM+wDGb4cn1yorHXYUWs8krwA== + dependencies: + pretty-format "^27.0.0" + "@tootallnate/once@1": version "1.1.2" resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-1.1.2.tgz#ccb91445360179a04e7fe6aff78c00ffc1eeaf82" @@ -3445,20 +3452,10 @@ camelcase@^6.0.0, camelcase@^6.2.0: resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-6.2.1.tgz#250fd350cfd555d0d2160b1d51510eaf8326e86e" integrity sha512-tVI4q5jjFV5CavAU8DXfza/TJcZutVKo/5Foskmsqcm0MsL91moHvwiGNnqaa2o6PF/7yT5ikDRcVcl8Rj6LCA== -caniuse-lite@^1.0.30001286: - version "1.0.30001287" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001287.tgz#5fab6a46ab9e47146d5dd35abfe47beaf8073c71" - integrity sha512-4udbs9bc0hfNrcje++AxBuc6PfLNHwh3PO9kbwnfCQWyqtlzg3py0YgFu8jyRTTo85VAz4U+VLxSlID09vNtWA== - -caniuse-lite@^1.0.30001317: - version "1.0.30001332" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001332.tgz#39476d3aa8d83ea76359c70302eafdd4a1d727dd" - integrity sha512-10T30NYOEQtN6C11YGg411yebhvpnC6Z102+B95eAsN0oB6KUs01ivE8u+G6FMIRtIrVlYXhL+LUwQ3/hXwDWw== - -caniuse-lite@^1.0.30001332: - version "1.0.30001340" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001340.tgz#029a2f8bfc025d4820fafbfaa6259fd7778340c7" - integrity sha512-jUNz+a9blQTQVu4uFcn17uAD8IDizPzQkIKh3LCJfg9BkyIqExYYdyc/ZSlWUSKb8iYiXxKsxbv4zYSvkqjrxw== +caniuse-lite@^1.0.30001286, caniuse-lite@^1.0.30001317, caniuse-lite@^1.0.30001332: + version "1.0.30001358" + resolved "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001358.tgz" + integrity sha512-hvp8PSRymk85R20bsDra7ZTCpSVGN/PAz9pSAjPSjKC+rNmnUk5vCRgJwiTT/O4feQ/yu/drvZYpKxxhbFuChw== chalk@4.1.0: version "4.1.0" @@ -7862,11 +7859,16 @@ react-devtools-core@^4.23.0: shell-quote "^1.6.1" ws "^7" -"react-is@^16.12.0 || ^17.0.0", react-is@^17.0.1: +"react-is@^16.12.0 || ^17.0.0", react-is@^17.0.1, react-is@^17.0.2: version "17.0.2" resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.2.tgz#e691d4a8e9c789365655539ab372762b0efb54f0" integrity sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w== +"react-is@^16.12.0 || ^17.0.0 || ^18.0.0": + version "18.2.0" + resolved "https://registry.yarnpkg.com/react-is/-/react-is-18.2.0.tgz#199431eeaaa2e09f86427efbb4f1473edb47609b" + integrity sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w== + react-is@^16.13.1, react-is@^16.8.1: version "16.13.1" resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4" @@ -7938,6 +7940,24 @@ react-shallow-renderer@16.14.1: object-assign "^4.1.1" react-is "^16.12.0 || ^17.0.0" +react-shallow-renderer@^16.13.1: + version "16.15.0" + resolved "https://registry.yarnpkg.com/react-shallow-renderer/-/react-shallow-renderer-16.15.0.tgz#48fb2cf9b23d23cde96708fe5273a7d3446f4457" + integrity sha512-oScf2FqQ9LFVQgA73vr86xl2NaOIX73rh+YFqcOp68CWj56tSfgtGKrEbyhCj0rSijyG9M1CYprTh39fBi5hzA== + dependencies: + object-assign "^4.1.1" + react-is "^16.12.0 || ^17.0.0 || ^18.0.0" + +react-test-renderer@17.0.2: + version "17.0.2" + resolved "https://registry.yarnpkg.com/react-test-renderer/-/react-test-renderer-17.0.2.tgz#4cd4ae5ef1ad5670fc0ef776e8cc7e1231d9866c" + integrity sha512-yaQ9cB89c17PUb0x6UfWRs7kQCorVdHlutU1boVPEsB8IDZH6n9tHxMacc3y0JoXOJUsZb/t/Mb8FUWMKaM7iQ== + dependencies: + object-assign "^4.1.1" + react-is "^17.0.2" + react-shallow-renderer "^16.13.1" + scheduler "^0.20.2" + react@17.0.2: version "17.0.2" resolved "https://registry.yarnpkg.com/react/-/react-17.0.2.tgz#d0b5cc516d29eb3eee383f75b62864cfb6800037"