project-sharing.spec.ts 11 KB

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