FileTreeDiffTests.js 22 KB

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