user-email-context.test.tsx 14 KB

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