project-sharing.spec.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361
  1. import { v4 as uuid } from 'uuid'
  2. import { isExcludedBySharding, startWith } from './helpers/config'
  3. import { ensureUserExists, login } from './helpers/login'
  4. import {
  5. createProject,
  6. enableLinkSharing,
  7. openProjectByName,
  8. openProjectViaLinkSharingAsAnon,
  9. openProjectViaLinkSharingAsUser,
  10. shareProjectByEmailAndAcceptInviteViaDash,
  11. shareProjectByEmailAndAcceptInviteViaEmail,
  12. } from './helpers/project'
  13. import { throttledRecompile } from './helpers/compile'
  14. import { beforeWithReRunOnTestRetry } from './helpers/beforeWithReRunOnTestRetry'
  15. describe('Project Sharing', function () {
  16. if (isExcludedBySharding('PRO_CUSTOM_4')) return
  17. ensureUserExists({ email: 'user@example.com' })
  18. startWith({ withDataDir: true, pro: true })
  19. let projectName: string
  20. beforeWithReRunOnTestRetry(function () {
  21. projectName = `Project ${uuid()}`
  22. setupTestProject()
  23. })
  24. beforeEach(() => {
  25. // Always start with a fresh session
  26. cy.session([uuid()], () => {})
  27. })
  28. let linkSharingReadOnly: string
  29. let linkSharingReadAndWrite: string
  30. function setupTestProject() {
  31. login('user@example.com')
  32. createProject(projectName)
  33. // Add chat message
  34. cy.findByText('Chat').click()
  35. // wait for lazy loading of the chat pane
  36. cy.findByText('Send your first message to your collaborators')
  37. cy.get(
  38. 'textarea[placeholder="Send a message to your collaborators…"]'
  39. ).type('New Chat Message{enter}')
  40. // Get link sharing links
  41. enableLinkSharing().then(
  42. ({ linkSharingReadOnly: ro, linkSharingReadAndWrite: rw }) => {
  43. linkSharingReadAndWrite = rw
  44. linkSharingReadOnly = ro
  45. }
  46. )
  47. }
  48. function expectContentReadOnlyAccess() {
  49. cy.url().should('match', /\/project\/[a-fA-F0-9]{24}/)
  50. cy.findByRole('textbox', { name: /Source Editor editing/i }).should(
  51. 'contain.text',
  52. '\\maketitle'
  53. )
  54. cy.findByRole('textbox', { name: /Source Editor editing/i }).should(
  55. 'have.attr',
  56. 'contenteditable',
  57. 'false'
  58. )
  59. }
  60. function expectContentWriteAccess() {
  61. const section = `Test Section ${uuid()}`
  62. cy.url().should('match', /\/project\/[a-fA-F0-9]{24}/)
  63. const recompile = throttledRecompile()
  64. // wait for the editor to finish loading
  65. cy.findByRole('textbox', { name: /Source Editor editing/i }).should(
  66. 'contain.text',
  67. '\\maketitle'
  68. )
  69. // the editor should be writable
  70. cy.findByRole('textbox', { name: /Source Editor editing/i }).should(
  71. 'have.attr',
  72. 'contenteditable',
  73. 'true'
  74. )
  75. cy.findByText('\\maketitle').parent().click()
  76. cy.findByText('\\maketitle').parent().type(`\n\\section{{}${section}}`)
  77. // should have written
  78. cy.findByRole('textbox', { name: /Source Editor editing/i }).should(
  79. 'contain.text',
  80. `\\section{${section}}`
  81. )
  82. // check PDF
  83. recompile()
  84. cy.get('.pdf-viewer').should('contain.text', projectName)
  85. cy.get('.pdf-viewer').should('contain.text', section)
  86. }
  87. function expectNoAccess() {
  88. // try read only access link
  89. cy.visit(linkSharingReadOnly)
  90. cy.url().should('match', /\/login/)
  91. // 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).
  92. // Effectively, cypress then instructs the browser to change the page from /login#read-only-hash to /login#read-and-write-hash.
  93. // 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).
  94. // Work around this by navigating away from the /login page in between checks.
  95. cy.visit('/user/password/reset')
  96. // try read and write access link
  97. cy.visit(linkSharingReadAndWrite)
  98. cy.url().should('match', /\/login/)
  99. }
  100. function expectChatAccess() {
  101. cy.findByText('Chat').click()
  102. cy.findByText('New Chat Message')
  103. }
  104. function expectHistoryAccess() {
  105. cy.findByText('History').click()
  106. cy.findByText('Labels')
  107. cy.findByText(/\\begin\{document}/)
  108. cy.findAllByTestId('history-version-metadata-users')
  109. .last()
  110. .should('have.text', 'user')
  111. cy.findByText('Back to editor').click()
  112. }
  113. function expectNoChatAccess() {
  114. cy.findByText('Layout') // wait for lazy loading
  115. cy.findByText('Chat').should('not.exist')
  116. }
  117. function expectNoHistoryAccess() {
  118. cy.findByText('Layout') // wait for lazy loading
  119. cy.findByText('History').should('not.exist')
  120. }
  121. function expectCommentAccess() {
  122. cy.findByRole('textbox', { name: /Source Editor editing/i }).should(
  123. 'contain.text',
  124. '\\maketitle'
  125. )
  126. cy.findByText('\\maketitle').parent().dblclick()
  127. cy.findByRole('button', { name: 'Add comment' }).should('be.visible')
  128. cy.findByRole('textbox', { name: /Source Editor editing/i }).click()
  129. }
  130. function expectNoCommentAccess() {
  131. cy.findByRole('textbox', { name: /Source Editor editing/i }).should(
  132. 'contain.text',
  133. '\\maketitle'
  134. )
  135. cy.findByText('\\maketitle').parent().dblclick()
  136. cy.findByRole('button', { name: 'Add comment' }).should('not.exist')
  137. cy.findByRole('textbox', { name: /Source Editor editing/i }).click()
  138. }
  139. function expectFullReadOnlyAccess() {
  140. expectContentReadOnlyAccess()
  141. expectChatAccess()
  142. expectHistoryAccess()
  143. expectNoCommentAccess()
  144. }
  145. function expectRestrictedReadOnlyAccess() {
  146. expectContentReadOnlyAccess()
  147. expectNoChatAccess()
  148. expectNoHistoryAccess()
  149. expectNoCommentAccess()
  150. }
  151. function expectFullReadAndWriteAccess() {
  152. expectContentWriteAccess()
  153. expectChatAccess()
  154. expectHistoryAccess()
  155. expectCommentAccess()
  156. }
  157. function expectAnonymousReadAndWriteAccess() {
  158. expectContentWriteAccess()
  159. expectChatAccess()
  160. expectHistoryAccess()
  161. expectNoCommentAccess()
  162. }
  163. function expectProjectDashboardEntry() {
  164. cy.visit('/project')
  165. cy.findByText(projectName)
  166. }
  167. function expectEditAuthoredAs(author: string) {
  168. cy.findByText('History').click()
  169. cy.findAllByTestId('history-version-metadata-users')
  170. .first()
  171. .should('contain.text', author) // might have other edits in the same group
  172. }
  173. describe('via email', function () {
  174. const email = 'collaborator-email@example.com'
  175. ensureUserExists({ email })
  176. beforeEach(function () {
  177. login('user@example.com')
  178. shareProjectByEmailAndAcceptInviteViaEmail(projectName, email, 'Viewer')
  179. })
  180. it('should grant the collaborator read access', () => {
  181. expectFullReadOnlyAccess()
  182. expectProjectDashboardEntry()
  183. })
  184. })
  185. describe('read only', () => {
  186. const email = 'collaborator-ro@example.com'
  187. ensureUserExists({ email })
  188. beforeWithReRunOnTestRetry(function () {
  189. login('user@example.com')
  190. shareProjectByEmailAndAcceptInviteViaDash(projectName, email, 'Viewer')
  191. })
  192. it('should grant the collaborator read access', () => {
  193. login(email)
  194. openProjectByName(projectName)
  195. expectFullReadOnlyAccess()
  196. expectProjectDashboardEntry()
  197. })
  198. })
  199. describe('read and write', () => {
  200. const email = 'collaborator-rw@example.com'
  201. ensureUserExists({ email })
  202. beforeWithReRunOnTestRetry(function () {
  203. login('user@example.com')
  204. shareProjectByEmailAndAcceptInviteViaDash(projectName, email, 'Editor')
  205. })
  206. it('should grant the collaborator write access', () => {
  207. login(email)
  208. openProjectByName(projectName)
  209. expectFullReadAndWriteAccess()
  210. expectEditAuthoredAs('You')
  211. expectProjectDashboardEntry()
  212. })
  213. })
  214. describe('token access', () => {
  215. describe('logged in', () => {
  216. describe('read only', () => {
  217. const email = 'collaborator-link-ro@example.com'
  218. ensureUserExists({ email })
  219. it('should grant restricted read access', () => {
  220. login(email)
  221. openProjectViaLinkSharingAsUser(
  222. linkSharingReadOnly,
  223. projectName,
  224. email
  225. )
  226. expectRestrictedReadOnlyAccess()
  227. expectProjectDashboardEntry()
  228. })
  229. })
  230. describe('read and write', () => {
  231. const email = 'collaborator-link-rw@example.com'
  232. ensureUserExists({ email })
  233. it('should grant full write access', () => {
  234. login(email)
  235. openProjectViaLinkSharingAsUser(
  236. linkSharingReadAndWrite,
  237. projectName,
  238. email
  239. )
  240. expectFullReadAndWriteAccess()
  241. expectEditAuthoredAs('You')
  242. expectProjectDashboardEntry()
  243. })
  244. })
  245. })
  246. describe('with OVERLEAF_ALLOW_PUBLIC_ACCESS=false', () => {
  247. describe('wrap startup', () => {
  248. startWith({
  249. pro: true,
  250. vars: {
  251. OVERLEAF_ALLOW_PUBLIC_ACCESS: 'false',
  252. },
  253. withDataDir: true,
  254. })
  255. it('should block access', () => {
  256. expectNoAccess()
  257. })
  258. })
  259. describe('with OVERLEAF_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING=true', () => {
  260. startWith({
  261. pro: true,
  262. vars: {
  263. OVERLEAF_ALLOW_PUBLIC_ACCESS: 'false',
  264. OVERLEAF_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING: 'true',
  265. },
  266. withDataDir: true,
  267. })
  268. it('should block access', () => {
  269. expectNoAccess()
  270. })
  271. })
  272. })
  273. describe('with OVERLEAF_ALLOW_PUBLIC_ACCESS=true', () => {
  274. describe('wrap startup', () => {
  275. startWith({
  276. pro: true,
  277. vars: {
  278. OVERLEAF_ALLOW_PUBLIC_ACCESS: 'true',
  279. },
  280. withDataDir: true,
  281. })
  282. it('should grant read access with read link', () => {
  283. openProjectViaLinkSharingAsAnon(linkSharingReadOnly)
  284. expectRestrictedReadOnlyAccess()
  285. })
  286. it('should prompt for login with write link', () => {
  287. cy.visit(linkSharingReadAndWrite)
  288. cy.url().should('match', /\/login/)
  289. })
  290. })
  291. describe('with OVERLEAF_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING=true', () => {
  292. startWith({
  293. pro: true,
  294. vars: {
  295. OVERLEAF_ALLOW_PUBLIC_ACCESS: 'true',
  296. OVERLEAF_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING: 'true',
  297. },
  298. withDataDir: true,
  299. })
  300. it('should grant read access with read link', () => {
  301. openProjectViaLinkSharingAsAnon(linkSharingReadOnly)
  302. expectRestrictedReadOnlyAccess()
  303. })
  304. it('should grant write access with write link', () => {
  305. openProjectViaLinkSharingAsAnon(linkSharingReadAndWrite)
  306. expectAnonymousReadAndWriteAccess()
  307. expectEditAuthoredAs('Anonymous')
  308. })
  309. })
  310. })
  311. })
  312. })