DiffManagerTests.js 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523
  1. import sinon from 'sinon'
  2. import { expect } from 'chai'
  3. import { strict as esmock } from 'esmock'
  4. const MODULE_PATH = '../../../../app/js/DiffManager.js'
  5. describe('DiffManager', function () {
  6. beforeEach(async function () {
  7. this.DocumentUpdaterManager = {}
  8. this.DiffGenerator = {
  9. buildDiff: sinon.stub(),
  10. }
  11. this.UpdatesProcessor = {
  12. processUpdatesForProject: sinon.stub(),
  13. }
  14. this.HistoryStoreManager = {
  15. getChunkAtVersion: sinon.stub(),
  16. }
  17. this.WebApiManager = {
  18. getHistoryId: sinon.stub(),
  19. }
  20. this.ChunkTranslator = {
  21. convertToDiffUpdates: sinon.stub(),
  22. }
  23. this.FileTreeDiffGenerator = {}
  24. this.DiffManager = await esmock(MODULE_PATH, {
  25. '../../../../app/js/DocumentUpdaterManager.js':
  26. this.DocumentUpdaterManager,
  27. '../../../../app/js/DiffGenerator.js': this.DiffGenerator,
  28. '../../../../app/js/UpdatesProcessor.js': this.UpdatesProcessor,
  29. '../../../../app/js/HistoryStoreManager.js': this.HistoryStoreManager,
  30. '../../../../app/js/WebApiManager.js': this.WebApiManager,
  31. '../../../../app/js/ChunkTranslator.js': this.ChunkTranslator,
  32. '../../../../app/js/FileTreeDiffGenerator.js': this.FileTreeDiffGenerator,
  33. })
  34. this.projectId = 'mock-project-id'
  35. this.callback = sinon.stub()
  36. })
  37. describe('getDiff', function () {
  38. beforeEach(function () {
  39. this.pathname = 'main.tex'
  40. this.fromVersion = 4
  41. this.toVersion = 8
  42. this.initialContent = 'foo bar baz'
  43. this.updates = ['mock-updates']
  44. this.diff = { mock: 'dif' }
  45. this.UpdatesProcessor.processUpdatesForProject
  46. .withArgs(this.projectId)
  47. .yields()
  48. this.DiffGenerator.buildDiff
  49. .withArgs(this.initialContent, this.updates)
  50. .returns(this.diff)
  51. })
  52. describe('with a text file', function () {
  53. beforeEach(function () {
  54. this.DiffManager._mocks._getProjectUpdatesBetweenVersions = sinon.stub()
  55. this.DiffManager._mocks._getProjectUpdatesBetweenVersions
  56. .withArgs(
  57. this.projectId,
  58. this.pathname,
  59. this.fromVersion,
  60. this.toVersion
  61. )
  62. .yields(null, {
  63. initialContent: this.initialContent,
  64. updates: this.updates,
  65. })
  66. this.DiffManager.getDiff(
  67. this.projectId,
  68. this.pathname,
  69. this.fromVersion,
  70. this.toVersion,
  71. this.callback
  72. )
  73. })
  74. it('should make sure all pending updates have been process', function () {
  75. this.UpdatesProcessor.processUpdatesForProject
  76. .calledWith(this.projectId)
  77. .should.equal(true)
  78. })
  79. it('should get the updates from the history backend', function () {
  80. this.DiffManager._mocks._getProjectUpdatesBetweenVersions
  81. .calledWith(
  82. this.projectId,
  83. this.pathname,
  84. this.fromVersion,
  85. this.toVersion
  86. )
  87. .should.equal(true)
  88. })
  89. it('should convert the updates to a diff', function () {
  90. this.DiffGenerator.buildDiff
  91. .calledWith(this.initialContent, this.updates)
  92. .should.equal(true)
  93. })
  94. it('should return the diff', function () {
  95. this.callback.calledWith(null, this.diff).should.equal(true)
  96. })
  97. })
  98. describe('with a binary file', function () {
  99. beforeEach(function () {
  100. this.DiffManager._mocks._getProjectUpdatesBetweenVersions = sinon.stub()
  101. this.DiffManager._mocks._getProjectUpdatesBetweenVersions
  102. .withArgs(
  103. this.projectId,
  104. this.pathname,
  105. this.fromVersion,
  106. this.toVersion
  107. )
  108. .yields(null, { binary: true })
  109. this.DiffManager.getDiff(
  110. this.projectId,
  111. this.pathname,
  112. this.fromVersion,
  113. this.toVersion,
  114. this.callback
  115. )
  116. })
  117. it('should make sure all pending updates have been process', function () {
  118. this.UpdatesProcessor.processUpdatesForProject
  119. .calledWith(this.projectId)
  120. .should.equal(true)
  121. })
  122. it('should get the updates from the history backend', function () {
  123. this.DiffManager._mocks._getProjectUpdatesBetweenVersions
  124. .calledWith(
  125. this.projectId,
  126. this.pathname,
  127. this.fromVersion,
  128. this.toVersion
  129. )
  130. .should.equal(true)
  131. })
  132. it('should not try convert any updates to a diff', function () {
  133. this.DiffGenerator.buildDiff.called.should.equal(false)
  134. })
  135. it('should return the binary diff', function () {
  136. this.callback.calledWith(null, { binary: true }).should.equal(true)
  137. })
  138. })
  139. })
  140. describe('_getProjectUpdatesBetweenVersions', function () {
  141. beforeEach(function () {
  142. this.pathname = 'main.tex'
  143. this.fromVersion = 4
  144. this.toVersion = 8
  145. this.chunks = ['mock-chunk-1', 'mock-chunk-2']
  146. this.concatted_chunk = 'mock-chunk'
  147. this.DiffManager._mocks._concatChunks = sinon.stub()
  148. this.DiffManager._mocks._concatChunks
  149. .withArgs(this.chunks)
  150. .returns(this.concatted_chunk)
  151. this.updates = ['mock-updates']
  152. this.initialContent = 'foo bar baz'
  153. this.ChunkTranslator.convertToDiffUpdates
  154. .withArgs(
  155. this.projectId,
  156. this.concatted_chunk,
  157. this.pathname,
  158. this.fromVersion,
  159. this.toVersion
  160. )
  161. .yields(null, {
  162. initialContent: this.initialContent,
  163. updates: this.updates,
  164. })
  165. })
  166. describe('for the normal case', function () {
  167. beforeEach(function () {
  168. this.DiffManager._mocks._getChunks = sinon.stub()
  169. this.DiffManager._mocks._getChunks
  170. .withArgs(this.projectId, this.fromVersion, this.toVersion)
  171. .yields(null, this.chunks)
  172. this.DiffManager._getProjectUpdatesBetweenVersions(
  173. this.projectId,
  174. this.pathname,
  175. this.fromVersion,
  176. this.toVersion,
  177. this.callback
  178. )
  179. })
  180. it('should get the relevant chunks', function () {
  181. this.DiffManager._mocks._getChunks
  182. .calledWith(this.projectId, this.fromVersion, this.toVersion)
  183. .should.equal(true)
  184. })
  185. it('should get the concat the chunks', function () {
  186. this.DiffManager._mocks._concatChunks
  187. .calledWith(this.chunks)
  188. .should.equal(true)
  189. })
  190. it('should convert the chunks to an initial version and updates', function () {
  191. this.ChunkTranslator.convertToDiffUpdates
  192. .calledWith(
  193. this.projectId,
  194. this.concatted_chunk,
  195. this.pathname,
  196. this.fromVersion,
  197. this.toVersion
  198. )
  199. .should.equal(true)
  200. })
  201. it('should return the initialContent and updates', function () {
  202. this.callback
  203. .calledWith(null, {
  204. initialContent: this.initialContent,
  205. updates: this.updates,
  206. })
  207. .should.equal(true)
  208. })
  209. })
  210. describe('for the error case', function () {
  211. beforeEach(function () {
  212. this.DiffManager._mocks._getChunks = sinon.stub()
  213. this.DiffManager._mocks._getChunks
  214. .withArgs(this.projectId, this.fromVersion, this.toVersion)
  215. .yields(new Error('failed to load chunk'))
  216. this.DiffManager._getProjectUpdatesBetweenVersions(
  217. this.projectId,
  218. this.pathname,
  219. this.fromVersion,
  220. this.toVersion,
  221. this.callback
  222. )
  223. })
  224. it('should call the callback with an error', function () {
  225. this.callback
  226. .calledWith(sinon.match.instanceOf(Error))
  227. .should.equal(true)
  228. })
  229. })
  230. })
  231. describe('_getChunks', function () {
  232. beforeEach(function () {
  233. this.historyId = 'mock-overleaf-id'
  234. this.WebApiManager.getHistoryId.yields(null, this.historyId)
  235. })
  236. describe('where only one chunk is needed', function () {
  237. beforeEach(function (done) {
  238. this.fromVersion = 4
  239. this.toVersion = 8
  240. this.chunk = {
  241. chunk: {
  242. startVersion: 2,
  243. }, // before fromVersion
  244. }
  245. this.HistoryStoreManager.getChunkAtVersion
  246. .withArgs(this.projectId, this.historyId, this.toVersion)
  247. .yields(null, this.chunk)
  248. this.DiffManager._getChunks(
  249. this.projectId,
  250. this.fromVersion,
  251. this.toVersion,
  252. (error, chunks) => {
  253. this.error = error
  254. this.chunks = chunks
  255. done()
  256. }
  257. )
  258. })
  259. it("should the project's overleaf id", function () {
  260. this.WebApiManager.getHistoryId
  261. .calledWith(this.projectId)
  262. .should.equal(true)
  263. })
  264. it('should request the first chunk', function () {
  265. this.HistoryStoreManager.getChunkAtVersion
  266. .calledWith(this.projectId, this.historyId, this.toVersion)
  267. .should.equal(true)
  268. })
  269. it('should return an array of chunks', function () {
  270. expect(this.chunks).to.deep.equal([this.chunk])
  271. })
  272. })
  273. describe('where multiple chunks are needed', function () {
  274. beforeEach(function (done) {
  275. this.fromVersion = 4
  276. this.toVersion = 8
  277. this.chunk1 = {
  278. chunk: {
  279. startVersion: 6,
  280. },
  281. }
  282. this.chunk2 = {
  283. chunk: {
  284. startVersion: 2,
  285. },
  286. }
  287. this.HistoryStoreManager.getChunkAtVersion
  288. .withArgs(this.projectId, this.historyId, this.toVersion)
  289. .yields(null, this.chunk1)
  290. this.HistoryStoreManager.getChunkAtVersion
  291. .withArgs(
  292. this.projectId,
  293. this.historyId,
  294. this.chunk1.chunk.startVersion
  295. )
  296. .yields(null, this.chunk2)
  297. this.DiffManager._mocks._getChunks(
  298. this.projectId,
  299. this.fromVersion,
  300. this.toVersion,
  301. (error, chunks) => {
  302. this.error = error
  303. this.chunks = chunks
  304. done()
  305. }
  306. )
  307. })
  308. it('should request the first chunk', function () {
  309. this.HistoryStoreManager.getChunkAtVersion
  310. .calledWith(this.projectId, this.historyId, this.toVersion)
  311. .should.equal(true)
  312. })
  313. it('should request the second chunk, from where the first one started', function () {
  314. this.HistoryStoreManager.getChunkAtVersion
  315. .calledWith(
  316. this.projectId,
  317. this.historyId,
  318. this.chunk1.chunk.startVersion
  319. )
  320. .should.equal(true)
  321. })
  322. it('should return an array of chunks', function () {
  323. expect(this.chunks).to.deep.equal([this.chunk1, this.chunk2])
  324. })
  325. })
  326. describe('where more than MAX_CHUNKS are requested', function () {
  327. beforeEach(function (done) {
  328. this.fromVersion = 0
  329. this.toVersion = 8
  330. this.chunk1 = {
  331. chunk: {
  332. startVersion: 6,
  333. },
  334. }
  335. this.chunk2 = {
  336. chunk: {
  337. startVersion: 4,
  338. },
  339. }
  340. this.chunk3 = {
  341. chunk: {
  342. startVersion: 2,
  343. },
  344. }
  345. this.DiffManager.setMaxChunkRequests(2)
  346. this.HistoryStoreManager.getChunkAtVersion
  347. .withArgs(this.projectId, this.historyId, this.toVersion)
  348. .yields(null, this.chunk1)
  349. this.HistoryStoreManager.getChunkAtVersion
  350. .withArgs(
  351. this.projectId,
  352. this.historyId,
  353. this.chunk1.chunk.startVersion
  354. )
  355. .yields(null, this.chunk2)
  356. this.DiffManager._mocks._getChunks(
  357. this.projectId,
  358. this.fromVersion,
  359. this.toVersion,
  360. (error, chunks) => {
  361. this.error = error
  362. this.chunks = chunks
  363. done()
  364. }
  365. )
  366. })
  367. it('should request the first chunk', function () {
  368. this.HistoryStoreManager.getChunkAtVersion
  369. .calledWith(this.projectId, this.historyId, this.toVersion)
  370. .should.equal(true)
  371. })
  372. it('should request the second chunk, from where the first one started', function () {
  373. this.HistoryStoreManager.getChunkAtVersion
  374. .calledWith(
  375. this.projectId,
  376. this.historyId,
  377. this.chunk1.chunk.startVersion
  378. )
  379. .should.equal(true)
  380. })
  381. it('should not request the third chunk', function () {
  382. this.HistoryStoreManager.getChunkAtVersion
  383. .calledWith(
  384. this.projectId,
  385. this.historyId,
  386. this.chunk2.chunk.startVersion
  387. )
  388. .should.equal(false)
  389. })
  390. it('should return an error', function () {
  391. expect(this.error).to.exist
  392. expect(this.error.message).to.equal('Diff spans too many chunks')
  393. expect(this.error.name).to.equal('BadRequestError')
  394. })
  395. })
  396. describe('where fromVersion == toVersion', function () {
  397. beforeEach(function (done) {
  398. this.fromVersion = 4
  399. this.toVersion = 4
  400. this.chunk = {
  401. chunk: {
  402. startVersion: 2,
  403. }, // before fromVersion
  404. }
  405. this.HistoryStoreManager.getChunkAtVersion
  406. .withArgs(this.projectId, this.historyId, this.toVersion)
  407. .yields(null, this.chunk)
  408. this.DiffManager._mocks._getChunks(
  409. this.projectId,
  410. this.fromVersion,
  411. this.toVersion,
  412. (error, chunks) => {
  413. this.error = error
  414. this.chunks = chunks
  415. done()
  416. }
  417. )
  418. })
  419. it('should still request the first chunk (because we need the file contents)', function () {
  420. this.HistoryStoreManager.getChunkAtVersion
  421. .calledWith(this.projectId, this.historyId, this.toVersion)
  422. .should.equal(true)
  423. })
  424. it('should return an array of chunks', function () {
  425. expect(this.chunks).to.deep.equal([this.chunk])
  426. })
  427. })
  428. })
  429. describe('_concatChunks', function () {
  430. it('should concat the chunks in reverse order', function () {
  431. const result = this.DiffManager._mocks._concatChunks([
  432. {
  433. chunk: {
  434. history: {
  435. snapshot: {
  436. files: {
  437. mock: 'files-updated-2',
  438. },
  439. },
  440. changes: [7, 8, 9],
  441. },
  442. },
  443. },
  444. {
  445. chunk: {
  446. history: {
  447. snapshot: {
  448. files: {
  449. mock: 'files-updated',
  450. },
  451. },
  452. changes: [4, 5, 6],
  453. },
  454. },
  455. },
  456. {
  457. chunk: {
  458. history: {
  459. snapshot: {
  460. files: {
  461. mock: 'files-original',
  462. },
  463. },
  464. changes: [1, 2, 3],
  465. },
  466. },
  467. },
  468. ])
  469. expect(result).to.deep.equal({
  470. chunk: {
  471. history: {
  472. snapshot: {
  473. files: {
  474. mock: 'files-original',
  475. },
  476. },
  477. changes: [1, 2, 3, 4, 5, 6, 7, 8, 9],
  478. },
  479. },
  480. })
  481. })
  482. })
  483. })