ProjectInviteTests.mjs 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149
  1. import { expect } from 'chai'
  2. import Async from 'async'
  3. import User from './helpers/User.mjs'
  4. import Subscription from './helpers/Subscription.mjs'
  5. import settings from '@overleaf/settings'
  6. import CollaboratorsEmailHandler from '../../../app/src/Features/Collaborators/CollaboratorsEmailHandler.mjs'
  7. import CollaboratorsInviteHelper from '../../../app/src/Features/Collaborators/CollaboratorsInviteHelper.mjs'
  8. import Features from '../../../app/src/infrastructure/Features.mjs'
  9. import sinon from 'sinon'
  10. let generateTokenSpy
  11. const createInvite = (sendingUser, projectId, email, callback) => {
  12. sendingUser.getCsrfToken(err => {
  13. if (err) {
  14. return callback(err)
  15. }
  16. sendingUser.request.post(
  17. {
  18. uri: `/project/${projectId}/invite`,
  19. json: {
  20. email,
  21. privileges: 'readAndWrite',
  22. },
  23. },
  24. (err, response, body) => {
  25. if (err) {
  26. return callback(err)
  27. }
  28. expect(response.statusCode).to.equal(200)
  29. expect(body.error).to.not.exist
  30. expect(body.invite).to.exist
  31. callback(null, body.invite)
  32. }
  33. )
  34. })
  35. }
  36. const createProject = (owner, projectName, callback) => {
  37. owner.createProject(projectName, (err, projectId) => {
  38. if (err) {
  39. throw err
  40. }
  41. const fakeProject = {
  42. _id: projectId,
  43. name: projectName,
  44. owner_ref: owner,
  45. }
  46. callback(err, projectId, fakeProject)
  47. })
  48. }
  49. const createProjectAndInvite = (owner, projectName, email, callback) => {
  50. createProject(owner, projectName, (err, projectId, project) => {
  51. if (err) {
  52. return callback(err)
  53. }
  54. createInvite(owner, projectId, email, (err, invite) => {
  55. if (err) {
  56. return callback(err)
  57. }
  58. // attach the token to the invite
  59. invite.token = generateTokenSpy.getCall(0).returnValue
  60. const link = CollaboratorsEmailHandler._buildInviteUrl(project, invite)
  61. callback(null, project, invite, link)
  62. })
  63. })
  64. }
  65. const revokeInvite = (sendingUser, projectId, inviteId, callback) => {
  66. sendingUser.getCsrfToken(err => {
  67. if (err) {
  68. return callback(err)
  69. }
  70. sendingUser.request.delete(
  71. {
  72. uri: `/project/${projectId}/invite/${inviteId}`,
  73. },
  74. err => {
  75. if (err) {
  76. return callback(err)
  77. }
  78. callback()
  79. }
  80. )
  81. })
  82. }
  83. // Actions
  84. const tryFollowInviteLink = (user, link, callback) => {
  85. user.request.get(
  86. {
  87. uri: link,
  88. baseUrl: null,
  89. },
  90. callback
  91. )
  92. }
  93. const tryAcceptInvite = (user, invite, projectId, callback) => {
  94. user.getCsrfToken(err => {
  95. if (err) {
  96. return callback(err)
  97. }
  98. user.request.post(
  99. {
  100. uri: `/project/${projectId}/invite/token/${invite.token}/accept`,
  101. json: {
  102. token: invite.token,
  103. },
  104. },
  105. callback
  106. )
  107. })
  108. }
  109. const tryFollowLoginLink = (user, loginLink, callback) => {
  110. user.getCsrfToken(error => {
  111. if (error != null) {
  112. return callback(error)
  113. }
  114. user.request.get(loginLink, callback)
  115. })
  116. }
  117. const tryLoginUser = (user, callback) => {
  118. user.getCsrfToken(error => {
  119. if (error != null) {
  120. return callback(error)
  121. }
  122. user.request.post(
  123. {
  124. url: '/login',
  125. json: {
  126. email: user.email,
  127. password: user.password,
  128. 'g-recaptcha-response': 'valid',
  129. },
  130. },
  131. callback
  132. )
  133. })
  134. }
  135. const tryGetInviteList = (user, projectId, callback) => {
  136. user.getCsrfToken(error => {
  137. if (error != null) {
  138. return callback(error)
  139. }
  140. user.request.get(
  141. {
  142. url: `/project/${projectId}/invites`,
  143. json: true,
  144. },
  145. callback
  146. )
  147. })
  148. }
  149. const updateSharingLink = (
  150. sendingUser,
  151. projectId,
  152. privileges,
  153. opts,
  154. callback
  155. ) => {
  156. if (typeof opts === 'function') {
  157. callback = opts
  158. opts = {}
  159. }
  160. sendingUser.getCsrfToken(err => {
  161. if (err) {
  162. return callback(err)
  163. }
  164. sendingUser.request.post(
  165. {
  166. uri: `/project/${projectId}/sharing-link`,
  167. json: {
  168. privileges,
  169. ...opts,
  170. },
  171. },
  172. (err, response, body) => {
  173. if (err) {
  174. return callback(err)
  175. }
  176. expect(response.statusCode).to.equal(200)
  177. expect(body).to.include.keys('_id', 'token', 'privileges')
  178. callback(null, body)
  179. }
  180. )
  181. })
  182. }
  183. const getSharingLink = (sendingUser, projectId, callback) => {
  184. sendingUser.getCsrfToken(err => {
  185. if (err) {
  186. return callback(err)
  187. }
  188. sendingUser.request.get(
  189. {
  190. uri: `/project/${projectId}/sharing-link`,
  191. json: true,
  192. },
  193. (err, response, body) => {
  194. if (err) {
  195. return callback(err)
  196. }
  197. callback(null, response, body)
  198. }
  199. )
  200. })
  201. }
  202. const validateSharingLink = (user, projectId, token, callback) => {
  203. user.getCsrfToken(err => {
  204. if (err) {
  205. return callback(err)
  206. }
  207. user.request.post(
  208. {
  209. uri: `/project/${projectId}/share/validate`,
  210. json: { token },
  211. },
  212. (err, response, body) => {
  213. if (err) {
  214. return callback(err)
  215. }
  216. callback(null, response, body)
  217. }
  218. )
  219. })
  220. }
  221. const acceptSharingLink = (user, projectId, token, callback) => {
  222. user.getCsrfToken(err => {
  223. if (err) {
  224. return callback(err)
  225. }
  226. user.request.post(
  227. {
  228. uri: `/project/${projectId}/share`,
  229. json: {
  230. token,
  231. },
  232. },
  233. callback
  234. )
  235. })
  236. }
  237. const tryJoinProject = (user, projectId, callback) => {
  238. return user.getCsrfToken(error => {
  239. if (error != null) {
  240. return callback(error)
  241. }
  242. user.request.post(
  243. {
  244. url: `/project/${projectId}/join`,
  245. auth: {
  246. user: settings.apis.web.user,
  247. pass: settings.apis.web.pass,
  248. sendImmediately: true,
  249. },
  250. json: { userId: user._id },
  251. jar: false,
  252. },
  253. callback
  254. )
  255. })
  256. }
  257. // Expectations
  258. const expectProjectAccess = (user, projectId, callback) => {
  259. // should have access to project
  260. user.openProject(projectId, err => {
  261. expect(err).not.to.exist
  262. callback()
  263. })
  264. }
  265. const expectNoProjectAccess = (user, projectId, callback) => {
  266. // should not have access to project page
  267. user.openProject(projectId, err => {
  268. expect(err).to.be.instanceof(Error)
  269. callback()
  270. })
  271. }
  272. const expectInvitePage = (user, link, callback) => {
  273. // view invite
  274. tryFollowInviteLink(user, link, (err, response, body) => {
  275. expect(err).not.to.exist
  276. expect(response.statusCode).to.equal(200)
  277. expect(body).to.match(/<title[^>]*>Project Invite - .*<\/title>/)
  278. callback()
  279. })
  280. }
  281. const expectInvalidInvitePage = (user, link, callback) => {
  282. // view invalid invite
  283. tryFollowInviteLink(user, link, (err, response, body) => {
  284. expect(err).not.to.exist
  285. expect(response.statusCode).to.equal(404)
  286. expect(body).to.match(/<title[^>]*>Invalid Invite - .*<\/title>/)
  287. callback()
  288. })
  289. }
  290. const expectInviteRedirectToRegister = (user, link, callback) => {
  291. // view invite, redirect to `/register`
  292. tryFollowInviteLink(user, link, (err, response) => {
  293. expect(err).not.to.exist
  294. expect(response.statusCode).to.equal(302)
  295. expect(response.headers.location).to.match(/^\/register.*$/)
  296. user.getSession((err, session) => {
  297. if (err) return callback(err)
  298. expect(session.sharedProjectData).deep.equals({
  299. project_name: PROJECT_NAME,
  300. user_first_name: OWNER_NAME,
  301. })
  302. callback()
  303. })
  304. })
  305. }
  306. const expectLoginPage = (user, callback) => {
  307. tryFollowLoginLink(user, '/login', (err, response, body) => {
  308. expect(err).not.to.exist
  309. expect(response.statusCode).to.equal(200)
  310. expect(body).to.match(
  311. /<title[^>]*>(Login|Log in to Overleaf) - .*<\/title>/
  312. )
  313. callback()
  314. })
  315. }
  316. const expectLoginRedirectToInvite = (user, link, callback) => {
  317. tryLoginUser(user, (err, response) => {
  318. expect(err).not.to.exist
  319. expect(response.statusCode).to.equal(200)
  320. callback()
  321. })
  322. }
  323. const expectRegistrationRedirectToInvite = (user, link, callback) => {
  324. user.register((err, _user, response) => {
  325. expect(err).not.to.exist
  326. expect(response.statusCode).to.equal(200)
  327. if (response.body.redir === '/registration/try-premium') {
  328. user.request.get('/registration/onboarding', (err, response) => {
  329. if (err) return callback(err)
  330. expect(response.statusCode).to.equal(200)
  331. callback()
  332. })
  333. } else {
  334. expect(response.body.redir).to.equal(link)
  335. callback()
  336. }
  337. })
  338. }
  339. const expectInviteRedirectToProject = (user, link, projectId, callback) => {
  340. // view invite, redirect straight to project
  341. tryFollowInviteLink(user, link, (err, response) => {
  342. expect(err).not.to.exist
  343. expect(response.statusCode).to.equal(302)
  344. expect(response.headers.location).to.equal(`/project/${projectId}`)
  345. callback()
  346. })
  347. }
  348. const expectAcceptInviteAndRedirect = (user, invite, projectId, callback) => {
  349. // should accept the invite and redirect to project
  350. tryAcceptInvite(user, invite, projectId, (err, response) => {
  351. expect(err).not.to.exist
  352. expect(response.statusCode).to.equal(302)
  353. expect(response.headers.location).to.equal(`/project/${projectId}`)
  354. callback()
  355. })
  356. }
  357. const expectInviteListCount = (user, projectId, count, callback) => {
  358. tryGetInviteList(user, projectId, (err, response, body) => {
  359. expect(err).not.to.exist
  360. expect(response.statusCode).to.equal(200)
  361. expect(body).to.have.all.keys(['invites'])
  362. expect(body.invites.length).to.equal(count)
  363. callback()
  364. })
  365. }
  366. const expectInvitesInJoinProjectCount = (user, projectId, count, callback) => {
  367. tryJoinProject(user, projectId, (err, response, body) => {
  368. expect(err).not.to.exist
  369. expect(response.statusCode).to.equal(200)
  370. expect(body.project).to.contain.keys(['invites'])
  371. expect(body.project.invites.length).to.equal(count)
  372. callback()
  373. })
  374. }
  375. const PROJECT_NAME = 'project name for sharing test'
  376. const OWNER_NAME = 'sending user name'
  377. describe('ProjectInviteTests', function () {
  378. beforeEach(function (done) {
  379. this.sendingUser = new User()
  380. this.user = new User()
  381. this.site_admin = new User({ email: `admin+${Math.random()}@example.com` })
  382. this.email = `smoketestuser+${Math.random()}@example.com`
  383. generateTokenSpy = sinon.spy(CollaboratorsInviteHelper, 'generateToken')
  384. Async.series(
  385. [
  386. cb => this.sendingUser.ensureUserExists(cb),
  387. cb => this.sendingUser.upgradeFeatures({ collaborators: 10 }, cb),
  388. cb => this.sendingUser.login(cb),
  389. cb =>
  390. this.sendingUser.mongoUpdate(
  391. {
  392. $set: { first_name: OWNER_NAME },
  393. },
  394. cb
  395. ),
  396. ],
  397. done
  398. )
  399. })
  400. afterEach(function () {
  401. generateTokenSpy.restore()
  402. })
  403. describe('creating invites', function () {
  404. describe('creating two invites', function () {
  405. beforeEach(function (done) {
  406. createProject(
  407. this.sendingUser,
  408. PROJECT_NAME,
  409. (err, projectId, project) => {
  410. expect(err).not.to.exist
  411. this.projectId = projectId
  412. this.fakeProject = project
  413. done()
  414. }
  415. )
  416. })
  417. it('should fail if email is not a string', function (done) {
  418. this.sendingUser.getCsrfToken(err => {
  419. if (err) {
  420. return done(err)
  421. }
  422. this.sendingUser.request.post(
  423. {
  424. uri: `/project/${this.projectId}/invite`,
  425. json: {
  426. email: {},
  427. privileges: 'readAndWrite',
  428. },
  429. },
  430. (err, response, body) => {
  431. if (err) {
  432. return done(err)
  433. }
  434. expect(response.statusCode).to.equal(400)
  435. done()
  436. }
  437. )
  438. })
  439. })
  440. it('should fail on invalid privileges', function (done) {
  441. this.sendingUser.getCsrfToken(err => {
  442. if (err) {
  443. return done(err)
  444. }
  445. this.sendingUser.request.post(
  446. {
  447. uri: `/project/${this.projectId}/invite`,
  448. json: {
  449. email: this.email,
  450. privileges: 'invalid-privilege',
  451. },
  452. },
  453. (err, response, body) => {
  454. if (err) {
  455. return done(err)
  456. }
  457. expect(response.statusCode).to.equal(400)
  458. done()
  459. }
  460. )
  461. })
  462. })
  463. it('should allow the project owner to create and remove invites', function (done) {
  464. Async.series(
  465. [
  466. cb => expectProjectAccess(this.sendingUser, this.projectId, cb),
  467. cb =>
  468. expectInviteListCount(this.sendingUser, this.projectId, 0, cb),
  469. // create invite, check invite list count
  470. cb => {
  471. createInvite(
  472. this.sendingUser,
  473. this.projectId,
  474. this.email,
  475. (err, invite) => {
  476. if (err) {
  477. return cb(err)
  478. }
  479. this.invite = invite
  480. cb()
  481. }
  482. )
  483. },
  484. cb =>
  485. expectInviteListCount(this.sendingUser, this.projectId, 1, cb),
  486. cb =>
  487. revokeInvite(
  488. this.sendingUser,
  489. this.projectId,
  490. this.invite._id,
  491. cb
  492. ),
  493. cb =>
  494. expectInviteListCount(this.sendingUser, this.projectId, 0, cb),
  495. // and a second time
  496. cb => {
  497. createInvite(
  498. this.sendingUser,
  499. this.projectId,
  500. this.email,
  501. (err, invite) => {
  502. if (err) {
  503. return cb(err)
  504. }
  505. this.invite = invite
  506. cb()
  507. }
  508. )
  509. },
  510. cb =>
  511. expectInviteListCount(this.sendingUser, this.projectId, 1, cb),
  512. // check the joinProject view
  513. cb =>
  514. expectInvitesInJoinProjectCount(
  515. this.sendingUser,
  516. this.projectId,
  517. 1,
  518. cb
  519. ),
  520. // revoke invite
  521. cb =>
  522. revokeInvite(
  523. this.sendingUser,
  524. this.projectId,
  525. this.invite._id,
  526. cb
  527. ),
  528. cb =>
  529. expectInviteListCount(this.sendingUser, this.projectId, 0, cb),
  530. cb =>
  531. expectInvitesInJoinProjectCount(
  532. this.sendingUser,
  533. this.projectId,
  534. 0,
  535. cb
  536. ),
  537. ],
  538. done
  539. )
  540. })
  541. it('should allow the project owner to create many invites at once', function (done) {
  542. Async.series(
  543. [
  544. cb => expectProjectAccess(this.sendingUser, this.projectId, cb),
  545. cb =>
  546. expectInviteListCount(this.sendingUser, this.projectId, 0, cb),
  547. // create first invite
  548. cb => {
  549. createInvite(
  550. this.sendingUser,
  551. this.projectId,
  552. this.email,
  553. (err, invite) => {
  554. if (err) {
  555. return cb(err)
  556. }
  557. this.inviteOne = invite
  558. cb()
  559. }
  560. )
  561. },
  562. cb =>
  563. expectInviteListCount(this.sendingUser, this.projectId, 1, cb),
  564. // and a second
  565. cb => {
  566. createInvite(
  567. this.sendingUser,
  568. this.projectId,
  569. this.email,
  570. (err, invite) => {
  571. if (err) {
  572. return cb(err)
  573. }
  574. this.inviteTwo = invite
  575. cb()
  576. }
  577. )
  578. },
  579. // should have two
  580. cb =>
  581. expectInviteListCount(this.sendingUser, this.projectId, 2, cb),
  582. cb =>
  583. expectInvitesInJoinProjectCount(
  584. this.sendingUser,
  585. this.projectId,
  586. 2,
  587. cb
  588. ),
  589. // revoke first
  590. cb =>
  591. revokeInvite(
  592. this.sendingUser,
  593. this.projectId,
  594. this.inviteOne._id,
  595. cb
  596. ),
  597. cb =>
  598. expectInviteListCount(this.sendingUser, this.projectId, 1, cb),
  599. // revoke second
  600. cb =>
  601. revokeInvite(
  602. this.sendingUser,
  603. this.projectId,
  604. this.inviteTwo._id,
  605. cb
  606. ),
  607. cb =>
  608. expectInviteListCount(this.sendingUser, this.projectId, 0, cb),
  609. ],
  610. done
  611. )
  612. })
  613. })
  614. })
  615. describe('clicking the invite link', function () {
  616. beforeEach(function (done) {
  617. createProjectAndInvite(
  618. this.sendingUser,
  619. PROJECT_NAME,
  620. this.email,
  621. (err, project, invite, link) => {
  622. expect(err).not.to.exist
  623. this.projectId = project._id
  624. this.fakeProject = project
  625. this.invite = invite
  626. this.link = link
  627. done()
  628. }
  629. )
  630. })
  631. describe('user is logged in already', function () {
  632. beforeEach(function (done) {
  633. this.user.login(done)
  634. })
  635. describe('user is already a member of the project', function () {
  636. beforeEach(function (done) {
  637. Async.series(
  638. [
  639. cb => expectInvitePage(this.user, this.link, cb),
  640. cb =>
  641. expectAcceptInviteAndRedirect(
  642. this.user,
  643. this.invite,
  644. this.projectId,
  645. cb
  646. ),
  647. cb => expectProjectAccess(this.user, this.projectId, cb),
  648. ],
  649. done
  650. )
  651. })
  652. describe('when user clicks on the invite a second time', function () {
  653. it('should just redirect to the project page', function (done) {
  654. Async.series(
  655. [
  656. cb => expectProjectAccess(this.user, this.projectId, cb),
  657. cb =>
  658. expectInviteRedirectToProject(
  659. this.user,
  660. this.link,
  661. this.projectId,
  662. cb
  663. ),
  664. cb => expectProjectAccess(this.user, this.projectId, cb),
  665. ],
  666. done
  667. )
  668. })
  669. describe('when the user recieves another invite to the same project', function () {
  670. it('should redirect to the project page', function (done) {
  671. Async.series(
  672. [
  673. cb => {
  674. createInvite(
  675. this.sendingUser,
  676. this.projectId,
  677. this.email,
  678. (err, invite) => {
  679. if (err) {
  680. throw err
  681. }
  682. this.secondInvite = invite
  683. const token = generateTokenSpy.getCall(1).returnValue
  684. this.secondLink =
  685. CollaboratorsEmailHandler._buildInviteUrl(
  686. this.fakeProject,
  687. { ...invite, token }
  688. )
  689. cb()
  690. }
  691. )
  692. },
  693. cb =>
  694. expectInviteRedirectToProject(
  695. this.user,
  696. this.secondLink,
  697. this.projectId,
  698. cb
  699. ),
  700. cb => expectProjectAccess(this.user, this.projectId, cb),
  701. cb =>
  702. revokeInvite(
  703. this.sendingUser,
  704. this.projectId,
  705. this.secondInvite._id,
  706. cb
  707. ),
  708. ],
  709. done
  710. )
  711. })
  712. })
  713. })
  714. })
  715. describe('user is not a member of the project', function () {
  716. it('should not grant access if the user does not accept the invite', function (done) {
  717. Async.series(
  718. [
  719. cb => expectInvitePage(this.user, this.link, cb),
  720. cb => expectNoProjectAccess(this.user, this.projectId, cb),
  721. ],
  722. done
  723. )
  724. })
  725. it('should render the invalid-invite page if the token is invalid', function (done) {
  726. Async.series(
  727. [
  728. cb => {
  729. const link = this.link.replace(
  730. this.invite.token,
  731. 'not_a_real_token'
  732. )
  733. expectInvalidInvitePage(this.user, link, cb)
  734. },
  735. cb => expectNoProjectAccess(this.user, this.projectId, cb),
  736. cb => expectNoProjectAccess(this.user, this.projectId, cb),
  737. ],
  738. done
  739. )
  740. })
  741. it('should allow the user to accept the invite and access the project', function (done) {
  742. Async.series(
  743. [
  744. cb => expectInvitePage(this.user, this.link, cb),
  745. cb =>
  746. expectAcceptInviteAndRedirect(
  747. this.user,
  748. this.invite,
  749. this.projectId,
  750. cb
  751. ),
  752. cb => expectProjectAccess(this.user, this.projectId, cb),
  753. ],
  754. done
  755. )
  756. })
  757. })
  758. })
  759. describe('user is not logged in initially', function () {
  760. describe('registration prompt workflow with valid token', function () {
  761. before(function () {
  762. if (!Features.hasFeature('registration')) {
  763. this.skip()
  764. }
  765. })
  766. it('should redirect to the register page', function (done) {
  767. expectInviteRedirectToRegister(this.user, this.link, done)
  768. })
  769. it('should allow user to accept the invite if the user registers a new account', function (done) {
  770. Async.series(
  771. [
  772. cb => expectInviteRedirectToRegister(this.user, this.link, cb),
  773. cb =>
  774. expectRegistrationRedirectToInvite(this.user, this.link, cb),
  775. cb => expectInvitePage(this.user, this.link, cb),
  776. cb =>
  777. expectAcceptInviteAndRedirect(
  778. this.user,
  779. this.invite,
  780. this.projectId,
  781. cb
  782. ),
  783. cb => expectProjectAccess(this.user, this.projectId, cb),
  784. ],
  785. done
  786. )
  787. })
  788. })
  789. describe('registration prompt workflow with non-valid token', function () {
  790. before(function () {
  791. if (!Features.hasFeature('registration')) {
  792. this.skip()
  793. }
  794. })
  795. it('should redirect to the register page', function (done) {
  796. Async.series(
  797. [
  798. cb => expectInviteRedirectToRegister(this.user, this.link, cb),
  799. cb => expectNoProjectAccess(this.user, this.projectId, cb),
  800. ],
  801. done
  802. )
  803. })
  804. it('should display invalid-invite right away', function (done) {
  805. const badLink = this.link.replace(
  806. this.invite.token,
  807. 'not_a_real_token'
  808. )
  809. Async.series(
  810. [
  811. cb => expectInvalidInvitePage(this.user, badLink, cb),
  812. cb => expectNoProjectAccess(this.user, this.projectId, cb),
  813. ],
  814. done
  815. )
  816. })
  817. })
  818. describe('login workflow with valid token', function () {
  819. beforeEach(function (done) {
  820. this.user.ensureUserExists(done)
  821. })
  822. it('should redirect to the register page', function (done) {
  823. Async.series(
  824. [
  825. cb => expectInviteRedirectToRegister(this.user, this.link, cb),
  826. cb => expectNoProjectAccess(this.user, this.projectId, cb),
  827. ],
  828. done
  829. )
  830. })
  831. it('should allow the user to login to view the invite', function (done) {
  832. Async.series(
  833. [
  834. cb => expectInviteRedirectToRegister(this.user, this.link, cb),
  835. cb => expectLoginPage(this.user, cb),
  836. cb => expectLoginRedirectToInvite(this.user, this.link, cb),
  837. cb => expectInvitePage(this.user, this.link, cb),
  838. cb => expectNoProjectAccess(this.user, this.projectId, cb),
  839. ],
  840. done
  841. )
  842. })
  843. it('should allow user to accept the invite if the user logs in', function (done) {
  844. Async.series(
  845. [
  846. cb => expectInviteRedirectToRegister(this.user, this.link, cb),
  847. cb => expectLoginPage(this.user, cb),
  848. cb => expectLoginRedirectToInvite(this.user, this.link, cb),
  849. cb => expectInvitePage(this.user, this.link, cb),
  850. cb =>
  851. expectAcceptInviteAndRedirect(
  852. this.user,
  853. this.invite,
  854. this.projectId,
  855. cb
  856. ),
  857. cb => expectProjectAccess(this.user, this.projectId, cb),
  858. ],
  859. done
  860. )
  861. })
  862. })
  863. describe('login workflow with non-valid token', function () {
  864. it('should redirect to the register page', function (done) {
  865. Async.series(
  866. [
  867. cb => expectInviteRedirectToRegister(this.user, this.link, cb),
  868. cb => expectNoProjectAccess(this.user, this.projectId, cb),
  869. ],
  870. done
  871. )
  872. })
  873. it('should show the invalid-invite page right away', function (done) {
  874. const badLink = this.link.replace(
  875. this.invite.token,
  876. 'not_a_real_token'
  877. )
  878. Async.series(
  879. [
  880. cb => expectInvalidInvitePage(this.user, badLink, cb),
  881. cb => expectNoProjectAccess(this.user, this.projectId, cb),
  882. ],
  883. done
  884. )
  885. })
  886. })
  887. })
  888. })
  889. describe('sharing link routes', function () {
  890. beforeEach(function (done) {
  891. this.projectName = `sharing-link-test-${Math.random()}`
  892. createProject(this.sendingUser, this.projectName, (err, projectId) => {
  893. if (err) {
  894. return done(err)
  895. }
  896. this.projectId = projectId
  897. done()
  898. })
  899. })
  900. it('should create and fetch sharing link without listing reusable invite in invite list', function (done) {
  901. Async.series(
  902. [
  903. cb => expectInviteListCount(this.sendingUser, this.projectId, 0, cb),
  904. cb => {
  905. updateSharingLink(
  906. this.sendingUser,
  907. this.projectId,
  908. 'readOnly',
  909. (err, link) => {
  910. if (err) {
  911. return cb(err)
  912. }
  913. this.sharingLink = link
  914. expect(link.privileges).to.equal('readOnly')
  915. cb()
  916. }
  917. )
  918. },
  919. cb => expectInviteListCount(this.sendingUser, this.projectId, 0, cb),
  920. cb => {
  921. getSharingLink(
  922. this.sendingUser,
  923. this.projectId,
  924. (err, response, body) => {
  925. if (err) {
  926. return cb(err)
  927. }
  928. expect(response.statusCode).to.equal(200)
  929. expect(body.privileges).to.equal('readOnly')
  930. expect(body.token).to.equal(this.sharingLink.token)
  931. cb()
  932. }
  933. )
  934. },
  935. ],
  936. done
  937. )
  938. })
  939. it('should validate and accept sharing link via /share flow', function (done) {
  940. Async.series(
  941. [
  942. cb => this.user.login(cb),
  943. cb => {
  944. updateSharingLink(
  945. this.sendingUser,
  946. this.projectId,
  947. 'readOnly',
  948. (err, link) => {
  949. if (err) {
  950. return cb(err)
  951. }
  952. this.sharingLink = link
  953. cb()
  954. }
  955. )
  956. },
  957. cb => expectNoProjectAccess(this.user, this.projectId, cb),
  958. cb => {
  959. validateSharingLink(
  960. this.user,
  961. this.projectId,
  962. this.sharingLink.token,
  963. (err, response, body) => {
  964. if (err) {
  965. return cb(err)
  966. }
  967. expect(response.statusCode).to.equal(200)
  968. expect(body.valid).to.equal(true)
  969. cb()
  970. }
  971. )
  972. },
  973. cb => {
  974. acceptSharingLink(
  975. this.user,
  976. this.projectId,
  977. this.sharingLink.token,
  978. (err, response) => {
  979. if (err) {
  980. return cb(err)
  981. }
  982. expect(response.statusCode).to.equal(302)
  983. expect(response.headers.location).to.equal(
  984. `/project/${this.projectId}`
  985. )
  986. cb()
  987. }
  988. )
  989. },
  990. cb => expectProjectAccess(this.user, this.projectId, cb),
  991. ],
  992. done
  993. )
  994. })
  995. it('should reject invalid sharing link token in validation', function (done) {
  996. Async.series(
  997. [
  998. cb => this.user.login(cb),
  999. cb => {
  1000. validateSharingLink(
  1001. this.user,
  1002. this.projectId,
  1003. 'not-a-valid-token',
  1004. (err, response, body) => {
  1005. if (err) {
  1006. return cb(err)
  1007. }
  1008. expect(response.statusCode).to.equal(200)
  1009. expect(body.valid).to.equal(false)
  1010. cb()
  1011. }
  1012. )
  1013. },
  1014. ],
  1015. done
  1016. )
  1017. })
  1018. it('should reject validation for user not in required subscription group', function (done) {
  1019. if (!Features.hasFeature('saas')) {
  1020. this.skip()
  1021. }
  1022. const subscription = new Subscription({
  1023. adminId: this.sendingUser._id,
  1024. memberIds: [this.sendingUser._id],
  1025. groupPlan: true,
  1026. planCode: 'group_professional',
  1027. paymentProvider: { state: 'active' },
  1028. })
  1029. let sharingLink
  1030. Async.series(
  1031. [
  1032. cb => this.user.login(cb),
  1033. cb => subscription.ensureExists(cb),
  1034. cb => {
  1035. updateSharingLink(
  1036. this.sendingUser,
  1037. this.projectId,
  1038. 'readOnly',
  1039. { subscriptionId: subscription._id.toString() },
  1040. (err, link) => {
  1041. if (err) {
  1042. return cb(err)
  1043. }
  1044. sharingLink = link
  1045. cb()
  1046. }
  1047. )
  1048. },
  1049. cb => {
  1050. validateSharingLink(
  1051. this.user,
  1052. this.projectId,
  1053. sharingLink.token,
  1054. (err, response, body) => {
  1055. if (err) {
  1056. return cb(err)
  1057. }
  1058. expect(response.statusCode).to.equal(200)
  1059. expect(body.valid).to.equal(false)
  1060. cb()
  1061. }
  1062. )
  1063. },
  1064. cb => subscription.addMember(this.user._id, cb),
  1065. cb => {
  1066. validateSharingLink(
  1067. this.user,
  1068. this.projectId,
  1069. sharingLink.token,
  1070. (err, response, body) => {
  1071. if (err) {
  1072. return cb(err)
  1073. }
  1074. expect(response.statusCode).to.equal(200)
  1075. expect(body.valid).to.equal(true)
  1076. cb()
  1077. }
  1078. )
  1079. },
  1080. ],
  1081. done
  1082. )
  1083. })
  1084. })
  1085. })