project-sharing.spec.ts 9.3 KB

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