FileTreeDiffTests.js 23 KB

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