From 41e4e61baafb7b9a307cfba1b161614d33be8e95 Mon Sep 17 00:00:00 2001
From: Bill Randall <william.randall@cru.org>
Date: Sat, 30 Nov 2024 21:48:55 -0500
Subject: [PATCH 1/7] Temporarily disable recaptcha since we went over quota

---
 .../components/Recaptcha/Recaptcha.test.tsx   | 20 ++---
 src/common/components/Recaptcha/Recaptcha.tsx | 83 ++++++++++---------
 2 files changed, 52 insertions(+), 51 deletions(-)

diff --git a/src/common/components/Recaptcha/Recaptcha.test.tsx b/src/common/components/Recaptcha/Recaptcha.test.tsx
index cb7e1994a..614988c42 100644
--- a/src/common/components/Recaptcha/Recaptcha.test.tsx
+++ b/src/common/components/Recaptcha/Recaptcha.test.tsx
@@ -45,7 +45,7 @@ describe('Recaptcha component', () => {
     expect(recaptchaEnabledButton.innerHTML).toEqual('Label')
   })
 
-  it('should disable the button until ready', async () => {
+  xit('should disable the button until ready', async () => {
     global.window.grecaptcha = undefined
 
     const { getByRole } = render(buildRecaptcha())
@@ -56,7 +56,7 @@ describe('Recaptcha component', () => {
     await waitFor(() => expect((recaptchaEnabledButton as HTMLButtonElement).disabled).toEqual(false))
   })
 
-  it('should successfully pass the recaptcha', async () => {
+  xit('should successfully pass the recaptcha', async () => {
     //@ts-ignore
     global.fetch = jest.fn(() => {
       return Promise.resolve({
@@ -77,7 +77,7 @@ describe('Recaptcha component', () => {
     })
   })
 
-  it('should successfully pass the recaptcha on branded checkout', async () => {
+  xit('should successfully pass the recaptcha on branded checkout', async () => {
     //@ts-ignore
     global.fetch = jest.fn(() => {
       return Promise.resolve({
@@ -98,7 +98,7 @@ describe('Recaptcha component', () => {
     })
   })
 
-  it('should log a warning due to low score', async () => {
+  xit('should log a warning due to low score', async () => {
     //@ts-ignore
     global.fetch = jest.fn(() => {
       return Promise.resolve({
@@ -119,7 +119,7 @@ describe('Recaptcha component', () => {
     })
   })
 
-  it('should fail the recaptcha call', async () => {
+  xit('should fail the recaptcha call', async () => {
     //@ts-ignore
     global.fetch = jest.fn(() => {
       return Promise.resolve({
@@ -140,7 +140,7 @@ describe('Recaptcha component', () => {
     })
   })
 
-  it('should call the fail function when not a valid action', async () => {
+  xit('should call the fail function when not a valid action', async () => {
     //@ts-ignore
     global.fetch = jest.fn(() => {
       return Promise.resolve({
@@ -161,7 +161,7 @@ describe('Recaptcha component', () => {
     })
   })
 
-  it('should skip the recaptcha call', async () => {
+  xit('should skip the recaptcha call', async () => {
     //@ts-ignore
     global.window.grecaptcha = { ready: mockRecaptchaReady }
 
@@ -178,7 +178,7 @@ describe('Recaptcha component', () => {
     })
   })
 
-  it('should not block the gift if something went wrong with recaptcha', async () => {
+  xit('should not block the gift if something went wrong with recaptcha', async () => {
     //@ts-ignore
     global.fetch = jest.fn(() => {
       return Promise.reject('Failed')
@@ -198,7 +198,7 @@ describe('Recaptcha component', () => {
     })
   })
 
-  it('should not block the gift if something went wrong with recaptcha JSON', async () => {
+  xit('should not block the gift if something went wrong with recaptcha JSON', async () => {
     //@ts-ignore
     global.fetch = jest.fn(() => {
       return Promise.resolve({
@@ -220,7 +220,7 @@ describe('Recaptcha component', () => {
     })
   })
 
-  it('should not block gifts if something weird happens', async () => {
+  xit('should not block gifts if something weird happens', async () => {
     //@ts-ignore
     global.fetch = jest.fn(() => {
       return Promise.resolve({
diff --git a/src/common/components/Recaptcha/Recaptcha.tsx b/src/common/components/Recaptcha/Recaptcha.tsx
index 49f7163e3..22760f64a 100644
--- a/src/common/components/Recaptcha/Recaptcha.tsx
+++ b/src/common/components/Recaptcha/Recaptcha.tsx
@@ -71,49 +71,50 @@ export const Recaptcha = ({
   }, [grecaptcha, buttonId])
 
   const handleReCaptchaVerify = useCallback(async () => {
-    if (!ready) {
-      $log.info('Execute recaptcha not yet available')
-      return
-    }
+    // if (!ready) {
+    //   $log.info('Execute recaptcha not yet available')
+    //   return
+    // }
 
-    grecaptcha.ready(async () => {
-      try {
-        const token = await grecaptcha.execute(recaptchaKey, { action: action })
-        const serverResponse = await fetch(`${apiUrl}/recaptcha/verify`, {
-          method: 'POST',
-          body: JSON.stringify({ token: token }),
-          headers: { 'Content-Type': 'application/json' }
-        })
-        const data = await serverResponse.json()
+    // grecaptcha.ready(async () => {
+    //   try {
+    //     const token = await grecaptcha.execute(recaptchaKey, { action: action })
+    //     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?.success === true && isValidAction(data?.action)) {
-          if (data.score < 0.5) {
-            $log.warn(`Captcha score was below the threshold: ${data.score}`)
-            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 (!data) {
-          $log.warn('Data was missing!')
-          onSuccess(componentInstance)
-          return
-        }
-        if (!isValidAction(data?.action)) {
-          $log.warn(`Invalid action: ${data?.action}`)
-          onFailure(componentInstance)
-        }
-      } catch (error) {
-        $log.error(`Failed to verify recaptcha, continuing on: ${error}`)
-        onSuccess(componentInstance)
-      }
-    })
+    //     if (data?.success === true && isValidAction(data?.action)) {
+    //       if (data.score < 0.5) {
+    //         $log.warn(`Captcha score was below the threshold: ${data.score}`)
+    //         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 (!data) {
+    //       $log.warn('Data was missing!')
+    //       onSuccess(componentInstance)
+    //       return
+    //     }
+    //     if (!isValidAction(data?.action)) {
+    //       $log.warn(`Invalid action: ${data?.action}`)
+    //       onFailure(componentInstance)
+    //     }
+    //   } catch (error) {
+    //     $log.error(`Failed to verify recaptcha, continuing on: ${error}`)
+    //     onSuccess(componentInstance)
+    //   }
+    // })
+    onSuccess(componentInstance)
   }, [grecaptcha, buttonId, ready])
 
   return (

From 18e3bdc26069ff6a05512434b1ce857ad58b9c56 Mon Sep 17 00:00:00 2001
From: Bill Randall <william.randall@cru.org>
Date: Mon, 2 Dec 2024 10:21:07 -0500
Subject: [PATCH 2/7] Revert "Temporarily disable recaptcha since we went over
 quota"

This reverts commit 41e4e61baafb7b9a307cfba1b161614d33be8e95.
---
 .../components/Recaptcha/Recaptcha.test.tsx   | 20 ++---
 src/common/components/Recaptcha/Recaptcha.tsx | 83 +++++++++----------
 2 files changed, 51 insertions(+), 52 deletions(-)

diff --git a/src/common/components/Recaptcha/Recaptcha.test.tsx b/src/common/components/Recaptcha/Recaptcha.test.tsx
index 614988c42..cb7e1994a 100644
--- a/src/common/components/Recaptcha/Recaptcha.test.tsx
+++ b/src/common/components/Recaptcha/Recaptcha.test.tsx
@@ -45,7 +45,7 @@ describe('Recaptcha component', () => {
     expect(recaptchaEnabledButton.innerHTML).toEqual('Label')
   })
 
-  xit('should disable the button until ready', async () => {
+  it('should disable the button until ready', async () => {
     global.window.grecaptcha = undefined
 
     const { getByRole } = render(buildRecaptcha())
@@ -56,7 +56,7 @@ describe('Recaptcha component', () => {
     await waitFor(() => expect((recaptchaEnabledButton as HTMLButtonElement).disabled).toEqual(false))
   })
 
-  xit('should successfully pass the recaptcha', async () => {
+  it('should successfully pass the recaptcha', async () => {
     //@ts-ignore
     global.fetch = jest.fn(() => {
       return Promise.resolve({
@@ -77,7 +77,7 @@ describe('Recaptcha component', () => {
     })
   })
 
-  xit('should successfully pass the recaptcha on branded checkout', async () => {
+  it('should successfully pass the recaptcha on branded checkout', async () => {
     //@ts-ignore
     global.fetch = jest.fn(() => {
       return Promise.resolve({
@@ -98,7 +98,7 @@ describe('Recaptcha component', () => {
     })
   })
 
-  xit('should log a warning due to low score', async () => {
+  it('should log a warning due to low score', async () => {
     //@ts-ignore
     global.fetch = jest.fn(() => {
       return Promise.resolve({
@@ -119,7 +119,7 @@ describe('Recaptcha component', () => {
     })
   })
 
-  xit('should fail the recaptcha call', async () => {
+  it('should fail the recaptcha call', async () => {
     //@ts-ignore
     global.fetch = jest.fn(() => {
       return Promise.resolve({
@@ -140,7 +140,7 @@ describe('Recaptcha component', () => {
     })
   })
 
-  xit('should call the fail function when not a valid action', async () => {
+  it('should call the fail function when not a valid action', async () => {
     //@ts-ignore
     global.fetch = jest.fn(() => {
       return Promise.resolve({
@@ -161,7 +161,7 @@ describe('Recaptcha component', () => {
     })
   })
 
-  xit('should skip the recaptcha call', async () => {
+  it('should skip the recaptcha call', async () => {
     //@ts-ignore
     global.window.grecaptcha = { ready: mockRecaptchaReady }
 
@@ -178,7 +178,7 @@ describe('Recaptcha component', () => {
     })
   })
 
-  xit('should not block the gift if something went wrong with recaptcha', async () => {
+  it('should not block the gift if something went wrong with recaptcha', async () => {
     //@ts-ignore
     global.fetch = jest.fn(() => {
       return Promise.reject('Failed')
@@ -198,7 +198,7 @@ describe('Recaptcha component', () => {
     })
   })
 
-  xit('should not block the gift if something went wrong with recaptcha JSON', async () => {
+  it('should not block the gift if something went wrong with recaptcha JSON', async () => {
     //@ts-ignore
     global.fetch = jest.fn(() => {
       return Promise.resolve({
@@ -220,7 +220,7 @@ describe('Recaptcha component', () => {
     })
   })
 
-  xit('should not block gifts if something weird happens', async () => {
+  it('should not block gifts if something weird happens', async () => {
     //@ts-ignore
     global.fetch = jest.fn(() => {
       return Promise.resolve({
diff --git a/src/common/components/Recaptcha/Recaptcha.tsx b/src/common/components/Recaptcha/Recaptcha.tsx
index 22760f64a..49f7163e3 100644
--- a/src/common/components/Recaptcha/Recaptcha.tsx
+++ b/src/common/components/Recaptcha/Recaptcha.tsx
@@ -71,50 +71,49 @@ export const Recaptcha = ({
   }, [grecaptcha, buttonId])
 
   const handleReCaptchaVerify = useCallback(async () => {
-    // if (!ready) {
-    //   $log.info('Execute recaptcha not yet available')
-    //   return
-    // }
+    if (!ready) {
+      $log.info('Execute recaptcha not yet available')
+      return
+    }
 
-    // grecaptcha.ready(async () => {
-    //   try {
-    //     const token = await grecaptcha.execute(recaptchaKey, { action: action })
-    //     const serverResponse = await fetch(`${apiUrl}/recaptcha/verify`, {
-    //       method: 'POST',
-    //       body: JSON.stringify({ token: token }),
-    //       headers: { 'Content-Type': 'application/json' }
-    //     })
-    //     const data = await serverResponse.json()
+    grecaptcha.ready(async () => {
+      try {
+        const token = await grecaptcha.execute(recaptchaKey, { action: action })
+        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?.success === true && isValidAction(data?.action)) {
-    //       if (data.score < 0.5) {
-    //         $log.warn(`Captcha score was below the threshold: ${data.score}`)
-    //         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 (!data) {
-    //       $log.warn('Data was missing!')
-    //       onSuccess(componentInstance)
-    //       return
-    //     }
-    //     if (!isValidAction(data?.action)) {
-    //       $log.warn(`Invalid action: ${data?.action}`)
-    //       onFailure(componentInstance)
-    //     }
-    //   } catch (error) {
-    //     $log.error(`Failed to verify recaptcha, continuing on: ${error}`)
-    //     onSuccess(componentInstance)
-    //   }
-    // })
-    onSuccess(componentInstance)
+        if (data?.success === true && isValidAction(data?.action)) {
+          if (data.score < 0.5) {
+            $log.warn(`Captcha score was below the threshold: ${data.score}`)
+            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 (!data) {
+          $log.warn('Data was missing!')
+          onSuccess(componentInstance)
+          return
+        }
+        if (!isValidAction(data?.action)) {
+          $log.warn(`Invalid action: ${data?.action}`)
+          onFailure(componentInstance)
+        }
+      } catch (error) {
+        $log.error(`Failed to verify recaptcha, continuing on: ${error}`)
+        onSuccess(componentInstance)
+      }
+    })
   }, [grecaptcha, buttonId, ready])
 
   return (

From f4169581b02ac051cc7e4b035007b0c1b9fcf511 Mon Sep 17 00:00:00 2001
From: Bill Randall <william.randall@cru.org>
Date: Mon, 2 Dec 2024 10:35:23 -0500
Subject: [PATCH 3/7] Add a couple of checks that would have prevented the sev1
 on Saturday. If the response is not in the format we are expecting, we will
 succeed but log a warning

---
 .../components/Recaptcha/Recaptcha.test.tsx   | 52 +++++++++++++++++--
 src/common/components/Recaptcha/Recaptcha.tsx | 11 ++--
 2 files changed, 54 insertions(+), 9 deletions(-)

diff --git a/src/common/components/Recaptcha/Recaptcha.test.tsx b/src/common/components/Recaptcha/Recaptcha.test.tsx
index cb7e1994a..f77a2e3dc 100644
--- a/src/common/components/Recaptcha/Recaptcha.test.tsx
+++ b/src/common/components/Recaptcha/Recaptcha.test.tsx
@@ -144,7 +144,7 @@ describe('Recaptcha component', () => {
     //@ts-ignore
     global.fetch = jest.fn(() => {
       return Promise.resolve({
-        json: () => Promise.resolve({ success: true, action: 'read' })
+        json: () => Promise.resolve({ success: true, action: 'read', score: 0.9 })
       })
     })
 
@@ -220,11 +220,11 @@ describe('Recaptcha component', () => {
     })
   })
 
-  it('should not block gifts if something weird happens', async () => {
+  it('should not block gifts if data is empty', async () => {
     //@ts-ignore
     global.fetch = jest.fn(() => {
       return Promise.resolve({
-        json: () => Promise.resolve()
+        json: () => Promise.resolve({})
       })
     })
 
@@ -238,7 +238,51 @@ describe('Recaptcha component', () => {
     await waitFor(() => {
       expect(onSuccess).toHaveBeenCalledTimes(1)
       expect(onFailure).not.toHaveBeenCalled()
-      expect($log.warn).toHaveBeenCalledWith('Data was missing!')
+      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' })
     })
   })
 
diff --git a/src/common/components/Recaptcha/Recaptcha.tsx b/src/common/components/Recaptcha/Recaptcha.tsx
index 49f7163e3..ddf7bf21e 100644
--- a/src/common/components/Recaptcha/Recaptcha.tsx
+++ b/src/common/components/Recaptcha/Recaptcha.tsx
@@ -86,6 +86,12 @@ export const Recaptcha = ({
         })
         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) {
             $log.warn(`Captcha score was below the threshold: ${data.score}`)
@@ -100,11 +106,6 @@ export const Recaptcha = ({
           onSuccess(componentInstance)
           return
         }
-        if (!data) {
-          $log.warn('Data was missing!')
-          onSuccess(componentInstance)
-          return
-        }
         if (!isValidAction(data?.action)) {
           $log.warn(`Invalid action: ${data?.action}`)
           onFailure(componentInstance)

From 754a38f5d90bf5c953fbdbe1b4737f6ba8e6230e Mon Sep 17 00:00:00 2001
From: Caleb Cox <canac@users.noreply.github.com>
Date: Wed, 4 Dec 2024 14:32:44 -0600
Subject: [PATCH 4/7] Fix designationsService tests

---
 .../services/api/designations.service.js      |  3 +-
 .../services/api/designations.service.spec.js | 92 +++++++++++--------
 2 files changed, 55 insertions(+), 40 deletions(-)

diff --git a/src/common/services/api/designations.service.js b/src/common/services/api/designations.service.js
index 810a3d1d2..802080590 100644
--- a/src/common/services/api/designations.service.js
+++ b/src/common/services/api/designations.service.js
@@ -4,6 +4,7 @@ import toFinite from 'lodash/toFinite'
 import startsWith from 'lodash/startsWith'
 import { Observable } from 'rxjs/Observable'
 import 'rxjs/add/observable/from'
+import 'rxjs/add/observable/of'
 import 'rxjs/add/operator/map'
 import 'rxjs/add/operator/catch'
 import moment from 'moment'
@@ -233,7 +234,7 @@ class DesignationsService {
         }
         return suggestedAmounts
       })
-      .catch(() => [])
+      .catch(() => Observable.of([]))
   }
 
   facebookPixel (code) {
diff --git a/src/common/services/api/designations.service.spec.js b/src/common/services/api/designations.service.spec.js
index 54145e941..c5b0c616e 100644
--- a/src/common/services/api/designations.service.spec.js
+++ b/src/common/services/api/designations.service.spec.js
@@ -24,7 +24,7 @@ describe('designation service', () => {
   })
 
   describe('productSearch', () => {
-    it('should send a request to API and get results', () => {
+    it('should send a request to API and get results', done => {
       self.$httpBackend.expectGET('https://give-stage2.cru.org/search?keyword=steve').respond(200, searchResponse)
       self.designationsService.productSearch({
         keyword: 'steve'
@@ -36,11 +36,12 @@ describe('designation service', () => {
             name: 'John and Jane Doe',
             type: 'Staff'
           })])
-        })
+          done()
+        }, done)
       self.$httpBackend.flush()
     })
 
-    it('should handle undefined fields', () => {
+    it('should handle undefined fields', done => {
       self.$httpBackend.expectGET('https://give-stage2.cru.org/search?keyword=steve').respond(200, { hits: [{}] })
       self.designationsService.productSearch({
         keyword: 'steve'
@@ -52,14 +53,15 @@ describe('designation service', () => {
             name: null,
             type: null
           })])
-        })
+          done()
+        }, done)
       self.$httpBackend.flush()
     })
   })
 
   describe('productLookup', () => {
     const expectedResponse = {
-      uri: 'items/crugive/a5t4fmspmfpwpqvqli7teksyhu=',
+      uri: 'carts/items/crugive/a5t4fmspmfpwpqvqli7teksyhu=/form',
       frequencies: [
         {
           name: 'QUARTERLY',
@@ -90,58 +92,63 @@ describe('designation service', () => {
       frequency: 'NA',
       displayName: 'Steve Peck',
       designationType: 'Staff',
+      orgId: 'STAFF',
       code: '0354433',
       designationNumber: '0354433'
     }
-    it('should get product details for a designation number', () => {
+    it('should get product details for a designation number', done => {
       self.$httpBackend.expectPOST('https://give-stage2.cru.org/cortex/items/crugive/lookups/form?FollowLocation=true&zoom=code,offer:code,definition,definition:options:element:selector:choice,definition:options:element:selector:choice:description,definition:options:element:selector:choice:selectaction,definition:options:element:selector:chosen,definition:options:element:selector:chosen:description',
         { code: '0354433' })
         .respond(200, lookupResponse)
       self.designationsService.productLookup('0354433')
         .subscribe((data) => {
           expect(data).toEqual(expectedResponse)
-        })
+          done()
+        }, done)
       self.$httpBackend.flush()
     })
 
-    it('should get product details for a uri', () => {
+    it('should get product details for a uri', done => {
       self.$httpBackend.expectPOST('https://give-stage2.cru.org/cortex/itemselections/crugive/a5t4fmspmhbkez6cwbnd6mrkla74hdgcupbl4xjb=/options/izzgk4lvmvxgg6i=/values/jzaq=/selector?FollowLocation=true&zoom=code,offer:code,definition,definition:options:element:selector:choice,definition:options:element:selector:choice:description,definition:options:element:selector:choice:selectaction,definition:options:element:selector:chosen,definition:options:element:selector:chosen:description')
         .respond(200, lookupResponse)
       self.designationsService.productLookup('/itemselections/crugive/a5t4fmspmhbkez6cwbnd6mrkla74hdgcupbl4xjb=/options/izzgk4lvmvxgg6i=/values/jzaq=/selector', true)
         .subscribe(data => {
           expect(data).toEqual(expectedResponse)
-        })
+          done()
+        }, done)
       self.$httpBackend.flush()
     })
 
-    it('should handle an empty response', () => {
+    it('should handle an empty response', done => {
       self.$httpBackend.expectPOST('https://give-stage2.cru.org/cortex/itemselections/crugive/a5t4fmspmhbkez6cwbnd6mrkla74hdgcupbl4xjb=/options/izzgk4lvmvxgg6i=/values/jzaq=/selector?FollowLocation=true&zoom=code,offer:code,definition,definition:options:element:selector:choice,definition:options:element:selector:choice:description,definition:options:element:selector:choice:selectaction,definition:options:element:selector:chosen,definition:options:element:selector:chosen:description')
         .respond(200, '')
       self.designationsService.productLookup('/itemselections/crugive/a5t4fmspmhbkez6cwbnd6mrkla74hdgcupbl4xjb=/options/izzgk4lvmvxgg6i=/values/jzaq=/selector', true)
         .subscribe(() => {
-          fail('success should not have been called')
+          done('success should not have been called')
         }, error => {
-          expect(error).toEqual('Product lookup response contains no code data')
+          expect(error.message).toEqual('Product lookup response contains no code data')
+          done()
         })
       self.$httpBackend.flush()
     })
   })
 
   describe('bulkLookup', () => {
-    it('should take an array of designation numbers and return corresponding links for items', () => {
+    it('should take an array of designation numbers and return corresponding links for items', done => {
       self.$httpBackend.expectPOST('https://give-stage2.cru.org/cortex/items/crugive/lookups/batches/form?FollowLocation=true',
         { codes: ['0123456', '1234567'] })
         .respond(200, bulkLookupResponse)
       self.designationsService.bulkLookup(['0123456', '1234567'])
         .subscribe(data => {
           expect(data).toEqual(bulkLookupResponse)
-        })
+          done()
+        }, done)
       self.$httpBackend.flush()
     })
   })
 
   describe('suggestedAmounts', () => {
-    it('should load suggested amounts', () => {
+    it('should load suggested amounts', done => {
       const itemConfig = { amount: 50, 'campaign-page': 9876 }
       self.$httpBackend.expectGET('https://give-stage2.cru.org/content/give/us/en/campaigns/0/1/2/3/4/0123456/9876.infinity.json')
         .respond(200, campaignResponse)
@@ -154,11 +161,12 @@ describe('designation service', () => {
 
           expect(itemConfig['default-campaign-code']).toEqual('867EM1')
           expect(itemConfig['jcr-title']).toEqual('PowerPacksTM for Inner City Children')
-        })
+          done()
+        }, done)
       self.$httpBackend.flush()
     })
 
-    it('should handle an invalid campaign page', () => {
+    it('should handle an invalid campaign page', done => {
       const itemConfig = { amount: 50, 'campaign-page': 9876 }
       self.$httpBackend.expectGET('https://give-stage2.cru.org/content/give/us/en/campaigns/0/1/2/3/4/0123456/9876.infinity.json')
         .respond(400, {})
@@ -167,11 +175,12 @@ describe('designation service', () => {
           expect(suggestedAmounts).toEqual([])
           expect(itemConfig['default-campaign-code']).toBeUndefined()
           expect(itemConfig['jcr-title']).toBeUndefined()
-        })
+          done()
+        }, done)
       self.$httpBackend.flush()
     })
 
-    it('should handle no campaign page', () => {
+    it('should handle no campaign page', done => {
       const itemConfig = { amount: 50 }
       self.$httpBackend.expectGET('https://give-stage2.cru.org/content/give/us/en/designations/0/1/2/3/4/0123456.infinity.json')
         .respond(200, designationResponse)
@@ -180,25 +189,27 @@ describe('designation service', () => {
           expect(suggestedAmounts).toEqual([])
           expect(itemConfig['default-campaign-code']).toEqual('867EM1')
           expect(itemConfig['jcr-title']).toEqual('PowerPacksTM for Inner City Children')
-        })
+          done()
+        }, done)
       self.$httpBackend.flush()
     })
   })
 
   describe('facebookPixel', () => {
-    it('should load facebook pixel id from JCR', () => {
+    it('should load facebook pixel id from JCR', done => {
       self.$httpBackend.expectGET('https://give-stage2.cru.org/content/give/us/en/designations/0/1/2/3/4/0123456.infinity.json')
         .respond(200, designationResponse)
       self.designationsService.facebookPixel('0123456')
         .subscribe(pixelId => {
           expect(pixelId).toEqual('123456')
-        })
+          done()
+        }, done)
       self.$httpBackend.flush()
     })
   })
 
   describe('givingLinks', () => {
-    it('should load givingLinks from JCR', () => {
+    it('should load givingLinks from JCR', done => {
       self.$httpBackend.expectGET('https://give-stage2.cru.org/content/give/us/en/designations/0/1/2/3/4/0123456.infinity.json')
         .respond(200, designationResponse)
       self.designationsService.givingLinks('0123456')
@@ -206,7 +217,8 @@ describe('designation service', () => {
           expect(givingLinks).toEqual([
             { name: 'Name', url: 'https://example.com', order: 0 }
           ])
-        })
+          done()
+        }, done)
       self.$httpBackend.flush()
     })
   })
@@ -244,31 +256,33 @@ describe('designation service', () => {
   })
 
   describe('ministriesList', () => {
-    it('should return a list of ministries', () => {
+    it('should return a list of ministries', done => {
       jest.spyOn(self.$location, 'protocol').mockImplementationOnce(() => 'https')
       jest.spyOn(self.$location, 'host').mockImplementationOnce(() => 'give-stage-cloud.cru.org')
       const pagePath = 'page.html'
       const ministriesResponse = {
-        ministries: [{
-          name: 'Some Ministry',
-          designationNumber: '0123456',
-          path: '/some-vanity',
-          extra: 'something-else'
-        }]
+        ministries: [
+          JSON.stringify({
+            name: 'Some Ministry',
+            designationNumber: '0123456',
+            path: '/some-vanity',
+            extra: 'something-else'
+          })
+        ]
       }
       self.$httpBackend.expectGET(`https://give-stage-cloud.cru.org/${pagePath}/jcr:content/content-parsys/designation_search_r.json`)
         .respond(200, ministriesResponse)
 
-      const expectedResult = {
-        ministries: [{
-          name: 'Some Ministry',
-          designationNumber: '0123456',
-          path: '/some-vanity'
-        }]
-      }
+      const expectedResult = [{
+        name: 'Some Ministry',
+        designationNumber: '0123456',
+        facet: null,
+        path: '/some-vanity'
+      }]
       self.designationsService.ministriesList(pagePath).subscribe(actualResult => {
         expect(actualResult).toEqual(expectedResult)
-      })
+        done()
+      }, done)
       self.$httpBackend.flush()
     })
   })

From 521dc078067985328ed93fcfe7e4a9ba1ba036be Mon Sep 17 00:00:00 2001
From: Caleb Cox <canac@users.noreply.github.com>
Date: Wed, 4 Dec 2024 13:44:53 -0600
Subject: [PATCH 5/7] Ignore giving links without names or urls

---
 src/common/services/api/designations.service.js   |  4 ++++
 .../services/api/designations.service.spec.js     | 15 +++++++++++++++
 2 files changed, 19 insertions(+)

diff --git a/src/common/services/api/designations.service.js b/src/common/services/api/designations.service.js
index 802080590..52b7e4710 100644
--- a/src/common/services/api/designations.service.js
+++ b/src/common/services/api/designations.service.js
@@ -254,6 +254,10 @@ class DesignationsService {
           // Map giving links
           if (data.data['jcr:content'].givingLinks) {
             angular.forEach(data.data['jcr:content'].givingLinks, (v, k) => {
+              if (!v || !v.name || !v.url) {
+                return
+              }
+
               if (toFinite(k) > 0 || startsWith(k, 'item')) {
                 givingLinks.push({
                   name: v.name,
diff --git a/src/common/services/api/designations.service.spec.js b/src/common/services/api/designations.service.spec.js
index c5b0c616e..0b4abfccf 100644
--- a/src/common/services/api/designations.service.spec.js
+++ b/src/common/services/api/designations.service.spec.js
@@ -221,6 +221,21 @@ describe('designation service', () => {
         }, done)
       self.$httpBackend.flush()
     })
+
+    it('should ignore givingLinks without names or urls', done => {
+      const response = angular.copy(designationResponse)
+      response['jcr:content'].givingLinks.item1 = { 'jcr:primaryType': 'nt:unstructured' }
+      self.$httpBackend.expectGET('https://give-stage2.cru.org/content/give/us/en/designations/0/1/2/3/4/0123456.infinity.json')
+        .respond(200, response)
+      self.designationsService.givingLinks('0123456')
+        .subscribe(givingLinks => {
+          expect(givingLinks).toEqual([
+            { name: 'Name', url: 'https://example.com', order: 0 }
+          ])
+          done()
+        }, done)
+      self.$httpBackend.flush()
+    })
   })
 
   describe('generatePath', () => {

From fe486ee05b1921022ca97aefcb15224b92c4fa78 Mon Sep 17 00:00:00 2001
From: Caleb Cox <canac@users.noreply.github.com>
Date: Wed, 4 Dec 2024 14:05:43 -0600
Subject: [PATCH 6/7] Add valid giving link after invalid giving link

---
 src/common/services/api/designations.service.spec.js | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/common/services/api/designations.service.spec.js b/src/common/services/api/designations.service.spec.js
index 0b4abfccf..b956af4fd 100644
--- a/src/common/services/api/designations.service.spec.js
+++ b/src/common/services/api/designations.service.spec.js
@@ -225,12 +225,14 @@ describe('designation service', () => {
     it('should ignore givingLinks without names or urls', done => {
       const response = angular.copy(designationResponse)
       response['jcr:content'].givingLinks.item1 = { 'jcr:primaryType': 'nt:unstructured' }
+      response['jcr:content'].givingLinks.item2 = { 'jcr:primaryType': 'nt:unstructured', url: 'https://example2.com', name: 'Name 2' }
       self.$httpBackend.expectGET('https://give-stage2.cru.org/content/give/us/en/designations/0/1/2/3/4/0123456.infinity.json')
         .respond(200, response)
       self.designationsService.givingLinks('0123456')
         .subscribe(givingLinks => {
           expect(givingLinks).toEqual([
-            { name: 'Name', url: 'https://example.com', order: 0 }
+            { name: 'Name', url: 'https://example.com', order: 0 },
+            { name: 'Name 2', url: 'https://example2.com', order: 2 }
           ])
           done()
         }, done)

From 06a8948a4c8448cfb9910da906a351a7bf402354 Mon Sep 17 00:00:00 2001
From: Caleb Cox <canac@users.noreply.github.com>
Date: Thu, 5 Dec 2024 08:52:08 -0600
Subject: [PATCH 7/7] Document workaround for empty giving links

---
 src/common/services/api/designations.service.js | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/src/common/services/api/designations.service.js b/src/common/services/api/designations.service.js
index 52b7e4710..f10dc15a2 100644
--- a/src/common/services/api/designations.service.js
+++ b/src/common/services/api/designations.service.js
@@ -255,6 +255,9 @@ class DesignationsService {
           if (data.data['jcr:content'].givingLinks) {
             angular.forEach(data.data['jcr:content'].givingLinks, (v, k) => {
               if (!v || !v.name || !v.url) {
+                // Some accounts contain multiple, empty giving links. Until we figure how how they
+                // are being created, we are ignoring them on the frontend.
+                // https://jira.cru.org/browse/EP-2554
                 return
               }