UserController.test.mjs 43 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395
  1. import { beforeEach, describe, expect, it, vi } from 'vitest'
  2. import sinon from 'sinon'
  3. import OError from '@overleaf/o-error'
  4. import Errors from '../../../../app/src/Features/Errors/Errors.js'
  5. const modulePath = '../../../../app/src/Features/User/UserController.mjs'
  6. vi.mock('../../../../app/src/Features/Errors/Errors.js', () => {
  7. return vi.importActual('../../../../app/src/Features/Errors/Errors.js')
  8. })
  9. describe('UserController', function () {
  10. beforeEach(async function (ctx) {
  11. ctx.user_id = '323123'
  12. ctx.user = {
  13. _id: ctx.user_id,
  14. email: 'email@overleaf.com',
  15. save: sinon.stub().resolves(),
  16. ace: {},
  17. }
  18. ctx.req = {
  19. user: {},
  20. session: {
  21. destroy() {},
  22. user: {
  23. _id: ctx.user_id,
  24. email: 'old@something.com',
  25. },
  26. analyticsId: ctx.user_id,
  27. },
  28. sessionID: '123',
  29. body: {},
  30. i18n: {
  31. translate: text => text,
  32. },
  33. ip: '0:0:0:0',
  34. query: {},
  35. headers: {},
  36. logger: {
  37. addFields: sinon.stub(),
  38. },
  39. }
  40. ctx.UserDeleter = { promises: { deleteUser: sinon.stub().resolves() } }
  41. ctx.UserGetter = {
  42. promises: { getUser: sinon.stub().resolves(ctx.user) },
  43. }
  44. ctx.User = {
  45. findById: sinon.stub().returns({ exec: sinon.stub().resolves(ctx.user) }),
  46. }
  47. ctx.NewsLetterManager = {
  48. promises: {
  49. subscribe: sinon.stub().resolves(),
  50. unsubscribe: sinon.stub().resolves(),
  51. },
  52. }
  53. ctx.SessionManager = {
  54. getLoggedInUserId: sinon.stub().returns(ctx.user._id),
  55. getSessionUser: sinon.stub().returns(ctx.req.session.user),
  56. setInSessionUser: sinon.stub(),
  57. }
  58. ctx.AuthenticationManager = {
  59. promises: {
  60. authenticate: sinon.stub(),
  61. setUserPassword: sinon.stub(),
  62. },
  63. getMessageForInvalidPasswordError: sinon
  64. .stub()
  65. .returns({ type: 'error', key: 'some-key' }),
  66. }
  67. ctx.UserUpdater = {
  68. promises: {
  69. changeEmailAddress: sinon.stub().resolves(),
  70. confirmEmail: sinon.stub().resolves(),
  71. addAffiliationForNewUser: sinon.stub().resolves(),
  72. },
  73. }
  74. ctx.settings = { siteUrl: 'overleaf.example.com' }
  75. ctx.UserHandler = {
  76. promises: { populateTeamInvites: sinon.stub().resolves() },
  77. }
  78. ctx.UserSessionsManager = {
  79. promises: {
  80. getAllUserSessions: sinon.stub().resolves(),
  81. removeSessionsFromRedis: sinon.stub().resolves(),
  82. untrackSession: sinon.stub().resolves(),
  83. },
  84. }
  85. ctx.HttpErrorHandler = {
  86. badRequest: sinon.stub(),
  87. conflict: sinon.stub(),
  88. unprocessableEntity: sinon.stub(),
  89. legacyInternal: sinon.stub(),
  90. }
  91. ctx.UrlHelper = {
  92. getSafeRedirectPath: sinon.stub(),
  93. }
  94. ctx.UrlHelper.getSafeRedirectPath
  95. .withArgs('https://evil.com')
  96. .returns(undefined)
  97. ctx.UrlHelper.getSafeRedirectPath.returnsArg(0)
  98. ctx.Features = {
  99. hasFeature: sinon.stub(),
  100. }
  101. ctx.UserAuditLogHandler = {
  102. promises: {
  103. addEntry: sinon.stub().resolves(),
  104. },
  105. addEntryInBackground: sinon.stub(),
  106. }
  107. ctx.RequestContentTypeDetection = {
  108. acceptsJson: sinon.stub().returns(false),
  109. }
  110. ctx.EmailHandler = {
  111. promises: { sendEmail: sinon.stub().resolves() },
  112. }
  113. ctx.OneTimeTokenHandler = {
  114. promises: { expireAllTokensForUser: sinon.stub().resolves() },
  115. }
  116. ctx.Modules = {
  117. promises: {
  118. hooks: {
  119. fire: sinon.stub().resolves(),
  120. },
  121. },
  122. }
  123. vi.doMock('../../../../app/src/Features/Helpers/UrlHelper', () => ({
  124. default: ctx.UrlHelper,
  125. }))
  126. vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({
  127. default: ctx.UserGetter,
  128. }))
  129. vi.doMock('../../../../app/src/Features/User/UserDeleter', () => ({
  130. default: ctx.UserDeleter,
  131. }))
  132. vi.doMock('../../../../app/src/Features/User/UserUpdater', () => ({
  133. default: ctx.UserUpdater,
  134. }))
  135. vi.doMock('../../../../app/src/models/User', () => ({
  136. User: ctx.User,
  137. }))
  138. vi.doMock(
  139. '../../../../app/src/Features/Newsletter/NewsletterManager',
  140. () => ({
  141. default: ctx.NewsLetterManager,
  142. })
  143. )
  144. vi.doMock(
  145. '../../../../app/src/Features/Authentication/AuthenticationController',
  146. () => ({
  147. default: ctx.AuthenticationController,
  148. })
  149. )
  150. vi.doMock(
  151. '../../../../app/src/Features/Authentication/SessionManager',
  152. () => ({
  153. default: ctx.SessionManager,
  154. })
  155. )
  156. vi.doMock(
  157. '../../../../app/src/Features/Authentication/AuthenticationManager',
  158. () => ({
  159. default: ctx.AuthenticationManager,
  160. })
  161. )
  162. vi.doMock('../../../../app/src/infrastructure/Features', () => ({
  163. default: ctx.Features,
  164. }))
  165. vi.doMock('../../../../app/src/Features/User/UserAuditLogHandler', () => ({
  166. default: ctx.UserAuditLogHandler,
  167. }))
  168. vi.doMock('../../../../app/src/Features/User/UserHandler', () => ({
  169. default: ctx.UserHandler,
  170. }))
  171. vi.doMock('../../../../app/src/Features/User/UserSessionsManager', () => ({
  172. default: ctx.UserSessionsManager,
  173. }))
  174. vi.doMock('../../../../app/src/Features/Errors/HttpErrorHandler', () => ({
  175. default: ctx.HttpErrorHandler,
  176. }))
  177. vi.doMock('@overleaf/settings', () => ({
  178. default: ctx.settings,
  179. }))
  180. vi.doMock('@overleaf/o-error', () => ({
  181. default: OError,
  182. }))
  183. vi.doMock('../../../../app/src/Features/Email/EmailHandler', () => ({
  184. default: ctx.EmailHandler,
  185. }))
  186. vi.doMock(
  187. '../../../../app/src/Features/Security/OneTimeTokenHandler',
  188. () => ({
  189. default: ctx.OneTimeTokenHandler,
  190. })
  191. )
  192. vi.doMock(
  193. '../../../../app/src/infrastructure/RequestContentTypeDetection',
  194. () => ctx.RequestContentTypeDetection
  195. )
  196. vi.doMock('../../../../app/src/infrastructure/Modules', () => ({
  197. default: ctx.Modules,
  198. }))
  199. ctx.UserController = (await import(modulePath)).default
  200. ctx.res = {
  201. send: sinon.stub(),
  202. status: sinon.stub(),
  203. sendStatus: sinon.stub(),
  204. json: sinon.stub(),
  205. }
  206. ctx.res.status.returns(ctx.res)
  207. ctx.next = sinon.stub()
  208. ctx.callback = sinon.stub()
  209. })
  210. describe('tryDeleteUser', function () {
  211. beforeEach(function (ctx) {
  212. ctx.req.body.password = 'wat'
  213. ctx.req.logout = sinon.stub().yields()
  214. ctx.req.session.destroy = sinon.stub().yields()
  215. ctx.SessionManager.getLoggedInUserId = sinon.stub().returns(ctx.user._id)
  216. ctx.AuthenticationManager.promises.authenticate.resolves({
  217. user: ctx.user,
  218. })
  219. })
  220. it('should send 200', function (ctx) {
  221. return new Promise(resolve => {
  222. ctx.res.sendStatus = code => {
  223. code.should.equal(200)
  224. resolve()
  225. }
  226. ctx.UserController.tryDeleteUser(ctx.req, ctx.res, ctx.next)
  227. })
  228. })
  229. it('should try to authenticate user', function (ctx) {
  230. return new Promise(resolve => {
  231. ctx.res.sendStatus = code => {
  232. ctx.AuthenticationManager.promises.authenticate.should.have.been
  233. .calledOnce
  234. ctx.AuthenticationManager.promises.authenticate.should.have.been.calledWith(
  235. { _id: ctx.user._id },
  236. ctx.req.body.password
  237. )
  238. resolve()
  239. }
  240. ctx.UserController.tryDeleteUser(ctx.req, ctx.res, ctx.next)
  241. })
  242. })
  243. it('should delete the user', function (ctx) {
  244. return new Promise(resolve => {
  245. ctx.res.sendStatus = code => {
  246. ctx.UserDeleter.promises.deleteUser.should.have.been.calledOnce
  247. ctx.UserDeleter.promises.deleteUser.should.have.been.calledWith(
  248. ctx.user._id
  249. )
  250. resolve()
  251. }
  252. ctx.UserController.tryDeleteUser(ctx.req, ctx.res, ctx.next)
  253. })
  254. })
  255. it('should call hook to try to delete v1 account', function (ctx) {
  256. return new Promise(resolve => {
  257. ctx.res.sendStatus = code => {
  258. expect(ctx.Modules.promises.hooks.fire).to.have.been.calledWith(
  259. 'tryDeleteV1Account',
  260. ctx.user
  261. )
  262. resolve()
  263. }
  264. ctx.UserController.tryDeleteUser(ctx.req, ctx.res, ctx.next)
  265. })
  266. })
  267. describe('when no password is supplied', function () {
  268. beforeEach(function (ctx) {
  269. ctx.req.body.password = ''
  270. })
  271. it('should return 403', function (ctx) {
  272. return new Promise(resolve => {
  273. ctx.res.sendStatus = code => {
  274. code.should.equal(403)
  275. resolve()
  276. }
  277. ctx.UserController.tryDeleteUser(ctx.req, ctx.res, ctx.next)
  278. })
  279. })
  280. })
  281. describe('when authenticate produces an error', function () {
  282. beforeEach(function (ctx) {
  283. ctx.AuthenticationManager.promises.authenticate.rejects(
  284. new Error('woops')
  285. )
  286. })
  287. it('should call next with an error', function (ctx) {
  288. return new Promise(resolve => {
  289. ctx.next = err => {
  290. expect(err).to.not.equal(null)
  291. expect(err).to.be.instanceof(Error)
  292. resolve()
  293. }
  294. ctx.UserController.tryDeleteUser(ctx.req, ctx.res, ctx.next)
  295. })
  296. })
  297. })
  298. describe('when authenticate does not produce a user', function () {
  299. beforeEach(function (ctx) {
  300. ctx.AuthenticationManager.promises.authenticate.resolves({
  301. user: null,
  302. })
  303. })
  304. it('should return 403', function (ctx) {
  305. return new Promise(resolve => {
  306. ctx.res.sendStatus = code => {
  307. code.should.equal(403)
  308. resolve()
  309. }
  310. ctx.UserController.tryDeleteUser(ctx.req, ctx.res, ctx.next)
  311. })
  312. })
  313. })
  314. describe('when deleteUser produces an error', function () {
  315. beforeEach(function (ctx) {
  316. ctx.UserDeleter.promises.deleteUser.rejects(new Error('woops'))
  317. })
  318. it('should call next with an error', function (ctx) {
  319. return new Promise(resolve => {
  320. ctx.next = err => {
  321. expect(err).to.not.equal(null)
  322. expect(err).to.be.instanceof(Error)
  323. resolve()
  324. }
  325. ctx.UserController.tryDeleteUser(ctx.req, ctx.res, ctx.next)
  326. })
  327. })
  328. })
  329. describe('when deleteUser produces a known error', function () {
  330. beforeEach(function (ctx) {
  331. ctx.UserDeleter.promises.deleteUser.rejects(
  332. new Errors.SubscriptionAdminDeletionError()
  333. )
  334. })
  335. it('should return a HTTP Unprocessable Entity error', function (ctx) {
  336. return new Promise(resolve => {
  337. ctx.HttpErrorHandler.unprocessableEntity = sinon.spy(
  338. (req, res, message, info) => {
  339. expect(req).to.exist
  340. expect(res).to.exist
  341. expect(message).to.equal('error while deleting user account')
  342. expect(info).to.deep.equal({
  343. error: 'SubscriptionAdminDeletionError',
  344. })
  345. resolve()
  346. }
  347. )
  348. ctx.UserController.tryDeleteUser(ctx.req, ctx.res)
  349. })
  350. })
  351. })
  352. describe('when session.destroy produces an error', function () {
  353. beforeEach(function (ctx) {
  354. ctx.req.session.destroy = sinon
  355. .stub()
  356. .callsArgWith(0, new Error('woops'))
  357. })
  358. it('should call next with an error', function (ctx) {
  359. return new Promise(resolve => {
  360. ctx.next = err => {
  361. expect(err).to.not.equal(null)
  362. expect(err).to.be.instanceof(Error)
  363. resolve()
  364. }
  365. ctx.UserController.tryDeleteUser(ctx.req, ctx.res, ctx.next)
  366. })
  367. })
  368. })
  369. })
  370. describe('subscribe', function () {
  371. it('should send the user to subscribe', function (ctx) {
  372. return new Promise(resolve => {
  373. ctx.res.json = data => {
  374. expect(data.message).to.equal('thanks_settings_updated')
  375. ctx.NewsLetterManager.promises.subscribe.should.have.been.calledWith(
  376. ctx.user
  377. )
  378. resolve()
  379. }
  380. ctx.UserController.subscribe(ctx.req, ctx.res)
  381. })
  382. })
  383. })
  384. describe('unsubscribe', function () {
  385. it('should send the user to unsubscribe', function (ctx) {
  386. return new Promise(resolve => {
  387. ctx.res.json = data => {
  388. expect(data.message).to.equal('thanks_settings_updated')
  389. ctx.NewsLetterManager.promises.unsubscribe.should.have.been.calledWith(
  390. ctx.user
  391. )
  392. resolve()
  393. }
  394. ctx.UserController.unsubscribe(ctx.req, ctx.res, ctx.next)
  395. })
  396. })
  397. })
  398. describe('updateUserSettings', function () {
  399. beforeEach(function (ctx) {
  400. ctx.auditLog = { initiatorId: ctx.user_id, ipAddress: ctx.req.ip }
  401. ctx.newEmail = 'hello@world.com'
  402. ctx.req.externalAuthenticationSystemUsed = sinon.stub().returns(false)
  403. })
  404. it('should call save', function (ctx) {
  405. return new Promise(resolve => {
  406. ctx.req.body = {}
  407. ctx.res.sendStatus = code => {
  408. ctx.user.save.called.should.equal(true)
  409. resolve()
  410. }
  411. ctx.UserController.updateUserSettings(ctx.req, ctx.res, ctx.next)
  412. })
  413. })
  414. it('should set the first name', function (ctx) {
  415. return new Promise(resolve => {
  416. ctx.req.body = { first_name: 'bobby ' }
  417. ctx.res.sendStatus = code => {
  418. ctx.user.first_name.should.equal('bobby')
  419. resolve()
  420. }
  421. ctx.UserController.updateUserSettings(ctx.req, ctx.res)
  422. })
  423. })
  424. it('should set the role', function (ctx) {
  425. return new Promise(resolve => {
  426. ctx.req.body = { role: 'student' }
  427. ctx.res.sendStatus = code => {
  428. ctx.user.role.should.equal('student')
  429. resolve()
  430. }
  431. ctx.UserController.updateUserSettings(ctx.req, ctx.res)
  432. })
  433. })
  434. it('should set the institution', function (ctx) {
  435. return new Promise(resolve => {
  436. ctx.req.body = { institution: 'MIT' }
  437. ctx.res.sendStatus = code => {
  438. ctx.user.institution.should.equal('MIT')
  439. resolve()
  440. }
  441. ctx.UserController.updateUserSettings(ctx.req, ctx.res)
  442. })
  443. })
  444. it('should set some props on ace', function (ctx) {
  445. return new Promise(resolve => {
  446. ctx.req.body = { editorTheme: 'something' }
  447. ctx.res.sendStatus = code => {
  448. ctx.user.ace.theme.should.equal('something')
  449. resolve()
  450. }
  451. ctx.UserController.updateUserSettings(ctx.req, ctx.res)
  452. })
  453. })
  454. it('should set the overall theme', function (ctx) {
  455. return new Promise(resolve => {
  456. ctx.req.body = { overallTheme: 'green-ish' }
  457. ctx.res.sendStatus = code => {
  458. ctx.user.ace.overallTheme.should.equal('green-ish')
  459. resolve()
  460. }
  461. ctx.UserController.updateUserSettings(ctx.req, ctx.res)
  462. })
  463. })
  464. it('should set referencesSearchMode to advanced', function (ctx) {
  465. return new Promise(resolve => {
  466. ctx.req.body = { referencesSearchMode: 'advanced' }
  467. ctx.res.sendStatus = code => {
  468. ctx.user.ace.referencesSearchMode.should.equal('advanced')
  469. resolve()
  470. }
  471. ctx.UserController.updateUserSettings(ctx.req, ctx.res)
  472. })
  473. })
  474. it('should set referencesSearchMode to simple', function (ctx) {
  475. return new Promise(resolve => {
  476. ctx.req.body = { referencesSearchMode: 'simple' }
  477. ctx.res.sendStatus = code => {
  478. ctx.user.ace.referencesSearchMode.should.equal('simple')
  479. resolve()
  480. }
  481. ctx.UserController.updateUserSettings(ctx.req, ctx.res)
  482. })
  483. })
  484. it('should not allow arbitrary referencesSearchMode', function (ctx) {
  485. return new Promise(resolve => {
  486. ctx.req.body = { referencesSearchMode: 'foobar' }
  487. ctx.res.sendStatus = code => {
  488. ctx.user.ace.referencesSearchMode.should.equal('advanced')
  489. resolve()
  490. }
  491. ctx.UserController.updateUserSettings(ctx.req, ctx.res)
  492. })
  493. })
  494. it('should set enableNewEditorStageFour to true', function (ctx) {
  495. return new Promise(resolve => {
  496. ctx.req.body = { enableNewEditor: true }
  497. ctx.res.sendStatus = code => {
  498. ctx.user.ace.enableNewEditorStageFour.should.equal(true)
  499. resolve()
  500. }
  501. ctx.UserController.updateUserSettings(ctx.req, ctx.res)
  502. })
  503. })
  504. it('should set enableNewEditorStageFour to false', function (ctx) {
  505. return new Promise(resolve => {
  506. ctx.req.body = { enableNewEditor: false }
  507. ctx.res.sendStatus = code => {
  508. ctx.user.ace.enableNewEditorStageFour.should.equal(false)
  509. resolve()
  510. }
  511. ctx.UserController.updateUserSettings(ctx.req, ctx.res)
  512. })
  513. })
  514. it('should keep enableNewEditorStageFour a boolean', function (ctx) {
  515. return new Promise(resolve => {
  516. ctx.req.body = { enableNewEditor: 'foobar' }
  517. ctx.res.sendStatus = code => {
  518. ctx.user.ace.enableNewEditorStageFour.should.equal(true)
  519. resolve()
  520. }
  521. ctx.UserController.updateUserSettings(ctx.req, ctx.res)
  522. })
  523. })
  524. it('should set darkModePdf to true', function (ctx) {
  525. return new Promise(resolve => {
  526. ctx.req.body = { darkModePdf: true }
  527. ctx.res.sendStatus = code => {
  528. ctx.user.ace.darkModePdf.should.equal(true)
  529. resolve()
  530. }
  531. ctx.UserController.updateUserSettings(ctx.req, ctx.res)
  532. })
  533. })
  534. it('should set darkModePdf to false', function (ctx) {
  535. return new Promise(resolve => {
  536. ctx.req.body = { darkModePdf: false }
  537. ctx.res.sendStatus = code => {
  538. ctx.user.ace.darkModePdf.should.equal(false)
  539. resolve()
  540. }
  541. ctx.UserController.updateUserSettings(ctx.req, ctx.res)
  542. })
  543. })
  544. it('should keep darkModePdf a boolean', function (ctx) {
  545. return new Promise(resolve => {
  546. ctx.req.body = { darkModePdf: 'foobar' }
  547. ctx.res.sendStatus = code => {
  548. ctx.user.ace.darkModePdf.should.equal(true)
  549. resolve()
  550. }
  551. ctx.UserController.updateUserSettings(ctx.req, ctx.res)
  552. })
  553. })
  554. it('should send an error if the email is 0 len', function (ctx) {
  555. return new Promise(resolve => {
  556. ctx.req.body.email = ''
  557. ctx.res.sendStatus = function (code) {
  558. code.should.equal(400)
  559. resolve()
  560. }
  561. ctx.UserController.updateUserSettings(ctx.req, ctx.res)
  562. })
  563. })
  564. it('should send an error if the email does not contain an @', function (ctx) {
  565. return new Promise(resolve => {
  566. ctx.req.body.email = 'bob at something dot com'
  567. ctx.res.sendStatus = function (code) {
  568. code.should.equal(400)
  569. resolve()
  570. }
  571. ctx.UserController.updateUserSettings(ctx.req, ctx.res)
  572. })
  573. })
  574. it('should call the user updater with the new email and user _id', function (ctx) {
  575. return new Promise(resolve => {
  576. ctx.req.body.email = ctx.newEmail.toUpperCase()
  577. ctx.res.sendStatus = code => {
  578. code.should.equal(200)
  579. ctx.UserUpdater.promises.changeEmailAddress.should.have.been.calledWith(
  580. ctx.user_id,
  581. ctx.newEmail,
  582. ctx.auditLog
  583. )
  584. resolve()
  585. }
  586. ctx.UserController.updateUserSettings(ctx.req, ctx.res)
  587. })
  588. })
  589. it('should update the email on the session', function (ctx) {
  590. return new Promise(resolve => {
  591. ctx.req.body.email = ctx.newEmail.toUpperCase()
  592. let callcount = 0
  593. ctx.User.findById = id => ({
  594. exec: async () => {
  595. if (++callcount === 2) {
  596. ctx.user.email = ctx.newEmail
  597. }
  598. return ctx.user
  599. },
  600. })
  601. ctx.res.sendStatus = code => {
  602. code.should.equal(200)
  603. ctx.SessionManager.setInSessionUser
  604. .calledWith(ctx.req.session, {
  605. email: ctx.newEmail,
  606. first_name: undefined,
  607. last_name: undefined,
  608. })
  609. .should.equal(true)
  610. resolve()
  611. }
  612. ctx.UserController.updateUserSettings(ctx.req, ctx.res)
  613. })
  614. })
  615. it('should call populateTeamInvites', function (ctx) {
  616. return new Promise(resolve => {
  617. ctx.req.body.email = ctx.newEmail.toUpperCase()
  618. ctx.res.sendStatus = code => {
  619. code.should.equal(200)
  620. ctx.UserHandler.promises.populateTeamInvites.should.have.been.calledWith(
  621. ctx.user
  622. )
  623. resolve()
  624. }
  625. ctx.UserController.updateUserSettings(ctx.req, ctx.res)
  626. })
  627. })
  628. describe('when changeEmailAddress yields an error', function () {
  629. it('should pass on an error and not send a success status', function (ctx) {
  630. return new Promise(resolve => {
  631. ctx.req.body.email = ctx.newEmail.toUpperCase()
  632. ctx.UserUpdater.promises.changeEmailAddress.rejects(new OError())
  633. ctx.HttpErrorHandler.legacyInternal = sinon.spy(
  634. (req, res, message, error) => {
  635. expect(req).to.exist
  636. expect(req).to.exist
  637. message.should.equal('problem_changing_email_address')
  638. expect(error).to.be.instanceof(OError)
  639. resolve()
  640. }
  641. )
  642. ctx.UserController.updateUserSettings(ctx.req, ctx.res, ctx.next)
  643. })
  644. })
  645. it('should call the HTTP conflict error handler when the email already exists', function (ctx) {
  646. return new Promise(resolve => {
  647. ctx.HttpErrorHandler.conflict = sinon.spy((req, res, message) => {
  648. expect(req).to.exist
  649. expect(req).to.exist
  650. message.should.equal('email_already_registered')
  651. resolve()
  652. })
  653. ctx.req.body.email = ctx.newEmail.toUpperCase()
  654. ctx.UserUpdater.promises.changeEmailAddress.rejects(
  655. new Errors.EmailExistsError()
  656. )
  657. ctx.UserController.updateUserSettings(ctx.req, ctx.res)
  658. })
  659. })
  660. })
  661. describe('when using an external auth source', function () {
  662. beforeEach(function (ctx) {
  663. ctx.newEmail = 'someone23@example.com'
  664. ctx.req.externalAuthenticationSystemUsed = sinon.stub().returns(true)
  665. })
  666. it('should not set a new email', function (ctx) {
  667. return new Promise(resolve => {
  668. ctx.req.body.email = ctx.newEmail
  669. ctx.res.sendStatus = code => {
  670. code.should.equal(200)
  671. ctx.UserUpdater.promises.changeEmailAddress
  672. .calledWith(ctx.user_id, ctx.newEmail)
  673. .should.equal(false)
  674. resolve()
  675. }
  676. ctx.UserController.updateUserSettings(ctx.req, ctx.res)
  677. })
  678. })
  679. })
  680. })
  681. describe('logout', function () {
  682. beforeEach(function (ctx) {
  683. ctx.RequestContentTypeDetection.acceptsJson.returns(false)
  684. })
  685. it('should destroy the session', function (ctx) {
  686. return new Promise(resolve => {
  687. ctx.req.session.destroy = sinon.stub().callsArgWith(0)
  688. ctx.res.redirect = url => {
  689. url.should.equal('/login')
  690. ctx.req.session.destroy.called.should.equal(true)
  691. resolve()
  692. }
  693. ctx.UserController.logout(ctx.req, ctx.res)
  694. })
  695. })
  696. it('should untrack session', function (ctx) {
  697. return new Promise(resolve => {
  698. ctx.req.session.destroy = sinon.stub().callsArgWith(0)
  699. ctx.res.redirect = url => {
  700. url.should.equal('/login')
  701. ctx.UserSessionsManager.promises.untrackSession.should.have.been
  702. .calledOnce
  703. ctx.UserSessionsManager.promises.untrackSession.should.have.been.calledWith(
  704. sinon.match(ctx.req.user),
  705. ctx.req.sessionID
  706. )
  707. resolve()
  708. }
  709. ctx.UserController.logout(ctx.req, ctx.res)
  710. })
  711. })
  712. it('should redirect after logout', function (ctx) {
  713. return new Promise(resolve => {
  714. ctx.req.body.redirect = '/sso-login'
  715. ctx.req.session.destroy = sinon.stub().callsArgWith(0)
  716. ctx.res.redirect = url => {
  717. url.should.equal(ctx.req.body.redirect)
  718. resolve()
  719. }
  720. ctx.UserController.logout(ctx.req, ctx.res)
  721. })
  722. })
  723. it('should redirect after logout, but not to evil.com', function (ctx) {
  724. return new Promise(resolve => {
  725. ctx.req.body.redirect = 'https://evil.com'
  726. ctx.req.session.destroy = sinon.stub().callsArgWith(0)
  727. ctx.res.redirect = url => {
  728. url.should.equal('/login')
  729. resolve()
  730. }
  731. ctx.UserController.logout(ctx.req, ctx.res)
  732. })
  733. })
  734. it('should redirect to login after logout when no redirect set', function (ctx) {
  735. return new Promise(resolve => {
  736. ctx.req.session.destroy = sinon.stub().callsArgWith(0)
  737. ctx.res.redirect = url => {
  738. url.should.equal('/login')
  739. resolve()
  740. }
  741. ctx.UserController.logout(ctx.req, ctx.res)
  742. })
  743. })
  744. it('should send json with redir property for json request', function (ctx) {
  745. return new Promise(resolve => {
  746. ctx.RequestContentTypeDetection.acceptsJson.returns(true)
  747. ctx.req.session.destroy = sinon.stub().callsArgWith(0)
  748. ctx.res.status = code => {
  749. code.should.equal(200)
  750. return ctx.res
  751. }
  752. ctx.res.json = data => {
  753. data.redir.should.equal('/login')
  754. resolve()
  755. }
  756. ctx.UserController.logout(ctx.req, ctx.res)
  757. })
  758. })
  759. })
  760. describe('clearSessions', function () {
  761. describe('success', function () {
  762. it('should call removeSessionsFromRedis', function (ctx) {
  763. return new Promise(resolve => {
  764. ctx.res.sendStatus.callsFake(() => {
  765. ctx.UserSessionsManager.promises.removeSessionsFromRedis.should.have
  766. .been.calledOnce
  767. resolve()
  768. })
  769. ctx.UserController.clearSessions(ctx.req, ctx.res)
  770. })
  771. })
  772. it('send a 201 response', function (ctx) {
  773. return new Promise(resolve => {
  774. ctx.res.sendStatus.callsFake(status => {
  775. status.should.equal(201)
  776. resolve()
  777. })
  778. ctx.UserController.clearSessions(ctx.req, ctx.res)
  779. })
  780. })
  781. it('sends a security alert email', function (ctx) {
  782. return new Promise(resolve => {
  783. ctx.res.sendStatus.callsFake(status => {
  784. ctx.EmailHandler.promises.sendEmail.callCount.should.equal(1)
  785. const expectedArg = {
  786. to: ctx.user.email,
  787. actionDescribed: `active sessions were cleared on your account ${ctx.user.email}`,
  788. action: 'active sessions cleared',
  789. }
  790. const emailCall = ctx.EmailHandler.promises.sendEmail.lastCall
  791. expect(emailCall.args[0]).to.equal('securityAlert')
  792. expect(emailCall.args[1]).to.deep.equal(expectedArg)
  793. resolve()
  794. })
  795. ctx.UserController.clearSessions(ctx.req, ctx.res)
  796. })
  797. })
  798. })
  799. describe('errors', function () {
  800. describe('when getAllUserSessions produces an error', function () {
  801. it('should return an error', function (ctx) {
  802. return new Promise(resolve => {
  803. ctx.UserSessionsManager.promises.getAllUserSessions.rejects(
  804. new Error('woops')
  805. )
  806. ctx.UserController.clearSessions(ctx.req, ctx.res, error => {
  807. expect(error).to.be.instanceof(Error)
  808. resolve()
  809. })
  810. })
  811. })
  812. })
  813. describe('when audit log addEntry produces an error', function () {
  814. it('should call next with an error', function (ctx) {
  815. return new Promise(resolve => {
  816. ctx.UserAuditLogHandler.promises.addEntry.rejects(
  817. new Error('woops')
  818. )
  819. ctx.UserController.clearSessions(ctx.req, ctx.res, error => {
  820. expect(error).to.be.instanceof(Error)
  821. resolve()
  822. })
  823. })
  824. })
  825. })
  826. describe('when removeSessionsFromRedis produces an error', function () {
  827. it('should call next with an error', function (ctx) {
  828. return new Promise(resolve => {
  829. ctx.UserSessionsManager.promises.removeSessionsFromRedis.rejects(
  830. new Error('woops')
  831. )
  832. ctx.UserController.clearSessions(ctx.req, ctx.res, error => {
  833. expect(error).to.be.instanceof(Error)
  834. resolve()
  835. })
  836. })
  837. })
  838. })
  839. describe('when EmailHandler produces an error', function () {
  840. const anError = new Error('oops')
  841. it('send a 201 response but log error', function (ctx) {
  842. return new Promise(resolve => {
  843. ctx.EmailHandler.promises.sendEmail.rejects(anError)
  844. ctx.res.sendStatus.callsFake(status => {
  845. status.should.equal(201)
  846. expect(ctx.logger.error).toHaveBeenCalledTimes(1)
  847. const loggerCall = ctx.logger.error.mock.calls[0]
  848. expect(loggerCall[0]).to.deep.equal({
  849. error: anError,
  850. userId: ctx.user_id,
  851. })
  852. expect(loggerCall[1]).to.contain(
  853. 'could not send security alert email when sessions cleared'
  854. )
  855. resolve()
  856. })
  857. ctx.UserController.clearSessions(ctx.req, ctx.res)
  858. })
  859. })
  860. })
  861. })
  862. })
  863. describe('changePassword', function () {
  864. describe('success', function () {
  865. beforeEach(function (ctx) {
  866. ctx.AuthenticationManager.promises.authenticate.resolves({
  867. user: ctx.user,
  868. })
  869. ctx.AuthenticationManager.promises.setUserPassword.resolves()
  870. ctx.req.body = {
  871. newPassword1: 'newpass',
  872. newPassword2: 'newpass',
  873. }
  874. })
  875. it('should set the new password if they do match', function (ctx) {
  876. return new Promise(resolve => {
  877. ctx.res.json.callsFake(() => {
  878. ctx.AuthenticationManager.promises.setUserPassword.should.have.been.calledWith(
  879. ctx.user,
  880. 'newpass'
  881. )
  882. resolve()
  883. })
  884. ctx.UserController.changePassword(ctx.req, ctx.res)
  885. })
  886. })
  887. it('should log the update', function (ctx) {
  888. return new Promise(resolve => {
  889. ctx.res.json.callsFake(() => {
  890. ctx.UserAuditLogHandler.promises.addEntry.should.have.been.calledWith(
  891. ctx.user._id,
  892. 'update-password',
  893. ctx.user._id,
  894. ctx.req.ip
  895. )
  896. ctx.AuthenticationManager.promises.setUserPassword.callCount.should.equal(
  897. 1
  898. )
  899. resolve()
  900. })
  901. ctx.UserController.changePassword(ctx.req, ctx.res)
  902. })
  903. })
  904. it('should send security alert email', function (ctx) {
  905. return new Promise(resolve => {
  906. ctx.res.json.callsFake(() => {
  907. const expectedArg = {
  908. to: ctx.user.email,
  909. actionDescribed: `your password has been changed on your account ${ctx.user.email}`,
  910. action: 'password changed',
  911. }
  912. const emailCall = ctx.EmailHandler.promises.sendEmail.lastCall
  913. expect(emailCall.args[0]).to.equal('securityAlert')
  914. expect(emailCall.args[1]).to.deep.equal(expectedArg)
  915. resolve()
  916. })
  917. ctx.UserController.changePassword(ctx.req, ctx.res)
  918. })
  919. })
  920. it('should expire password reset tokens', function (ctx) {
  921. return new Promise(resolve => {
  922. ctx.res.json.callsFake(() => {
  923. ctx.OneTimeTokenHandler.promises.expireAllTokensForUser.should.have.been.calledWith(
  924. ctx.user._id,
  925. 'password'
  926. )
  927. resolve()
  928. })
  929. ctx.UserController.changePassword(ctx.req, ctx.res)
  930. })
  931. })
  932. })
  933. describe('errors', function () {
  934. it('should check the old password is the current one at the moment', function (ctx) {
  935. return new Promise(resolve => {
  936. ctx.AuthenticationManager.promises.authenticate.resolves({})
  937. ctx.req.body = { currentPassword: 'oldpasshere' }
  938. ctx.HttpErrorHandler.badRequest.callsFake(() => {
  939. expect(ctx.HttpErrorHandler.badRequest).to.have.been.calledWith(
  940. ctx.req,
  941. ctx.res,
  942. 'password_change_old_password_wrong'
  943. )
  944. ctx.AuthenticationManager.promises.authenticate.should.have.been.calledWith(
  945. { _id: ctx.user._id },
  946. 'oldpasshere'
  947. )
  948. ctx.AuthenticationManager.promises.setUserPassword.callCount.should.equal(
  949. 0
  950. )
  951. resolve()
  952. })
  953. ctx.UserController.changePassword(ctx.req, ctx.res)
  954. })
  955. })
  956. it('it should not set the new password if they do not match', function (ctx) {
  957. return new Promise(resolve => {
  958. ctx.AuthenticationManager.promises.authenticate.resolves({
  959. user: ctx.user,
  960. })
  961. ctx.req.body = {
  962. newPassword1: '1',
  963. newPassword2: '2',
  964. }
  965. ctx.HttpErrorHandler.badRequest.callsFake(() => {
  966. expect(ctx.HttpErrorHandler.badRequest).to.have.been.calledWith(
  967. ctx.req,
  968. ctx.res,
  969. 'password_change_passwords_do_not_match'
  970. )
  971. ctx.AuthenticationManager.promises.setUserPassword.callCount.should.equal(
  972. 0
  973. )
  974. resolve()
  975. })
  976. ctx.UserController.changePassword(ctx.req, ctx.res)
  977. })
  978. })
  979. it('it should not set the new password if it is invalid', function (ctx) {
  980. return new Promise(resolve => {
  981. // this.AuthenticationManager.validatePassword = sinon
  982. // .stub()
  983. // .returns({ message: 'validation-error' })
  984. const err = new Error('bad')
  985. err.name = 'InvalidPasswordError'
  986. const message = {
  987. type: 'error',
  988. key: 'some-message-key',
  989. }
  990. ctx.AuthenticationManager.getMessageForInvalidPasswordError.returns(
  991. message
  992. )
  993. ctx.AuthenticationManager.promises.setUserPassword.rejects(err)
  994. ctx.AuthenticationManager.promises.authenticate.resolves({
  995. user: ctx.user,
  996. })
  997. ctx.req.body = {
  998. newPassword1: 'newpass',
  999. newPassword2: 'newpass',
  1000. }
  1001. ctx.res.json.callsFake(result => {
  1002. expect(result.message).to.deep.equal(message)
  1003. ctx.AuthenticationManager.promises.setUserPassword.callCount.should.equal(
  1004. 1
  1005. )
  1006. resolve()
  1007. })
  1008. ctx.UserController.changePassword(ctx.req, ctx.res)
  1009. })
  1010. })
  1011. describe('UserAuditLogHandler error', function () {
  1012. it('should return error and not update password', function (ctx) {
  1013. return new Promise(resolve => {
  1014. ctx.UserAuditLogHandler.promises.addEntry.rejects(new Error('oops'))
  1015. ctx.AuthenticationManager.promises.authenticate.resolves({
  1016. user: ctx.user,
  1017. })
  1018. ctx.AuthenticationManager.promises.setUserPassword.resolves()
  1019. ctx.req.body = {
  1020. newPassword1: 'newpass',
  1021. newPassword2: 'newpass',
  1022. }
  1023. ctx.UserController.changePassword(ctx.req, ctx.res, error => {
  1024. expect(error).to.be.instanceof(Error)
  1025. ctx.AuthenticationManager.promises.setUserPassword.callCount.should.equal(
  1026. 1
  1027. )
  1028. resolve()
  1029. })
  1030. })
  1031. })
  1032. })
  1033. describe('EmailHandler error', function () {
  1034. const anError = new Error('oops')
  1035. beforeEach(function (ctx) {
  1036. ctx.AuthenticationManager.promises.authenticate.resolves({
  1037. user: ctx.user,
  1038. })
  1039. ctx.AuthenticationManager.promises.setUserPassword.resolves()
  1040. ctx.req.body = {
  1041. newPassword1: 'newpass',
  1042. newPassword2: 'newpass',
  1043. }
  1044. ctx.EmailHandler.promises.sendEmail.rejects(anError)
  1045. })
  1046. it('should not return error but should log it', function (ctx) {
  1047. return new Promise(resolve => {
  1048. ctx.res.json.callsFake(result => {
  1049. expect(result.message.type).to.equal('success')
  1050. expect(ctx.logger.error).toHaveBeenCalledTimes(1)
  1051. expect(ctx.logger.error).toHaveBeenCalledWith(
  1052. {
  1053. error: anError,
  1054. userId: ctx.user_id,
  1055. },
  1056. 'could not send security alert email when password changed'
  1057. )
  1058. resolve()
  1059. })
  1060. ctx.UserController.changePassword(ctx.req, ctx.res)
  1061. })
  1062. })
  1063. })
  1064. })
  1065. })
  1066. describe('ensureAffiliationMiddleware', function () {
  1067. describe('without affiliations feature', function () {
  1068. beforeEach(async function (ctx) {
  1069. await ctx.UserController.ensureAffiliationMiddleware(
  1070. ctx.req,
  1071. ctx.res,
  1072. ctx.next
  1073. )
  1074. })
  1075. it('should not run affiliation check', function (ctx) {
  1076. expect(ctx.UserGetter.promises.getUser).to.not.have.been.called
  1077. expect(ctx.UserUpdater.promises.confirmEmail).to.not.have.been.called
  1078. expect(ctx.UserUpdater.promises.addAffiliationForNewUser).to.not.have
  1079. .been.called
  1080. })
  1081. it('should not return an error', function (ctx) {
  1082. expect(ctx.next).to.be.calledWith()
  1083. })
  1084. })
  1085. describe('without ensureAffiliation query parameter', function () {
  1086. beforeEach(async function (ctx) {
  1087. ctx.Features.hasFeature.withArgs('affiliations').returns(true)
  1088. await ctx.UserController.ensureAffiliationMiddleware(
  1089. ctx.req,
  1090. ctx.res,
  1091. ctx.next
  1092. )
  1093. })
  1094. it('should not run middleware', function (ctx) {
  1095. expect(ctx.UserGetter.promises.getUser).to.not.have.been.called
  1096. expect(ctx.UserUpdater.promises.confirmEmail).to.not.have.been.called
  1097. expect(ctx.UserUpdater.promises.addAffiliationForNewUser).to.not.have
  1098. .been.called
  1099. })
  1100. it('should not return an error', function (ctx) {
  1101. expect(ctx.next).to.be.calledWith()
  1102. })
  1103. })
  1104. describe('no flagged email', function () {
  1105. beforeEach(async function (ctx) {
  1106. const email = 'unit-test@overleaf.com'
  1107. ctx.user.email = email
  1108. ctx.user.emails = [
  1109. {
  1110. email,
  1111. },
  1112. ]
  1113. ctx.Features.hasFeature.withArgs('affiliations').returns(true)
  1114. ctx.req.query.ensureAffiliation = true
  1115. await ctx.UserController.ensureAffiliationMiddleware(
  1116. ctx.req,
  1117. ctx.res,
  1118. ctx.next
  1119. )
  1120. })
  1121. it('should get the user', function (ctx) {
  1122. expect(ctx.UserGetter.promises.getUser).to.have.been.calledWith(
  1123. ctx.user._id
  1124. )
  1125. })
  1126. it('should not try to add affiliation or update user', function (ctx) {
  1127. expect(ctx.UserUpdater.promises.addAffiliationForNewUser).to.not.have
  1128. .been.called
  1129. })
  1130. it('should not return an error', function (ctx) {
  1131. expect(ctx.next).to.be.calledWith()
  1132. })
  1133. })
  1134. describe('flagged non-SSO email', function () {
  1135. let emailFlagged
  1136. beforeEach(async function (ctx) {
  1137. emailFlagged = 'flagged@overleaf.com'
  1138. ctx.user.email = emailFlagged
  1139. ctx.user.emails = [
  1140. {
  1141. email: emailFlagged,
  1142. affiliationUnchecked: true,
  1143. },
  1144. ]
  1145. ctx.Features.hasFeature.withArgs('affiliations').returns(true)
  1146. ctx.req.query.ensureAffiliation = true
  1147. ctx.req.assertPermission = sinon.stub()
  1148. await ctx.UserController.ensureAffiliationMiddleware(
  1149. ctx.req,
  1150. ctx.res,
  1151. ctx.next
  1152. )
  1153. })
  1154. it('should check the user has permission', function (ctx) {
  1155. expect(ctx.req.assertPermission).to.have.been.calledWith(
  1156. 'add-affiliation'
  1157. )
  1158. })
  1159. it('should unflag the emails but not confirm', function (ctx) {
  1160. expect(
  1161. ctx.UserUpdater.promises.addAffiliationForNewUser
  1162. ).to.have.been.calledWith(ctx.user._id, emailFlagged)
  1163. expect(
  1164. ctx.UserUpdater.promises.confirmEmail
  1165. ).to.not.have.been.calledWith(ctx.user._id, emailFlagged)
  1166. })
  1167. it('should not return an error', function (ctx) {
  1168. expect(ctx.next).to.be.calledWith()
  1169. })
  1170. })
  1171. describe('flagged SSO email', function () {
  1172. let emailFlagged
  1173. beforeEach(async function (ctx) {
  1174. emailFlagged = 'flagged@overleaf.com'
  1175. ctx.user.email = emailFlagged
  1176. ctx.user.emails = [
  1177. {
  1178. email: emailFlagged,
  1179. affiliationUnchecked: true,
  1180. samlProviderId: '123',
  1181. },
  1182. ]
  1183. ctx.Features.hasFeature.withArgs('affiliations').returns(true)
  1184. ctx.req.query.ensureAffiliation = true
  1185. ctx.req.assertPermission = sinon.stub()
  1186. await ctx.UserController.ensureAffiliationMiddleware(
  1187. ctx.req,
  1188. ctx.res,
  1189. ctx.next
  1190. )
  1191. })
  1192. it('should check the user has permission', function (ctx) {
  1193. expect(ctx.req.assertPermission).to.have.been.calledWith(
  1194. 'add-affiliation'
  1195. )
  1196. })
  1197. it('should add affiliation to v1, unflag and confirm on v2', function (ctx) {
  1198. expect(ctx.UserUpdater.promises.addAffiliationForNewUser).to.have.not
  1199. .been.called
  1200. expect(ctx.UserUpdater.promises.confirmEmail).to.have.been.calledWith(
  1201. ctx.user._id,
  1202. emailFlagged
  1203. )
  1204. })
  1205. it('should not return an error', function (ctx) {
  1206. expect(ctx.next).to.be.calledWith()
  1207. })
  1208. })
  1209. describe('when v1 returns an error', function () {
  1210. let emailFlagged
  1211. beforeEach(async function (ctx) {
  1212. ctx.UserUpdater.promises.addAffiliationForNewUser.rejects()
  1213. emailFlagged = 'flagged@overleaf.com'
  1214. ctx.user.email = emailFlagged
  1215. ctx.user.emails = [
  1216. {
  1217. email: emailFlagged,
  1218. affiliationUnchecked: true,
  1219. },
  1220. ]
  1221. ctx.Features.hasFeature.withArgs('affiliations').returns(true)
  1222. ctx.req.query.ensureAffiliation = true
  1223. ctx.req.assertPermission = sinon.stub()
  1224. await ctx.UserController.ensureAffiliationMiddleware(
  1225. ctx.req,
  1226. ctx.res,
  1227. ctx.next
  1228. )
  1229. })
  1230. it('should check the user has permission', function (ctx) {
  1231. expect(ctx.req.assertPermission).to.have.been.calledWith(
  1232. 'add-affiliation'
  1233. )
  1234. })
  1235. it('should return the error', function (ctx) {
  1236. expect(ctx.next).to.be.calledWith(sinon.match.instanceOf(Error))
  1237. })
  1238. })
  1239. describe('when user is not found', function () {
  1240. beforeEach(async function (ctx) {
  1241. ctx.UserGetter.promises.getUser.rejects(new Error('not found'))
  1242. ctx.Features.hasFeature.withArgs('affiliations').returns(true)
  1243. ctx.req.query.ensureAffiliation = true
  1244. await ctx.UserController.ensureAffiliationMiddleware(
  1245. ctx.req,
  1246. ctx.res,
  1247. ctx.next
  1248. )
  1249. })
  1250. it('should return the error', function (ctx) {
  1251. expect(ctx.next).to.be.calledWith(sinon.match.instanceOf(Error))
  1252. })
  1253. })
  1254. })
  1255. })