Преглед изворни кода

Merge pull request #33610 from overleaf/jel-hostname-capturedByGroup

[web] Check `capturedByGroup` when adding new email

GitOrigin-RevId: f9ef3d4cc7387dc0139a70aecd6cfcb20170abc6
Jessica Lawshe пре 2 месеци
родитељ
комит
9acf3b8e7f

+ 28 - 9
services/web/frontend/js/features/settings/components/emails/add-email.tsx

@@ -22,6 +22,17 @@ import { ConfirmEmailForm } from '@/features/settings/components/emails/confirm-
 import RecaptchaConditions from '@/shared/components/recaptcha-conditions'
 import RecaptchaConditions from '@/shared/components/recaptcha-conditions'
 import SsoLinkingInfoGroup from './add-email/sso-linking-info-group'
 import SsoLinkingInfoGroup from './add-email/sso-linking-info-group'
 import Notification from '@/shared/components/notification'
 import Notification from '@/shared/components/notification'
+import { useFeatureFlag } from '@/shared/context/split-test-context'
+
+function isDomainCapturedByGroup(
+  domainInfo: DomainInfo,
+  domainCapturedByGroupRolloutFlagEnabled: boolean
+): boolean {
+  return domainCapturedByGroupRolloutFlagEnabled
+    ? (domainInfo.capturedByGroup && domainInfo.group?.domainCaptureEnabled) ||
+        false
+    : domainInfo.group?.domainCaptureEnabled || false
+}
 
 
 function AddEmail() {
 function AddEmail() {
   const { t } = useTranslation()
   const { t } = useTranslation()
@@ -49,6 +60,10 @@ function AddEmail() {
   const emailAddressLimit = getMeta('ol-emailAddressLimit') || 10
   const emailAddressLimit = getMeta('ol-emailAddressLimit') || 10
   const { ref: recaptchaRef, getReCaptchaToken } = useRecaptcha()
   const { ref: recaptchaRef, getReCaptchaToken } = useRecaptcha()
 
 
+  const domainCapturedByGroupRolloutFlagEnabled = useFeatureFlag(
+    'domain-captured-by-group'
+  )
+
   useEffect(() => {
   useEffect(() => {
     setUserEmailsContextLoading(isLoading)
     setUserEmailsContextLoading(isLoading)
   }, [setUserEmailsContextLoading, isLoading])
   }, [setUserEmailsContextLoading, isLoading])
@@ -196,8 +211,15 @@ function AddEmail() {
     )
     )
   }
   }
 
 
+  const isDomainCaptured = newEmailMatchedDomain
+    ? isDomainCapturedByGroup(
+        newEmailMatchedDomain,
+        domainCapturedByGroupRolloutFlagEnabled
+      )
+    : false
   const isSsoAvailableForDomain =
   const isSsoAvailableForDomain =
