ProjectInviteTests.js 23 KB

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