project-sharing.spec.ts 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283
  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. shareProjectByEmailAndAcceptInvite,
  8. } from './helpers/project'
  9. import { throttledRecompile } from './helpers/compile'
  10. import { beforeWithReRunOnTestRetry } from './helpers/beforeWithReRunOnTestRetry'
  11. describe('Project Sharing', function () {
  12. if (isExcludedBySharding('CE_CUSTOM_2')) return
  13. ensureUserExists({ email: 'user@example.com' })
  14. startWith({ withDataDir: true })
  15. let projectName: string
  16. beforeWithReRunOnTestRetry(function () {
  17. projectName = `Project ${uuid()}`
  18. setupTestProject()
  19. })
  20. beforeEach(() => {
  21. // Always start with a fresh session
  22. cy.session([uuid()], () => {})
  23. })
  24. let linkSharingReadOnly: string
  25. let linkSharingReadAndWrite: string
  26. function setupTestProject() {
  27. login('user@example.com')
  28. cy.visit('/project')
  29. createProject(projectName)
  30. // Add chat message
  31. cy.findByText('Chat').click()
  32. cy.get(
  33. 'textarea[placeholder="Send a message to your collaborators…"]'
  34. ).type('New Chat Message{enter}')
  35. // Get link sharing links
  36. enableLinkSharing().then(
  37. ({ linkSharingReadOnly: ro, linkSharingReadAndWrite: rw }) => {
  38. linkSharingReadAndWrite = rw
  39. linkSharingReadOnly = ro
  40. }
  41. )
  42. }
  43. function expectContentReadOnlyAccess() {
  44. cy.url().should('match', /\/project\/[a-fA-F0-9]{24}/)
  45. cy.get('.cm-content').should('contain.text', '\\maketitle')
  46. cy.get('.cm-content').should('have.attr', 'contenteditable', 'false')
  47. }
  48. function expectContentWriteAccess() {
  49. const section = `Test Section ${uuid()}`
  50. cy.url().should('match', /\/project\/[a-fA-F0-9]{24}/)
  51. const recompile = throttledRecompile()
  52. // wait for the editor to finish loading
  53. cy.get('.cm-content').should('contain.text', '\\maketitle')
  54. // the editor should be writable
  55. cy.get('.cm-content').should('have.attr', 'contenteditable', 'true')
  56. cy.findByText('\\maketitle').parent().click()
  57. cy.findByText('\\maketitle').parent().type(`\n\\section{{}${section}}`)
  58. // should have written
  59. cy.get('.cm-content').should('contain.text', `\\section{${section}}`)
  60. // check PDF
  61. recompile()
  62. cy.get('.pdf-viewer').should('contain.text', projectName)
  63. cy.get('.pdf-viewer').should('contain.text', section)
  64. }
  65. function expectNoAccess() {
  66. // try read only access link
  67. cy.visit(linkSharingReadOnly)
  68. cy.url().should('match', /\/login/)
  69. // 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).
  70. // Effectively, cypress then instructs the browser to change the page from /login#read-only-hash to /login#read-and-write-hash.
  71. // 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).
  72. // Work around this by navigating away from the /login page in between checks.
  73. cy.visit('/user/password/reset')
  74. // try read and write access link
  75. cy.visit(linkSharingReadAndWrite)
  76. cy.url().should('match', /\/login/)
  77. }
  78. function expectChatAccess() {
  79. cy.findByText('Chat').click()
  80. cy.findByText('New Chat Message')
  81. }
  82. function expectHistoryAccess() {
  83. cy.findByText('History').click()
  84. cy.findByText('Labels')
  85. cy.findByText(/\\begin\{document}/)
  86. cy.findAllByTestId('history-version-metadata-users')
  87. .last()
  88. .should('have.text', 'user')
  89. cy.findByText('Back to editor').click()
  90. }
  91. function expectNoChatAccess() {
  92. cy.findByText('Layout') // wait for lazy loading
  93. cy.findByText('Chat').should('not.exist')
  94. }
  95. function expectNoHistoryAccess() {
  96. cy.findByText('Layout') // wait for lazy loading
  97. cy.findByText('History').should('not.exist')
  98. }
  99. function expectFullReadOnlyAccess() {
  100. expectContentReadOnlyAccess()
  101. expectChatAccess()
  102. expectHistoryAccess()
  103. }
  104. function expectRestrictedReadOnlyAccess() {
  105. expectContentReadOnlyAccess()
  106. expectNoChatAccess()
  107. expectNoHistoryAccess()
  108. }
  109. function expectReadAndWriteAccess() {
  110. expectContentWriteAccess()
  111. expectChatAccess()
  112. expectHistoryAccess()
  113. }
  114. function expectProjectDashboardEntry() {
  115. cy.visit('/project')
  116. cy.findByText(projectName)
  117. }
  118. function expectEditAuthoredAs(author: string) {
  119. cy.findByText('History').click()
  120. cy.findAllByTestId('history-version-metadata-users')
  121. .first()
  122. .should('contain.text', author) // might have other edits in the same group
  123. }
  124. describe('read only', () => {
  125. const email = 'collaborator-ro@example.com'
  126. ensureUserExists({ email })
  127. beforeWithReRunOnTestRetry(function () {
  128. login('user@example.com')
  129. shareProjectByEmailAndAcceptInvite(projectName, email, 'Read only')
  130. })
  131. it('should grant the collaborator read access', () => {
  132. login(email)
  133. cy.visit('/project')
  134. cy.findByText(projectName).click()
  135. expectFullReadOnlyAccess()
  136. expectProjectDashboardEntry()
  137. })
  138. })
  139. describe('read and write', () => {
  140. const email = 'collaborator-rw@example.com'
  141. ensureUserExists({ email })
  142. beforeWithReRunOnTestRetry(function () {
  143. login('user@example.com')
  144. shareProjectByEmailAndAcceptInvite(projectName, email, 'Can edit')
  145. })
  146. it('should grant the collaborator write access', () => {
  147. login(email)
  148. cy.visit('/project')
  149. cy.findByText(projectName).click()
  150. expectReadAndWriteAccess()
  151. expectEditAuthoredAs('You')
  152. expectProjectDashboardEntry()
  153. })
  154. })
  155. describe('token access', () => {
  156. describe('logged in', () => {
  157. describe('read only', () => {
  158. const email = 'collaborator-link-ro@example.com'
  159. ensureUserExists({ email })
  160. it('should grant restricted read access', () => {
  161. login(email)
  162. cy.visit(linkSharingReadOnly)
  163. cy.findByText(projectName) // wait for lazy loading
  164. cy.findByText('Join Project').click()
  165. expectRestrictedReadOnlyAccess()
  166. expectProjectDashboardEntry()
  167. })
  168. })
  169. describe('read and write', () => {
  170. const email = 'collaborator-link-rw@example.com'
  171. ensureUserExists({ email })
  172. it('should grant full write access', () => {
  173. login(email)
  174. cy.visit(linkSharingReadAndWrite)
  175. cy.findByText(projectName) // wait for lazy loading
  176. cy.findByText('Join Project').click()
  177. expectReadAndWriteAccess()
  178. expectEditAuthoredAs('You')
  179. expectProjectDashboardEntry()
  180. })
  181. })
  182. })
  183. describe('with OVERLEAF_ALLOW_PUBLIC_ACCESS=false', () => {
  184. describe('wrap startup', () => {
  185. startWith({
  186. vars: {
  187. OVERLEAF_ALLOW_PUBLIC_ACCESS: 'false',
  188. },
  189. withDataDir: true,
  190. })
  191. it('should block access', () => {
  192. expectNoAccess()
  193. })
  194. })
  195. describe('with OVERLEAF_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING=true', () => {
  196. startWith({
  197. vars: {
  198. OVERLEAF_ALLOW_PUBLIC_ACCESS: 'false',
  199. OVERLEAF_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING: 'true',
  200. },
  201. withDataDir: true,
  202. })
  203. it('should block access', () => {
  204. expectNoAccess()
  205. })
  206. })
  207. })
  208. describe('with OVERLEAF_ALLOW_PUBLIC_ACCESS=true', () => {
  209. describe('wrap startup', () => {
  210. startWith({
  211. vars: {
  212. OVERLEAF_ALLOW_PUBLIC_ACCESS: 'true',
  213. },
  214. withDataDir: true,
  215. })
  216. it('should grant read access with read link', () => {
  217. cy.visit(linkSharingReadOnly)
  218. expectRestrictedReadOnlyAccess()
  219. })
  220. it('should prompt for login with write link', () => {
  221. cy.visit(linkSharingReadAndWrite)
  222. cy.url().should('match', /\/login/)
  223. })
  224. })
  225. describe('with OVERLEAF_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING=true', () => {
  226. startWith({
  227. vars: {
  228. OVERLEAF_ALLOW_PUBLIC_ACCESS: 'true',
  229. OVERLEAF_ALLOW_ANONYMOUS_READ_AND_WRITE_SHARING: 'true',
  230. },
  231. withDataDir: true,
  232. })
  233. it('should grant read access with read link', () => {
  234. cy.visit(linkSharingReadOnly)
  235. expectRestrictedReadOnlyAccess()
  236. })
  237. it('should grant write access with write link', () => {
  238. cy.visit(linkSharingReadAndWrite)
  239. expectReadAndWriteAccess()
  240. expectEditAuthoredAs('Anonymous')
  241. })
  242. })
  243. })
  244. })
  245. })