UserPagesController.test.mjs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624
  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.session.user = ctx.user
  143. ctx.res = new MockResponse()
  144. })
  145. describe('registerPage', function () {
  146. it('should render the register page', async function (ctx) {
  147. await new Promise((resolve, reject) => {
  148. ctx.res.callback = () => {
  149. ctx.res.renderedTemplate.should.equal('user/register')
  150. resolve()
  151. }
  152. ctx.UserPagesController.registerPage(
  153. ctx.req,
  154. ctx.res,
  155. ctx.rejectOnError(reject)
  156. )
  157. })
  158. })
  159. it('should set sharedProjectData', async function (ctx) {
  160. ctx.req.session.sharedProjectData = {
  161. project_name: 'myProject',
  162. user_first_name: 'user_first_name_here',
  163. }
  164. await new Promise((resolve, reject) => {
  165. ctx.res.callback = () => {
  166. ctx.res.renderedVariables.sharedProjectData.project_name.should.equal(
  167. 'myProject'
  168. )
  169. ctx.res.renderedVariables.sharedProjectData.user_first_name.should.equal(
  170. 'user_first_name_here'
  171. )
  172. resolve()
  173. }
  174. ctx.UserPagesController.registerPage(
  175. ctx.req,
  176. ctx.res,
  177. ctx.rejectOnError(reject)
  178. )
  179. })
  180. })
  181. it('should set newTemplateData', async function (ctx) {
  182. await new Promise((resolve, reject) => {
  183. ctx.req.session.templateData = { templateName: 'templateName' }
  184. ctx.res.callback = () => {
  185. ctx.res.renderedVariables.newTemplateData.templateName.should.equal(
  186. 'templateName'
  187. )
  188. resolve()
  189. }
  190. ctx.UserPagesController.registerPage(
  191. ctx.req,
  192. ctx.res,
  193. ctx.rejectOnError(reject)
  194. )
  195. })
  196. })
  197. it('should not set the newTemplateData if there is nothing in the session', async function (ctx) {
  198. await new Promise((resolve, reject) => {
  199. ctx.res.callback = () => {
  200. assert.equal(
  201. ctx.res.renderedVariables.newTemplateData.templateName,
  202. undefined
  203. )
  204. resolve()
  205. }
  206. ctx.UserPagesController.registerPage(
  207. ctx.req,
  208. ctx.res,
  209. ctx.rejectOnError(reject)
  210. )
  211. })
  212. })
  213. })
  214. describe('loginForm', function () {
  215. it('should render the login page', async function (ctx) {
  216. await new Promise((resolve, reject) => {
  217. ctx.res.callback = () => {
  218. ctx.res.renderedTemplate.should.equal('user/login')
  219. resolve()
  220. }
  221. ctx.UserPagesController.loginPage(
  222. ctx.req,
  223. ctx.res,
  224. ctx.rejectOnError(reject)
  225. )
  226. })
  227. })
  228. describe('when an explicit redirect is set via query string', function () {
  229. beforeEach(function (ctx) {
  230. ctx.AuthenticationController.getRedirectFromSession = sinon
  231. .stub()
  232. .returns(null)
  233. ctx.AuthenticationController.setRedirectInSession = sinon.stub()
  234. ctx.req.query.redir = '/somewhere/in/particular'
  235. })
  236. it('should set a redirect', async function (ctx) {
  237. await new Promise((resolve, reject) => {
  238. ctx.res.callback = page => {
  239. ctx.AuthenticationController.setRedirectInSession.callCount.should.equal(
  240. 1
  241. )
  242. expect(
  243. ctx.AuthenticationController.setRedirectInSession.lastCall.args[1]
  244. ).to.equal(ctx.req.query.redir)
  245. resolve()
  246. }
  247. ctx.UserPagesController.loginPage(
  248. ctx.req,
  249. ctx.res,
  250. ctx.rejectOnError(reject)
  251. )
  252. })
  253. })
  254. })
  255. })
  256. describe('sessionsPage', function () {
  257. beforeEach(function (ctx) {
  258. ctx.UserSessionsManager.getAllUserSessions.callsArgWith(2, null, [])
  259. })
  260. it('should render user/sessions', async function (ctx) {
  261. await new Promise((resolve, reject) => {
  262. ctx.res.callback = () => {
  263. ctx.res.renderedTemplate.should.equal('user/sessions')
  264. resolve()
  265. }
  266. ctx.UserPagesController.sessionsPage(
  267. ctx.req,
  268. ctx.res,
  269. ctx.rejectOnError(reject)
  270. )
  271. })
  272. })
  273. it('should include current session data in the view', async function (ctx) {
  274. await new Promise((resolve, reject) => {
  275. ctx.res.callback = () => {
  276. expect(ctx.res.renderedVariables.currentSession).to.deep.equal({
  277. ip_address: '1.1.1.1',
  278. session_created: 'timestamp',
  279. })
  280. resolve()
  281. }
  282. ctx.UserPagesController.sessionsPage(
  283. ctx.req,
  284. ctx.res,
  285. ctx.rejectOnError(reject)
  286. )
  287. })
  288. })
  289. it('should have called getAllUserSessions', async function (ctx) {
  290. await new Promise((resolve, reject) => {
  291. ctx.res.callback = page => {
  292. ctx.UserSessionsManager.getAllUserSessions.callCount.should.equal(1)
  293. resolve()
  294. }
  295. ctx.UserPagesController.sessionsPage(
  296. ctx.req,
  297. ctx.res,
  298. ctx.rejectOnError(reject)
  299. )
  300. })
  301. })
  302. describe('when getAllUserSessions produces an error', function () {
  303. beforeEach(function (ctx) {
  304. ctx.UserSessionsManager.getAllUserSessions.callsArgWith(
  305. 2,
  306. new Error('woops')
  307. )
  308. })
  309. it('should call next with an error', async function (ctx) {
  310. await new Promise(resolve => {
  311. ctx.next = err => {
  312. assert(err !== null)
  313. assert(err instanceof Error)
  314. resolve()
  315. }
  316. ctx.UserPagesController.sessionsPage(ctx.req, ctx.res, ctx.next)
  317. })
  318. })
  319. })
  320. })
  321. describe('emailPreferencesPage', function () {
  322. beforeEach(function (ctx) {
  323. ctx.UserGetter.getUser = sinon.stub().yields(null, ctx.user)
  324. })
  325. it('render page with subscribed status', async function (ctx) {
  326. ctx.NewsletterManager.subscribed.yields(null, true)
  327. await new Promise((resolve, reject) => {
  328. ctx.res.callback = () => {
  329. ctx.res.renderedTemplate.should.equal('user/email-preferences')
  330. ctx.res.renderedVariables.title.should.equal('newsletter_info_title')
  331. ctx.res.renderedVariables.subscribed.should.equal(true)
  332. resolve()
  333. }
  334. ctx.UserPagesController.emailPreferencesPage(
  335. ctx.req,
  336. ctx.res,
  337. ctx.rejectOnError(reject)
  338. )
  339. })
  340. })
  341. it('render page with unsubscribed status', async function (ctx) {
  342. ctx.NewsletterManager.subscribed.yields(null, false)
  343. await new Promise((resolve, reject) => {
  344. ctx.res.callback = () => {
  345. ctx.res.renderedTemplate.should.equal('user/email-preferences')
  346. ctx.res.renderedVariables.title.should.equal('newsletter_info_title')
  347. ctx.res.renderedVariables.subscribed.should.equal(false)
  348. resolve()
  349. }
  350. ctx.UserPagesController.emailPreferencesPage(
  351. ctx.req,
  352. ctx.res,
  353. ctx.rejectOnError(reject)
  354. )
  355. })
  356. })
  357. })
  358. describe('settingsPage', function () {
  359. beforeEach(function (ctx) {
  360. ctx.request.get = sinon
  361. .stub()
  362. .callsArgWith(1, null, { statusCode: 200 }, { has_password: true })
  363. ctx.UserGetter.promises.getUser = sinon.stub().resolves(ctx.user)
  364. })
  365. it('should render user/settings', async function (ctx) {
  366. await new Promise((resolve, reject) => {
  367. ctx.res.callback = () => {
  368. ctx.res.renderedTemplate.should.equal('user/settings')
  369. resolve()
  370. }
  371. ctx.UserPagesController.settingsPage(
  372. ctx.req,
  373. ctx.res,
  374. ctx.rejectOnError(reject)
  375. )
  376. })
  377. })
  378. it('should send user', async function (ctx) {
  379. await new Promise((resolve, reject) => {
  380. ctx.res.callback = () => {
  381. ctx.res.renderedVariables.user.id.should.equal(ctx.user._id)
  382. ctx.res.renderedVariables.user.email.should.equal(ctx.user.email)
  383. resolve()
  384. }
  385. ctx.UserPagesController.settingsPage(
  386. ctx.req,
  387. ctx.res,
  388. ctx.rejectOnError(reject)
  389. )
  390. })
  391. })
  392. it("should set 'shouldAllowEditingDetails' to true", async function (ctx) {
  393. await new Promise((resolve, reject) => {
  394. ctx.res.callback = () => {
  395. ctx.res.renderedVariables.shouldAllowEditingDetails.should.equal(true)
  396. resolve()
  397. }
  398. ctx.UserPagesController.settingsPage(
  399. ctx.req,
  400. ctx.res,
  401. ctx.rejectOnError(reject)
  402. )
  403. })
  404. })
  405. it('should restructure thirdPartyIdentifiers data for template use', async function (ctx) {
  406. const expectedResult = {
  407. google: 'testId',
  408. }
  409. await new Promise((resolve, reject) => {
  410. ctx.res.callback = () => {
  411. expect(ctx.res.renderedVariables.thirdPartyIds).to.include(
  412. expectedResult
  413. )
  414. resolve()
  415. }
  416. ctx.UserPagesController.settingsPage(
  417. ctx.req,
  418. ctx.res,
  419. ctx.rejectOnError(reject)
  420. )
  421. })
  422. })
  423. it("should set and clear 'projectSyncSuccessMessage'", async function (ctx) {
  424. ctx.req.session.projectSyncSuccessMessage = 'Some Sync Success'
  425. await new Promise((resolve, reject) => {
  426. ctx.res.callback = () => {
  427. ctx.res.renderedVariables.projectSyncSuccessMessage.should.equal(
  428. 'Some Sync Success'
  429. )
  430. expect(ctx.req.session.projectSyncSuccessMessage).to.not.exist
  431. resolve()
  432. }
  433. ctx.UserPagesController.settingsPage(
  434. ctx.req,
  435. ctx.res,
  436. ctx.rejectOnError(reject)
  437. )
  438. })
  439. })
  440. it('should cast refProviders to booleans', async function (ctx) {
  441. await new Promise((resolve, reject) => {
  442. ctx.res.callback = () => {
  443. expect(ctx.res.renderedVariables.user.refProviders).to.deep.equal({
  444. mendeley: true,
  445. papers: true,
  446. zotero: true,
  447. })
  448. resolve()
  449. }
  450. ctx.UserPagesController.settingsPage(
  451. ctx.req,
  452. ctx.res,
  453. ctx.rejectOnError(reject)
  454. )
  455. })
  456. })
  457. it('should send the correct managed user admin email', async function (ctx) {
  458. await new Promise((resolve, reject) => {
  459. ctx.res.callback = () => {
  460. expect(
  461. ctx.res.renderedVariables.currentManagedUserAdminEmail
  462. ).to.equal(ctx.adminEmail)
  463. resolve()
  464. }
  465. ctx.UserPagesController.settingsPage(
  466. ctx.req,
  467. ctx.res,
  468. ctx.rejectOnError(reject)
  469. )
  470. })
  471. })
  472. it('should send info for groups with SSO enabled', async function (ctx) {
  473. ctx.user.enrollment = {
  474. sso: [
  475. {
  476. groupId: 'abc123abc123',
  477. primary: true,
  478. linkedAt: new Date(),
  479. },
  480. ],
  481. }
  482. const group1 = {
  483. _id: 'abc123abc123',
  484. teamName: 'Group SSO Rulz',
  485. admin_id: {
  486. email: 'admin.email@ssolove.com',
  487. },
  488. linked: true,
  489. }
  490. const group2 = {
  491. _id: 'def456def456',
  492. admin_id: {
  493. email: 'someone.else@noname.co.uk',
  494. },
  495. linked: false,
  496. }
  497. ctx.Modules.promises.hooks.fire
  498. .withArgs('getUserGroupsSSOEnrollmentStatus')
  499. .resolves([[group1, group2]])
  500. await new Promise((resolve, reject) => {
  501. ctx.res.callback = () => {
  502. expect(
  503. ctx.res.renderedVariables.memberOfSSOEnabledGroups
  504. ).to.deep.equal([
  505. {
  506. groupId: 'abc123abc123',
  507. groupName: 'Group SSO Rulz',
  508. adminEmail: 'admin.email@ssolove.com',
  509. linked: true,
  510. },
  511. {
  512. groupId: 'def456def456',
  513. groupName: undefined,
  514. adminEmail: 'someone.else@noname.co.uk',
  515. linked: false,
  516. },
  517. ])
  518. resolve()
  519. }
  520. ctx.UserPagesController.settingsPage(
  521. ctx.req,
  522. ctx.res,
  523. ctx.rejectOnError(reject)
  524. )
  525. })
  526. })
  527. describe('when ldap.updateUserDetailsOnLogin is true', function () {
  528. beforeEach(function (ctx) {
  529. ctx.settings.ldap = { updateUserDetailsOnLogin: true }
  530. })
  531. afterEach(function (ctx) {
  532. delete ctx.settings.ldap
  533. })
  534. it('should set "shouldAllowEditingDetails" to false', async function (ctx) {
  535. await new Promise((resolve, reject) => {
  536. ctx.res.callback = () => {
  537. ctx.res.renderedVariables.shouldAllowEditingDetails.should.equal(
  538. false
  539. )
  540. resolve()
  541. }
  542. ctx.UserPagesController.settingsPage(
  543. ctx.req,
  544. ctx.res,
  545. ctx.rejectOnError(reject)
  546. )
  547. })
  548. })
  549. })
  550. describe('when saml.updateUserDetailsOnLogin is true', function () {
  551. beforeEach(function (ctx) {
  552. ctx.settings.saml = { updateUserDetailsOnLogin: true }
  553. })
  554. afterEach(function (ctx) {
  555. delete ctx.settings.saml
  556. })
  557. it('should set "shouldAllowEditingDetails" to false', async function (ctx) {
  558. await new Promise((resolve, reject) => {
  559. ctx.res.callback = () => {
  560. ctx.res.renderedVariables.shouldAllowEditingDetails.should.equal(
  561. false
  562. )
  563. resolve()
  564. }
  565. ctx.UserPagesController.settingsPage(
  566. ctx.req,
  567. ctx.res,
  568. ctx.rejectOnError(reject)
  569. )
  570. })
  571. })
  572. })
  573. })
  574. })