upgrading.spec.ts 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232
  1. import {
  2. ensureUserExists,
  3. login,
  4. resetCreatedUsersCache,
  5. } from './helpers/login'
  6. import { startWith } from './helpers/config'
  7. import { dockerCompose, resetData, runScript } from './helpers/hostAdminClient'
  8. import { createProject } from './helpers/project'
  9. import { throttledRecompile } from './helpers/compile'
  10. import { v4 as uuid } from 'uuid'
  11. const USER = 'user@example.com'
  12. const PROJECT_NAME = 'Old Project'
  13. describe('Upgrading', function () {
  14. function testUpgrade(
  15. steps: {
  16. version: string
  17. vars?: Object
  18. newProjectButtonMatcher?: RegExp
  19. hook?: () => void
  20. }[]
  21. ) {
  22. const startOptions = steps.shift()!
  23. before(async () => {
  24. cy.log('Reset mongo/redis/on-disk data')
  25. resetCreatedUsersCache()
  26. await resetData()
  27. cy.log('Create old instance')
  28. })
  29. startWith({
  30. pro: true,
  31. version: startOptions.version,
  32. withDataDir: true,
  33. vars: startOptions.vars,
  34. })
  35. before(function () {
  36. cy.log('Create initial user after deleting it')
  37. })
  38. ensureUserExists({ email: USER })
  39. before(() => {
  40. cy.log('Populate old instance')
  41. login(USER)
  42. cy.visit('/project')
  43. createProject(PROJECT_NAME, {
  44. newProjectButtonMatcher: startOptions.newProjectButtonMatcher,
  45. })
  46. const recompile = throttledRecompile()
  47. cy.log('Wait for successful compile')
  48. cy.get('.pdf-viewer').should('contain.text', PROJECT_NAME)
  49. cy.log('Increment the doc version three times')
  50. for (let i = 0; i < 3; i++) {
  51. cy.log('Add content')
  52. cy.findByText('\\maketitle').parent().click()
  53. cy.findByText('\\maketitle')
  54. .parent()
  55. .type(`\n\\section{{}Old Section ${i}}`)
  56. cy.log('Trigger full flush')
  57. recompile()
  58. cy.get('header').findByText('Menu').click()
  59. cy.findByText('Source').click()
  60. cy.get('#left-menu-modal').click()
  61. }
  62. cy.log('Check compile and history')
  63. for (let i = 0; i < 3; i++) {
  64. cy.get('.pdf-viewer').should('contain.text', `Old Section ${i}`)
  65. }
  66. cy.findByText('History').click()
  67. for (let i = 0; i < 3; i++) {
  68. cy.findByText(new RegExp(`\\\\section\{Old Section ${i}}`))
  69. }
  70. })
  71. for (const step of steps) {
  72. before(() => {
  73. cy.log(`Upgrade to version ${step.version}`)
  74. // Navigate way from editor to avoid redirect to /login when the next instance comes up (which slows down tests)
  75. cy.visit('/project', {})
  76. })
  77. before(async function () {
  78. cy.log('Graceful shutdown: flush all the things')
  79. this.timeout(20 * 1000)
  80. // Ideally we could use the container shutdown procedure, but it's too slow and unreliable for tests.
  81. // TODO(das7pad): adopt the below after speeding up the graceful shutdown procedure on all supported releases
  82. // await dockerCompose('stop', 'sharelatex')
  83. // For now, we are stuck with manually flushing things
  84. await runScript({
  85. cwd: 'services/document-updater',
  86. script: 'scripts/flush_all.js',
  87. })
  88. await runScript({
  89. cwd: 'services/project-history',
  90. script: 'scripts/flush_all.js',
  91. })
  92. })
  93. startWith({
  94. pro: true,
  95. version: step.version,
  96. vars: step.vars,
  97. withDataDir: true,
  98. })
  99. step.hook?.()
  100. }
  101. beforeEach(() => {
  102. login(USER)
  103. })
  104. it('should list the old project', () => {
  105. cy.visit('/project')
  106. cy.findByText(PROJECT_NAME)
  107. })
  108. it('should open the old project', () => {
  109. cy.visit('/project')
  110. cy.findByText(PROJECT_NAME).click()
  111. cy.url().should('match', /\/project\/[a-fA-F0-9]{24}/)
  112. cy.findByRole('navigation').within(() => {
  113. cy.findByText(PROJECT_NAME)
  114. })
  115. const recompile = throttledRecompile()
  116. cy.log('wait for successful compile')
  117. cy.get('.pdf-viewer').should('contain.text', PROJECT_NAME)
  118. cy.get('.pdf-viewer').should('contain.text', 'Old Section 2')
  119. cy.log('Add more content')
  120. const newSection = `New Section ${uuid()}`
  121. cy.findByText('\\maketitle').parent().click()
  122. cy.findByText('\\maketitle').parent().type(`\n\\section{{}${newSection}}`)
  123. cy.log('Check compile and history')
  124. recompile()
  125. cy.get('.pdf-viewer').should('contain.text', newSection)
  126. cy.findByText('History').click()
  127. cy.findByText(/\\section\{Old Section 2}/)
  128. cy.findByText(new RegExp(`\\\\section\\{${newSection}}`))
  129. })
  130. }
  131. const optionsFourDotTwo = {
  132. version: '4.2',
  133. vars: {
  134. // Add database vars with old branding
  135. SHARELATEX_MONGO_URL: 'mongodb://mongo/sharelatex',
  136. SHARELATEX_REDIS_HOST: 'redis',
  137. },
  138. newProjectButtonMatcher: /create first project/i,
  139. }
  140. describe('from 4.2 to latest', () => {
  141. testUpgrade([optionsFourDotTwo, { version: 'latest' }])
  142. })
  143. describe('from 5.0 to latest', () => {
  144. testUpgrade([{ version: '5.0' }, { version: 'latest' }])
  145. })
  146. describe('doc version recovery', () => {
  147. testUpgrade([
  148. optionsFourDotTwo,
  149. {
  150. version: '5.0.1-RC1',
  151. hook() {
  152. before(function () {
  153. login(USER)
  154. cy.visit('/')
  155. cy.findByText(PROJECT_NAME).click()
  156. const recompile = throttledRecompile()
  157. cy.log('Make a change')
  158. cy.findByText('\\maketitle').parent().click()
  159. cy.findByText('\\maketitle')
  160. .parent()
  161. .type('\n\\section{{}FiveOOne Section}')
  162. cy.log('Trigger flush')
  163. recompile()
  164. cy.get('.pdf-viewer').should('contain.text', 'FiveOOne Section')
  165. cy.log('Check for broken history, i.e. not synced with latest edit')
  166. cy.findByText('History').click()
  167. cy.findByText(/\\section\{Old Section 2}/) // wait for lazy loading
  168. cy.findByText(/\\section\{FiveOOne Section}/).should('not.exist')
  169. })
  170. },
  171. },
  172. {
  173. version: 'latest',
  174. hook() {
  175. before(async function () {
  176. this.timeout(20_000)
  177. const needle = 'Finished resyncing history for all projects.'
  178. for (let i = 0; i < 30; i++) {
  179. const { stdout } = await dockerCompose('logs', 'sharelatex')
  180. if (stdout.includes(needle)) {
  181. return
  182. }
  183. await new Promise(resolve => setTimeout(resolve, 500))
  184. }
  185. const { stdout } = await dockerCompose('logs', 'sharelatex')
  186. expect(stdout).to.contain(
  187. needle,
  188. 'Doc version recovery did not finish yet.'
  189. )
  190. })
  191. before(function () {
  192. login(USER)
  193. cy.visit('/')
  194. cy.findByText(PROJECT_NAME).click()
  195. cy.log(
  196. 'The edit that was made while the history was broken should be there now.'
  197. )
  198. cy.findByText('History').click()
  199. cy.findByText(/\\section\{FiveOOne Section}/)
  200. cy.log('Check indicator of force resync')
  201. cy.findByText('Overleaf History System')
  202. })
  203. },
  204. },
  205. ])
  206. })
  207. })