FileTreeDiffTests.js 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783
  1. import { expect } from 'chai'
  2. import crypto from 'node:crypto'
  3. import mongodb from 'mongodb-legacy'
  4. import nock from 'nock'
  5. import * as ProjectHistoryClient from './helpers/ProjectHistoryClient.js'
  6. import * as ProjectHistoryApp from './helpers/ProjectHistoryApp.js'
  7. const { ObjectId } = mongodb
  8. const MockHistoryStore = () => nock('http://127.0.0.1:3100')
  9. const MockWeb = () => nock('http://127.0.0.1:3000')
  10. const sha = data => crypto.createHash('sha1').update(data).digest('hex')
  11. describe('FileTree Diffs', function () {
  12. beforeEach(async function () {
  13. await ProjectHistoryApp.ensureRunning()
  14. this.historyId = new ObjectId().toString()
  15. this.projectId = new ObjectId().toString()
  16. MockHistoryStore().post('/api/projects').reply(200, {
  17. projectId: this.historyId,
  18. })
  19. MockWeb()
  20. .get(`/project/${this.projectId}/details`)
  21. .reply(200, {
  22. name: 'Test Project',
  23. overleaf: { history: { id: this.historyId } },
  24. })
  25. await ProjectHistoryClient.initializeProject(this.historyId)
  26. })
  27. afterEach(function () {
  28. return nock.cleanAll()
  29. })
  30. it('should return a diff of the updates to a doc from a single chunk', async function () {
  31. MockHistoryStore()
  32. .get(`/api/projects/${this.historyId}/versions/7/history`)
  33. .reply(200, {
  34. chunk: {
  35. history: {
  36. snapshot: {
  37. files: {
  38. 'foo.tex': {
  39. hash: sha('mock-sha-foo'),
  40. stringLength: 42,
  41. },
  42. 'renamed.tex': {
  43. hash: sha('mock-sha-renamed'),
  44. stringLength: 42,
  45. },
  46. 'deleted.tex': {
  47. hash: sha('mock-sha-deleted'),
  48. stringLength: 42,
  49. },
  50. },
  51. },
  52. changes: [
  53. {
  54. operations: [
  55. {
  56. pathname: 'renamed.tex',
  57. newPathname: 'newName.tex',
  58. },
  59. ],
  60. timestamp: '2017-12-04T10:29:17.786Z',
  61. authors: [31],
  62. },
  63. {
  64. operations: [
  65. {
  66. pathname: 'foo.tex',
  67. textOperation: ['lorem ipsum'],
  68. },
  69. ],
  70. timestamp: '2017-12-04T10:29:17.786Z',
  71. authors: [31],
  72. },
  73. {
  74. operations: [
  75. {
  76. pathname: 'deleted.tex',
  77. newPathname: '',
  78. },
  79. ],
  80. timestamp: '2017-12-04T10:29:22.905Z',
  81. authors: [31],
  82. },
  83. {
  84. operations: [
  85. {
  86. file: {
  87. hash: sha('new-sha'),
  88. stringLength: 42,
  89. },
  90. pathname: 'added.tex',
  91. },
  92. ],
  93. timestamp: '2017-12-04T10:29:22.905Z',
  94. authors: [31],
  95. },
  96. ],
  97. },
  98. startVersion: 3,
  99. },
  100. authors: [{ id: 31, email: 'james.allen@overleaf.com', name: 'James' }],
  101. })
  102. const { diff } = await ProjectHistoryClient.getFileTreeDiff(
  103. this.projectId,
  104. 3,
  105. 7
  106. )
  107. expect(diff).to.deep.equal({
  108. diff: [
  109. {
  110. pathname: 'foo.tex',
  111. operation: 'edited',
  112. },
  113. {
  114. pathname: 'deleted.tex',
  115. operation: 'removed',
  116. deletedAtV: 5,
  117. editable: true,
  118. },
  119. {
  120. newPathname: 'newName.tex',
  121. pathname: 'renamed.tex',
  122. operation: 'renamed',
  123. editable: true,
  124. },
  125. {
  126. pathname: 'added.tex',
  127. operation: 'added',
  128. editable: true,
  129. },
  130. ],
  131. })
  132. })
  133. it('should return a diff of the updates to a doc across multiple chunks', async function () {
  134. MockHistoryStore()
  135. .get(`/api/projects/${this.historyId}/versions/5/history`)
  136. .reply(200, {
  137. chunk: {
  138. history: {
  139. snapshot: {
  140. files: {
  141. 'foo.tex': {
  142. // Updated in this chunk
  143. hash: sha('mock-sha-foo'),
  144. stringLength: 42,
  145. },
  146. 'bar.tex': {
  147. // Updated in the next chunk
  148. hash: sha('mock-sha-bar'),
  149. stringLength: 42,
  150. },
  151. 'baz.tex': {
  152. // Not updated
  153. hash: sha('mock-sha-bar'),
  154. stringLength: 42,
  155. },
  156. 'renamed.tex': {
  157. hash: sha('mock-sha-renamed'),
  158. stringLength: 42,
  159. },
  160. 'deleted.tex': {
  161. hash: sha('mock-sha-deleted'),
  162. stringLength: 42,
  163. },
  164. },
  165. },
  166. changes: [
  167. {
  168. operations: [
  169. {
  170. pathname: 'renamed.tex',
  171. newPathname: 'newName.tex',
  172. },
  173. ],
  174. timestamp: '2017-12-04T10:29:17.786Z',
  175. authors: [31],
  176. },
  177. {
  178. operations: [
  179. {
  180. pathname: 'foo.tex',
  181. textOperation: ['lorem ipsum'],
  182. },
  183. ],
  184. timestamp: '2017-12-04T10:29:19.786Z',
  185. authors: [31],
  186. },
  187. {
  188. operations: [
  189. {
  190. pathname: 'deleted.tex',
  191. newPathname: '',
  192. },
  193. ],
  194. timestamp: '2017-12-04T10:29:22.905Z',
  195. authors: [31],
  196. },
  197. ],
  198. },
  199. startVersion: 2,
  200. },
  201. authors: [{ id: 31, email: 'james.allen@overleaf.com', name: 'James' }],
  202. })
  203. MockHistoryStore()
  204. .get(`/api/projects/${this.historyId}/versions/7/history`)
  205. .reply(200, {
  206. chunk: {
  207. history: {
  208. snapshot: {
  209. files: {
  210. 'foo.tex': {
  211. hash: sha('mock-sha-foo'),
  212. stringLength: 42,
  213. },
  214. 'baz.tex': {
  215. hash: sha('mock-sha-bar'),
  216. stringLength: 42,
  217. },
  218. 'newName.tex': {
  219. hash: sha('mock-sha-renamed'),
  220. stringLength: 42,
  221. },
  222. },
  223. },
  224. changes: [
  225. {
  226. operations: [
  227. {
  228. file: {
  229. hash: sha('new-sha'),
  230. stringLength: 42,
  231. },
  232. pathname: 'added.tex',
  233. },
  234. ],
  235. timestamp: '2017-12-04T10:29:22.905Z',
  236. authors: [31],
  237. },
  238. {
  239. operations: [
  240. {
  241. pathname: 'bar.tex',
  242. textOperation: ['lorem ipsum'],
  243. },
  244. ],
  245. timestamp: '2017-12-04T10:29:23.786Z',
  246. authors: [31],
  247. },
  248. ],
  249. },
  250. startVersion: 5,
  251. },
  252. authors: [{ id: 31, email: 'james.allen@overleaf.com', name: 'James' }],
  253. })
  254. const { diff } = await ProjectHistoryClient.getFileTreeDiff(
  255. this.projectId,
  256. 2,
  257. 7
  258. )
  259. expect(diff).to.deep.equal({
  260. diff: [
  261. {
  262. pathname: 'foo.tex',
  263. operation: 'edited',
  264. },
  265. {
  266. pathname: 'bar.tex',
  267. operation: 'edited',
  268. },
  269. {
  270. pathname: 'baz.tex',
  271. editable: true,
  272. },
  273. {
  274. pathname: 'deleted.tex',
  275. operation: 'removed',
  276. deletedAtV: 4,
  277. editable: true,
  278. },
  279. {
  280. newPathname: 'newName.tex',
  281. pathname: 'renamed.tex',
  282. operation: 'renamed',
  283. editable: true,
  284. },
  285. {
  286. pathname: 'added.tex',
  287. operation: 'added',
  288. editable: true,
  289. },
  290. ],
  291. })
  292. })
  293. it('should return a diff that includes multiple renames', async function () {
  294. MockHistoryStore()
  295. .get(`/api/projects/${this.historyId}/versions/5/history`)
  296. .reply(200, {
  297. chunk: {
  298. history: {
  299. snapshot: {
  300. files: {
  301. 'one.tex': {
  302. hash: sha('mock-sha'),
  303. stringLength: 42,
  304. },
  305. },
  306. },
  307. changes: [
  308. {
  309. operations: [
  310. {
  311. pathname: 'one.tex',
  312. newPathname: 'two.tex',
  313. },
  314. ],
  315. timestamp: '2017-12-04T10:29:17.786Z',
  316. authors: [31],
  317. },
  318. {
  319. operations: [
  320. {
  321. pathname: 'two.tex',
  322. newPathname: 'three.tex',
  323. },
  324. ],
  325. timestamp: '2017-12-04T10:29:22.905Z',
  326. authors: [31],
  327. },
  328. ],
  329. },
  330. startVersion: 3,
  331. },
  332. authors: [{ id: 31, email: 'james.allen@overleaf.com', name: 'James' }],
  333. })
  334. const { diff } = await ProjectHistoryClient.getFileTreeDiff(
  335. this.projectId,
  336. 3,
  337. 5
  338. )
  339. expect(diff).to.deep.equal({
  340. diff: [
  341. {
  342. newPathname: 'three.tex',
  343. pathname: 'one.tex',
  344. operation: 'renamed',
  345. editable: true,
  346. },
  347. ],
  348. })
  349. })
  350. it('should handle deleting then re-adding a file', async function () {
  351. MockHistoryStore()
  352. .get(`/api/projects/${this.historyId}/versions/5/history`)
  353. .reply(200, {
  354. chunk: {
  355. history: {
  356. snapshot: {
  357. files: {
  358. 'one.tex': {
  359. hash: sha('mock-sha'),
  360. stringLength: 42,
  361. },
  362. },
  363. },
  364. changes: [
  365. {
  366. operations: [
  367. {
  368. pathname: 'one.tex',
  369. newPathname: '',
  370. },
  371. ],
  372. timestamp: '2017-12-04T10:29:17.786Z',
  373. authors: [31],
  374. },
  375. {
  376. operations: [
  377. {
  378. pathname: 'one.tex',
  379. file: {
  380. hash: sha('mock-sha'),
  381. },
  382. },
  383. ],
  384. timestamp: '2017-12-04T10:29:22.905Z',
  385. authors: [31],
  386. },
  387. ],
  388. },
  389. startVersion: 3,
  390. },
  391. authors: [{ id: 31, email: 'james.allen@overleaf.com', name: 'James' }],
  392. })
  393. const { diff } = await ProjectHistoryClient.getFileTreeDiff(
  394. this.projectId,
  395. 3,
  396. 5
  397. )
  398. expect(diff).to.deep.equal({
  399. diff: [
  400. {
  401. pathname: 'one.tex',
  402. operation: 'added',
  403. editable: null,
  404. },
  405. ],
  406. })
  407. })
  408. it('should handle deleting the renaming a file to the same place', async function () {
  409. MockHistoryStore()
  410. .get(`/api/projects/${this.historyId}/versions/5/history`)
  411. .reply(200, {
  412. chunk: {
  413. history: {
  414. snapshot: {
  415. files: {
  416. 'one.tex': {
  417. hash: sha('mock-sha-one'),
  418. stringLength: 42,
  419. },
  420. 'two.tex': {
  421. hash: sha('mock-sha-two'),
  422. stringLength: 42,
  423. },
  424. },
  425. },
  426. changes: [
  427. {
  428. operations: [
  429. {
  430. pathname: 'one.tex',
  431. newPathname: '',
  432. },
  433. ],
  434. timestamp: '2017-12-04T10:29:17.786Z',
  435. authors: [31],
  436. },
  437. {
  438. operations: [
  439. {
  440. pathname: 'two.tex',
  441. newPathname: 'one.tex',
  442. },
  443. ],
  444. timestamp: '2017-12-04T10:29:22.905Z',
  445. authors: [31],
  446. },
  447. ],
  448. },
  449. startVersion: 3,
  450. },
  451. authors: [{ id: 31, email: 'james.allen@overleaf.com', name: 'James' }],
  452. })
  453. const { diff } = await ProjectHistoryClient.getFileTreeDiff(
  454. this.projectId,
  455. 3,
  456. 5
  457. )
  458. expect(diff).to.deep.equal({
  459. diff: [
  460. {
  461. pathname: 'two.tex',
  462. newPathname: 'one.tex',
  463. operation: 'renamed',
  464. editable: true,
  465. },
  466. ],
  467. })
  468. })
  469. it('should handle adding then renaming a file', async function () {
  470. MockHistoryStore()
  471. .get(`/api/projects/${this.historyId}/versions/5/history`)
  472. .reply(200, {
  473. chunk: {
  474. history: {
  475. snapshot: {
  476. files: {},
  477. },
  478. changes: [
  479. {
  480. operations: [
  481. {
  482. pathname: 'one.tex',
  483. file: {
  484. hash: sha('mock-sha'),
  485. stringLength: 42,
  486. },
  487. },
  488. ],
  489. timestamp: '2017-12-04T10:29:17.786Z',
  490. authors: [31],
  491. },
  492. {
  493. operations: [
  494. {
  495. pathname: 'one.tex',
  496. newPathname: 'two.tex',
  497. },
  498. ],
  499. timestamp: '2017-12-04T10:29:22.905Z',
  500. authors: [31],
  501. },
  502. ],
  503. },
  504. startVersion: 3,
  505. },
  506. authors: [{ id: 31, email: 'james.allen@overleaf.com', name: 'James' }],
  507. })
  508. const { diff } = await ProjectHistoryClient.getFileTreeDiff(
  509. this.projectId,
  510. 3,
  511. 5
  512. )
  513. expect(diff).to.deep.equal({
  514. diff: [
  515. {
  516. pathname: 'two.tex',
  517. operation: 'added',
  518. editable: true,
  519. },
  520. ],
  521. })
  522. })
  523. it('should return 422 with a chunk with an invalid rename', async function () {
  524. MockHistoryStore()
  525. .get(`/api/projects/${this.historyId}/versions/6/history`)
  526. .reply(200, {
  527. chunk: {
  528. history: {
  529. snapshot: {
  530. files: {
  531. 'foo.tex': {
  532. hash: sha('mock-sha-foo'),
  533. stringLength: 42,
  534. },
  535. 'bar.tex': {
  536. hash: sha('mock-sha-bar'),
  537. stringLength: 42,
  538. },
  539. },
  540. },
  541. changes: [
  542. {
  543. operations: [
  544. {
  545. pathname: 'foo.tex',
  546. newPathname: 'bar.tex',
  547. },
  548. ],
  549. timestamp: '2017-12-04T10:29:17.786Z',
  550. authors: [31],
  551. },
  552. ],
  553. },
  554. startVersion: 5,
  555. },
  556. authors: [{ id: 31, email: 'james.allen@overleaf.com', name: 'James' }],
  557. })
  558. const { statusCode } = await ProjectHistoryClient.getFileTreeDiff(
  559. this.projectId,
  560. 5,
  561. 6
  562. )
  563. expect(statusCode).to.equal(422)
  564. })
  565. it('should return 200 with a chunk with an invalid add', async function () {
  566. MockHistoryStore()
  567. .get(`/api/projects/${this.historyId}/versions/6/history`)
  568. .reply(200, {
  569. chunk: {
  570. history: {
  571. snapshot: {
  572. files: {
  573. 'foo.tex': {
  574. hash: sha('mock-sha-foo'),
  575. stringLength: 42,
  576. },
  577. },
  578. },
  579. changes: [
  580. {
  581. operations: [
  582. {
  583. file: {
  584. hash: sha('new-sha'),
  585. },
  586. pathname: 'foo.tex',
  587. },
  588. ],
  589. timestamp: '2017-12-04T10:29:17.786Z',
  590. authors: [31],
  591. },
  592. ],
  593. },
  594. startVersion: 5,
  595. },
  596. authors: [{ id: 31, email: 'james.allen@overleaf.com', name: 'James' }],
  597. })
  598. const { diff, statusCode } = await ProjectHistoryClient.getFileTreeDiff(
  599. this.projectId,
  600. 5,
  601. 6
  602. )
  603. expect(diff).to.deep.equal({
  604. diff: [
  605. {
  606. pathname: 'foo.tex',
  607. operation: 'added',
  608. editable: null,
  609. },
  610. ],
  611. })
  612. expect(statusCode).to.equal(200)
  613. })
  614. it('should handle edits of missing/invalid files ', async function () {
  615. MockHistoryStore()
  616. .get(`/api/projects/${this.historyId}/versions/5/history`)
  617. .reply(200, {
  618. chunk: {
  619. history: {
  620. snapshot: {
  621. files: {},
  622. },
  623. changes: [
  624. {
  625. operations: [
  626. {
  627. pathname: 'new.tex',
  628. textOperation: ['lorem ipsum'],
  629. },
  630. ],
  631. timestamp: '2017-12-04T10:29:18.786Z',
  632. authors: [31],
  633. },
  634. {
  635. operations: [
  636. {
  637. pathname: '',
  638. textOperation: ['lorem ipsum'],
  639. },
  640. ],
  641. timestamp: '2017-12-04T10:29:17.786Z',
  642. authors: [31],
  643. },
  644. ],
  645. },
  646. startVersion: 3,
  647. },
  648. authors: [{ id: 31, email: 'james.allen@overleaf.com', name: 'James' }],
  649. })
  650. const { diff } = await ProjectHistoryClient.getFileTreeDiff(
  651. this.projectId,
  652. 3,
  653. 5
  654. )
  655. expect(diff).to.deep.equal({
  656. diff: [
  657. {
  658. operation: 'edited',
  659. pathname: 'new.tex',
  660. },
  661. ],
  662. })
  663. })
  664. it('should handle deletions of missing/invalid files ', async function () {
  665. MockHistoryStore()
  666. .get(`/api/projects/${this.historyId}/versions/5/history`)
  667. .reply(200, {
  668. chunk: {
  669. history: {
  670. snapshot: {
  671. files: {},
  672. },
  673. changes: [
  674. {
  675. operations: [
  676. {
  677. pathname: 'missing.tex',
  678. newPathname: '',
  679. },
  680. ],
  681. timestamp: '2017-12-04T10:29:17.786Z',
  682. authors: [31],
  683. },
  684. {
  685. operations: [
  686. {
  687. pathname: '',
  688. newPathname: '',
  689. },
  690. ],
  691. timestamp: '2017-12-04T10:29:17.786Z',
  692. authors: [31],
  693. },
  694. ],
  695. },
  696. startVersion: 3,
  697. },
  698. authors: [{ id: 31, email: 'james.allen@overleaf.com', name: 'James' }],
  699. })
  700. const { diff } = await ProjectHistoryClient.getFileTreeDiff(
  701. this.projectId,
  702. 3,
  703. 5
  704. )
  705. expect(diff).to.deep.equal({
  706. diff: [],
  707. })
  708. })
  709. return it('should handle renames of missing/invalid files ', async function () {
  710. MockHistoryStore()
  711. .get(`/api/projects/${this.historyId}/versions/5/history`)
  712. .reply(200, {
  713. chunk: {
  714. history: {
  715. snapshot: {
  716. files: {},
  717. },
  718. changes: [
  719. {
  720. operations: [
  721. {
  722. pathname: 'missing.tex',
  723. newPathname: 'missing-renamed.tex',
  724. },
  725. ],
  726. timestamp: '2017-12-04T10:29:17.786Z',
  727. authors: [31],
  728. },
  729. {
  730. operations: [
  731. {
  732. pathname: '',
  733. newPathname: 'missing-renamed-other.tex',
  734. },
  735. ],
  736. timestamp: '2017-12-04T10:29:17.786Z',
  737. authors: [31],
  738. },
  739. ],
  740. },
  741. startVersion: 3,
  742. },
  743. authors: [{ id: 31, email: 'james.allen@overleaf.com', name: 'James' }],
  744. })
  745. const { diff } = await ProjectHistoryClient.getFileTreeDiff(
  746. this.projectId,
  747. 3,
  748. 5
  749. )
  750. expect(diff).to.deep.equal({
  751. diff: [],
  752. })
  753. })
  754. })