UpdateManagerTests.js 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487
  1. /* eslint-disable
  2. no-return-assign,
  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. * DS101: Remove unnecessary use of Array.from
  10. * DS102: Remove unnecessary code created because of implicit returns
  11. * DS206: Consider reworking classes to avoid initClass
  12. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  13. */
  14. const sinon = require('sinon');
  15. const chai = require('chai');
  16. const should = chai.should();
  17. const modulePath = "../../../../app/js/UpdateManager.js";
  18. const SandboxedModule = require('sandboxed-module');
  19. describe("UpdateManager", function() {
  20. beforeEach(function() {
  21. let Profiler, Timer;
  22. this.project_id = "project-id-123";
  23. this.projectHistoryId = "history-id-123";
  24. this.doc_id = "document-id-123";
  25. this.callback = sinon.stub();
  26. return this.UpdateManager = SandboxedModule.require(modulePath, { requires: {
  27. "./LockManager" : (this.LockManager = {}),
  28. "./RedisManager" : (this.RedisManager = {}),
  29. "./RealTimeRedisManager" : (this.RealTimeRedisManager = {}),
  30. "./ShareJsUpdateManager" : (this.ShareJsUpdateManager = {}),
  31. "./HistoryManager" : (this.HistoryManager = {}),
  32. "logger-sharelatex": (this.logger = { log: sinon.stub() }),
  33. "./Metrics": (this.Metrics = {
  34. Timer: (Timer = (function() {
  35. Timer = class Timer {
  36. static initClass() {
  37. this.prototype.done = sinon.stub();
  38. }
  39. };
  40. Timer.initClass();
  41. return Timer;
  42. })())
  43. }),
  44. "settings-sharelatex": (this.Settings = {}),
  45. "./DocumentManager": (this.DocumentManager = {}),
  46. "./RangesManager": (this.RangesManager = {}),
  47. "./SnapshotManager": (this.SnapshotManager = {}),
  48. "./Profiler": (Profiler = (function() {
  49. Profiler = class Profiler {
  50. static initClass() {
  51. this.prototype.log = sinon.stub().returns({ end: sinon.stub() });
  52. this.prototype.end = sinon.stub();
  53. }
  54. };
  55. Profiler.initClass();
  56. return Profiler;
  57. })())
  58. }
  59. }
  60. );
  61. });
  62. describe("processOutstandingUpdates", function() {
  63. beforeEach(function() {
  64. this.UpdateManager.fetchAndApplyUpdates = sinon.stub().callsArg(2);
  65. return this.UpdateManager.processOutstandingUpdates(this.project_id, this.doc_id, this.callback);
  66. });
  67. it("should apply the updates", function() {
  68. return this.UpdateManager.fetchAndApplyUpdates.calledWith(this.project_id, this.doc_id).should.equal(true);
  69. });
  70. it("should call the callback", function() {
  71. return this.callback.called.should.equal(true);
  72. });
  73. return it("should time the execution", function() {
  74. return this.Metrics.Timer.prototype.done.called.should.equal(true);
  75. });
  76. });
  77. describe("processOutstandingUpdatesWithLock", function() {
  78. describe("when the lock is free", function() {
  79. beforeEach(function() {
  80. this.LockManager.tryLock = sinon.stub().callsArgWith(1, null, true, (this.lockValue = "mock-lock-value"));
  81. this.LockManager.releaseLock = sinon.stub().callsArg(2);
  82. this.UpdateManager.continueProcessingUpdatesWithLock = sinon.stub().callsArg(2);
  83. return this.UpdateManager.processOutstandingUpdates = sinon.stub().callsArg(2);
  84. });
  85. describe("successfully", function() {
  86. beforeEach(function() {
  87. return this.UpdateManager.processOutstandingUpdatesWithLock(this.project_id, this.doc_id, this.callback);
  88. });
  89. it("should acquire the lock", function() {
  90. return this.LockManager.tryLock.calledWith(this.doc_id).should.equal(true);
  91. });
  92. it("should free the lock", function() {
  93. return this.LockManager.releaseLock.calledWith(this.doc_id, this.lockValue).should.equal(true);
  94. });
  95. it("should process the outstanding updates", function() {
  96. return this.UpdateManager.processOutstandingUpdates.calledWith(this.project_id, this.doc_id).should.equal(true);
  97. });
  98. it("should do everything with the lock acquired", function() {
  99. this.UpdateManager.processOutstandingUpdates.calledAfter(this.LockManager.tryLock).should.equal(true);
  100. return this.UpdateManager.processOutstandingUpdates.calledBefore(this.LockManager.releaseLock).should.equal(true);
  101. });
  102. it("should continue processing new updates that may have come in", function() {
  103. return this.UpdateManager.continueProcessingUpdatesWithLock.calledWith(this.project_id, this.doc_id).should.equal(true);
  104. });
  105. return it("should return the callback", function() {
  106. return this.callback.called.should.equal(true);
  107. });
  108. });
  109. return describe("when processOutstandingUpdates returns an error", function() {
  110. beforeEach(function() {
  111. this.UpdateManager.processOutstandingUpdates = sinon.stub().callsArgWith(2, (this.error = new Error("Something went wrong")));
  112. return this.UpdateManager.processOutstandingUpdatesWithLock(this.project_id, this.doc_id, this.callback);
  113. });
  114. it("should free the lock", function() {
  115. return this.LockManager.releaseLock.calledWith(this.doc_id, this.lockValue).should.equal(true);
  116. });
  117. return it("should return the error in the callback", function() {
  118. return this.callback.calledWith(this.error).should.equal(true);
  119. });
  120. });
  121. });
  122. return describe("when the lock is taken", function() {
  123. beforeEach(function() {
  124. this.LockManager.tryLock = sinon.stub().callsArgWith(1, null, false);
  125. this.UpdateManager.processOutstandingUpdates = sinon.stub().callsArg(2);
  126. return this.UpdateManager.processOutstandingUpdatesWithLock(this.project_id, this.doc_id, this.callback);
  127. });
  128. it("should return the callback", function() {
  129. return this.callback.called.should.equal(true);
  130. });
  131. return it("should not process the updates", function() {
  132. return this.UpdateManager.processOutstandingUpdates.called.should.equal(false);
  133. });
  134. });
  135. });
  136. describe("continueProcessingUpdatesWithLock", function() {
  137. describe("when there are outstanding updates", function() {
  138. beforeEach(function() {
  139. this.RealTimeRedisManager.getUpdatesLength = sinon.stub().callsArgWith(1, null, 3);
  140. this.UpdateManager.processOutstandingUpdatesWithLock = sinon.stub().callsArg(2);
  141. return this.UpdateManager.continueProcessingUpdatesWithLock(this.project_id, this.doc_id, this.callback);
  142. });
  143. it("should process the outstanding updates", function() {
  144. return this.UpdateManager.processOutstandingUpdatesWithLock.calledWith(this.project_id, this.doc_id).should.equal(true);
  145. });
  146. return it("should return the callback", function() {
  147. return this.callback.called.should.equal(true);
  148. });
  149. });
  150. return describe("when there are no outstanding updates", function() {
  151. beforeEach(function() {
  152. this.RealTimeRedisManager.getUpdatesLength = sinon.stub().callsArgWith(1, null, 0);
  153. this.UpdateManager.processOutstandingUpdatesWithLock = sinon.stub().callsArg(2);
  154. return this.UpdateManager.continueProcessingUpdatesWithLock(this.project_id, this.doc_id, this.callback);
  155. });
  156. it("should not try to process the outstanding updates", function() {
  157. return this.UpdateManager.processOutstandingUpdatesWithLock.called.should.equal(false);
  158. });
  159. return it("should return the callback", function() {
  160. return this.callback.called.should.equal(true);
  161. });
  162. });
  163. });
  164. describe("fetchAndApplyUpdates", function() {
  165. describe("with updates", function() {
  166. beforeEach(function() {
  167. this.updates = [{p: 1, t: "foo"}];
  168. this.updatedDocLines = ["updated", "lines"];
  169. this.version = 34;
  170. this.RealTimeRedisManager.getPendingUpdatesForDoc = sinon.stub().callsArgWith(1, null, this.updates);
  171. this.UpdateManager.applyUpdate = sinon.stub().callsArgWith(3, null, this.updatedDocLines, this.version);
  172. return this.UpdateManager.fetchAndApplyUpdates(this.project_id, this.doc_id, this.callback);
  173. });
  174. it("should get the pending updates", function() {
  175. return this.RealTimeRedisManager.getPendingUpdatesForDoc.calledWith(this.doc_id).should.equal(true);
  176. });
  177. it("should apply the updates", function() {
  178. return Array.from(this.updates).map((update) =>
  179. this.UpdateManager.applyUpdate
  180. .calledWith(this.project_id, this.doc_id, update)
  181. .should.equal(true));
  182. });
  183. return it("should call the callback", function() {
  184. return this.callback.called.should.equal(true);
  185. });
  186. });
  187. return describe("when there are no updates", function() {
  188. beforeEach(function() {
  189. this.updates = [];
  190. this.RealTimeRedisManager.getPendingUpdatesForDoc = sinon.stub().callsArgWith(1, null, this.updates);
  191. this.UpdateManager.applyUpdate = sinon.stub();
  192. this.RedisManager.setDocument = sinon.stub();
  193. return this.UpdateManager.fetchAndApplyUpdates(this.project_id, this.doc_id, this.callback);
  194. });
  195. it("should not call applyUpdate", function() {
  196. return this.UpdateManager.applyUpdate.called.should.equal(false);
  197. });
  198. return it("should call the callback", function() {
  199. return this.callback.called.should.equal(true);
  200. });
  201. });
  202. });
  203. describe("applyUpdate", function() {
  204. beforeEach(function() {
  205. this.updateMeta = { user_id: 'last-author-fake-id' };
  206. this.update = {op: [{p: 42, i: "foo"}], meta: this.updateMeta};
  207. this.updatedDocLines = ["updated", "lines"];
  208. this.version = 34;
  209. this.lines = ["original", "lines"];
  210. this.ranges = { entries: "mock", comments: "mock" };
  211. this.updated_ranges = { entries: "updated", comments: "updated" };
  212. this.appliedOps = [ {v: 42, op: "mock-op-42"}, { v: 45, op: "mock-op-45" }];
  213. this.doc_ops_length = sinon.stub();
  214. this.project_ops_length = sinon.stub();
  215. this.pathname = '/a/b/c.tex';
  216. this.DocumentManager.getDoc = sinon.stub().yields(null, this.lines, this.version, this.ranges, this.pathname, this.projectHistoryId);
  217. this.RangesManager.applyUpdate = sinon.stub().yields(null, this.updated_ranges, false);
  218. this.ShareJsUpdateManager.applyUpdate = sinon.stub().yields(null, this.updatedDocLines, this.version, this.appliedOps);
  219. this.RedisManager.updateDocument = sinon.stub().yields(null, this.doc_ops_length, this.project_ops_length);
  220. this.RealTimeRedisManager.sendData = sinon.stub();
  221. this.UpdateManager._addProjectHistoryMetadataToOps = sinon.stub();
  222. return this.HistoryManager.recordAndFlushHistoryOps = sinon.stub().callsArg(5);
  223. });
  224. describe("normally", function() {
  225. beforeEach(function() {
  226. return this.UpdateManager.applyUpdate(this.project_id, this.doc_id, this.update, this.callback);
  227. });
  228. it("should apply the updates via ShareJS", function() {
  229. return this.ShareJsUpdateManager.applyUpdate
  230. .calledWith(this.project_id, this.doc_id, this.update, this.lines, this.version)
  231. .should.equal(true);
  232. });
  233. it("should update the ranges", function() {
  234. return this.RangesManager.applyUpdate
  235. .calledWith(this.project_id, this.doc_id, this.ranges, this.appliedOps, this.updatedDocLines)
  236. .should.equal(true);
  237. });
  238. it("should save the document", function() {
  239. return this.RedisManager.updateDocument
  240. .calledWith(this.project_id, this.doc_id, this.updatedDocLines, this.version, this.appliedOps, this.updated_ranges, this.updateMeta)
  241. .should.equal(true);
  242. });
  243. it("should add metadata to the ops" , function() {
  244. return this.UpdateManager._addProjectHistoryMetadataToOps
  245. .calledWith(this.appliedOps, this.pathname, this.projectHistoryId, this.lines)
  246. .should.equal(true);
  247. });
  248. it("should push the applied ops into the history queue", function() {
  249. return this.HistoryManager.recordAndFlushHistoryOps
  250. .calledWith(this.project_id, this.doc_id, this.appliedOps, this.doc_ops_length, this.project_ops_length)
  251. .should.equal(true);
  252. });
  253. return it("should call the callback", function() {
  254. return this.callback.called.should.equal(true);
  255. });
  256. });
  257. describe("with UTF-16 surrogate pairs in the update", function() {
  258. beforeEach(function() {
  259. this.update = {op: [{p: 42, i: "\uD835\uDC00"}]};
  260. return this.UpdateManager.applyUpdate(this.project_id, this.doc_id, this.update, this.callback);
  261. });
  262. return it("should apply the update but with surrogate pairs removed", function() {
  263. this.ShareJsUpdateManager.applyUpdate
  264. .calledWith(this.project_id, this.doc_id, this.update)
  265. .should.equal(true);
  266. // \uFFFD is 'replacement character'
  267. return this.update.op[0].i.should.equal("\uFFFD\uFFFD");
  268. });
  269. });
  270. describe("with an error", function() {
  271. beforeEach(function() {
  272. this.error = new Error("something went wrong");
  273. this.ShareJsUpdateManager.applyUpdate = sinon.stub().yields(this.error);
  274. return this.UpdateManager.applyUpdate(this.project_id, this.doc_id, this.update, this.callback);
  275. });
  276. it("should call RealTimeRedisManager.sendData with the error", function() {
  277. return this.RealTimeRedisManager.sendData
  278. .calledWith({
  279. project_id: this.project_id,
  280. doc_id: this.doc_id,
  281. error: this.error.message
  282. })
  283. .should.equal(true);
  284. });
  285. return it("should call the callback with the error", function() {
  286. return this.callback.calledWith(this.error).should.equal(true);
  287. });
  288. });
  289. return describe("when ranges get collapsed", function() {
  290. beforeEach(function() {
  291. this.RangesManager.applyUpdate = sinon.stub().yields(null, this.updated_ranges, true);
  292. this.SnapshotManager.recordSnapshot = sinon.stub().yields();
  293. return this.UpdateManager.applyUpdate(this.project_id, this.doc_id, this.update, this.callback);
  294. });
  295. return it("should call SnapshotManager.recordSnapshot", function() {
  296. return this.SnapshotManager.recordSnapshot
  297. .calledWith(
  298. this.project_id,
  299. this.doc_id,
  300. this.version,
  301. this.pathname,
  302. this.lines,
  303. this.ranges
  304. )
  305. .should.equal(true);
  306. });
  307. });
  308. });
  309. describe("_addProjectHistoryMetadataToOps", function() { return it("should add projectHistoryId, pathname and doc_length metadata to the ops", function() {
  310. const lines = [
  311. 'some',
  312. 'test',
  313. 'data'
  314. ];
  315. const appliedOps = [
  316. { v: 42, op: [{i: "foo", p: 4}, { i: "bar", p: 6 }] },
  317. { v: 45, op: [{d: "qux", p: 4}, { i: "bazbaz", p: 14 }] },
  318. { v: 49, op: [{i: "penguin", p: 18}] }
  319. ];
  320. this.UpdateManager._addProjectHistoryMetadataToOps(appliedOps, this.pathname, this.projectHistoryId, lines);
  321. return appliedOps.should.deep.equal([{
  322. projectHistoryId: this.projectHistoryId,
  323. v: 42,
  324. op: [{i: "foo", p: 4}, { i: "bar", p: 6 }],
  325. meta: {
  326. pathname: this.pathname,
  327. doc_length: 14
  328. }
  329. }, {
  330. projectHistoryId: this.projectHistoryId,
  331. v: 45,
  332. op: [{d: "qux", p: 4}, { i: "bazbaz", p: 14 }],
  333. meta: {
  334. pathname: this.pathname,
  335. doc_length: 20
  336. } // 14 + 'foo' + 'bar'
  337. }, {
  338. projectHistoryId: this.projectHistoryId,
  339. v: 49,
  340. op: [{i: "penguin", p: 18}],
  341. meta: {
  342. pathname: this.pathname,
  343. doc_length: 23
  344. } // 14 - 'qux' + 'bazbaz'
  345. }]);
  346. }); });
  347. return describe("lockUpdatesAndDo", function() {
  348. beforeEach(function() {
  349. this.method = sinon.stub().callsArgWith(3, null, this.response_arg1);
  350. this.callback = sinon.stub();
  351. this.arg1 = "argument 1";
  352. this.response_arg1 = "response argument 1";
  353. this.lockValue = "mock-lock-value";
  354. this.LockManager.getLock = sinon.stub().callsArgWith(1, null, this.lockValue);
  355. return this.LockManager.releaseLock = sinon.stub().callsArg(2);
  356. });
  357. describe("successfully", function() {
  358. beforeEach(function() {
  359. this.UpdateManager.continueProcessingUpdatesWithLock = sinon.stub();
  360. this.UpdateManager.processOutstandingUpdates = sinon.stub().callsArg(2);
  361. return this.UpdateManager.lockUpdatesAndDo(this.method, this.project_id, this.doc_id, this.arg1, this.callback);
  362. });
  363. it("should lock the doc", function() {
  364. return this.LockManager.getLock
  365. .calledWith(this.doc_id)
  366. .should.equal(true);
  367. });
  368. it("should process any outstanding updates", function() {
  369. return this.UpdateManager.processOutstandingUpdates
  370. .calledWith(this.project_id, this.doc_id)
  371. .should.equal(true);
  372. });
  373. it("should call the method", function() {
  374. return this.method
  375. .calledWith(this.project_id, this.doc_id, this.arg1)
  376. .should.equal(true);
  377. });
  378. it("should return the method response to the callback", function() {
  379. return this.callback
  380. .calledWith(null, this.response_arg1)
  381. .should.equal(true);
  382. });
  383. it("should release the lock", function() {
  384. return this.LockManager.releaseLock
  385. .calledWith(this.doc_id, this.lockValue)
  386. .should.equal(true);
  387. });
  388. return it("should continue processing updates", function() {
  389. return this.UpdateManager.continueProcessingUpdatesWithLock
  390. .calledWith(this.project_id, this.doc_id)
  391. .should.equal(true);
  392. });
  393. });
  394. describe("when processOutstandingUpdates returns an error", function() {
  395. beforeEach(function() {
  396. this.UpdateManager.processOutstandingUpdates = sinon.stub().callsArgWith(2, (this.error = new Error("Something went wrong")));
  397. return this.UpdateManager.lockUpdatesAndDo(this.method, this.project_id, this.doc_id, this.arg1, this.callback);
  398. });
  399. it("should free the lock", function() {
  400. return this.LockManager.releaseLock.calledWith(this.doc_id, this.lockValue).should.equal(true);
  401. });
  402. return it("should return the error in the callback", function() {
  403. return this.callback.calledWith(this.error).should.equal(true);
  404. });
  405. });
  406. return describe("when the method returns an error", function() {
  407. beforeEach(function() {
  408. this.UpdateManager.processOutstandingUpdates = sinon.stub().callsArg(2);
  409. this.method = sinon.stub().callsArgWith(3, (this.error = new Error("something went wrong")), this.response_arg1);
  410. return this.UpdateManager.lockUpdatesAndDo(this.method, this.project_id, this.doc_id, this.arg1, this.callback);
  411. });
  412. it("should free the lock", function() {
  413. return this.LockManager.releaseLock.calledWith(this.doc_id, this.lockValue).should.equal(true);
  414. });
  415. return it("should return the error in the callback", function() {
  416. return this.callback.calledWith(this.error).should.equal(true);
  417. });
  418. });
  419. });
  420. });