project-sharing.spec.ts 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  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('CE_CUSTOM_2')) return
  17. ensureUserExists({ email: 'user@example.com' })
  18. startWith({ withDataDir: 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.get('.cm-content').should('contain.text', '\\maketitle')
  51. cy.get('.cm-content').should('have.attr', 'contenteditable', 'false')
  52. }
  53. function expectContentWriteAccess() {
  54. const section = `Test Section ${uuid()}`
  55. cy.url().should('match', /\/project\/[a-fA-F0-9]{24}/)
  56. const recompile = throttledRecompile()
  57. // wait for the editor to finish loading
  58. cy.get('.cm-content').should('contain.text', '\\maketitle')
  59. // the editor should be writable
  60. cy.get('.cm-content').should('have.attr', 'contenteditable', 'true')
  61. cy.findByText('\\maketitle').parent().click()
  62. cy.findByText('\\maketitle').parent().type(`\n\\section{{}${section}}`)
  63. // should have written
  64. cy.get('.cm-content').should('contain.text', `\\section{${section}}`)
  65. // check PDF
  66. recompile()
  67. cy.get('.pdf-viewer').should('contain.text', projectName)
  68. cy.get('.pdf-viewer').should('contain.text', section)
  69. }
  70. function expectNoAccess() {
  71. // try read only access link
  72. cy.visit(linkSharingReadOnly)
  73. cy.url().should('match', /\/login/)
  74. // 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).
  75. // Effectively, cypress then instructs the browser to change the page from /login#read-only-hash to /login#read-and-write-hash.
  76. // 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).
  77. // Work around this by navigating away from the /login page in between checks.
  78. cy.visit('/user/password/reset')
  79. // try read and write access link
  80. cy.visit(linkSharingReadAndWrite)
  81. cy.url().should('match', /\/login/)
  82. }
  83. function expectChatAccess() {
  84. cy.findByText('Chat').click()
  85. cy.findByText('New Chat Message')
  86. }
  87. function expectHistoryAccess() {
  88. cy.findByText('History').click()
  89. cy.findByText('Labels')
  90. cy.findByText(/\\begin\{document}/)
  91. cy.findAllByTestId('history-version-metadata-users')
  92. .last()
  93. .should('have.text', 'user')
  94. cy.findByText('Back to editor').click()
  95. }
  96. function expectNoChatAccess() {
  97. cy.findByText('Layout') // wait for lazy loading
  98. cy.findByText('Chat').should('not.exist')
  99. }
  100. function expectNoHistoryAccess() {
  101. cy.findByText('Layout') // wait for lazy loading
  102. cy.findByText('History').should('not.exist')
  103. }
  104. function expectFullReadOnlyAccess() {
  105. expectContentReadOnlyAccess()
  106. expectChatAccess()
  107. expectHistoryAccess()
  108. }
  109. function expectRestrictedReadOnlyAccess() {
  110. expectContentReadOnlyAccess()
  111. expectNoChatAccess()
  112. expectNoHistoryAccess()
  113. }
  114. function expectReadAndWriteAccess() {
  115. expectContentWriteAccess()
  116. expectChatAccess()
  117. expectHistoryAccess()
  118. }
  119. function expectProjectDashboardEntry() {
  120. cy.visit('/project')
  121. cy.findByText(projectName)
  122. }
  123. function expectEditAuthoredAs(author: string) {
  124. cy.findByText('History').click()
  125. cy.findAllByTestId('history-version-metadata-users')
  126. .first()
  127. .should('contain.text', author) // might have other edits in the same group
  128. }
  129. describe('via email', function () {
  130. const email = 'collaborator-email@example.com'
  131. ensureUserExists({ email })
  132. beforeEach(function () {
  133. login('user@example.com')
  134. shareProjectByEmailAndAcceptInviteViaEmail(projectName, email, 'Viewer')
  135. })
  136. it('should grant the collaborator read access', () => {
  137. expectFullReadOnlyAccess()
  138. expectProjectDashboardEntry()
  139. })
  140. })
  141. describe('read only', () => {
  142. const email = 'collaborator-ro@example.com'
  143. ensureUserExists({ email })
  144. beforeWithReRunOnTestRetry(function () {
  145. login('user@example.com')
  146. shareProjectByEmailAndAcceptInviteViaDash(projectName, email, 'Viewer')
  147. })
  148. it('should grant the collaborator read access', () => {
  149. login(email)
  150. openProjectByName(projectName)
  151. expectFullReadOnlyAccess()
  152. expectProjectDashboardEntry()
  153. })
  154. })
  155. describe('read and write', () => {
  156. const email = 'collaborator-rw@example.com'
  157. ensureUserExists({ email })
  158. beforeWithReRunOnTestRetry(function () {
  159. login('user@example.com')
  160. shareProjectByEmailAndAcceptInviteViaDash(projectName, email, 'Editor')
  161. })
  162. it('should grant the collaborator write access', () => {
  163. login(email)
  164. openProjectByName(projectName)
  165. expectReadAndWriteAccess()
  166. expectEditAuthoredAs('You')
  167. expectProjectDashboardEntry()
  168. })
  169. })
  170. describe('token access', () => {
  171. describe('logged in', () => {
  172. describe('read only', () => {
  173. const email = 'collaborator-link-ro@example.com'
  174. ensureUserExists({ email })
  175. it('should grant restricted read access', () => {
  176. login(email)
  177. openProjectViaLinkSharingAsUser(
  178. linkSharingReadOnly,
  179. projectName,
  180. email
  181. )
  182. expectRestrictedReadOnlyAccess()
  183. expectProjectDashboardEntry()
  184. })
  185. })
  186. describe('read and write', () => {
  187. const email = 'collaborator-link-rw@example.com'
  188. ensureUserExists({ email })
  189. it('should grant full write access', () => {
  190. login(email)
  191. openProjectViaLinkSharingAsUser(
  192. linkSharingReadAndWrite,
  193. projectName,
  194. email
  195. )
  196. expectReadAndWriteAccess()
  197. expectEditAuthoredAs('You')
  198. expectProjectDashboardEntry()
  199. })
  200. })
  201. })
  202. describe('with OVERLEAF_ALLOW_PUBLIC_ACCESS=false', () => {
  203. describe('wrap startup', () => {
  204. startWith({
  205. vars: {
  206. OVERLEAF_ALLOW_PUBLIC_ACCESS: 'false',
  207. },
  208. withDataDir: true,
  209. })
  210. it('should block access', () => {
  211. expectNoAccess()
  212. })
  213. })
  214. describe('with OVERLEAF_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING=true', () => {
  215. startWith({
  216. vars: {
  217. OVERLEAF_ALLOW_PUBLIC_ACCESS: 'false',
  218. OVERLEAF_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING: 'true',
  219. },
  220. withDataDir: true,
  221. })
  222. it('should block access', () => {
  223. expectNoAccess()
  224. })
  225. })
  226. })
  227. describe('with OVERLEAF_ALLOW_PUBLIC_ACCESS=true', () => {
  228. describe('wrap startup', () => {
  229. startWith({
  230. vars: {
  231. OVERLEAF_ALLOW_PUBLIC_ACCESS: 'true',
  232. },
  233. withDataDir: true,
  234. })
  235. it('should grant read access with read link', () => {
  236. openProjectViaLinkSharingAsAnon(linkSharingReadOnly)
  237. expectRestrictedReadOnlyAccess()
  238. })
  239. it('should prompt for login with write link', () => {
  240. cy.visit(linkSharingReadAndWrite)
  241. cy.url().should('match', /\/login/)
  242. })
  243. })
  244. describe('with OVERLEAF_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING=true', () => {
  245. startWith({
  246. vars: {
  247. OVERLEAF_ALLOW_PUBLIC_ACCESS: 'true',
  248. OVERLEAF_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING: 'true',
  249. },
  250. withDataDir: true,
  251. })
  252. it('should grant read access with read link', () => {
  253. openProjectViaLinkSharingAsAnon(linkSharingReadOnly)
  254. expectRestrictedReadOnlyAccess()
  255. })
  256. it('should grant write access with write link', () => {
  257. openProjectViaLinkSharingAsAnon(linkSharingReadAndWrite)
  258. expectReadAndWriteAccess()
  259. expectEditAuthoredAs('Anonymous')
  260. })
  261. })
  262. })
  263. })
  264. })