project-sharing.spec.ts 15 KB

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