FileTreeDiffTests.js 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879
  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 then 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. operation: 'added',
  454. editable: null,
  455. },
  456. ],
  457. })
  458. return done()
  459. }
  460. )
  461. })
  462. it('should handle deleting the renaming a file to the same place', function (done) {
  463. MockHistoryStore()
  464. .get(`/api/projects/${this.historyId}/versions/5/history`)
  465. .reply(200, {
  466. chunk: {
  467. history: {
  468. snapshot: {
  469. files: {
  470. 'one.tex': {
  471. hash: sha('mock-sha-one'),
  472. stringLength: 42,
  473. },
  474. 'two.tex': {
  475. hash: sha('mock-sha-two'),
  476. stringLength: 42,
  477. },
  478. },
  479. },
  480. changes: [
  481. {
  482. operations: [
  483. {
  484. pathname: 'one.tex',
  485. newPathname: '',
  486. },
  487. ],
  488. timestamp: '2017-12-04T10:29:17.786Z',
  489. authors: [31],
  490. },
  491. {
  492. operations: [
  493. {
  494. pathname: 'two.tex',
  495. newPathname: 'one.tex',
  496. },
  497. ],
  498. timestamp: '2017-12-04T10:29:22.905Z',
  499. authors: [31],
  500. },
  501. ],
  502. },
  503. startVersion: 3,
  504. },
  505. authors: [{ id: 31, email: 'james.allen@overleaf.com', name: 'James' }],
  506. })
  507. return ProjectHistoryClient.getFileTreeDiff(
  508. this.projectId,
  509. 3,
  510. 5,
  511. (error, diff) => {
  512. if (error != null) {
  513. throw error
  514. }
  515. expect(diff).to.deep.equal({
  516. diff: [
  517. {
  518. pathname: 'two.tex',
  519. newPathname: 'one.tex',
  520. operation: 'renamed',
  521. editable: true,
  522. },
  523. ],
  524. })
  525. return done()
  526. }
  527. )
  528. })
  529. it('should handle adding then renaming a file', function (done) {
  530. MockHistoryStore()
  531. .get(`/api/projects/${this.historyId}/versions/5/history`)
  532. .reply(200, {
  533. chunk: {
  534. history: {
  535. snapshot: {
  536. files: {},
  537. },
  538. changes: [
  539. {
  540. operations: [
  541. {
  542. pathname: 'one.tex',
  543. file: {
  544. hash: sha('mock-sha'),
  545. stringLength: 42,
  546. },
  547. },
  548. ],
  549. timestamp: '2017-12-04T10:29:17.786Z',
  550. authors: [31],
  551. },
  552. {
  553. operations: [
  554. {
  555. pathname: 'one.tex',
  556. newPathname: 'two.tex',
  557. },
  558. ],
  559. timestamp: '2017-12-04T10:29:22.905Z',
  560. authors: [31],
  561. },
  562. ],
  563. },
  564. startVersion: 3,
  565. },
  566. authors: [{ id: 31, email: 'james.allen@overleaf.com', name: 'James' }],
  567. })
  568. return ProjectHistoryClient.getFileTreeDiff(
  569. this.projectId,
  570. 3,
  571. 5,
  572. (error, diff) => {
  573. if (error != null) {
  574. throw error
  575. }
  576. expect(diff).to.deep.equal({
  577. diff: [
  578. {
  579. pathname: 'two.tex',
  580. operation: 'added',
  581. editable: true,
  582. },
  583. ],
  584. })
  585. return done()
  586. }
  587. )
  588. })
  589. it('should return 422 with a chunk with an invalid rename', function (done) {
  590. MockHistoryStore()
  591. .get(`/api/projects/${this.historyId}/versions/6/history`)
  592. .reply(200, {
  593. chunk: {
  594. history: {
  595. snapshot: {
  596. files: {
  597. 'foo.tex': {
  598. hash: sha('mock-sha-foo'),
  599. stringLength: 42,
  600. },
  601. 'bar.tex': {
  602. hash: sha('mock-sha-bar'),
  603. stringLength: 42,
  604. },
  605. },
  606. },
  607. changes: [
  608. {
  609. operations: [
  610. {
  611. pathname: 'foo.tex',
  612. newPathname: 'bar.tex',
  613. },
  614. ],
  615. timestamp: '2017-12-04T10:29:17.786Z',
  616. authors: [31],
  617. },
  618. ],
  619. },
  620. startVersion: 5,
  621. },
  622. authors: [{ id: 31, email: 'james.allen@overleaf.com', name: 'James' }],
  623. })
  624. return ProjectHistoryClient.getFileTreeDiff(
  625. this.projectId,
  626. 5,
  627. 6,
  628. (error, diff, statusCode) => {
  629. if (error != null) {
  630. throw error
  631. }
  632. expect(statusCode).to.equal(422)
  633. return done()
  634. }
  635. )
  636. })
  637. it('should return 200 with a chunk with an invalid add', function (done) {
  638. MockHistoryStore()
  639. .get(`/api/projects/${this.historyId}/versions/6/history`)
  640. .reply(200, {
  641. chunk: {
  642. history: {
  643. snapshot: {
  644. files: {
  645. 'foo.tex': {
  646. hash: sha('mock-sha-foo'),
  647. stringLength: 42,
  648. },
  649. },
  650. },
  651. changes: [
  652. {
  653. operations: [
  654. {
  655. file: {
  656. hash: sha('new-sha'),
  657. },
  658. pathname: 'foo.tex',
  659. },
  660. ],
  661. timestamp: '2017-12-04T10:29:17.786Z',
  662. authors: [31],
  663. },
  664. ],
  665. },
  666. startVersion: 5,
  667. },
  668. authors: [{ id: 31, email: 'james.allen@overleaf.com', name: 'James' }],
  669. })
  670. return ProjectHistoryClient.getFileTreeDiff(
  671. this.projectId,
  672. 5,
  673. 6,
  674. (error, diff, statusCode) => {
  675. if (error != null) {
  676. throw error
  677. }
  678. expect(diff).to.deep.equal({
  679. diff: [
  680. {
  681. pathname: 'foo.tex',
  682. operation: 'added',
  683. editable: null,
  684. },
  685. ],
  686. })
  687. expect(statusCode).to.equal(200)
  688. return done()
  689. }
  690. )
  691. })
  692. it('should handle edits of missing/invalid files ', function (done) {
  693. MockHistoryStore()
  694. .get(`/api/projects/${this.historyId}/versions/5/history`)
  695. .reply(200, {
  696. chunk: {
  697. history: {
  698. snapshot: {
  699. files: {},
  700. },
  701. changes: [
  702. {
  703. operations: [
  704. {
  705. pathname: 'new.tex',
  706. textOperation: ['lorem ipsum'],
  707. },
  708. ],
  709. timestamp: '2017-12-04T10:29:18.786Z',
  710. authors: [31],
  711. },
  712. {
  713. operations: [
  714. {
  715. pathname: '',
  716. textOperation: ['lorem ipsum'],
  717. },
  718. ],
  719. timestamp: '2017-12-04T10:29:17.786Z',
  720. authors: [31],
  721. },
  722. ],
  723. },
  724. startVersion: 3,
  725. },
  726. authors: [{ id: 31, email: 'james.allen@overleaf.com', name: 'James' }],
  727. })
  728. return ProjectHistoryClient.getFileTreeDiff(
  729. this.projectId,
  730. 3,
  731. 5,
  732. (error, diff) => {
  733. if (error != null) {
  734. throw error
  735. }
  736. expect(diff).to.deep.equal({
  737. diff: [
  738. {
  739. operation: 'edited',
  740. pathname: 'new.tex',
  741. },
  742. ],
  743. })
  744. return done()
  745. }
  746. )
  747. })
  748. it('should handle deletions of missing/invalid files ', function (done) {
  749. MockHistoryStore()
  750. .get(`/api/projects/${this.historyId}/versions/5/history`)
  751. .reply(200, {
  752. chunk: {
  753. history: {
  754. snapshot: {
  755. files: {},
  756. },
  757. changes: [
  758. {
  759. operations: [
  760. {
  761. pathname: 'missing.tex',
  762. newPathname: '',
  763. },
  764. ],
  765. timestamp: '2017-12-04T10:29:17.786Z',
  766. authors: [31],
  767. },
  768. {
  769. operations: [
  770. {
  771. pathname: '',
  772. newPathname: '',
  773. },
  774. ],
  775. timestamp: '2017-12-04T10:29:17.786Z',
  776. authors: [31],
  777. },
  778. ],
  779. },
  780. startVersion: 3,
  781. },
  782. authors: [{ id: 31, email: 'james.allen@overleaf.com', name: 'James' }],
  783. })
  784. return ProjectHistoryClient.getFileTreeDiff(
  785. this.projectId,
  786. 3,
  787. 5,
  788. (error, diff) => {
  789. if (error != null) {
  790. throw error
  791. }
  792. expect(diff).to.deep.equal({
  793. diff: [],
  794. })
  795. return done()
  796. }
  797. )
  798. })
  799. return it('should handle renames of missing/invalid files ', function (done) {
  800. MockHistoryStore()
  801. .get(`/api/projects/${this.historyId}/versions/5/history`)
  802. .reply(200, {
  803. chunk: {
  804. history: {
  805. snapshot: {
  806. files: {},
  807. },
  808. changes: [
  809. {
  810. operations: [
  811. {
  812. pathname: 'missing.tex',
  813. newPathname: 'missing-renamed.tex',
  814. },
  815. ],
  816. timestamp: '2017-12-04T10:29:17.786Z',
  817. authors: [31],
  818. },
  819. {
  820. operations: [
  821. {
  822. pathname: '',
  823. newPathname: 'missing-renamed-other.tex',
  824. },
  825. ],
  826. timestamp: '2017-12-04T10:29:17.786Z',
  827. authors: [31],
  828. },
  829. ],
  830. },
  831. startVersion: 3,
  832. },
  833. authors: [{ id: 31, email: 'james.allen@overleaf.com', name: 'James' }],
  834. })
  835. return ProjectHistoryClient.getFileTreeDiff(
  836. this.projectId,
  837. 3,
  838. 5,
  839. (error, diff) => {
  840. if (error != null) {
  841. throw error
  842. }
  843. expect(diff).to.deep.equal({
  844. diff: [],
  845. })
  846. return done()
  847. }
  848. )
  849. })
  850. })