ProjectInviteTests.js 22 KB

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