UserEmailsConfirmationHandler.test.mjs 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290
  1. // TODO: This file was created by bulk-decaffeinate.
  2. // Fix any style issues and re-enable lint.
  3. /*
  4. * decaffeinate suggestions:
  5. * DS102: Remove unnecessary code created because of implicit returns
  6. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  7. */
  8. import { vi, expect } from 'vitest'
  9. import sinon from 'sinon'
  10. import Errors from '../../../../app/src/Features/Errors/Errors.js'
  11. import EmailHelper from '../../../../app/src/Features/Helpers/EmailHelper.mjs'
  12. const modulePath =
  13. '../../../../app/src/Features/User/UserEmailsConfirmationHandler'
  14. vi.mock('../../../../app/src/Features/Errors/Errors.js', () =>
  15. vi.importActual('../../../../app/src/Features/Errors/Errors.js')
  16. )
  17. describe('UserEmailsConfirmationHandler', function () {
  18. beforeEach(async function (ctx) {
  19. ctx.mockUser = {
  20. _id: 'mock-user-id',
  21. email: 'mock@example.com',
  22. emails: [{ email: 'mock@example.com' }],
  23. }
  24. ctx.user_id = ctx.mockUser._id
  25. ctx.email = ctx.mockUser.email
  26. ctx.req = {}
  27. vi.doMock('@overleaf/settings', () => ({
  28. default: (ctx.settings = {
  29. siteUrl: 'https://emails.example.com',
  30. }),
  31. }))
  32. vi.doMock(
  33. '../../../../app/src/Features/Security/OneTimeTokenHandler',
  34. () => ({
  35. default: (ctx.OneTimeTokenHandler = {
  36. promises: {},
  37. }),
  38. })
  39. )
  40. vi.doMock('../../../../app/src/Features/User/UserUpdater', () => ({
  41. default: (ctx.UserUpdater = {
  42. promises: {},
  43. }),
  44. }))
  45. vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({
  46. default: (ctx.UserGetter = {
  47. getUser: sinon.stub().yields(null, ctx.mockUser),
  48. promises: {
  49. getUser: sinon.stub().resolves(ctx.mockUser),
  50. },
  51. }),
  52. }))
  53. vi.doMock('../../../../app/src/Features/Email/EmailHandler', () => ({
  54. default: (ctx.EmailHandler = {
  55. promises: {},
  56. }),
  57. }))
  58. vi.doMock('../../../../app/src/Features/Helpers/EmailHelper', () => ({
  59. default: EmailHelper,
  60. }))
  61. vi.doMock(
  62. '../../../../app/src/Features/Authentication/SessionManager',
  63. () => ({
  64. default: (ctx.SessionManager = {
  65. getLoggedInUserId: sinon.stub().returns(ctx.mockUser._id),
  66. }),
  67. })
  68. )
  69. ctx.UserEmailsConfirmationHandler = (await import(modulePath)).default
  70. return (ctx.callback = sinon.stub())
  71. })
  72. describe('sendConfirmationEmail', function () {
  73. beforeEach(function (ctx) {
  74. ctx.OneTimeTokenHandler.promises.getNewToken = sinon
  75. .stub()
  76. .resolves((ctx.token = 'new-token'))
  77. return (ctx.EmailHandler.promises.sendEmail = sinon.stub().resolves())
  78. })
  79. describe('successfully', function () {
  80. beforeEach(async function (ctx) {
  81. await ctx.UserEmailsConfirmationHandler.promises.sendConfirmationEmail(
  82. ctx.user_id,
  83. ctx.email
  84. )
  85. })
  86. it('should generate a token for the user which references their id and email', function (ctx) {
  87. return ctx.OneTimeTokenHandler.promises.getNewToken
  88. .calledWith(
  89. 'email_confirmation',
  90. { user_id: ctx.user_id, email: ctx.email },
  91. { expiresIn: 90 * 24 * 60 * 60 }
  92. )
  93. .should.equal(true)
  94. })
  95. it('should send an email to the user', function (ctx) {
  96. return ctx.EmailHandler.promises.sendEmail
  97. .calledWith('confirmEmail', {
  98. to: ctx.email,
  99. confirmEmailUrl:
  100. 'https://emails.example.com/user/emails/confirm?token=new-token',
  101. sendingUser_id: ctx.user_id,
  102. })
  103. .should.equal(true)
  104. })
  105. })
  106. describe('with invalid email', function () {
  107. it('should reject with an error', async function (ctx) {
  108. await expect(
  109. ctx.UserEmailsConfirmationHandler.promises.sendConfirmationEmail(
  110. ctx.user_id,
  111. '!"£$%^&*()'
  112. )
  113. ).to.be.rejectedWith(Error)
  114. })
  115. })
  116. describe('a custom template', function () {
  117. beforeEach(async function (ctx) {
  118. await ctx.UserEmailsConfirmationHandler.promises.sendConfirmationEmail(
  119. ctx.user_id,
  120. ctx.email,
  121. 'myCustomTemplate'
  122. )
  123. })
  124. it('should send an email with the given template', function (ctx) {
  125. return ctx.EmailHandler.promises.sendEmail
  126. .calledWith('myCustomTemplate')
  127. .should.equal(true)
  128. })
  129. })
  130. })
  131. describe('confirmEmailFromToken', function () {
  132. beforeEach(function (ctx) {
  133. ctx.OneTimeTokenHandler.promises.peekValueFromToken = sinon
  134. .stub()
  135. .resolves({ data: { user_id: ctx.user_id, email: ctx.email } })
  136. ctx.OneTimeTokenHandler.promises.expireToken = sinon.stub().resolves()
  137. ctx.UserUpdater.promises.confirmEmail = sinon.stub().resolves()
  138. })
  139. describe('successfully', function () {
  140. beforeEach(async function (ctx) {
  141. await ctx.UserEmailsConfirmationHandler.promises.confirmEmailFromToken(
  142. ctx.req,
  143. (ctx.token = 'mock-token')
  144. )
  145. })
  146. it('should call peekValueFromToken', function (ctx) {
  147. return ctx.OneTimeTokenHandler.promises.peekValueFromToken
  148. .calledWith('email_confirmation', ctx.token)
  149. .should.equal(true)
  150. })
  151. it('should call expireToken', function (ctx) {
  152. return ctx.OneTimeTokenHandler.promises.expireToken
  153. .calledWith('email_confirmation', ctx.token)
  154. .should.equal(true)
  155. })
  156. it('should confirm the email of the user_id', function (ctx) {
  157. return ctx.UserUpdater.promises.confirmEmail
  158. .calledWith(ctx.user_id, ctx.email)
  159. .should.equal(true)
  160. })
  161. })
  162. describe('with an expired token', function () {
  163. beforeEach(function (ctx) {
  164. ctx.OneTimeTokenHandler.promises.peekValueFromToken = sinon
  165. .stub()
  166. .rejects(new Errors.NotFoundError('no token found'))
  167. })
  168. it('should reject with a NotFoundError', async function (ctx) {
  169. await expect(
  170. ctx.UserEmailsConfirmationHandler.promises.confirmEmailFromToken(
  171. ctx.req,
  172. (ctx.token = 'mock-token')
  173. )
  174. ).to.be.rejectedWith(Errors.NotFoundError)
  175. })
  176. })
  177. describe('with no user_id in the token', function () {
  178. beforeEach(function (ctx) {
  179. ctx.OneTimeTokenHandler.promises.peekValueFromToken = sinon
  180. .stub()
  181. .resolves({ data: { email: ctx.email } })
  182. })
  183. it('should reject with a NotFoundError', async function (ctx) {
  184. await expect(
  185. ctx.UserEmailsConfirmationHandler.promises.confirmEmailFromToken(
  186. ctx.req,
  187. (ctx.token = 'mock-token')
  188. )
  189. ).to.be.rejectedWith(Errors.NotFoundError)
  190. })
  191. })
  192. describe('with no email in the token', function () {
  193. beforeEach(function (ctx) {
  194. ctx.OneTimeTokenHandler.promises.peekValueFromToken = sinon
  195. .stub()
  196. .resolves({ data: { user_id: ctx.user_id } })
  197. })
  198. it('should reject with a NotFoundError', async function (ctx) {
  199. await expect(
  200. ctx.UserEmailsConfirmationHandler.promises.confirmEmailFromToken(
  201. ctx.req,
  202. (ctx.token = 'mock-token')
  203. )
  204. ).to.be.rejectedWith(Errors.NotFoundError)
  205. })
  206. })
  207. describe('with no user found', function () {
  208. beforeEach(function (ctx) {
  209. ctx.UserGetter.promises.getUser.resolves(null)
  210. })
  211. it('should reject with a NotFoundError', async function (ctx) {
  212. await expect(
  213. ctx.UserEmailsConfirmationHandler.promises.confirmEmailFromToken(
  214. ctx.req,
  215. (ctx.token = 'mock-token')
  216. )
  217. ).to.be.rejectedWith(Errors.NotFoundError)
  218. })
  219. })
  220. describe('with secondary email missing on user', function () {
  221. beforeEach(function (ctx) {
  222. ctx.OneTimeTokenHandler.promises.peekValueFromToken = sinon
  223. .stub()
  224. .resolves({
  225. data: { user_id: ctx.user_id, email: 'deleted@email.com' },
  226. })
  227. })
  228. it('should reject with a NotFoundError', async function (ctx) {
  229. await expect(
  230. ctx.UserEmailsConfirmationHandler.promises.confirmEmailFromToken(
  231. ctx.req,
  232. (ctx.token = 'mock-token')
  233. )
  234. ).to.be.rejectedWith(Errors.NotFoundError)
  235. })
  236. })
  237. describe('when the logged in user is not the token user', function () {
  238. beforeEach(function (ctx) {
  239. ctx.SessionManager.getLoggedInUserId = sinon
  240. .stub()
  241. .returns('other-user-id')
  242. })
  243. it('should reject with a ForbiddenError', async function (ctx) {
  244. await expect(
  245. ctx.UserEmailsConfirmationHandler.promises.confirmEmailFromToken(
  246. ctx.req,
  247. (ctx.token = 'mock-token')
  248. )
  249. ).to.be.rejectedWith(Errors.ForbiddenError)
  250. })
  251. })
  252. })
  253. })