MalformedFiletreesTests.mjs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567
  1. import { exec } from 'node:child_process'
  2. import { promisify } from 'node:util'
  3. import { expect } from 'chai'
  4. import logger from '@overleaf/logger'
  5. import { filterOutput } from './helpers/settings.mjs'
  6. import { db, ObjectId } from '../../../app/src/infrastructure/mongodb.js'
  7. async function runScriptFind() {
  8. try {
  9. const result = await promisify(exec)(
  10. ['node', 'scripts/find_malformed_filetrees.mjs'].join(' ')
  11. )
  12. return result.stdout.split('\n').filter(filterOutput)
  13. } catch (error) {
  14. logger.error({ error }, 'script failed')
  15. throw error
  16. }
  17. }
  18. async function runScriptFix(instructions) {
  19. const adhocFile = instructions.map(entry => JSON.stringify(entry)).join('\n')
  20. try {
  21. return await promisify(exec)(
  22. [
  23. 'node',
  24. 'scripts/fix_malformed_filetree.mjs',
  25. `--logs=<(echo '${adhocFile}')`,
  26. ].join(' '),
  27. { shell: '/bin/bash' }
  28. )
  29. } catch (error) {
  30. logger.error({ error }, 'fix script failed unexpectedly')
  31. throw error
  32. }
  33. }
  34. const findProjects = () =>
  35. db.projects
  36. .find({}, { projection: { rootFolder: 1, _id: 1, version: 1 } })
  37. .toArray()
  38. const projectId = new ObjectId()
  39. const rootFolderId = new ObjectId()
  40. const idDic = {}
  41. const id = key => {
  42. if (!idDic[key]) {
  43. idDic[key] = new ObjectId()
  44. }
  45. return idDic[key]
  46. }
  47. const strId = key => {
  48. return idDic[key].toString()
  49. }
  50. const wellFormedFolder = name => ({
  51. _id: id(name),
  52. name,
  53. folders: [],
  54. docs: [],
  55. fileRefs: [],
  56. })
  57. const wellFormedDoc = name => ({ _id: id(name), name })
  58. const wellFormedFileRef = name => ({ _id: id(name), name, hash: 'h' })
  59. const wellFormedProject = {
  60. _id: projectId,
  61. rootFolder: [
  62. {
  63. _id: rootFolderId,
  64. name: 'rootFolder',
  65. folders: [wellFormedFolder('f00'), wellFormedFolder('f01')],
  66. docs: [wellFormedDoc('d00'), wellFormedDoc('d01')],
  67. fileRefs: [wellFormedFileRef('fr00'), wellFormedFileRef('fr01')],
  68. },
  69. ],
  70. }
  71. const testCases = [
  72. ...[{}, { rootFolder: undefined }, { rootFolder: '1234' }].map(
  73. (project, idx) => ({
  74. name: `bad rootFolder ${idx + 1}`,
  75. project: { _id: projectId, ...project },
  76. expectFind: [
  77. {
  78. _id: null,
  79. projectId: projectId.toString(),
  80. msg: 'bad file-tree path',
  81. reason: 'bad rootFolder',
  82. path: 'rootFolder',
  83. },
  84. ],
  85. // FIXME: This is a bug in the script.
  86. expectFixError: 'Unexpected mongo path: rootFolder',
  87. })
  88. ),
  89. {
  90. name: `missing rootFolder`,
  91. project: { _id: projectId, rootFolder: [] },
  92. expectFind: [
  93. {
  94. _id: null,
  95. projectId: projectId.toString(),
  96. msg: 'bad file-tree path',
  97. reason: 'missing rootFolder',
  98. path: 'rootFolder.0',
  99. },
  100. ],
  101. expectFixStdout:
  102. '"gracefulShutdownInitiated":false,"processedLines":1,"success":1,"alreadyProcessed":0,"hash":0,"failed":0,"unmatched":0',
  103. expectProject: updatedProject => {
  104. expect(updatedProject.rootFolder[0]._id).to.be.an.instanceOf(ObjectId)
  105. expect(updatedProject).to.deep.equal({
  106. _id: projectId,
  107. rootFolder: [
  108. {
  109. _id: updatedProject.rootFolder[0]._id,
  110. name: 'rootFolder',
  111. folders: [],
  112. fileRefs: [],
  113. docs: [],
  114. },
  115. ],
  116. })
  117. },
  118. },
  119. {
  120. name: 'empty folder',
  121. project: {
  122. _id: projectId,
  123. rootFolder: [{ _id: '1234' }],
  124. },
  125. expectFind: [
  126. { reason: 'bad folder id', path: 'rootFolder.0._id' },
  127. { reason: 'bad folder name', path: 'rootFolder.0.name' },
  128. { reason: 'missing .folders', path: 'rootFolder.0.folders' },
  129. { reason: 'missing .docs', path: 'rootFolder.0.docs' },
  130. { reason: 'missing .fileRefs', path: 'rootFolder.0.fileRefs' },
  131. ].map(entry => ({
  132. ...entry,
  133. _id: '1234',
  134. msg: 'bad file-tree path',
  135. projectId: String(projectId),
  136. })),
  137. // FIXME: This is a bug in the script.
  138. expectFixError: 'Unexpected mongo path: rootFolder.0._id',
  139. },
  140. {
  141. name: 'missing fields',
  142. project: {
  143. _id: projectId,
  144. rootFolder: [{ _id: rootFolderId }],
  145. },
  146. expectFind: [
  147. { reason: 'bad folder name', path: 'rootFolder.0.name' },
  148. { reason: 'missing .folders', path: 'rootFolder.0.folders' },
  149. { reason: 'missing .docs', path: 'rootFolder.0.docs' },
  150. { reason: 'missing .fileRefs', path: 'rootFolder.0.fileRefs' },
  151. ].map(entry => ({
  152. ...entry,
  153. _id: rootFolderId.toString(),
  154. msg: 'bad file-tree path',
  155. projectId: String(projectId),
  156. })),
  157. expectFixStdout:
  158. '"gracefulShutdownInitiated":false,"processedLines":4,"success":4,"alreadyProcessed":0,"hash":0,"failed":0,"unmatched":0',
  159. expectProject: updatedProject => {
  160. expect(updatedProject).to.deep.equal({
  161. _id: projectId,
  162. rootFolder: [
  163. {
  164. _id: rootFolderId,
  165. docs: [],
  166. fileRefs: [],
  167. folders: [],
  168. name: 'rootFolder',
  169. },
  170. ],
  171. })
  172. },
  173. },
  174. {
  175. name: 'bad folder, bad doc, bad fileRef',
  176. project: {
  177. _id: projectId,
  178. rootFolder: [
  179. {
  180. _id: rootFolderId,
  181. name: 'rootFolder',
  182. folders: [null],
  183. docs: [null],
  184. fileRefs: [null, null],
  185. },
  186. ],
  187. },
  188. expectFind: [
  189. {
  190. path: 'rootFolder.0.folders.0',
  191. reason: 'bad folder',
  192. },
  193. {
  194. path: 'rootFolder.0.docs.0',
  195. reason: 'bad doc',
  196. },
  197. {
  198. path: 'rootFolder.0.fileRefs.0',
  199. reason: 'bad file',
  200. },
  201. {
  202. path: 'rootFolder.0.fileRefs.1',
  203. reason: 'bad file',
  204. },
  205. ].map(entry => ({
  206. ...entry,
  207. _id: rootFolderId.toString(),
  208. projectId: projectId.toString(),
  209. msg: 'bad file-tree path',
  210. })),
  211. expectFixStdout:
  212. '"gracefulShutdownInitiated":false,"processedLines":4,"success":3,"alreadyProcessed":1,"hash":0,"failed":0,"unmatched":0',
  213. expectProject: updatedProject => {
  214. expect(updatedProject).to.deep.equal({
  215. _id: projectId,
  216. rootFolder: [
  217. {
  218. _id: rootFolderId,
  219. name: 'rootFolder',
  220. docs: [],
  221. fileRefs: [],
  222. folders: [],
  223. },
  224. ],
  225. })
  226. },
  227. },
  228. {
  229. name: 'bad [folder|doc|fileRef] id',
  230. project: {
  231. _id: projectId,
  232. rootFolder: [
  233. {
  234. _id: rootFolderId,
  235. name: 'rootFolder',
  236. folders: [
  237. { _id: 123, name: 'file-a', folders: [], docs: [], fileRefs: [] },
  238. { name: 'file-b', folders: [], docs: [], fileRefs: [] },
  239. ],
  240. docs: [{ _id: '456', name: 'doc-a' }, { name: 'doc-b' }],
  241. fileRefs: [{ _id: null, name: 'ref-a' }, { name: 'ref-b' }],
  242. },
  243. ],
  244. },
  245. expectFind: [
  246. { reason: 'bad folder id', path: 'rootFolder.0.folders.0._id', _id: 123 },
  247. { reason: 'bad folder id', path: 'rootFolder.0.folders.1._id' },
  248. { reason: 'bad doc id', path: 'rootFolder.0.docs.0._id', _id: '456' },
  249. { reason: 'bad doc id', path: 'rootFolder.0.docs.1._id' },
  250. { reason: 'bad file id', path: 'rootFolder.0.fileRefs.0._id', _id: null },
  251. { reason: 'bad file id', path: 'rootFolder.0.fileRefs.1._id' },
  252. ].map(entry => ({
  253. ...entry,
  254. projectId: projectId.toString(),
  255. msg: 'bad file-tree path',
  256. })),
  257. expectFixStdout:
  258. '"gracefulShutdownInitiated":false,"processedLines":6,"success":3,"alreadyProcessed":3,"hash":0,"failed":0,"unmatched":0',
  259. expectProject: updatedProject => {
  260. expect(updatedProject).to.deep.equal({
  261. _id: projectId,
  262. rootFolder: [
  263. {
  264. _id: rootFolderId,
  265. name: 'rootFolder',
  266. folders: [
  267. { _id: 123, name: 'file-a', folders: [], docs: [], fileRefs: [] },
  268. {
  269. _id: updatedProject.rootFolder[0].folders[1]._id,
  270. name: 'file-b',
  271. folders: [],
  272. docs: [],
  273. fileRefs: [],
  274. },
  275. ],
  276. docs: [{ _id: '456', name: 'doc-a' }],
  277. fileRefs: [],
  278. },
  279. ],
  280. })
  281. },
  282. },
  283. {
  284. name: 'bad [folder|doc|fileRef] name',
  285. project: {
  286. _id: projectId,
  287. rootFolder: [
  288. {
  289. _id: rootFolderId,
  290. name: 'rootFolder',
  291. folders: [
  292. { _id: id('f00'), folders: [], docs: [], fileRefs: [] },
  293. { _id: id('f01'), name: 8, folders: [], docs: [], fileRefs: [] },
  294. ],
  295. docs: [{ _id: id('d00') }, { _id: id('d01'), name: null }],
  296. fileRefs: [
  297. { _id: id('fr00'), hash: 'h' },
  298. { _id: id('fr01'), hash: 'h', name: [] },
  299. ],
  300. },
  301. ],
  302. },
  303. expectFind: [
  304. {
  305. reason: 'bad folder name',
  306. path: 'rootFolder.0.folders.0.name',
  307. _id: strId('f00'),
  308. },
  309. {
  310. reason: 'bad folder name',
  311. path: 'rootFolder.0.folders.1.name',
  312. _id: strId('f01'),
  313. },
  314. {
  315. reason: 'bad doc name',
  316. path: 'rootFolder.0.docs.0.name',
  317. _id: strId('d00'),
  318. },
  319. {
  320. reason: 'bad doc name',
  321. path: 'rootFolder.0.docs.1.name',
  322. _id: strId('d01'),
  323. },
  324. {
  325. reason: 'bad file name',
  326. path: 'rootFolder.0.fileRefs.0.name',
  327. _id: strId('fr00'),
  328. },
  329. {
  330. reason: 'bad file name',
  331. path: 'rootFolder.0.fileRefs.1.name',
  332. _id: strId('fr01'),
  333. },
  334. ].map(entry => ({
  335. ...entry,
  336. projectId: projectId.toString(),
  337. msg: 'bad file-tree path',
  338. })),
  339. expectFixStdout:
  340. '"gracefulShutdownInitiated":false,"processedLines":6,"success":6,"alreadyProcessed":0,"hash":0,"failed":0,"unmatched":0',
  341. expectProject: updatedProject => {
  342. expect(updatedProject).to.deep.equal({
  343. _id: projectId,
  344. rootFolder: [
  345. {
  346. _id: rootFolderId,
  347. name: 'rootFolder',
  348. folders: [
  349. {
  350. _id: id('f00'),
  351. name: 'untitled',
  352. folders: [],
  353. docs: [],
  354. fileRefs: [],
  355. },
  356. {
  357. _id: id('f01'),
  358. name: 'untitled-1',
  359. folders: [],
  360. docs: [],
  361. fileRefs: [],
  362. },
  363. ],
  364. docs: [
  365. { _id: id('d00'), name: 'untitled' },
  366. { _id: id('d01'), name: 'untitled-1' },
  367. ],
  368. fileRefs: [
  369. { _id: id('fr00'), hash: 'h', name: 'untitled' },
  370. { _id: id('fr01'), hash: 'h', name: 'untitled-1' },
  371. ],
  372. },
  373. ],
  374. })
  375. },
  376. },
  377. {
  378. name: 'bad file hash',
  379. project: {
  380. ...wellFormedProject,
  381. rootFolder: [
  382. {
  383. ...wellFormedProject.rootFolder[0],
  384. fileRefs: [
  385. { _id: id('fa'), name: 'ref-a', hash: null },
  386. { _id: id('fb'), name: 'ref-b', hash: {} },
  387. ],
  388. },
  389. ],
  390. },
  391. expectFind: [
  392. { path: 'rootFolder.0.fileRefs.0.hash', _id: strId('fa') },
  393. { path: 'rootFolder.0.fileRefs.1.hash', _id: strId('fb') },
  394. ].map(entry => ({
  395. ...entry,
  396. projectId: projectId.toString(),
  397. reason: 'bad file hash',
  398. msg: 'bad file-tree path',
  399. })),
  400. expectFixError: new RegExp(
  401. `Missing file hash: ${projectId.toString()}/${id('fa').toString()}`
  402. ),
  403. },
  404. {
  405. name: 'well formed filetrees',
  406. project: wellFormedProject,
  407. expectFind: [],
  408. expectFixStdout:
  409. '"gracefulShutdownInitiated":false,"processedLines":1,"success":0,"alreadyProcessed":0,"hash":0,"failed":0,"unmatched":0',
  410. expectProject: updatedProject => {
  411. expect(updatedProject).to.deep.equal(wellFormedProject)
  412. },
  413. },
  414. {
  415. name: 'bug: shifted arrays in filetree',
  416. project: {
  417. _id: projectId,
  418. rootFolder: [
  419. {
  420. _id: rootFolderId,
  421. name: 'rootFolder',
  422. folders: [null, null, { ...wellFormedFolder('f02'), name: null }],
  423. docs: [null, null, { ...wellFormedDoc('d02'), name: null }],
  424. fileRefs: [null, null, { ...wellFormedFileRef('fr02'), name: null }],
  425. },
  426. ],
  427. },
  428. expectFind: [
  429. {
  430. _id: rootFolderId.toString(),
  431. path: 'rootFolder.0.folders.0',
  432. reason: 'bad folder',
  433. },
  434. {
  435. _id: rootFolderId.toString(),
  436. path: 'rootFolder.0.folders.1',
  437. reason: 'bad folder',
  438. },
  439. {
  440. _id: strId('f02'),
  441. path: 'rootFolder.0.folders.2.name',
  442. reason: 'bad folder name',
  443. },
  444. {
  445. _id: rootFolderId.toString(),
  446. path: 'rootFolder.0.docs.0',
  447. reason: 'bad doc',
  448. },
  449. {
  450. _id: rootFolderId.toString(),
  451. path: 'rootFolder.0.docs.1',
  452. reason: 'bad doc',
  453. },
  454. {
  455. _id: strId('d02'),
  456. path: 'rootFolder.0.docs.2.name',
  457. reason: 'bad doc name',
  458. },
  459. {
  460. _id: rootFolderId.toString(),
  461. path: 'rootFolder.0.fileRefs.0',
  462. reason: 'bad file',
  463. },
  464. {
  465. _id: rootFolderId.toString(),
  466. path: 'rootFolder.0.fileRefs.1',
  467. reason: 'bad file',
  468. },
  469. {
  470. _id: strId('fr02'),
  471. path: 'rootFolder.0.fileRefs.2.name',
  472. reason: 'bad file name',
  473. },
  474. ].map(entry => ({
  475. ...entry,
  476. projectId: projectId.toString(),
  477. msg: 'bad file-tree path',
  478. })),
  479. expectFixStdout:
  480. '"gracefulShutdownInitiated":false,"processedLines":9,"success":6,"alreadyProcessed":3,"hash":0,"failed":0,"unmatched":0',
  481. expectProject: updatedProject => {
  482. expect(updatedProject).to.deep.equal({
  483. _id: projectId,
  484. rootFolder: [
  485. {
  486. _id: rootFolderId,
  487. name: 'rootFolder',
  488. // FIXME: The 3 arrays should only contain 1 item: the well-formed item with the name 'untitled'.
  489. folders: [
  490. { ...wellFormedFolder('f02'), name: null },
  491. null,
  492. { name: 'untitled' },
  493. ],
  494. docs: [
  495. { ...wellFormedDoc('d02'), name: null },
  496. null,
  497. { name: 'untitled' },
  498. ],
  499. fileRefs: [
  500. { ...wellFormedFileRef('fr02'), name: null },
  501. null,
  502. { name: 'untitled' },
  503. ],
  504. },
  505. ],
  506. })
  507. },
  508. },
  509. ]
  510. describe('find_malformed_filetrees and fix_malformed_filetree scripts', function () {
  511. testCases.forEach(
  512. ({
  513. name,
  514. project,
  515. expectFind,
  516. expectFixStdout,
  517. expectFixError,
  518. expectProject,
  519. }) => {
  520. describe(name, function () {
  521. beforeEach(async function () {
  522. await db.projects.insertOne(project)
  523. })
  524. it('finds malformed filetree', async function () {
  525. const stdout = await runScriptFind()
  526. expect(stdout.map(line => JSON.parse(line))).to.deep.equal(expectFind)
  527. })
  528. if (expectFixError) {
  529. it('fails to fix malformed filetrees', async function () {
  530. await expect(runScriptFix(expectFind)).to.be.rejectedWith(
  531. expectFixError
  532. )
  533. })
  534. } else {
  535. it('fixes malformed filetrees', async function () {
  536. const { stdout } = await runScriptFix(expectFind)
  537. expect(expectFixStdout).to.be.a('string')
  538. expect(stdout).to.include(expectFixStdout)
  539. const [updatedProject] = await findProjects()
  540. expectProject(updatedProject)
  541. })
  542. }
  543. })
  544. }
  545. )
  546. })