user-email-context.test.tsx 14 KB

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