DiffManagerTests.js 15 KB

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