UserController.test.mjs 45 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449
  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 darkModePdf to true', function (ctx) {
  464. return new Promise(resolve => {
  465. ctx.req.body = { darkModePdf: true }
  466. ctx.res.sendStatus = code => {
  467. ctx.user.ace.darkModePdf.should.equal(true)
  468. resolve()
  469. }
  470. ctx.UserController.updateUserSettings(ctx.req, ctx.res)
  471. })
  472. })
  473. it('should set darkModePdf to false', function (ctx) {
  474. return new Promise(resolve => {
  475. ctx.req.body = { darkModePdf: false }
  476. ctx.res.sendStatus = code => {
  477. ctx.user.ace.darkModePdf.should.equal(false)
  478. resolve()
  479. }
  480. ctx.UserController.updateUserSettings(ctx.req, ctx.res)
  481. })
  482. })
  483. it('should keep darkModePdf a boolean', function (ctx) {
  484. return new Promise(resolve => {
  485. ctx.req.body = { darkModePdf: 'foobar' }
  486. ctx.res.sendStatus = code => {
  487. ctx.user.ace.darkModePdf.should.equal(true)
  488. resolve()
  489. }
  490. ctx.UserController.updateUserSettings(ctx.req, ctx.res)
  491. })
  492. })
  493. it('should set zotero settings object', function (ctx) {
  494. return new Promise(resolve => {
  495. ctx.req.body = {
  496. zotero: {
  497. enabled: false,
  498. groups: [{ id: '123' }],
  499. disablePersonalLibrary: true,
  500. },
  501. }
  502. ctx.res.sendStatus = code => {
  503. ctx.user.ace.zotero.enabled.should.equal(false)
  504. ctx.user.ace.zotero.groups.should.deep.equal([{ id: '123' }])
  505. ctx.user.ace.zotero.disablePersonalLibrary.should.equal(true)
  506. resolve()
  507. }
  508. ctx.UserController.updateUserSettings(ctx.req, ctx.res)
  509. })
  510. })
  511. it('should set zotero settings with partial update', function (ctx) {
  512. return new Promise(resolve => {
  513. ctx.user.ace.zotero = {
  514. enabled: true,
  515. groups: [{ id: 'existing' }],
  516. disablePersonalLibrary: false,
  517. }
  518. ctx.req.body = {
  519. zotero: { enabled: false },
  520. }
  521. ctx.res.sendStatus = code => {
  522. ctx.user.ace.zotero.enabled.should.equal(false)
  523. resolve()
  524. }
  525. ctx.UserController.updateUserSettings(ctx.req, ctx.res)
  526. })
  527. })
  528. it('should set mendeley settings object', function (ctx) {
  529. return new Promise(resolve => {
  530. ctx.req.body = {
  531. mendeley: {
  532. enabled: false,
  533. groups: [{ id: 'group-456' }],
  534. disablePersonalLibrary: true,
  535. },
  536. }
  537. ctx.res.sendStatus = code => {
  538. ctx.user.ace.mendeley.enabled.should.equal(false)
  539. ctx.user.ace.mendeley.groups.should.deep.equal([{ id: 'group-456' }])
  540. ctx.user.ace.mendeley.disablePersonalLibrary.should.equal(true)
  541. resolve()
  542. }
  543. ctx.UserController.updateUserSettings(ctx.req, ctx.res)
  544. })
  545. })
  546. it('should set mendeley with multiple groups', function (ctx) {
  547. return new Promise(resolve => {
  548. ctx.req.body = {
  549. mendeley: {
  550. enabled: true,
  551. groups: [{ id: 'group-1' }, { id: 'group-2' }, { id: 'group-3' }],
  552. disablePersonalLibrary: false,
  553. },
  554. }
  555. ctx.res.sendStatus = code => {
  556. ctx.user.ace.mendeley.groups.should.have.length(3)
  557. ctx.user.ace.mendeley.groups[0].id.should.equal('group-1')
  558. ctx.user.ace.mendeley.groups[1].id.should.equal('group-2')
  559. ctx.user.ace.mendeley.groups[2].id.should.equal('group-3')
  560. resolve()
  561. }
  562. ctx.UserController.updateUserSettings(ctx.req, ctx.res)
  563. })
  564. })
  565. it('should set papers settings object', function (ctx) {
  566. return new Promise(resolve => {
  567. ctx.req.body = {
  568. papers: {
  569. enabled: true,
  570. groups: [],
  571. disablePersonalLibrary: false,
  572. },
  573. }
  574. ctx.res.sendStatus = code => {
  575. ctx.user.ace.papers.enabled.should.equal(true)
  576. ctx.user.ace.papers.groups.should.deep.equal([])
  577. ctx.user.ace.papers.disablePersonalLibrary.should.equal(false)
  578. resolve()
  579. }
  580. ctx.UserController.updateUserSettings(ctx.req, ctx.res)
  581. })
  582. })
  583. it('should allow setting only papers disablePersonalLibrary', function (ctx) {
  584. return new Promise(resolve => {
  585. ctx.req.body = {
  586. papers: { disablePersonalLibrary: true },
  587. }
  588. ctx.res.sendStatus = code => {
  589. ctx.user.ace.papers.disablePersonalLibrary.should.equal(true)
  590. resolve()
  591. }
  592. ctx.UserController.updateUserSettings(ctx.req, ctx.res)
  593. })
  594. })
  595. it('should handle undefined mendeley by not setting it', function (ctx) {
  596. return new Promise(resolve => {
  597. ctx.user.ace.mendeley = { enabled: true, groups: [] }
  598. ctx.req.body = { mendeley: undefined }
  599. ctx.res.sendStatus = code => {
  600. ctx.user.ace.mendeley.enabled.should.equal(true)
  601. resolve()
  602. }
  603. ctx.UserController.updateUserSettings(ctx.req, ctx.res)
  604. })
  605. })
  606. it('should send an error if the email is 0 len', function (ctx) {
  607. return new Promise(resolve => {
  608. ctx.req.body.email = ''
  609. ctx.res.sendStatus = function (code) {
  610. code.should.equal(400)
  611. resolve()
  612. }
  613. ctx.UserController.updateUserSettings(ctx.req, ctx.res)
  614. })
  615. })
  616. it('should send an error if the email does not contain an @', function (ctx) {
  617. return new Promise(resolve => {
  618. ctx.req.body.email = 'bob at something dot com'
  619. ctx.res.sendStatus = function (code) {
  620. code.should.equal(400)
  621. resolve()
  622. }
  623. ctx.UserController.updateUserSettings(ctx.req, ctx.res)
  624. })
  625. })
  626. it('should call the user updater with the new email and user _id', function (ctx) {
  627. return new Promise(resolve => {
  628. ctx.req.body.email = ctx.newEmail.toUpperCase()
  629. ctx.res.sendStatus = code => {
  630. code.should.equal(200)
  631. ctx.UserUpdater.promises.changeEmailAddress.should.have.been.calledWith(
  632. ctx.user_id,
  633. ctx.newEmail,
  634. ctx.auditLog
  635. )
  636. resolve()
  637. }
  638. ctx.UserController.updateUserSettings(ctx.req, ctx.res)
  639. })
  640. })
  641. it('should update the email on the session', function (ctx) {
  642. return new Promise(resolve => {
  643. ctx.req.body.email = ctx.newEmail.toUpperCase()
  644. let callcount = 0
  645. ctx.User.findById = id => ({
  646. exec: async () => {
  647. if (++callcount === 2) {
  648. ctx.user.email = ctx.newEmail
  649. }
  650. return ctx.user
  651. },
  652. })
  653. ctx.res.sendStatus = code => {
  654. code.should.equal(200)
  655. ctx.SessionManager.setInSessionUser
  656. .calledWith(ctx.req.session, {
  657. email: ctx.newEmail,
  658. first_name: undefined,
  659. last_name: undefined,
  660. })
  661. .should.equal(true)
  662. resolve()
  663. }
  664. ctx.UserController.updateUserSettings(ctx.req, ctx.res)
  665. })
  666. })
  667. it('should call populateTeamInvites', function (ctx) {
  668. return new Promise(resolve => {
  669. ctx.req.body.email = ctx.newEmail.toUpperCase()
  670. ctx.res.sendStatus = code => {
  671. code.should.equal(200)
  672. ctx.UserHandler.promises.populateTeamInvites.should.have.been.calledWith(
  673. ctx.user
  674. )
  675. resolve()
  676. }
  677. ctx.UserController.updateUserSettings(ctx.req, ctx.res)
  678. })
  679. })
  680. describe('when changeEmailAddress yields an error', function () {
  681. it('should pass on an error and not send a success status', function (ctx) {
  682. return new Promise(resolve => {
  683. ctx.req.body.email = ctx.newEmail.toUpperCase()
  684. ctx.UserUpdater.promises.changeEmailAddress.rejects(new OError())
  685. ctx.HttpErrorHandler.legacyInternal = sinon.spy(
  686. (req, res, message, error) => {
  687. expect(req).to.exist
  688. expect(req).to.exist
  689. message.should.equal('problem_changing_email_address')
  690. expect(error).to.be.instanceof(OError)
  691. resolve()
  692. }
  693. )
  694. ctx.UserController.updateUserSettings(ctx.req, ctx.res, ctx.next)
  695. })
  696. })
  697. it('should call the HTTP conflict error handler when the email already exists', function (ctx) {
  698. return new Promise(resolve => {
  699. ctx.HttpErrorHandler.conflict = sinon.spy((req, res, message) => {
  700. expect(req).to.exist
  701. expect(req).to.exist
  702. message.should.equal('email_already_registered')
  703. resolve()
  704. })
  705. ctx.req.body.email = ctx.newEmail.toUpperCase()
  706. ctx.UserUpdater.promises.changeEmailAddress.rejects(
  707. new Errors.EmailExistsError()
  708. )
  709. ctx.UserController.updateUserSettings(ctx.req, ctx.res)
  710. })
  711. })
  712. })
  713. describe('when using an external auth source', function () {
  714. beforeEach(function (ctx) {
  715. ctx.newEmail = 'someone23@example.com'
  716. ctx.req.externalAuthenticationSystemUsed = sinon.stub().returns(true)
  717. })
  718. it('should not set a new email', function (ctx) {
  719. return new Promise(resolve => {
  720. ctx.req.body.email = ctx.newEmail
  721. ctx.res.sendStatus = code => {
  722. code.should.equal(200)
  723. ctx.UserUpdater.promises.changeEmailAddress
  724. .calledWith(ctx.user_id, ctx.newEmail)
  725. .should.equal(false)
  726. resolve()
  727. }
  728. ctx.UserController.updateUserSettings(ctx.req, ctx.res)
  729. })
  730. })
  731. })
  732. })
  733. describe('logout', function () {
  734. beforeEach(function (ctx) {
  735. ctx.RequestContentTypeDetection.acceptsJson.returns(false)
  736. })
  737. it('should destroy the session', function (ctx) {
  738. return new Promise(resolve => {
  739. ctx.req.session.destroy = sinon.stub().callsArgWith(0)
  740. ctx.res.redirect = url => {
  741. url.should.equal('/login')
  742. ctx.req.session.destroy.called.should.equal(true)
  743. resolve()
  744. }
  745. ctx.UserController.logout(ctx.req, ctx.res)
  746. })
  747. })
  748. it('should untrack session', function (ctx) {
  749. return new Promise(resolve => {
  750. ctx.req.session.destroy = sinon.stub().callsArgWith(0)
  751. ctx.res.redirect = url => {
  752. url.should.equal('/login')
  753. ctx.UserSessionsManager.promises.untrackSession.should.have.been
  754. .calledOnce
  755. ctx.UserSessionsManager.promises.untrackSession.should.have.been.calledWith(
  756. sinon.match(ctx.req.user),
  757. ctx.req.sessionID
  758. )
  759. resolve()
  760. }
  761. ctx.UserController.logout(ctx.req, ctx.res)
  762. })
  763. })
  764. it('should redirect after logout', function (ctx) {
  765. return new Promise(resolve => {
  766. ctx.req.body.redirect = '/sso-login'
  767. ctx.req.session.destroy = sinon.stub().callsArgWith(0)
  768. ctx.res.redirect = url => {
  769. url.should.equal(ctx.req.body.redirect)
  770. resolve()
  771. }
  772. ctx.UserController.logout(ctx.req, ctx.res)
  773. })
  774. })
  775. it('should redirect after logout, but not to evil.com', function (ctx) {
  776. return new Promise(resolve => {
  777. ctx.req.body.redirect = 'https://evil.com'
  778. ctx.req.session.destroy = sinon.stub().callsArgWith(0)
  779. ctx.res.redirect = url => {
  780. url.should.equal('/login')
  781. resolve()
  782. }
  783. ctx.UserController.logout(ctx.req, ctx.res)
  784. })
  785. })
  786. it('should redirect to login after logout when no redirect set', function (ctx) {
  787. return new Promise(resolve => {
  788. ctx.req.session.destroy = sinon.stub().callsArgWith(0)
  789. ctx.res.redirect = url => {
  790. url.should.equal('/login')
  791. resolve()
  792. }
  793. ctx.UserController.logout(ctx.req, ctx.res)
  794. })
  795. })
  796. it('should send json with redir property for json request', function (ctx) {
  797. return new Promise(resolve => {
  798. ctx.RequestContentTypeDetection.acceptsJson.returns(true)
  799. ctx.req.session.destroy = sinon.stub().callsArgWith(0)
  800. ctx.res.status = code => {
  801. code.should.equal(200)
  802. return ctx.res
  803. }
  804. ctx.res.json = data => {
  805. data.redir.should.equal('/login')
  806. resolve()
  807. }
  808. ctx.UserController.logout(ctx.req, ctx.res)
  809. })
  810. })
  811. })
  812. describe('clearSessions', function () {
  813. describe('success', function () {
  814. it('should call removeSessionsFromRedis', function (ctx) {
  815. return new Promise(resolve => {
  816. ctx.res.sendStatus.callsFake(() => {
  817. ctx.UserSessionsManager.promises.removeSessionsFromRedis.should.have
  818. .been.calledOnce
  819. resolve()
  820. })
  821. ctx.UserController.clearSessions(ctx.req, ctx.res)
  822. })
  823. })
  824. it('send a 201 response', function (ctx) {
  825. return new Promise(resolve => {
  826. ctx.res.sendStatus.callsFake(status => {
  827. status.should.equal(201)
  828. resolve()
  829. })
  830. ctx.UserController.clearSessions(ctx.req, ctx.res)
  831. })
  832. })
  833. it('sends a security alert email', function (ctx) {
  834. return new Promise(resolve => {
  835. ctx.res.sendStatus.callsFake(status => {
  836. ctx.EmailHandler.promises.sendEmail.callCount.should.equal(1)
  837. const expectedArg = {
  838. to: ctx.user.email,
  839. actionDescribed: `active sessions were cleared on your account ${ctx.user.email}`,
  840. action: 'active sessions cleared',
  841. }
  842. const emailCall = ctx.EmailHandler.promises.sendEmail.lastCall
  843. expect(emailCall.args[0]).to.equal('securityAlert')
  844. expect(emailCall.args[1]).to.deep.equal(expectedArg)
  845. resolve()
  846. })
  847. ctx.UserController.clearSessions(ctx.req, ctx.res)
  848. })
  849. })
  850. })
  851. describe('errors', function () {
  852. describe('when getAllUserSessions produces an error', function () {
  853. it('should return an error', function (ctx) {
  854. return new Promise(resolve => {
  855. ctx.UserSessionsManager.promises.getAllUserSessions.rejects(
  856. new Error('woops')
  857. )
  858. ctx.UserController.clearSessions(ctx.req, ctx.res, error => {
  859. expect(error).to.be.instanceof(Error)
  860. resolve()
  861. })
  862. })
  863. })
  864. })
  865. describe('when audit log addEntry produces an error', function () {
  866. it('should call next with an error', function (ctx) {
  867. return new Promise(resolve => {
  868. ctx.UserAuditLogHandler.promises.addEntry.rejects(
  869. new Error('woops')
  870. )
  871. ctx.UserController.clearSessions(ctx.req, ctx.res, error => {
  872. expect(error).to.be.instanceof(Error)
  873. resolve()
  874. })
  875. })
  876. })
  877. })
  878. describe('when removeSessionsFromRedis produces an error', function () {
  879. it('should call next with an error', function (ctx) {
  880. return new Promise(resolve => {
  881. ctx.UserSessionsManager.promises.removeSessionsFromRedis.rejects(
  882. new Error('woops')
  883. )
  884. ctx.UserController.clearSessions(ctx.req, ctx.res, error => {
  885. expect(error).to.be.instanceof(Error)
  886. resolve()
  887. })
  888. })
  889. })
  890. })
  891. describe('when EmailHandler produces an error', function () {
  892. const anError = new Error('oops')
  893. it('send a 201 response but log error', function (ctx) {
  894. return new Promise(resolve => {
  895. ctx.EmailHandler.promises.sendEmail.rejects(anError)
  896. ctx.res.sendStatus.callsFake(status => {
  897. status.should.equal(201)
  898. expect(ctx.logger.error).toHaveBeenCalledTimes(1)
  899. const loggerCall = ctx.logger.error.mock.calls[0]
  900. expect(loggerCall[0]).to.deep.equal({
  901. error: anError,
  902. userId: ctx.user_id,
  903. })
  904. expect(loggerCall[1]).to.contain(
  905. 'could not send security alert email when sessions cleared'
  906. )
  907. resolve()
  908. })
  909. ctx.UserController.clearSessions(ctx.req, ctx.res)
  910. })
  911. })
  912. })
  913. })
  914. })
  915. describe('changePassword', function () {
  916. describe('success', function () {
  917. beforeEach(function (ctx) {
  918. ctx.AuthenticationManager.promises.authenticate.resolves({
  919. user: ctx.user,
  920. })
  921. ctx.AuthenticationManager.promises.setUserPassword.resolves()
  922. ctx.req.body = {
  923. newPassword1: 'newpass',
  924. newPassword2: 'newpass',
  925. }
  926. })
  927. it('should set the new password if they do match', function (ctx) {
  928. return new Promise(resolve => {
  929. ctx.res.json.callsFake(() => {
  930. ctx.AuthenticationManager.promises.setUserPassword.should.have.been.calledWith(
  931. ctx.user,
  932. 'newpass'
  933. )
  934. resolve()
  935. })
  936. ctx.UserController.changePassword(ctx.req, ctx.res)
  937. })
  938. })
  939. it('should log the update', function (ctx) {
  940. return new Promise(resolve => {
  941. ctx.res.json.callsFake(() => {
  942. ctx.UserAuditLogHandler.promises.addEntry.should.have.been.calledWith(
  943. ctx.user._id,
  944. 'update-password',
  945. ctx.user._id,
  946. ctx.req.ip
  947. )
  948. ctx.AuthenticationManager.promises.setUserPassword.callCount.should.equal(
  949. 1
  950. )
  951. resolve()
  952. })
  953. ctx.UserController.changePassword(ctx.req, ctx.res)
  954. })
  955. })
  956. it('should send security alert email', function (ctx) {
  957. return new Promise(resolve => {
  958. ctx.res.json.callsFake(() => {
  959. const expectedArg = {
  960. to: ctx.user.email,
  961. actionDescribed: `your password has been changed on your account ${ctx.user.email}`,
  962. action: 'password changed',
  963. }
  964. const emailCall = ctx.EmailHandler.promises.sendEmail.lastCall
  965. expect(emailCall.args[0]).to.equal('securityAlert')
  966. expect(emailCall.args[1]).to.deep.equal(expectedArg)
  967. resolve()
  968. })
  969. ctx.UserController.changePassword(ctx.req, ctx.res)
  970. })
  971. })
  972. it('should expire password reset tokens', function (ctx) {
  973. return new Promise(resolve => {
  974. ctx.res.json.callsFake(() => {
  975. ctx.OneTimeTokenHandler.promises.expireAllTokensForUser.should.have.been.calledWith(
  976. ctx.user._id,
  977. 'password'
  978. )
  979. resolve()
  980. })
  981. ctx.UserController.changePassword(ctx.req, ctx.res)
  982. })
  983. })
  984. })
  985. describe('errors', function () {
  986. it('should check the old password is the current one at the moment', function (ctx) {
  987. return new Promise(resolve => {
  988. ctx.AuthenticationManager.promises.authenticate.resolves({})
  989. ctx.req.body = { currentPassword: 'oldpasshere' }
  990. ctx.HttpErrorHandler.badRequest.callsFake(() => {
  991. expect(ctx.HttpErrorHandler.badRequest).to.have.been.calledWith(
  992. ctx.req,
  993. ctx.res,
  994. 'password_change_old_password_wrong'
  995. )
  996. ctx.AuthenticationManager.promises.authenticate.should.have.been.calledWith(
  997. { _id: ctx.user._id },
  998. 'oldpasshere'
  999. )
  1000. ctx.AuthenticationManager.promises.setUserPassword.callCount.should.equal(
  1001. 0
  1002. )
  1003. resolve()
  1004. })
  1005. ctx.UserController.changePassword(ctx.req, ctx.res)
  1006. })
  1007. })
  1008. it('it should not set the new password if they do not match', function (ctx) {
  1009. return new Promise(resolve => {
  1010. ctx.AuthenticationManager.promises.authenticate.resolves({
  1011. user: ctx.user,
  1012. })
  1013. ctx.req.body = {
  1014. newPassword1: '1',
  1015. newPassword2: '2',
  1016. }
  1017. ctx.HttpErrorHandler.badRequest.callsFake(() => {
  1018. expect(ctx.HttpErrorHandler.badRequest).to.have.been.calledWith(
  1019. ctx.req,
  1020. ctx.res,
  1021. 'password_change_passwords_do_not_match'
  1022. )
  1023. ctx.AuthenticationManager.promises.setUserPassword.callCount.should.equal(
  1024. 0
  1025. )
  1026. resolve()
  1027. })
  1028. ctx.UserController.changePassword(ctx.req, ctx.res)
  1029. })
  1030. })
  1031. it('it should not set the new password if it is invalid', function (ctx) {
  1032. return new Promise(resolve => {
  1033. // this.AuthenticationManager.validatePassword = sinon
  1034. // .stub()
  1035. // .returns({ message: 'validation-error' })
  1036. const err = new Error('bad')
  1037. err.name = 'InvalidPasswordError'
  1038. const message = {
  1039. type: 'error',
  1040. key: 'some-message-key',
  1041. }
  1042. ctx.AuthenticationManager.getMessageForInvalidPasswordError.returns(
  1043. message
  1044. )
  1045. ctx.AuthenticationManager.promises.setUserPassword.rejects(err)
  1046. ctx.AuthenticationManager.promises.authenticate.resolves({
  1047. user: ctx.user,
  1048. })
  1049. ctx.req.body = {
  1050. newPassword1: 'newpass',
  1051. newPassword2: 'newpass',
  1052. }
  1053. ctx.res.json.callsFake(result => {
  1054. expect(result.message).to.deep.equal(message)
  1055. ctx.AuthenticationManager.promises.setUserPassword.callCount.should.equal(
  1056. 1
  1057. )
  1058. resolve()
  1059. })
  1060. ctx.UserController.changePassword(ctx.req, ctx.res)
  1061. })
  1062. })
  1063. describe('UserAuditLogHandler error', function () {
  1064. it('should return error and not update password', function (ctx) {
  1065. return new Promise(resolve => {
  1066. ctx.UserAuditLogHandler.promises.addEntry.rejects(new Error('oops'))
  1067. ctx.AuthenticationManager.promises.authenticate.resolves({
  1068. user: ctx.user,
  1069. })
  1070. ctx.AuthenticationManager.promises.setUserPassword.resolves()
  1071. ctx.req.body = {
  1072. newPassword1: 'newpass',
  1073. newPassword2: 'newpass',
  1074. }
  1075. ctx.UserController.changePassword(ctx.req, ctx.res, error => {
  1076. expect(error).to.be.instanceof(Error)
  1077. ctx.AuthenticationManager.promises.setUserPassword.callCount.should.equal(
  1078. 1
  1079. )
  1080. resolve()
  1081. })
  1082. })
  1083. })
  1084. })
  1085. describe('EmailHandler error', function () {
  1086. const anError = new Error('oops')
  1087. beforeEach(function (ctx) {
  1088. ctx.AuthenticationManager.promises.authenticate.resolves({
  1089. user: ctx.user,
  1090. })
  1091. ctx.AuthenticationManager.promises.setUserPassword.resolves()
  1092. ctx.req.body = {
  1093. newPassword1: 'newpass',
  1094. newPassword2: 'newpass',
  1095. }
  1096. ctx.EmailHandler.promises.sendEmail.rejects(anError)
  1097. })
  1098. it('should not return error but should log it', function (ctx) {
  1099. return new Promise(resolve => {
  1100. ctx.res.json.callsFake(result => {
  1101. expect(result.message.type).to.equal('success')
  1102. expect(ctx.logger.error).toHaveBeenCalledTimes(1)
  1103. expect(ctx.logger.error).toHaveBeenCalledWith(
  1104. {
  1105. error: anError,
  1106. userId: ctx.user_id,
  1107. },
  1108. 'could not send security alert email when password changed'
  1109. )
  1110. resolve()
  1111. })
  1112. ctx.UserController.changePassword(ctx.req, ctx.res)
  1113. })
  1114. })
  1115. })
  1116. })
  1117. })
  1118. describe('ensureAffiliationMiddleware', function () {
  1119. describe('without affiliations feature', function () {
  1120. beforeEach(async function (ctx) {
  1121. await ctx.UserController.ensureAffiliationMiddleware(
  1122. ctx.req,
  1123. ctx.res,
  1124. ctx.next
  1125. )
  1126. })
  1127. it('should not run affiliation check', function (ctx) {
  1128. expect(ctx.UserGetter.promises.getUser).to.not.have.been.called
  1129. expect(ctx.UserUpdater.promises.confirmEmail).to.not.have.been.called
  1130. expect(ctx.UserUpdater.promises.addAffiliationForNewUser).to.not.have
  1131. .been.called
  1132. })
  1133. it('should not return an error', function (ctx) {
  1134. expect(ctx.next).to.be.calledWith()
  1135. })
  1136. })
  1137. describe('without ensureAffiliation query parameter', function () {
  1138. beforeEach(async function (ctx) {
  1139. ctx.Features.hasFeature.withArgs('affiliations').returns(true)
  1140. await ctx.UserController.ensureAffiliationMiddleware(
  1141. ctx.req,
  1142. ctx.res,
  1143. ctx.next
  1144. )
  1145. })
  1146. it('should not run middleware', function (ctx) {
  1147. expect(ctx.UserGetter.promises.getUser).to.not.have.been.called
  1148. expect(ctx.UserUpdater.promises.confirmEmail).to.not.have.been.called
  1149. expect(ctx.UserUpdater.promises.addAffiliationForNewUser).to.not.have
  1150. .been.called
  1151. })
  1152. it('should not return an error', function (ctx) {
  1153. expect(ctx.next).to.be.calledWith()
  1154. })
  1155. })
  1156. describe('no flagged email', function () {
  1157. beforeEach(async function (ctx) {
  1158. const email = 'unit-test@overleaf.com'
  1159. ctx.user.email = email
  1160. ctx.user.emails = [
  1161. {
  1162. email,
  1163. },
  1164. ]
  1165. ctx.Features.hasFeature.withArgs('affiliations').returns(true)
  1166. ctx.req.query.ensureAffiliation = true
  1167. await ctx.UserController.ensureAffiliationMiddleware(
  1168. ctx.req,
  1169. ctx.res,
  1170. ctx.next
  1171. )
  1172. })
  1173. it('should get the user', function (ctx) {
  1174. expect(ctx.UserGetter.promises.getUser).to.have.been.calledWith(
  1175. ctx.user._id
  1176. )
  1177. })
  1178. it('should not try to add affiliation or update user', function (ctx) {
  1179. expect(ctx.UserUpdater.promises.addAffiliationForNewUser).to.not.have
  1180. .been.called
  1181. })
  1182. it('should not return an error', function (ctx) {
  1183. expect(ctx.next).to.be.calledWith()
  1184. })
  1185. })
  1186. describe('flagged non-SSO email', function () {
  1187. let emailFlagged
  1188. beforeEach(async function (ctx) {
  1189. emailFlagged = 'flagged@overleaf.com'
  1190. ctx.user.email = emailFlagged
  1191. ctx.user.emails = [
  1192. {
  1193. email: emailFlagged,
  1194. affiliationUnchecked: true,
  1195. },
  1196. ]
  1197. ctx.Features.hasFeature.withArgs('affiliations').returns(true)
  1198. ctx.req.query.ensureAffiliation = true
  1199. ctx.req.assertPermission = sinon.stub()
  1200. await ctx.UserController.ensureAffiliationMiddleware(
  1201. ctx.req,
  1202. ctx.res,
  1203. ctx.next
  1204. )
  1205. })
  1206. it('should check the user has permission', function (ctx) {
  1207. expect(ctx.req.assertPermission).to.have.been.calledWith(
  1208. 'add-affiliation'
  1209. )
  1210. })
  1211. it('should unflag the emails but not confirm', function (ctx) {
  1212. expect(
  1213. ctx.UserUpdater.promises.addAffiliationForNewUser
  1214. ).to.have.been.calledWith(ctx.user._id, emailFlagged)
  1215. expect(
  1216. ctx.UserUpdater.promises.confirmEmail
  1217. ).to.not.have.been.calledWith(ctx.user._id, emailFlagged)
  1218. })
  1219. it('should not return an error', function (ctx) {
  1220. expect(ctx.next).to.be.calledWith()
  1221. })
  1222. })
  1223. describe('flagged SSO email', function () {
  1224. let emailFlagged
  1225. beforeEach(async function (ctx) {
  1226. emailFlagged = 'flagged@overleaf.com'
  1227. ctx.user.email = emailFlagged
  1228. ctx.user.emails = [
  1229. {
  1230. email: emailFlagged,
  1231. affiliationUnchecked: true,
  1232. samlProviderId: '123',
  1233. },
  1234. ]
  1235. ctx.Features.hasFeature.withArgs('affiliations').returns(true)
  1236. ctx.req.query.ensureAffiliation = true
  1237. ctx.req.assertPermission = sinon.stub()
  1238. await ctx.UserController.ensureAffiliationMiddleware(
  1239. ctx.req,
  1240. ctx.res,
  1241. ctx.next
  1242. )
  1243. })
  1244. it('should check the user has permission', function (ctx) {
  1245. expect(ctx.req.assertPermission).to.have.been.calledWith(
  1246. 'add-affiliation'
  1247. )
  1248. })
  1249. it('should add affiliation to v1, unflag and confirm on v2', function (ctx) {
  1250. expect(ctx.UserUpdater.promises.addAffiliationForNewUser).to.have.not
  1251. .been.called
  1252. expect(ctx.UserUpdater.promises.confirmEmail).to.have.been.calledWith(
  1253. ctx.user._id,
  1254. emailFlagged
  1255. )
  1256. })
  1257. it('should not return an error', function (ctx) {
  1258. expect(ctx.next).to.be.calledWith()
  1259. })
  1260. })
  1261. describe('when v1 returns an error', function () {
  1262. let emailFlagged
  1263. beforeEach(async function (ctx) {
  1264. ctx.UserUpdater.promises.addAffiliationForNewUser.rejects()
  1265. emailFlagged = 'flagged@overleaf.com'
  1266. ctx.user.email = emailFlagged
  1267. ctx.user.emails = [
  1268. {
  1269. email: emailFlagged,
  1270. affiliationUnchecked: true,
  1271. },
  1272. ]
  1273. ctx.Features.hasFeature.withArgs('affiliations').returns(true)
  1274. ctx.req.query.ensureAffiliation = true
  1275. ctx.req.assertPermission = sinon.stub()
  1276. await ctx.UserController.ensureAffiliationMiddleware(
  1277. ctx.req,
  1278. ctx.res,
  1279. ctx.next
  1280. )
  1281. })
  1282. it('should check the user has permission', function (ctx) {
  1283. expect(ctx.req.assertPermission).to.have.been.calledWith(
  1284. 'add-affiliation'
  1285. )
  1286. })
  1287. it('should return the error', function (ctx) {
  1288. expect(ctx.next).to.be.calledWith(sinon.match.instanceOf(Error))
  1289. })
  1290. })
  1291. describe('when user is not found', function () {
  1292. beforeEach(async function (ctx) {
  1293. ctx.UserGetter.promises.getUser.rejects(new Error('not found'))
  1294. ctx.Features.hasFeature.withArgs('affiliations').returns(true)
  1295. ctx.req.query.ensureAffiliation = true
  1296. await ctx.UserController.ensureAffiliationMiddleware(
  1297. ctx.req,
  1298. ctx.res,
  1299. ctx.next
  1300. )
  1301. })
  1302. it('should return the error', function (ctx) {
  1303. expect(ctx.next).to.be.calledWith(sinon.match.instanceOf(Error))
  1304. })
  1305. })
  1306. })
  1307. })