ProjectInviteTests.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781
  1. const { expect } = require('chai')
  2. const Async = require('async')
  3. const User = require('./helpers/User')
  4. const settings = require('@overleaf/settings')
  5. const CollaboratorsEmailHandler = require('../../../app/src/Features/Collaborators/CollaboratorsEmailHandler')
  6. const Features = require('../../../app/src/infrastructure/Features')
  7. const cheerio = require('cheerio')
  8. const createInvite = (sendingUser, projectId, email, callback) => {
  9. sendingUser.getCsrfToken(err => {
  10. if (err) {
  11. return callback(err)
  12. }
  13. sendingUser.request.post(
  14. {
  15. uri: `/project/${projectId}/invite`,
  16. json: {
  17. email,
  18. privileges: 'readAndWrite',
  19. },
  20. },
  21. (err, response, body) => {
  22. if (err) {
  23. return callback(err)
  24. }
  25. expect(response.statusCode).to.equal(200)
  26. expect(body.error).to.not.exist
  27. expect(body.invite).to.exist
  28. callback(null, body.invite)
  29. }
  30. )
  31. })
  32. }
  33. const createProject = (owner, projectName, callback) => {
  34. owner.createProject(projectName, (err, projectId) => {
  35. if (err) {
  36. throw err
  37. }
  38. const fakeProject = {
  39. _id: projectId,
  40. name: projectName,
  41. owner_ref: owner,
  42. }
  43. callback(err, projectId, fakeProject)
  44. })
  45. }
  46. const createProjectAndInvite = (owner, projectName, email, callback) => {
  47. createProject(owner, projectName, (err, projectId, project) => {
  48. if (err) {
  49. return callback(err)
  50. }
  51. createInvite(owner, projectId, email, (err, invite) => {
  52. if (err) {
  53. return callback(err)
  54. }
  55. const link = CollaboratorsEmailHandler._buildInviteUrl(project, invite)
  56. callback(null, project, invite, link)
  57. })
  58. })
  59. }
  60. const revokeInvite = (sendingUser, projectId, inviteId, callback) => {
  61. sendingUser.getCsrfToken(err => {
  62. if (err) {
  63. return callback(err)
  64. }
  65. sendingUser.request.delete(
  66. {
  67. uri: `/project/${projectId}/invite/${inviteId}`,
  68. },
  69. err => {
  70. if (err) {
  71. return callback(err)
  72. }
  73. callback()
  74. }
  75. )
  76. })
  77. }
  78. // Actions
  79. const tryFollowInviteLink = (user, link, callback) => {
  80. user.request.get(
  81. {
  82. uri: link,
  83. baseUrl: null,
  84. },
  85. callback
  86. )
  87. }
  88. const tryAcceptInvite = (user, invite, callback) => {
  89. user.getCsrfToken(err => {
  90. if (err) {
  91. return callback(err)
  92. }
  93. user.request.post(
  94. {
  95. uri: `/project/${invite.projectId}/invite/token/${invite.token}/accept`,
  96. json: {
  97. token: invite.token,
  98. },
  99. },
  100. callback
  101. )
  102. })
  103. }
  104. const tryFollowLoginLink = (user, loginLink, callback) => {
  105. user.getCsrfToken(error => {
  106. if (error != null) {
  107. return callback(error)
  108. }
  109. user.request.get(loginLink, callback)
  110. })
  111. }
  112. const tryLoginUser = (user, callback) => {
  113. user.getCsrfToken(error => {
  114. if (error != null) {
  115. return callback(error)
  116. }
  117. user.request.post(
  118. {
  119. url: '/login',
  120. json: {
  121. email: user.email,
  122. password: user.password,
  123. 'g-recaptcha-response': 'valid',
  124. },
  125. },
  126. callback
  127. )
  128. })
  129. }
  130. const tryGetInviteList = (user, projectId, callback) => {
  131. user.getCsrfToken(error => {
  132. if (error != null) {
  133. return callback(error)
  134. }
  135. user.request.get(
  136. {
  137. url: `/project/${projectId}/invites`,
  138. json: true,
  139. },
  140. callback
  141. )
  142. })
  143. }
  144. const tryJoinProject = (user, projectId, callback) => {
  145. return user.getCsrfToken(error => {
  146. if (error != null) {
  147. return callback(error)
  148. }
  149. user.request.post(
  150. {
  151. url: `/project/${projectId}/join`,
  152. auth: {
  153. user: settings.apis.web.user,
  154. pass: settings.apis.web.pass,
  155. sendImmediately: true,
  156. },
  157. json: { userId: user._id },
  158. jar: false,
  159. },
  160. callback
  161. )
  162. })
  163. }
  164. // Expectations
  165. const expectProjectAccess = (user, projectId, callback) => {
  166. // should have access to project
  167. user.openProject(projectId, err => {
  168. expect(err).not.to.exist
  169. callback()
  170. })
  171. }
  172. const expectNoProjectAccess = (user, projectId, callback) => {
  173. // should not have access to project page
  174. user.openProject(projectId, err => {
  175. expect(err).to.be.instanceof(Error)
  176. callback()
  177. })
  178. }
  179. const expectInvitePage = (user, link, callback) => {
  180. // view invite
  181. tryFollowInviteLink(user, link, (err, response, body) => {
  182. expect(err).not.to.exist
  183. expect(response.statusCode).to.equal(200)
  184. expect(body).to.match(/<title>Project Invite - .*<\/title>/)
  185. callback()
  186. })
  187. }
  188. const expectInvalidInvitePage = (user, link, callback) => {
  189. // view invalid invite
  190. tryFollowInviteLink(user, link, (err, response, body) => {
  191. expect(err).not.to.exist
  192. expect(response.statusCode).to.equal(404)
  193. expect(body).to.match(/<title>Invalid Invite - .*<\/title>/)
  194. callback()
  195. })
  196. }
  197. const expectInviteRedirectToRegister = (user, link, callback) => {
  198. // view invite, redirect to `/register`
  199. tryFollowInviteLink(user, link, (err, response) => {
  200. expect(err).not.to.exist
  201. expect(response.statusCode).to.equal(302)
  202. expect(response.headers.location).to.match(/^\/register.*$/)
  203. user.getSession((err, session) => {
  204. if (err) return callback(err)
  205. expect(session.sharedProjectData).deep.equals({
  206. project_name: PROJECT_NAME,
  207. user_first_name: OWNER_NAME,
  208. })
  209. callback()
  210. })
  211. })
  212. }
  213. const expectLoginPage = (user, callback) => {
  214. tryFollowLoginLink(user, '/login', (err, response, body) => {
  215. expect(err).not.to.exist
  216. expect(response.statusCode).to.equal(200)
  217. expect(body).to.match(/<title>(Login|Log in to Overleaf) - .*<\/title>/)
  218. callback()
  219. })
  220. }
  221. const expectLoginRedirectToInvite = (user, link, callback) => {
  222. tryLoginUser(user, (err, response) => {
  223. expect(err).not.to.exist
  224. expect(response.statusCode).to.equal(200)
  225. callback()
  226. })
  227. }
  228. const expectRegistrationRedirectToInvite = (user, link, callback) => {
  229. user.register((err, _user, response) => {
  230. expect(err).not.to.exist
  231. expect(response.statusCode).to.equal(200)
  232. if (response.body.redir === '/registration/try-premium') {
  233. user.request.get('/registration/onboarding', (err, response) => {
  234. if (err) return callback(err)
  235. expect(response.statusCode).to.equal(200)
  236. const dom = cheerio.load(response.body)
  237. const skipUrl = dom('meta[name="ol-skipUrl"]')[0].attribs.content
  238. expect(new URL(skipUrl, settings.siteUrl).href).to.equal(
  239. new URL(link, settings.siteUrl).href
  240. )
  241. callback()
  242. })
  243. } else {
  244. expect(response.body.redir).to.equal(link)
  245. callback()
  246. }
  247. })
  248. }
  249. const expectInviteRedirectToProject = (user, link, invite, callback) => {
  250. // view invite, redirect straight to project
  251. tryFollowInviteLink(user, link, (err, response) => {
  252. expect(err).not.to.exist
  253. expect(response.statusCode).to.equal(302)
  254. expect(response.headers.location).to.equal(`/project/${invite.projectId}`)
  255. callback()
  256. })
  257. }
  258. const expectAcceptInviteAndRedirect = (user, invite, callback) => {
  259. // should accept the invite and redirect to project
  260. tryAcceptInvite(user, invite, (err, response) => {
  261. expect(err).not.to.exist
  262. expect(response.statusCode).to.equal(302)
  263. expect(response.headers.location).to.equal(`/project/${invite.projectId}`)
  264. callback()
  265. })
  266. }
  267. const expectInviteListCount = (user, projectId, count, callback) => {
  268. tryGetInviteList(user, projectId, (err, response, body) => {
  269. expect(err).not.to.exist
  270. expect(response.statusCode).to.equal(200)
  271. expect(body).to.have.all.keys(['invites'])
  272. expect(body.invites.length).to.equal(count)
  273. callback()
  274. })
  275. }
  276. const expectInvitesInJoinProjectCount = (user, projectId, count, callback) => {
  277. tryJoinProject(user, projectId, (err, response, body) => {
  278. expect(err).not.to.exist
  279. expect(response.statusCode).to.equal(200)
  280. expect(body.project).to.contain.keys(['invites'])
  281. expect(body.project.invites.length).to.equal(count)
  282. callback()
  283. })
  284. }
  285. const PROJECT_NAME = 'project name for sharing test'
  286. const OWNER_NAME = 'sending user name'
  287. describe('ProjectInviteTests', function () {
  288. beforeEach(function (done) {
  289. this.sendingUser = new User()
  290. this.user = new User()
  291. this.site_admin = new User({ email: `admin+${Math.random()}@example.com` })
  292. this.email = `smoketestuser+${Math.random()}@example.com`
  293. Async.series(
  294. [
  295. cb => this.sendingUser.login(cb),
  296. cb => this.sendingUser.setFeatures({ collaborators: 10 }, cb),
  297. cb =>
  298. this.sendingUser.mongoUpdate(
  299. {
  300. $set: { first_name: OWNER_NAME },
  301. },
  302. cb
  303. ),
  304. cb =>
  305. this.sendingUser.setFeaturesOverride(
  306. {
  307. note: 'ProjectInviteTests acceptance tests',
  308. features: { collaborators: 10 },
  309. },
  310. cb
  311. ),
  312. ],
  313. done
  314. )
  315. })
  316. describe('creating invites', function () {
  317. describe('creating two invites', function () {
  318. beforeEach(function (done) {
  319. createProject(
  320. this.sendingUser,
  321. PROJECT_NAME,
  322. (err, projectId, project) => {
  323. expect(err).not.to.exist
  324. this.projectId = projectId
  325. this.fakeProject = project
  326. done()
  327. }
  328. )
  329. })
  330. it('should allow the project owner to create and remove invites', function (done) {
  331. Async.series(
  332. [
  333. cb => expectProjectAccess(this.sendingUser, this.projectId, cb),
  334. cb =>
  335. expectInviteListCount(this.sendingUser, this.projectId, 0, cb),
  336. // create invite, check invite list count
  337. cb => {
  338. createInvite(
  339. this.sendingUser,
  340. this.projectId,
  341. this.email,
  342. (err, invite) => {
  343. if (err) {
  344. return cb(err)
  345. }
  346. this.invite = invite
  347. cb()
  348. }
  349. )
  350. },
  351. cb =>
  352. expectInviteListCount(this.sendingUser, this.projectId, 1, cb),
  353. cb =>
  354. revokeInvite(
  355. this.sendingUser,
  356. this.projectId,
  357. this.invite._id,
  358. cb
  359. ),
  360. cb =>
  361. expectInviteListCount(this.sendingUser, this.projectId, 0, cb),
  362. // and a second time
  363. cb => {
  364. createInvite(
  365. this.sendingUser,
  366. this.projectId,
  367. this.email,
  368. (err, invite) => {
  369. if (err) {
  370. return cb(err)
  371. }
  372. this.invite = invite
  373. cb()
  374. }
  375. )
  376. },
  377. cb =>
  378. expectInviteListCount(this.sendingUser, this.projectId, 1, cb),
  379. // check the joinProject view
  380. cb =>
  381. expectInvitesInJoinProjectCount(
  382. this.sendingUser,
  383. this.projectId,
  384. 1,
  385. cb
  386. ),
  387. // revoke invite
  388. cb =>
  389. revokeInvite(
  390. this.sendingUser,
  391. this.projectId,
  392. this.invite._id,
  393. cb
  394. ),
  395. cb =>
  396. expectInviteListCount(this.sendingUser, this.projectId, 0, cb),
  397. cb =>
  398. expectInvitesInJoinProjectCount(
  399. this.sendingUser,
  400. this.projectId,
  401. 0,
  402. cb
  403. ),
  404. ],
  405. done
  406. )
  407. })
  408. it('should allow the project owner to create many invites at once', function (done) {
  409. Async.series(
  410. [
  411. cb => expectProjectAccess(this.sendingUser, this.projectId, cb),
  412. cb =>
  413. expectInviteListCount(this.sendingUser, this.projectId, 0, cb),
  414. // create first invite
  415. cb => {
  416. createInvite(
  417. this.sendingUser,
  418. this.projectId,
  419. this.email,
  420. (err, invite) => {
  421. if (err) {
  422. return cb(err)
  423. }
  424. this.inviteOne = invite
  425. cb()
  426. }
  427. )
  428. },
  429. cb =>
  430. expectInviteListCount(this.sendingUser, this.projectId, 1, cb),
  431. // and a second
  432. cb => {
  433. createInvite(
  434. this.sendingUser,
  435. this.projectId,
  436. this.email,
  437. (err, invite) => {
  438. if (err) {
  439. return cb(err)
  440. }
  441. this.inviteTwo = invite
  442. cb()
  443. }
  444. )
  445. },
  446. // should have two
  447. cb =>
  448. expectInviteListCount(this.sendingUser, this.projectId, 2, cb),
  449. cb =>
  450. expectInvitesInJoinProjectCount(
  451. this.sendingUser,
  452. this.projectId,
  453. 2,
  454. cb
  455. ),
  456. // revoke first
  457. cb =>
  458. revokeInvite(
  459. this.sendingUser,
  460. this.projectId,
  461. this.inviteOne._id,
  462. cb
  463. ),
  464. cb =>
  465. expectInviteListCount(this.sendingUser, this.projectId, 1, cb),
  466. // revoke second
  467. cb =>
  468. revokeInvite(
  469. this.sendingUser,
  470. this.projectId,
  471. this.inviteTwo._id,
  472. cb
  473. ),
  474. cb =>
  475. expectInviteListCount(this.sendingUser, this.projectId, 0, cb),
  476. ],
  477. done
  478. )
  479. })
  480. })
  481. })
  482. describe('clicking the invite link', function () {
  483. beforeEach(function (done) {
  484. createProjectAndInvite(
  485. this.sendingUser,
  486. PROJECT_NAME,
  487. this.email,
  488. (err, project, invite, link) => {
  489. expect(err).not.to.exist
  490. this.projectId = project._id
  491. this.fakeProject = project
  492. this.invite = invite
  493. this.link = link
  494. done()
  495. }
  496. )
  497. })
  498. describe('user is logged in already', function () {
  499. beforeEach(function (done) {
  500. this.user.login(done)
  501. })
  502. describe('user is already a member of the project', function () {
  503. beforeEach(function (done) {
  504. Async.series(
  505. [
  506. cb => expectInvitePage(this.user, this.link, cb),
  507. cb => expectAcceptInviteAndRedirect(this.user, this.invite, cb),
  508. cb => expectProjectAccess(this.user, this.invite.projectId, cb),
  509. ],
  510. done
  511. )
  512. })
  513. describe('when user clicks on the invite a second time', function () {
  514. it('should just redirect to the project page', function (done) {
  515. Async.series(
  516. [
  517. cb => expectProjectAccess(this.user, this.invite.projectId, cb),
  518. cb =>
  519. expectInviteRedirectToProject(
  520. this.user,
  521. this.link,
  522. this.invite,
  523. cb
  524. ),
  525. cb => expectProjectAccess(this.user, this.invite.projectId, cb),
  526. ],
  527. done
  528. )
  529. })
  530. describe('when the user recieves another invite to the same project', function () {
  531. it('should redirect to the project page', function (done) {
  532. Async.series(
  533. [
  534. cb => {
  535. createInvite(
  536. this.sendingUser,
  537. this.projectId,
  538. this.email,
  539. (err, invite) => {
  540. if (err) {
  541. throw err
  542. }
  543. this.secondInvite = invite
  544. this.secondLink =
  545. CollaboratorsEmailHandler._buildInviteUrl(
  546. this.fakeProject,
  547. invite
  548. )
  549. cb()
  550. }
  551. )
  552. },
  553. cb =>
  554. expectInviteRedirectToProject(
  555. this.user,
  556. this.secondLink,
  557. this.secondInvite,
  558. cb
  559. ),
  560. cb =>
  561. expectProjectAccess(this.user, this.invite.projectId, cb),
  562. cb =>
  563. revokeInvite(
  564. this.sendingUser,
  565. this.projectId,
  566. this.secondInvite._id,
  567. cb
  568. ),
  569. ],
  570. done
  571. )
  572. })
  573. })
  574. })
  575. })
  576. describe('user is not a member of the project', function () {
  577. it('should not grant access if the user does not accept the invite', function (done) {
  578. Async.series(
  579. [
  580. cb => expectInvitePage(this.user, this.link, cb),
  581. cb => expectNoProjectAccess(this.user, this.invite.projectId, cb),
  582. ],
  583. done
  584. )
  585. })
  586. it('should render the invalid-invite page if the token is invalid', function (done) {
  587. Async.series(
  588. [
  589. cb => {
  590. const link = this.link.replace(
  591. this.invite.token,
  592. 'not_a_real_token'
  593. )
  594. expectInvalidInvitePage(this.user, link, cb)
  595. },
  596. cb => expectNoProjectAccess(this.user, this.invite.projectId, cb),
  597. cb => expectNoProjectAccess(this.user, this.invite.projectId, cb),
  598. ],
  599. done
  600. )
  601. })
  602. it('should allow the user to accept the invite and access the project', function (done) {
  603. Async.series(
  604. [
  605. cb => expectInvitePage(this.user, this.link, cb),
  606. cb => expectAcceptInviteAndRedirect(this.user, this.invite, cb),
  607. cb => expectProjectAccess(this.user, this.invite.projectId, cb),
  608. ],
  609. done
  610. )
  611. })
  612. })
  613. })
  614. describe('user is not logged in initially', function () {
  615. describe('registration prompt workflow with valid token', function () {
  616. before(function () {
  617. if (!Features.hasFeature('registration')) {
  618. this.skip()
  619. }
  620. })
  621. it('should redirect to the register page', function (done) {
  622. expectInviteRedirectToRegister(this.user, this.link, done)
  623. })
  624. it('should allow user to accept the invite if the user registers a new account', function (done) {
  625. Async.series(
  626. [
  627. cb => expectInviteRedirectToRegister(this.user, this.link, cb),
  628. cb =>
  629. expectRegistrationRedirectToInvite(this.user, this.link, cb),
  630. cb => expectInvitePage(this.user, this.link, cb),
  631. cb => expectAcceptInviteAndRedirect(this.user, this.invite, cb),
  632. cb => expectProjectAccess(this.user, this.invite.projectId, cb),
  633. ],
  634. done
  635. )
  636. })
  637. })
  638. describe('registration prompt workflow with non-valid token', function () {
  639. before(function () {
  640. if (!Features.hasFeature('registration')) {
  641. this.skip()
  642. }
  643. })
  644. it('should redirect to the register page', function (done) {
  645. Async.series(
  646. [
  647. cb => expectInviteRedirectToRegister(this.user, this.link, cb),
  648. cb => expectNoProjectAccess(this.user, this.invite.projectId, cb),
  649. ],
  650. done
  651. )
  652. })
  653. it('should display invalid-invite right away', function (done) {
  654. const badLink = this.link.replace(
  655. this.invite.token,
  656. 'not_a_real_token'
  657. )
  658. Async.series(
  659. [
  660. cb => expectInvalidInvitePage(this.user, badLink, cb),
  661. cb => expectNoProjectAccess(this.user, this.invite.projectId, cb),
  662. ],
  663. done
  664. )
  665. })
  666. })
  667. describe('login workflow with valid token', function () {
  668. beforeEach(function (done) {
  669. this.user.ensureUserExists(done)
  670. })
  671. it('should redirect to the register page', function (done) {
  672. Async.series(
  673. [
  674. cb => expectInviteRedirectToRegister(this.user, this.link, cb),
  675. cb => expectNoProjectAccess(this.user, this.invite.projectId, cb),
  676. ],
  677. done
  678. )
  679. })
  680. it('should allow the user to login to view the invite', function (done) {
  681. Async.series(
  682. [
  683. cb => expectInviteRedirectToRegister(this.user, this.link, cb),
  684. cb => expectLoginPage(this.user, cb),
  685. cb => expectLoginRedirectToInvite(this.user, this.link, cb),
  686. cb => expectInvitePage(this.user, this.link, cb),
  687. cb => expectNoProjectAccess(this.user, this.invite.projectId, cb),
  688. ],
  689. done
  690. )
  691. })
  692. it('should allow user to accept the invite if the user logs in', function (done) {
  693. Async.series(
  694. [
  695. cb => expectInviteRedirectToRegister(this.user, this.link, cb),
  696. cb => expectLoginPage(this.user, cb),
  697. cb => expectLoginRedirectToInvite(this.user, this.link, cb),
  698. cb => expectInvitePage(this.user, this.link, cb),
  699. cb => expectAcceptInviteAndRedirect(this.user, this.invite, cb),
  700. cb => expectProjectAccess(this.user, this.invite.projectId, cb),
  701. ],
  702. done
  703. )
  704. })
  705. })
  706. describe('login workflow with non-valid token', function () {
  707. it('should redirect to the register page', function (done) {
  708. Async.series(
  709. [
  710. cb => expectInviteRedirectToRegister(this.user, this.link, cb),
  711. cb => expectNoProjectAccess(this.user, this.invite.projectId, cb),
  712. ],
  713. done
  714. )
  715. })
  716. it('should show the invalid-invite page right away', function (done) {
  717. const badLink = this.link.replace(
  718. this.invite.token,
  719. 'not_a_real_token'
  720. )
  721. Async.series(
  722. [
  723. cb => expectInvalidInvitePage(this.user, badLink, cb),
  724. cb => expectNoProjectAccess(this.user, this.invite.projectId, cb),
  725. ],
  726. done
  727. )
  728. })
  729. })
  730. })
  731. })
  732. })