emails-section-add-new-email.test.tsx 34 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126
  1. import {
  2. render,
  3. screen,
  4. fireEvent,
  5. waitForElementToBeRemoved,
  6. within,
  7. } from '@testing-library/react'
  8. import userEvent from '@testing-library/user-event'
  9. import EmailsSection from '../../../../../../frontend/js/features/settings/components/emails-section'
  10. import { expect } from 'chai'
  11. import fetchMock from 'fetch-mock'
  12. import { UserEmailData } from '../../../../../../types/user-email'
  13. import { Affiliation } from '../../../../../../types/affiliation'
  14. import withMarkup from '../../../../helpers/with-markup'
  15. import getMeta from '@/utils/meta'
  16. import { clearDomainCache } from '../../../../../../frontend/js/features/settings/components/emails/add-email/input'
  17. import { SplitTestProvider } from '@/shared/context/split-test-context'
  18. const userEmailData: UserEmailData & { affiliation: Affiliation } = {
  19. affiliation: {
  20. cachedConfirmedAt: null,
  21. cachedEntitlement: null,
  22. cachedLastDayToReconfirm: null,
  23. cachedPastReconfirmDate: false,
  24. cachedReconfirmedAt: null,
  25. department: 'Art History',
  26. institution: {
  27. commonsAccount: false,
  28. enterpriseCommons: false,
  29. writefullCommonsAccount: false,
  30. confirmed: true,
  31. id: 1,
  32. isUniversity: true,
  33. maxConfirmationMonths: null,
  34. name: 'Overleaf',
  35. ssoEnabled: false,
  36. ssoBeta: false,
  37. },
  38. inReconfirmNotificationPeriod: false,
  39. inferred: false,
  40. licence: 'pro_plus',
  41. pastReconfirmDate: false,
  42. portal: { slug: '', templates_count: 1 },
  43. role: 'Reader',
  44. },
  45. email: 'baz@overleaf.com',
  46. default: false,
  47. }
  48. const institutionDomainData = [
  49. {
  50. university: {
  51. id: 1234,
  52. ssoEnabled: true,
  53. name: 'Auto Complete University',
  54. },
  55. hostname: 'autocomplete.edu',
  56. confirmed: true,
  57. },
  58. ] as const
  59. function renderEmailsSection() {
  60. return render(<EmailsSection />, {
  61. wrapper: ({ children }) => (
  62. <SplitTestProvider>{children}</SplitTestProvider>
  63. ),
  64. })
  65. }
  66. function resetFetchMock() {
  67. fetchMock.removeRoutes().clearHistory()
  68. fetchMock.get('express:/institutions/domains', [])
  69. }
  70. async function confirmCodeForEmail(email: string) {
  71. const inputCode = await screen.findByRole('textbox', {
  72. name: /Enter the 6-digit confirmation code sent to /,
  73. })
  74. screen.getByText(email, { exact: false })
  75. fireEvent.change(inputCode, { target: { value: '123456' } })
  76. const submitCodeBtn = screen.getByRole<HTMLButtonElement>('button', {
  77. name: 'Confirm',
  78. })
  79. fireEvent.click(submitCodeBtn)
  80. await waitForElementToBeRemoved(() =>
  81. screen.getByRole('button', { name: 'Confirming' })
  82. )
  83. }
  84. describe('<EmailsSection />', function () {
  85. beforeEach(function () {
  86. Object.assign(getMeta('ol-ExposedSettings'), {
  87. hasAffiliationsFeature: true,
  88. hasSamlFeature: true,
  89. samlInitPath: 'saml/init',
  90. })
  91. fetchMock.removeRoutes().clearHistory()
  92. })
  93. afterEach(function () {
  94. resetFetchMock()
  95. clearDomainCache()
  96. })
  97. it('renders "add another email" button', async function () {
  98. fetchMock.get('/user/emails?ensureAffiliation=true', [])
  99. renderEmailsSection()
  100. await screen.findByRole('button', { name: 'Add another email' })
  101. })
  102. it('renders input', async function () {
  103. fetchMock.get('/user/emails?ensureAffiliation=true', [])
  104. renderEmailsSection()
  105. await fetchMock.callHistory.flush(true)
  106. const button = await screen.findByRole<HTMLButtonElement>('button', {
  107. name: 'Add another email',
  108. })
  109. fireEvent.click(button)
  110. await screen.findByRole('textbox', { name: 'Email' })
  111. })
  112. it('renders "Start adding your address" until a valid email is typed', async function () {
  113. fetchMock.get('/user/emails?ensureAffiliation=true', [])
  114. fetchMock.get(`/institutions/domains?hostname=email.com&limit=1`, 200)
  115. fetchMock.get(`/institutions/domains?hostname=email&limit=1`, 200)
  116. renderEmailsSection()
  117. await fetchMock.callHistory.flush(true)
  118. const button = await screen.findByRole<HTMLButtonElement>('button', {
  119. name: 'Add another email',
  120. })
  121. fireEvent.click(button)
  122. const input = screen.getByRole('textbox', { name: 'Email' })
  123. // initially the text is displayed and the "add email" button disabled
  124. screen.getByText('Start by adding your email address.')
  125. expect(
  126. screen.getByRole<HTMLButtonElement>('button', {
  127. name: 'Add new email',
  128. }).disabled
  129. ).to.be.true
  130. // no changes while writing the email address
  131. fireEvent.change(input, {
  132. target: { value: 'partial@email' },
  133. })
  134. screen.getByText('Start by adding your email address.')
  135. expect(
  136. screen.getByRole<HTMLButtonElement>('button', {
  137. name: 'Add new email',
  138. }).disabled
  139. ).to.be.true
  140. // the text is removed when the complete email address is typed, and the "add button" is reenabled
  141. fireEvent.change(input, {
  142. target: { value: 'valid@email.com' },
  143. })
  144. expect(screen.queryByText('Start by adding your email address.')).to.be.null
  145. expect(
  146. screen.getByRole<HTMLButtonElement>('button', {
  147. name: 'Add new email',
  148. }).disabled
  149. ).to.be.false
  150. })
  151. it('renders "add new email" button', async function () {
  152. fetchMock.get('/user/emails?ensureAffiliation=true', [])
  153. renderEmailsSection()
  154. const button = await screen.findByRole<HTMLButtonElement>('button', {
  155. name: 'Add another email',
  156. })
  157. fireEvent.click(button)
  158. screen.getByRole('button', { name: 'Add new email' })
  159. })
  160. it('prevent users from adding new emails when the limit is reached', async function () {
  161. const emails = []
  162. for (let i = 0; i < 10; i++) {
  163. emails.push({ email: `bar${i}@overleaf.com` })
  164. }
  165. fetchMock.get('/user/emails?ensureAffiliation=true', emails)
  166. renderEmailsSection()
  167. const findByTextWithMarkup = withMarkup(screen.findByText)
  168. await findByTextWithMarkup(
  169. 'You can have a maximum of 10 email addresses on this account. To add another email address, please delete an existing one.'
  170. )
  171. expect(screen.queryByRole('button', { name: 'Add another email' })).to.not
  172. .exist
  173. })
  174. it('adds new email address', async function () {
  175. fetchMock.get('/user/emails?ensureAffiliation=true', [])
  176. renderEmailsSection()
  177. const addAnotherEmailBtn = await screen.findByRole<HTMLButtonElement>(
  178. 'button',
  179. { name: 'Add another email' }
  180. )
  181. await fetchMock.callHistory.flush(true)
  182. resetFetchMock()
  183. fetchMock
  184. .get('/user/emails?ensureAffiliation=true', [userEmailData])
  185. .post('/user/emails/secondary', 200)
  186. .post('/user/emails/confirm-secondary', 200)
  187. fireEvent.click(addAnotherEmailBtn)
  188. const input = screen.getByRole('textbox', { name: 'Email' })
  189. fireEvent.change(input, {
  190. target: { value: userEmailData.email },
  191. })
  192. const submitBtn = screen.getByRole<HTMLButtonElement>('button', {
  193. name: 'Add new email',
  194. })
  195. expect(submitBtn.disabled).to.be.false
  196. fireEvent.click(submitBtn)
  197. expect(submitBtn.disabled).to.be.true
  198. await waitForElementToBeRemoved(() =>
  199. screen.getByRole('button', {
  200. name: /adding/i,
  201. })
  202. )
  203. await confirmCodeForEmail(userEmailData.email)
  204. const elements = await screen.findAllByText(userEmailData.email)
  205. expect(elements.length).to.equal(2)
  206. })
  207. it('fails to add add new email address', async function () {
  208. fetchMock.get('/user/emails?ensureAffiliation=true', [])
  209. renderEmailsSection()
  210. const addAnotherEmailBtn = await screen.findByRole<HTMLButtonElement>(
  211. 'button',
  212. { name: 'Add another email' }
  213. )
  214. await fetchMock.callHistory.flush(true)
  215. resetFetchMock()
  216. fetchMock
  217. .get('/user/emails?ensureAffiliation=true', [])
  218. .post('/user/emails/secondary', 400)
  219. fireEvent.click(addAnotherEmailBtn)
  220. const input = screen.getByRole('textbox', { name: 'Email' })
  221. fireEvent.change(input, {
  222. target: { value: userEmailData.email },
  223. })
  224. const submitBtn = screen.getByRole<HTMLButtonElement>('button', {
  225. name: 'Add new email',
  226. })
  227. expect(submitBtn.disabled).to.be.false
  228. fireEvent.click(submitBtn)
  229. expect(submitBtn.disabled).to.be.true
  230. await screen.findByText(
  231. 'Invalid Request. Please correct the data and try again.'
  232. )
  233. expect(submitBtn).to.not.be.null
  234. expect(submitBtn.disabled).to.be.false
  235. })
  236. it('can link email address to an existing SSO institution', async function () {
  237. fetchMock.get('/user/emails?ensureAffiliation=true', [])
  238. renderEmailsSection()
  239. const button = await screen.findByRole<HTMLButtonElement>('button', {
  240. name: 'Add another email',
  241. })
  242. await fetchMock.callHistory.flush(true)
  243. fetchMock.removeRoutes().clearHistory()
  244. fetchMock.get('express:/institutions/domains', institutionDomainData)
  245. await userEvent.click(button)
  246. const input = screen.getByRole('textbox', { name: 'Email' })
  247. fireEvent.change(input, {
  248. target: { value: 'user@autocomplete.edu' },
  249. })
  250. await screen.findByRole('button', { name: 'Link accounts and add email' })
  251. })
  252. it('prevents user from linking to same SSO institution twice', async function () {
  253. fetchMock.get('/user/emails?ensureAffiliation=true', [
  254. { email: 'bar@autocomplete.edu', samlProviderId: '1234' },
  255. ])
  256. renderEmailsSection()
  257. const button = await screen.findByRole<HTMLButtonElement>('button', {
  258. name: 'Add another email',
  259. })
  260. await fetchMock.callHistory.flush(true)
  261. fetchMock.removeRoutes().clearHistory()
  262. fetchMock.get('express:/institutions/domains', institutionDomainData)
  263. await userEvent.click(button)
  264. const input = screen.getByRole('textbox', { name: 'Email' })
  265. fireEvent.change(input, {
  266. target: { value: 'baz@autocomplete.edu' },
  267. })
  268. await screen.findByText(
  269. 'This institution is already linked with your account via another email address.'
  270. )
  271. })
  272. it('adds new email address with existing institution and custom departments', async function () {
  273. const country = 'Germany'
  274. const customDepartment = 'Custom department'
  275. fetchMock.get('/user/emails?ensureAffiliation=true', [])
  276. renderEmailsSection()
  277. const button = await screen.findByRole<HTMLButtonElement>('button', {
  278. name: 'Add another email',
  279. })
  280. await fetchMock.callHistory.flush(true)
  281. resetFetchMock()
  282. await userEvent.click(button)
  283. await userEvent.type(
  284. screen.getByRole('textbox', { name: 'Email' }),
  285. userEmailData.email
  286. )
  287. await userEvent.click(screen.getByRole('button', { name: 'Let us know' }))
  288. const universityInput = screen.getByRole<HTMLInputElement>('combobox', {
  289. name: 'University',
  290. })
  291. expect(universityInput.disabled).to.be.true
  292. fetchMock.get('/institutions/list?country_code=de', [
  293. {
  294. id: userEmailData.affiliation.institution.id,
  295. name: userEmailData.affiliation.institution.name,
  296. country_code: 'de',
  297. departments: [customDepartment],
  298. },
  299. ])
  300. // Select the country from dropdown
  301. await userEvent.type(
  302. screen.getByRole('combobox', {
  303. name: 'Country',
  304. }),
  305. country
  306. )
  307. await userEvent.click(screen.getByText(country))
  308. expect(universityInput.disabled).to.be.false
  309. await fetchMock.callHistory.flush(true)
  310. resetFetchMock()
  311. // Select the university from dropdown
  312. await userEvent.click(universityInput)
  313. await userEvent.click(
  314. await screen.findByText(userEmailData.affiliation.institution.name)
  315. )
  316. const roleInput = screen.getByRole('combobox', { name: 'Role' })
  317. await userEvent.type(roleInput, userEmailData.affiliation.role!)
  318. const departmentInput = screen.getByRole('combobox', {
  319. name: 'Department',
  320. })
  321. await userEvent.click(departmentInput)
  322. await userEvent.click(screen.getByText(customDepartment))
  323. const userEmailDataCopy = {
  324. ...userEmailData,
  325. affiliation: {
  326. ...userEmailData.affiliation,
  327. department: customDepartment,
  328. },
  329. }
  330. fetchMock
  331. .get('/user/emails?ensureAffiliation=true', [userEmailDataCopy])
  332. .post('/user/emails/secondary', 200)
  333. .post('/user/emails/confirm-secondary', 200)
  334. await userEvent.click(
  335. screen.getByRole('button', {
  336. name: 'Add new email',
  337. })
  338. )
  339. const request = fetchMock.callHistory.calls(/\/user\/emails/).at(0)
  340. expect(
  341. JSON.parse(request?.options.body?.toString() || '{}')
  342. ).to.deep.include({
  343. email: userEmailData.email,
  344. university: {
  345. id: userEmailData.affiliation?.institution.id,
  346. },
  347. role: userEmailData.affiliation?.role,
  348. department: customDepartment,
  349. })
  350. await screen.findByText(/Enter the 6-digit confirmation code sent to /)
  351. await confirmCodeForEmail(userEmailData.email)
  352. await screen.findByText(userEmailData.affiliation.role!, { exact: false })
  353. await screen.findByText(customDepartment, { exact: false })
  354. })
  355. it('autocompletes institution name', async function () {
  356. fetchMock.get('/user/emails?ensureAffiliation=true', [])
  357. renderEmailsSection()
  358. const button = await screen.findByRole<HTMLButtonElement>('button', {
  359. name: 'Add another email',
  360. })
  361. await fetchMock.callHistory.flush(true)
  362. resetFetchMock()
  363. fetchMock.get('/institutions/list?country_code=de', [
  364. {
  365. id: 1,
  366. name: 'University of Bonn',
  367. },
  368. {
  369. id: 2,
  370. name: 'Bochum institute of Science',
  371. },
  372. ])
  373. // open "add new email" section and click "let us know" to open the Country/University form
  374. await userEvent.click(button)
  375. await userEvent.type(
  376. screen.getByRole('textbox', { name: 'Email' }),
  377. userEmailData.email
  378. )
  379. await userEvent.click(screen.getByRole('button', { name: 'Let us know' }))
  380. // select a country
  381. const countryInput = screen.getByRole<HTMLInputElement>('combobox', {
  382. name: 'Country',
  383. })
  384. await userEvent.click(countryInput)
  385. await userEvent.type(countryInput, 'Germ')
  386. await userEvent.click(await screen.findByText('Germany'))
  387. // match several universities on initial typing
  388. const universityInput = screen.getByRole<HTMLInputElement>('combobox', {
  389. name: 'University',
  390. })
  391. await userEvent.click(universityInput)
  392. await userEvent.type(universityInput, 'bo')
  393. await screen.findByText('University of Bonn')
  394. await screen.findByText('Bochum institute of Science')
  395. // match a single university when typing to refine the search
  396. await userEvent.type(universityInput, 'nn')
  397. await screen.findByText('University of Bonn')
  398. expect(screen.queryByText('Bochum institute of Science')).to.be.null
  399. })
  400. it('adds new email address without existing institution', async function () {
  401. const country = 'Germany'
  402. const countryCode = 'de'
  403. const newUniversity = 'Abcdef'
  404. fetchMock.get('/user/emails?ensureAffiliation=true', [])
  405. renderEmailsSection()
  406. const button = await screen.findByRole<HTMLButtonElement>('button', {
  407. name: 'Add another email',
  408. })
  409. await fetchMock.callHistory.flush(true)
  410. resetFetchMock()
  411. await userEvent.click(button)
  412. await userEvent.type(
  413. screen.getByRole('textbox', { name: 'Email' }),
  414. userEmailData.email
  415. )
  416. await userEvent.click(screen.getByRole('button', { name: 'Let us know' }))
  417. const universityInput = screen.getByRole<HTMLInputElement>('combobox', {
  418. name: 'University',
  419. })
  420. expect(universityInput.disabled).to.be.true
  421. fetchMock.get(/\/institutions\/list/, [
  422. {
  423. id: userEmailData.affiliation.institution.id,
  424. name: userEmailData.affiliation.institution.name,
  425. country_code: 'de',
  426. departments: [],
  427. },
  428. ])
  429. // Select the country from dropdown
  430. await userEvent.type(
  431. screen.getByRole('combobox', {
  432. name: 'Country',
  433. }),
  434. country
  435. )
  436. await userEvent.click(screen.getByText(country))
  437. expect(universityInput.disabled).to.be.false
  438. await fetchMock.callHistory.flush(true)
  439. resetFetchMock()
  440. // Enter the university manually
  441. await userEvent.type(universityInput, newUniversity)
  442. const roleInput = screen.getByRole('combobox', { name: 'Role' })
  443. await userEvent.type(roleInput, userEmailData.affiliation.role!)
  444. const departmentInput = screen.getByRole('combobox', {
  445. name: 'Department',
  446. })
  447. await userEvent.type(departmentInput, userEmailData.affiliation.department!)
  448. const userEmailDataCopy = {
  449. ...userEmailData,
  450. affiliation: {
  451. ...userEmailData.affiliation,
  452. institution: {
  453. ...userEmailData.affiliation.institution,
  454. name: newUniversity,
  455. },
  456. },
  457. }
  458. fetchMock
  459. .get('/user/emails?ensureAffiliation=true', [userEmailDataCopy])
  460. .post('/user/emails/secondary', 200)
  461. .post('/user/emails/confirm-secondary', 200)
  462. await userEvent.click(
  463. screen.getByRole('button', {
  464. name: 'Add new email',
  465. })
  466. )
  467. await confirmCodeForEmail(userEmailData.email)
  468. const request = fetchMock.callHistory.calls(/\/user\/emails/).at(0)
  469. expect(
  470. JSON.parse(request?.options.body?.toString() || '{}')
  471. ).to.deep.include({
  472. email: userEmailData.email,
  473. university: {
  474. name: newUniversity,
  475. country_code: countryCode,
  476. },
  477. role: userEmailData.affiliation?.role,
  478. department: userEmailData.affiliation?.department,
  479. })
  480. const elements = await screen.findAllByText(userEmailData.email)
  481. expect(elements.length).to.equal(2)
  482. await screen.findByText(newUniversity)
  483. await screen.findByText(userEmailData.affiliation.role!, { exact: false })
  484. await screen.findByText(userEmailData.affiliation.department!, {
  485. exact: false,
  486. })
  487. })
  488. it('shows country, university, role and department fields based on whether `change` was clicked or not', async function () {
  489. const institutionDomainDataCopy = [
  490. {
  491. ...institutionDomainData[0],
  492. university: {
  493. ...institutionDomainData[0].university,
  494. ssoEnabled: false,
  495. },
  496. },
  497. ]
  498. const hostnameFirstChar = institutionDomainDataCopy[0].hostname.charAt(0)
  499. fetchMock.get('/user/emails?ensureAffiliation=true', [])
  500. renderEmailsSection()
  501. const button = await screen.findByRole<HTMLButtonElement>('button', {
  502. name: 'Add another email',
  503. })
  504. await fetchMock.callHistory.flush(true)
  505. fetchMock.removeRoutes().clearHistory()
  506. fetchMock.get(
  507. `/institutions/domains?hostname=${hostnameFirstChar}&limit=1`,
  508. institutionDomainDataCopy
  509. )
  510. await userEvent.click(button)
  511. await userEvent.type(
  512. screen.getByRole('textbox', { name: 'Email' }),
  513. `user@${hostnameFirstChar}`
  514. )
  515. await userEvent.keyboard('{Tab}')
  516. await fetchMock.callHistory.flush(true)
  517. fetchMock.removeRoutes().clearHistory()
  518. expect(
  519. screen.queryByRole('combobox', {
  520. name: 'Country',
  521. })
  522. ).to.be.null
  523. expect(
  524. screen.queryByRole('combobox', {
  525. name: 'University',
  526. })
  527. ).to.be.null
  528. screen.getByRole('combobox', {
  529. name: 'Role',
  530. })
  531. screen.getByRole('combobox', {
  532. name: 'Department',
  533. })
  534. await userEvent.click(screen.getByRole('button', { name: 'Change' }))
  535. screen.getByRole('combobox', {
  536. name: 'Country',
  537. })
  538. screen.getByRole('combobox', {
  539. name: 'University',
  540. })
  541. expect(
  542. screen.queryByRole('combobox', {
  543. name: 'Role',
  544. })
  545. ).to.be.null
  546. expect(
  547. screen.queryByRole('combobox', {
  548. name: 'Department',
  549. })
  550. ).to.be.null
  551. })
  552. it('displays institution name with change button when autocompleted and adds new record', async function () {
  553. const institutionDomainDataCopy = [
  554. {
  555. ...institutionDomainData[0],
  556. university: {
  557. ...institutionDomainData[0].university,
  558. ssoEnabled: false,
  559. },
  560. },
  561. ]
  562. const hostnameFirstChar = institutionDomainDataCopy[0].hostname.charAt(0)
  563. fetchMock.get('/user/emails?ensureAffiliation=true', [])
  564. renderEmailsSection()
  565. const button = await screen.findByRole<HTMLButtonElement>('button', {
  566. name: 'Add another email',
  567. })
  568. await fetchMock.callHistory.flush(true)
  569. fetchMock.removeRoutes().clearHistory()
  570. fetchMock.get(
  571. `/institutions/domains?hostname=${hostnameFirstChar}&limit=1`,
  572. institutionDomainDataCopy
  573. )
  574. await userEvent.click(button)
  575. await userEvent.type(
  576. screen.getByRole('textbox', { name: 'Email' }),
  577. `user@${hostnameFirstChar}`
  578. )
  579. await userEvent.keyboard('{Tab}')
  580. await fetchMock.callHistory.flush(true)
  581. fetchMock.removeRoutes().clearHistory()
  582. screen.getByText(institutionDomainDataCopy[0].university.name)
  583. const userEmailDataCopy = {
  584. ...userEmailData,
  585. affiliation: {
  586. ...userEmailData.affiliation,
  587. institution: {
  588. ...userEmailData.affiliation.institution,
  589. name: institutionDomainDataCopy[0].university.name,
  590. },
  591. },
  592. }
  593. fetchMock
  594. .get('/user/emails?ensureAffiliation=true', [userEmailDataCopy])
  595. .post('/user/emails/secondary', 200)
  596. .post('/user/emails/confirm-secondary', 200)
  597. await userEvent.type(
  598. screen.getByRole('combobox', { name: 'Role' }),
  599. userEmailData.affiliation.role!
  600. )
  601. await userEvent.type(
  602. screen.getByRole('combobox', { name: 'Department' }),
  603. userEmailData.affiliation.department!
  604. )
  605. await userEvent.click(
  606. screen.getByRole('button', {
  607. name: 'Add new email',
  608. })
  609. )
  610. await confirmCodeForEmail('user@autocomplete.edu')
  611. await fetchMock.callHistory.flush(true)
  612. fetchMock.removeRoutes().clearHistory()
  613. screen.getByText(userEmailDataCopy.affiliation.institution.name, {
  614. exact: false,
  615. })
  616. screen.getByText(userEmailDataCopy.affiliation.role!, { exact: false })
  617. screen.getByText(userEmailDataCopy.affiliation.department!, {
  618. exact: false,
  619. })
  620. })
  621. describe('when domain is captured by a group', function () {
  622. describe('and managed users is not enabled', function () {
  623. beforeEach(async function () {
  624. await fetchMock.callHistory.flush(true)
  625. fetchMock.removeRoutes().clearHistory()
  626. const institution = {
  627. university: {
  628. id: 1234,
  629. ssoEnabled: false,
  630. name: 'Auto Complete University',
  631. },
  632. hostname: 'autocomplete.edu',
  633. confirmed: true,
  634. group: {
  635. domainCaptureEnabled: true,
  636. ssoConfig: {
  637. enabled: true,
  638. },
  639. },
  640. }
  641. fetchMock.get('express:/institutions/domains', [institution])
  642. })
  643. it('can add email address via SSO', async function () {
  644. // note: this UI is a WIP
  645. fetchMock.get('/user/emails?ensureAffiliation=true', [])
  646. renderEmailsSection()
  647. const button = await screen.findByRole<HTMLButtonElement>('button', {
  648. name: 'Add another email',
  649. })
  650. await userEvent.click(button)
  651. const input = screen.getByRole('textbox', { name: 'Email' })
  652. fireEvent.change(input, {
  653. target: { value: 'user@autocomplete.edu' },
  654. })
  655. await screen.findByText('This feature is currently unavailable.')
  656. })
  657. })
  658. describe('and managed users is enabled', function () {
  659. beforeEach(async function () {
  660. await fetchMock.callHistory.flush(true)
  661. fetchMock.removeRoutes().clearHistory()
  662. const institution = {
  663. university: {
  664. id: 1234,
  665. ssoEnabled: false,
  666. name: 'Auto Complete University',
  667. },
  668. hostname: 'autocomplete.edu',
  669. confirmed: true,
  670. group: {
  671. domainCaptureEnabled: true,
  672. managedUsersEnabled: true,
  673. ssoConfig: {
  674. enabled: true,
  675. },
  676. },
  677. }
  678. fetchMock.get('express:/institutions/domains', [institution])
  679. })
  680. it('renders error', async function () {
  681. // note: this UI is a WIP
  682. fetchMock.get('/user/emails?ensureAffiliation=true', [])
  683. renderEmailsSection()
  684. const button = await screen.findByRole<HTMLButtonElement>('button', {
  685. name: 'Add another email',
  686. })
  687. await userEvent.click(button)
  688. const input = screen.getByRole('textbox', { name: 'Email' })
  689. fireEvent.change(input, {
  690. target: { value: 'user@autocomplete.edu' },
  691. })
  692. const notification = await screen.findByRole('alert')
  693. within(notification).getByText(
  694. 'Your company email address has been registered under a verified domain, and cannot be added as a secondary email.',
  695. { exact: false }
  696. )
  697. })
  698. })
  699. describe('if Commons SSO is enabled and managed users is not enabled, Commons takes priority over group UI', function () {
  700. beforeEach(async function () {
  701. await fetchMock.callHistory.flush(true)
  702. fetchMock.removeRoutes().clearHistory()
  703. const institution = {
  704. university: {
  705. id: 1234,
  706. ssoEnabled: true,
  707. name: 'Auto Complete University',
  708. },
  709. hostname: 'autocomplete.edu',
  710. confirmed: true,
  711. group: {
  712. domainCaptureEnabled: true,
  713. ssoConfig: {
  714. enabled: true,
  715. },
  716. },
  717. }
  718. fetchMock.get('express:/institutions/domains', [institution])
  719. })
  720. it('renders Commons UI', async function () {
  721. fetchMock.get('/user/emails?ensureAffiliation=true', [])
  722. renderEmailsSection()
  723. const button = await screen.findByRole<HTMLButtonElement>('button', {
  724. name: 'Add another email',
  725. })
  726. await userEvent.click(button)
  727. const input = screen.getByRole('textbox', { name: 'Email' })
  728. fireEvent.change(input, {
  729. target: { value: 'user@autocomplete.edu' },
  730. })
  731. await screen.findByRole('button', {
  732. name: 'Link accounts and add email',
  733. })
  734. })
  735. })
  736. describe('if Commons SSO is enabled and managed users is enabled, group domain capture takes priority', function () {
  737. beforeEach(async function () {
  738. await fetchMock.callHistory.flush(true)
  739. fetchMock.removeRoutes().clearHistory()
  740. const institution = {
  741. university: {
  742. id: 1234,
  743. ssoEnabled: true,
  744. name: 'Auto Complete University',
  745. },
  746. hostname: 'autocomplete.edu',
  747. confirmed: true,
  748. group: {
  749. domainCaptureEnabled: true,
  750. managedUsersEnabled: true,
  751. ssoConfig: {
  752. enabled: true,
  753. },
  754. },
  755. }
  756. fetchMock.get('express:/institutions/domains', [institution])
  757. })
  758. it('renders group domain-capture error instead of Commons UI', async function () {
  759. fetchMock.get('/user/emails?ensureAffiliation=true', [])
  760. renderEmailsSection()
  761. const button = await screen.findByRole<HTMLButtonElement>('button', {
  762. name: 'Add another email',
  763. })
  764. await userEvent.click(button)
  765. const input = screen.getByRole('textbox', { name: 'Email' })
  766. fireEvent.change(input, {
  767. target: { value: 'user@autocomplete.edu' },
  768. })
  769. const notification = await screen.findByRole('alert')
  770. within(notification).getByText(
  771. 'Your company email address has been registered under a verified domain, and cannot be added as a secondary email.',
  772. { exact: false }
  773. )
  774. expect(
  775. screen.queryByRole('button', {
  776. name: 'Link accounts and add email',
  777. })
  778. ).to.be.null
  779. })
  780. })
  781. describe('when the domain-captured-by-group feature flag is enabled', function () {
  782. beforeEach(function () {
  783. window.metaAttributesCache.set('ol-splitTestVariants', {
  784. 'domain-captured-by-group': 'enabled',
  785. })
  786. })
  787. afterEach(function () {
  788. window.metaAttributesCache.set('ol-splitTestVariants', {})
  789. })
  790. describe('and capturedByGroup is true with managedUsersEnabled', function () {
  791. beforeEach(async function () {
  792. await fetchMock.callHistory.flush(true)
  793. fetchMock.removeRoutes().clearHistory()
  794. const institution = {
  795. university: {
  796. id: 1234,
  797. ssoEnabled: false,
  798. name: 'Auto Complete University',
  799. },
  800. hostname: 'autocomplete.edu',
  801. confirmed: true,
  802. capturedByGroup: true,
  803. group: {
  804. domainCaptureEnabled: true,
  805. managedUsersEnabled: true,
  806. ssoConfig: { enabled: true },
  807. },
  808. }
  809. fetchMock.get('express:/institutions/domains', [institution])
  810. })
  811. it('renders the domain-capture error', async function () {
  812. fetchMock.get('/user/emails?ensureAffiliation=true', [])
  813. renderEmailsSection()
  814. const button = await screen.findByRole<HTMLButtonElement>('button', {
  815. name: 'Add another email',
  816. })
  817. await userEvent.click(button)
  818. const input = screen.getByRole('textbox', { name: 'Email' })
  819. fireEvent.change(input, {
  820. target: { value: 'user@autocomplete.edu' },
  821. })
  822. const notification = await screen.findByRole('alert')
  823. within(notification).getByText(
  824. 'Your company email address has been registered under a verified domain, and cannot be added as a secondary email.',
  825. { exact: false }
  826. )
  827. })
  828. })
  829. describe('and capturedByGroup is false with managedUsersEnabled (domainCaptureEnabled alone is not enough)', function () {
  830. beforeEach(async function () {
  831. await fetchMock.callHistory.flush(true)
  832. fetchMock.removeRoutes().clearHistory()
  833. const institution = {
  834. university: {
  835. id: 1234,
  836. ssoEnabled: false,
  837. name: 'Auto Complete University',
  838. },
  839. hostname: 'autocomplete.edu',
  840. confirmed: true,
  841. capturedByGroup: false,
  842. group: {
  843. domainCaptureEnabled: true,
  844. managedUsersEnabled: true,
  845. ssoConfig: { enabled: true },
  846. },
  847. }
  848. fetchMock.get('express:/institutions/domains', [institution])
  849. })
  850. it('does not render the domain-capture error and shows regular institution fields', async function () {
  851. fetchMock.get('/user/emails?ensureAffiliation=true', [])
  852. renderEmailsSection()
  853. const button = await screen.findByRole<HTMLButtonElement>('button', {
  854. name: 'Add another email',
  855. })
  856. await userEvent.click(button)
  857. const input = screen.getByRole('textbox', { name: 'Email' })
  858. fireEvent.change(input, {
  859. target: { value: 'user@autocomplete.edu' },
  860. })
  861. await screen.findByRole('button', { name: 'Add new email' })
  862. expect(screen.queryByRole('alert')).to.be.null
  863. })
  864. })
  865. describe('and capturedByGroup is true with ssoConfig enabled (no managedUsers)', function () {
  866. beforeEach(async function () {
  867. await fetchMock.callHistory.flush(true)
  868. fetchMock.removeRoutes().clearHistory()
  869. const institution = {
  870. university: {
  871. id: 1234,
  872. ssoEnabled: false,
  873. name: 'Auto Complete University',
  874. },
  875. hostname: 'autocomplete.edu',
  876. confirmed: true,
  877. capturedByGroup: true,
  878. group: {
  879. domainCaptureEnabled: true,
  880. managedUsersEnabled: false,
  881. ssoConfig: { enabled: true },
  882. },
  883. }
  884. fetchMock.get('express:/institutions/domains', [institution])
  885. })
  886. it('renders the group SSO unavailable message', async function () {
  887. fetchMock.get('/user/emails?ensureAffiliation=true', [])
  888. renderEmailsSection()
  889. const button = await screen.findByRole<HTMLButtonElement>('button', {
  890. name: 'Add another email',
  891. })
  892. await userEvent.click(button)
  893. const input = screen.getByRole('textbox', { name: 'Email' })
  894. fireEvent.change(input, {
  895. target: { value: 'user@autocomplete.edu' },
  896. })
  897. await screen.findByText('This feature is currently unavailable.')
  898. })
  899. })
  900. describe('and capturedByGroup is false with ssoConfig enabled', function () {
  901. beforeEach(async function () {
  902. await fetchMock.callHistory.flush(true)
  903. fetchMock.removeRoutes().clearHistory()
  904. const institution = {
  905. university: {
  906. id: 1234,
  907. ssoEnabled: false,
  908. name: 'Auto Complete University',
  909. },
  910. hostname: 'autocomplete.edu',
  911. confirmed: true,
  912. capturedByGroup: false,
  913. group: {
  914. domainCaptureEnabled: true,
  915. managedUsersEnabled: false,
  916. ssoConfig: { enabled: true },
  917. },
  918. }
  919. fetchMock.get('express:/institutions/domains', [institution])
  920. })
  921. it('does not render SSO UI and shows regular institution fields', async function () {
  922. fetchMock.get('/user/emails?ensureAffiliation=true', [])
  923. renderEmailsSection()
  924. const button = await screen.findByRole<HTMLButtonElement>('button', {
  925. name: 'Add another email',
  926. })
  927. await userEvent.click(button)
  928. const input = screen.getByRole('textbox', { name: 'Email' })
  929. fireEvent.change(input, {
  930. target: { value: 'user@autocomplete.edu' },
  931. })
  932. await screen.findByRole('button', { name: 'Add new email' })
  933. expect(screen.queryByText('This feature is currently unavailable.'))
  934. .to.be.null
  935. })
  936. })
  937. })
  938. })
  939. })