UserPagesController.test.mjs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625
  1. import { expect, vi } from 'vitest'
  2. import assert from 'assert'
  3. import sinon from 'sinon'
  4. import MockResponse from '../helpers/MockResponse.js'
  5. import MockRequest from '../helpers/MockRequest.js'
  6. const modulePath = '../../../../app/src/Features/User/UserPagesController'
  7. describe('UserPagesController', function () {
  8. beforeEach(async function (ctx) {
  9. ctx.settings = {
  10. apis: {
  11. v1: {
  12. url: 'some.host',
  13. user: 'one',
  14. pass: 'two',
  15. },
  16. },
  17. }
  18. ctx.user = {
  19. _id: (ctx.user_id = 'kwjewkl'),
  20. features: {},
  21. email: 'joe@example.com',
  22. ip_address: '1.1.1.1',
  23. session_created: 'timestamp',
  24. thirdPartyIdentifiers: [
  25. {
  26. providerId: 'google',
  27. externalUserId: 'testId',
  28. },
  29. ],
  30. refProviders: {
  31. mendeley: { encrypted: 'aaaa' },
  32. zotero: { encrypted: 'bbbb' },
  33. papers: { encrypted: 'cccc' },
  34. },
  35. }
  36. ctx.adminEmail = 'group-admin-email@overleaf.com'
  37. ctx.subscriptionViewModel = {
  38. memberGroupSubscriptions: [],
  39. }
  40. ctx.UserGetter = {
  41. getUser: sinon.stub(),
  42. promises: { getUser: sinon.stub() },
  43. }
  44. ctx.UserSessionsManager = { getAllUserSessions: sinon.stub() }
  45. ctx.dropboxStatus = {}
  46. ctx.ErrorController = { notFound: sinon.stub() }
  47. ctx.SessionManager = {
  48. getLoggedInUserId: sinon.stub().returns(ctx.user._id),
  49. getSessionUser: sinon.stub().returns(ctx.user),
  50. }
  51. ctx.NewsletterManager = {
  52. subscribed: sinon.stub().yields(),
  53. }
  54. ctx.AuthenticationController = {
  55. getRedirectFromSession: sinon.stub(),
  56. setRedirectInSession: sinon.stub(),
  57. }
  58. ctx.Features = {
  59. hasFeature: sinon.stub().returns(false),
  60. }
  61. ctx.PersonalAccessTokenManager = {
  62. listTokens: sinon.stub().returns([]),
  63. }
  64. ctx.SubscriptionLocator = {
  65. promises: {
  66. getAdminEmail: sinon.stub().returns(ctx.adminEmail),
  67. getMemberSubscriptions: sinon.stub().resolves(),
  68. },
  69. }
  70. ctx.SplitTestHandler = {
  71. promises: {
  72. getAssignment: sinon.stub().returns('default'),
  73. },
  74. }
  75. ctx.Modules = {
  76. promises: {
  77. hooks: {
  78. fire: sinon.stub().resolves(),
  79. },
  80. },
  81. }
  82. vi.doMock('@overleaf/settings', () => ({
  83. default: ctx.settings,
  84. }))
  85. vi.doMock('../../../../app/src/Features/User/UserGetter', () => ({
  86. default: ctx.UserGetter,
  87. }))
  88. vi.doMock('../../../../app/src/Features/User/UserSessionsManager', () => ({
  89. default: ctx.UserSessionsManager,
  90. }))
  91. vi.doMock(
  92. '../../../../app/src/Features/Newsletter/NewsletterManager',
  93. () => ({
  94. default: ctx.NewsletterManager,
  95. })
  96. )
  97. vi.doMock('../../../../app/src/Features/Errors/ErrorController', () => ({
  98. default: ctx.ErrorController,
  99. }))
  100. vi.doMock(
  101. '../../../../app/src/Features/Authentication/AuthenticationController',
  102. () => ({
  103. default: ctx.AuthenticationController,
  104. })
  105. )
  106. vi.doMock(
  107. '../../../../app/src/Features/Subscription/SubscriptionLocator',
  108. () => ({
  109. default: ctx.SubscriptionLocator,
  110. })
  111. )
  112. vi.doMock('../../../../app/src/infrastructure/Features', () => ({
  113. default: ctx.Features,
  114. }))
  115. vi.doMock(
  116. '../../../../modules/oauth2-server/app/src/OAuthPersonalAccessTokenManager',
  117. () => ({
  118. default: ctx.PersonalAccessTokenManager,
  119. })
  120. )
  121. vi.doMock(
  122. '../../../../app/src/Features/Authentication/SessionManager',
  123. () => ({
  124. default: ctx.SessionManager,
  125. })
  126. )
  127. vi.doMock(
  128. '../../../../app/src/Features/SplitTests/SplitTestHandler',
  129. () => ({
  130. default: ctx.SplitTestHandler,
  131. })
  132. )
  133. vi.doMock('../../../../app/src/infrastructure/Modules', () => ({
  134. default: ctx.Modules,
  135. }))
  136. ctx.request = sinon.stub()
  137. vi.doMock('request', () => ({
  138. default: ctx.request,
  139. }))
  140. ctx.UserPagesController = (await import(modulePath)).default
  141. ctx.req = new MockRequest()
  142. ctx.req.capabilitySet = new Set()
  143. ctx.req.session.user = ctx.user
  144. ctx.res = new MockResponse()
  145. })
  146. describe('registerPage', function () {
  147. it('should render the register page', async function (ctx) {
  148. await new Promise((resolve, reject) => {
  149. ctx.res.callback = () => {
  150. ctx.res.renderedTemplate.should.equal('user/register')
  151. resolve()
  152. }
  153. ctx.UserPagesController.registerPage(
  154. ctx.req,
  155. ctx.res,
  156. ctx.rejectOnError(reject)
  157. )
  158. })
  159. })
  160. it('should set sharedProjectData', async function (ctx) {
  161. ctx.req.session.sharedProjectData = {
  162. project_name: 'myProject',
  163. user_first_name: 'user_first_name_here',
  164. }
  165. await new Promise((resolve, reject) => {
  166. ctx.res.callback = () => {
  167. ctx.res.renderedVariables.sharedProjectData.project_name.should.equal(
  168. 'myProject'
  169. )
  170. ctx.res.renderedVariables.sharedProjectData.user_first_name.should.equal(
  171. 'user_first_name_here'
  172. )
  173. resolve()
  174. }
  175. ctx.UserPagesController.registerPage(
  176. ctx.req,
  177. ctx.res,
  178. ctx.rejectOnError(reject)
  179. )
  180. })
  181. })
  182. it('should set newTemplateData', async function (ctx) {
  183. await new Promise((resolve, reject) => {
  184. ctx.req.session.templateData = { templateName: 'templateName' }
  185. ctx.res.callback = () => {
  186. ctx.res.renderedVariables.newTemplateData.templateName.should.equal(
  187. 'templateName'
  188. )
  189. resolve()
  190. }
  191. ctx.UserPagesController.registerPage(
  192. ctx.req,
  193. ctx.res,
  194. ctx.rejectOnError(reject)
  195. )
  196. })
  197. })
  198. it('should not set the newTemplateData if there is nothing in the session', async function (ctx) {
  199. await new Promise((resolve, reject) => {
  200. ctx.res.callback = () => {
  201. assert.equal(
  202. ctx.res.renderedVariables.newTemplateData.templateName,
  203. undefined
  204. )
  205. resolve()
  206. }
  207. ctx.UserPagesController.registerPage(
  208. ctx.req,
  209. ctx.res,
  210. ctx.rejectOnError(reject)
  211. )
  212. })
  213. })
  214. })
  215. describe('loginForm', function () {
  216. it('should render the login page', async function (ctx) {
  217. await new Promise((resolve, reject) => {
  218. ctx.res.callback = () => {
  219. ctx.res.renderedTemplate.should.equal('user/login')
  220. resolve()
  221. }
  222. ctx.UserPagesController.loginPage(
  223. ctx.req,
  224. ctx.res,
  225. ctx.rejectOnError(reject)
  226. )
  227. })
  228. })
  229. describe('when an explicit redirect is set via query string', function () {
  230. beforeEach(function (ctx) {
  231. ctx.AuthenticationController.getRedirectFromSession = sinon
  232. .stub()
  233. .returns(null)
  234. ctx.AuthenticationController.setRedirectInSession = sinon.stub()
  235. ctx.req.query.redir = '/somewhere/in/particular'
  236. })
  237. it('should set a redirect', async function (ctx) {
  238. await new Promise((resolve, reject) => {
  239. ctx.res.callback = page => {
  240. ctx.AuthenticationController.setRedirectInSession.callCount.should.equal(
  241. 1
  242. )
  243. expect(
  244. ctx.AuthenticationController.setRedirectInSession.lastCall.args[1]
  245. ).to.equal(ctx.req.query.redir)
  246. resolve()
  247. }
  248. ctx.UserPagesController.loginPage(
  249. ctx.req,
  250. ctx.res,
  251. ctx.rejectOnError(reject)
  252. )
  253. })
  254. })
  255. })
  256. })
  257. describe('sessionsPage', function () {
  258. beforeEach(function (ctx) {
  259. ctx.UserSessionsManager.getAllUserSessions.callsArgWith(2, null, [])
  260. })
  261. it('should render user/sessions', async function (ctx) {
  262. await new Promise((resolve, reject) => {
  263. ctx.res.callback = () => {
  264. ctx.res.renderedTemplate.should.equal('user/sessions')
  265. resolve()
  266. }
  267. ctx.UserPagesController.sessionsPage(
  268. ctx.req,
  269. ctx.res,
  270. ctx.rejectOnError(reject)
  271. )
  272. })
  273. })
  274. it('should include current session data in the view', async function (ctx) {
  275. await new Promise((resolve, reject) => {
  276. ctx.res.callback = () => {
  277. expect(ctx.res.renderedVariables.currentSession).to.deep.equal({
  278. ip_address: '1.1.1.1',
  279. session_created: 'timestamp',
  280. })
  281. resolve()
  282. }
  283. ctx.UserPagesController.sessionsPage(
  284. ctx.req,
  285. ctx.res,
  286. ctx.rejectOnError(reject)
  287. )
  288. })
  289. })
  290. it('should have called getAllUserSessions', async function (ctx) {
  291. await new Promise((resolve, reject) => {
  292. ctx.res.callback = page => {
  293. ctx.UserSessionsManager.getAllUserSessions.callCount.should.equal(1)
  294. resolve()
  295. }
  296. ctx.UserPagesController.sessionsPage(
  297. ctx.req,
  298. ctx.res,
  299. ctx.rejectOnError(reject)
  300. )
  301. })
  302. })
  303. describe('when getAllUserSessions produces an error', function () {
  304. beforeEach(function (ctx) {
  305. ctx.UserSessionsManager.getAllUserSessions.callsArgWith(
  306. 2,
  307. new Error('woops')
  308. )
  309. })
  310. it('should call next with an error', async function (ctx) {
  311. await new Promise(resolve => {
  312. ctx.next = err => {
  313. assert(err !== null)
  314. assert(err instanceof Error)
  315. resolve()
  316. }
  317. ctx.UserPagesController.sessionsPage(ctx.req, ctx.res, ctx.next)
  318. })
  319. })
  320. })
  321. })
  322. describe('emailPreferencesPage', function () {
  323. beforeEach(function (ctx) {
  324. ctx.UserGetter.getUser = sinon.stub().yields(null, ctx.user)
  325. })
  326. it('render page with subscribed status', async function (ctx) {
  327. ctx.NewsletterManager.subscribed.yields(null, true)
  328. await new Promise((resolve, reject) => {
  329. ctx.res.callback = () => {
  330. ctx.res.renderedTemplate.should.equal('user/email-preferences')
  331. ctx.res.renderedVariables.title.should.equal('newsletter_info_title')
  332. ctx.res.renderedVariables.subscribed.should.equal(true)
  333. resolve()
  334. }
  335. ctx.UserPagesController.emailPreferencesPage(
  336. ctx.req,
  337. ctx.res,
  338. ctx.rejectOnError(reject)
  339. )
  340. })
  341. })
  342. it('render page with unsubscribed status', async function (ctx) {
  343. ctx.NewsletterManager.subscribed.yields(null, false)
  344. await new Promise((resolve, reject) => {
  345. ctx.res.callback = () => {
  346. ctx.res.renderedTemplate.should.equal('user/email-preferences')
  347. ctx.res.renderedVariables.title.should.equal('newsletter_info_title')
  348. ctx.res.renderedVariables.subscribed.should.equal(false)
  349. resolve()
  350. }
  351. ctx.UserPagesController.emailPreferencesPage(
  352. ctx.req,
  353. ctx.res,
  354. ctx.rejectOnError(reject)
  355. )
  356. })
  357. })
  358. })
  359. describe('settingsPage', function () {
  360. beforeEach(function (ctx) {
  361. ctx.request.get = sinon
  362. .stub()
  363. .callsArgWith(1, null, { statusCode: 200 }, { has_password: true })
  364. ctx.UserGetter.promises.getUser = sinon.stub().resolves(ctx.user)
  365. })
  366. it('should render user/settings', async function (ctx) {
  367. await new Promise((resolve, reject) => {
  368. ctx.res.callback = () => {
  369. ctx.res.renderedTemplate.should.equal('user/settings')
  370. resolve()
  371. }
  372. ctx.UserPagesController.settingsPage(
  373. ctx.req,
  374. ctx.res,
  375. ctx.rejectOnError(reject)
  376. )
  377. })
  378. })
  379. it('should send user', async function (ctx) {
  380. await new Promise((resolve, reject) => {
  381. ctx.res.callback = () => {
  382. ctx.res.renderedVariables.user.id.should.equal(ctx.user._id)
  383. ctx.res.renderedVariables.user.email.should.equal(ctx.user.email)
  384. resolve()
  385. }
  386. ctx.UserPagesController.settingsPage(
  387. ctx.req,
  388. ctx.res,
  389. ctx.rejectOnError(reject)
  390. )
  391. })
  392. })
  393. it("should set 'shouldAllowEditingDetails' to true", async function (ctx) {
  394. await new Promise((resolve, reject) => {
  395. ctx.res.callback = () => {
  396. ctx.res.renderedVariables.shouldAllowEditingDetails.should.equal(true)
  397. resolve()
  398. }
  399. ctx.UserPagesController.settingsPage(
  400. ctx.req,
  401. ctx.res,
  402. ctx.rejectOnError(reject)
  403. )
  404. })
  405. })
  406. it('should restructure thirdPartyIdentifiers data for template use', async function (ctx) {
  407. const expectedResult = {
  408. google: 'testId',
  409. }
  410. await new Promise((resolve, reject) => {
  411. ctx.res.callback = () => {
  412. expect(ctx.res.renderedVariables.thirdPartyIds).to.include(
  413. expectedResult
  414. )
  415. resolve()
  416. }
  417. ctx.UserPagesController.settingsPage(
  418. ctx.req,
  419. ctx.res,
  420. ctx.rejectOnError(reject)
  421. )
  422. })
  423. })
  424. it("should set and clear 'projectSyncSuccessMessage'", async function (ctx) {
  425. ctx.req.session.projectSyncSuccessMessage = 'Some Sync Success'
  426. await new Promise((resolve, reject) => {
  427. ctx.res.callback = () => {
  428. ctx.res.renderedVariables.projectSyncSuccessMessage.should.equal(
  429. 'Some Sync Success'
  430. )
  431. expect(ctx.req.session.projectSyncSuccessMessage).to.not.exist
  432. resolve()
  433. }
  434. ctx.UserPagesController.settingsPage(
  435. ctx.req,
  436. ctx.res,
  437. ctx.rejectOnError(reject)
  438. )
  439. })
  440. })
  441. it('should cast refProviders to booleans', async function (ctx) {
  442. await new Promise((resolve, reject) => {
  443. ctx.res.callback = () => {
  444. expect(ctx.res.renderedVariables.user.refProviders).to.deep.equal({
  445. mendeley: true,
  446. papers: true,
  447. zotero: true,
  448. })
  449. resolve()
  450. }
  451. ctx.UserPagesController.settingsPage(
  452. ctx.req,
  453. ctx.res,
  454. ctx.rejectOnError(reject)
  455. )
  456. })
  457. })
  458. it('should send the correct managed user admin email', async function (ctx) {
  459. await new Promise((resolve, reject) => {
  460. ctx.res.callback = () => {
  461. expect(
  462. ctx.res.renderedVariables.currentManagedUserAdminEmail
  463. ).to.equal(ctx.adminEmail)
  464. resolve()
  465. }
  466. ctx.UserPagesController.settingsPage(
  467. ctx.req,
  468. ctx.res,
  469. ctx.rejectOnError(reject)
  470. )
  471. })
  472. })
  473. it('should send info for groups with SSO enabled', async function (ctx) {
  474. ctx.user.enrollment = {
  475. sso: [
  476. {
  477. groupId: 'abc123abc123',
  478. primary: true,
  479. linkedAt: new Date(),
  480. },
  481. ],
  482. }
  483. const group1 = {
  484. _id: 'abc123abc123',
  485. teamName: 'Group SSO Rulz',
  486. admin_id: {
  487. email: 'admin.email@ssolove.com',
  488. },
  489. linked: true,
  490. }
  491. const group2 = {
  492. _id: 'def456def456',
  493. admin_id: {
  494. email: 'someone.else@noname.co.uk',
  495. },
  496. linked: false,
  497. }
  498. ctx.Modules.promises.hooks.fire
  499. .withArgs('getUserGroupsSSOEnrollmentStatus')
  500. .resolves([[group1, group2]])
  501. await new Promise((resolve, reject) => {
  502. ctx.res.callback = () => {
  503. expect(
  504. ctx.res.renderedVariables.memberOfSSOEnabledGroups
  505. ).to.deep.equal([
  506. {
  507. groupId: 'abc123abc123',
  508. groupName: 'Group SSO Rulz',
  509. adminEmail: 'admin.email@ssolove.com',
  510. linked: true,
  511. },
  512. {
  513. groupId: 'def456def456',
  514. groupName: undefined,
  515. adminEmail: 'someone.else@noname.co.uk',
  516. linked: false,
  517. },
  518. ])
  519. resolve()
  520. }
  521. ctx.UserPagesController.settingsPage(
  522. ctx.req,
  523. ctx.res,
  524. ctx.rejectOnError(reject)
  525. )
  526. })
  527. })
  528. describe('when ldap.updateUserDetailsOnLogin is true', function () {
  529. beforeEach(function (ctx) {
  530. ctx.settings.ldap = { updateUserDetailsOnLogin: true }
  531. })
  532. afterEach(function (ctx) {
  533. delete ctx.settings.ldap
  534. })
  535. it('should set "shouldAllowEditingDetails" to false', async function (ctx) {
  536. await new Promise((resolve, reject) => {
  537. ctx.res.callback = () => {
  538. ctx.res.renderedVariables.shouldAllowEditingDetails.should.equal(
  539. false
  540. )
  541. resolve()
  542. }
  543. ctx.UserPagesController.settingsPage(
  544. ctx.req,
  545. ctx.res,
  546. ctx.rejectOnError(reject)
  547. )
  548. })
  549. })
  550. })
  551. describe('when saml.updateUserDetailsOnLogin is true', function () {
  552. beforeEach(function (ctx) {
  553. ctx.settings.saml = { updateUserDetailsOnLogin: true }
  554. })
  555. afterEach(function (ctx) {
  556. delete ctx.settings.saml
  557. })
  558. it('should set "shouldAllowEditingDetails" to false', async function (ctx) {
  559. await new Promise((resolve, reject) => {
  560. ctx.res.callback = () => {
  561. ctx.res.renderedVariables.shouldAllowEditingDetails.should.equal(
  562. false
  563. )
  564. resolve()
  565. }
  566. ctx.UserPagesController.settingsPage(
  567. ctx.req,
  568. ctx.res,
  569. ctx.rejectOnError(reject)
  570. )
  571. })
  572. })
  573. })
  574. })
  575. })