user-email-context.test.tsx 13 KB

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