user-email-context.test.tsx 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395
  1. import { expect } from 'chai'
  2. import { cloneDeep } from 'lodash'
  3. import { renderHook, waitFor } from '@testing-library/react'
  4. import {
  5. EmailContextType,
  6. UserEmailsProvider,
  7. useUserEmailsContext,
  8. } from '../../../../../frontend/js/features/settings/context/user-email-context'
  9. import fetchMock from 'fetch-mock'
  10. import {
  11. confirmedUserData,
  12. professionalUserData,
  13. unconfirmedUserData,
  14. fakeUsersData,
  15. unconfirmedCommonsUserData,
  16. untrustedUserData,
  17. } from '../fixtures/test-user-email-data'
  18. import localStorage from '@/infrastructure/local-storage'
  19. const renderUserEmailsContext = () =>
  20. renderHook(() => useUserEmailsContext(), {
  21. wrapper: ({ children }) => (
  22. <UserEmailsProvider>{children}</UserEmailsProvider>
  23. ),
  24. })
  25. describe('UserEmailContext', function () {
  26. beforeEach(function () {
  27. fetchMock.removeRoutes().clearHistory()
  28. })
  29. describe('context bootstrap', function () {
  30. it('should start with an "in progress" initialisation state', function () {
  31. const { result } = renderUserEmailsContext()
  32. expect(result.current.isInitializing).to.equal(true)
  33. expect(result.current.isInitializingSuccess).to.equal(false)
  34. expect(result.current.isInitializingError).to.equal(false)
  35. })
  36. it('should start with an empty state', function () {
  37. const { result } = renderUserEmailsContext()
  38. expect(result.current.state.data.byId).to.deep.equal({})
  39. expect(result.current.state.data.emailAffiliationBeingEdited).to.be.null
  40. expect(result.current.state.data.linkedInstitutionIds).to.have.length(0)
  41. })
  42. it('should load all user emails and update the initialisation state to "success"', async function () {
  43. fetchMock.get(/\/user\/emails/, fakeUsersData)
  44. const { result } = renderUserEmailsContext()
  45. await fetchMock.callHistory.flush(true)
  46. expect(fetchMock.callHistory.calls()).to.have.lengthOf(1)
  47. await waitFor(() =>
  48. expect(result.current.state.data.byId).to.deep.equal({
  49. 'bar@overleaf.com': { ...untrustedUserData, ...confirmedUserData },
  50. 'baz@overleaf.com': unconfirmedUserData,
  51. 'foo@overleaf.com': professionalUserData,
  52. 'qux@overleaf.com': unconfirmedCommonsUserData,
  53. })
  54. )
  55. expect(result.current.state.data.linkedInstitutionIds).to.have.lengthOf(0)
  56. expect(result.current.isInitializing).to.equal(false)
  57. expect(result.current.isInitializingSuccess).to.equal(true)
  58. })
  59. it('when loading user email fails, it should update the initialisation state to "failed"', async function () {
  60. fetchMock.get(/\/user\/emails/, 500)
  61. const { result } = renderUserEmailsContext()
  62. await fetchMock.callHistory.flush()
  63. await waitFor(() => expect(result.current.isInitializing).to.equal(false))
  64. expect(result.current.isInitializingError).to.equal(true)
  65. })
  66. describe('state.isLoading', function () {
  67. it('should be `true` on bootstrap', function () {
  68. const { result } = renderUserEmailsContext()
  69. expect(result.current.state.isLoading).to.equal(true)
  70. })
  71. it('should be updated with `setLoading`', async function () {
  72. const { result } = renderUserEmailsContext()
  73. result.current.setLoading(true)
  74. await waitFor(() =>
  75. expect(result.current.state.isLoading).to.equal(true)
  76. )
  77. result.current.setLoading(false)
  78. await waitFor(() =>
  79. expect(result.current.state.isLoading).to.equal(false)
  80. )
  81. })
  82. })
  83. })
  84. describe('context initialised', function () {
  85. let result: { current: EmailContextType }
  86. beforeEach(async function () {
  87. fetchMock.get(/\/user\/emails/, fakeUsersData)
  88. const value = renderUserEmailsContext()
  89. result = value.result
  90. await fetchMock.callHistory.flush(true)
  91. })
  92. describe('getEmails()', function () {
  93. beforeEach(async function () {
  94. fetchMock.removeRoutes().clearHistory()
  95. })
  96. it('should set `isLoading === true`', async function () {
  97. fetchMock.get(
  98. /\/user\/emails/,
  99. [
  100. {
  101. email: 'new@email.com',
  102. default: true,
  103. },
  104. ],
  105. { delay: 100 }
  106. )
  107. result.current.getEmails()
  108. await waitFor(() => expect(result.current.state.isLoading).to.be.true)
  109. })
  110. it('requests a new set of emails', async function () {
  111. const emailData = {
  112. email: 'new@email.com',
  113. default: true,
  114. }
  115. fetchMock.get(/\/user\/emails/, [emailData])
  116. result.current.getEmails()
  117. await fetchMock.callHistory.flush(true)
  118. await waitFor(() =>
  119. expect(result.current.state.data.byId).to.deep.equal({
  120. 'new@email.com': emailData,
  121. })
  122. )
  123. })
  124. it('should populate `linkedInstitutionIds`', async function () {
  125. fetchMock.get(/\/user\/emails/, [
  126. confirmedUserData,
  127. { ...unconfirmedUserData, samlProviderId: 'saml_provider_1' },
  128. { ...professionalUserData, samlProviderId: 'saml_provider_2' },
  129. ])
  130. const { result } = renderUserEmailsContext()
  131. await fetchMock.callHistory.flush(true)
  132. await waitFor(() =>
  133. expect(result.current.state.data.linkedInstitutionIds).to.deep.equal([
  134. 'saml_provider_1',
  135. 'saml_provider_2',
  136. ])
  137. )
  138. })
  139. })
  140. describe('makePrimary()', function () {
  141. it('sets an email as `default`', async function () {
  142. expect(result.current.state.data.byId['bar@overleaf.com'].default).to.be
  143. .false
  144. result.current.makePrimary('bar@overleaf.com')
  145. await waitFor(
  146. () =>
  147. expect(result.current.state.data.byId['bar@overleaf.com'].default)
  148. .to.be.true
  149. )
  150. })
  151. it('sets `default=false` for the current primary email ', async function () {
  152. expect(result.current.state.data.byId['foo@overleaf.com'].default).to.be
  153. .true
  154. result.current.makePrimary('bar@overleaf.com')
  155. await waitFor(
  156. () =>
  157. expect(result.current.state.data.byId['foo@overleaf.com'].default)
  158. .to.be.false
  159. )
  160. })
  161. it('produces no effect when passing a non-existing email', async function () {
  162. const emails = cloneDeep(result.current.state.data.byId)
  163. result.current.makePrimary('non-existing@email.com')
  164. await waitFor(() =>
  165. expect(result.current.state.data.byId).to.deep.equal(emails)
  166. )
  167. })
  168. })
  169. describe('deleteEmail()', function () {
  170. it('removes data from the deleted email', async function () {
  171. result.current.deleteEmail('bar@overleaf.com')
  172. await waitFor(
  173. () =>
  174. expect(result.current.state.data.byId['bar@overleaf.com']).to.be
  175. .undefined
  176. )
  177. })
  178. it('produces no effect when passing a non-existing email', function () {
  179. const emails = cloneDeep(result.current.state.data.byId)
  180. result.current.deleteEmail('non-existing@email.com')
  181. expect(result.current.state.data.byId).to.deep.equal(emails)
  182. })
  183. })
  184. describe('setEmailAffiliationBeingEdited()', function () {
  185. it('sets an email as currently being edited', async function () {
  186. result.current.setEmailAffiliationBeingEdited('bar@overleaf.com')
  187. await waitFor(() =>
  188. expect(
  189. result.current.state.data.emailAffiliationBeingEdited
  190. ).to.equal('bar@overleaf.com')
  191. )
  192. result.current.setEmailAffiliationBeingEdited(null)
  193. await waitFor(
  194. () =>
  195. expect(result.current.state.data.emailAffiliationBeingEdited).to.be
  196. .null
  197. )
  198. })
  199. it('produces no effect when passing a non-existing email', function () {
  200. expect(result.current.state.data.emailAffiliationBeingEdited).to.be.null
  201. result.current.setEmailAffiliationBeingEdited('non-existing@email.com')
  202. expect(result.current.state.data.emailAffiliationBeingEdited).to.be.null
  203. })
  204. })
  205. describe('updateAffiliation()', function () {
  206. it('updates affiliation data for an email', async function () {
  207. result.current.updateAffiliation(
  208. 'foo@overleaf.com',
  209. 'new role',
  210. 'new department'
  211. )
  212. await waitFor(() =>
  213. expect(
  214. result.current.state.data.byId['foo@overleaf.com'].affiliation!.role
  215. ).to.equal('new role')
  216. )
  217. expect(
  218. result.current.state.data.byId['foo@overleaf.com'].affiliation!
  219. .department
  220. ).to.equal('new department')
  221. })
  222. it('clears an email from currently being edited', function () {
  223. result.current.setEmailAffiliationBeingEdited('foo@overleaf.com')
  224. result.current.updateAffiliation(
  225. 'foo@overleaf.com',
  226. 'new role',
  227. 'new department'
  228. )
  229. expect(result.current.state.data.emailAffiliationBeingEdited).to.be.null
  230. })
  231. it('produces no effect when passing an email with no affiliation', function () {
  232. const emails = cloneDeep(result.current.state.data.byId)
  233. result.current.updateAffiliation(
  234. 'bar@overleaf.com',
  235. 'new role',
  236. 'new department'
  237. )
  238. expect(result.current.state.data.byId).to.deep.equal(emails)
  239. })
  240. it('produces no effect when passing a non-existing email', function () {
  241. const emails = cloneDeep(result.current.state.data.byId)
  242. result.current.updateAffiliation(
  243. 'non-existing@email.com',
  244. 'new role',
  245. 'new department'
  246. )
  247. expect(result.current.state.data.byId).to.deep.equal(emails)
  248. })
  249. })
  250. describe('resetLeaversSurveyExpiration()', function () {
  251. beforeEach(function () {
  252. localStorage.removeItem('showInstitutionalLeaversSurveyUntil')
  253. })
  254. it('when the leaver has institution license, and there is another email with institution license, it should not reset the survey expiration date', async function () {
  255. const affiliatedEmail1 = cloneDeep(professionalUserData)
  256. affiliatedEmail1.email = 'institution-test@example.com'
  257. affiliatedEmail1.emailHasInstitutionLicence = true
  258. const affiliatedEmail2 = cloneDeep(professionalUserData)
  259. affiliatedEmail2.emailHasInstitutionLicence = true
  260. fetchMock.removeRoutes().clearHistory()
  261. fetchMock.get(/\/user\/emails/, [affiliatedEmail1, affiliatedEmail2])
  262. result.current.getEmails()
  263. await fetchMock.callHistory.flush(true)
  264. // `resetLeaversSurveyExpiration` always happens after deletion
  265. result.current.deleteEmail(affiliatedEmail1.email)
  266. result.current.resetLeaversSurveyExpiration(affiliatedEmail1)
  267. const expiration = localStorage.getItem(
  268. 'showInstitutionalLeaversSurveyUntil'
  269. ) as number
  270. expect(expiration).to.be.null
  271. })
  272. it("when the leaver's affiliation is past reconfirmation date, and there is another email with institution license, it should not reset the survey expiration date", async function () {
  273. const affiliatedEmail1 = cloneDeep(professionalUserData)
  274. affiliatedEmail1.email = 'institution-test@example.com'
  275. affiliatedEmail1.affiliation.pastReconfirmDate = true
  276. const affiliatedEmail2 = cloneDeep(professionalUserData)
  277. affiliatedEmail2.emailHasInstitutionLicence = true
  278. fetchMock.removeRoutes().clearHistory()
  279. fetchMock.get(/\/user\/emails/, [affiliatedEmail1, affiliatedEmail2])
  280. result.current.getEmails()
  281. await fetchMock.callHistory.flush(true)
  282. // `resetLeaversSurveyExpiration` always happens after deletion
  283. result.current.deleteEmail(affiliatedEmail1.email)
  284. result.current.resetLeaversSurveyExpiration(affiliatedEmail1)
  285. const expiration = localStorage.getItem(
  286. 'showInstitutionalLeaversSurveyUntil'
  287. ) as number
  288. expect(expiration).to.be.null
  289. })
  290. it('when there are no other emails with institution license, it should reset the survey expiration date', async function () {
  291. const affiliatedEmail1 = cloneDeep(professionalUserData)
  292. affiliatedEmail1.emailHasInstitutionLicence = true
  293. affiliatedEmail1.email = 'institution-test@example.com'
  294. affiliatedEmail1.affiliation.pastReconfirmDate = true
  295. fetchMock.removeRoutes().clearHistory()
  296. fetchMock.get(/\/user\/emails/, [confirmedUserData, affiliatedEmail1])
  297. result.current.getEmails()
  298. await fetchMock.callHistory.flush(true)
  299. // `resetLeaversSurveyExpiration` always happens after deletion
  300. result.current.deleteEmail(affiliatedEmail1.email)
  301. result.current.resetLeaversSurveyExpiration(affiliatedEmail1)
  302. await waitFor(() =>
  303. expect(
  304. localStorage.getItem('showInstitutionalLeaversSurveyUntil')
  305. ).to.be.greaterThan(Date.now())
  306. )
  307. })
  308. it("when the leaver has no institution license, it shouldn't reset the survey expiration date", async function () {
  309. const emailWithInstitutionLicense = cloneDeep(professionalUserData)
  310. emailWithInstitutionLicense.email = 'institution-licensed@example.com'
  311. emailWithInstitutionLicense.emailHasInstitutionLicence = false
  312. fetchMock.removeRoutes().clearHistory()
  313. fetchMock.get(/\/user\/emails/, [emailWithInstitutionLicense])
  314. result.current.getEmails()
  315. await fetchMock.callHistory.flush(true)
  316. // `resetLeaversSurveyExpiration` always happens after deletion
  317. result.current.deleteEmail(emailWithInstitutionLicense.email)
  318. result.current.resetLeaversSurveyExpiration(professionalUserData)
  319. expect(localStorage.getItem('showInstitutionalLeaversSurveyUntil')).to
  320. .be.null
  321. })
  322. it("when the leaver is not past its reconfirmation date, it shouldn't reset the survey expiration date", async function () {
  323. const emailWithInstitutionLicense = cloneDeep(professionalUserData)
  324. emailWithInstitutionLicense.email = 'institution-licensed@example.com'
  325. emailWithInstitutionLicense.affiliation.pastReconfirmDate = false
  326. fetchMock.removeRoutes().clearHistory()
  327. fetchMock.get(/\/user\/emails/, [emailWithInstitutionLicense])
  328. result.current.getEmails()
  329. await fetchMock.callHistory.flush(true)
  330. // `resetLeaversSurveyExpiration` always happens after deletion
  331. result.current.deleteEmail(emailWithInstitutionLicense.email)
  332. result.current.resetLeaversSurveyExpiration(professionalUserData)
  333. expect(localStorage.getItem('showInstitutionalLeaversSurveyUntil')).to
  334. .be.null
  335. })
  336. })
  337. })
  338. })