ProjectInviteTests.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784
  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. },
  138. },
  139. callback
  140. )
  141. })
  142. }
  143. const tryGetInviteList = (user, projectId, callback) => {
  144. user.getCsrfToken(error => {
  145. if (error != null) {
  146. return callback(error)
  147. }
  148. user.request.get(
  149. {
  150. url: `/project/${projectId}/invites`,
  151. json: true,
  152. },
  153. callback
  154. )
  155. })
  156. }
  157. const tryJoinProject = (user, projectId, callback) => {
  158. return user.getCsrfToken(error => {
  159. if (error != null) {
  160. return callback(error)
  161. }
  162. user.request.post(
  163. {
  164. url: `/project/${projectId}/join`,
  165. qs: { user_id: user._id },
  166. auth: {
  167. user: settings.apis.web.user,
  168. pass: settings.apis.web.pass,
  169. sendImmediately: true,
  170. },
  171. json: true,
  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 - .*<\/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 = CollaboratorsEmailHandler._buildInviteUrl(
  534. this.fakeProject,
  535. invite
  536. )
  537. cb()
  538. }
  539. )
  540. },
  541. cb =>
  542. expectInviteRedirectToProject(
  543. this.user,
  544. this.secondLink,
  545. this.secondInvite,
  546. cb
  547. ),
  548. cb =>
  549. expectProjectAccess(this.user, this.invite.projectId, cb),
  550. cb =>
  551. revokeInvite(
  552. this.sendingUser,
  553. this.projectId,
  554. this.secondInvite._id,
  555. cb
  556. ),
  557. ],
  558. done
  559. )
  560. })
  561. })
  562. })
  563. })
  564. describe('user is not a member of the project', function () {
  565. it('should not grant access if the user does not accept the invite', function (done) {
  566. Async.series(
  567. [
  568. cb => expectInvitePage(this.user, this.link, cb),
  569. cb => expectNoProjectAccess(this.user, this.invite.projectId, cb),
  570. ],
  571. done
  572. )
  573. })
  574. it('should render the invalid-invite page if the token is invalid', function (done) {
  575. Async.series(
  576. [
  577. cb => {
  578. const link = this.link.replace(
  579. this.invite.token,
  580. 'not_a_real_token'
  581. )
  582. expectInvalidInvitePage(this.user, link, cb)
  583. },
  584. cb => expectNoProjectAccess(this.user, this.invite.projectId, cb),
  585. cb => expectNoProjectAccess(this.user, this.invite.projectId, cb),
  586. ],
  587. done
  588. )
  589. })
  590. it('should allow the user to accept the invite and access the project', function (done) {
  591. Async.series(
  592. [
  593. cb => expectInvitePage(this.user, this.link, cb),
  594. cb => expectAcceptInviteAndRedirect(this.user, this.invite, cb),
  595. cb => expectProjectAccess(this.user, this.invite.projectId, cb),
  596. ],
  597. done
  598. )
  599. })
  600. })
  601. })
  602. describe('user is not logged in initially', function () {
  603. describe('registration prompt workflow with valid token', function () {
  604. before(function () {
  605. if (!Features.hasFeature('registration')) {
  606. this.skip()
  607. }
  608. })
  609. it('should redirect to the register page', function (done) {
  610. expectInviteRedirectToRegister(this.user, this.link, done)
  611. })
  612. it('should allow user to accept the invite if the user registers a new account', function (done) {
  613. Async.series(
  614. [
  615. cb => expectInviteRedirectToRegister(this.user, this.link, cb),
  616. cb =>
  617. expectRegistrationRedirectToInvite(
  618. this.user,
  619. 'some_email@example.com',
  620. this.link,
  621. cb
  622. ),
  623. cb => expectInvitePage(this.user, this.link, cb),
  624. cb => expectAcceptInviteAndRedirect(this.user, this.invite, cb),
  625. cb => expectProjectAccess(this.user, this.invite.projectId, cb),
  626. ],
  627. done
  628. )
  629. })
  630. })
  631. describe('registration prompt workflow with non-valid token', function () {
  632. before(function () {
  633. if (!Features.hasFeature('registration')) {
  634. this.skip()
  635. }
  636. })
  637. it('should redirect to the register page', function (done) {
  638. Async.series(
  639. [
  640. cb => expectInviteRedirectToRegister(this.user, this.link, cb),
  641. cb => expectNoProjectAccess(this.user, this.invite.projectId, cb),
  642. ],
  643. done
  644. )
  645. })
  646. it('should display invalid-invite if the user registers a new account', function (done) {
  647. const badLink = this.link.replace(
  648. this.invite.token,
  649. 'not_a_real_token'
  650. )
  651. Async.series(
  652. [
  653. cb => expectInviteRedirectToRegister(this.user, badLink, cb),
  654. cb =>
  655. expectRegistrationRedirectToInvite(
  656. this.user,
  657. 'some_email@example.com',
  658. badLink,
  659. cb
  660. ),
  661. cb => expectInvalidInvitePage(this.user, badLink, cb),
  662. cb => expectNoProjectAccess(this.user, this.invite.projectId, cb),
  663. ],
  664. done
  665. )
  666. })
  667. })
  668. describe('login workflow with valid token', function () {
  669. it('should redirect to the register page', function (done) {
  670. Async.series(
  671. [
  672. cb => expectInviteRedirectToRegister(this.user, this.link, cb),
  673. cb => expectNoProjectAccess(this.user, this.invite.projectId, cb),
  674. ],
  675. done
  676. )
  677. })
  678. it('should allow the user to login to view the invite', function (done) {
  679. Async.series(
  680. [
  681. cb => expectInviteRedirectToRegister(this.user, this.link, cb),
  682. cb => expectLoginPage(this.user, cb),
  683. cb => expectLoginRedirectToInvite(this.user, this.link, cb),
  684. cb => expectInvitePage(this.user, this.link, cb),
  685. cb => expectNoProjectAccess(this.user, this.invite.projectId, cb),
  686. ],
  687. done
  688. )
  689. })
  690. it('should allow user to accept the invite if the user logs in', function (done) {
  691. Async.series(
  692. [
  693. cb => expectInviteRedirectToRegister(this.user, this.link, cb),
  694. cb => expectLoginPage(this.user, cb),
  695. cb => expectLoginRedirectToInvite(this.user, this.link, cb),
  696. cb => expectInvitePage(this.user, this.link, cb),
  697. cb => expectAcceptInviteAndRedirect(this.user, this.invite, cb),
  698. cb => expectProjectAccess(this.user, this.invite.projectId, cb),
  699. ],
  700. done
  701. )
  702. })
  703. })
  704. describe('login workflow with non-valid token', function () {
  705. it('should redirect to the register page', function (done) {
  706. Async.series(
  707. [
  708. cb => expectInviteRedirectToRegister(this.user, this.link, cb),
  709. cb => expectNoProjectAccess(this.user, this.invite.projectId, cb),
  710. ],
  711. done
  712. )
  713. })
  714. it('should show the invalid-invite page once the user has logged in', function (done) {
  715. const badLink = this.link.replace(
  716. this.invite.token,
  717. 'not_a_real_token'
  718. )
  719. Async.series(
  720. [
  721. cb => {
  722. return expectInviteRedirectToRegister(this.user, badLink, cb)
  723. },
  724. cb => {
  725. return expectLoginPage(this.user, cb)
  726. },
  727. cb => expectLoginRedirectToInvite(this.user, badLink, cb),
  728. cb => expectInvalidInvitePage(this.user, badLink, cb),
  729. cb => expectNoProjectAccess(this.user, this.invite.projectId, cb),
  730. ],
  731. done
  732. )
  733. })
  734. })
  735. })
  736. })
  737. })