ProjectInviteTests.js 23 KB

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