ProjectInviteTests.js 22 KB

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