user-email-context.test.tsx 13 KB

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