ProjectInviteTests.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776
  1. const { expect } = require('chai')
  2. const Async = require('async')
  3. const User = require('./helpers/User')
  4. const settings = require('settings-sharelatex')
  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. ],
  291. done
  292. )
  293. })
  294. describe('creating invites', function () {
  295. beforeEach(function () {
  296. this.projectName = 'wat'
  297. })
  298. describe('creating two invites', function () {
  299. beforeEach(function (done) {
  300. createProject(
  301. this.sendingUser,
  302. this.projectName,
  303. (err, projectId, project) => {
  304. expect(err).not.to.exist
  305. this.projectId = projectId
  306. this.fakeProject = project
  307. done()
  308. }
  309. )
  310. })
  311. it('should allow the project owner to create and remove invites', function (done) {
  312. Async.series(
  313. [
  314. cb => expectProjectAccess(this.sendingUser, this.projectId, cb),
  315. cb =>
  316. expectInviteListCount(this.sendingUser, this.projectId, 0, cb),
  317. // create invite, check invite list count
  318. cb => {
  319. createInvite(
  320. this.sendingUser,
  321. this.projectId,
  322. this.email,
  323. (err, invite) => {
  324. if (err) {
  325. return cb(err)
  326. }
  327. this.invite = invite
  328. cb()
  329. }
  330. )
  331. },
  332. cb =>
  333. expectInviteListCount(this.sendingUser, this.projectId, 1, cb),
  334. cb =>
  335. revokeInvite(
  336. this.sendingUser,
  337. this.projectId,
  338. this.invite._id,
  339. cb
  340. ),
  341. cb =>
  342. expectInviteListCount(this.sendingUser, this.projectId, 0, cb),
  343. // and a second time
  344. cb => {
  345. createInvite(
  346. this.sendingUser,
  347. this.projectId,
  348. this.email,
  349. (err, invite) => {
  350. if (err) {
  351. return cb(err)
  352. }
  353. this.invite = invite
  354. cb()
  355. }
  356. )
  357. },
  358. cb =>
  359. expectInviteListCount(this.sendingUser, this.projectId, 1, cb),
  360. // check the joinProject view
  361. cb =>
  362. expectInvitesInJoinProjectCount(
  363. this.sendingUser,
  364. this.projectId,
  365. 1,
  366. cb
  367. ),
  368. // revoke invite
  369. cb =>
  370. revokeInvite(
  371. this.sendingUser,
  372. this.projectId,
  373. this.invite._id,
  374. cb
  375. ),
  376. cb =>
  377. expectInviteListCount(this.sendingUser, this.projectId, 0, cb),
  378. cb =>
  379. expectInvitesInJoinProjectCount(
  380. this.sendingUser,
  381. this.projectId,
  382. 0,
  383. cb
  384. ),
  385. ],
  386. done
  387. )
  388. })
  389. it('should allow the project owner to create many invites at once', function (done) {
  390. Async.series(
  391. [
  392. cb => expectProjectAccess(this.sendingUser, this.projectId, cb),
  393. cb =>
  394. expectInviteListCount(this.sendingUser, this.projectId, 0, cb),
  395. // create first invite
  396. cb => {
  397. createInvite(
  398. this.sendingUser,
  399. this.projectId,
  400. this.email,
  401. (err, invite) => {
  402. if (err) {
  403. return cb(err)
  404. }
  405. this.inviteOne = invite
  406. cb()
  407. }
  408. )
  409. },
  410. cb =>
  411. expectInviteListCount(this.sendingUser, this.projectId, 1, cb),
  412. // and a second
  413. cb => {
  414. createInvite(
  415. this.sendingUser,
  416. this.projectId,
  417. this.email,
  418. (err, invite) => {
  419. if (err) {
  420. return cb(err)
  421. }
  422. this.inviteTwo = invite
  423. cb()
  424. }
  425. )
  426. },
  427. // should have two
  428. cb =>
  429. expectInviteListCount(this.sendingUser, this.projectId, 2, cb),
  430. cb =>
  431. expectInvitesInJoinProjectCount(
  432. this.sendingUser,
  433. this.projectId,
  434. 2,
  435. cb
  436. ),
  437. // revoke first
  438. cb =>
  439. revokeInvite(
  440. this.sendingUser,
  441. this.projectId,
  442. this.inviteOne._id,
  443. cb
  444. ),
  445. cb =>
  446. expectInviteListCount(this.sendingUser, this.projectId, 1, cb),
  447. // revoke second
  448. cb =>
  449. revokeInvite(
  450. this.sendingUser,
  451. this.projectId,
  452. this.inviteTwo._id,
  453. cb
  454. ),
  455. cb =>
  456. expectInviteListCount(this.sendingUser, this.projectId, 0, cb),
  457. ],
  458. done
  459. )
  460. })
  461. })
  462. })
  463. describe('clicking the invite link', function () {
  464. beforeEach(function (done) {
  465. createProjectAndInvite(
  466. this.sendingUser,
  467. this.projectName,
  468. this.email,
  469. (err, project, invite, link) => {
  470. expect(err).not.to.exist
  471. this.projectId = project._id
  472. this.fakeProject = project
  473. this.invite = invite
  474. this.link = link
  475. done()
  476. }
  477. )
  478. })
  479. describe('user is logged in already', function () {
  480. beforeEach(function (done) {
  481. this.user.login(done)
  482. })
  483. describe('user is already a member of the project', function () {
  484. beforeEach(function (done) {
  485. Async.series(
  486. [
  487. cb => expectInvitePage(this.user, this.link, cb),
  488. cb => expectAcceptInviteAndRedirect(this.user, this.invite, cb),
  489. cb => expectProjectAccess(this.user, this.invite.projectId, cb),
  490. ],
  491. done
  492. )
  493. })
  494. describe('when user clicks on the invite a second time', function () {
  495. it('should just redirect to the project page', function (done) {
  496. Async.series(
  497. [
  498. cb => expectProjectAccess(this.user, this.invite.projectId, cb),
  499. cb =>
  500. expectInviteRedirectToProject(
  501. this.user,
  502. this.link,
  503. this.invite,
  504. cb
  505. ),
  506. cb => expectProjectAccess(this.user, this.invite.projectId, cb),
  507. ],
  508. done
  509. )
  510. })
  511. describe('when the user recieves another invite to the same project', function () {
  512. it('should redirect to the project page', function (done) {
  513. Async.series(
  514. [
  515. cb => {
  516. createInvite(
  517. this.sendingUser,
  518. this.projectId,
  519. this.email,
  520. (err, invite) => {
  521. if (err) {
  522. throw err
  523. }
  524. this.secondInvite = invite
  525. this.secondLink = CollaboratorsEmailHandler._buildInviteUrl(
  526. this.fakeProject,
  527. invite
  528. )
  529. cb()
  530. }
  531. )
  532. },
  533. cb =>
  534. expectInviteRedirectToProject(
  535. this.user,
  536. this.secondLink,
  537. this.secondInvite,
  538. cb
  539. ),
  540. cb =>
  541. expectProjectAccess(this.user, this.invite.projectId, cb),
  542. cb =>
  543. revokeInvite(
  544. this.sendingUser,
  545. this.projectId,
  546. this.secondInvite._id,
  547. cb
  548. ),
  549. ],
  550. done
  551. )
  552. })
  553. })
  554. })
  555. })
  556. describe('user is not a member of the project', function () {
  557. it('should not grant access if the user does not accept the invite', function (done) {
  558. Async.series(
  559. [
  560. cb => expectInvitePage(this.user, this.link, cb),
  561. cb => expectNoProjectAccess(this.user, this.invite.projectId, cb),
  562. ],
  563. done
  564. )
  565. })
  566. it('should render the invalid-invite page if the token is invalid', function (done) {
  567. Async.series(
  568. [
  569. cb => {
  570. const link = this.link.replace(
  571. this.invite.token,
  572. 'not_a_real_token'
  573. )
  574. expectInvalidInvitePage(this.user, link, cb)
  575. },
  576. cb => expectNoProjectAccess(this.user, this.invite.projectId, cb),
  577. cb => expectNoProjectAccess(this.user, this.invite.projectId, cb),
  578. ],
  579. done
  580. )
  581. })
  582. it('should allow the user to accept the invite and access the project', function (done) {
  583. Async.series(
  584. [
  585. cb => expectInvitePage(this.user, this.link, cb),
  586. cb => expectAcceptInviteAndRedirect(this.user, this.invite, cb),
  587. cb => expectProjectAccess(this.user, this.invite.projectId, cb),
  588. ],
  589. done
  590. )
  591. })
  592. })
  593. })
  594. describe('user is not logged in initially', function () {
  595. describe('registration prompt workflow with valid token', function () {
  596. before(function () {
  597. if (!Features.hasFeature('public-registration')) {
  598. this.skip()
  599. }
  600. })
  601. it('should redirect to the register page', function (done) {
  602. expectInviteRedirectToRegister(this.user, this.link, done)
  603. })
  604. it('should allow user to accept the invite if the user registers a new account', function (done) {
  605. Async.series(
  606. [
  607. cb => expectInviteRedirectToRegister(this.user, this.link, cb),
  608. cb =>
  609. expectRegistrationRedirectToInvite(
  610. this.user,
  611. 'some_email@example.com',
  612. this.link,
  613. cb
  614. ),
  615. cb => expectInvitePage(this.user, this.link, cb),
  616. cb => expectAcceptInviteAndRedirect(this.user, this.invite, cb),
  617. cb => expectProjectAccess(this.user, this.invite.projectId, cb),
  618. ],
  619. done
  620. )
  621. })
  622. })
  623. describe('registration prompt workflow with non-valid token', function () {
  624. before(function () {
  625. if (!Features.hasFeature('public-registration')) {
  626. this.skip()
  627. }
  628. })
  629. it('should redirect to the register page', function (done) {
  630. Async.series(
  631. [
  632. cb => expectInviteRedirectToRegister(this.user, this.link, cb),
  633. cb => expectNoProjectAccess(this.user, this.invite.projectId, cb),
  634. ],
  635. done
  636. )
  637. })
  638. it('should display invalid-invite if the user registers a new account', function (done) {
  639. const badLink = this.link.replace(
  640. this.invite.token,
  641. 'not_a_real_token'
  642. )
  643. Async.series(
  644. [
  645. cb => expectInviteRedirectToRegister(this.user, badLink, cb),
  646. cb =>
  647. expectRegistrationRedirectToInvite(
  648. this.user,
  649. 'some_email@example.com',
  650. badLink,
  651. cb
  652. ),
  653. cb => expectInvalidInvitePage(this.user, badLink, cb),
  654. cb => expectNoProjectAccess(this.user, this.invite.projectId, cb),
  655. ],
  656. done
  657. )
  658. })
  659. })
  660. describe('login workflow with valid token', function () {
  661. it('should redirect to the register page', function (done) {
  662. Async.series(
  663. [
  664. cb => expectInviteRedirectToRegister(this.user, this.link, cb),
  665. cb => expectNoProjectAccess(this.user, this.invite.projectId, cb),
  666. ],
  667. done
  668. )
  669. })
  670. it('should allow the user to login to view the invite', function (done) {
  671. Async.series(
  672. [
  673. cb => expectInviteRedirectToRegister(this.user, this.link, cb),
  674. cb => expectLoginPage(this.user, cb),
  675. cb => expectLoginRedirectToInvite(this.user, this.link, cb),
  676. cb => expectInvitePage(this.user, this.link, cb),
  677. cb => expectNoProjectAccess(this.user, this.invite.projectId, cb),
  678. ],
  679. done
  680. )
  681. })
  682. it('should allow user to accept the invite if the user logs in', function (done) {
  683. Async.series(
  684. [
  685. cb => expectInviteRedirectToRegister(this.user, this.link, cb),
  686. cb => expectLoginPage(this.user, cb),
  687. cb => expectLoginRedirectToInvite(this.user, this.link, cb),
  688. cb => expectInvitePage(this.user, this.link, cb),
  689. cb => expectAcceptInviteAndRedirect(this.user, this.invite, cb),
  690. cb => expectProjectAccess(this.user, this.invite.projectId, cb),
  691. ],
  692. done
  693. )
  694. })
  695. })
  696. describe('login workflow with non-valid token', function () {
  697. it('should redirect to the register page', function (done) {
  698. Async.series(
  699. [
  700. cb => expectInviteRedirectToRegister(this.user, this.link, cb),
  701. cb => expectNoProjectAccess(this.user, this.invite.projectId, cb),
  702. ],
  703. done
  704. )
  705. })
  706. it('should show the invalid-invite page once the user has logged in', function (done) {
  707. const badLink = this.link.replace(
  708. this.invite.token,
  709. 'not_a_real_token'
  710. )
  711. Async.series(
  712. [
  713. cb => {
  714. return expectInviteRedirectToRegister(this.user, badLink, cb)
  715. },
  716. cb => {
  717. return expectLoginPage(this.user, cb)
  718. },
  719. cb => expectLoginRedirectToInvite(this.user, badLink, cb),
  720. cb => expectInvalidInvitePage(this.user, badLink, cb),
  721. cb => expectNoProjectAccess(this.user, this.invite.projectId, cb),
  722. ],
  723. done
  724. )
  725. })
  726. })
  727. })
  728. })
  729. })