project-sharing.spec.ts 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540
  1. import { v4 as uuid } from 'uuid'
  2. import {
  3. isExcludedBySharding,
  4. startWith,
  5. reloadWith,
  6. STARTUP_TIMEOUT,
  7. } from './helpers/config'
  8. import { ensureUserExists, login } from './helpers/login'
  9. import {
  10. createProjectAndOpenInNewEditor,
  11. enableLinkSharing,
  12. getSpamSafeProjectName,
  13. openProjectByName,
  14. openProjectViaLinkSharingAsAnon,
  15. openProjectViaLinkSharingAsUser,
  16. shareProjectByEmailAndAcceptInviteViaDash,
  17. shareProjectByEmailAndAcceptInviteViaEmail,
  18. } from './helpers/project'
  19. import { prepareWaitForNextCompileSlot } from './helpers/compile'
  20. import { beforeWithReRunOnTestRetry } from './helpers/beforeWithReRunOnTestRetry'
  21. describe('Project Sharing', function () {
  22. if (isExcludedBySharding('PRO_CUSTOM_4')) return
  23. ensureUserExists({ email: 'user@example.com' })
  24. startWith({ withDataDir: true, pro: true })
  25. let projectName: string
  26. let recompile: () => void
  27. let waitForCompile: (triggerCompile: () => void) => void
  28. // Link-sharing urls have to be created before disabling link sharing.
  29. // We use the `beforeEach` hook to reload the server with link sharing
  30. // disabled **after** the initial setup which happens in the `before`
  31. // block. The `before` hook always runs prior to the `beforeEach` hook.
  32. // Set up retained access before disabling link sharing
  33. beforeWithReRunOnTestRetry(() => {
  34. // undo the link sharing restriction before joining via link-sharing below
  35. cy.wrap(reloadWith({ withDataDir: true, pro: true }), {
  36. timeout: STARTUP_TIMEOUT,
  37. })
  38. projectName = getSpamSafeProjectName()
  39. ;({ recompile, waitForCompile } = prepareWaitForNextCompileSlot())
  40. setupTestProject()
  41. })
  42. beforeEach(function () {
  43. // Always start with a fresh session
  44. cy.session([uuid()], () => {})
  45. })
  46. let linkSharingReadOnly: string
  47. let linkSharingReadAndWrite: string
  48. function setupTestProject() {
  49. login('user@example.com')
  50. waitForCompile(() => {
  51. createProjectAndOpenInNewEditor(projectName)
  52. })
  53. // Add chat message
  54. cy.findByRole('tab', { name: 'Chat' }).click()
  55. // wait for lazy loading of the chat pane
  56. cy.findByText('Start the conversation by saying hello or sharing an update')
  57. cy.get(
  58. 'textarea[placeholder="Send a message to your collaborators…"]'
  59. ).type('New Chat Message{enter}')
  60. // Get link sharing links
  61. enableLinkSharing().then(
  62. ({ linkSharingReadOnly: ro, linkSharingReadAndWrite: rw }) => {
  63. linkSharingReadAndWrite = rw
  64. linkSharingReadOnly = ro
  65. }
  66. )
  67. }
  68. function expectContentReadOnlyAccess() {
  69. cy.url().should('match', /\/project\/[a-fA-F0-9]{24}/)
  70. cy.findByRole('textbox', { name: 'Source Editor editing' }).should(
  71. 'contain.text',
  72. '\\maketitle'
  73. )
  74. cy.findByRole('textbox', { name: 'Source Editor editing' }).should(
  75. 'have.attr',
  76. 'contenteditable',
  77. 'false'
  78. )
  79. }
  80. function expectContentWriteAccess() {
  81. const section = `Test Section ${uuid()}`
  82. cy.url().should('match', /\/project\/[a-fA-F0-9]{24}/)
  83. // wait for the editor to finish loading
  84. cy.findByRole('textbox', { name: 'Source Editor editing' }).should(
  85. 'contain.text',
  86. '\\maketitle'
  87. )
  88. // the editor should be writable
  89. cy.findByRole('textbox', { name: 'Source Editor editing' }).should(
  90. 'have.attr',
  91. 'contenteditable',
  92. 'true'
  93. )
  94. cy.findByRole('textbox', { name: 'Source Editor editing' }).within(() => {
  95. cy.findByText('\\maketitle').parent().click()
  96. cy.findByText('\\maketitle').parent().type(`\n\\section{{}${section}}`)
  97. })
  98. // should have written
  99. cy.findByRole('textbox', { name: 'Source Editor editing' }).should(
  100. 'contain.text',
  101. `\\section{${section}}`
  102. )
  103. // check PDF
  104. recompile()
  105. cy.findByRole('region', { name: 'PDF preview' }).within(() => {
  106. cy.findByLabelText(/Page.*1/i).should('be.visible')
  107. cy.findByText(projectName).should('be.visible')
  108. })
  109. cy.findByRole('region', { name: 'PDF preview' }).within(() => {
  110. cy.findByLabelText(/Page.*1/i).should('be.visible')
  111. cy.contains(section)
  112. })
  113. }
  114. function expectNoAccess() {
  115. // try read only access link
  116. cy.visit(linkSharingReadOnly)
  117. cy.url().should('match', /\/login/)
  118. // Cypress bugs: cypress resolves the link-sharing link outside the browser, and it carries over the hash of the link-sharing link to the login page redirect (bug 1).
  119. // Effectively, cypress then instructs the browser to change the page from /login#read-only-hash to /login#read-and-write-hash.
  120. // This is turn does not trigger a "page load", but rather just "scrolling", which in turn trips up the "page loaded" detection in cypress (bug 2).
  121. // Work around this by navigating away from the /login page in between checks.
  122. cy.visit('/user/password/reset')
  123. // try read and write access link
  124. cy.visit(linkSharingReadAndWrite)
  125. cy.url().should('match', /\/login/)
  126. }
  127. function expectChatAccess() {
  128. cy.findByRole('tab', { name: 'Chat' }).click()
  129. cy.findByText('New Chat Message')
  130. }
  131. function expectHistoryAccess() {
  132. cy.findByRole('button', { name: 'History' }).click()
  133. // The input is not clickable due to being visually hidden, click its label instead
  134. cy.findByRole('complementary', {
  135. name: 'Project history and labels',
  136. }).within(() => {
  137. cy.findByRole('group', {
  138. name: 'Show all of the project history or only labelled versions.',
  139. }).within(() => {
  140. cy.findByText('All history').click()
  141. })
  142. cy.findByRole('radio', { name: 'Labels' }).should('not.be.checked')
  143. cy.findByRole('radio', { name: 'All history' }).should('be.checked')
  144. })
  145. cy.findByText(/\\begin\{document}/)
  146. cy.findByRole('complementary', {
  147. name: 'Project history and labels',
  148. }).within(() => {
  149. cy.findAllByTestId('history-version-metadata-users')
  150. .last()
  151. .should('have.text', 'user')
  152. })
  153. cy.findByRole('button', { name: 'Back to editor' }).click()
  154. }
  155. function expectNoChatAccess() {
  156. cy.findByRole('button', { name: 'Layout options' }) // wait for lazy loading
  157. cy.findByRole('tab', { name: 'Chat' }).should('not.exist')
  158. }
  159. function expectNoHistoryAccess() {
  160. cy.findByRole('button', { name: 'Layout options' }) // wait for lazy loading
  161. cy.findByRole('button', { name: 'History' }).should('not.exist')
  162. }
  163. function expectCommentAccess() {
  164. cy.findByRole('textbox', { name: 'Source Editor editing' }).should(
  165. 'contain.text',
  166. '\\maketitle'
  167. )
  168. cy.findByText('\\maketitle').parent().dblclick()
  169. cy.findByRole('button', { name: 'Add comment' }).should('be.visible')
  170. cy.findByRole('textbox', { name: 'Source Editor editing' }).click()
  171. }
  172. function expectNoCommentAccess() {
  173. cy.findByRole('textbox', { name: 'Source Editor editing' }).should(
  174. 'contain.text',
  175. '\\maketitle'
  176. )
  177. cy.findByText('\\maketitle').parent().dblclick()
  178. cy.findByRole('button', { name: 'Add comment' }).should('not.exist')
  179. cy.findByRole('textbox', { name: 'Source Editor editing' }).click()
  180. }
  181. function expectFullReadOnlyAccess() {
  182. expectContentReadOnlyAccess()
  183. expectChatAccess()
  184. expectHistoryAccess()
  185. expectNoCommentAccess()
  186. }
  187. function expectRestrictedReadOnlyAccess() {
  188. expectContentReadOnlyAccess()
  189. expectNoChatAccess()
  190. expectNoHistoryAccess()
  191. expectNoCommentAccess()
  192. }
  193. function expectFullReadAndWriteAccess() {
  194. expectContentWriteAccess()
  195. expectChatAccess()
  196. expectHistoryAccess()
  197. expectCommentAccess()
  198. }
  199. function expectAnonymousReadAndWriteAccess() {
  200. expectContentWriteAccess()
  201. expectChatAccess()
  202. expectHistoryAccess()
  203. expectNoCommentAccess()
  204. }
  205. function expectProjectDashboardEntry() {
  206. cy.visit('/project')
  207. cy.findByText(projectName)
  208. }
  209. function expectEditAuthoredAs(author: string) {
  210. cy.findByRole('button', { name: 'History' }).click()
  211. cy.findByRole('complementary', {
  212. name: 'Project history and labels',
  213. }).within(() => {
  214. cy.findAllByTestId('history-version-metadata-users')
  215. .first()
  216. .should('contain.text', author) // might have other edits in the same group
  217. })
  218. }
  219. describe('via email', function () {
  220. const email = 'collaborator-email@example.com'
  221. ensureUserExists({ email })
  222. beforeEach(function () {
  223. login('user@example.com')
  224. shareProjectByEmailAndAcceptInviteViaEmail(
  225. projectName,
  226. email,
  227. 'Viewer',
  228. true
  229. )
  230. })
  231. it('should grant the collaborator read access', function () {
  232. expectFullReadOnlyAccess()
  233. expectProjectDashboardEntry()
  234. })
  235. })
  236. describe('read only', function () {
  237. const email = 'collaborator-ro@example.com'
  238. ensureUserExists({ email })
  239. beforeWithReRunOnTestRetry(() => {
  240. login('user@example.com')
  241. shareProjectByEmailAndAcceptInviteViaDash(
  242. projectName,
  243. email,
  244. 'Viewer',
  245. true
  246. )
  247. })
  248. it('should grant the collaborator read access', function () {
  249. login(email)
  250. openProjectByName(projectName, true)
  251. expectFullReadOnlyAccess()
  252. expectProjectDashboardEntry()
  253. })
  254. })
  255. describe('read and write', function () {
  256. const email = 'collaborator-rw@example.com'
  257. ensureUserExists({ email })
  258. beforeWithReRunOnTestRetry(() => {
  259. login('user@example.com')
  260. shareProjectByEmailAndAcceptInviteViaDash(
  261. projectName,
  262. email,
  263. 'Editor',
  264. true
  265. )
  266. })
  267. it('should grant the collaborator write access', function () {
  268. login(email)
  269. openProjectByName(projectName, true)
  270. expectFullReadAndWriteAccess()
  271. expectEditAuthoredAs('You')
  272. expectProjectDashboardEntry()
  273. })
  274. })
  275. describe('token access', function () {
  276. describe('logged in', function () {
  277. describe('read only', function () {
  278. const email = 'collaborator-link-ro@example.com'
  279. ensureUserExists({ email })
  280. it('should grant restricted read access', function () {
  281. login(email)
  282. openProjectViaLinkSharingAsUser(
  283. linkSharingReadOnly,
  284. projectName,
  285. email,
  286. true
  287. )
  288. expectRestrictedReadOnlyAccess()
  289. expectProjectDashboardEntry()
  290. })
  291. })
  292. describe('read and write', function () {
  293. const email = 'collaborator-link-rw@example.com'
  294. ensureUserExists({ email })
  295. it('should grant full write access', function () {
  296. login(email)
  297. openProjectViaLinkSharingAsUser(
  298. linkSharingReadAndWrite,
  299. projectName,
  300. email,
  301. true
  302. )
  303. expectFullReadAndWriteAccess()
  304. expectEditAuthoredAs('You')
  305. expectProjectDashboardEntry()
  306. })
  307. })
  308. })
  309. describe('with OVERLEAF_ALLOW_PUBLIC_ACCESS=false', function () {
  310. describe('wrap startup', function () {
  311. startWith({
  312. pro: true,
  313. vars: {
  314. OVERLEAF_ALLOW_PUBLIC_ACCESS: 'false',
  315. },
  316. withDataDir: true,
  317. })
  318. it('should block access', function () {
  319. expectNoAccess()
  320. })
  321. })
  322. describe('with OVERLEAF_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING=true', function () {
  323. startWith({
  324. pro: true,
  325. vars: {
  326. OVERLEAF_ALLOW_PUBLIC_ACCESS: 'false',
  327. OVERLEAF_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING: 'true',
  328. },
  329. withDataDir: true,
  330. })
  331. it('should block access', function () {
  332. expectNoAccess()
  333. })
  334. })
  335. })
  336. describe('with OVERLEAF_ALLOW_PUBLIC_ACCESS=true', function () {
  337. describe('wrap startup', function () {
  338. startWith({
  339. pro: true,
  340. vars: {
  341. OVERLEAF_ALLOW_PUBLIC_ACCESS: 'true',
  342. },
  343. withDataDir: true,
  344. })
  345. it('should grant read access with read link', function () {
  346. openProjectViaLinkSharingAsAnon(linkSharingReadOnly, true)
  347. expectRestrictedReadOnlyAccess()
  348. })
  349. it('should prompt for login with write link', function () {
  350. cy.visit(linkSharingReadAndWrite)
  351. cy.url().should('match', /\/login/)
  352. })
  353. })
  354. describe('with OVERLEAF_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING=true', function () {
  355. startWith({
  356. pro: true,
  357. vars: {
  358. OVERLEAF_ALLOW_PUBLIC_ACCESS: 'true',
  359. OVERLEAF_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING: 'true',
  360. },
  361. withDataDir: true,
  362. })
  363. it('should grant read access with read link', function () {
  364. openProjectViaLinkSharingAsAnon(linkSharingReadOnly, true)
  365. expectRestrictedReadOnlyAccess()
  366. })
  367. it('should grant write access with write link', function () {
  368. openProjectViaLinkSharingAsAnon(linkSharingReadAndWrite, true)
  369. expectAnonymousReadAndWriteAccess()
  370. expectEditAuthoredAs('Anonymous')
  371. })
  372. })
  373. })
  374. describe('with OVERLEAF_DISABLE_LINK_SHARING=true', function () {
  375. const email = 'collaborator-email@example.com'
  376. ensureUserExists({ email })
  377. const invitedEmail = 'invited-email@example.com'
  378. ensureUserExists({ email: invitedEmail })
  379. const retainedViewerEmail = 'collaborator-retained-viewer@example.com'
  380. ensureUserExists({ email: retainedViewerEmail })
  381. const retainedEditorEmail = 'collaborator-retained-editor@example.com'
  382. ensureUserExists({ email: retainedEditorEmail })
  383. beforeWithReRunOnTestRetry(() => {
  384. // Set up retained viewer access
  385. login(retainedViewerEmail)
  386. openProjectViaLinkSharingAsUser(
  387. linkSharingReadOnly,
  388. projectName,
  389. retainedViewerEmail,
  390. true
  391. )
  392. // Set up retained editor access
  393. login(retainedEditorEmail)
  394. openProjectViaLinkSharingAsUser(
  395. linkSharingReadAndWrite,
  396. projectName,
  397. retainedEditorEmail,
  398. true
  399. )
  400. })
  401. beforeEach(function () {
  402. this.timeout(STARTUP_TIMEOUT) // Increase timeout for server reload
  403. return cy.wrap(
  404. reloadWith({
  405. pro: true,
  406. vars: {
  407. OVERLEAF_ALLOW_PUBLIC_ACCESS: 'true',
  408. OVERLEAF_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING: 'true',
  409. OVERLEAF_DISABLE_LINK_SHARING: 'true',
  410. },
  411. withDataDir: true,
  412. }),
  413. { timeout: STARTUP_TIMEOUT }
  414. )
  415. })
  416. it('should not display link sharing in the sharing modal', function () {
  417. login('user@example.com')
  418. openProjectByName(projectName, true)
  419. cy.findByRole('navigation', {
  420. name: 'Project actions',
  421. })
  422. .findByRole('button', { name: 'Share' })
  423. .click()
  424. cy.findByRole('button', { name: 'Turn on link sharing' }).should(
  425. 'not.exist'
  426. )
  427. })
  428. it('should block new access to read-only link shared projects', function () {
  429. login(email)
  430. // Test read-only link returns 404
  431. cy.request({
  432. url: linkSharingReadOnly,
  433. failOnStatusCode: false,
  434. }).then(response => {
  435. expect(response.status).to.eq(404)
  436. })
  437. })
  438. it('should block new access to read-write link shared projects', function () {
  439. login(email)
  440. // Test read-write link returns 404
  441. cy.request({
  442. url: linkSharingReadAndWrite,
  443. failOnStatusCode: false,
  444. }).then(response => {
  445. expect(response.status).to.eq(404)
  446. })
  447. })
  448. it('should continue to allow email sharing', function () {
  449. login('user@example.com')
  450. shareProjectByEmailAndAcceptInviteViaEmail(
  451. projectName,
  452. invitedEmail,
  453. 'Viewer',
  454. true
  455. )
  456. expectFullReadOnlyAccess()
  457. expectProjectDashboardEntry()
  458. })
  459. it('should retain read-only access when project was joined via link before link sharing was turned off', function () {
  460. login(retainedViewerEmail)
  461. openProjectByName(projectName, true)
  462. expectRestrictedReadOnlyAccess()
  463. expectProjectDashboardEntry()
  464. })
  465. it('should retain read-write access when project was joined via link before link sharing was turned off', function () {
  466. login(retainedEditorEmail)
  467. openProjectByName(projectName, true)
  468. expectFullReadAndWriteAccess()
  469. expectProjectDashboardEntry()
  470. })
  471. })
  472. })
  473. })