upgrading.spec.ts 7.9 KB

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