UserRegistrationHandler.test.mjs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343
  1. import { vi, expect } from 'vitest'
  2. import assert from 'node:assert'
  3. import sinon from 'sinon'
  4. import EmailHelper from '../../../../app/src/Features/Helpers/EmailHelper.mjs'
  5. const modulePath = '../../../../app/src/Features/User/UserRegistrationHandler'
  6. describe('UserRegistrationHandler', function () {
  7. beforeEach(async function (ctx) {
  8. ctx.analyticsId = '123456'
  9. ctx.user = {
  10. _id: (ctx.user_id = '31j2lk21kjl'),
  11. analyticsId: ctx.analyticsId,
  12. }
  13. ctx.User = {
  14. updateOne: sinon.stub().returns({ exec: sinon.stub().resolves() }),
  15. }
  16. ctx.UserGetter = {
  17. promises: {
  18. getUserByAnyEmail: sinon.stub(),
  19. },
  20. }
  21. ctx.UserCreator = {
  22. promises: {
  23. createNewUser: sinon.stub().resolves(ctx.user),
  24. },
  25. }
  26. ctx.AuthenticationManager = {
  27. validateEmail: sinon.stub().returns(null),
  28. validatePassword: sinon.stub().returns(null),
  29. promises: {
  30. setUserPassword: sinon.stub().resolves(ctx.user),
  31. },
  32. }
  33. ctx.Modules = {
  34. promises: {
  35. hooks: {
  36. fire: sinon.stub().resolves([]),
  37. },
  38. },
  39. }
  40. ctx.EmailHandler = {
  41. promises: { sendEmail: sinon.stub().resolves() },
  42. }
  43. ctx.OneTimeTokenHandler = { promises: { getNewToken: sinon.stub() } }
  44. vi.doMock('../../../../app/src/models/User', () => ({
  45. User: ctx.User,
  46. }))
  47. vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({
  48. default: ctx.UserGetter,
  49. }))
  50. vi.doMock('../../../../app/src/Features/User/UserCreator', () => ({
  51. default: ctx.UserCreator,
  52. }))
  53. vi.doMock(
  54. '../../../../app/src/Features/Authentication/AuthenticationManager',
  55. () => ({
  56. default: ctx.AuthenticationManager,
  57. })
  58. )
  59. vi.doMock('../../../../app/src/infrastructure/Modules', () => ({
  60. default: ctx.Modules,
  61. }))
  62. vi.doMock('crypto', () => ({
  63. default: (ctx.crypto = {
  64. randomUUID: sinon
  65. .stub()
  66. .returns('8055c676-bcc7-4e64-a66f-8069f9a0bd92'),
  67. }),
  68. }))
  69. vi.doMock('../../../../app/src/Features/Email/EmailHandler', () => ({
  70. default: ctx.EmailHandler,
  71. }))
  72. vi.doMock(
  73. '../../../../app/src/Features/Security/OneTimeTokenHandler',
  74. () => ({
  75. default: ctx.OneTimeTokenHandler,
  76. })
  77. )
  78. vi.doMock(
  79. '../../../../app/src/Features/Analytics/AnalyticsManager',
  80. () => ({
  81. default: (ctx.AnalyticsManager = {
  82. recordEventForUser: sinon.stub(),
  83. setUserPropertyForUser: sinon.stub(),
  84. identifyUser: sinon.stub(),
  85. }),
  86. })
  87. )
  88. vi.doMock('@overleaf/settings', () => ({
  89. default: (ctx.settings = {
  90. siteUrl: 'http://sl.example.com',
  91. }),
  92. }))
  93. vi.doMock('../../../../app/src/Features/Helpers/EmailHelper', () => ({
  94. default: EmailHelper,
  95. }))
  96. ctx.handler = (await import(modulePath)).default
  97. ctx.passingRequest = {
  98. email: 'something@email.com',
  99. password: '123',
  100. analyticsId: ctx.analyticsId,
  101. }
  102. })
  103. describe('validate Register Request', function () {
  104. it('allows passing validation through', function (ctx) {
  105. const result = ctx.handler.promises._registrationRequestIsValid(
  106. ctx.passingRequest
  107. )
  108. result.should.equal(true)
  109. })
  110. describe('failing email validation', function () {
  111. beforeEach(function (ctx) {
  112. ctx.AuthenticationManager.validateEmail.returns({
  113. message: 'email not set',
  114. })
  115. })
  116. it('does not allow through', function (ctx) {
  117. const result = ctx.handler.promises._registrationRequestIsValid(
  118. ctx.passingRequest
  119. )
  120. return result.should.equal(false)
  121. })
  122. })
  123. describe('failing password validation', function () {
  124. beforeEach(function (ctx) {
  125. ctx.AuthenticationManager.validatePassword.returns({
  126. message: 'password is too short',
  127. })
  128. })
  129. it('does not allow through', function (ctx) {
  130. const result = ctx.handler.promises._registrationRequestIsValid(
  131. ctx.passingRequest
  132. )
  133. result.should.equal(false)
  134. })
  135. })
  136. })
  137. describe('registerNewUser', function () {
  138. describe('holdingAccount', function (done) {
  139. beforeEach(function (ctx) {
  140. ctx.user.holdingAccount = true
  141. ctx.handler.promises._registrationRequestIsValid = sinon
  142. .stub()
  143. .returns(true)
  144. ctx.UserGetter.promises.getUserByAnyEmail.resolves(ctx.user)
  145. })
  146. it('should not create a new user if there is a holding account there', async function (ctx) {
  147. await ctx.handler.promises.registerNewUser(ctx.passingRequest)
  148. ctx.UserCreator.promises.createNewUser.called.should.equal(false)
  149. })
  150. it('should set holding account to false', async function (ctx) {
  151. await ctx.handler.promises.registerNewUser(ctx.passingRequest)
  152. const update = ctx.User.updateOne.args[0]
  153. assert.deepEqual(update[0], { _id: ctx.user._id })
  154. assert.deepEqual(update[1], { $set: { holdingAccount: false } })
  155. })
  156. })
  157. describe('invalidRequest', function () {
  158. it('should not create a new user if the the request is not valid', async function (ctx) {
  159. ctx.handler.promises._registrationRequestIsValid = sinon
  160. .stub()
  161. .returns(false)
  162. expect(ctx.handler.promises.registerNewUser(ctx.passingRequest)).to.be
  163. .rejected
  164. ctx.UserCreator.promises.createNewUser.called.should.equal(false)
  165. })
  166. it('should return email registered in the error if there is a non holdingAccount there', async function (ctx) {
  167. ctx.UserGetter.promises.getUserByAnyEmail.resolves(
  168. (ctx.user = { holdingAccount: false })
  169. )
  170. expect(
  171. ctx.handler.promises.registerNewUser(ctx.passingRequest)
  172. ).to.be.rejectedWith('EmailAlreadyRegistered')
  173. })
  174. })
  175. describe('validRequest', function () {
  176. beforeEach(function (ctx) {
  177. ctx.handler.promises._registrationRequestIsValid = sinon
  178. .stub()
  179. .returns(true)
  180. ctx.UserGetter.promises.getUserByAnyEmail.resolves()
  181. })
  182. it('should create a new user', async function (ctx) {
  183. await ctx.handler.promises.registerNewUser(ctx.passingRequest)
  184. sinon.assert.calledWith(ctx.UserCreator.promises.createNewUser, {
  185. email: ctx.passingRequest.email,
  186. holdingAccount: false,
  187. first_name: ctx.passingRequest.first_name,
  188. last_name: ctx.passingRequest.last_name,
  189. analyticsId: ctx.user.analyticsId,
  190. })
  191. })
  192. it('lower case email', async function (ctx) {
  193. ctx.passingRequest.email = 'soMe@eMail.cOm'
  194. await ctx.handler.promises.registerNewUser(ctx.passingRequest)
  195. ctx.UserCreator.promises.createNewUser.args[0][0].email.should.equal(
  196. 'some@email.com'
  197. )
  198. })
  199. it('trim white space from email', async function (ctx) {
  200. ctx.passingRequest.email = ' some@email.com '
  201. await ctx.handler.promises.registerNewUser(ctx.passingRequest)
  202. ctx.UserCreator.promises.createNewUser.args[0][0].email.should.equal(
  203. 'some@email.com'
  204. )
  205. })
  206. it('should set the password', async function (ctx) {
  207. await ctx.handler.promises.registerNewUser(ctx.passingRequest)
  208. ctx.AuthenticationManager.promises.setUserPassword
  209. .calledWith(ctx.user, ctx.passingRequest.password)
  210. .should.equal(true)
  211. })
  212. it('should add the user to the newsletter if accepted terms', async function (ctx) {
  213. ctx.passingRequest.subscribeToNewsletter = 'true'
  214. await ctx.handler.promises.registerNewUser(ctx.passingRequest)
  215. expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith(
  216. 'updateTopicSubscription',
  217. ctx.user._id,
  218. 'newsletter',
  219. true
  220. )
  221. })
  222. it('should not add the user to the newsletter if not accepted terms', async function (ctx) {
  223. await ctx.handler.promises.registerNewUser(ctx.passingRequest)
  224. expect(ctx.Modules.promises.hooks.fire).to.not.have.been.calledWith(
  225. 'updateTopicSubscription',
  226. sinon.match.any,
  227. sinon.match.any,
  228. true
  229. )
  230. })
  231. })
  232. })
  233. describe('registerNewUserAndSendActivationEmail', function () {
  234. beforeEach(function (ctx) {
  235. ctx.email = 'Email@example.com'
  236. ctx.crypto.randomBytes = sinon.stub().returns({
  237. toString: () => {
  238. return (ctx.password = 'mock-password')
  239. },
  240. })
  241. ctx.OneTimeTokenHandler.promises.getNewToken.resolves(
  242. (ctx.token = 'mock-token')
  243. )
  244. ctx.handler.promises.registerNewUser = sinon.stub()
  245. })
  246. describe('with a new user', function () {
  247. beforeEach(async function (ctx) {
  248. ctx.user.email = ctx.email.toLowerCase()
  249. ctx.handler.promises.registerNewUser.resolves(ctx.user)
  250. ctx.result =
  251. await ctx.handler.promises.registerNewUserAndSendActivationEmail(
  252. ctx.email
  253. )
  254. })
  255. it('should ask the UserRegistrationHandler to register user', function (ctx) {
  256. sinon.assert.calledWith(ctx.handler.promises.registerNewUser, {
  257. email: ctx.email,
  258. password: ctx.password,
  259. analyticsId: '8055c676-bcc7-4e64-a66f-8069f9a0bd92',
  260. })
  261. })
  262. it('should generate a new password reset token', function (ctx) {
  263. const data = {
  264. user_id: ctx.user._id.toString(),
  265. email: ctx.user.email,
  266. }
  267. ctx.OneTimeTokenHandler.promises.getNewToken
  268. .calledWith('password', data, { expiresIn: 7 * 24 * 60 * 60 })
  269. .should.equal(true)
  270. })
  271. it('should send a registered email', function (ctx) {
  272. ctx.EmailHandler.promises.sendEmail
  273. .calledWith('registered', {
  274. to: ctx.user.email,
  275. setNewPasswordUrl: `${ctx.settings.siteUrl}/user/activate?token=${ctx.token}&user_id=${ctx.user_id}`,
  276. })
  277. .should.equal(true)
  278. })
  279. it('should return the user and new password url', function (ctx) {
  280. const { user, setNewPasswordUrl } = ctx.result
  281. expect(user).to.deep.equal(ctx.user)
  282. expect(setNewPasswordUrl).to.equal(
  283. `${ctx.settings.siteUrl}/user/activate?token=${ctx.token}&user_id=${ctx.user_id}`
  284. )
  285. })
  286. })
  287. describe('with a user that already exists', function () {
  288. beforeEach(async function (ctx) {
  289. ctx.handler.promises.registerNewUser.rejects(
  290. new Error('EmailAlreadyRegistered')
  291. )
  292. ctx.UserGetter.promises.getUserByAnyEmail.resolves(ctx.user)
  293. await ctx.handler.promises.registerNewUserAndSendActivationEmail(
  294. ctx.email
  295. )
  296. })
  297. it('should still generate a new password token and email', function (ctx) {
  298. ctx.OneTimeTokenHandler.promises.getNewToken.called.should.equal(true)
  299. ctx.EmailHandler.promises.sendEmail.called.should.equal(true)
  300. })
  301. })
  302. })
  303. })