project-sharing.spec.ts 9.2 KB

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