From db5efb4cc3333e8d7f53d48cedba3b3c230ca750 Mon Sep 17 00:00:00 2001 From: Bill Randall Date: Tue, 13 Aug 2024 10:49:09 -0400 Subject: [PATCH 01/11] Update the key to the enterprise one --- src/common/app.config.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/common/app.config.js b/src/common/app.config.js index a5b3be9c0..09441e42a 100644 --- a/src/common/app.config.js +++ b/src/common/app.config.js @@ -54,7 +54,7 @@ export const appConfig = /* @ngInject */ function (envServiceProvider, $compileP publicCru: 'https://stage.cru.org', publicGive: 'https://give-stage2.cru.org', acsUrl: 'https://cru-mkt-stage1.adobe-campaign.com/lp/LP63?_uuid=f1938f90-38ea-41a6-baad-9ac133f6d2ec&service=%404k83N_C5RZnLNvwz7waA2SwyzIuP6ATcN8vJjmT5km0iZPYKUUYk54sthkZjj-hltAuOKDYocuEi5Pxv8BSICoA4uppcvU_STKCzjv9RzLpE4hqj&pkey=', - recaptchaKey: '6LdNz5UlAAAAAPSrzydROuY76yGVIquVQAup69PO' + recaptchaKey: '6LduSiQqAAAAAOLA7NEU8-3-mdCmBKEUCwaFQuJF' }, devcloud: { apiUrl: 'https://give-stage2.cru.org', @@ -63,7 +63,7 @@ export const appConfig = /* @ngInject */ function (envServiceProvider, $compileP publicCru: 'https://stage-cloud.cru.org', publicGive: 'https://give-dev-cloud.cru.org', acsUrl: 'https://cru-mkt-stage1.adobe-campaign.com/lp/LP63?_uuid=f1938f90-38ea-41a6-baad-9ac133f6d2ec&service=%404k83N_C5RZnLNvwz7waA2SwyzIuP6ATcN8vJjmT5km0iZPYKUUYk54sthkZjj-hltAuOKDYocuEi5Pxv8BSICoA4uppcvU_STKCzjv9RzLpE4hqj&pkey=', - recaptchaKey: '6LdNz5UlAAAAAPSrzydROuY76yGVIquVQAup69PO' + recaptchaKey: '6LduSiQqAAAAAOLA7NEU8-3-mdCmBKEUCwaFQuJF' }, stagecloud: { apiUrl: 'https://give-stage-cloud.cru.org', @@ -72,7 +72,7 @@ export const appConfig = /* @ngInject */ function (envServiceProvider, $compileP publicCru: 'https://stage-cloud.cru.org', publicGive: 'https://give-stage-cloud.cru.org', acsUrl: 'https://cru-mkt-stage1.adobe-campaign.com/lp/LP63?_uuid=f1938f90-38ea-41a6-baad-9ac133f6d2ec&service=%404k83N_C5RZnLNvwz7waA2SwyzIuP6ATcN8vJjmT5km0iZPYKUUYk54sthkZjj-hltAuOKDYocuEi5Pxv8BSICoA4uppcvU_STKCzjv9RzLpE4hqj&pkey=', - recaptchaKey: '6LdNz5UlAAAAAPSrzydROuY76yGVIquVQAup69PO' + recaptchaKey: '6LduSiQqAAAAAOLA7NEU8-3-mdCmBKEUCwaFQuJF' }, prodcloud: { apiUrl: 'https://give-prod-cloud.cru.org', @@ -81,7 +81,7 @@ export const appConfig = /* @ngInject */ function (envServiceProvider, $compileP publicCru: 'https://www.cru.org', publicGive: 'https://give-prod-cloud.cru.org', acsUrl: 'https://cru-mkt-prod1-m.adobe-campaign.com/lp/LPEmailPrefCenter?_uuid=8831d67a-0d46-406b-8987-fd07c97c4ca7&service=%400fAlW4GPmxXExp8qlx7HDlAM6FSZUd0yYRlQg6HRsO_kglfi0gs650oHPZX6LrOvg7OHoIWWpobOeGZduxdNU_m5alc&pkey=', - recaptchaKey: '6LdNz5UlAAAAAPSrzydROuY76yGVIquVQAup69PO' + recaptchaKey: '6LduSiQqAAAAAOLA7NEU8-3-mdCmBKEUCwaFQuJF' }, staging: { apiUrl: 'https://give-stage2.cru.org', @@ -90,7 +90,7 @@ export const appConfig = /* @ngInject */ function (envServiceProvider, $compileP publicCru: 'https://stage.cru.org', publicGive: 'https://give-stage2.cru.org', acsUrl: 'https://cru-mkt-stage1.adobe-campaign.com/lp/LP63?_uuid=f1938f90-38ea-41a6-baad-9ac133f6d2ec&service=%404k83N_C5RZnLNvwz7waA2SwyzIuP6ATcN8vJjmT5km0iZPYKUUYk54sthkZjj-hltAuOKDYocuEi5Pxv8BSICoA4uppcvU_STKCzjv9RzLpE4hqj&pkey=', - recaptchaKey: '6LdNz5UlAAAAAPSrzydROuY76yGVIquVQAup69PO' + recaptchaKey: '6LduSiQqAAAAAOLA7NEU8-3-mdCmBKEUCwaFQuJF' }, nonprod: { apiUrl: 'https://give-stage2-next.cru.org', @@ -116,7 +116,7 @@ export const appConfig = /* @ngInject */ function (envServiceProvider, $compileP publicCru: 'https://www.cru.org', publicGive: 'https://give.cru.org', acsUrl: 'https://cru-mkt-prod1-m.adobe-campaign.com/lp/LPEmailPrefCenter?_uuid=8831d67a-0d46-406b-8987-fd07c97c4ca7&service=%400fAlW4GPmxXExp8qlx7HDlAM6FSZUd0yYRlQg6HRsO_kglfi0gs650oHPZX6LrOvg7OHoIWWpobOeGZduxdNU_m5alc&pkey=', - recaptchaKey: '6LdNz5UlAAAAAPSrzydROuY76yGVIquVQAup69PO' + recaptchaKey: '6LduSiQqAAAAAOLA7NEU8-3-mdCmBKEUCwaFQuJF' }, defaults: { isCheckout: false, From a73c8a70538effebe74acf9dd778698fd05cfed3 Mon Sep 17 00:00:00 2001 From: Bill Randall Date: Tue, 13 Aug 2024 10:50:48 -0400 Subject: [PATCH 02/11] Update the scripts to use enterprise --- src/common/components/Recaptcha/Recaptcha.test.tsx | 8 +++++--- src/common/components/Recaptcha/Recaptcha.tsx | 4 ++-- src/common/components/Recaptcha/RecaptchaWrapper.tsx | 2 +- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/src/common/components/Recaptcha/Recaptcha.test.tsx b/src/common/components/Recaptcha/Recaptcha.test.tsx index 5a1a124d6..7f8fffdcf 100644 --- a/src/common/components/Recaptcha/Recaptcha.test.tsx +++ b/src/common/components/Recaptcha/Recaptcha.test.tsx @@ -15,8 +15,10 @@ jest.mock('@datadog/browser-rum', () => { let mockExecuteRecaptcha = jest.fn() const mockRecaptchaReady = jest.fn() const mockRecaptcha = { - ready: mockRecaptchaReady, - execute: mockExecuteRecaptcha + enterprise: { + ready: mockRecaptchaReady, + execute: mockExecuteRecaptcha + } } const onSuccess = jest.fn() const onFailure = jest.fn() @@ -177,7 +179,7 @@ describe('Recaptcha component', () => { it('should skip the recaptcha call', async () => { //@ts-ignore - global.window.grecaptcha = { ready: mockRecaptchaReady } + global.window.grecaptcha = { enterprise: { ready: mockRecaptchaReady }} onSuccess.mockImplementation(() => console.log('fail')) diff --git a/src/common/components/Recaptcha/Recaptcha.tsx b/src/common/components/Recaptcha/Recaptcha.tsx index adbd4c847..1f575ad51 100644 --- a/src/common/components/Recaptcha/Recaptcha.tsx +++ b/src/common/components/Recaptcha/Recaptcha.tsx @@ -77,9 +77,9 @@ export const Recaptcha = ({ return } - grecaptcha.ready(async () => { + grecaptcha.enterprise.ready(async () => { try { - const token = await grecaptcha.execute(recaptchaKey, { action: action }) + const token = await grecaptcha.enterprise.execute(recaptchaKey, { action: action }) const serverResponse = await fetch(`${apiUrl}/recaptcha/verify`, { method: 'POST', body: JSON.stringify({ token: token }), diff --git a/src/common/components/Recaptcha/RecaptchaWrapper.tsx b/src/common/components/Recaptcha/RecaptchaWrapper.tsx index 05a863846..91afb5138 100644 --- a/src/common/components/Recaptcha/RecaptchaWrapper.tsx +++ b/src/common/components/Recaptcha/RecaptchaWrapper.tsx @@ -45,7 +45,7 @@ export const RecaptchaWrapper = ({ useMemo(() => { const script = document.createElement('script') - script.src = `https://www.google.com/recaptcha/api.js?render=${recaptchaKey}` + script.src = `https://www.google.com/recaptcha/enterprise.js?render=${recaptchaKey}` script.id = 'give-checkout-recaptcha' document.body.appendChild(script) }, []) From c85fdfce82683c57b2a76664bbe089209d9ff3df Mon Sep 17 00:00:00 2001 From: Bill Randall Date: Fri, 8 Nov 2024 10:05:28 -0500 Subject: [PATCH 03/11] Send recaptchaToken to cortex gateway --- .../components/Recaptcha/Recaptcha.test.tsx | 11 ++++ src/common/components/Recaptcha/Recaptcha.tsx | 1 + src/common/services/api/order.service.js | 2 + src/common/services/api/order.service.spec.js | 50 ++++++++++++++++--- 4 files changed, 57 insertions(+), 7 deletions(-) diff --git a/src/common/components/Recaptcha/Recaptcha.test.tsx b/src/common/components/Recaptcha/Recaptcha.test.tsx index 7f8fffdcf..29a5ff6b6 100644 --- a/src/common/components/Recaptcha/Recaptcha.test.tsx +++ b/src/common/components/Recaptcha/Recaptcha.test.tsx @@ -67,6 +67,17 @@ describe('Recaptcha component', () => { await waitFor(() => expect((recaptchaEnabledButton as HTMLButtonElement).disabled).toEqual(false)) }) + it('should store the recaptcha token', async () => { + const { getByRole } = render( + buildRecaptcha() + ) + + await userEvent.click(getByRole('button')) + await waitFor(() => { + expect(window.sessionStorage.getItem('recaptchaToken')).toEqual('token') + }) + }) + it('should successfully pass the recaptcha', async () => { //@ts-ignore global.fetch = jest.fn(() => { diff --git a/src/common/components/Recaptcha/Recaptcha.tsx b/src/common/components/Recaptcha/Recaptcha.tsx index 1f575ad51..0dfed1682 100644 --- a/src/common/components/Recaptcha/Recaptcha.tsx +++ b/src/common/components/Recaptcha/Recaptcha.tsx @@ -80,6 +80,7 @@ export const Recaptcha = ({ grecaptcha.enterprise.ready(async () => { try { const token = await grecaptcha.enterprise.execute(recaptchaKey, { action: action }) + window.sessionStorage.setItem('recaptchaToken', token) const serverResponse = await fetch(`${apiUrl}/recaptcha/verify`, { method: 'POST', body: JSON.stringify({ token: token }), diff --git a/src/common/services/api/order.service.js b/src/common/services/api/order.service.js index 964c40d74..46b8213e0 100644 --- a/src/common/services/api/order.service.js +++ b/src/common/services/api/order.service.js @@ -308,6 +308,7 @@ class Order { postData['cover-cc-fees'] = !!this.retrieveCoverFeeDecision() postData['radio-call-letters'] = this.retrieveRadioStationCallLetters() postData['tsys-device'] = this.tsysService.getDevice() + postData['recaptcha-token'] = this.sessionStorage.getItem('recaptchaToken') return this.cortexApiService.post({ path: this.hateoasHelperService.getLink(data.enhancedpurchaseform, 'submitenhancedpurchaseaction'), data: postData, @@ -317,6 +318,7 @@ class Order { .do((data) => { this.storeLastPurchaseLink(data.self.uri) this.cartService.setCartCountCookie(0) + this.sessionStorage.removeItem('recaptchaToken') }) } diff --git a/src/common/services/api/order.service.spec.js b/src/common/services/api/order.service.spec.js index 3072792d4..80eb02975 100644 --- a/src/common/services/api/order.service.spec.js +++ b/src/common/services/api/order.service.spec.js @@ -855,7 +855,7 @@ describe('order service', () => { it('should send a request to finalize the purchase', (done) => { self.$httpBackend.expectPOST( 'https://give-stage2.cru.org/cortex/enhancedpurchases/orders/crugive/me3gkzrrmm4dillegq4tiljugmztillbmq4weljqga3wezrwmq3tozjwmu=?FollowLocation=true', - { 'cover-cc-fees': false, 'radio-call-letters': null, 'tsys-device': '' } + { 'cover-cc-fees': false, 'radio-call-letters': null, 'tsys-device': '', 'recaptcha-token': null } ).respond(200, purchaseResponse) self.orderService.submit() @@ -870,7 +870,7 @@ describe('order service', () => { it('should send a request to finalize the purchase and with a CVV', (done) => { self.$httpBackend.expectPOST( 'https://give-stage2.cru.org/cortex/enhancedpurchases/orders/crugive/me3gkzrrmm4dillegq4tiljugmztillbmq4weljqga3wezrwmq3tozjwmu=?FollowLocation=true', - { 'security-code': '123', 'cover-cc-fees': false, 'radio-call-letters': null, 'tsys-device': '' } + { 'security-code': '123', 'cover-cc-fees': false, 'radio-call-letters': null, 'tsys-device': '', 'recaptcha-token': null } ).respond(200, purchaseResponse) self.orderService.submit('123') @@ -887,7 +887,7 @@ describe('order service', () => { self.$httpBackend.expectPOST( 'https://give-stage2.cru.org/cortex/enhancedpurchases/orders/crugive/me3gkzrrmm4dillegq4tiljugmztillbmq4weljqga3wezrwmq3tozjwmu=?FollowLocation=true', - { 'cover-cc-fees': true, 'radio-call-letters': null, 'tsys-device': '' } + { 'cover-cc-fees': true, 'radio-call-letters': null, 'tsys-device': '', 'recaptcha-token': null } ).respond(200, purchaseResponse) self.orderService.submit() @@ -904,7 +904,7 @@ describe('order service', () => { self.$httpBackend.expectPOST( 'https://give-stage2.cru.org/cortex/enhancedpurchases/orders/crugive/me3gkzrrmm4dillegq4tiljugmztillbmq4weljqga3wezrwmq3tozjwmu=?FollowLocation=true', - { 'cover-cc-fees': false, 'radio-call-letters': 'WXYZ', 'tsys-device': '' } + { 'cover-cc-fees': false, 'radio-call-letters': 'WXYZ', 'tsys-device': '', 'recaptcha-token': null } ).respond(200, purchaseResponse) self.orderService.submit() @@ -921,7 +921,7 @@ describe('order service', () => { self.$httpBackend.expectPOST( 'https://give-stage2.cru.org/cortex/enhancedpurchases/orders/crugive/me3gkzrrmm4dillegq4tiljugmztillbmq4weljqga3wezrwmq3tozjwmu=?FollowLocation=true', - { 'cover-cc-fees': false, 'radio-call-letters': null, 'tsys-device': 'test-env' } + { 'cover-cc-fees': false, 'radio-call-letters': null, 'tsys-device': 'test-env', 'recaptcha-token': null } ).respond(200, purchaseResponse) self.orderService.submit() @@ -938,7 +938,7 @@ describe('order service', () => { self.$httpBackend.expectPOST( 'https://give-stage2.cru.org/cortex/enhancedpurchases/orders/crugive/me3gkzrrmm4dillegq4tiljugmztillbmq4weljqga3wezrwmq3tozjwmu=?FollowLocation=true', - { 'cover-cc-fees': false, 'radio-call-letters': null, 'tsys-device':'' } + { 'cover-cc-fees': false, 'radio-call-letters': null, 'tsys-device':'', 'recaptcha-token': null } ).respond(200, purchaseResponse) self.orderService.submit() @@ -955,7 +955,7 @@ describe('order service', () => { self.$httpBackend.expectPOST( 'https://give-stage2.cru.org/cortex/enhancedpurchases/orders/crugive/me3gkzrrmm4dillegq4tiljugmztillbmq4weljqga3wezrwmq3tozjwmu=?FollowLocation=true', - { 'cover-cc-fees': false, 'radio-call-letters': null, 'tsys-device':'' } + { 'cover-cc-fees': false, 'radio-call-letters': null, 'tsys-device':'', 'recaptcha-token': null } ).respond(200, purchaseResponse) self.orderService.submit() @@ -966,6 +966,42 @@ describe('order service', () => { self.$httpBackend.flush() }) + + it('should send the recaptcha data to the server', (done) => { + const token = 'token' + self.$window.sessionStorage.setItem('recaptchaToken', token) + + self.$httpBackend.expectPOST( + 'https://give-stage2.cru.org/cortex/enhancedpurchases/orders/crugive/me3gkzrrmm4dillegq4tiljugmztillbmq4weljqga3wezrwmq3tozjwmu=?FollowLocation=true', + { 'cover-cc-fees': false, 'radio-call-letters': null, 'tsys-device':'', 'recaptcha-token': token } + ).respond(200, purchaseResponse) + + self.orderService.submit() + .subscribe((data) => { + expect(data).toEqual(purchaseResponse) + done() + }) + + self.$httpBackend.flush() + }) + + it('should clear the recaptcha data from session storage', (done) => { + const token = 'token' + self.$window.sessionStorage.setItem('recaptchaToken', token) + + self.$httpBackend.expectPOST( + 'https://give-stage2.cru.org/cortex/enhancedpurchases/orders/crugive/me3gkzrrmm4dillegq4tiljugmztillbmq4weljqga3wezrwmq3tozjwmu=?FollowLocation=true', + { 'cover-cc-fees': false, 'radio-call-letters': null, 'tsys-device':'', 'recaptcha-token': token } + ).respond(200, purchaseResponse) + + self.orderService.submit() + .subscribe((data) => { + expect(self.$window.sessionStorage.getItem('recaptchaToken')).toEqual(null) + done() + }) + + self.$httpBackend.flush() + }) }) describe('storeCardSecurityCode', () => { From 8743e58517ded398b8039eeb55eb5bc29505d127 Mon Sep 17 00:00:00 2001 From: Bill Randall Date: Fri, 8 Nov 2024 10:06:54 -0500 Subject: [PATCH 04/11] Remove the call to AEM, this will be handled by cortex gateway --- .../cart-summary/cart-summary.component.js | 5 - .../cart-summary/cart-summary.spec.js | 8 - .../cart-summary/cart-summary.tpl.html | 1 - src/app/checkout/step-3/step-3.component.js | 17 +- .../checkout/step-3/step-3.component.spec.js | 22 -- src/app/checkout/step-3/step-3.tpl.html | 1 - .../components/Recaptcha/Recaptcha.test.tsx | 201 +----------------- src/common/components/Recaptcha/Recaptcha.tsx | 52 +---- .../Recaptcha/RecaptchaWrapper.test.tsx | 2 - .../components/Recaptcha/RecaptchaWrapper.tsx | 8 +- 10 files changed, 8 insertions(+), 309 deletions(-) diff --git a/src/app/checkout/cart-summary/cart-summary.component.js b/src/app/checkout/cart-summary/cart-summary.component.js index 087f0ba97..ffe6ab1e0 100644 --- a/src/app/checkout/cart-summary/cart-summary.component.js +++ b/src/app/checkout/cart-summary/cart-summary.component.js @@ -8,7 +8,6 @@ import template from './cart-summary.tpl.html' const componentName = 'checkoutCartSummary' -export const recaptchaFailedEvent = 'recaptchaFailedEvent' export const submitOrderEvent = 'submitOrderEvent' class CartSummaryController { @@ -22,10 +21,6 @@ class CartSummaryController { return this.cartService.buildCartUrl() } - handleRecaptchaFailure (componentInstance) { - componentInstance.$rootScope.$emit(recaptchaFailedEvent) - } - onSubmit (componentInstance) { componentInstance.$rootScope.$emit(submitOrderEvent) } diff --git a/src/app/checkout/cart-summary/cart-summary.spec.js b/src/app/checkout/cart-summary/cart-summary.spec.js index 5adf7ad7b..e5b1106b4 100644 --- a/src/app/checkout/cart-summary/cart-summary.spec.js +++ b/src/app/checkout/cart-summary/cart-summary.spec.js @@ -36,13 +36,5 @@ describe('checkout', function () { expect(componentInstance.$rootScope.$emit).toHaveBeenCalledWith(submitOrderEvent) }) }) - - describe('handleRecaptchaFailure', () => { - it('should emit an event', () => { - jest.spyOn(componentInstance.$rootScope, '$emit').mockImplementation(() => {}) - self.controller.handleRecaptchaFailure(componentInstance) - expect(componentInstance.$rootScope.$emit).toHaveBeenCalledWith(recaptchaFailedEvent) - }) - }) }) }) diff --git a/src/app/checkout/cart-summary/cart-summary.tpl.html b/src/app/checkout/cart-summary/cart-summary.tpl.html index 0c1d3b086..47f2b1cf1 100644 --- a/src/app/checkout/cart-summary/cart-summary.tpl.html +++ b/src/app/checkout/cart-summary/cart-summary.tpl.html @@ -32,7 +32,6 @@

Cart Summary

ng-if="$ctrl.showSubmitBtn" action="'submit_gift'" on-success="$ctrl.onSubmit" - on-failure="$ctrl.handleRecaptchaFailure" component-instance="$ctrl.componentReference" button-id="'submitGiftButton'" button-type="'submit'" diff --git a/src/app/checkout/step-3/step-3.component.js b/src/app/checkout/step-3/step-3.component.js index 904aeb406..21fb26041 100644 --- a/src/app/checkout/step-3/step-3.component.js +++ b/src/app/checkout/step-3/step-3.component.js @@ -21,7 +21,7 @@ import { datadogRum } from '@datadog/browser-rum' import template from './step-3.tpl.html' import analyticsFactory from 'app/analytics/analytics.factory' -import { recaptchaFailedEvent, submitOrderEvent } from 'app/checkout/cart-summary/cart-summary.component' +import { submitOrderEvent } from 'app/checkout/cart-summary/cart-summary.component' const componentName = 'checkoutStep3' @@ -46,9 +46,6 @@ class Step3Controller { this.$onInit() }) - this.$rootScope.$on(recaptchaFailedEvent, () => { - this.handleRecaptchaFailure(this) - }) this.$rootScope.$on(submitOrderEvent, () => { this.submitOrder() }) @@ -183,18 +180,6 @@ class Step3Controller { componentInstance.$window.scrollTo(0, 0) }) } - - handleRecaptchaFailure (componentInstance) { - componentInstance.analyticsFactory.checkoutFieldError('submitOrder', 'failed') - componentInstance.submittingOrder = false - componentInstance.onSubmittingOrder({ value: false }) - - componentInstance.loadCart() - - componentInstance.onSubmitted() - componentInstance.submissionError = 'generic error' - componentInstance.$window.scrollTo(0, 0) - } } export default angular diff --git a/src/app/checkout/step-3/step-3.component.spec.js b/src/app/checkout/step-3/step-3.component.spec.js index e40f0d1ca..51ea39e11 100644 --- a/src/app/checkout/step-3/step-3.component.spec.js +++ b/src/app/checkout/step-3/step-3.component.spec.js @@ -448,34 +448,12 @@ describe('checkout', () => { }) }) - describe('handleRecaptchaFailure', () => { - it('should show an error if recaptcha fails', () => { - const componentInstance = self.controller - jest.spyOn(componentInstance.analyticsFactory, 'checkoutFieldError').mockImplementation(() => {}) - self.controller.handleRecaptchaFailure(componentInstance) - - expect(componentInstance.analyticsFactory.checkoutFieldError).toHaveBeenCalledWith('submitOrder', 'failed') - expect(componentInstance.submittingOrder).toEqual(false) - expect(componentInstance.onSubmittingOrder).toHaveBeenCalledWith({ value: false }) - expect(componentInstance.loadCart).toHaveBeenCalled() - expect(componentInstance.onSubmitted).toHaveBeenCalled() - expect(componentInstance.submissionError).toEqual('generic error') - expect(componentInstance.$window.scrollTo).toHaveBeenCalledWith(0, 0) - }) - }) - describe('event handling', () => { it('should call submit order if the submitOrderEvent is received', () => { jest.spyOn(self.controller, 'submitOrder').mockImplementation(() => {}) self.controller.$rootScope.$emit(submitOrderEvent) expect(self.controller.submitOrder).toHaveBeenCalled() }) - - it('should call handleRecaptchaFailure if the recaptchaFailedEvent is received', () => { - jest.spyOn(self.controller, 'handleRecaptchaFailure').mockImplementation(() => {}) - self.controller.$rootScope.$emit(recaptchaFailedEvent) - expect(self.controller.handleRecaptchaFailure).toHaveBeenCalledWith(self.controller) - }) }) }) }) diff --git a/src/app/checkout/step-3/step-3.tpl.html b/src/app/checkout/step-3/step-3.tpl.html index 6e62128de..59194aab1 100644 --- a/src/app/checkout/step-3/step-3.tpl.html +++ b/src/app/checkout/step-3/step-3.tpl.html @@ -205,7 +205,6 @@ { const $translate = { @@ -35,16 +34,15 @@ describe('Recaptcha component', () => { beforeEach(() => { global.window.grecaptcha = mockRecaptcha + onSuccess.mockImplementation(() => console.log('success')) $translate.instant.mockImplementation((input) => input) mockExecuteRecaptcha.mockImplementation(() => Promise.resolve('token')) mockRecaptchaReady.mockImplementation((callback) => { callback() }) onSuccess.mockClear() - onFailure.mockClear() }) it('should render', () => { - onSuccess.mockImplementation(() => console.log('success')) const { getAllByRole } = render( buildRecaptcha() ) @@ -79,15 +77,6 @@ describe('Recaptcha component', () => { }) it('should successfully pass the recaptcha', async () => { - //@ts-ignore - global.fetch = jest.fn(() => { - return Promise.resolve({ - json: () => Promise.resolve({ success: true, score: 0.9, action: 'submit_gift' }) - }) - }) - - onSuccess.mockImplementation(() => console.log('success')) - const { getByRole } = render( buildRecaptcha() ) @@ -95,96 +84,6 @@ describe('Recaptcha component', () => { await userEvent.click(getByRole('button')) await waitFor(() => { expect(onSuccess).toHaveBeenCalledTimes(1) - expect(global.fetch).toHaveBeenCalledWith('https://give-stage2.cru.org/recaptcha/verify', expect.anything()) - }) - }) - - it('should successfully pass the recaptcha on branded checkout', async () => { - //@ts-ignore - global.fetch = jest.fn(() => { - return Promise.resolve({ - json: () => Promise.resolve({ success: true, score: 0.6, action: 'branded_submit' }) - }) - }) - - onSuccess.mockImplementation(() => console.log('success')) - - const { getByRole } = render( - buildRecaptcha() - ) - - await userEvent.click(getByRole('button')) - await waitFor(() => { - expect(onSuccess).toHaveBeenCalledTimes(1) - expect(global.fetch).toHaveBeenCalledWith('https://give-stage2.cru.org/recaptcha/verify', expect.anything()) - }) - }) - - it('should log a warning due to low score', async () => { - //@ts-ignore - global.fetch = jest.fn(() => { - return Promise.resolve({ - json: () => Promise.resolve({ success: true, score: 0.2, action: 'submit_gift' }) - }) - }) - - onFailure.mockImplementation(() => console.log('warning')) - - const { getByRole } = render( - buildRecaptcha() - ) - - await userEvent.click(getByRole('button')) - await waitFor(() => { - const errorMessage = 'Captcha score was below the threshold: 0.2' - expect($log.warn).toHaveBeenCalledWith(errorMessage) - expect(datadogRum.addError).toHaveBeenCalledWith(new Error(`Error submitting purchase: ${errorMessage}`), { context: 'Recaptcha', errorCode: 'lowScore' }) - expect(onFailure).toHaveBeenCalledTimes(1) - }) - }) - - it('should fail the recaptcha call', async () => { - //@ts-ignore - global.fetch = jest.fn(() => { - return Promise.resolve({ - json: () => Promise.resolve({ success: false, error: 'some error', action: 'submit_gift' }) - }) - }) - - onFailure.mockImplementation(() => console.log('fail')) - - const { getByRole } = render( - buildRecaptcha() - ) - - await userEvent.click(getByRole('button')) - await waitFor(() => { - expect(onSuccess).toHaveBeenCalled() - expect(onFailure).not.toHaveBeenCalled() - }) - }) - - it('should call the fail function when not a valid action', async () => { - //@ts-ignore - global.fetch = jest.fn(() => { - return Promise.resolve({ - json: () => Promise.resolve({ success: true, action: 'read', score: 0.9 }) - }) - }) - - onSuccess.mockImplementation(() => console.log('fail')) - - const { getByRole } = render( - buildRecaptcha() - ) - - await userEvent.click(getByRole('button')) - await waitFor(() => { - const errorMessage = 'Invalid action: read' - expect(onSuccess).not.toHaveBeenCalled() - expect(onFailure).toHaveBeenCalled() - expect($log.warn).toHaveBeenCalledWith(errorMessage) - expect(datadogRum.addError).toHaveBeenCalledWith(new Error(`Error submitting purchase: ${errorMessage}`), { context: 'Recaptcha', errorCode: 'invalidAction' }) }) }) @@ -201,16 +100,11 @@ describe('Recaptcha component', () => { await userEvent.click(getByRole('button')) await waitFor(() => { expect(onSuccess).toHaveBeenCalled() - expect(onFailure).not.toHaveBeenCalled() }) }) it('should not block the gift if something went wrong with recaptcha', async () => { - //@ts-ignore - global.fetch = jest.fn(() => { - return Promise.reject('Failed') - }) - + mockExecuteRecaptcha.mockImplementationOnce(() => Promise.reject(('Failed'))) onSuccess.mockImplementation(() => console.log('success after error')) const { getByRole } = render( @@ -220,104 +114,14 @@ describe('Recaptcha component', () => { await userEvent.click(getByRole('button')) await waitFor(() => { expect(onSuccess).toHaveBeenCalledTimes(1) - expect(onFailure).not.toHaveBeenCalled() expect($log.error).toHaveBeenCalledWith('Failed to verify recaptcha, continuing on: Failed') }) }) - it('should not block the gift if something went wrong with recaptcha JSON', async () => { - //@ts-ignore - global.fetch = jest.fn(() => { - return Promise.resolve({ - json: () => Promise.reject('Failed') - }) - }) - - onSuccess.mockImplementation(() => console.log('success after JSON error')) - - const { getByRole } = render( - buildRecaptcha() - ) - - await userEvent.click(getByRole('button')) - await waitFor(() => { - expect(onSuccess).toHaveBeenCalledTimes(1) - expect(onFailure).not.toHaveBeenCalled() - expect($log.error).toHaveBeenCalledWith(`Failed to verify recaptcha, continuing on: Failed`) - }) - }) - - it('should not block gifts if data is empty', async () => { - //@ts-ignore - global.fetch = jest.fn(() => { - return Promise.resolve({ - json: () => Promise.resolve({}) - }) - }) - - onSuccess.mockImplementation(() => console.log('success after weird')) - - const { getByRole } = render( - buildRecaptcha() - ) - - await userEvent.click(getByRole('button')) - await waitFor(() => { - expect(onSuccess).toHaveBeenCalledTimes(1) - expect(onFailure).not.toHaveBeenCalled() - expect($log.warn).toHaveBeenCalledWith('Recaptcha returned an unusual response:', {}) - }) - }) - - it('should not block gifts if action is undefined', async () => { - //@ts-ignore - global.fetch = jest.fn(() => { - return Promise.resolve({ - json: () => Promise.resolve({ success: true, score: 0.9 }) - }) - }) - - onSuccess.mockImplementation(() => console.log('success after weird')) - - const { getByRole } = render( - buildRecaptcha() - ) - - await userEvent.click(getByRole('button')) - await waitFor(() => { - expect(onSuccess).toHaveBeenCalledTimes(1) - expect(onFailure).not.toHaveBeenCalled() - expect($log.warn).toHaveBeenCalledWith('Recaptcha returned an unusual response:', { success: true, score: 0.9 }) - }) - }) - - it('should not block gifts if score is undefined', async () => { - //@ts-ignore - global.fetch = jest.fn(() => { - return Promise.resolve({ - json: () => Promise.resolve({ success: true, action: 'submit_gift' }) - }) - }) - - onSuccess.mockImplementation(() => console.log('success after weird')) - - const { getByRole } = render( - buildRecaptcha() - ) - - await userEvent.click(getByRole('button')) - await waitFor(() => { - expect(onSuccess).toHaveBeenCalledTimes(1) - expect(onFailure).not.toHaveBeenCalled() - expect($log.warn).toHaveBeenCalledWith('Recaptcha returned an unusual response:', { success: true, action: 'submit_gift' }) - }) - }) - const buildRecaptcha = () => { return { $translate={$translate} $log={$log} recaptchaKey='key' - apiUrl={'https://give-stage2.cru.org'} /> } }) diff --git a/src/common/components/Recaptcha/Recaptcha.tsx b/src/common/components/Recaptcha/Recaptcha.tsx index 0dfed1682..43282a586 100644 --- a/src/common/components/Recaptcha/Recaptcha.tsx +++ b/src/common/components/Recaptcha/Recaptcha.tsx @@ -17,14 +17,9 @@ export enum ButtonType { Button = 'button' } -const isValidAction = (action: string): boolean => { - return action === 'submit_gift' || action === 'branded_submit' -} - interface RecaptchaProps { action: string onSuccess: (componentInstance: any) => void - onFailure: (componentInstance: any) => void componentInstance: any buttonId: string buttonType?: ButtonType @@ -32,15 +27,13 @@ interface RecaptchaProps { buttonDisabled: boolean buttonLabel: string $translate: any - $log: any, - recaptchaKey: string, - apiUrl: string, + $log: any + recaptchaKey: string } export const Recaptcha = ({ action, onSuccess, - onFailure, componentInstance, buttonId, buttonType, @@ -50,7 +43,6 @@ export const Recaptcha = ({ $translate, $log, recaptchaKey, - apiUrl }: RecaptchaProps): JSX.Element => { const [ready, setReady] = useState(false) @@ -81,41 +73,7 @@ export const Recaptcha = ({ try { const token = await grecaptcha.enterprise.execute(recaptchaKey, { action: action }) window.sessionStorage.setItem('recaptchaToken', token) - const serverResponse = await fetch(`${apiUrl}/recaptcha/verify`, { - method: 'POST', - body: JSON.stringify({ token: token }), - headers: { 'Content-Type': 'application/json' } - }) - const data = await serverResponse.json() - - if (!data || !data.score || !data.action) { - $log.warn('Recaptcha returned an unusual response:', data) - onSuccess(componentInstance) - return - } - - if (data?.success === true && isValidAction(data?.action)) { - if (data.score < 0.5) { - const errorMessage = `Captcha score was below the threshold: ${data.score}` - $log.warn(errorMessage) - datadogRum.addError(new Error(`Error submitting purchase: ${errorMessage}`), { context: 'Recaptcha', errorCode: 'lowScore' }) - onFailure(componentInstance) - return - } - onSuccess(componentInstance) - return - } - if (data?.success === false && isValidAction(data?.action)) { - $log.warn('Recaptcha call was unsuccessful, continuing anyway') - onSuccess(componentInstance) - return - } - if (!isValidAction(data?.action)) { - const errorMessage = `Invalid action: ${data?.action}` - $log.warn(errorMessage) - datadogRum.addError(new Error(`Error submitting purchase: ${errorMessage}`), { context: 'Recaptcha', errorCode: 'invalidAction' }) - onFailure(componentInstance) - } + onSuccess(componentInstance) } catch (error) { $log.error(`Failed to verify recaptcha, continuing on: ${error}`) onSuccess(componentInstance) @@ -141,15 +99,13 @@ export default angular [ 'action', 'onSuccess', - 'onFailure', 'componentInstance', 'buttonId', 'buttonType', 'buttonClasses', 'buttonDisabled', 'buttonLabel', - 'recaptchaKey', - 'apiUrl' + 'recaptchaKey' ], ['$translate', '$log'])) diff --git a/src/common/components/Recaptcha/RecaptchaWrapper.test.tsx b/src/common/components/Recaptcha/RecaptchaWrapper.test.tsx index 6e962f040..102514b4c 100644 --- a/src/common/components/Recaptcha/RecaptchaWrapper.test.tsx +++ b/src/common/components/Recaptcha/RecaptchaWrapper.test.tsx @@ -38,7 +38,6 @@ describe('RecaptchaWrapper component', () => { { void - onFailure: (componentInstance: any) => void componentInstance: any buttonId: string buttonType?: ButtonType @@ -29,7 +28,6 @@ interface RecaptchaWrapperProps { export const RecaptchaWrapper = ({ action, onSuccess, - onFailure, componentInstance, buttonId, buttonType, @@ -41,7 +39,6 @@ export const RecaptchaWrapper = ({ $log }: RecaptchaWrapperProps): JSX.Element => { const recaptchaKey = envService.read('recaptchaKey') - const apiUrl = envService.read('apiUrl') useMemo(() => { const script = document.createElement('script') @@ -53,7 +50,6 @@ export const RecaptchaWrapper = ({ return ( + recaptchaKey={recaptchaKey}> ) } @@ -76,7 +71,6 @@ export default angular [ 'action', 'onSuccess', - 'onFailure', 'componentInstance', 'buttonId', 'buttonType', From 8050097d1c0423af9a258f558265b06a19240a4f Mon Sep 17 00:00:00 2001 From: Bill Randall Date: Fri, 8 Nov 2024 11:29:40 -0500 Subject: [PATCH 05/11] Send the action to CG so it can compare and make sure it matches the server response --- .../components/Recaptcha/Recaptcha.test.tsx | 3 ++- src/common/components/Recaptcha/Recaptcha.tsx | 1 + src/common/services/api/order.service.js | 2 ++ src/common/services/api/order.service.spec.js | 23 +++++++++++-------- 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/common/components/Recaptcha/Recaptcha.test.tsx b/src/common/components/Recaptcha/Recaptcha.test.tsx index f92ba8523..d54d64849 100644 --- a/src/common/components/Recaptcha/Recaptcha.test.tsx +++ b/src/common/components/Recaptcha/Recaptcha.test.tsx @@ -65,7 +65,7 @@ describe('Recaptcha component', () => { await waitFor(() => expect((recaptchaEnabledButton as HTMLButtonElement).disabled).toEqual(false)) }) - it('should store the recaptcha token', async () => { + it('should store the recaptcha token and action', async () => { const { getByRole } = render( buildRecaptcha() ) @@ -73,6 +73,7 @@ describe('Recaptcha component', () => { await userEvent.click(getByRole('button')) await waitFor(() => { expect(window.sessionStorage.getItem('recaptchaToken')).toEqual('token') + expect(window.sessionStorage.getItem('recaptchaAction')).toEqual('submit_gift') }) }) diff --git a/src/common/components/Recaptcha/Recaptcha.tsx b/src/common/components/Recaptcha/Recaptcha.tsx index 43282a586..fdde5f27f 100644 --- a/src/common/components/Recaptcha/Recaptcha.tsx +++ b/src/common/components/Recaptcha/Recaptcha.tsx @@ -73,6 +73,7 @@ export const Recaptcha = ({ try { const token = await grecaptcha.enterprise.execute(recaptchaKey, { action: action }) window.sessionStorage.setItem('recaptchaToken', token) + window.sessionStorage.setItem('recaptchaAction', action) onSuccess(componentInstance) } catch (error) { $log.error(`Failed to verify recaptcha, continuing on: ${error}`) diff --git a/src/common/services/api/order.service.js b/src/common/services/api/order.service.js index 46b8213e0..ce83ccad7 100644 --- a/src/common/services/api/order.service.js +++ b/src/common/services/api/order.service.js @@ -309,6 +309,7 @@ class Order { postData['radio-call-letters'] = this.retrieveRadioStationCallLetters() postData['tsys-device'] = this.tsysService.getDevice() postData['recaptcha-token'] = this.sessionStorage.getItem('recaptchaToken') + postData['recaptcha-action'] = this.sessionStorage.getItem('recaptchaAction') return this.cortexApiService.post({ path: this.hateoasHelperService.getLink(data.enhancedpurchaseform, 'submitenhancedpurchaseaction'), data: postData, @@ -319,6 +320,7 @@ class Order { this.storeLastPurchaseLink(data.self.uri) this.cartService.setCartCountCookie(0) this.sessionStorage.removeItem('recaptchaToken') + this.sessionStorage.removeItem('recaptchaAction') }) } diff --git a/src/common/services/api/order.service.spec.js b/src/common/services/api/order.service.spec.js index 80eb02975..4b9fcbbf2 100644 --- a/src/common/services/api/order.service.spec.js +++ b/src/common/services/api/order.service.spec.js @@ -855,7 +855,7 @@ describe('order service', () => { it('should send a request to finalize the purchase', (done) => { self.$httpBackend.expectPOST( 'https://give-stage2.cru.org/cortex/enhancedpurchases/orders/crugive/me3gkzrrmm4dillegq4tiljugmztillbmq4weljqga3wezrwmq3tozjwmu=?FollowLocation=true', - { 'cover-cc-fees': false, 'radio-call-letters': null, 'tsys-device': '', 'recaptcha-token': null } + { 'cover-cc-fees': false, 'radio-call-letters': null, 'tsys-device': '', 'recaptcha-token': null, 'recaptcha-action': null } ).respond(200, purchaseResponse) self.orderService.submit() @@ -870,7 +870,7 @@ describe('order service', () => { it('should send a request to finalize the purchase and with a CVV', (done) => { self.$httpBackend.expectPOST( 'https://give-stage2.cru.org/cortex/enhancedpurchases/orders/crugive/me3gkzrrmm4dillegq4tiljugmztillbmq4weljqga3wezrwmq3tozjwmu=?FollowLocation=true', - { 'security-code': '123', 'cover-cc-fees': false, 'radio-call-letters': null, 'tsys-device': '', 'recaptcha-token': null } + { 'security-code': '123', 'cover-cc-fees': false, 'radio-call-letters': null, 'tsys-device': '', 'recaptcha-token': null, 'recaptcha-action': null } ).respond(200, purchaseResponse) self.orderService.submit('123') @@ -887,7 +887,7 @@ describe('order service', () => { self.$httpBackend.expectPOST( 'https://give-stage2.cru.org/cortex/enhancedpurchases/orders/crugive/me3gkzrrmm4dillegq4tiljugmztillbmq4weljqga3wezrwmq3tozjwmu=?FollowLocation=true', - { 'cover-cc-fees': true, 'radio-call-letters': null, 'tsys-device': '', 'recaptcha-token': null } + { 'cover-cc-fees': true, 'radio-call-letters': null, 'tsys-device': '', 'recaptcha-token': null, 'recaptcha-action': null } ).respond(200, purchaseResponse) self.orderService.submit() @@ -904,7 +904,7 @@ describe('order service', () => { self.$httpBackend.expectPOST( 'https://give-stage2.cru.org/cortex/enhancedpurchases/orders/crugive/me3gkzrrmm4dillegq4tiljugmztillbmq4weljqga3wezrwmq3tozjwmu=?FollowLocation=true', - { 'cover-cc-fees': false, 'radio-call-letters': 'WXYZ', 'tsys-device': '', 'recaptcha-token': null } + { 'cover-cc-fees': false, 'radio-call-letters': 'WXYZ', 'tsys-device': '', 'recaptcha-token': null, 'recaptcha-action': null } ).respond(200, purchaseResponse) self.orderService.submit() @@ -921,7 +921,7 @@ describe('order service', () => { self.$httpBackend.expectPOST( 'https://give-stage2.cru.org/cortex/enhancedpurchases/orders/crugive/me3gkzrrmm4dillegq4tiljugmztillbmq4weljqga3wezrwmq3tozjwmu=?FollowLocation=true', - { 'cover-cc-fees': false, 'radio-call-letters': null, 'tsys-device': 'test-env', 'recaptcha-token': null } + { 'cover-cc-fees': false, 'radio-call-letters': null, 'tsys-device': 'test-env', 'recaptcha-token': null, 'recaptcha-action': null } ).respond(200, purchaseResponse) self.orderService.submit() @@ -938,7 +938,7 @@ describe('order service', () => { self.$httpBackend.expectPOST( 'https://give-stage2.cru.org/cortex/enhancedpurchases/orders/crugive/me3gkzrrmm4dillegq4tiljugmztillbmq4weljqga3wezrwmq3tozjwmu=?FollowLocation=true', - { 'cover-cc-fees': false, 'radio-call-letters': null, 'tsys-device':'', 'recaptcha-token': null } + { 'cover-cc-fees': false, 'radio-call-letters': null, 'tsys-device':'', 'recaptcha-token': null, 'recaptcha-action': null } ).respond(200, purchaseResponse) self.orderService.submit() @@ -955,7 +955,7 @@ describe('order service', () => { self.$httpBackend.expectPOST( 'https://give-stage2.cru.org/cortex/enhancedpurchases/orders/crugive/me3gkzrrmm4dillegq4tiljugmztillbmq4weljqga3wezrwmq3tozjwmu=?FollowLocation=true', - { 'cover-cc-fees': false, 'radio-call-letters': null, 'tsys-device':'', 'recaptcha-token': null } + { 'cover-cc-fees': false, 'radio-call-letters': null, 'tsys-device':'', 'recaptcha-token': null, 'recaptcha-action': null } ).respond(200, purchaseResponse) self.orderService.submit() @@ -969,11 +969,13 @@ describe('order service', () => { it('should send the recaptcha data to the server', (done) => { const token = 'token' + const action = 'action' self.$window.sessionStorage.setItem('recaptchaToken', token) + self.$window.sessionStorage.setItem('recaptchaAction', action) self.$httpBackend.expectPOST( 'https://give-stage2.cru.org/cortex/enhancedpurchases/orders/crugive/me3gkzrrmm4dillegq4tiljugmztillbmq4weljqga3wezrwmq3tozjwmu=?FollowLocation=true', - { 'cover-cc-fees': false, 'radio-call-letters': null, 'tsys-device':'', 'recaptcha-token': token } + { 'cover-cc-fees': false, 'radio-call-letters': null, 'tsys-device':'', 'recaptcha-token': token, 'recaptcha-action': action } ).respond(200, purchaseResponse) self.orderService.submit() @@ -987,16 +989,19 @@ describe('order service', () => { it('should clear the recaptcha data from session storage', (done) => { const token = 'token' + const action = 'action' self.$window.sessionStorage.setItem('recaptchaToken', token) + self.$window.sessionStorage.setItem('recaptchaAction', action) self.$httpBackend.expectPOST( 'https://give-stage2.cru.org/cortex/enhancedpurchases/orders/crugive/me3gkzrrmm4dillegq4tiljugmztillbmq4weljqga3wezrwmq3tozjwmu=?FollowLocation=true', - { 'cover-cc-fees': false, 'radio-call-letters': null, 'tsys-device':'', 'recaptcha-token': token } + { 'cover-cc-fees': false, 'radio-call-letters': null, 'tsys-device':'', 'recaptcha-token': token, 'recaptcha-action': action } ).respond(200, purchaseResponse) self.orderService.submit() .subscribe((data) => { expect(self.$window.sessionStorage.getItem('recaptchaToken')).toEqual(null) + expect(self.$window.sessionStorage.getItem('recaptchaAction')).toEqual(null) done() }) From d3ae9c0723348ea283cddcdea68c6a1fb8b02aa1 Mon Sep 17 00:00:00 2001 From: Bill Randall Date: Mon, 18 Nov 2024 16:27:30 -0500 Subject: [PATCH 06/11] Use new actions to match up with enterprise --- src/app/checkout/cart-summary/cart-summary.tpl.html | 2 +- src/app/checkout/step-3/step-3.tpl.html | 2 +- src/common/components/Recaptcha/Recaptcha.test.tsx | 4 ++-- src/common/components/Recaptcha/RecaptchaWrapper.test.tsx | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/app/checkout/cart-summary/cart-summary.tpl.html b/src/app/checkout/cart-summary/cart-summary.tpl.html index 47f2b1cf1..00e43211e 100644 --- a/src/app/checkout/cart-summary/cart-summary.tpl.html +++ b/src/app/checkout/cart-summary/cart-summary.tpl.html @@ -30,7 +30,7 @@

Cart Summary

Edit Cart
{ await userEvent.click(getByRole('button')) await waitFor(() => { expect(window.sessionStorage.getItem('recaptchaToken')).toEqual('token') - expect(window.sessionStorage.getItem('recaptchaAction')).toEqual('submit_gift') + expect(window.sessionStorage.getItem('recaptchaAction')).toEqual('checkout') }) }) @@ -121,7 +121,7 @@ describe('Recaptcha component', () => { const buildRecaptcha = () => { return { const onSuccess = jest.fn(() => console.log('success')) const { getAllByRole } = render( { document.body.appendChild(script) render( Date: Thu, 21 Nov 2024 16:37:42 -0500 Subject: [PATCH 07/11] Update the non-prod recaptcha key --- src/common/app.config.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/common/app.config.js b/src/common/app.config.js index 09441e42a..fae3b4c51 100644 --- a/src/common/app.config.js +++ b/src/common/app.config.js @@ -54,7 +54,7 @@ export const appConfig = /* @ngInject */ function (envServiceProvider, $compileP publicCru: 'https://stage.cru.org', publicGive: 'https://give-stage2.cru.org', acsUrl: 'https://cru-mkt-stage1.adobe-campaign.com/lp/LP63?_uuid=f1938f90-38ea-41a6-baad-9ac133f6d2ec&service=%404k83N_C5RZnLNvwz7waA2SwyzIuP6ATcN8vJjmT5km0iZPYKUUYk54sthkZjj-hltAuOKDYocuEi5Pxv8BSICoA4uppcvU_STKCzjv9RzLpE4hqj&pkey=', - recaptchaKey: '6LduSiQqAAAAAOLA7NEU8-3-mdCmBKEUCwaFQuJF' + recaptchaKey: '6LcCMoYqAAAAABMoyLs5CyKWwE8qn_YslEaiRPRD' }, devcloud: { apiUrl: 'https://give-stage2.cru.org', @@ -63,7 +63,7 @@ export const appConfig = /* @ngInject */ function (envServiceProvider, $compileP publicCru: 'https://stage-cloud.cru.org', publicGive: 'https://give-dev-cloud.cru.org', acsUrl: 'https://cru-mkt-stage1.adobe-campaign.com/lp/LP63?_uuid=f1938f90-38ea-41a6-baad-9ac133f6d2ec&service=%404k83N_C5RZnLNvwz7waA2SwyzIuP6ATcN8vJjmT5km0iZPYKUUYk54sthkZjj-hltAuOKDYocuEi5Pxv8BSICoA4uppcvU_STKCzjv9RzLpE4hqj&pkey=', - recaptchaKey: '6LduSiQqAAAAAOLA7NEU8-3-mdCmBKEUCwaFQuJF' + recaptchaKey: '6LcCMoYqAAAAABMoyLs5CyKWwE8qn_YslEaiRPRD' }, stagecloud: { apiUrl: 'https://give-stage-cloud.cru.org', @@ -72,7 +72,7 @@ export const appConfig = /* @ngInject */ function (envServiceProvider, $compileP publicCru: 'https://stage-cloud.cru.org', publicGive: 'https://give-stage-cloud.cru.org', acsUrl: 'https://cru-mkt-stage1.adobe-campaign.com/lp/LP63?_uuid=f1938f90-38ea-41a6-baad-9ac133f6d2ec&service=%404k83N_C5RZnLNvwz7waA2SwyzIuP6ATcN8vJjmT5km0iZPYKUUYk54sthkZjj-hltAuOKDYocuEi5Pxv8BSICoA4uppcvU_STKCzjv9RzLpE4hqj&pkey=', - recaptchaKey: '6LduSiQqAAAAAOLA7NEU8-3-mdCmBKEUCwaFQuJF' + recaptchaKey: '6LcCMoYqAAAAABMoyLs5CyKWwE8qn_YslEaiRPRD' }, prodcloud: { apiUrl: 'https://give-prod-cloud.cru.org', @@ -90,7 +90,7 @@ export const appConfig = /* @ngInject */ function (envServiceProvider, $compileP publicCru: 'https://stage.cru.org', publicGive: 'https://give-stage2.cru.org', acsUrl: 'https://cru-mkt-stage1.adobe-campaign.com/lp/LP63?_uuid=f1938f90-38ea-41a6-baad-9ac133f6d2ec&service=%404k83N_C5RZnLNvwz7waA2SwyzIuP6ATcN8vJjmT5km0iZPYKUUYk54sthkZjj-hltAuOKDYocuEi5Pxv8BSICoA4uppcvU_STKCzjv9RzLpE4hqj&pkey=', - recaptchaKey: '6LduSiQqAAAAAOLA7NEU8-3-mdCmBKEUCwaFQuJF' + recaptchaKey: '6LcCMoYqAAAAABMoyLs5CyKWwE8qn_YslEaiRPRD' }, nonprod: { apiUrl: 'https://give-stage2-next.cru.org', From 84c201ebbd21f6bfeae3aea47c819138bfb0e0c0 Mon Sep 17 00:00:00 2001 From: Bill Randall Date: Fri, 6 Dec 2024 10:45:21 -0500 Subject: [PATCH 08/11] Only add the same Recaptcha script once (this useMemo runs twice on page load) --- .../Recaptcha/RecaptchaWrapper.test.tsx | 33 +++++++++++++++++-- .../components/Recaptcha/RecaptchaWrapper.tsx | 4 ++- 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/src/common/components/Recaptcha/RecaptchaWrapper.test.tsx b/src/common/components/Recaptcha/RecaptchaWrapper.test.tsx index 2c4232189..e638c875d 100644 --- a/src/common/components/Recaptcha/RecaptchaWrapper.test.tsx +++ b/src/common/components/Recaptcha/RecaptchaWrapper.test.tsx @@ -24,12 +24,19 @@ describe('RecaptchaWrapper component', () => { } const script = document.createElement('script') - script.src = 'https://www.google.com/recaptcha/api.js?render=123' - script.id = 'test-script' beforeEach(() => { $translate.instant.mockImplementation((input) => input) global.window.grecaptcha = mockRecaptcha + script.src = 'https://www.google.com/recaptcha/api.js?render=123' + script.id = 'test-script' + }) + + afterEach(() => { + const foundScript = document.getElementById('give-checkout-recaptcha') + if (foundScript) { + document.body.removeChild(foundScript) + } }) it('should render', () => { @@ -78,4 +85,26 @@ describe('RecaptchaWrapper component', () => { expect(document.getElementById('give-checkout-recaptcha')).not.toBeNull() expect(document.getElementById('test-script')).not.toBeNull() }) + + it('should only add this script once', () => { + script.id = 'give-checkout-recaptcha' + document.body.appendChild(script) + expect(document.getElementById('give-checkout-recaptcha')).not.toBeNull() + render( + + ) + expect(document.querySelectorAll('#give-checkout-recaptcha')).toHaveLength(1) + }) }) diff --git a/src/common/components/Recaptcha/RecaptchaWrapper.tsx b/src/common/components/Recaptcha/RecaptchaWrapper.tsx index 937676270..4464051c6 100644 --- a/src/common/components/Recaptcha/RecaptchaWrapper.tsx +++ b/src/common/components/Recaptcha/RecaptchaWrapper.tsx @@ -44,7 +44,9 @@ export const RecaptchaWrapper = ({ const script = document.createElement('script') script.src = `https://www.google.com/recaptcha/enterprise.js?render=${recaptchaKey}` script.id = 'give-checkout-recaptcha' - document.body.appendChild(script) + if (!document.getElementById(script.id)) { + document.body.appendChild(script) + } }, []) return ( From caf1823dbbcbb9b3ee09a6ee58023993eb0d7202 Mon Sep 17 00:00:00 2001 From: Bill Randall Date: Fri, 6 Dec 2024 11:45:24 -0500 Subject: [PATCH 09/11] Put the script in the head instead of the body --- src/common/components/Recaptcha/RecaptchaWrapper.test.tsx | 6 +++--- src/common/components/Recaptcha/RecaptchaWrapper.tsx | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/common/components/Recaptcha/RecaptchaWrapper.test.tsx b/src/common/components/Recaptcha/RecaptchaWrapper.test.tsx index e638c875d..43a48a39a 100644 --- a/src/common/components/Recaptcha/RecaptchaWrapper.test.tsx +++ b/src/common/components/Recaptcha/RecaptchaWrapper.test.tsx @@ -35,7 +35,7 @@ describe('RecaptchaWrapper component', () => { afterEach(() => { const foundScript = document.getElementById('give-checkout-recaptcha') if (foundScript) { - document.body.removeChild(foundScript) + document.head.removeChild(foundScript) } }) @@ -66,7 +66,7 @@ describe('RecaptchaWrapper component', () => { }) it('should add a script even if one already exists', () => { - document.body.appendChild(script) + document.head.appendChild(script) render( { it('should only add this script once', () => { script.id = 'give-checkout-recaptcha' - document.body.appendChild(script) + document.head.appendChild(script) expect(document.getElementById('give-checkout-recaptcha')).not.toBeNull() render( Date: Fri, 6 Dec 2024 14:27:10 -0500 Subject: [PATCH 10/11] Separate out the creation of the Recaptcha script so we can put it on pages that the action is not being run on --- src/app/branded/branded-checkout.component.js | 4 ++ src/app/branded/branded-checkout.spec.js | 10 +++- src/app/checkout/checkout.component.js | 4 ++ src/app/checkout/checkout.spec.js | 4 ++ .../Recaptcha/RecaptchaWrapper.test.tsx | 55 ------------------- .../components/Recaptcha/RecaptchaWrapper.tsx | 11 +--- .../checkoutHelpers/checkout.service.js | 8 +++ .../checkoutHelpers/checkout.service.spec.js | 42 ++++++++++++++ 8 files changed, 72 insertions(+), 66 deletions(-) create mode 100644 src/common/services/checkoutHelpers/checkout.service.js create mode 100644 src/common/services/checkoutHelpers/checkout.service.spec.js diff --git a/src/app/branded/branded-checkout.component.js b/src/app/branded/branded-checkout.component.js index 1b7366224..ee4262be8 100644 --- a/src/app/branded/branded-checkout.component.js +++ b/src/app/branded/branded-checkout.component.js @@ -13,6 +13,7 @@ import thankYouSummary from 'app/thankYou/summary/thankYouSummary.component' import sessionService from 'common/services/session/session.service' import orderService from 'common/services/api/order.service' +import * as checkoutService from 'common/services/checkoutHelpers/checkout.service' import brandedAnalyticsFactory from './analytics/branded-analytics.factory' import 'common/lib/fakeLocalStorage' @@ -33,6 +34,7 @@ class BrandedCheckoutController { this.envService = envService this.orderService = orderService this.$translate = $translate + this.checkoutService = checkoutService this.orderService.clearCoverFees() } @@ -57,6 +59,8 @@ class BrandedCheckoutController { console.error(err) }) this.$translate.use(this.language || 'en') + + this.checkoutService.initializeRecaptcha.call(this) } formatDonorDetails () { diff --git a/src/app/branded/branded-checkout.spec.js b/src/app/branded/branded-checkout.spec.js index 9f099e65c..5502e4370 100644 --- a/src/app/branded/branded-checkout.spec.js +++ b/src/app/branded/branded-checkout.spec.js @@ -50,7 +50,10 @@ describe('branded checkout', () => { onOrderCompleted: jest.fn(), onOrderFailed: jest.fn(), }, - ); + ) + $ctrl.checkoutService = { + initializeRecaptcha: jest.fn() + } })) describe('$onInit', () => { @@ -74,6 +77,11 @@ describe('branded checkout', () => { expect($ctrl.formatDonorDetails).toHaveBeenCalled() expect($ctrl.$window.sessionStorage.removeItem).toHaveBeenCalledWith('initialLoadComplete') }) + + it('should initialize recaptcha', () => { + $ctrl.$onInit() + expect($ctrl.checkoutService.initializeRecaptcha).toHaveBeenCalled() + }) }) describe('formatDonorDetails', () => { diff --git a/src/app/checkout/checkout.component.js b/src/app/checkout/checkout.component.js index dc5d11362..64c96abe5 100644 --- a/src/app/checkout/checkout.component.js +++ b/src/app/checkout/checkout.component.js @@ -14,6 +14,7 @@ import showErrors from 'common/filters/showErrors.filter' import cartService from 'common/services/api/cart.service' import orderService from 'common/services/api/order.service' import designationsService from 'common/services/api/designations.service' +import * as checkoutService from 'common/services/checkoutHelpers/checkout.service' import sessionEnforcerService, { EnforcerCallbacks } from 'common/services/session/sessionEnforcer.service' import { Roles, SignOutEvent } from 'common/services/session/session.service' @@ -40,6 +41,7 @@ class CheckoutController { this.loadingCartData = true this.analyticsFactory = analyticsFactory this.selfReference = this + this.checkoutService = checkoutService } $onInit () { @@ -56,6 +58,8 @@ class CheckoutController { this.initStepParam(true) this.listenForLocationChange() this.analyticsFactory.pageLoaded(true) + + this.checkoutService.initializeRecaptcha.call(this) } $onDestroy () { diff --git a/src/app/checkout/checkout.spec.js b/src/app/checkout/checkout.spec.js index b4dc203ec..716ce8be5 100644 --- a/src/app/checkout/checkout.spec.js +++ b/src/app/checkout/checkout.spec.js @@ -19,6 +19,9 @@ describe('checkout', function () { search: '' }, scrollTo: jest.fn() } }) + self.controller.checkoutService = { + initializeRecaptcha: jest.fn() + } })) it('to be defined', function () { @@ -53,6 +56,7 @@ describe('checkout', function () { self.controller.$rootScope.$on.mock.calls[0][1]() expect(self.controller.signedOut).toHaveBeenCalled() + expect(self.controller.checkoutService.initializeRecaptcha).toHaveBeenCalled() }) describe('sessionEnforcerService success', () => { diff --git a/src/common/components/Recaptcha/RecaptchaWrapper.test.tsx b/src/common/components/Recaptcha/RecaptchaWrapper.test.tsx index 43a48a39a..9b323650b 100644 --- a/src/common/components/Recaptcha/RecaptchaWrapper.test.tsx +++ b/src/common/components/Recaptcha/RecaptchaWrapper.test.tsx @@ -23,20 +23,9 @@ describe('RecaptchaWrapper component', () => { execute: mockExecuteRecaptcha } - const script = document.createElement('script') - beforeEach(() => { $translate.instant.mockImplementation((input) => input) global.window.grecaptcha = mockRecaptcha - script.src = 'https://www.google.com/recaptcha/api.js?render=123' - script.id = 'test-script' - }) - - afterEach(() => { - const foundScript = document.getElementById('give-checkout-recaptcha') - if (foundScript) { - document.head.removeChild(foundScript) - } }) it('should render', () => { @@ -62,49 +51,5 @@ describe('RecaptchaWrapper component', () => { expect(recaptchaEnabledButton.className).toEqual('btn') expect((recaptchaEnabledButton as HTMLButtonElement).disabled).toEqual(false) expect(recaptchaEnabledButton.innerHTML).toEqual('Label') - expect(document.getElementById('give-checkout-recaptcha')).not.toBeNull() - }) - - it('should add a script even if one already exists', () => { - document.head.appendChild(script) - render( - - ) - expect(document.getElementById('give-checkout-recaptcha')).not.toBeNull() - expect(document.getElementById('test-script')).not.toBeNull() - }) - - it('should only add this script once', () => { - script.id = 'give-checkout-recaptcha' - document.head.appendChild(script) - expect(document.getElementById('give-checkout-recaptcha')).not.toBeNull() - render( - - ) - expect(document.querySelectorAll('#give-checkout-recaptcha')).toHaveLength(1) }) }) diff --git a/src/common/components/Recaptcha/RecaptchaWrapper.tsx b/src/common/components/Recaptcha/RecaptchaWrapper.tsx index aba24e734..aecc6fd59 100644 --- a/src/common/components/Recaptcha/RecaptchaWrapper.tsx +++ b/src/common/components/Recaptcha/RecaptchaWrapper.tsx @@ -1,6 +1,6 @@ import angular from 'angular' import { react2angular } from 'react2angular' -import React, { useMemo } from 'react' +import React from 'react' import { ButtonType, Recaptcha } from './Recaptcha' const componentName = 'recaptchaWrapper' @@ -40,15 +40,6 @@ export const RecaptchaWrapper = ({ }: RecaptchaWrapperProps): JSX.Element => { const recaptchaKey = envService.read('recaptchaKey') - useMemo(() => { - const script = document.createElement('script') - script.src = `https://www.google.com/recaptcha/enterprise.js?render=${recaptchaKey}` - script.id = 'give-checkout-recaptcha' - if (!document.getElementById(script.id)) { - document.head.appendChild(script) - } - }, []) - return ( { + const $ctrl = { + $window: { + document: document + }, + envService: { + read: jest.fn() + } + } + const script = document.createElement('script') + + beforeEach(() => { + script.src = 'https://www.google.com/recaptcha/enterprise.js?render=123' + script.id = 'test-script' + $ctrl.envService.read.mockReturnValue('123') + }) + + afterEach(() => { + const foundScript = document.getElementById('give-checkout-recaptcha') + if (foundScript) { + document.head.removeChild(foundScript) + } + }) + + it('should add a script even if one already exists', () => { + document.head.appendChild(script) + checkoutService.initializeRecaptcha.call($ctrl) + expect(document.getElementById('give-checkout-recaptcha')).not.toBeNull() + expect(document.getElementById('test-script')).not.toBeNull() + }) + + it('should only add this script once', () => { + script.id = 'give-checkout-recaptcha' + document.head.appendChild(script) + expect(document.getElementById('give-checkout-recaptcha')).not.toBeNull() + checkoutService.initializeRecaptcha.call($ctrl) + expect(document.querySelectorAll('#give-checkout-recaptcha')).toHaveLength(1) + }) +}) + From 2dc3a443a4a99e93a76b44b2dcfbe3b631b5fa25 Mon Sep 17 00:00:00 2001 From: Bill Randall Date: Wed, 8 Jan 2025 17:26:10 -0500 Subject: [PATCH 11/11] Turn checkout service into an Angular service --- src/app/branded/branded-checkout.component.js | 7 +- src/app/branded/branded-checkout.spec.js | 7 +- src/app/checkout/checkout.component.js | 7 +- src/app/checkout/checkout.spec.js | 4 +- .../checkoutHelpers/checkout.service.js | 28 ++++-- .../checkoutHelpers/checkout.service.spec.js | 87 +++++++++++-------- 6 files changed, 85 insertions(+), 55 deletions(-) diff --git a/src/app/branded/branded-checkout.component.js b/src/app/branded/branded-checkout.component.js index ee4262be8..21664c110 100644 --- a/src/app/branded/branded-checkout.component.js +++ b/src/app/branded/branded-checkout.component.js @@ -13,7 +13,7 @@ import thankYouSummary from 'app/thankYou/summary/thankYouSummary.component' import sessionService from 'common/services/session/session.service' import orderService from 'common/services/api/order.service' -import * as checkoutService from 'common/services/checkoutHelpers/checkout.service' +import checkoutService from 'common/services/checkoutHelpers/checkout.service' import brandedAnalyticsFactory from './analytics/branded-analytics.factory' import 'common/lib/fakeLocalStorage' @@ -24,7 +24,7 @@ const componentName = 'brandedCheckout' class BrandedCheckoutController { /* @ngInject */ - constructor ($element, $window, analyticsFactory, brandedAnalyticsFactory, tsysService, sessionService, envService, orderService, $translate) { + constructor ($element, $window, analyticsFactory, brandedAnalyticsFactory, tsysService, sessionService, envService, orderService, checkoutService, $translate) { this.$element = $element[0] // extract the DOM element from the jqLite wrapper this.$window = $window this.analyticsFactory = analyticsFactory @@ -60,7 +60,7 @@ class BrandedCheckoutController { }) this.$translate.use(this.language || 'en') - this.checkoutService.initializeRecaptcha.call(this) + this.checkoutService.initializeRecaptcha() } formatDonorDetails () { @@ -156,6 +156,7 @@ export default angular thankYouSummary.name, sessionService.name, orderService.name, + checkoutService.name, brandedAnalyticsFactory.name, uibModal, 'environment', diff --git a/src/app/branded/branded-checkout.spec.js b/src/app/branded/branded-checkout.spec.js index 5502e4370..b4f4d2b76 100644 --- a/src/app/branded/branded-checkout.spec.js +++ b/src/app/branded/branded-checkout.spec.js @@ -51,12 +51,13 @@ describe('branded checkout', () => { onOrderFailed: jest.fn(), }, ) - $ctrl.checkoutService = { - initializeRecaptcha: jest.fn() - } })) describe('$onInit', () => { + beforeEach(() => { + jest.spyOn($ctrl.checkoutService, 'initializeRecaptcha').mockImplementation(() => {}) + }) + it('should set API Url if custom one is set', () => { $ctrl.apiUrl = 'https://custom-api.cru.org' $ctrl.$onInit() diff --git a/src/app/checkout/checkout.component.js b/src/app/checkout/checkout.component.js index 64c96abe5..9e238584d 100644 --- a/src/app/checkout/checkout.component.js +++ b/src/app/checkout/checkout.component.js @@ -14,7 +14,7 @@ import showErrors from 'common/filters/showErrors.filter' import cartService from 'common/services/api/cart.service' import orderService from 'common/services/api/order.service' import designationsService from 'common/services/api/designations.service' -import * as checkoutService from 'common/services/checkoutHelpers/checkout.service' +import checkoutService from 'common/services/checkoutHelpers/checkout.service' import sessionEnforcerService, { EnforcerCallbacks } from 'common/services/session/sessionEnforcer.service' import { Roles, SignOutEvent } from 'common/services/session/session.service' @@ -28,7 +28,7 @@ const componentName = 'checkout' class CheckoutController { /* @ngInject */ - constructor ($window, $location, $rootScope, $log, cartService, envService, orderService, designationsService, sessionEnforcerService, analyticsFactory) { + constructor ($window, $location, $rootScope, $log, cartService, envService, orderService, designationsService, sessionEnforcerService, checkoutService, analyticsFactory) { this.$log = $log this.$window = $window this.$location = $location @@ -59,7 +59,7 @@ class CheckoutController { this.listenForLocationChange() this.analyticsFactory.pageLoaded(true) - this.checkoutService.initializeRecaptcha.call(this) + this.checkoutService.initializeRecaptcha() } $onDestroy () { @@ -150,6 +150,7 @@ export default angular orderService.name, designationsService.name, sessionEnforcerService.name, + checkoutService.name, showErrors.name, analyticsFactory.name ]) diff --git a/src/app/checkout/checkout.spec.js b/src/app/checkout/checkout.spec.js index 716ce8be5..fcb7d38ed 100644 --- a/src/app/checkout/checkout.spec.js +++ b/src/app/checkout/checkout.spec.js @@ -19,9 +19,6 @@ describe('checkout', function () { search: '' }, scrollTo: jest.fn() } }) - self.controller.checkoutService = { - initializeRecaptcha: jest.fn() - } })) it('to be defined', function () { @@ -38,6 +35,7 @@ describe('checkout', function () { jest.spyOn(self.controller, 'sessionEnforcerService').mockImplementation(() => {}) jest.spyOn(self.controller.$rootScope, '$on').mockImplementation(() => {}) jest.spyOn(self.controller, 'signedOut').mockImplementation(() => {}) + jest.spyOn(self.controller.checkoutService, 'initializeRecaptcha').mockImplementation(() => {}) self.controller.$onInit() }) diff --git a/src/common/services/checkoutHelpers/checkout.service.js b/src/common/services/checkoutHelpers/checkout.service.js index 0efbe89d2..3e2729a67 100644 --- a/src/common/services/checkoutHelpers/checkout.service.js +++ b/src/common/services/checkoutHelpers/checkout.service.js @@ -1,8 +1,24 @@ -export function initializeRecaptcha () { - const script = this.$window.document.createElement('script') - script.src = `https://www.google.com/recaptcha/enterprise.js?render=${this.envService.read('recaptchaKey')}` - script.id = 'give-checkout-recaptcha' - if (!this.$window.document.getElementById(script.id)) { - this.$window.document.head.appendChild(script) +import angular from 'angular' + +const serviceName = 'checkoutService' + +class CheckoutService { + /* @ngInject */ + constructor ($window, envService) { + this.$window = $window + this.envService = envService + } + + initializeRecaptcha () { + const script = this.$window.document.createElement('script') + script.src = `https://www.google.com/recaptcha/enterprise.js?render=${this.envService.read('recaptchaKey')}` + script.id = 'give-checkout-recaptcha' + if (!this.$window.document.getElementById(script.id)) { + this.$window.document.head.appendChild(script) + } } } + +export default angular + .module(serviceName, []) + .service(serviceName, CheckoutService) diff --git a/src/common/services/checkoutHelpers/checkout.service.spec.js b/src/common/services/checkoutHelpers/checkout.service.spec.js index 883d2770c..8204e1bf5 100644 --- a/src/common/services/checkoutHelpers/checkout.service.spec.js +++ b/src/common/services/checkoutHelpers/checkout.service.spec.js @@ -1,42 +1,55 @@ -import * as checkoutService from './checkout.service' - -describe('initializeRecaptcha()', () => { - const $ctrl = { - $window: { - document: document - }, - envService: { - read: jest.fn() - } - } - const script = document.createElement('script') - - beforeEach(() => { - script.src = 'https://www.google.com/recaptcha/enterprise.js?render=123' - script.id = 'test-script' - $ctrl.envService.read.mockReturnValue('123') - }) +import angular from 'angular' +import 'angular-mocks' - afterEach(() => { - const foundScript = document.getElementById('give-checkout-recaptcha') - if (foundScript) { - document.head.removeChild(foundScript) - } - }) +import module from './checkout.service' - it('should add a script even if one already exists', () => { - document.head.appendChild(script) - checkoutService.initializeRecaptcha.call($ctrl) - expect(document.getElementById('give-checkout-recaptcha')).not.toBeNull() - expect(document.getElementById('test-script')).not.toBeNull() - }) +describe('checkout service', () => { + beforeEach(angular.mock.module(module.name)) + + beforeEach(angular.mock.module(($provide) => { + $provide.value('envService', { + read: () => '123' + }) + })) + + const self = {} + let script + + beforeEach(inject((checkoutService, envService, $window) => { + self.checkoutService = checkoutService + self.envService = envService + self.$window = $window + self.$window.document = document - it('should only add this script once', () => { - script.id = 'give-checkout-recaptcha' - document.head.appendChild(script) - expect(document.getElementById('give-checkout-recaptcha')).not.toBeNull() - checkoutService.initializeRecaptcha.call($ctrl) - expect(document.querySelectorAll('#give-checkout-recaptcha')).toHaveLength(1) + script = self.$window.document.createElement('script') + })) + + describe('initializeRecaptcha()', () => { + beforeEach(() => { + script.src = 'https://www.google.com/recaptcha/enterprise.js?render=123' + script.id = 'test-script' + }) + + afterEach(() => { + const foundScript = self.$window.document.getElementById('give-checkout-recaptcha') + if (foundScript) { + self.$window.document.head.removeChild(foundScript) + } + }) + + it('should add a script even if one already exists', () => { + self.$window.document.head.appendChild(script) + self.checkoutService.initializeRecaptcha() + expect(self.$window.document.getElementById('give-checkout-recaptcha')).not.toBeNull() + expect(self.$window.document.getElementById('test-script')).not.toBeNull() + }) + + it('should only add this script once', () => { + script.id = 'give-checkout-recaptcha' + self.$window.document.head.appendChild(script) + expect(self.$window.document.getElementById('give-checkout-recaptcha')).not.toBeNull() + self.checkoutService.initializeRecaptcha() + expect(self.$window.document.querySelectorAll('#give-checkout-recaptcha')).toHaveLength(1) + }) }) }) -