FileTreeDiffTests.js 23 KB

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