UserController.test.mjs 42 KB

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