| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364 |
- const MockWebApi = require('./helpers/MockWebApi')
- const DocUpdaterClient = require('./helpers/DocUpdaterClient')
- const DocUpdaterApp = require('./helpers/DocUpdaterApp')
- const { promisify } = require('node:util')
- const { exec } = require('node:child_process')
- const { expect } = require('chai')
- const Settings = require('@overleaf/settings')
- const fs = require('node:fs')
- const Path = require('node:path')
- const MockDocstoreApi = require('./helpers/MockDocstoreApi')
- const sinon = require('sinon')
- const rclient = require('@overleaf/redis-wrapper').createClient(
- Settings.redis.documentupdater
- )
- describe('CheckRedisMongoSyncState', function () {
- beforeEach(async function () {
- await DocUpdaterApp.ensureRunning()
- })
- beforeEach(async function () {
- await rclient.flushall()
- })
- let peekDocumentInDocstore
- beforeEach(function () {
- peekDocumentInDocstore = sinon.spy(MockDocstoreApi, 'peekDocument')
- })
- afterEach(function () {
- peekDocumentInDocstore.restore()
- })
- async function runScript(options) {
- let result
- try {
- result = await promisify(exec)(
- Object.entries(options)
- .map(([key, value]) => `${key}=${value}`)
- .concat(['node', 'scripts/check_redis_mongo_sync_state.js'])
- .join(' ')
- )
- } catch (error) {
- // includes details like exit code, stdErr and stdOut
- return error
- }
- result.code = 0
- return result
- }
- describe('without projects', function () {
- it('should work when in sync', async function () {
- const result = await runScript({})
- expect(result.code).to.equal(0)
- expect(result.stdout).to.include('Processed 0 projects')
- expect(result.stdout).to.include(
- 'Found 0 projects with 0 out of sync docs'
- )
- })
- })
- describe('with a project', function () {
- let projectId, docId
- beforeEach(async function () {
- projectId = DocUpdaterClient.randomId()
- docId = DocUpdaterClient.randomId()
- MockWebApi.insertDoc(projectId, docId, {
- lines: ['mongo', 'lines'],
- version: 1,
- })
- await DocUpdaterClient.preloadDoc(projectId, docId)
- })
- it('should work when in sync', async function () {
- const result = await runScript({})
- expect(result.code).to.equal(0)
- expect(result.stdout).to.include('Processed 1 projects')
- expect(result.stdout).to.include(
- 'Found 0 projects with 0 out of sync docs'
- )
- expect(peekDocumentInDocstore).to.not.have.been.called
- })
- describe('with out of sync lines', function () {
- beforeEach(function () {
- MockWebApi.insertDoc(projectId, docId, {
- lines: ['updated', 'mongo', 'lines'],
- version: 1,
- })
- })
- it('should detect the out of sync state', async function () {
- const result = await runScript({})
- expect(result.code).to.equal(1)
- expect(result.stdout).to.include('Processed 1 projects')
- expect(result.stdout).to.include(
- 'Found 1 projects with 1 out of sync docs'
- )
- })
- })
- describe('with out of sync ranges', function () {
- beforeEach(function () {
- MockWebApi.insertDoc(projectId, docId, {
- lines: ['mongo', 'lines'],
- version: 1,
- ranges: { changes: ['FAKE CHANGE'] },
- })
- })
- it('should detect the out of sync state', async function () {
- const result = await runScript({})
- expect(result.code).to.equal(1)
- expect(result.stdout).to.include('Processed 1 projects')
- expect(result.stdout).to.include(
- 'Found 1 projects with 1 out of sync docs'
- )
- })
- })
- describe('with out of sync version', function () {
- beforeEach(function () {
- MockWebApi.insertDoc(projectId, docId, {
- lines: ['mongo', 'lines'],
- version: 2,
- })
- })
- it('should detect the out of sync state', async function () {
- const result = await runScript({})
- expect(result.code).to.equal(1)
- expect(result.stdout).to.include('Processed 1 projects')
- expect(result.stdout).to.include(
- 'Found 1 projects with 1 out of sync docs'
- )
- })
- it('should auto-fix the out of sync state', async function () {
- const result = await runScript({
- AUTO_FIX_VERSION_MISMATCH: 'true',
- })
- expect(result.code).to.equal(0)
- expect(result.stdout).to.include('Processed 1 projects')
- expect(result.stdout).to.include(
- 'Found 0 projects with 0 out of sync docs'
- )
- })
- })
- describe('with a project', function () {
- let projectId2, docId2
- beforeEach(async function () {
- projectId2 = DocUpdaterClient.randomId()
- docId2 = DocUpdaterClient.randomId()
- MockWebApi.insertDoc(projectId2, docId2, {
- lines: ['mongo', 'lines'],
- version: 1,
- })
- await DocUpdaterClient.preloadDoc(projectId2, docId2)
- })
- it('should work when in sync', async function () {
- const result = await runScript({})
- expect(result.code).to.equal(0)
- expect(result.stdout).to.include('Processed 2 projects')
- expect(result.stdout).to.include(
- 'Found 0 projects with 0 out of sync docs'
- )
- })
- describe('with one out of sync', function () {
- beforeEach(function () {
- MockWebApi.insertDoc(projectId, docId, {
- lines: ['updated', 'mongo', 'lines'],
- version: 1,
- })
- })
- it('should detect one project out of sync', async function () {
- const result = await runScript({})
- expect(result.code).to.equal(1)
- expect(result.stdout).to.include('Processed 2 projects')
- expect(result.stdout).to.include(
- 'Found 1 projects with 1 out of sync docs'
- )
- })
- it('should write differences to disk', async function () {
- const FOLDER = '/tmp/folder'
- await fs.promises.rm(FOLDER, { recursive: true, force: true })
- const result = await runScript({
- WRITE_CONTENT: 'true',
- FOLDER,
- })
- expect(result.code).to.equal(1)
- expect(result.stdout).to.include('Processed 2 projects')
- expect(result.stdout).to.include(
- 'Found 1 projects with 1 out of sync docs'
- )
- const dir = Path.join(FOLDER, projectId, docId)
- expect(await fs.promises.readdir(FOLDER)).to.deep.equal([projectId])
- expect(await fs.promises.readdir(dir)).to.deep.equal([
- 'mongo-snapshot.txt',
- 'redis-snapshot.txt',
- ])
- expect(
- await fs.promises.readFile(
- Path.join(dir, 'mongo-snapshot.txt'),
- 'utf-8'
- )
- ).to.equal('updated\nmongo\nlines')
- expect(
- await fs.promises.readFile(
- Path.join(dir, 'redis-snapshot.txt'),
- 'utf-8'
- )
- ).to.equal('mongo\nlines')
- })
- })
- describe('with both out of sync', function () {
- beforeEach(function () {
- MockWebApi.insertDoc(projectId, docId, {
- lines: ['updated', 'mongo', 'lines'],
- version: 1,
- })
- MockWebApi.insertDoc(projectId2, docId2, {
- lines: ['updated2', 'mongo', 'lines'],
- version: 1,
- })
- })
- it('should detect both projects out of sync', async function () {
- const result = await runScript({})
- expect(result.code).to.equal(1)
- expect(result.stdout).to.include('Processed 2 projects')
- expect(result.stdout).to.include(
- 'Found 2 projects with 2 out of sync docs'
- )
- })
- })
- })
- })
- describe('with more projects than the LIMIT', function () {
- for (let i = 0; i < 20; i++) {
- beforeEach(async function () {
- const projectId = DocUpdaterClient.randomId()
- const docId = DocUpdaterClient.randomId()
- MockWebApi.insertDoc(projectId, docId, {
- lines: ['mongo', 'lines'],
- version: 1,
- })
- await DocUpdaterClient.preloadDoc(projectId, docId)
- })
- }
- it('should flag limit', async function () {
- const result = await runScript({ LIMIT: '4' })
- expect(result.code).to.equal(2)
- // A redis SCAN may return more than COUNT (aka LIMIT) entries. Match loosely.
- expect(result.stdout).to.match(/Processed \d+ projects/)
- expect(result.stderr).to.include(
- 'Found too many un-flushed projects (LIMIT=4). Please fix the reported projects first, then try again.'
- )
- })
- it('should continue with auto-flush', async function () {
- const result = await runScript({
- LIMIT: '4',
- FLUSH_IN_SYNC_PROJECTS: 'true',
- })
- expect(result.code).to.equal(0)
- expect(result.stdout).to.include('Processed 20 projects')
- })
- })
- describe('with partially deleted doc', function () {
- let projectId, docId
- beforeEach(async function () {
- projectId = DocUpdaterClient.randomId()
- docId = DocUpdaterClient.randomId()
- MockWebApi.insertDoc(projectId, docId, {
- lines: ['mongo', 'lines'],
- version: 1,
- })
- MockDocstoreApi.insertDoc(projectId, docId, {
- lines: ['mongo', 'lines'],
- version: 1,
- })
- await DocUpdaterClient.preloadDoc(projectId, docId)
- MockWebApi.clearDocs()
- })
- describe('with only the file-tree entry deleted', function () {
- it('should flag the partial deletion', async function () {
- const result = await runScript({})
- expect(result.code).to.equal(0)
- expect(result.stdout).to.include('Processed 1 projects')
- expect(result.stdout).to.include(
- `Found partially deleted doc ${docId} in project ${projectId}: use AUTO_FIX_PARTIALLY_DELETED_DOC_METADATA=true to fix metadata`
- )
- expect(result.stdout).to.include(
- 'Found 0 projects with 0 out of sync docs'
- )
- expect(MockDocstoreApi.getDoc(projectId, docId)).to.not.include({
- deleted: true,
- name: 'c.tex',
- })
- expect(peekDocumentInDocstore).to.have.been.called
- })
- it('should autofix the partial deletion', async function () {
- const result = await runScript({
- AUTO_FIX_PARTIALLY_DELETED_DOC_METADATA: 'true',
- })
- expect(result.code).to.equal(0)
- expect(result.stdout).to.include('Processed 1 projects')
- expect(result.stdout).to.include(
- `Found partially deleted doc ${docId} in project ${projectId}: fixing metadata`
- )
- expect(result.stdout).to.include(
- 'Found 0 projects with 0 out of sync docs'
- )
- expect(MockDocstoreApi.getDoc(projectId, docId)).to.include({
- deleted: true,
- name: 'c.tex',
- })
- const result2 = await runScript({})
- expect(result2.code).to.equal(0)
- expect(result2.stdout).to.include('Processed 1 projects')
- expect(result2.stdout).to.not.include(
- `Found partially deleted doc ${docId} in project ${projectId}`
- )
- expect(result2.stdout).to.include(
- 'Found 0 projects with 0 out of sync docs'
- )
- })
- })
- describe('with docstore metadata updated', function () {
- beforeEach(async function () {
- await MockDocstoreApi.patchDocument(projectId, docId, {
- deleted: true,
- deletedAt: new Date(),
- name: 'c.tex',
- })
- })
- it('should work when in sync', async function () {
- const result = await runScript({})
- expect(result.code).to.equal(0)
- expect(result.stdout).to.include('Processed 1 projects')
- expect(result.stdout).to.not.include(
- `Found partially deleted doc ${docId} in project ${projectId}`
- )
- expect(result.stdout).to.include(
- 'Found 0 projects with 0 out of sync docs'
- )
- expect(peekDocumentInDocstore).to.have.been.called
- })
- })
- })
- })
|