project-sharing.spec.ts 9.4 KB

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