project-sharing.spec.ts 9.5 KB

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