| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733 |
- import { exec } from 'node:child_process'
- import { promisify } from 'node:util'
- import { expect } from 'chai'
- import logger from '@overleaf/logger'
- import { filterOutput } from './helpers/settings.mjs'
- import { db, ObjectId } from '../../../app/src/infrastructure/mongodb.mjs'
- const lastUpdated = new Date(42)
- const lastUpdatedBy = new ObjectId()
- const lastUpdatedChanged = new Date(1337)
- async function runScriptFind() {
- try {
- const result = await promisify(exec)(
- ['node', 'scripts/find_malformed_filetrees.mjs'].join(' ')
- )
- return result.stdout.split('\n').filter(filterOutput)
- } catch (error) {
- logger.error({ error }, 'script failed')
- throw error
- }
- }
- async function runScriptFix(instructions) {
- const adhocFile = instructions.map(entry => JSON.stringify(entry)).join('\n')
- try {
- return await promisify(exec)(
- [
- 'node',
- 'scripts/fix_malformed_filetree.mjs',
- `--logs=<(echo '${adhocFile}')`,
- ].join(' '),
- { shell: '/bin/bash' }
- )
- } catch (error) {
- logger.error({ error }, 'fix script failed unexpectedly')
- throw error
- }
- }
- const findProjects = () =>
- db.projects
- .find(
- {},
- {
- projection: {
- rootFolder: 1,
- _id: 1,
- version: 1,
- lastUpdated: 1,
- lastUpdatedBy: 1,
- },
- }
- )
- .toArray()
- const projectId = new ObjectId()
- const rootFolderId = new ObjectId()
- const idDic = {}
- const id = key => {
- if (!idDic[key]) {
- idDic[key] = new ObjectId()
- }
- return idDic[key]
- }
- const strId = key => {
- return idDic[key].toString()
- }
- const wellFormedFolder = name => ({
- _id: id(name),
- name,
- folders: [],
- docs: [],
- fileRefs: [],
- })
- const wellFormedDoc = name => ({ _id: id(name), name })
- const wellFormedFileRef = name => ({ _id: id(name), name, hash: 'h' })
- const wellFormedProject = {
- _id: projectId,
- rootFolder: [
- {
- _id: rootFolderId,
- name: 'rootFolder',
- folders: [wellFormedFolder('f00'), wellFormedFolder('f01')],
- docs: [wellFormedDoc('d00'), wellFormedDoc('d01')],
- fileRefs: [wellFormedFileRef('fr00'), wellFormedFileRef('fr01')],
- },
- ],
- lastUpdated,
- lastUpdatedBy,
- }
- const testCases = [
- ...[{}, { rootFolder: undefined }, { rootFolder: '1234' }].map(
- (project, idx) => ({
- name: `bad rootFolder ${idx + 1}`,
- project: { _id: projectId, ...project, lastUpdated, lastUpdatedBy },
- expectFind: [
- {
- _id: null,
- projectId: projectId.toString(),
- msg: 'bad file-tree path',
- reason: 'bad rootFolder',
- path: 'rootFolder',
- },
- ],
- // FIXME: This is a bug in the script.
- expectFixError: 'Unexpected mongo path: rootFolder',
- })
- ),
- {
- name: `missing rootFolder`,
- project: { _id: projectId, rootFolder: [], lastUpdated, lastUpdatedBy },
- expectFind: [
- {
- _id: null,
- projectId: projectId.toString(),
- msg: 'bad file-tree path',
- reason: 'missing rootFolder',
- path: 'rootFolder.0',
- },
- ],
- expectFixStdout:
- '"gracefulShutdownInitiated":false,"processedLines":1,"success":1,"alreadyProcessed":0,"hash":0,"failed":0,"unmatched":0',
- expectProject: updatedProject => {
- expect(updatedProject.rootFolder[0]._id).to.be.an.instanceOf(ObjectId)
- expect(updatedProject).to.deep.equal({
- _id: projectId,
- rootFolder: [
- {
- _id: updatedProject.rootFolder[0]._id,
- name: 'rootFolder',
- folders: [],
- fileRefs: [],
- docs: [],
- },
- ],
- lastUpdated: lastUpdatedChanged,
- lastUpdatedBy: null,
- })
- },
- },
- {
- name: 'empty folder',
- project: {
- _id: projectId,
- rootFolder: [{ _id: '1234' }],
- lastUpdated,
- lastUpdatedBy,
- },
- expectFind: [
- { reason: 'bad folder id', path: 'rootFolder.0._id' },
- { reason: 'bad folder name', path: 'rootFolder.0.name' },
- { reason: 'missing .folders', path: 'rootFolder.0.folders' },
- { reason: 'missing .docs', path: 'rootFolder.0.docs' },
- { reason: 'missing .fileRefs', path: 'rootFolder.0.fileRefs' },
- ].map(entry => ({
- ...entry,
- _id: '1234',
- msg: 'bad file-tree path',
- projectId: String(projectId),
- })),
- // FIXME: This is a bug in the script.
- expectFixError: 'Unexpected mongo path: rootFolder.0._id',
- },
- {
- name: 'missing fields',
- project: {
- _id: projectId,
- rootFolder: [{ _id: rootFolderId }],
- lastUpdated,
- lastUpdatedBy,
- },
- expectFind: [
- { reason: 'bad folder name', path: 'rootFolder.0.name' },
- { reason: 'missing .folders', path: 'rootFolder.0.folders' },
- { reason: 'missing .docs', path: 'rootFolder.0.docs' },
- { reason: 'missing .fileRefs', path: 'rootFolder.0.fileRefs' },
- ].map(entry => ({
- ...entry,
- _id: rootFolderId.toString(),
- msg: 'bad file-tree path',
- projectId: String(projectId),
- })),
- expectFixStdout:
- '"gracefulShutdownInitiated":false,"processedLines":4,"success":4,"alreadyProcessed":0,"hash":0,"failed":0,"unmatched":0',
- expectProject: updatedProject => {
- expect(updatedProject).to.deep.equal({
- _id: projectId,
- rootFolder: [
- {
- _id: rootFolderId,
- docs: [],
- fileRefs: [],
- folders: [],
- name: 'rootFolder',
- },
- ],
- lastUpdated: lastUpdatedChanged,
- lastUpdatedBy: null,
- })
- },
- },
- {
- name: 'bad folder, bad doc, bad fileRef',
- project: {
- _id: projectId,
- rootFolder: [
- {
- _id: rootFolderId,
- name: 'rootFolder',
- folders: [null],
- docs: [null],
- fileRefs: [null, null],
- },
- ],
- lastUpdated,
- lastUpdatedBy,
- },
- expectFind: [
- {
- path: 'rootFolder.0.folders.0',
- reason: 'bad folder',
- },
- {
- path: 'rootFolder.0.docs.0',
- reason: 'bad doc',
- },
- {
- path: 'rootFolder.0.fileRefs.0',
- reason: 'bad file',
- },
- {
- path: 'rootFolder.0.fileRefs.1',
- reason: 'bad file',
- },
- ].map(entry => ({
- ...entry,
- _id: rootFolderId.toString(),
- projectId: projectId.toString(),
- msg: 'bad file-tree path',
- })),
- expectFixStdout:
- '"gracefulShutdownInitiated":false,"processedLines":4,"success":1,"alreadyProcessed":3,"hash":0,"failed":0,"unmatched":0',
- expectProject: updatedProject => {
- expect(updatedProject).to.deep.equal({
- _id: projectId,
- rootFolder: [
- {
- _id: rootFolderId,
- name: 'rootFolder',
- docs: [],
- fileRefs: [],
- folders: [],
- },
- ],
- lastUpdated: lastUpdatedChanged,
- lastUpdatedBy: null,
- })
- },
- },
- {
- name: 'bad [folder|doc|fileRef] id',
- project: {
- _id: projectId,
- rootFolder: [
- {
- _id: rootFolderId,
- name: 'rootFolder',
- folders: [
- { _id: 123, name: 'file-a', folders: [], docs: [], fileRefs: [] },
- { name: 'file-b', folders: [], docs: [], fileRefs: [] },
- ],
- docs: [{ _id: '456', name: 'doc-a' }, { name: 'doc-b' }],
- fileRefs: [{ _id: null, name: 'ref-a' }, { name: 'ref-b' }],
- },
- ],
- lastUpdated,
- lastUpdatedBy,
- },
- expectFind: [
- { reason: 'bad folder id', path: 'rootFolder.0.folders.0._id', _id: 123 },
- { reason: 'bad folder id', path: 'rootFolder.0.folders.1._id' },
- { reason: 'bad doc id', path: 'rootFolder.0.docs.0._id', _id: '456' },
- { reason: 'bad doc id', path: 'rootFolder.0.docs.1._id' },
- { reason: 'bad file id', path: 'rootFolder.0.fileRefs.0._id', _id: null },
- { reason: 'bad file id', path: 'rootFolder.0.fileRefs.1._id' },
- ].map(entry => ({
- ...entry,
- projectId: projectId.toString(),
- msg: 'bad file-tree path',
- })),
- expectFixStdout:
- '"gracefulShutdownInitiated":false,"processedLines":6,"success":3,"alreadyProcessed":3,"hash":0,"failed":0,"unmatched":0',
- expectProject: updatedProject => {
- expect(updatedProject).to.deep.equal({
- _id: projectId,
- rootFolder: [
- {
- _id: rootFolderId,
- name: 'rootFolder',
- folders: [
- { _id: 123, name: 'file-a', folders: [], docs: [], fileRefs: [] },
- {
- _id: updatedProject.rootFolder[0].folders[1]._id,
- name: 'file-b',
- folders: [],
- docs: [],
- fileRefs: [],
- },
- ],
- docs: [{ _id: '456', name: 'doc-a' }],
- fileRefs: [],
- },
- ],
- lastUpdated: lastUpdatedChanged,
- lastUpdatedBy: null,
- })
- },
- },
- {
- name: 'bad [folder|doc|fileRef] name',
- project: {
- _id: projectId,
- rootFolder: [
- {
- _id: rootFolderId,
- name: 'rootFolder',
- folders: [
- { _id: id('f00'), folders: [], docs: [], fileRefs: [] },
- { _id: id('f01'), name: 8, folders: [], docs: [], fileRefs: [] },
- ],
- docs: [{ _id: id('d00') }, { _id: id('d01'), name: null }],
- fileRefs: [
- { _id: id('fr00'), hash: 'h' },
- { _id: id('fr01'), hash: 'h', name: [] },
- ],
- },
- ],
- lastUpdated,
- lastUpdatedBy,
- },
- expectFind: [
- {
- reason: 'bad folder name',
- path: 'rootFolder.0.folders.0.name',
- _id: strId('f00'),
- },
- {
- reason: 'bad folder name',
- path: 'rootFolder.0.folders.1.name',
- _id: strId('f01'),
- },
- {
- reason: 'bad doc name',
- path: 'rootFolder.0.docs.0.name',
- _id: strId('d00'),
- },
- {
- reason: 'bad doc name',
- path: 'rootFolder.0.docs.1.name',
- _id: strId('d01'),
- },
- {
- reason: 'bad file name',
- path: 'rootFolder.0.fileRefs.0.name',
- _id: strId('fr00'),
- },
- {
- reason: 'bad file name',
- path: 'rootFolder.0.fileRefs.1.name',
- _id: strId('fr01'),
- },
- ].map(entry => ({
- ...entry,
- projectId: projectId.toString(),
- msg: 'bad file-tree path',
- })),
- expectFixStdout:
- '"gracefulShutdownInitiated":false,"processedLines":6,"success":6,"alreadyProcessed":0,"hash":0,"failed":0,"unmatched":0',
- expectProject: updatedProject => {
- expect(updatedProject).to.deep.equal({
- _id: projectId,
- rootFolder: [
- {
- _id: rootFolderId,
- name: 'rootFolder',
- folders: [
- {
- _id: id('f00'),
- name: 'untitled',
- folders: [],
- docs: [],
- fileRefs: [],
- },
- {
- _id: id('f01'),
- name: 'untitled-1',
- folders: [],
- docs: [],
- fileRefs: [],
- },
- ],
- docs: [
- { _id: id('d00'), name: 'untitled' },
- { _id: id('d01'), name: 'untitled-1' },
- ],
- fileRefs: [
- { _id: id('fr00'), hash: 'h', name: 'untitled' },
- { _id: id('fr01'), hash: 'h', name: 'untitled-1' },
- ],
- },
- ],
- lastUpdated: lastUpdatedChanged,
- lastUpdatedBy: null,
- })
- },
- },
- {
- name: 'bad file hash',
- project: {
- ...wellFormedProject,
- rootFolder: [
- {
- ...wellFormedProject.rootFolder[0],
- fileRefs: [
- { _id: id('fa'), name: 'ref-a', hash: null },
- { _id: id('fb'), name: 'ref-b', hash: {} },
- ],
- },
- ],
- lastUpdated,
- lastUpdatedBy,
- },
- expectFind: [
- { path: 'rootFolder.0.fileRefs.0.hash', _id: strId('fa') },
- { path: 'rootFolder.0.fileRefs.1.hash', _id: strId('fb') },
- ].map(entry => ({
- ...entry,
- projectId: projectId.toString(),
- reason: 'bad file hash',
- msg: 'bad file-tree path',
- })),
- expectFixError: new RegExp(
- `Missing file hash: ${projectId.toString()}/${id('fa').toString()}`
- ),
- },
- {
- name: 'well formed filetrees',
- project: wellFormedProject,
- expectFind: [],
- expectFixStdout:
- '"gracefulShutdownInitiated":false,"processedLines":1,"success":0,"alreadyProcessed":0,"hash":0,"failed":0,"unmatched":0',
- expectProject: updatedProject => {
- expect(updatedProject).to.deep.equal(wellFormedProject)
- },
- },
- {
- name: 'bug: shifted arrays in filetree',
- project: {
- _id: projectId,
- rootFolder: [
- {
- _id: rootFolderId,
- name: 'rootFolder',
- folders: [null, null, { ...wellFormedFolder('f02'), name: null }],
- docs: [null, null, { ...wellFormedDoc('d02'), name: null }],
- fileRefs: [null, null, { ...wellFormedFileRef('fr02'), name: null }],
- },
- ],
- lastUpdated,
- lastUpdatedBy,
- },
- expectFind: [
- {
- _id: rootFolderId.toString(),
- path: 'rootFolder.0.folders.0',
- reason: 'bad folder',
- },
- {
- _id: rootFolderId.toString(),
- path: 'rootFolder.0.folders.1',
- reason: 'bad folder',
- },
- {
- _id: strId('f02'),
- path: 'rootFolder.0.folders.2.name',
- reason: 'bad folder name',
- },
- {
- _id: rootFolderId.toString(),
- path: 'rootFolder.0.docs.0',
- reason: 'bad doc',
- },
- {
- _id: rootFolderId.toString(),
- path: 'rootFolder.0.docs.1',
- reason: 'bad doc',
- },
- {
- _id: strId('d02'),
- path: 'rootFolder.0.docs.2.name',
- reason: 'bad doc name',
- },
- {
- _id: rootFolderId.toString(),
- path: 'rootFolder.0.fileRefs.0',
- reason: 'bad file',
- },
- {
- _id: rootFolderId.toString(),
- path: 'rootFolder.0.fileRefs.1',
- reason: 'bad file',
- },
- {
- _id: strId('fr02'),
- path: 'rootFolder.0.fileRefs.2.name',
- reason: 'bad file name',
- },
- ].map(entry => ({
- ...entry,
- projectId: projectId.toString(),
- msg: 'bad file-tree path',
- })),
- expectFixStdout:
- '"gracefulShutdownInitiated":false,"processedLines":9,"success":4,"alreadyProcessed":5,"hash":0,"failed":0,"unmatched":0',
- expectProject: updatedProject => {
- expect(updatedProject).to.deep.equal({
- _id: projectId,
- rootFolder: [
- {
- _id: rootFolderId,
- name: 'rootFolder',
- folders: [{ ...wellFormedFolder('f02'), name: 'untitled' }],
- docs: [{ ...wellFormedDoc('d02'), name: 'untitled' }],
- fileRefs: [{ ...wellFormedFileRef('fr02'), name: 'untitled' }],
- },
- ],
- lastUpdated: lastUpdatedChanged,
- lastUpdatedBy: null,
- })
- },
- },
- {
- name: 'bug: shifted arrays in filetree folder',
- project: {
- _id: projectId,
- rootFolder: [
- {
- _id: rootFolderId,
- name: 'rootFolder',
- folders: [
- null,
- null,
- {
- ...wellFormedFolder('f02'),
- name: 'folder 1',
- folders: [null, null, { ...wellFormedFolder('f022') }],
- docs: [null, null, { ...wellFormedDoc('d022'), name: null }],
- fileRefs: [
- null,
- null,
- { ...wellFormedFileRef('fr022'), name: null },
- ],
- },
- ],
- docs: [],
- fileRefs: [],
- },
- ],
- lastUpdated,
- lastUpdatedBy,
- },
- expectFind: [
- {
- _id: rootFolderId.toString(),
- path: 'rootFolder.0.folders.0',
- reason: 'bad folder',
- },
- {
- _id: rootFolderId.toString(),
- path: 'rootFolder.0.folders.1',
- reason: 'bad folder',
- },
- {
- _id: strId('f02'),
- path: 'rootFolder.0.folders.2.folders.0',
- reason: 'bad folder',
- },
- {
- _id: strId('f02'),
- path: 'rootFolder.0.folders.2.folders.1',
- reason: 'bad folder',
- },
- {
- _id: strId('f02'),
- path: 'rootFolder.0.folders.2.docs.0',
- reason: 'bad doc',
- },
- {
- _id: strId('f02'),
- path: 'rootFolder.0.folders.2.docs.1',
- reason: 'bad doc',
- },
- {
- _id: strId('d022'),
- path: 'rootFolder.0.folders.2.docs.2.name',
- reason: 'bad doc name',
- },
- {
- _id: strId('f02'),
- path: 'rootFolder.0.folders.2.fileRefs.0',
- reason: 'bad file',
- },
- {
- _id: strId('f02'),
- path: 'rootFolder.0.folders.2.fileRefs.1',
- reason: 'bad file',
- },
- {
- _id: strId('fr022'),
- path: 'rootFolder.0.folders.2.fileRefs.2.name',
- reason: 'bad file name',
- },
- ].map(entry => ({
- ...entry,
- projectId: projectId.toString(),
- msg: 'bad file-tree path',
- })),
- expectFixStdout:
- '"gracefulShutdownInitiated":false,"processedLines":10,"success":4,"alreadyProcessed":6,"hash":0,"failed":0,"unmatched":0',
- expectProject: updatedProject => {
- expect(updatedProject).to.deep.equal({
- _id: projectId,
- rootFolder: [
- {
- _id: rootFolderId,
- name: 'rootFolder',
- folders: [
- {
- ...wellFormedFolder('f02'),
- name: 'folder 1',
- docs: [
- {
- ...wellFormedDoc('d022'),
- name: 'untitled',
- },
- ],
- fileRefs: [
- {
- ...wellFormedFileRef('fr022'),
- // FIXME: Make the names unique across different file types
- name: 'untitled',
- },
- ],
- folders: [
- {
- ...wellFormedFolder('f022'),
- name: 'f022',
- folders: [],
- docs: [],
- fileRefs: [],
- },
- ],
- },
- ],
- docs: [],
- fileRefs: [],
- },
- ],
- lastUpdated: lastUpdatedChanged,
- lastUpdatedBy: null,
- })
- },
- },
- ]
- describe('find_malformed_filetrees and fix_malformed_filetree scripts', function () {
- testCases.forEach(
- ({
- name,
- project,
- expectFind,
- expectFixStdout,
- expectFixError,
- expectProject,
- }) => {
- describe(name, function () {
- beforeEach(async function () {
- await db.projects.insertOne(project)
- })
- it('finds malformed filetree', async function () {
- const stdout = await runScriptFind()
- expect(stdout.map(line => JSON.parse(line))).to.deep.equal(expectFind)
- })
- if (expectFixError) {
- it('fails to fix malformed filetrees', async function () {
- await expect(runScriptFix(expectFind)).to.be.rejectedWith(
- expectFixError
- )
- })
- } else {
- it('fixes malformed filetrees', async function () {
- const { stdout } = await runScriptFix(expectFind)
- expect(expectFixStdout).to.be.a('string')
- expect(stdout).to.include(expectFixStdout)
- const [updatedProject] = await findProjects()
- if (updatedProject.lastUpdated > lastUpdated) {
- updatedProject.lastUpdated = lastUpdatedChanged
- }
- expectProject(updatedProject)
- })
- }
- })
- }
- )
- })
|