diff --git a/src/async-tokenize/async-tokenize.test.ts b/src/async-tokenize/async-tokenize.test.ts deleted file mode 100644 index f80b445..0000000 --- a/src/async-tokenize/async-tokenize.test.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { fireEvent, waitFor } from '@testing-library/dom' - -import { AsyncTokenize } from './async-tokenize' -import { - configureFormSubmissionMock, - formElementsMock, - malgaConfigurations, -} from 'tests/mocks/common-configurations' -import { generateForm } from 'tests/mocks/form-dom' -import { Malga } from 'src/common/malga' - -const onSubmit = vi.fn() - -vi.mock('src/common/malga', async (importOriginal) => { - const Malga = await importOriginal() - return { - ...Malga, - tokenization: vi.fn(), - } -}) - -describe('async-tokenize', () => { - beforeEach(() => { - document.body.innerHTML = '' - }) - test('should be possible to find a tokenId element in the DOM and consequently contained in the form element', async () => { - configureFormSubmissionMock() - generateForm(onSubmit) - - const malga = new Malga(malgaConfigurations(false)) - - const asyncTokenizeObject = new AsyncTokenize(malga, formElementsMock) - - asyncTokenizeObject.handle() - - const form = document.querySelector('form') - fireEvent.submit(form!) - - await waitFor(() => { - const tokenIdInput = document.querySelector( - 'input[name="tokenId"]', - ) - expect(tokenIdInput).toBeInTheDocument() - expect(form).toContain(tokenIdInput) - expect(tokenIdInput).toBeTruthy() - }) - }) - test('should be return correct tokenId in sandbox environment ', async () => { - configureFormSubmissionMock() - - generateForm(onSubmit) - - const malga = new Malga(malgaConfigurations(true)) - - const asyncTokenizeObject = new AsyncTokenize(malga, formElementsMock) - - asyncTokenizeObject.handle() - - const form = document.querySelector('form') - fireEvent.submit(form!) - - await waitFor(() => { - const tokenIdInput = document.querySelector( - 'input[name="tokenId"]', - ) - expect(tokenIdInput?.value).toBe('sandbox-token-id') - }) - }) - test('should be return correct tokenId in production environment', async () => { - configureFormSubmissionMock() - - generateForm(onSubmit) - - const malgaConfigurationsProduction = { - apiKey: '17a64c8f-a387-4682-bdd8-d280493715e0', - clientId: 'd1d2b51a-0446-432a-b055-034518c2660e', - } - - const malga = new Malga(malgaConfigurationsProduction) - - const asyncTokenizeObject = new AsyncTokenize(malga, formElementsMock) - - asyncTokenizeObject.handle() - - const form = document.querySelector('form') - fireEvent.submit(form!) - - await waitFor(() => { - const tokenIdInput = document.querySelector( - 'input[name="tokenId"]', - ) - expect(tokenIdInput?.value).toBe('production-token-id') - }) - }) - test('should be possible to remove the elements and thus there is only 1 after the creation of the tokenIdElement and 4 before its creation', async () => { - configureFormSubmissionMock() - - generateForm(onSubmit) - - const malga = new Malga(malgaConfigurations(false)) - - const asyncTokenizeObject = new AsyncTokenize(malga, formElementsMock) - - const inputs = document.querySelectorAll('input') - expect(inputs.length).toBe(4) - - asyncTokenizeObject.handle() - - const form = document.querySelector('form') - fireEvent.submit(form!) - - await waitFor(() => { - const inputs = document.querySelectorAll('input') - expect(inputs.length).toBe(1) - }) - }) - - test('should be possible to return an error if empty apiKey and clientId are sent to the Malga constructor', async () => { - configureFormSubmissionMock() - - generateForm(onSubmit) - - const malgaConfigurationsEmpty = { - apiKey: '', - clientId: '', - } - - const malga = new Malga(malgaConfigurationsEmpty) - - const asyncTokenizeObject = new AsyncTokenize(malga, formElementsMock) - - expect(asyncTokenizeObject.handle).toThrowError( - "Cannot read properties of undefined (reading 'elements')", - ) - }) - test('should be possible to throw an error when the elements passed are incompatible with those in the DOM', async () => { - configureFormSubmissionMock() - - generateForm(onSubmit) - - const malga = new Malga(malgaConfigurations(false)) - - const asyncTokenizeObject = new AsyncTokenize(malga, { - form: 'data-form', - holderName: 'data-holder-name', - number: 'data-number', - expirationDate: 'data-expiration-date', - cvv: 'data-cvv', - }) - - expect(asyncTokenizeObject.handle).toThrowError( - "Cannot read properties of undefined (reading 'elements')", - ) - }) - test('should be possible to return an error if the form inputs do not have values assigned', async () => { - configureFormSubmissionMock() - generateForm() - - const malga = new Malga(malgaConfigurations(false)) - - const asyncTokenizeObject = new AsyncTokenize(malga, formElementsMock) - - expect(asyncTokenizeObject.handle).toThrowError() - }) -}) diff --git a/src/async-tokenize/async-tokenize.ts b/src/async-tokenize/async-tokenize.ts deleted file mode 100644 index 94fee1d..0000000 --- a/src/async-tokenize/async-tokenize.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { Malga } from 'src/common/malga' -import { MalgaFormElements } from 'src/common/interfaces' -import { - getFormElements, - removeFormElements, - createFormElement, - getFormValues, -} from '../common/utils' - -export class AsyncTokenize { - constructor( - private readonly malga: Malga, - private readonly elements: MalgaFormElements, - ) {} - - public handle() { - const { form } = getFormElements(this.elements) - - form?.addEventListener('submit', async (event) => { - event.preventDefault() - - const { holderName, number, expirationDate, cvv } = getFormValues( - this.elements, - ) - - const { tokenId } = await this.malga.tokenization({ - holderName, - number, - expirationDate, - cvv, - }) - - removeFormElements(this.elements) - const tokenIdElement = createFormElement('tokenId', tokenId) - - form?.appendChild(tokenIdElement) - - form?.submit() - }) - } -} diff --git a/src/async-tokenize/index.ts b/src/async-tokenize/index.ts deleted file mode 100644 index 0a88b9e..0000000 --- a/src/async-tokenize/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { AsyncTokenize } from './async-tokenize' diff --git a/src/common/enums/Events.ts b/src/common/enums/Events.ts deleted file mode 100644 index b23a32b..0000000 --- a/src/common/enums/Events.ts +++ /dev/null @@ -1,10 +0,0 @@ -export enum Event { - Submit = 'submit', - Focus = 'focus', - Blur = 'blur', - OnChange = 'change', - Input = 'input', - Tokenize = 'tokenize', - UpdateField = 'updateField', - SetTypeField = 'setTypeField', -} diff --git a/src/common/interfaces/form.ts b/src/common/interfaces/form.ts deleted file mode 100644 index d89a0ad..0000000 --- a/src/common/interfaces/form.ts +++ /dev/null @@ -1,7 +0,0 @@ -export interface MalgaFormElements { - form: string - holderName: string - cvv: string - expirationDate: string - number: string -} diff --git a/src/common/interfaces/malga.ts b/src/common/interfaces/malga.ts deleted file mode 100644 index c74448f..0000000 --- a/src/common/interfaces/malga.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface MalgaCreateTokenResponse { - cardHolderName: string - cardNumber: string - cardExpirationDate: string - cardCvv: string -} diff --git a/src/common/malga/index.ts b/src/common/malga/index.ts deleted file mode 100644 index 53bd6c4..0000000 --- a/src/common/malga/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { Malga } from './malga' diff --git a/src/common/malga/interfaces/error.ts b/src/common/malga/interfaces/error.ts deleted file mode 100644 index ec8e550..0000000 --- a/src/common/malga/interfaces/error.ts +++ /dev/null @@ -1,41 +0,0 @@ -type MalgaErrorType = - | 'api_error' - | 'bad_request' - | 'invalid_request_error' - | 'card_declined' - -type MalgaErrorDeclinedCode = - | 'card_not_supported' - | 'expired_card' - | 'fraud_confirmed' - | 'fraud_suspect' - | 'generic' - | 'insufficient_funds' - | 'invalid_amount' - | 'invalid_cvv' - | 'invalid_data' - | 'invalid_installment' - | 'invalid_merchant' - | 'invalid_number' - | 'invalid_pin' - | 'issuer_not_available' - | 'lost_card' - | 'not_permitted' - | 'pickup_card' - | 'pin_try_exceeded' - | 'restricted_card' - | 'security_violation' - | 'service_not_allowed' - | 'stolen_card' - | 'transaction_not_allowed' - | 'try_again' - -export interface MalgaErrorResponse { - error: { - type: MalgaErrorType - code: number - message: string - details?: string | string[] - declinedCode?: MalgaErrorDeclinedCode - } -} diff --git a/src/common/malga/interfaces/index.ts b/src/common/malga/interfaces/index.ts deleted file mode 100644 index 8160d53..0000000 --- a/src/common/malga/interfaces/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './error' -export * from './tokenization' diff --git a/src/common/malga/interfaces/tokenization.ts b/src/common/malga/interfaces/tokenization.ts deleted file mode 100644 index 20a8312..0000000 --- a/src/common/malga/interfaces/tokenization.ts +++ /dev/null @@ -1,6 +0,0 @@ -export interface TokenizationPayload { - holderName: string - number: string - expirationDate: string - cvv: string -} diff --git a/src/common/malga/malga.test.ts b/src/common/malga/malga.test.ts deleted file mode 100644 index ad09883..0000000 --- a/src/common/malga/malga.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { Malga } from './malga' - -describe('Malga', () => { - test('should make the request to the production domain', async () => { - const malga = new Malga({ apiKey: 'API_KEY', clientId: 'CLIENT_ID' }) - const response = await malga.tokenization({ - holderName: 'Alan Turing', - number: '5309459504335006', - cvv: '951', - expirationDate: '05/2030', - }) - - expect(response).toMatchObject({ tokenId: 'production-token-id' }) - }) - - test('should make the request to the sandbox domain', async () => { - const malga = new Malga({ - apiKey: 'API_KEY', - clientId: 'CLIENT_ID', - options: { sandbox: true }, - }) - const response = await malga.tokenization({ - holderName: 'Alan Turing', - number: '5309459504335006', - cvv: '951', - expirationDate: '05/2030', - }) - - expect(response).toMatchObject({ tokenId: 'sandbox-token-id' }) - }) - - test('should handle a request failure', async () => { - const malga = new Malga({ - apiKey: '', - clientId: '', - }) - - try { - await malga.tokenization({ - holderName: 'Alan Turing', - number: '5309459504335006', - cvv: '951', - expirationDate: '05/2030', - }) - } catch (error) { - expect(error).toMatchObject({ - error: { - code: 403, - message: 'forbidden', - type: 'invalid_request_error', - }, - }) - } - }) -}) diff --git a/src/common/malga/malga.ts b/src/common/malga/malga.ts deleted file mode 100644 index 220c9e3..0000000 --- a/src/common/malga/malga.ts +++ /dev/null @@ -1,70 +0,0 @@ -import axios, { AxiosError, AxiosInstance, AxiosResponse } from 'axios' - -import { MalgaConfigurations } from 'src/common/interfaces' - -import { MalgaErrorResponse, TokenizationPayload } from './interfaces' - -export class Malga { - private readonly api: AxiosInstance - - constructor(private readonly configurations: MalgaConfigurations) { - this.api = axios.create({ - baseURL: this.getBaseUrl(), - headers: { - 'Content-Type': 'application/json', - 'X-Api-Key': this.configurations.apiKey, - 'X-Client-Id': this.configurations.clientId, - }, - }) - } - - private getBaseUrl() { - if (this.configurations.options?.sandbox) { - return 'https://sandbox-api.dev.malga.io/v1' - } - - return 'https://api.dev.malga.io/v1' - } - - private handleSuccess(response: AxiosResponse) { - return response.data - } - - private handleError(error: AxiosError): Promise { - if (!error.response?.data || error.response.status >= 500) { - return Promise.reject({ - error: { - type: 'api_error', - code: 500, - message: 'unexpected error', - }, - }) - } - - if (error.response.status === 403) { - return Promise.reject({ - error: { - type: 'invalid_request_error', - code: 403, - message: 'forbidden', - }, - }) - } - - return Promise.reject(error.response.data) - } - - public async tokenization(payload: TokenizationPayload) { - const parsedPayload = { - cardNumber: payload.number, - cardCvv: payload.cvv, - cardExpirationDate: payload.expirationDate, - cardHolderName: payload.holderName, - } - - return this.api - .post('/tokens', parsedPayload) - .then(this.handleSuccess) - .catch(this.handleError) - } -} diff --git a/src/common/utils/form-elements/form-elements.test.ts b/src/common/utils/form-elements/form-elements.test.ts deleted file mode 100644 index f0d38f6..0000000 --- a/src/common/utils/form-elements/form-elements.test.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { - createFormElement, - getFormElements, - removeFormElements, -} from './form-elements' -import { generateForm } from 'tests/mocks/form-dom' - -const tokenId = '54595fec-87db-44f8-996a-2f4d6bf270b9' -describe('form-elements', () => { - describe('getFormElements', () => { - test('should be possible to find the elements in the dom', () => { - generateForm() - - const formElements = getFormElements({ - form: 'data-malga-tokenization-form', - holderName: 'data-malga-tokenization-holder-name', - number: 'data-malga-tokenization-number', - expirationDate: 'data-malga-tokenization-expiration-date', - cvv: 'data-malga-tokenization-cvv', - }) - - expect(formElements.form).toBeInTheDocument() - expect(formElements.holderName).toBeInTheDocument() - expect(formElements.cvv).toBeInTheDocument() - expect(formElements.expirationDate).toBeInTheDocument() - expect(formElements.number).toBeInTheDocument() - }) - - test("Should be returned null when elements aren't finded", () => { - generateForm() - - const formElements = getFormElements({ - form: 'data-malga-form', - holderName: 'data-malga-holder-name', - number: 'data-malga-number', - expirationDate: 'data-malga-expiration-date', - cvv: 'data-malga-cvv', - }) - expect(formElements.form).toBeNull() - expect(formElements.holderName).toBeNull() - expect(formElements.number).toBeNull() - expect(formElements.expirationDate).toBeNull() - expect(formElements.cvv).toBeNull() - }) - }) - describe('removeFormElements', () => { - beforeEach(() => { - document.body.innerHTML = '' - }) - test('should be possible to return null when trying to find the elements in the dom after calling the function', () => { - generateForm() - - removeFormElements({ - form: 'data-malga-tokenization-form', - holderName: 'data-malga-tokenization-holder-name', - number: 'data-malga-tokenization-number', - expirationDate: 'data-malga-tokenization-expiration-date', - cvv: 'data-malga-tokenization-cvv', - }) - - expect( - document.querySelector('data-malga-tokenization-holder-name'), - ).toBeNull() - expect( - document.querySelector('data-malga-tokenization-number'), - ).toBeNull() - expect( - document.querySelector('data-malga-tokenization-expiration-date'), - ).toBeNull() - expect(document.querySelector('data-malga-tokenization-cvv')).toBeNull() - }) - test('should be returned the elements in the DOM, when the function are called with selectores wrong, since the elements could not be removed', () => { - generateForm() - - removeFormElements({ - form: 'data-malga-form', - holderName: 'data-malga-holder-name', - number: 'data-malga-number', - expirationDate: 'data-malga-expiration-date', - cvv: 'data-malga-cvv', - }) - const formElements = getFormElements({ - form: 'data-malga-tokenization-form', - holderName: 'data-malga-tokenization-holder-name', - number: 'data-malga-tokenization-number', - expirationDate: 'data-malga-tokenization-expiration-date', - cvv: 'data-malga-tokenization-cvv', - }) - expect(formElements.form).toBeInTheDocument() - expect(formElements.holderName).toBeInTheDocument() - expect(formElements.number).toBeInTheDocument() - expect(formElements.expirationDate).toBeInTheDocument() - expect(formElements.cvv).toBeInTheDocument() - }) - }) - describe('createFormElements', () => { - test('should be possible to return a field with attributes of name, type and value which must be the passed tokenId', () => { - const tokenIdElement = createFormElement('tokenId', tokenId) - - expect(tokenIdElement).toHaveAttribute('name', 'tokenId') - expect(tokenIdElement).toHaveValue(tokenId) - expect(tokenIdElement).toHaveAttribute('type', 'hidden') - }) - - test('should be possible for the field field to be the same as the one mounted in the dom', () => { - const tokenIdElement = createFormElement('tokenId', tokenId) - - const field = document.createElement('input') - field.type = 'hidden' - field.name = 'tokenId' - field.value = '54595fec-87db-44f8-996a-2f4d6bf270b9' - - expect(tokenIdElement).toEqual(field) - }) - test('should be possible to insert the element in the body in the dom', () => { - const tokenIdElement = createFormElement('tokenId', tokenId) - - document.body.appendChild(tokenIdElement) - - expect(tokenIdElement).toBeInTheDocument() - }) - }) -}) diff --git a/src/common/utils/form-elements/form-elements.ts b/src/common/utils/form-elements/form-elements.ts deleted file mode 100644 index a71578b..0000000 --- a/src/common/utils/form-elements/form-elements.ts +++ /dev/null @@ -1,35 +0,0 @@ -import { MalgaFormElements } from 'src/common/interfaces' - -export function getFormElements(formElements: MalgaFormElements) { - return { - form: document.querySelector(`[${formElements.form}]`), - holderName: document.querySelector( - `[${formElements.holderName}]`, - ), - cvv: document.querySelector(`[${formElements.cvv}]`), - expirationDate: document.querySelector( - `[${formElements.expirationDate}]`, - ), - number: document.querySelector( - `[${formElements.number}]`, - ), - } -} - -export function removeFormElements(formElements: MalgaFormElements) { - const elements = getFormElements(formElements) - - elements.holderName?.remove() - elements.number?.remove() - elements.expirationDate?.remove() - elements.cvv?.remove() -} - -export function createFormElement(name: string, value: string) { - const field = document.createElement('input') - field.type = 'hidden' - field.name = name - field.value = value - - return field -} diff --git a/src/common/utils/form-elements/index.ts b/src/common/utils/form-elements/index.ts deleted file mode 100644 index fb1a797..0000000 --- a/src/common/utils/form-elements/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { - getFormElements, - createFormElement, - removeFormElements, -} from './form-elements' diff --git a/src/common/utils/index.ts b/src/common/utils/index.ts deleted file mode 100644 index 89b4941..0000000 --- a/src/common/utils/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export * from './form-elements' -export * from './form-iframes' -export * from './form-events' diff --git a/src/constants/index.ts b/src/constants/index.ts new file mode 100644 index 0000000..c7bb35f --- /dev/null +++ b/src/constants/index.ts @@ -0,0 +1 @@ +export * from './url' diff --git a/src/constants/url.ts b/src/constants/url.ts new file mode 100644 index 0000000..ca11ee6 --- /dev/null +++ b/src/constants/url.ts @@ -0,0 +1 @@ +export const URL_HOSTED_FIELD = 'https://develop.d3krxmg1839vaa.amplifyapp.com/' diff --git a/src/enums/Events.ts b/src/enums/Events.ts new file mode 100644 index 0000000..e99553a --- /dev/null +++ b/src/enums/Events.ts @@ -0,0 +1,16 @@ +export enum Event { + Submit = 'submit', + Focus = 'focus', + Blur = 'blur', + Tokenize = 'tokenize', + SetTypeField = 'setTypeField', + CardTypeChanged = 'cardTypeChanged', + Validity = 'validity', +} + +export enum EventEmits { + Validity = 'validity', + CardTypeChange = 'cardTypeChanged', + Focus = 'focus', + Blur = 'blur', +} diff --git a/src/common/enums/Fields.ts b/src/enums/Fields.ts similarity index 100% rename from src/common/enums/Fields.ts rename to src/enums/Fields.ts diff --git a/src/enums/Styles.ts b/src/enums/Styles.ts new file mode 100644 index 0000000..826a046 --- /dev/null +++ b/src/enums/Styles.ts @@ -0,0 +1,6 @@ +export enum CSSClasses { + Invalid = 'malga-hosted-field-invalid', + Valid = 'malga-hosted-field-valid', + Focused = 'malga-hosted-field-focused', + Default = 'malga-hosted-field', +} diff --git a/src/common/enums/index.ts b/src/enums/index.ts similarity index 66% rename from src/common/enums/index.ts rename to src/enums/index.ts index fed42d7..fa33c21 100644 --- a/src/common/enums/index.ts +++ b/src/enums/index.ts @@ -1,2 +1,3 @@ export * from './Events' export * from './Fields' +export * from './Styles' diff --git a/src/common/utils/form-events/events.ts b/src/events/events.ts similarity index 100% rename from src/common/utils/form-events/events.ts rename to src/events/events.ts diff --git a/src/common/utils/form-events/index.ts b/src/events/index.ts similarity index 100% rename from src/common/utils/form-events/index.ts rename to src/events/index.ts diff --git a/src/common/utils/form-events/validation.ts b/src/events/validation.ts similarity index 61% rename from src/common/utils/form-events/validation.ts rename to src/events/validation.ts index 4cd1f81..0108b70 100644 --- a/src/common/utils/form-events/validation.ts +++ b/src/events/validation.ts @@ -1,3 +1,4 @@ +import { CSSClasses } from 'src/enums' import { eventsEmitter } from 'src/tokenization' export function validation(data: any, parentNode: Element | null) { @@ -6,13 +7,13 @@ export function validation(data: any, parentNode: Element | null) { const isPotentiallyValid = data.potentialValid if (isEmpty || isPotentiallyValid) { - parentNode?.classList.remove('malga-hosted-field-valid') - parentNode?.classList.remove('malga-hosted-field-invalid') + parentNode?.classList.remove(CSSClasses.Valid) + parentNode?.classList.remove(CSSClasses.Invalid) return } - parentNode?.classList.toggle('malga-hosted-field-valid', isValid) - parentNode?.classList.toggle('malga-hosted-field-invalid', !isValid) + parentNode?.classList.toggle(CSSClasses.Valid, isValid) + parentNode?.classList.toggle(CSSClasses.Invalid, !isValid) eventsEmitter.emit('validity', { field: data.fieldType, diff --git a/src/iframes/create.test.ts b/src/iframes/create.test.ts new file mode 100644 index 0000000..274c623 --- /dev/null +++ b/src/iframes/create.test.ts @@ -0,0 +1,37 @@ +import { CSSClasses } from 'src/enums' +import { create } from './create' +import { URL_HOSTED_FIELD } from '../constants' +import { camelToKebabCase } from '../utils/parsedString' + +describe('create', () => { + function testCreatingIframe(field: string) { + test(`should be possible to create an iframe with field ${field}`, () => { + const type = camelToKebabCase(field) + const parentNode = document.createElement('div') + parentNode.id = type + document.body.appendChild(parentNode) + + create(type, { + container: `#${type}`, + type: 'text', + }) + + const iframe = document.querySelector(`iframe[name=${type}]`) + + expect(iframe).toBeInTheDocument() + expect(iframe).toHaveAttribute('src', URL_HOSTED_FIELD) + expect(iframe).toHaveAttribute('name', type) + expect(parentNode.classList.contains(CSSClasses.Default)).toBe(true) + expect(parentNode.getAttribute('id')).toBe(type) + }) + } + + const fields = [ + 'cardNumber', + 'cardHolderName', + 'cardExpirationDate', + 'cardCvv', + ] + + fields.forEach(testCreatingIframe) +}) diff --git a/src/common/utils/form-iframes/create.ts b/src/iframes/create.ts similarity index 59% rename from src/common/utils/form-iframes/create.ts rename to src/iframes/create.ts index 852149d..40c26e1 100644 --- a/src/common/utils/form-iframes/create.ts +++ b/src/iframes/create.ts @@ -1,6 +1,7 @@ -import type { MalgaInputFieldConfiguration } from 'src/common/interfaces' -import { waitingForElement } from './observer' -import { camelToKebabCase } from './parsedString' +import { CSSClasses } from 'src/enums' +import type { MalgaInputFieldConfiguration } from 'src/interfaces' +import { camelToKebabCase, waitingForElement } from 'src/utils' +import { URL_HOSTED_FIELD } from '../constants' export function create( type: string, @@ -10,14 +11,14 @@ export function create( const iframeName = camelToKebabCase(type) iframe.setAttribute('name', iframeName) - iframe.setAttribute('src', 'https://develop.d3krxmg1839vaa.amplifyapp.com/') //URL DA APLICAÇÃO + iframe.setAttribute('src', URL_HOSTED_FIELD) iframe.setAttribute('width', '100%') iframe.setAttribute('height', '100%') iframe.setAttribute('frameborder', '0') waitingForElement(fieldConfig.container, (parentNode) => { parentNode?.appendChild(iframe) - parentNode.classList.add('malga-hosted-field') + parentNode.classList.add(CSSClasses.Default) }) return iframe diff --git a/src/common/utils/form-iframes/index.ts b/src/iframes/index.ts similarity index 58% rename from src/common/utils/form-iframes/index.ts rename to src/iframes/index.ts index 97c6cf2..0ea5906 100644 --- a/src/common/utils/form-iframes/index.ts +++ b/src/iframes/index.ts @@ -1,6 +1,6 @@ export * from './create' export * from './loaded' -export * from './parsedString' -export * from './observer' +export * from '../utils/parsedString' +export * from '../utils/observer' export * from './submit' export * from './listener' diff --git a/src/iframes/listener.test.ts b/src/iframes/listener.test.ts new file mode 100644 index 0000000..4b40e0a --- /dev/null +++ b/src/iframes/listener.test.ts @@ -0,0 +1,146 @@ +import { validation } from 'src/events' +import { listener } from './listener' +import { CSSClasses, Event } from 'src/enums' +import { eventsEmitter } from 'src/tokenization' +import { handleCreateMockEvent } from 'tests/mocks' + +vi.mock('src/events', async () => { + const actual = await vi.importActual('src/events') + const actualExports = + typeof actual === 'object' && actual !== null ? actual : {} + + return { + ...actualExports, + validation: vi.fn(), + } +}) + +vi.mock('src/tokenization', () => ({ + eventsEmitter: { + emit: vi.fn(), + }, +})) + +describe('listener', () => { + let addEventListenerSpy: any + let parentNode: HTMLElement + + beforeEach(() => { + addEventListenerSpy = vi.spyOn(window, 'addEventListener') + parentNode = document.createElement('div') + + parentNode.id = 'card-number' + vi.spyOn(parentNode.classList, 'toggle') + + document.querySelector = vi.fn((selector) => { + if (selector === '#card-number') return parentNode + return null + }) + }) + + afterEach(() => { + vi.clearAllMocks() + parentNode.classList.remove(CSSClasses.Focused) + }) + + test('should add a message event listener to the window', () => { + listener() + + expect(addEventListenerSpy).toHaveBeenCalledWith( + 'message', + expect.any(Function), + ) + }) + + test('should get the data and type from event with correct origin', () => { + listener() + const messageHandler = addEventListenerSpy.mock.calls[0][1] + + const event = handleCreateMockEvent('successOrigin') + messageHandler(event) + + expect(document.querySelector).toHaveBeenCalledWith( + `#${event.data.data.fieldType}`, + ) + expect(document.querySelector).toHaveBeenCalledTimes(1) + }) + + test('should ignore messages from incorrect origins', () => { + listener() + const messageHandler = addEventListenerSpy.mock.calls[0][1] + + const event = handleCreateMockEvent('test', 'https://wrong-origin.com') + + messageHandler(event) + expect(document.querySelector).not.toHaveBeenCalled() + }) + + test('should call validation for Validity event type', () => { + listener() + const messageHandler = addEventListenerSpy.mock.calls[0][1] + + const event = handleCreateMockEvent(Event.Validity) + + messageHandler(event) + expect(validation).toHaveBeenCalledWith( + event.data.data, + expect.any(Element), + ) + }) + + test('should emit CardTypeChanged event for CardTypeChanged event type', () => { + listener() + const messageHandler = addEventListenerSpy.mock.calls[0][1] + const event = handleCreateMockEvent(Event.CardTypeChanged) + + const updateEvent = { + ...event, + data: { + ...event.data, + data: { + ...event.data.data, + card: 'visa', + }, + }, + } + + messageHandler(updateEvent) + + expect(eventsEmitter.emit).toHaveBeenCalledWith('cardTypeChanged', { + card: 'visa', + parentNode: expect.any(Element), + }) + }) + + test('should emit Focus event for Focus event type', () => { + listener() + const messageHandler = addEventListenerSpy.mock.calls[0][1] + + const event = handleCreateMockEvent(Event.Focus) + + messageHandler(event) + expect(eventsEmitter.emit).toHaveBeenCalledWith('focus', { + field: 'card-number', + parentNode: expect.any(Element), + }) + expect(parentNode.classList.toggle).toHaveBeenCalledWith(CSSClasses.Focused) + expect(parentNode.classList.contains(CSSClasses.Focused)).toBe(true) + }) + + test('should emit Blur event for Blur event type', () => { + parentNode.classList.add(CSSClasses.Focused) + + listener() + const messageHandler = addEventListenerSpy.mock.calls[0][1] + + const event = handleCreateMockEvent(Event.Blur) + + messageHandler(event) + expect(eventsEmitter.emit).toHaveBeenCalledWith('blur', { + field: 'card-number', + parentNode: expect.any(Element), + }) + expect(parentNode.classList.toggle).toHaveBeenCalledWith(CSSClasses.Focused) + expect(parentNode.classList.contains(CSSClasses.Focused)).toBe(false) + }) +}) diff --git a/src/common/utils/form-iframes/listener.ts b/src/iframes/listener.ts similarity index 54% rename from src/common/utils/form-iframes/listener.ts rename to src/iframes/listener.ts index 7486c9d..79690f4 100644 --- a/src/common/utils/form-iframes/listener.ts +++ b/src/iframes/listener.ts @@ -1,5 +1,6 @@ +import { CSSClasses, EventEmits, Event } from 'src/enums' +import { EventListener, validation } from 'src/events' import { eventsEmitter } from 'src/tokenization' -import { EventListener, validation } from '../form-events' export function listener() { const windowMessage = new EventListener(window.parent) @@ -11,24 +12,27 @@ export function listener() { const parentNode = document.querySelector(`#${data?.fieldType}`) if (!parentNode) return - if (type === 'validation') { + if (type === Event.Validity) { validation(data, parentNode) } - if (type === 'cardTypeChanged') { + if (type === Event.CardTypeChanged) { eventsEmitter.emit('cardTypeChanged', { card: data.card, parentNode: parentNode, }) } - if (type === 'focus' || type === 'blur') { - parentNode?.classList.toggle('malga-hosted-field-focused') + if (type === Event.Focus || type === Event.Blur) { + parentNode?.classList.toggle(CSSClasses.Focused) - eventsEmitter.emit(type === 'focus' ? 'focus' : 'blur', { - field: data.fieldType, - parentNode: parentNode, - }) + eventsEmitter.emit( + type === EventEmits.Focus ? EventEmits.Focus : EventEmits.Blur, + { + field: data.fieldType, + parentNode: parentNode, + }, + ) } }) } diff --git a/src/iframes/loaded.test.ts b/src/iframes/loaded.test.ts new file mode 100644 index 0000000..b032b04 --- /dev/null +++ b/src/iframes/loaded.test.ts @@ -0,0 +1,37 @@ +import { URL_HOSTED_FIELD } from 'src/constants' +import { loaded } from './loaded' +import { camelToKebabCase } from 'src/utils' +import { configurationsSDK } from 'tests/mocks' + +vi.mock('./create', () => ({ + create: vi.fn((field) => { + const iframe = document.createElement('iframe') + iframe.src = URL_HOSTED_FIELD + iframe.name = camelToKebabCase(field) + document.body.appendChild(iframe) + + return iframe + }), +})) + +describe('loaded', () => { + test('should be possible to loaded the iframes', () => { + loaded(configurationsSDK.options?.config) + + const iframes = document.querySelectorAll('iframe') + + expect(iframes).toHaveLength(4) + + iframes.forEach((iframe) => { + expect(iframe).toHaveAttribute('src', URL_HOSTED_FIELD) + }) + + if (iframes) { + const onloadEvent = new Event('load') + + iframes.forEach((iframe) => { + iframe.dispatchEvent(onloadEvent) + }) + } + }) +}) diff --git a/src/common/utils/form-iframes/loaded.ts b/src/iframes/loaded.ts similarity index 78% rename from src/common/utils/form-iframes/loaded.ts rename to src/iframes/loaded.ts index df57a44..2ce1199 100644 --- a/src/common/utils/form-iframes/loaded.ts +++ b/src/iframes/loaded.ts @@ -1,14 +1,14 @@ -import type { MalgaInputFieldConfigurations } from 'src/common/interfaces' +import type { MalgaInputFieldConfigurations } from 'src/interfaces' import { create } from './create' -import { Event } from 'src/common/enums' -import { camelToKebabCase } from './parsedString' +import { Event } from 'src/enums' +import { camelToKebabCase } from 'src/utils' +import { URL_HOSTED_FIELD } from '../constants' export function loaded(config: MalgaInputFieldConfigurations) { const fields = Object.keys(config.fields) for (const field of fields) { const fieldConfig = config.fields[field as keyof typeof config.fields] - const iframe = create(field, fieldConfig) const iframeName = camelToKebabCase(field) @@ -31,7 +31,7 @@ export function loaded(config: MalgaInputFieldConfigurations) { styles: config.styles, preventAutofill: config.preventAutofill ?? true, }, - 'https://develop.d3krxmg1839vaa.amplifyapp.com/', //URL DA APLICAÇÃO + URL_HOSTED_FIELD, ) } } diff --git a/src/iframes/submit.test.ts b/src/iframes/submit.test.ts new file mode 100644 index 0000000..eccf5f4 --- /dev/null +++ b/src/iframes/submit.test.ts @@ -0,0 +1,40 @@ +import { submit } from './submit' +import { + configurationsSDK, + configurationWithSubmitData, + handleRemoveIframe, + handleSetupIframeInDOM, +} from 'tests/mocks' + +describe('submit', () => { + let iframeCardNumber: HTMLIFrameElement + + beforeAll(() => { + iframeCardNumber = handleSetupIframeInDOM('card-number', { + postMessage: vi.fn(), + }) + }) + + afterEach(() => { + vi.clearAllMocks() + handleRemoveIframe(iframeCardNumber) + }) + + test('should send a post message with authorization data', () => { + submit(configurationsSDK) + + expect(iframeCardNumber).toBeInTheDocument() + expect(iframeCardNumber.contentWindow?.postMessage).toHaveBeenCalledTimes(1) + expect(iframeCardNumber.contentWindow?.postMessage).toHaveBeenCalledWith( + configurationWithSubmitData, + '*', + ) + }) + + test('should not send a post message if iframe does not exist', () => { + handleRemoveIframe(iframeCardNumber) + + submit(configurationsSDK) + expect(iframeCardNumber.contentWindow?.postMessage).not.toHaveBeenCalled() + }) +}) diff --git a/src/common/utils/form-iframes/submit.ts b/src/iframes/submit.ts similarity index 61% rename from src/common/utils/form-iframes/submit.ts rename to src/iframes/submit.ts index 6ba4fa9..a3f056f 100644 --- a/src/common/utils/form-iframes/submit.ts +++ b/src/iframes/submit.ts @@ -1,14 +1,16 @@ -import type { MalgaConfigurations } from 'src/common/interfaces' -import { EventPostMessage } from '../form-events' -import { Event } from 'src/common/enums' +import type { MalgaConfigurations } from 'src/interfaces' +import { Event } from 'src/enums' +import { EventPostMessage } from 'src/events' export function submit(configurations: MalgaConfigurations) { const iframeCardNumber = document.querySelector( 'iframe[name=card-number]', ) as HTMLIFrameElement - if (!iframeCardNumber) { - console.error('iframeCardNumber is null, cannot send postMessage') + if (!iframeCardNumber || !iframeCardNumber.contentWindow) { + console.error( + 'iframeCardNumber is null or has no contentWindow, cannot send postMessage', + ) return } diff --git a/src/common/interfaces/configurations.ts b/src/interfaces/configurations.ts similarity index 96% rename from src/common/interfaces/configurations.ts rename to src/interfaces/configurations.ts index e87a7f9..3cb1150 100644 --- a/src/common/interfaces/configurations.ts +++ b/src/interfaces/configurations.ts @@ -1,6 +1,5 @@ export interface MalgaInputFieldConfiguration { container: string - // selector: string placeholder?: string type?: string needMask?: boolean diff --git a/src/common/interfaces/index.ts b/src/interfaces/index.ts similarity index 100% rename from src/common/interfaces/index.ts rename to src/interfaces/index.ts diff --git a/src/tokenization.test.ts b/src/tokenization.test.ts index 561a843..51df579 100644 --- a/src/tokenization.test.ts +++ b/src/tokenization.test.ts @@ -1,207 +1,93 @@ -import { fireEvent, waitFor } from '@testing-library/dom' -import { - configureFormSubmissionMock, - formElementsMock, - formValuesMock, - handleFormMock, - malgaConfigurations, -} from '../tests/mocks/common-configurations' -import { MalgaTokenization } from './tokenization' -import { generateForm, generateFormEmptyValues } from 'tests/mocks/form-dom' - -vi.mock('./common/malga', async (importOriginal) => { - const Malga = await importOriginal() - return { - ...Malga, - tokenization: vi.fn(), - } +import { MalgaTokenization, eventsEmitter } from './tokenization' // Adjust path as needed +import { Tokenize } from './tokenize' +import { loaded, listener } from './iframes' +import { configurationsSDK } from 'tests/mocks' + +vi.mock('./tokenize') +vi.mock('./iframes') +vi.mock('./events', () => { + const MockEvents = vi.fn(() => ({ + on: vi.fn(), + emit: vi.fn(), + })) + return { Events: MockEvents } }) -const onSubmit = vi.fn() - -function FormForInit(onSubmit: any) { - const { form, holderNameInput, cvvInput, expirationDateInput, numberInput } = - handleFormMock() - - form.setAttribute(formElementsMock.form, '') - form.onsubmit = onSubmit - form.id = 'form' - form.method = 'POST' - form.action = '/test' - - holderNameInput.setAttribute(formElementsMock.holderName, '') - numberInput.setAttribute(formElementsMock.number, '') - cvvInput.setAttribute(formElementsMock.cvv, '') - expirationDateInput.setAttribute(formElementsMock.expirationDate, '') - - document.body.appendChild(form) - form.appendChild(holderNameInput) - form.appendChild(numberInput) - form.appendChild(expirationDateInput) - form.appendChild(cvvInput) - - const inputs = document.querySelectorAll('input') - inputs[0].value = formValuesMock.holderName - inputs[1].value = formValuesMock.number - inputs[2].value = formValuesMock.expirationDate - inputs[3].value = formValuesMock.cvv -} -function FormForTokenize() { - const { form, holderNameInput, cvvInput, expirationDateInput, numberInput } = - handleFormMock() - - form.setAttribute(formElementsMock.form, '') - - form.id = 'form' - form.method = 'POST' - form.action = '/test' - - holderNameInput.setAttribute(formElementsMock.holderName, '') - numberInput.setAttribute(formElementsMock.number, '') - cvvInput.setAttribute(formElementsMock.cvv, '') - expirationDateInput.setAttribute(formElementsMock.expirationDate, '') - - document.body.appendChild(form) - form.appendChild(holderNameInput) - form.appendChild(numberInput) - form.appendChild(expirationDateInput) - form.appendChild(cvvInput) - - const inputs = document.querySelectorAll('input') - inputs[0].value = formValuesMock.holderName - inputs[1].value = formValuesMock.number - inputs[2].value = formValuesMock.expirationDate - inputs[3].value = formValuesMock.cvv -} -describe('MalgaTokenization', () => { - describe('init', () => { - beforeEach(() => { - document.body.innerHTML = '' - }) - test('should be possible to return the tokenId element', async () => { - configureFormSubmissionMock() - - generateForm(onSubmit) - - const malgaTokenizationObject = new MalgaTokenization( - malgaConfigurations(false), - ) - - malgaTokenizationObject.init() - - const form = document.querySelector('form') - fireEvent.submit(form!) - - await waitFor(() => { - const tokenIdInput = document.querySelector( - 'input[name="tokenId"]', - ) - expect(tokenIdInput).toBeInTheDocument() - expect(form).toContain(tokenIdInput) - expect(tokenIdInput).toBeTruthy() - }) - }) - test('should be possible to return an error if form elements do not have values assigned', async () => { - configureFormSubmissionMock() - - generateFormEmptyValues(onSubmit) - - const malgaTokenizationObject = new MalgaTokenization( - malgaConfigurations(false), - ) - - await waitFor(() => { - expect(malgaTokenizationObject.init).rejects.toThrowError() - }) - }) - test('should be possible to return an error if apiKey and clientId are passed empty', async () => { - configureFormSubmissionMock() - - FormForInit(onSubmit) - - const malgaConfigurationsEmpty = { - apiKey: '', - clientId: '', - } - - const malgaTokenizationObject = new MalgaTokenization( - malgaConfigurationsEmpty, - ) - - await waitFor(() => { - expect(malgaTokenizationObject.init).rejects.toThrowError() - }) - }) + +describe('tokenization', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + test('should create a new MalgaTokenization with configurations', () => { + const malgaTokenization = new MalgaTokenization(configurationsSDK) + + expect(malgaTokenization).toBeDefined() + expect(loaded).toHaveBeenCalled() + expect(listener).toHaveBeenCalled() + }) + + test('should show an error if API key is missing', () => { + const consoleErrorSpy = vi.spyOn(console, 'error') + new MalgaTokenization({ ...configurationsSDK, apiKey: '' }) + + expect(consoleErrorSpy).toHaveBeenCalledWith( + 'Missing API key. Pass it to the constructor `new MalgaTokenization({ apiKey: "YOUR_API_KEY", clientId: "YOUR_CLIENT_ID" })`', + ) + }) + + test('should show an error if client ID is missing', () => { + const consoleErrorSpy = vi.spyOn(console, 'error') + new MalgaTokenization({ ...configurationsSDK, clientId: '' }) + + expect(consoleErrorSpy).toHaveBeenCalledWith( + 'Missing API key. Pass it to the constructor `new MalgaTokenization({ apiKey: "YOUR_API_KEY", clientId: "YOUR_CLIENT_ID" })`', + ) + }) + + test('should call handle function in tokenization class', async () => { + const mockTokenizeHandle = vi + .fn() + .mockResolvedValue('623e25e1-9c40-442e-beaa-a9d7b735bdc1') + + vi.spyOn(Tokenize.prototype, 'handle').mockImplementation( + mockTokenizeHandle, + ) + + const malgaTokenization = new MalgaTokenization(configurationsSDK) + const token = await malgaTokenization.tokenize() + + expect(Tokenize).toHaveBeenCalledWith(configurationsSDK) + expect(mockTokenizeHandle).toHaveBeenCalled() + expect(token).toBe('623e25e1-9c40-442e-beaa-a9d7b735bdc1') }) - describe('tokenize', () => { - beforeEach(() => { - document.body.innerHTML = '' - }) - test('should be possible to return a not falsy value equal to production-token-id', async () => { - configureFormSubmissionMock() - const malgaTokenizationObject = new MalgaTokenization( - malgaConfigurations(false), - ) - FormForTokenize() - const form = document.querySelector('form') - fireEvent.submit(form!) - const { tokenId } = await malgaTokenizationObject.tokenize() - await waitFor(() => { - expect(tokenId).toBe('production-token-id') - }) - }) - test('should be possible to return an error if form elements do not have values assigned', async () => { - configureFormSubmissionMock() - const malgaTokenizationObject = new MalgaTokenization( - malgaConfigurations(false), - ) - - const { - form, - holderNameInput, - cvvInput, - expirationDateInput, - numberInput, - } = handleFormMock() - - form.setAttribute(formElementsMock.form, '') - form.id = 'form' - form.method = 'POST' - form.action = '/test' - - holderNameInput.setAttribute(formElementsMock.holderName, '') - numberInput.setAttribute(formElementsMock.number, '') - cvvInput.setAttribute(formElementsMock.cvv, '') - expirationDateInput.setAttribute(formElementsMock.expirationDate, '') - - document.body.appendChild(form) - form.appendChild(holderNameInput) - form.appendChild(numberInput) - form.appendChild(expirationDateInput) - form.appendChild(cvvInput) - - const form2 = document.querySelector('form') - fireEvent.submit(form2!) - - await expect(malgaTokenizationObject.tokenize()).rejects.toThrowError() - }) - test('should be possible to return an error if apiKey and clientId are passed empty', async () => { - configureFormSubmissionMock() - - const malgaConfigurationsEmpty = { - apiKey: '', - clientId: '', - } - - const malgaTokenizationObject = new MalgaTokenization( - malgaConfigurationsEmpty, - ) - - FormForTokenize() - - const form = document.querySelector('form') - fireEvent.submit(form!) - - await expect(malgaTokenizationObject.tokenize()).rejects.toThrowError() - }) + + test('should call eventsEmitter.on when type cardTypeChanged is called', () => { + const malgaTokenization = new MalgaTokenization(configurationsSDK) + const mockEventHandler = vi.fn() + + malgaTokenization.on('cardTypeChanged', mockEventHandler) + + expect(eventsEmitter.on).toHaveBeenCalledWith( + 'cardTypeChanged', + mockEventHandler, + ) + }) + + test('should call eventsEmitter.on when type focus is called', () => { + const malgaTokenization = new MalgaTokenization(configurationsSDK) + const mockEventHandler = vi.fn() + + malgaTokenization.on('focus', mockEventHandler) + + expect(eventsEmitter.on).toHaveBeenCalledWith('focus', mockEventHandler) + }) + + test('should call eventsEmitter.on when type validity is called', () => { + const malgaTokenization = new MalgaTokenization(configurationsSDK) + const mockEventHandler = vi.fn() + + malgaTokenization.on('validity', mockEventHandler) + + expect(eventsEmitter.on).toHaveBeenCalledWith('validity', mockEventHandler) }) }) diff --git a/src/tokenization.ts b/src/tokenization.ts index 9d721c9..acaf3dc 100644 --- a/src/tokenization.ts +++ b/src/tokenization.ts @@ -1,7 +1,8 @@ -import type { MalgaConfigurations } from 'src/common/interfaces' +import type { MalgaConfigurations } from 'src/interfaces' import { Tokenize } from './tokenize' -import { Events, listener, loaded } from './common/utils' +import { Events } from './events' +import { listener, loaded } from './iframes' export const eventsEmitter = new Events() diff --git a/src/tokenize/tokenize.test.ts b/src/tokenize/tokenize.test.ts index d611d18..97bd0f1 100644 --- a/src/tokenize/tokenize.test.ts +++ b/src/tokenize/tokenize.test.ts @@ -1,116 +1,105 @@ +import { Event } from 'src/enums' +import { submit } from 'src/iframes' +import { Tokenize } from './../tokenize/tokenize' +import * as iframesModule from 'src/iframes' import { - formElementsMock, - handleFormMock, - malgaConfigurations, -} from 'tests/mocks/common-configurations' -import { generateForm } from 'tests/mocks/form-dom' -import { Malga } from 'src/common/malga' -import { Tokenize } from './tokenize' - -vi.mock('src/common/malga', async (importOriginal) => { - const Malga = await importOriginal() - return { - ...Malga, - tokenization: vi.fn(), - } -}) + handleSetupIframeInDOM, + handleRemoveIframe, + handleCreateMessageEventMock, + configurationsSDK, +} from 'tests/mocks' + +describe('tokenize', () => { + let iframe: HTMLIFrameElement + let contentWindowMock: Window + + beforeEach(() => { + contentWindowMock = { + postMessage: vi.fn(), + addEventListener: vi.fn(), + } as unknown as Window + + iframe = handleSetupIframeInDOM('card-number', contentWindowMock) + }) + + afterEach(() => { + vi.clearAllMocks() + handleRemoveIframe(iframe) + }) + + test('should resolve with token data on successful message', async () => { + const tokenize = new Tokenize(configurationsSDK) + const promise = tokenize.handle() + const messageEvent = handleCreateMessageEventMock( + Event.Tokenize, + '623e25e1-9c40-442e-beaa-a9d7b735bdc1', + ) + global.dispatchEvent(messageEvent) + + const response = await promise + expect(response).toEqual('623e25e1-9c40-442e-beaa-a9d7b735bdc1') + expect(contentWindowMock.postMessage).toHaveBeenCalledTimes(1) + }) + + test('should handle error for undefined data', async () => { + const tokenize = new Tokenize(configurationsSDK) + + const promise = tokenize.handle() + + const messageEvent = handleCreateMessageEventMock( + Event.Tokenize, + undefined, + 'https://develop.d3krxmg1839vaa.amplifyapp.com', + ) + global.dispatchEvent(messageEvent) + + const response = await promise + expect(response).toEqual(undefined) + expect(contentWindowMock.postMessage).toHaveBeenCalledTimes(1) + }) + + test('should ignore messages from different origins', async () => { + const tokenize = new Tokenize(configurationsSDK) + const consoleErrorSpy = vi + .spyOn(console, 'error') + .mockImplementation(() => {}) + const promise = tokenize.handle() + + const messageEvent = handleCreateMessageEventMock( + Event.Tokenize, + '623e25e1-9c40-442e-beaa-a9d7b735bdc1', + 'https://wrong-origin.com', + ) + global.dispatchEvent(messageEvent) + + await new Promise((resolve) => setTimeout(resolve, 10)) + + expect(consoleErrorSpy).toHaveBeenCalledWith('Unauthorized') + expect(promise).not.resolves + expect(contentWindowMock.postMessage).toHaveBeenCalledTimes(1) + consoleErrorSpy.mockRestore() + }) + + test('should call submit with correct configurations', () => { + const submitSpy = vi.spyOn(iframesModule, 'submit') + new Tokenize(configurationsSDK).handle() + + expect(submitSpy).toHaveBeenCalledWith(configurationsSDK) + submitSpy.mockRestore() + }) + + test('should show error when iframeCardNumber is not found', () => { + const querySelectorSpy = vi.spyOn(document, 'querySelector') + querySelectorSpy.mockReturnValue(null) + + const consoleErrorSpy = vi.spyOn(console, 'error') + submit(configurationsSDK) + + expect(consoleErrorSpy).toHaveBeenCalledWith( + 'iframeCardNumber is null or has no contentWindow, cannot send postMessage', + ) -describe('Tokenize', () => { - describe('handle', () => { - beforeEach(() => { - document.body.innerHTML = '' - }) - - test('should be possible for a tokenId to exist when elements are passed correctly', async () => { - generateForm() - - const malga = new Malga(malgaConfigurations(false)) - - const tokenizeObject = new Tokenize(malga, formElementsMock) - const tokenId = await tokenizeObject.handle() - - expect(tokenId).toBeTruthy() - }) - test('should be possible to return a tokenId equal to sandbox-token-id when configurations include sandbox equal to true', async () => { - generateForm() - - const malga = new Malga(malgaConfigurations(true)) - - const tokenizeObject = new Tokenize(malga, formElementsMock) - - const tokenId = await tokenizeObject.handle() - expect(tokenId).toMatchObject({ tokenId: 'sandbox-token-id' }) - }) - test('should be possible to return a tokenId equal to sandbox-token-id when configurations include production equal to true', async () => { - generateForm() - - const malga = new Malga(malgaConfigurations(false)) - - const tokenizeObject = new Tokenize(malga, formElementsMock) - - const tokenId = await tokenizeObject.handle() - expect(tokenId).toMatchObject({ tokenId: 'production-token-id' }) - }) - test('should be possible to return error when elements are not passed correctly', async () => { - generateForm() - - const malga = new Malga(malgaConfigurations(false)) - - const elementsMock = { - form: 'jenjen', - holderName: 'le', - number: 'li', - expirationDate: 'lo', - cvv: 'lu', - } - const tokenizeObject = new Tokenize(malga, elementsMock) - - await expect(tokenizeObject.handle()).rejects.toThrowError( - "Cannot read properties of null (reading 'value')", - ) - }) - test('should be possible to return an error if the apiKey and clientId settings are empty', async () => { - generateForm() - - const malgaConfigurationsEmpty = { - apiKey: '', - clientId: '', - } - - const malga = new Malga(malgaConfigurationsEmpty) - - const tokenizeObject = new Tokenize(malga, formElementsMock) - - await expect(tokenizeObject.handle).rejects.toThrowError( - "Cannot read properties of undefined (reading 'elements')", - ) - }) - test('should be possible to return an error if the form inputs do not have values assigned', async () => { - const { - form, - holderNameInput, - cvvInput, - expirationDateInput, - numberInput, - } = handleFormMock() - - form.setAttribute(formElementsMock.form, '') - holderNameInput.setAttribute(formElementsMock.holderName, '') - numberInput.setAttribute(formElementsMock.number, '') - cvvInput.setAttribute(formElementsMock.cvv, '') - expirationDateInput.setAttribute(formElementsMock.expirationDate, '') - - document.body.appendChild(form) - form.appendChild(holderNameInput) - form.appendChild(numberInput) - form.appendChild(expirationDateInput) - form.appendChild(cvvInput) - - const malga = new Malga(malgaConfigurations(false)) - - const tokenizeObject = new Tokenize(malga, formElementsMock) - - await expect(tokenizeObject.handle()).rejects.toThrowError() - }) + querySelectorSpy.mockRestore() + consoleErrorSpy.mockRestore() }) }) diff --git a/src/tokenize/tokenize.ts b/src/tokenize/tokenize.ts index 1c6239e..9dfe132 100644 --- a/src/tokenize/tokenize.ts +++ b/src/tokenize/tokenize.ts @@ -1,6 +1,7 @@ -import { Event } from 'src/common/enums' -import type { MalgaConfigurations } from 'src/common/interfaces' -import { EventListener, submit } from 'src/common/utils' +import { Event } from 'src/enums' +import { EventListener } from 'src/events' +import { submit } from 'src/iframes' +import type { MalgaConfigurations } from 'src/interfaces' type TokenizeResponse = { tokenId: string diff --git a/src/utils/index.ts b/src/utils/index.ts new file mode 100644 index 0000000..35108c5 --- /dev/null +++ b/src/utils/index.ts @@ -0,0 +1,2 @@ +export * from './observer' +export * from './parsedString' diff --git a/src/common/utils/form-iframes/observer.ts b/src/utils/observer.ts similarity index 100% rename from src/common/utils/form-iframes/observer.ts rename to src/utils/observer.ts diff --git a/src/common/utils/form-iframes/parsedString.ts b/src/utils/parsedString.ts similarity index 100% rename from src/common/utils/form-iframes/parsedString.ts rename to src/utils/parsedString.ts diff --git a/tests/mocks/common-configurations.ts b/tests/mocks/common-configurations.ts index 4a36d7b..4804567 100644 --- a/tests/mocks/common-configurations.ts +++ b/tests/mocks/common-configurations.ts @@ -1,46 +1,44 @@ -import { MalgaFormElements } from 'src/common/interfaces/form' +import type { MalgaConfigurations } from 'src/interfaces' -export function malgaConfigurations(isSandbox: boolean) { - return { - apiKey: '17a64c8f-a387-4682-bdd8-d280493715e0', - clientId: 'd1d2b51a-0446-432a-b055-034518c2660e', - options: { - sandbox: isSandbox, +export const configurationsSDK: MalgaConfigurations = { + clientId: 'test-client-id', + apiKey: 'test-api-key', + options: { + config: { + fields: { + cardNumber: { + container: '#card-number', + placeholder: '9999 9999 9999 9999', + type: 'text', + }, + cardHolderName: { + container: '#card-holder-name', + placeholder: 'Its a test', + type: 'text', + }, + cardExpirationDate: { + container: '#card-expiration-date', + placeholder: 'MM/YY', + type: 'text', + }, + cardCvv: { + container: '#card-cvv', + placeholder: '999', + type: 'text', + }, + }, }, - } + sandbox: true, + }, } -export function configureFormSubmissionMock(eventSubmit?: any) { - eventSubmit - ? (window.HTMLFormElement.prototype.submit = eventSubmit) - : (window.HTMLFormElement.prototype.submit = () => {}) - const onSubmit = vi.fn() - onSubmit.mockImplementation((event) => { - event.preventDefault() - }) -} - -export const formValuesMock = { - holderName: 'Taylor Swift', - number: '5173000265860114', - expirationDate: '05/08/2024', - cvv: '114', -} - -export function handleFormMock() { - return { - form: document.createElement('form'), - holderNameInput: document.createElement('input'), - numberInput: document.createElement('input'), - expirationDateInput: document.createElement('input'), - cvvInput: document.createElement('input'), - } -} - -export const formElementsMock: MalgaFormElements = { - form: 'data-malga-tokenization-form', - holderName: 'data-malga-tokenization-holder-name', - number: 'data-malga-tokenization-number', - expirationDate: 'data-malga-tokenization-expiration-date', - cvv: 'data-malga-tokenization-cvv', +export const configurationWithSubmitData = { + type: 'submit', + data: { + authorizationData: { + clientId: 'test-client-id', + apiKey: 'test-api-key', + }, + sandbox: true, + }, } diff --git a/tests/mocks/events.ts b/tests/mocks/events.ts new file mode 100644 index 0000000..f60f789 --- /dev/null +++ b/tests/mocks/events.ts @@ -0,0 +1,26 @@ +export function handleCreateMockEvent(type: string, origin?: string) { + const eventMocked = { + origin: origin ?? 'https://develop.d3krxmg1839vaa.amplifyapp.com', + data: { + type: type, + data: { + fieldType: 'card-number', + }, + }, + } + + return eventMocked +} + +export function handleCreateMessageEventMock( + type: string, + tokenId?: string, + origin?: string, +) { + const messageEvent = new MessageEvent('message', { + origin: origin ?? 'https://develop.d3krxmg1839vaa.amplifyapp.com', + data: { type: type, data: tokenId }, + }) + + return messageEvent +} diff --git a/tests/mocks/form-dom.ts b/tests/mocks/form-dom.ts deleted file mode 100644 index f7dda88..0000000 --- a/tests/mocks/form-dom.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { - formElementsMock, - formValuesMock, - handleFormMock, -} from './common-configurations' - -export function generateForm(onSubmit?: any) { - const { form, holderNameInput, cvvInput, expirationDateInput, numberInput } = - handleFormMock() - - form.setAttribute(formElementsMock.form, '') - form.onsubmit = onSubmit - form.id = 'form' - form.method = 'POST' - form.action = '/test' - - holderNameInput.setAttribute(formElementsMock.holderName, '') - numberInput.setAttribute(formElementsMock.number, '') - cvvInput.setAttribute(formElementsMock.cvv, '') - expirationDateInput.setAttribute(formElementsMock.expirationDate, '') - - document.body.appendChild(form) - form.appendChild(holderNameInput) - form.appendChild(numberInput) - form.appendChild(expirationDateInput) - form.appendChild(cvvInput) - - const inputs = document.querySelectorAll('input') - inputs[0].value = formValuesMock.holderName - inputs[1].value = formValuesMock.number - inputs[2].value = formValuesMock.expirationDate - inputs[3].value = formValuesMock.cvv -} - -export function generateFormEmptyValues(onSubmit?: any) { - const { form, holderNameInput, cvvInput, expirationDateInput, numberInput } = - handleFormMock() - - form.setAttribute(formElementsMock.form, '') - form.onsubmit = onSubmit - form.id = 'form' - form.method = 'POST' - form.action = '/test' - - holderNameInput.setAttribute(formElementsMock.holderName, '') - numberInput.setAttribute(formElementsMock.number, '') - cvvInput.setAttribute(formElementsMock.cvv, '') - expirationDateInput.setAttribute(formElementsMock.expirationDate, '') - - document.body.appendChild(form) - form.appendChild(holderNameInput) - form.appendChild(numberInput) - form.appendChild(expirationDateInput) - form.appendChild(cvvInput) -} diff --git a/tests/mocks/iframe-dom.ts b/tests/mocks/iframe-dom.ts new file mode 100644 index 0000000..afdc83a --- /dev/null +++ b/tests/mocks/iframe-dom.ts @@ -0,0 +1,23 @@ +export function handleSetupIframeInDOM( + iframeName: string, + mockValue: any, +): HTMLIFrameElement { + const iframe = document.createElement('iframe') + iframe.name = iframeName + document.body.appendChild(iframe) + + Object.defineProperty(iframe, 'contentWindow', { + value: mockValue, + writable: true, + }) + + return iframe +} + +export function handleRemoveIframe(iframe: HTMLIFrameElement) { + if (document.body.contains(iframe)) { + document.body.removeChild(iframe) + } + + return +} diff --git a/tests/mocks/index.ts b/tests/mocks/index.ts index 5a774a6..b7180f3 100644 --- a/tests/mocks/index.ts +++ b/tests/mocks/index.ts @@ -1 +1,3 @@ -export * from './setup' +export * from './common-configurations' +export * from './iframe-dom' +export * from './events' diff --git a/tests/mocks/request-handlers.ts b/tests/mocks/request-handlers.ts deleted file mode 100644 index 929b190..0000000 --- a/tests/mocks/request-handlers.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { http, HttpResponse, PathParams } from 'msw' -import { MalgaCreateTokenResponse } from 'src/common/interfaces/malga' - -export const handlers = [ - http.post('https://sandbox-api.malga.io/v1/tokens', () => { - return HttpResponse.json({ tokenId: 'sandbox-token-id' }) - }), - http.post( - 'https://api.malga.io/v1/tokens', - async ({ request }) => { - const apiKey = request.headers.get('X-Api-Key') - const clientId = request.headers.get('X-Client-Id') - - const data = await request.json() - - const cardHolderName = data.cardHolderName - const cardNumber = data.cardNumber - const cardExpirationDate = data.cardExpirationDate - const cardCvv = data.cardCvv - - if (!cardHolderName || !cardNumber || !cardExpirationDate || !cardCvv) { - return new HttpResponse({ message: 'invalid card number' } as any, { - status: 400, - }) - } - - if (!apiKey && !clientId) { - return new HttpResponse({ message: 'Forbidden' } as any, { - status: 403, - }) - } - - return HttpResponse.json({ tokenId: 'production-token-id' }) - }, - ), -] diff --git a/tests/mocks/setup.ts b/tests/mocks/setup.ts deleted file mode 100644 index 9052ecd..0000000 --- a/tests/mocks/setup.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { setupServer } from 'msw/node' -import { handlers } from './request-handlers' - -export const server = setupServer(...handlers) diff --git a/tests/setupTests.ts b/tests/setupTests.ts index 5390180..74e0532 100644 --- a/tests/setupTests.ts +++ b/tests/setupTests.ts @@ -1,13 +1,7 @@ import '@testing-library/jest-dom' import '@testing-library/jest-dom/vitest' -import { afterEach, afterAll, beforeAll, expect } from 'vitest' +import { expect } from 'vitest' import * as matchers from '@testing-library/jest-dom/matchers' -import { server } from './mocks' - -beforeAll(() => server.listen()) -afterEach(() => server.resetHandlers()) -afterAll(() => server.close()) - expect.extend(matchers)