ProjectInviteTests.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786
  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. qs: { user_id: user._id },
  167. auth: {
  168. user: settings.apis.web.user,
  169. pass: settings.apis.web.pass,
  170. sendImmediately: true,
  171. },
  172. json: true,
  173. jar: false,
  174. },
  175. callback
  176. )
  177. })
  178. }
  179. // Expectations
  180. const expectProjectAccess = (user, projectId, callback) => {
  181. // should have access to project
  182. user.openProject(projectId, err => {
  183. expect(err).not.to.exist
  184. callback()
  185. })
  186. }
  187. const expectNoProjectAccess = (user, projectId, callback) => {
  188. // should not have access to project page
  189. user.openProject(projectId, err => {
  190. expect(err).to.be.instanceof(Error)
  191. callback()
  192. })
  193. }
  194. const expectInvitePage = (user, link, callback) => {
  195. // view invite
  196. tryFollowInviteLink(user, link, (err, response, body) => {
  197. expect(err).not.to.exist
  198. expect(response.statusCode).to.equal(200)
  199. expect(body).to.match(/<title>Project Invite - .*<\/title>/)
  200. callback()
  201. })
  202. }
  203. const expectInvalidInvitePage = (user, link, callback) => {
  204. // view invalid invite
  205. tryFollowInviteLink(user, link, (err, response, body) => {
  206. expect(err).not.to.exist
  207. expect(response.statusCode).to.equal(200)
  208. expect(body).to.match(/<title>Invalid Invite - .*<\/title>/)
  209. callback()
  210. })
  211. }
  212. const expectInviteRedirectToRegister = (user, link, callback) => {
  213. // view invite, redirect to `/register`
  214. tryFollowInviteLink(user, link, (err, response) => {
  215. expect(err).not.to.exist
  216. expect(response.statusCode).to.equal(302)
  217. expect(response.headers.location).to.match(/^\/register.*$/)
  218. callback()
  219. })
  220. }
  221. const expectLoginPage = (user, callback) => {
  222. tryFollowLoginLink(user, '/login', (err, response, body) => {
  223. expect(err).not.to.exist
  224. expect(response.statusCode).to.equal(200)
  225. expect(body).to.match(/<title>Login - .*<\/title>/)
  226. callback()
  227. })
  228. }
  229. const expectLoginRedirectToInvite = (user, link, callback) => {
  230. tryLoginUser(user, (err, response) => {
  231. expect(err).not.to.exist
  232. expect(response.statusCode).to.equal(200)
  233. callback()
  234. })
  235. }
  236. const expectRegistrationRedirectToInvite = (user, email, link, callback) => {
  237. tryRegisterUser(user, email, (err, response) => {
  238. expect(err).not.to.exist
  239. expect(response.statusCode).to.equal(200)
  240. callback()
  241. })
  242. }
  243. const expectInviteRedirectToProject = (user, link, invite, callback) => {
  244. // view invite, redirect straight to project
  245. tryFollowInviteLink(user, link, (err, response) => {
  246. expect(err).not.to.exist
  247. expect(response.statusCode).to.equal(302)
  248. expect(response.headers.location).to.equal(`/project/${invite.projectId}`)
  249. callback()
  250. })
  251. }
  252. const expectAcceptInviteAndRedirect = (user, invite, callback) => {
  253. // should accept the invite and redirect to project
  254. tryAcceptInvite(user, invite, (err, response) => {
  255. expect(err).not.to.exist
  256. expect(response.statusCode).to.equal(302)
  257. expect(response.headers.location).to.equal(`/project/${invite.projectId}`)
  258. callback()
  259. })
  260. }
  261. const expectInviteListCount = (user, projectId, count, callback) => {
  262. tryGetInviteList(user, projectId, (err, response, body) => {
  263. expect(err).not.to.exist
  264. expect(response.statusCode).to.equal(200)
  265. expect(body).to.have.all.keys(['invites'])
  266. expect(body.invites.length).to.equal(count)
  267. callback()
  268. })
  269. }
  270. const expectInvitesInJoinProjectCount = (user, projectId, count, callback) => {
  271. tryJoinProject(user, projectId, (err, response, body) => {
  272. expect(err).not.to.exist
  273. expect(response.statusCode).to.equal(200)
  274. expect(body.project).to.contain.keys(['invites'])
  275. expect(body.project.invites.length).to.equal(count)
  276. callback()
  277. })
  278. }
  279. describe('ProjectInviteTests', function () {
  280. beforeEach(function (done) {
  281. this.sendingUser = new User()
  282. this.user = new User()
  283. this.site_admin = new User({ email: 'admin@example.com' })
  284. this.email = 'smoketestuser@example.com'
  285. this.projectName = 'sharing test'
  286. Async.series(
  287. [
  288. cb => this.user.ensureUserExists(cb),
  289. cb => this.sendingUser.login(cb),
  290. cb => this.sendingUser.setFeatures({ collaborators: 10 }, cb),
  291. cb =>
  292. this.sendingUser.setFeaturesOverride(
  293. {
  294. note: 'ProjectInviteTests acceptance tests',
  295. features: { collaborators: 10 },
  296. },
  297. cb
  298. ),
  299. ],
  300. done
  301. )
  302. })
  303. describe('creating invites', function () {
  304. beforeEach(function () {
  305. this.projectName = 'wat'
  306. })
  307. describe('creating two invites', function () {
  308. beforeEach(function (done) {
  309. createProject(
  310. this.sendingUser,
  311. this.projectName,
  312. (err, projectId, project) => {
  313. expect(err).not.to.exist
  314. this.projectId = projectId
  315. this.fakeProject = project
  316. done()
  317. }
  318. )
  319. })
  320. it('should allow the project owner to create and remove invites', function (done) {
  321. Async.series(
  322. [
  323. cb => expectProjectAccess(this.sendingUser, this.projectId, cb),
  324. cb =>
  325. expectInviteListCount(this.sendingUser, this.projectId, 0, cb),
  326. // create invite, check invite list count
  327. cb => {
  328. createInvite(
  329. this.sendingUser,
  330. this.projectId,
  331. this.email,
  332. (err, invite) => {
  333. if (err) {
  334. return cb(err)
  335. }
  336. this.invite = invite
  337. cb()
  338. }
  339. )
  340. },
  341. cb =>
  342. expectInviteListCount(this.sendingUser, this.projectId, 1, cb),
  343. cb =>
  344. revokeInvite(
  345. this.sendingUser,
  346. this.projectId,
  347. this.invite._id,
  348. cb
  349. ),
  350. cb =>
  351. expectInviteListCount(this.sendingUser, this.projectId, 0, cb),
  352. // and a second time
  353. cb => {
  354. createInvite(
  355. this.sendingUser,
  356. this.projectId,
  357. this.email,
  358. (err, invite) => {
  359. if (err) {
  360. return cb(err)
  361. }
  362. this.invite = invite
  363. cb()
  364. }
  365. )
  366. },
  367. cb =>
  368. expectInviteListCount(this.sendingUser, this.projectId, 1, cb),
  369. // check the joinProject view
  370. cb =>
  371. expectInvitesInJoinProjectCount(
  372. this.sendingUser,
  373. this.projectId,
  374. 1,
  375. cb
  376. ),
  377. // revoke invite
  378. cb =>
  379. revokeInvite(
  380. this.sendingUser,
  381. this.projectId,
  382. this.invite._id,
  383. cb
  384. ),
  385. cb =>
  386. expectInviteListCount(this.sendingUser, this.projectId, 0, cb),
  387. cb =>
  388. expectInvitesInJoinProjectCount(
  389. this.sendingUser,
  390. this.projectId,
  391. 0,
  392. cb
  393. ),
  394. ],
  395. done
  396. )
  397. })
  398. it('should allow the project owner to create many invites at once', function (done) {
  399. Async.series(
  400. [
  401. cb => expectProjectAccess(this.sendingUser, this.projectId, cb),
  402. cb =>
  403. expectInviteListCount(this.sendingUser, this.projectId, 0, cb),
  404. // create first invite
  405. cb => {
  406. createInvite(
  407. this.sendingUser,
  408. this.projectId,
  409. this.email,
  410. (err, invite) => {
  411. if (err) {
  412. return cb(err)
  413. }
  414. this.inviteOne = invite
  415. cb()
  416. }
  417. )
  418. },
  419. cb =>
  420. expectInviteListCount(this.sendingUser, this.projectId, 1, cb),
  421. // and a second
  422. cb => {
  423. createInvite(
  424. this.sendingUser,
  425. this.projectId,
  426. this.email,
  427. (err, invite) => {
  428. if (err) {
  429. return cb(err)
  430. }
  431. this.inviteTwo = invite
  432. cb()
  433. }
  434. )
  435. },
  436. // should have two
  437. cb =>
  438. expectInviteListCount(this.sendingUser, this.projectId, 2, cb),
  439. cb =>
  440. expectInvitesInJoinProjectCount(
  441. this.sendingUser,
  442. this.projectId,
  443. 2,
  444. cb
  445. ),
  446. // revoke first
  447. cb =>
  448. revokeInvite(
  449. this.sendingUser,
  450. this.projectId,
  451. this.inviteOne._id,
  452. cb
  453. ),
  454. cb =>
  455. expectInviteListCount(this.sendingUser, this.projectId, 1, cb),
  456. // revoke second
  457. cb =>
  458. revokeInvite(
  459. this.sendingUser,
  460. this.projectId,
  461. this.inviteTwo._id,
  462. cb
  463. ),
  464. cb =>
  465. expectInviteListCount(this.sendingUser, this.projectId, 0, cb),
  466. ],
  467. done
  468. )
  469. })
  470. })
  471. })
  472. describe('clicking the invite link', function () {
  473. beforeEach(function (done) {
  474. createProjectAndInvite(
  475. this.sendingUser,
  476. this.projectName,
  477. this.email,
  478. (err, project, invite, link) => {
  479. expect(err).not.to.exist
  480. this.projectId = project._id
  481. this.fakeProject = project
  482. this.invite = invite
  483. this.link = link
  484. done()
  485. }
  486. )
  487. })
  488. describe('user is logged in already', function () {
  489. beforeEach(function (done) {
  490. this.user.login(done)
  491. })
  492. describe('user is already a member of the project', function () {
  493. beforeEach(function (done) {
  494. Async.series(
  495. [
  496. cb => expectInvitePage(this.user, this.link, cb),
  497. cb => expectAcceptInviteAndRedirect(this.user, this.invite, cb),
  498. cb => expectProjectAccess(this.user, this.invite.projectId, cb),
  499. ],
  500. done
  501. )
  502. })
  503. describe('when user clicks on the invite a second time', function () {
  504. it('should just redirect to the project page', function (done) {
  505. Async.series(
  506. [
  507. cb => expectProjectAccess(this.user, this.invite.projectId, cb),
  508. cb =>
  509. expectInviteRedirectToProject(
  510. this.user,
  511. this.link,
  512. this.invite,
  513. cb
  514. ),
  515. cb => expectProjectAccess(this.user, this.invite.projectId, cb),
  516. ],
  517. done
  518. )
  519. })
  520. describe('when the user recieves another invite to the same project', function () {
  521. it('should redirect to the project page', function (done) {
  522. Async.series(
  523. [
  524. cb => {
  525. createInvite(
  526. this.sendingUser,
  527. this.projectId,
  528. this.email,
  529. (err, invite) => {
  530. if (err) {
  531. throw err
  532. }
  533. this.secondInvite = invite
  534. this.secondLink =
  535. CollaboratorsEmailHandler._buildInviteUrl(
  536. this.fakeProject,
  537. invite
  538. )
  539. cb()
  540. }
  541. )
  542. },
  543. cb =>
  544. expectInviteRedirectToProject(
  545. this.user,
  546. this.secondLink,
  547. this.secondInvite,
  548. cb
  549. ),
  550. cb =>
  551. expectProjectAccess(this.user, this.invite.projectId, cb),
  552. cb =>
  553. revokeInvite(
  554. this.sendingUser,
  555. this.projectId,
  556. this.secondInvite._id,
  557. cb
  558. ),
  559. ],
  560. done
  561. )
  562. })
  563. })
  564. })
  565. })
  566. describe('user is not a member of the project', function () {
  567. it('should not grant access if the user does not accept the invite', function (done) {
  568. Async.series(
  569. [
  570. cb => expectInvitePage(this.user, this.link, cb),
  571. cb => expectNoProjectAccess(this.user, this.invite.projectId, cb),
  572. ],
  573. done
  574. )
  575. })
  576. it('should render the invalid-invite page if the token is invalid', function (done) {
  577. Async.series(
  578. [
  579. cb => {
  580. const link = this.link.replace(
  581. this.invite.token,
  582. 'not_a_real_token'
  583. )
  584. expectInvalidInvitePage(this.user, link, cb)
  585. },
  586. cb => expectNoProjectAccess(this.user, this.invite.projectId, cb),
  587. cb => expectNoProjectAccess(this.user, this.invite.projectId, cb),
  588. ],
  589. done
  590. )
  591. })
  592. it('should allow the user to accept the invite and access the project', function (done) {
  593. Async.series(
  594. [
  595. cb => expectInvitePage(this.user, this.link, cb),
  596. cb => expectAcceptInviteAndRedirect(this.user, this.invite, cb),
  597. cb => expectProjectAccess(this.user, this.invite.projectId, cb),
  598. ],
  599. done
  600. )
  601. })
  602. })
  603. })
  604. describe('user is not logged in initially', function () {
  605. describe('registration prompt workflow with valid token', function () {
  606. before(function () {
  607. if (!Features.hasFeature('registration')) {
  608. this.skip()
  609. }
  610. })
  611. it('should redirect to the register page', function (done) {
  612. expectInviteRedirectToRegister(this.user, this.link, done)
  613. })
  614. it('should allow user to accept the invite if the user registers a new account', function (done) {
  615. Async.series(
  616. [
  617. cb => expectInviteRedirectToRegister(this.user, this.link, cb),
  618. cb =>
  619. expectRegistrationRedirectToInvite(
  620. this.user,
  621. 'some_email@example.com',
  622. this.link,
  623. cb
  624. ),
  625. cb => expectInvitePage(this.user, this.link, cb),
  626. cb => expectAcceptInviteAndRedirect(this.user, this.invite, cb),
  627. cb => expectProjectAccess(this.user, this.invite.projectId, cb),
  628. ],
  629. done
  630. )
  631. })
  632. })
  633. describe('registration prompt workflow with non-valid token', function () {
  634. before(function () {
  635. if (!Features.hasFeature('registration')) {
  636. this.skip()
  637. }
  638. })
  639. it('should redirect to the register page', function (done) {
  640. Async.series(
  641. [
  642. cb => expectInviteRedirectToRegister(this.user, this.link, cb),
  643. cb => expectNoProjectAccess(this.user, this.invite.projectId, cb),
  644. ],
  645. done
  646. )
  647. })
  648. it('should display invalid-invite if the user registers a new account', function (done) {
  649. const badLink = this.link.replace(
  650. this.invite.token,
  651. 'not_a_real_token'
  652. )
  653. Async.series(
  654. [
  655. cb => expectInviteRedirectToRegister(this.user, badLink, cb),
  656. cb =>
  657. expectRegistrationRedirectToInvite(
  658. this.user,
  659. 'some_email@example.com',
  660. badLink,
  661. cb
  662. ),
  663. cb => expectInvalidInvitePage(this.user, badLink, cb),
  664. cb => expectNoProjectAccess(this.user, this.invite.projectId, cb),
  665. ],
  666. done
  667. )
  668. })
  669. })
  670. describe('login workflow with valid token', function () {
  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 once the user has logged in', function (done) {
  717. const badLink = this.link.replace(
  718. this.invite.token,
  719. 'not_a_real_token'
  720. )
  721. Async.series(
  722. [
  723. cb => {
  724. return expectInviteRedirectToRegister(this.user, badLink, cb)
  725. },
  726. cb => {
  727. return expectLoginPage(this.user, cb)
  728. },
  729. cb => expectLoginRedirectToInvite(this.user, badLink, cb),
  730. cb => expectInvalidInvitePage(this.user, badLink, cb),
  731. cb => expectNoProjectAccess(this.user, this.invite.projectId, cb),
  732. ],
  733. done
  734. )
  735. })
  736. })
  737. })
  738. })
  739. })