ShareJsUpdateManagerTests.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181
  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 modulePath = "../../../../app/js/ShareJsUpdateManager.js";
  10. const SandboxedModule = require('sandboxed-module');
  11. const crypto = require('crypto');
  12. describe("ShareJsUpdateManager", function() {
  13. beforeEach(function() {
  14. let Model;
  15. this.project_id = "project-id-123";
  16. this.doc_id = "document-id-123";
  17. this.callback = sinon.stub();
  18. return this.ShareJsUpdateManager = SandboxedModule.require(modulePath, {
  19. requires: {
  20. "./sharejs/server/model":
  21. (Model = class Model {
  22. constructor(db) {
  23. this.db = db;
  24. }
  25. }),
  26. "./ShareJsDB" : (this.ShareJsDB = { mockDB: true }),
  27. "redis-sharelatex" : { createClient: () => { return this.rclient = {auth() {}}; }
  28. },
  29. "logger-sharelatex": (this.logger = { log: sinon.stub() }),
  30. "./RealTimeRedisManager": (this.RealTimeRedisManager = {}),
  31. "./Metrics": (this.metrics = { inc: sinon.stub() })
  32. },
  33. globals: {
  34. clearTimeout: (this.clearTimeout = sinon.stub())
  35. }
  36. }
  37. );
  38. });
  39. describe("applyUpdate", function() {
  40. beforeEach(function() {
  41. this.lines = ["one", "two"];
  42. this.version = 34;
  43. this.updatedDocLines = ["onefoo", "two"];
  44. const content = this.updatedDocLines.join("\n");
  45. this.hash = crypto.createHash('sha1').update("blob " + content.length + "\x00").update(content, 'utf8').digest('hex');
  46. this.update = {p: 4, t: "foo", v:this.version, hash:this.hash};
  47. this.model = {
  48. applyOp: sinon.stub().callsArg(2),
  49. getSnapshot: sinon.stub(),
  50. db: {
  51. appliedOps: {}
  52. }
  53. };
  54. this.ShareJsUpdateManager.getNewShareJsModel = sinon.stub().returns(this.model);
  55. this.ShareJsUpdateManager._listenForOps = sinon.stub();
  56. return this.ShareJsUpdateManager.removeDocFromCache = sinon.stub().callsArg(1);
  57. });
  58. describe("successfully", function() {
  59. beforeEach(function(done) {
  60. this.model.getSnapshot.callsArgWith(1, null, {snapshot: this.updatedDocLines.join("\n"), v: this.version});
  61. this.model.db.appliedOps[`${this.project_id}:${this.doc_id}`] = (this.appliedOps = ["mock-ops"]);
  62. return this.ShareJsUpdateManager.applyUpdate(this.project_id, this.doc_id, this.update, this.lines, this.version, (err, docLines, version, appliedOps) => {
  63. this.callback(err, docLines, version, appliedOps);
  64. return done();
  65. });
  66. });
  67. it("should create a new ShareJs model", function() {
  68. return this.ShareJsUpdateManager.getNewShareJsModel
  69. .calledWith(this.project_id, this.doc_id, this.lines, this.version)
  70. .should.equal(true);
  71. });
  72. it("should listen for ops on the model", function() {
  73. return this.ShareJsUpdateManager._listenForOps
  74. .calledWith(this.model)
  75. .should.equal(true);
  76. });
  77. it("should send the update to ShareJs", function() {
  78. return this.model.applyOp
  79. .calledWith(`${this.project_id}:${this.doc_id}`, this.update)
  80. .should.equal(true);
  81. });
  82. it("should get the updated doc lines", function() {
  83. return this.model.getSnapshot
  84. .calledWith(`${this.project_id}:${this.doc_id}`)
  85. .should.equal(true);
  86. });
  87. return it("should return the updated doc lines, version and ops", function() {
  88. return this.callback.calledWith(null, this.updatedDocLines, this.version, this.appliedOps).should.equal(true);
  89. });
  90. });
  91. describe("when applyOp fails", function() {
  92. beforeEach(function(done) {
  93. this.error = new Error("Something went wrong");
  94. this.model.applyOp = sinon.stub().callsArgWith(2, this.error);
  95. return this.ShareJsUpdateManager.applyUpdate(this.project_id, this.doc_id, this.update, this.lines, this.version, (err, docLines, version) => {
  96. this.callback(err, docLines, version);
  97. return done();
  98. });
  99. });
  100. return it("should call the callback with the error", function() {
  101. return this.callback.calledWith(this.error).should.equal(true);
  102. });
  103. });
  104. describe("when getSnapshot fails", function() {
  105. beforeEach(function(done) {
  106. this.error = new Error("Something went wrong");
  107. this.model.getSnapshot.callsArgWith(1, this.error);
  108. return this.ShareJsUpdateManager.applyUpdate(this.project_id, this.doc_id, this.update, this.lines, this.version, (err, docLines, version) => {
  109. this.callback(err, docLines, version);
  110. return done();
  111. });
  112. });
  113. return it("should call the callback with the error", function() {
  114. return this.callback.calledWith(this.error).should.equal(true);
  115. });
  116. });
  117. return describe("with an invalid hash", function() {
  118. beforeEach(function(done) {
  119. this.error = new Error("invalid hash");
  120. this.model.getSnapshot.callsArgWith(1, null, {snapshot: "unexpected content", v: this.version});
  121. this.model.db.appliedOps[`${this.project_id}:${this.doc_id}`] = (this.appliedOps = ["mock-ops"]);
  122. return this.ShareJsUpdateManager.applyUpdate(this.project_id, this.doc_id, this.update, this.lines, this.version, (err, docLines, version, appliedOps) => {
  123. this.callback(err, docLines, version, appliedOps);
  124. return done();
  125. });
  126. });
  127. return it("should call the callback with the error", function() {
  128. return this.callback.calledWith(this.error).should.equal(true);
  129. });
  130. });
  131. });
  132. return describe("_listenForOps", function() {
  133. beforeEach(function() {
  134. this.model = { on: (event, callback) => {
  135. return this.callback = callback;
  136. }
  137. };
  138. sinon.spy(this.model, "on");
  139. return this.ShareJsUpdateManager._listenForOps(this.model);
  140. });
  141. it("should listen to the model for updates", function() {
  142. return this.model.on.calledWith("applyOp")
  143. .should.equal(true);
  144. });
  145. return describe("the callback", function() {
  146. beforeEach(function() {
  147. this.opData = {
  148. op: {t: "foo", p: 1},
  149. meta: { source: "bar"
  150. }
  151. };
  152. this.RealTimeRedisManager.sendData = sinon.stub();
  153. return this.callback(`${this.project_id}:${this.doc_id}`, this.opData);
  154. });
  155. return it("should publish the op to redis", function() {
  156. return this.RealTimeRedisManager.sendData
  157. .calledWith({project_id: this.project_id, doc_id: this.doc_id, op: this.opData})
  158. .should.equal(true);
  159. });
  160. });
  161. });
  162. });