DiffManagerTests.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. /*
  2. * decaffeinate suggestions:
  3. * DS102: Remove unnecessary code created because of implicit returns
  4. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  5. */
  6. const sinon = require('sinon');
  7. const chai = require('chai');
  8. const should = chai.should();
  9. const { expect } = chai;
  10. const modulePath = "../../../../app/js/DiffManager.js";
  11. const SandboxedModule = require('sandboxed-module');
  12. describe("DiffManager", function() {
  13. beforeEach(function() {
  14. this.DiffManager = SandboxedModule.require(modulePath, { requires: {
  15. "logger-sharelatex": (this.logger = { log: sinon.stub(), error: sinon.stub(), warn: sinon.stub() }),
  16. "./UpdatesManager": (this.UpdatesManager = {}),
  17. "./DocumentUpdaterManager": (this.DocumentUpdaterManager = {}),
  18. "./DiffGenerator": (this.DiffGenerator = {})
  19. }
  20. });
  21. this.callback = sinon.stub();
  22. this.from = new Date();
  23. this.to = new Date(Date.now() + 10000);
  24. this.project_id = "mock-project-id";
  25. return this.doc_id = "mock-doc-id";
  26. });
  27. describe("getLatestDocAndUpdates", function() {
  28. beforeEach(function() {
  29. this.content = "hello world";
  30. this.version = 42;
  31. this.updates = [ "mock-update-1", "mock-update-2" ];
  32. this.DocumentUpdaterManager.getDocument = sinon.stub().callsArgWith(2, null, this.content, this.version);
  33. return this.UpdatesManager.getDocUpdatesWithUserInfo = sinon.stub().callsArgWith(3, null, this.updates);
  34. });
  35. describe("with a fromVersion", function() {
  36. beforeEach(function() {
  37. return this.DiffManager.getLatestDocAndUpdates(this.project_id, this.doc_id, this.from, this.callback);
  38. });
  39. it("should get the latest version of the doc", function() {
  40. return this.DocumentUpdaterManager.getDocument
  41. .calledWith(this.project_id, this.doc_id)
  42. .should.equal(true);
  43. });
  44. it("should get the latest updates", function() {
  45. return this.UpdatesManager.getDocUpdatesWithUserInfo
  46. .calledWith(this.project_id, this.doc_id, {from: this.from})
  47. .should.equal(true);
  48. });
  49. return it("should call the callback with the content, version and updates", function() {
  50. return this.callback.calledWith(null, this.content, this.version, this.updates).should.equal(true);
  51. });
  52. });
  53. return describe("with no fromVersion", function() {
  54. beforeEach(function() {
  55. return this.DiffManager.getLatestDocAndUpdates(this.project_id, this.doc_id, null, this.callback);
  56. });
  57. it("should get the latest version of the doc", function() {
  58. return this.DocumentUpdaterManager.getDocument
  59. .calledWith(this.project_id, this.doc_id)
  60. .should.equal(true);
  61. });
  62. it("should not get the latest updates", function() {
  63. return this.UpdatesManager.getDocUpdatesWithUserInfo
  64. .called.should.equal(false);
  65. });
  66. return it("should call the callback with the content, version and blank updates", function() {
  67. return this.callback.calledWith(null, this.content, this.version, []).should.equal(true);
  68. });
  69. });
  70. });
  71. describe("getDiff", function() {
  72. beforeEach(function() {
  73. this.content = "hello world";
  74. // Op versions are the version they were applied to, so doc is always one version
  75. // ahead.s
  76. this.version = 43;
  77. this.updates = [
  78. { op: "mock-4", v: 42, meta: { start_ts: new Date(this.to.getTime() + 20)} },
  79. { op: "mock-3", v: 41, meta: { start_ts: new Date(this.to.getTime() + 10)} },
  80. { op: "mock-2", v: 40, meta: { start_ts: new Date(this.to.getTime() - 10)} },
  81. { op: "mock-1", v: 39, meta: { start_ts: new Date(this.to.getTime() - 20)} }
  82. ];
  83. this.fromVersion = 39;
  84. this.toVersion = 40;
  85. this.diffed_updates = this.updates.slice(2);
  86. this.rewound_content = "rewound-content";
  87. return this.diff = [ {u: "mock-diff"} ];});
  88. describe("with matching versions", function() {
  89. beforeEach(function() {
  90. this.DiffManager.getDocumentBeforeVersion = sinon.stub().callsArgWith(3, null, this.rewound_content, this.updates);
  91. this.DiffGenerator.buildDiff = sinon.stub().returns(this.diff);
  92. return this.DiffManager.getDiff(this.project_id, this.doc_id, this.fromVersion, this.toVersion, this.callback);
  93. });
  94. it("should get the latest doc and version with all recent updates", function() {
  95. return this.DiffManager.getDocumentBeforeVersion
  96. .calledWith(this.project_id, this.doc_id, this.fromVersion)
  97. .should.equal(true);
  98. });
  99. it("should generate the diff", function() {
  100. return this.DiffGenerator.buildDiff
  101. .calledWith(this.rewound_content, this.diffed_updates.slice().reverse())
  102. .should.equal(true);
  103. });
  104. return it("should call the callback with the diff", function() {
  105. return this.callback.calledWith(null, this.diff).should.equal(true);
  106. });
  107. });
  108. return describe("when the updates are inconsistent", function() {
  109. beforeEach(function() {
  110. this.DiffManager.getLatestDocAndUpdates = sinon.stub().callsArgWith(3, null, this.content, this.version, this.updates);
  111. this.DiffGenerator.buildDiff = sinon.stub().throws(this.error = new Error("inconsistent!"));
  112. return this.DiffManager.getDiff(this.project_id, this.doc_id, this.fromVersion, this.toVersion, this.callback);
  113. });
  114. return it("should call the callback with an error", function() {
  115. return this.callback
  116. .calledWith(this.error)
  117. .should.equal(true);
  118. });
  119. });
  120. });
  121. describe("getDocumentBeforeVersion", function() {
  122. beforeEach(function() {
  123. this.DiffManager._tryGetDocumentBeforeVersion = sinon.stub();
  124. this.document = "mock-documents";
  125. return this.rewound_updates = "mock-rewound-updates";
  126. });
  127. describe("succesfully", function() {
  128. beforeEach(function() {
  129. this.DiffManager._tryGetDocumentBeforeVersion.yields(null, this.document, this.rewound_updates);
  130. return this.DiffManager.getDocumentBeforeVersion(this.project_id, this.doc_id, this.version, this.callback);
  131. });
  132. it("should call _tryGetDocumentBeforeVersion", function() {
  133. return this.DiffManager._tryGetDocumentBeforeVersion
  134. .calledWith(this.project_id, this.doc_id, this.version)
  135. .should.equal(true);
  136. });
  137. return it("should call the callback with the response", function() {
  138. return this.callback.calledWith(null, this.document, this.rewound_updates).should.equal(true);
  139. });
  140. });
  141. describe("with a retry needed", function() {
  142. beforeEach(function() {
  143. let retried = false;
  144. this.DiffManager._tryGetDocumentBeforeVersion = (project_id, doc_id, version, callback) => {
  145. if (!retried) {
  146. retried = true;
  147. const error = new Error();
  148. error.retry = true;
  149. return callback(error);
  150. } else {
  151. return callback(null, this.document, this.rewound_updates);
  152. }
  153. };
  154. sinon.spy(this.DiffManager, "_tryGetDocumentBeforeVersion");
  155. return this.DiffManager.getDocumentBeforeVersion(this.project_id, this.doc_id, this.version, this.callback);
  156. });
  157. it("should call _tryGetDocumentBeforeVersion twice", function() {
  158. return this.DiffManager._tryGetDocumentBeforeVersion
  159. .calledTwice
  160. .should.equal(true);
  161. });
  162. return it("should call the callback with the response", function() {
  163. return this.callback.calledWith(null, this.document, this.rewound_updates).should.equal(true);
  164. });
  165. });
  166. describe("with a non-retriable error", function() {
  167. beforeEach(function() {
  168. this.error = new Error("oops");
  169. this.DiffManager._tryGetDocumentBeforeVersion.yields(this.error);
  170. return this.DiffManager.getDocumentBeforeVersion(this.project_id, this.doc_id, this.version, this.callback);
  171. });
  172. it("should call _tryGetDocumentBeforeVersion once", function() {
  173. return this.DiffManager._tryGetDocumentBeforeVersion
  174. .calledOnce
  175. .should.equal(true);
  176. });
  177. return it("should call the callback with the error", function() {
  178. return this.callback.calledWith(this.error).should.equal(true);
  179. });
  180. });
  181. return describe("when retry limit is matched", function() {
  182. beforeEach(function() {
  183. this.error = new Error("oops");
  184. this.error.retry = true;
  185. this.DiffManager._tryGetDocumentBeforeVersion.yields(this.error);
  186. return this.DiffManager.getDocumentBeforeVersion(this.project_id, this.doc_id, this.version, this.callback);
  187. });
  188. it("should call _tryGetDocumentBeforeVersion three times (max retries)", function() {
  189. return this.DiffManager._tryGetDocumentBeforeVersion
  190. .calledThrice
  191. .should.equal(true);
  192. });
  193. return it("should call the callback with the error", function() {
  194. return this.callback.calledWith(this.error).should.equal(true);
  195. });
  196. });
  197. });
  198. return describe("_tryGetDocumentBeforeVersion", function() {
  199. beforeEach(function() {
  200. this.content = "hello world";
  201. // Op versions are the version they were applied to, so doc is always one version
  202. // ahead.s
  203. this.version = 43;
  204. this.updates = [
  205. { op: "mock-4", v: 42, meta: { start_ts: new Date(this.to.getTime() + 20)} },
  206. { op: "mock-3", v: 41, meta: { start_ts: new Date(this.to.getTime() + 10)} },
  207. { op: "mock-2", v: 40, meta: { start_ts: new Date(this.to.getTime() - 10)} },
  208. { op: "mock-1", v: 39, meta: { start_ts: new Date(this.to.getTime() - 20)} }
  209. ];
  210. this.fromVersion = 39;
  211. this.rewound_content = "rewound-content";
  212. return this.diff = [ {u: "mock-diff"} ];});
  213. describe("with matching versions", function() {
  214. beforeEach(function() {
  215. this.DiffManager.getLatestDocAndUpdates = sinon.stub().callsArgWith(3, null, this.content, this.version, this.updates);
  216. this.DiffGenerator.rewindUpdates = sinon.spy((content, updates) => {
  217. // the rewindUpdates method reverses the 'updates' array
  218. updates.reverse();
  219. return this.rewound_content;
  220. });
  221. this.rewindUpdatesWithArgs = this.DiffGenerator.rewindUpdates.withArgs(this.content, this.updates.slice().reverse());
  222. return this.DiffManager._tryGetDocumentBeforeVersion(this.project_id, this.doc_id, this.fromVersion, this.callback);
  223. });
  224. it("should get the latest doc and version with all recent updates", function() {
  225. return this.DiffManager.getLatestDocAndUpdates
  226. .calledWith(this.project_id, this.doc_id, this.fromVersion)
  227. .should.equal(true);
  228. });
  229. it("should rewind the diff", function() {
  230. return sinon.assert.calledOnce(this.rewindUpdatesWithArgs);
  231. });
  232. return it("should call the callback with the rewound document and updates", function() {
  233. return this.callback.calledWith(null, this.rewound_content, this.updates).should.equal(true);
  234. });
  235. });
  236. describe("with mismatching versions", function() {
  237. beforeEach(function() {
  238. this.version = 50;
  239. this.updates = [ { op: "mock-1", v: 40 }, { op: "mock-1", v: 39 } ];
  240. this.DiffManager.getLatestDocAndUpdates = sinon.stub().callsArgWith(3, null, this.content, this.version, this.updates);
  241. return this.DiffManager._tryGetDocumentBeforeVersion(this.project_id, this.doc_id, this.fromVersion, this.callback);
  242. });
  243. return it("should call the callback with an error with retry = true set", function() {
  244. this.callback.calledOnce.should.equal(true);
  245. const error = this.callback.args[0][0];
  246. return expect(error.retry).to.equal(true);
  247. });
  248. });
  249. return describe("when the updates are inconsistent", function() {
  250. beforeEach(function() {
  251. this.DiffManager.getLatestDocAndUpdates = sinon.stub().callsArgWith(3, null, this.content, this.version, this.updates);
  252. this.DiffGenerator.rewindUpdates = sinon.stub().throws(this.error = new Error("inconsistent!"));
  253. return this.DiffManager.getDocumentBeforeVersion(this.project_id, this.doc_id, this.fromVersion, this.callback);
  254. });
  255. return it("should call the callback with an error", function() {
  256. return this.callback
  257. .calledWith(this.error)
  258. .should.equal(true);
  259. });
  260. });
  261. });
  262. });