-    newEmailMatchedDomain && ssoAvailableForDomain(newEmailMatchedDomain)
+    newEmailMatchedDomain &&
+    ssoAvailableForDomain(newEmailMatchedDomain, isDomainCaptured)
 
 
   return (
   return (
     <form>
     <form>
@@ -243,6 +265,7 @@ function AddEmail() {
                 <AddEmailViaSSO
                 <AddEmailViaSSO
                   email={newEmail}
                   email={newEmail}
                   domainInfo={newEmailMatchedDomain}
                   domainInfo={newEmailMatchedDomain}
+                  isDomainCaptured={isDomainCaptured}
                   userInstitutions={state.data.linkedInstitutionIds}
                   userInstitutions={state.data.linkedInstitutionIds}
                 />
                 />
               </div>
               </div>
@@ -259,15 +282,14 @@ function AddEmailViaSSO({
   email,
   email,
   domainInfo,
   domainInfo,
   userInstitutions,
   userInstitutions,
+  isDomainCaptured,
 }: {
 }: {
   email: string
   email: string
   domainInfo: DomainInfo
   domainInfo: DomainInfo
   userInstitutions: string[]
   userInstitutions: string[]
+  isDomainCaptured: boolean
 }) {
 }) {
-  if (
-    domainInfo.group?.domainCaptureEnabled &&
-    domainInfo.group?.managedUsersEnabled
-  ) {
+  if (isDomainCaptured && domainInfo.group?.managedUsersEnabled) {
     return (
     return (
       <Notification
       <Notification
         type="error"
         type="error"
@@ -298,10 +320,7 @@ function AddEmailViaSSO({
       )
       )
     }
     }
     return <SsoLinkingInfo email={email} domainInfo={domainInfo} />
     return <SsoLinkingInfo email={email} domainInfo={domainInfo} />
-  } else if (
-    domainInfo.group?.domainCaptureEnabled &&
-    domainInfo.group?.ssoConfig?.enabled
-  ) {
+  } else if (isDomainCaptured && domainInfo.group?.ssoConfig?.enabled) {
     return <SsoLinkingInfoGroup domainInfo={domainInfo} />
     return <SsoLinkingInfoGroup domainInfo={domainInfo} />
   }
   }
 }
 }

+ 1 - 0
services/web/frontend/js/features/settings/components/emails/add-email/input.tsx

@@ -27,6 +27,7 @@ function matchLocalAndDomain(emailHint: string) {
 export type DomainInfo = {
 export type DomainInfo = {
   hostname: string
   hostname: string
   confirmed?: boolean
   confirmed?: boolean
+  capturedByGroup?: boolean
   university: {
   university: {
     id: number
     id: number
     name: string
     name: string

+ 3 - 2
services/web/frontend/js/features/settings/utils/sso.ts

@@ -3,7 +3,8 @@ import { DomainInfo } from '../components/emails/add-email/input'
 import { Institution } from '../../../../../types/institution'
 import { Institution } from '../../../../../types/institution'
 
 
 export const ssoAvailableForDomain = (
 export const ssoAvailableForDomain = (
-  domain: DomainInfo | null
+  domain: DomainInfo | null,
+  isDomainCapturedByGroup: boolean
 ): domain is DomainInfo => {
 ): domain is DomainInfo => {
   const { hasSamlBeta, hasSamlFeature } = getMeta('ol-ExposedSettings')
   const { hasSamlBeta, hasSamlFeature } = getMeta('ol-ExposedSettings')
   if (!hasSamlFeature || !domain || !domain.confirmed || !domain.university) {
   if (!hasSamlFeature || !domain || !domain.confirmed || !domain.university) {
@@ -13,7 +14,7 @@ export const ssoAvailableForDomain = (
     return true
     return true
   }
   }
 
 
-  if (domain.group?.ssoConfig?.enabled) {
+  if (isDomainCapturedByGroup && domain.group?.ssoConfig?.enabled) {
     return true
     return true
   }
   }
 
 

+ 214 - 18
services/web/test/frontend/features/settings/components/emails/emails-section-add-new-email.test.tsx

@@ -14,6 +14,7 @@ import { Affiliation } from '../../../../../../types/affiliation'
 import withMarkup from '../../../../helpers/with-markup'
 import withMarkup from '../../../../helpers/with-markup'
 import getMeta from '@/utils/meta'
 import getMeta from '@/utils/meta'
 import { clearDomainCache } from '../../../../../../frontend/js/features/settings/components/emails/add-email/input'
 import { clearDomainCache } from '../../../../../../frontend/js/features/settings/components/emails/add-email/input'
+import { SplitTestProvider } from '@/shared/context/split-test-context'
 
 
 const userEmailData: UserEmailData & { affiliation: Affiliation } = {
 const userEmailData: UserEmailData & { affiliation: Affiliation } = {
   affiliation: {
   affiliation: {
@@ -57,6 +58,14 @@ const institutionDomainData = [
   },
   },
 ] as const
 ] as const
 
 
+function renderEmailsSection() {
+  return render(<EmailsSection />, {
+    wrapper: ({ children }) => (
+      <SplitTestProvider>{children}</SplitTestProvider>
+    ),
+  })
+}
+
 function resetFetchMock() {
 function resetFetchMock() {
   fetchMock.removeRoutes().clearHistory()
   fetchMock.removeRoutes().clearHistory()
   fetchMock.get('express:/institutions/domains', [])
   fetchMock.get('express:/institutions/domains', [])
@@ -94,14 +103,14 @@ describe('<EmailsSection />', function () {
 
 
   it('renders "add another email" button', async function () {
   it('renders "add another email" button', async function () {
     fetchMock.get('/user/emails?ensureAffiliation=true', [])
     fetchMock.get('/user/emails?ensureAffiliation=true', [])
-    render(<EmailsSection />)
+    renderEmailsSection()
 
 
     await screen.findByRole('button', { name: 'Add another email' })
     await screen.findByRole('button', { name: 'Add another email' })
   })
   })
 
 
   it('renders input', async function () {
   it('renders input', async function () {
     fetchMock.get('/user/emails?ensureAffiliation=true', [])
     fetchMock.get('/user/emails?ensureAffiliation=true', [])
-    render(<EmailsSection />)
+    renderEmailsSection()
     await fetchMock.callHistory.flush(true)
     await fetchMock.callHistory.flush(true)
 
 
     const button = await screen.findByRole<HTMLButtonElement>('button', {
     const button = await screen.findByRole<HTMLButtonElement>('button', {
@@ -116,7 +125,7 @@ describe('<EmailsSection />', function () {
     fetchMock.get('/user/emails?ensureAffiliation=true', [])
     fetchMock.get('/user/emails?ensureAffiliation=true', [])
     fetchMock.get(`/institutions/domains?hostname=email.com&limit=1`, 200)
     fetchMock.get(`/institutions/domains?hostname=email.com&limit=1`, 200)
     fetchMock.get(`/institutions/domains?hostname=email&limit=1`, 200)
     fetchMock.get(`/institutions/domains?hostname=email&limit=1`, 200)
-    render(<EmailsSection />)
+    renderEmailsSection()
     await fetchMock.callHistory.flush(true)
     await fetchMock.callHistory.flush(true)
 
 
     const button = await screen.findByRole<HTMLButtonElement>('button', {
     const button = await screen.findByRole<HTMLButtonElement>('button', {
@@ -159,7 +168,7 @@ describe('<EmailsSection />', function () {
 
 
   it('renders "add new email" button', async function () {
   it('renders "add new email" button', async function () {
     fetchMock.get('/user/emails?ensureAffiliation=true', [])
     fetchMock.get('/user/emails?ensureAffiliation=true', [])
-    render(<EmailsSection />)
+    renderEmailsSection()
 
 
     const button = await screen.findByRole<HTMLButtonElement>('button', {
     const button = await screen.findByRole<HTMLButtonElement>('button', {
       name: 'Add another email',
       name: 'Add another email',
@@ -175,7 +184,7 @@ describe('<EmailsSection />', function () {
       emails.push({ email: `bar${i}@overleaf.com` })
       emails.push({ email: `bar${i}@overleaf.com` })
     }
     }
     fetchMock.get('/user/emails?ensureAffiliation=true', emails)
     fetchMock.get('/user/emails?ensureAffiliation=true', emails)
-    render(<EmailsSection />)
+    renderEmailsSection()
 
 
     const findByTextWithMarkup = withMarkup(screen.findByText)
     const findByTextWithMarkup = withMarkup(screen.findByText)
     await findByTextWithMarkup(
     await findByTextWithMarkup(
@@ -188,7 +197,7 @@ describe('<EmailsSection />', function () {
 
 
   it('adds new email address', async function () {
   it('adds new email address', async function () {
     fetchMock.get('/user/emails?ensureAffiliation=true', [])
     fetchMock.get('/user/emails?ensureAffiliation=true', [])
-    render(<EmailsSection />)
+    renderEmailsSection()
 
 
     const addAnotherEmailBtn = await screen.findByRole<HTMLButtonElement>(
     const addAnotherEmailBtn = await screen.findByRole<HTMLButtonElement>(
       'button',
       'button',
@@ -232,7 +241,7 @@ describe('<EmailsSection />', function () {
 
 
   it('fails to add add new email address', async function () {
   it('fails to add add new email address', async function () {
     fetchMock.get('/user/emails?ensureAffiliation=true', [])
     fetchMock.get('/user/emails?ensureAffiliation=true', [])
-    render(<EmailsSection />)
+    renderEmailsSection()
 
 
     const addAnotherEmailBtn = await screen.findByRole<HTMLButtonElement>(
     const addAnotherEmailBtn = await screen.findByRole<HTMLButtonElement>(
       'button',
       'button',
@@ -271,7 +280,7 @@ describe('<EmailsSection />', function () {
 
 
   it('can link email address to an existing SSO institution', async function () {
   it('can link email address to an existing SSO institution', async function () {
     fetchMock.get('/user/emails?ensureAffiliation=true', [])
     fetchMock.get('/user/emails?ensureAffiliation=true', [])
-    render(<EmailsSection />)
+    renderEmailsSection()
 
 
     const button = await screen.findByRole<HTMLButtonElement>('button', {
     const button = await screen.findByRole<HTMLButtonElement>('button', {
       name: 'Add another email',
       name: 'Add another email',
@@ -295,7 +304,7 @@ describe('<EmailsSection />', function () {
     fetchMock.get('/user/emails?ensureAffiliation=true', [
     fetchMock.get('/user/emails?ensureAffiliation=true', [
       { email: 'bar@autocomplete.edu', samlProviderId: '1234' },
       { email: 'bar@autocomplete.edu', samlProviderId: '1234' },
     ])
     ])
-    render(<EmailsSection />)
+    renderEmailsSection()
 
 
     const button = await screen.findByRole<HTMLButtonElement>('button', {
     const button = await screen.findByRole<HTMLButtonElement>('button', {
       name: 'Add another email',
       name: 'Add another email',
@@ -321,7 +330,7 @@ describe('<EmailsSection />', function () {
     const country = 'Germany'
     const country = 'Germany'
     const customDepartment = 'Custom department'
     const customDepartment = 'Custom department'
     fetchMock.get('/user/emails?ensureAffiliation=true', [])
     fetchMock.get('/user/emails?ensureAffiliation=true', [])
-    render(<EmailsSection />)
+    renderEmailsSection()
 
 
     const button = await screen.findByRole<HTMLButtonElement>('button', {
     const button = await screen.findByRole<HTMLButtonElement>('button', {
       name: 'Add another email',
       name: 'Add another email',
@@ -424,7 +433,7 @@ describe('<EmailsSection />', function () {
 
 
   it('autocompletes institution name', async function () {
   it('autocompletes institution name', async function () {
     fetchMock.get('/user/emails?ensureAffiliation=true', [])
     fetchMock.get('/user/emails?ensureAffiliation=true', [])
-    render(<EmailsSection />)
+    renderEmailsSection()
 
 
     const button = await screen.findByRole<HTMLButtonElement>('button', {
     const button = await screen.findByRole<HTMLButtonElement>('button', {
       name: 'Add another email',
       name: 'Add another email',
@@ -480,7 +489,7 @@ describe('<EmailsSection />', function () {
     const countryCode = 'de'
     const countryCode = 'de'
     const newUniversity = 'Abcdef'
     const newUniversity = 'Abcdef'
     fetchMock.get('/user/emails?ensureAffiliation=true', [])
     fetchMock.get('/user/emails?ensureAffiliation=true', [])
-    render(<EmailsSection />)
+    renderEmailsSection()
 
 
     const button = await screen.findByRole<HTMLButtonElement>('button', {
     const button = await screen.findByRole<HTMLButtonElement>('button', {
       name: 'Add another email',
       name: 'Add another email',
@@ -596,7 +605,7 @@ describe('<EmailsSection />', function () {
     ]
     ]
     const hostnameFirstChar = institutionDomainDataCopy[0].hostname.charAt(0)
     const hostnameFirstChar = institutionDomainDataCopy[0].hostname.charAt(0)
     fetchMock.get('/user/emails?ensureAffiliation=true', [])
     fetchMock.get('/user/emails?ensureAffiliation=true', [])
-    render(<EmailsSection />)
+    renderEmailsSection()
 
 
     const button = await screen.findByRole<HTMLButtonElement>('button', {
     const button = await screen.findByRole<HTMLButtonElement>('button', {
       name: 'Add another email',
       name: 'Add another email',
@@ -669,7 +678,7 @@ describe('<EmailsSection />', function () {
     ]
     ]
     const hostnameFirstChar = institutionDomainDataCopy[0].hostname.charAt(0)
     const hostnameFirstChar = institutionDomainDataCopy[0].hostname.charAt(0)
     fetchMock.get('/user/emails?ensureAffiliation=true', [])
     fetchMock.get('/user/emails?ensureAffiliation=true', [])
-    render(<EmailsSection />)
+    renderEmailsSection()
 
 
     const button = await screen.findByRole<HTMLButtonElement>('button', {
     const button = await screen.findByRole<HTMLButtonElement>('button', {
       name: 'Add another email',
       name: 'Add another email',
@@ -766,7 +775,7 @@ describe('<EmailsSection />', function () {
       it('can add email address via SSO', async function () {
       it('can add email address via SSO', async function () {
         // note: this UI is a WIP
         // note: this UI is a WIP
         fetchMock.get('/user/emails?ensureAffiliation=true', [])
         fetchMock.get('/user/emails?ensureAffiliation=true', [])
-        render(<EmailsSection />)
+        renderEmailsSection()
 
 
         const button = await screen.findByRole<HTMLButtonElement>('button', {
         const button = await screen.findByRole<HTMLButtonElement>('button', {
           name: 'Add another email',
           name: 'Add another email',
@@ -809,7 +818,7 @@ describe('<EmailsSection />', function () {
       it('renders error', async function () {
       it('renders error', async function () {
         // note: this UI is a WIP
         // note: this UI is a WIP
         fetchMock.get('/user/emails?ensureAffiliation=true', [])
         fetchMock.get('/user/emails?ensureAffiliation=true', [])
-        render(<EmailsSection />)
+        renderEmailsSection()
 
 
         const button = await screen.findByRole<HTMLButtonElement>('button', {
         const button = await screen.findByRole<HTMLButtonElement>('button', {
           name: 'Add another email',
           name: 'Add another email',
@@ -855,7 +864,7 @@ describe('<EmailsSection />', function () {
 
 
       it('renders Commons UI', async function () {
       it('renders Commons UI', async function () {
         fetchMock.get('/user/emails?ensureAffiliation=true', [])
         fetchMock.get('/user/emails?ensureAffiliation=true', [])
-        render(<EmailsSection />)
+        renderEmailsSection()
 
 
         const button = await screen.findByRole<HTMLButtonElement>('button', {
         const button = await screen.findByRole<HTMLButtonElement>('button', {
           name: 'Add another email',
           name: 'Add another email',
@@ -900,7 +909,7 @@ describe('<EmailsSection />', function () {
 
 
       it('renders group domain-capture error instead of Commons UI', async function () {
       it('renders group domain-capture error instead of Commons UI', async function () {
         fetchMock.get('/user/emails?ensureAffiliation=true', [])
         fetchMock.get('/user/emails?ensureAffiliation=true', [])
-        render(<EmailsSection />)
+        renderEmailsSection()
 
 
         const button = await screen.findByRole<HTMLButtonElement>('button', {
         const button = await screen.findByRole<HTMLButtonElement>('button', {
           name: 'Add another email',
           name: 'Add another email',
@@ -925,5 +934,192 @@ describe('<EmailsSection />', function () {
         ).to.be.null
         ).to.be.null
       })
       })
     })
     })
+
+    describe('when the domain-captured-by-group feature flag is enabled', function () {
+      beforeEach(function () {
+        window.metaAttributesCache.set('ol-splitTestVariants', {
+          'domain-captured-by-group': 'enabled',
+        })
+      })
+
+      afterEach(function () {
+        window.metaAttributesCache.set('ol-splitTestVariants', {})
+      })
+
+      describe('and capturedByGroup is true with managedUsersEnabled', function () {
+        beforeEach(async function () {
+          await fetchMock.callHistory.flush(true)
+          fetchMock.removeRoutes().clearHistory()
+          const institution = {
+            university: {
+              id: 1234,
+              ssoEnabled: false,
+              name: 'Auto Complete University',
+            },
+            hostname: 'autocomplete.edu',
+            confirmed: true,
+            capturedByGroup: true,
+            group: {
+              domainCaptureEnabled: true,
+              managedUsersEnabled: true,
+              ssoConfig: { enabled: true },
+            },
+          }
+
+          fetchMock.get('express:/institutions/domains', [institution])
+        })
+
+        it('renders the domain-capture error', async function () {
+          fetchMock.get('/user/emails?ensureAffiliation=true', [])
+          renderEmailsSection()
+
+          const button = await screen.findByRole<HTMLButtonElement>('button', {
+            name: 'Add another email',
+          })
+
+          await userEvent.click(button)
+
+          const input = screen.getByRole('textbox', { name: 'Email' })
+          fireEvent.change(input, {
+            target: { value: 'user@autocomplete.edu' },
+          })
+
+          const notification = await screen.findByRole('alert')
+          within(notification).getByText(
+            'Your company email address has been registered under a verified domain, and cannot be added as a secondary email.',
+            { exact: false }
+          )
+        })
+      })
+
+      describe('and capturedByGroup is false with managedUsersEnabled (domainCaptureEnabled alone is not enough)', function () {
+        beforeEach(async function () {
+          await fetchMock.callHistory.flush(true)
+          fetchMock.removeRoutes().clearHistory()
+          const institution = {
+            university: {
+              id: 1234,
+              ssoEnabled: false,
+              name: 'Auto Complete University',
+            },
+            hostname: 'autocomplete.edu',
+            confirmed: true,
+            capturedByGroup: false,
+            group: {
+              domainCaptureEnabled: true,
+              managedUsersEnabled: true,
+              ssoConfig: { enabled: true },
+            },
+          }
+
+          fetchMock.get('express:/institutions/domains', [institution])
+        })
+
+        it('does not render the domain-capture error and shows regular institution fields', async function () {
+          fetchMock.get('/user/emails?ensureAffiliation=true', [])
+          renderEmailsSection()
+
+          const button = await screen.findByRole<HTMLButtonElement>('button', {
+            name: 'Add another email',
+          })
+
+          await userEvent.click(button)
+
+          const input = screen.getByRole('textbox', { name: 'Email' })
+          fireEvent.change(input, {
+            target: { value: 'user@autocomplete.edu' },
+          })
+
+          await screen.findByRole('button', { name: 'Add new email' })
+          expect(screen.queryByRole('alert')).to.be.null
+        })
+      })
+
+      describe('and capturedByGroup is true with ssoConfig enabled (no managedUsers)', function () {
+        beforeEach(async function () {
+          await fetchMock.callHistory.flush(true)
+          fetchMock.removeRoutes().clearHistory()
+          const institution = {
+            university: {
+              id: 1234,
+              ssoEnabled: false,
+              name: 'Auto Complete University',
+            },
+            hostname: 'autocomplete.edu',
+            confirmed: true,
+            capturedByGroup: true,
+            group: {
+              domainCaptureEnabled: true,
+              managedUsersEnabled: false,
+              ssoConfig: { enabled: true },
+            },
+          }
+
+          fetchMock.get('express:/institutions/domains', [institution])
+        })
+
+        it('renders the group SSO unavailable message', async function () {
+          fetchMock.get('/user/emails?ensureAffiliation=true', [])
+          renderEmailsSection()
+
+          const button = await screen.findByRole<HTMLButtonElement>('button', {
+            name: 'Add another email',
+          })
+
+          await userEvent.click(button)
+
+          const input = screen.getByRole('textbox', { name: 'Email' })
+          fireEvent.change(input, {
+            target: { value: 'user@autocomplete.edu' },
+          })
+
+          await screen.findByText('This feature is currently unavailable.')
+        })
+      })
+
+      describe('and capturedByGroup is false with ssoConfig enabled', function () {
+        beforeEach(async function () {
+          await fetchMock.callHistory.flush(true)
+          fetchMock.removeRoutes().clearHistory()
+          const institution = {
+            university: {
+              id: 1234,
+              ssoEnabled: false,
+              name: 'Auto Complete University',
+            },
+            hostname: 'autocomplete.edu',
+            confirmed: true,
+            capturedByGroup: false,
+            group: {
+              domainCaptureEnabled: true,
+              managedUsersEnabled: false,
+              ssoConfig: { enabled: true },
+            },
+          }
+
+          fetchMock.get('express:/institutions/domains', [institution])
+        })
+
+        it('does not render SSO UI and shows regular institution fields', async function () {
+          fetchMock.get('/user/emails?ensureAffiliation=true', [])
+          renderEmailsSection()
+
+          const button = await screen.findByRole<HTMLButtonElement>('button', {
+            name: 'Add another email',
+          })
+
+          await userEvent.click(button)
+
+          const input = screen.getByRole('textbox', { name: 'Email' })
+          fireEvent.change(input, {
+            target: { value: 'user@autocomplete.edu' },
+          })
+
+          await screen.findByRole('button', { name: 'Add new email' })
+          expect(screen.queryByText('This feature is currently unavailable.'))
+            .to.be.null
+        })
+      })
+    })
   })
   })
 })
 })

+ 11 - 2
services/web/test/frontend/features/settings/components/emails/emails-section-institution-and-role.test.tsx

@@ -12,6 +12,7 @@ import { UserEmailsProvider } from '../../../../../../frontend/js/features/setti
 import EmailsSection from '../../../../../../frontend/js/features/settings/components/emails-section'
 import EmailsSection from '../../../../../../frontend/js/features/settings/components/emails-section'
 import { Affiliation } from '../../../../../../types/affiliation'
 import { Affiliation } from '../../../../../../types/affiliation'
 import getMeta from '@/utils/meta'
 import getMeta from '@/utils/meta'
+import { SplitTestProvider } from '@/shared/context/split-test-context'
 
 
 const userData1: UserEmailData & { affiliation: Affiliation } = {
 const userData1: UserEmailData & { affiliation: Affiliation } = {
   affiliation: {
   affiliation: {
@@ -74,6 +75,14 @@ const userData2: UserEmailData & { affiliation: Affiliation } = {
   default: false,
   default: false,
 }
 }
 
 
+function renderEmailsSection() {
+  return render(<EmailsSection />, {
+    wrapper: ({ children }) => (
+      <SplitTestProvider>{children}</SplitTestProvider>
+    ),
+  })
+}
+
 describe('user role and institution', function () {
 describe('user role and institution', function () {
   beforeEach(function () {
   beforeEach(function () {
     Object.assign(getMeta('ol-ExposedSettings'), {
     Object.assign(getMeta('ol-ExposedSettings'), {
@@ -125,7 +134,7 @@ describe('user role and institution', function () {
   it('fetches institution data and replaces departments dropdown on add/change', async function () {
   it('fetches institution data and replaces departments dropdown on add/change', async function () {
     const userEmailData = userData1
     const userEmailData = userData1
     fetchMock.modifyRoute('get user emails', { response: [userEmailData] })
     fetchMock.modifyRoute('get user emails', { response: [userEmailData] })
-    render(<EmailsSection />)
+    renderEmailsSection()
 
 
     await fetchMock.callHistory.flush(true)
     await fetchMock.callHistory.flush(true)
     fetchMock.removeRoutes().clearHistory()
     fetchMock.removeRoutes().clearHistory()
@@ -158,7 +167,7 @@ describe('user role and institution', function () {
       .modifyRoute('get user emails', { response: [userData1] })
       .modifyRoute('get user emails', { response: [userData1] })
       .get(/\/institutions\/list/, { departments: [] })
       .get(/\/institutions\/list/, { departments: [] })
       .post('/user/emails/endorse', 200)
       .post('/user/emails/endorse', 200)
-    render(<EmailsSection />)
+    renderEmailsSection()
 
 
     const addBtn = await screen.findByRole('button', {
     const addBtn = await screen.findByRole('button', {
       name: /add role and department/i,
       name: /add role and department/i,

+ 22 - 13
services/web/test/frontend/features/settings/components/emails/emails-section.test.tsx

@@ -16,6 +16,15 @@ import {
   unconfirmedUserData,
   unconfirmedUserData,
 } from '../../fixtures/test-user-email-data'
 } from '../../fixtures/test-user-email-data'
 import getMeta from '@/utils/meta'
 import getMeta from '@/utils/meta'
+import { SplitTestProvider } from '@/shared/context/split-test-context'
+
+function renderEmailsSection() {
+  return render(<EmailsSection />, {
+    wrapper: ({ children }) => (
+      <SplitTestProvider>{children}</SplitTestProvider>
+    ),
+  })
+}
 
 
 describe('<EmailsSection />', function () {
 describe('<EmailsSection />', function () {
   beforeEach(function () {
   beforeEach(function () {
@@ -30,13 +39,13 @@ describe('<EmailsSection />', function () {
   })
   })
 
 
   it('renders translated heading', function () {
   it('renders translated heading', function () {
-    render(<EmailsSection />)
+    renderEmailsSection()
 
 
     screen.getByRole('heading', { name: /emails and affiliations/i })
     screen.getByRole('heading', { name: /emails and affiliations/i })
   })
   })
 
 
   it('renders translated description', function () {
   it('renders translated description', function () {
-    render(<EmailsSection />)
+    renderEmailsSection()
 
 
     screen.getByText(/add additional email addresses/i)
     screen.getByText(/add additional email addresses/i)
     screen.getByText(/to change your primary email/i)
     screen.getByText(/to change your primary email/i)
@@ -46,14 +55,14 @@ describe('<EmailsSection />', function () {
   })
   })
 
 
   it('renders a loading message when loading', async function () {
   it('renders a loading message when loading', async function () {
-    render(<EmailsSection />)
+    renderEmailsSection()
 
 
     await screen.findByText(/loading/i)
     await screen.findByText(/loading/i)
   })
   })
 
 
   it('renders an error message and hides loading message on error', async function () {
   it('renders an error message and hides loading message on error', async function () {
     fetchMock.get('/user/emails?ensureAffiliation=true', 500)
     fetchMock.get('/user/emails?ensureAffiliation=true', 500)
-    render(<EmailsSection />)
+    renderEmailsSection()
 
 
     await screen.findByText(
     await screen.findByText(
       /an error has occurred while performing your request/i
       /an error has occurred while performing your request/i
@@ -63,7 +72,7 @@ describe('<EmailsSection />', function () {
 
 
   it('renders user emails', async function () {
   it('renders user emails', async function () {
     fetchMock.get('/user/emails?ensureAffiliation=true', fakeUsersData)
     fetchMock.get('/user/emails?ensureAffiliation=true', fakeUsersData)
-    render(<EmailsSection />)
+    renderEmailsSection()
 
 
     await waitFor(() => {
     await waitFor(() => {
       fakeUsersData.forEach(userData => {
       fakeUsersData.forEach(userData => {
@@ -74,7 +83,7 @@ describe('<EmailsSection />', function () {
 
 
   it('renders primary status', async function () {
   it('renders primary status', async function () {
     fetchMock.get('/user/emails?ensureAffiliation=true', [professionalUserData])
     fetchMock.get('/user/emails?ensureAffiliation=true', [professionalUserData])
-    render(<EmailsSection />)
+    renderEmailsSection()
 
 
     await screen.findByText(`${professionalUserData.email}`)
     await screen.findByText(`${professionalUserData.email}`)
     screen.getByText('Primary')
     screen.getByText('Primary')
@@ -82,14 +91,14 @@ describe('<EmailsSection />', function () {
 
 
   it('shows confirmation status for unconfirmed users', async function () {
   it('shows confirmation status for unconfirmed users', async function () {
     fetchMock.get('/user/emails?ensureAffiliation=true', [unconfirmedUserData])
     fetchMock.get('/user/emails?ensureAffiliation=true', [unconfirmedUserData])
-    render(<EmailsSection />)
+    renderEmailsSection()
 
 
     await screen.findByText(/unconfirmed/i)
     await screen.findByText(/unconfirmed/i)
   })
   })
 
 
   it('hides confirmation status for confirmed users', async function () {
   it('hides confirmation status for confirmed users', async function () {
     fetchMock.get('/user/emails?ensureAffiliation=true', [confirmedUserData])
     fetchMock.get('/user/emails?ensureAffiliation=true', [confirmedUserData])
-    render(<EmailsSection />)
+    renderEmailsSection()
     await waitForElementToBeRemoved(() => screen.getByText(/loading/i))
     await waitForElementToBeRemoved(() => screen.getByText(/loading/i))
 
 
     expect(screen.queryByText(/please check your inbox/i)).to.be.null
     expect(screen.queryByText(/please check your inbox/i)).to.be.null
@@ -97,14 +106,14 @@ describe('<EmailsSection />', function () {
 
 
   it('renders resend link', async function () {
   it('renders resend link', async function () {
     fetchMock.get('/user/emails?ensureAffiliation=true', [unconfirmedUserData])
     fetchMock.get('/user/emails?ensureAffiliation=true', [unconfirmedUserData])
-    render(<EmailsSection />)
+    renderEmailsSection()
 
 
     await screen.findByRole('button', { name: 'Send confirmation code' })
     await screen.findByRole('button', { name: 'Send confirmation code' })
   })
   })
 
 
   it('renders professional label', async function () {
   it('renders professional label', async function () {
     fetchMock.get('/user/emails?ensureAffiliation=true', [professionalUserData])
     fetchMock.get('/user/emails?ensureAffiliation=true', [professionalUserData])
-    render(<EmailsSection />)
+    renderEmailsSection()
 
 
     const node = await screen.findByText(professionalUserData.email, {
     const node = await screen.findByText(professionalUserData.email, {
       exact: false,
       exact: false,
@@ -115,7 +124,7 @@ describe('<EmailsSection />', function () {
   it('shows loader when resending email', async function () {
   it('shows loader when resending email', async function () {
     fetchMock.get('/user/emails?ensureAffiliation=true', [unconfirmedUserData])
     fetchMock.get('/user/emails?ensureAffiliation=true', [unconfirmedUserData])
 
 
-    render(<EmailsSection />)
+    renderEmailsSection()
     await waitForElementToBeRemoved(() => screen.getByText(/loading/i))
     await waitForElementToBeRemoved(() => screen.getByText(/loading/i))
 
 
     fetchMock.post('/user/emails/send-confirmation-code', 200)
     fetchMock.post('/user/emails/send-confirmation-code', 200)
@@ -145,7 +154,7 @@ describe('<EmailsSection />', function () {
   it('shows error when resending email fails', async function () {
   it('shows error when resending email fails', async function () {
     fetchMock.get('/user/emails?ensureAffiliation=true', [unconfirmedUserData])
     fetchMock.get('/user/emails?ensureAffiliation=true', [unconfirmedUserData])
 
 
-    render(<EmailsSection />)
+    renderEmailsSection()
     await waitForElementToBeRemoved(() => screen.getByText(/loading/i))
     await waitForElementToBeRemoved(() => screen.getByText(/loading/i))
 
 
     fetchMock.post('/user/emails/send-confirmation-code', 503)
     fetchMock.post('/user/emails/send-confirmation-code', 503)
@@ -195,7 +204,7 @@ describe('<EmailsSection />', function () {
     ]
     ]
 
 
     fetchMock.get('/user/emails?ensureAffiliation=true', emails)
     fetchMock.get('/user/emails?ensureAffiliation=true', emails)
-    render(<EmailsSection />)
+    renderEmailsSection()
 
 
     await waitForElementToBeRemoved(() => screen.getByText(/loading/i))
     await waitForElementToBeRemoved(() => screen.getByText(/loading/i))
 
 

+ 93 - 0
services/web/test/frontend/features/settings/utils/sso.test.ts

@@ -0,0 +1,93 @@
+import { expect } from 'chai'
+import { ssoAvailableForDomain } from '../../../../../frontend/js/features/settings/utils/sso'
+import { DomainInfo } from '../../../../../frontend/js/features/settings/components/emails/add-email/input'
+
+const baseDomain: DomainInfo = {
+  hostname: 'example.edu',
+  confirmed: true,
+  university: {
+    id: 1,
+    name: 'Example University',
+    ssoEnabled: false,
+    ssoBeta: false,
+  },
+  group: {
+    domainCaptureEnabled: false,
+    ssoConfig: {
+      enabled: false,
+    },
+  },
+}
+
+describe('ssoAvailableForDomain', function () {
+  beforeEach(function () {
+    window.metaAttributesCache.set('ol-ExposedSettings', {
+      hasSamlFeature: true,
+      hasSamlBeta: false,
+    })
+  })
+
+  it('returns false when hasSamlFeature is false', function () {
+    window.metaAttributesCache.set('ol-ExposedSettings', {
+      hasSamlFeature: false,
+      hasSamlBeta: false,
+    })
+    expect(ssoAvailableForDomain(baseDomain, false)).to.be.false
+  })
+
+  it('returns false when domain is null', function () {
+    expect(ssoAvailableForDomain(null, false)).to.be.false
+  })
+
+  it('returns false when domain is not confirmed', function () {
+    const domain = { ...baseDomain, confirmed: false }
+    expect(ssoAvailableForDomain(domain, false)).to.be.false
+  })
+
+  it('returns true when university.ssoEnabled is true, regardless of isDomainCapturedByGroup', function () {
+    const domain: DomainInfo = {
+      ...baseDomain,
+      university: { ...baseDomain.university, ssoEnabled: true },
+    }
+    expect(ssoAvailableForDomain(domain, false)).to.be.true
+    expect(ssoAvailableForDomain(domain, true)).to.be.true
+  })
+
+  describe('group SSO via ssoConfig', function () {
+    const domainWithGroupSso: DomainInfo = {
+      ...baseDomain,
+      group: {
+        domainCaptureEnabled: true,
+        ssoConfig: { enabled: true },
+      },
+    }
+
+    it('returns true when isDomainCapturedByGroup is true and group ssoConfig is enabled', function () {
+      expect(ssoAvailableForDomain(domainWithGroupSso, true)).to.be.true
+    })
+
+    it('returns false when isDomainCapturedByGroup is false even if group ssoConfig is enabled', function () {
+      expect(ssoAvailableForDomain(domainWithGroupSso, false)).to.be.false
+    })
+  })
+
+  it('returns true when hasSamlBeta and university.ssoBeta are both true', function () {
+    window.metaAttributesCache.set('ol-ExposedSettings', {
+      hasSamlFeature: true,
+      hasSamlBeta: true,
+    })
+    const domain: DomainInfo = {
+      ...baseDomain,
+      university: { ...baseDomain.university, ssoBeta: true },
+    }
+    expect(ssoAvailableForDomain(domain, false)).to.be.true
+  })
+
+  it('returns false when hasSamlBeta is false and university.ssoBeta is true', function () {
+    const domain: DomainInfo = {
+      ...baseDomain,
+      university: { ...baseDomain.university, ssoBeta: true },
+    }
+    expect(ssoAvailableForDomain(domain, false)).to.be.false
+  })
+})