decaffeinate: Convert DiffCodecTests.coffee and 23 other files to JS

This commit is contained in:
decaffeinate 2020-05-06 12:10:51 +02:00 committed by Tim Alby
parent 6c4d7fb838
commit c781526af0
24 changed files with 5566 additions and 4009 deletions

View file

@ -1,56 +1,76 @@
sinon = require('sinon')
chai = require('chai')
should = chai.should()
expect = chai.expect
modulePath = "../../../../app/js/DiffCodec.js"
SandboxedModule = require('sandboxed-module')
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const sinon = require('sinon');
const chai = require('chai');
const should = chai.should();
const {
expect
} = chai;
const modulePath = "../../../../app/js/DiffCodec.js";
const SandboxedModule = require('sandboxed-module');
describe "DiffCodec", ->
beforeEach ->
@callback = sinon.stub()
@DiffCodec = SandboxedModule.require modulePath
describe("DiffCodec", function() {
beforeEach(function() {
this.callback = sinon.stub();
return this.DiffCodec = SandboxedModule.require(modulePath);
});
describe "diffAsShareJsOps", ->
it "should insert new text correctly", (done) ->
@before = ["hello world"]
@after = ["hello beautiful world"]
@DiffCodec.diffAsShareJsOp @before, @after, (error, ops) ->
expect(ops).to.deep.equal [
i: "beautiful "
return describe("diffAsShareJsOps", function() {
it("should insert new text correctly", function(done) {
this.before = ["hello world"];
this.after = ["hello beautiful world"];
return this.DiffCodec.diffAsShareJsOp(this.before, this.after, function(error, ops) {
expect(ops).to.deep.equal([{
i: "beautiful ",
p: 6
]
done()
}
]);
return done();
});
});
it "should shift later inserts by previous inserts", (done) ->
@before = ["the boy played with the ball"]
@after = ["the tall boy played with the red ball"]
@DiffCodec.diffAsShareJsOp @before, @after, (error, ops) ->
expect(ops).to.deep.equal [
{ i: "tall ", p: 4 }
it("should shift later inserts by previous inserts", function(done) {
this.before = ["the boy played with the ball"];
this.after = ["the tall boy played with the red ball"];
return this.DiffCodec.diffAsShareJsOp(this.before, this.after, function(error, ops) {
expect(ops).to.deep.equal([
{ i: "tall ", p: 4 },
{ i: "red ", p: 29 }
]
done()
]);
return done();
});
});
it "should delete text correctly", (done) ->
@before = ["hello beautiful world"]
@after = ["hello world"]
@DiffCodec.diffAsShareJsOp @before, @after, (error, ops) ->
expect(ops).to.deep.equal [
d: "beautiful "
it("should delete text correctly", function(done) {
this.before = ["hello beautiful world"];
this.after = ["hello world"];
return this.DiffCodec.diffAsShareJsOp(this.before, this.after, function(error, ops) {
expect(ops).to.deep.equal([{
d: "beautiful ",
p: 6
]
done()
}
]);
return done();
});
});
it "should shift later deletes by the first deletes", (done) ->
@before = ["the tall boy played with the red ball"]
@after = ["the boy played with the ball"]
@DiffCodec.diffAsShareJsOp @before, @after, (error, ops) ->
expect(ops).to.deep.equal [
{ d: "tall ", p: 4 }
return it("should shift later deletes by the first deletes", function(done) {
this.before = ["the tall boy played with the red ball"];
this.after = ["the boy played with the ball"];
return this.DiffCodec.diffAsShareJsOp(this.before, this.after, function(error, ops) {
expect(ops).to.deep.equal([
{ d: "tall ", p: 4 },
{ d: "red ", p: 24 }
]
done()
]);
return done();
});
});
});
});

View file

@ -1,110 +1,147 @@
sinon = require('sinon')
chai = require('chai')
should = chai.should()
modulePath = "../../../../app/js/DispatchManager.js"
SandboxedModule = require('sandboxed-module')
Errors = require "../../../../app/js/Errors.js"
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const sinon = require('sinon');
const chai = require('chai');
const should = chai.should();
const modulePath = "../../../../app/js/DispatchManager.js";
const SandboxedModule = require('sandboxed-module');
const Errors = require("../../../../app/js/Errors.js");
describe "DispatchManager", ->
beforeEach ->
@timeout(3000)
@DispatchManager = SandboxedModule.require modulePath, requires:
"./UpdateManager" : @UpdateManager = {}
"logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub(), warn: sinon.stub() }
"settings-sharelatex": @settings =
redis:
describe("DispatchManager", function() {
beforeEach(function() {
this.timeout(3000);
this.DispatchManager = SandboxedModule.require(modulePath, { requires: {
"./UpdateManager" : (this.UpdateManager = {}),
"logger-sharelatex": (this.logger = { log: sinon.stub(), error: sinon.stub(), warn: sinon.stub() }),
"settings-sharelatex": (this.settings = {
redis: {
documentupdater: {}
"redis-sharelatex": @redis = {}
"./RateLimitManager": {}
"./Errors": Errors
"./Metrics":
Timer: ->
done: ->
@callback = sinon.stub()
@RateLimiter = { run: (task,cb) -> task(cb) } # run task without rate limit
}
}),
"redis-sharelatex": (this.redis = {}),
"./RateLimitManager": {},
"./Errors": Errors,
"./Metrics": {
Timer() {
return {done() {}};
}
}
}
}
);
this.callback = sinon.stub();
return this.RateLimiter = { run(task,cb) { return task(cb); } };}); // run task without rate limit
describe "each worker", ->
beforeEach ->
@client =
auth: sinon.stub()
@redis.createClient = sinon.stub().returns @client
@worker = @DispatchManager.createDispatcher(@RateLimiter)
return describe("each worker", function() {
beforeEach(function() {
this.client =
{auth: sinon.stub()};
this.redis.createClient = sinon.stub().returns(this.client);
return this.worker = this.DispatchManager.createDispatcher(this.RateLimiter);
});
it "should create a new redis client", ->
@redis.createClient.called.should.equal true
it("should create a new redis client", function() {
return this.redis.createClient.called.should.equal(true);
});
describe "_waitForUpdateThenDispatchWorker", ->
beforeEach ->
@project_id = "project-id-123"
@doc_id = "doc-id-123"
@doc_key = "#{@project_id}:#{@doc_id}"
@client.blpop = sinon.stub().callsArgWith(2, null, ["pending-updates-list", @doc_key])
describe("_waitForUpdateThenDispatchWorker", function() {
beforeEach(function() {
this.project_id = "project-id-123";
this.doc_id = "doc-id-123";
this.doc_key = `${this.project_id}:${this.doc_id}`;
return this.client.blpop = sinon.stub().callsArgWith(2, null, ["pending-updates-list", this.doc_key]);
});
describe "in the normal case", ->
beforeEach ->
@UpdateManager.processOutstandingUpdatesWithLock = sinon.stub().callsArg(2)
@worker._waitForUpdateThenDispatchWorker @callback
describe("in the normal case", function() {
beforeEach(function() {
this.UpdateManager.processOutstandingUpdatesWithLock = sinon.stub().callsArg(2);
return this.worker._waitForUpdateThenDispatchWorker(this.callback);
});
it "should call redis with BLPOP", ->
@client.blpop
it("should call redis with BLPOP", function() {
return this.client.blpop
.calledWith("pending-updates-list", 0)
.should.equal true
.should.equal(true);
});
it "should call processOutstandingUpdatesWithLock", ->
@UpdateManager.processOutstandingUpdatesWithLock
.calledWith(@project_id, @doc_id)
.should.equal true
it("should call processOutstandingUpdatesWithLock", function() {
return this.UpdateManager.processOutstandingUpdatesWithLock
.calledWith(this.project_id, this.doc_id)
.should.equal(true);
});
it "should not log any errors", ->
@logger.error.called.should.equal false
@logger.warn.called.should.equal false
it("should not log any errors", function() {
this.logger.error.called.should.equal(false);
return this.logger.warn.called.should.equal(false);
});
it "should call the callback", ->
@callback.called.should.equal true
return it("should call the callback", function() {
return this.callback.called.should.equal(true);
});
});
describe "with an error", ->
beforeEach ->
@UpdateManager.processOutstandingUpdatesWithLock = sinon.stub().callsArgWith(2, new Error("a generic error"))
@worker._waitForUpdateThenDispatchWorker @callback
describe("with an error", function() {
beforeEach(function() {
this.UpdateManager.processOutstandingUpdatesWithLock = sinon.stub().callsArgWith(2, new Error("a generic error"));
return this.worker._waitForUpdateThenDispatchWorker(this.callback);
});
it "should log an error", ->
@logger.error.called.should.equal true
it("should log an error", function() {
return this.logger.error.called.should.equal(true);
});
it "should call the callback", ->
@callback.called.should.equal true
return it("should call the callback", function() {
return this.callback.called.should.equal(true);
});
});
describe "with a 'Delete component' error", ->
beforeEach ->
@UpdateManager.processOutstandingUpdatesWithLock = sinon.stub().callsArgWith(2, new Errors.DeleteMismatchError())
@worker._waitForUpdateThenDispatchWorker @callback
return describe("with a 'Delete component' error", function() {
beforeEach(function() {
this.UpdateManager.processOutstandingUpdatesWithLock = sinon.stub().callsArgWith(2, new Errors.DeleteMismatchError());
return this.worker._waitForUpdateThenDispatchWorker(this.callback);
});
it "should log a warning", ->
@logger.warn.called.should.equal true
it("should log a warning", function() {
return this.logger.warn.called.should.equal(true);
});
it "should call the callback", ->
@callback.called.should.equal true
return it("should call the callback", function() {
return this.callback.called.should.equal(true);
});
});
});
describe "run", ->
it "should call _waitForUpdateThenDispatchWorker until shutting down", (done) ->
callCount = 0
@worker._waitForUpdateThenDispatchWorker = (callback = (error) ->) =>
callCount++
if callCount == 3
@settings.shuttingDown = true
setTimeout () ->
callback()
, 10
sinon.spy @worker, "_waitForUpdateThenDispatchWorker"
@worker.run()
return describe("run", () => it("should call _waitForUpdateThenDispatchWorker until shutting down", function(done) {
let callCount = 0;
this.worker._waitForUpdateThenDispatchWorker = callback => {
if (callback == null) { callback = function(error) {}; }
callCount++;
if (callCount === 3) {
this.settings.shuttingDown = true;
}
return setTimeout(() => callback()
, 10);
};
sinon.spy(this.worker, "_waitForUpdateThenDispatchWorker");
this.worker.run();
checkStatus = () =>
if not @settings.shuttingDown # retry until shutdown
setTimeout checkStatus, 100
return
else
@worker._waitForUpdateThenDispatchWorker.callCount.should.equal 3
done()
var checkStatus = () => {
if (!this.settings.shuttingDown) { // retry until shutdown
setTimeout(checkStatus, 100);
return;
} else {
this.worker._waitForUpdateThenDispatchWorker.callCount.should.equal(3);
return done();
}
};
checkStatus()
return checkStatus();
}));
});
});

View file

@ -1,241 +1,307 @@
SandboxedModule = require('sandboxed-module')
sinon = require('sinon')
require('chai').should()
modulePath = require('path').join __dirname, '../../../../app/js/HistoryManager'
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const SandboxedModule = require('sandboxed-module');
const sinon = require('sinon');
require('chai').should();
const modulePath = require('path').join(__dirname, '../../../../app/js/HistoryManager');
describe "HistoryManager", ->
beforeEach ->
@HistoryManager = SandboxedModule.require modulePath, requires:
"request": @request = {}
"settings-sharelatex": @Settings = {
apis:
project_history:
enabled: true
describe("HistoryManager", function() {
beforeEach(function() {
this.HistoryManager = SandboxedModule.require(modulePath, { requires: {
"request": (this.request = {}),
"settings-sharelatex": (this.Settings = {
apis: {
project_history: {
enabled: true,
url: "http://project_history.example.com"
trackchanges:
},
trackchanges: {
url: "http://trackchanges.example.com"
}
"logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub(), debug: sinon.stub() }
"./DocumentManager": @DocumentManager = {}
"./HistoryRedisManager": @HistoryRedisManager = {}
"./RedisManager": @RedisManager = {}
"./ProjectHistoryRedisManager": @ProjectHistoryRedisManager = {}
"./Metrics": @metrics = {inc: sinon.stub()}
@project_id = "mock-project-id"
@doc_id = "mock-doc-id"
@callback = sinon.stub()
}
}
}),
"logger-sharelatex": (this.logger = { log: sinon.stub(), error: sinon.stub(), debug: sinon.stub() }),
"./DocumentManager": (this.DocumentManager = {}),
"./HistoryRedisManager": (this.HistoryRedisManager = {}),
"./RedisManager": (this.RedisManager = {}),
"./ProjectHistoryRedisManager": (this.ProjectHistoryRedisManager = {}),
"./Metrics": (this.metrics = {inc: sinon.stub()})
}
});
this.project_id = "mock-project-id";
this.doc_id = "mock-doc-id";
return this.callback = sinon.stub();
});
describe "flushDocChangesAsync", ->
beforeEach ->
@request.post = sinon.stub().callsArgWith(1, null, statusCode: 204)
describe("flushDocChangesAsync", function() {
beforeEach(function() {
return this.request.post = sinon.stub().callsArgWith(1, null, {statusCode: 204});
});
describe "when the project uses track changes", ->
beforeEach ->
@RedisManager.getHistoryType = sinon.stub().yields(null, 'track-changes')
@HistoryManager.flushDocChangesAsync @project_id, @doc_id
describe("when the project uses track changes", function() {
beforeEach(function() {
this.RedisManager.getHistoryType = sinon.stub().yields(null, 'track-changes');
return this.HistoryManager.flushDocChangesAsync(this.project_id, this.doc_id);
});
it "should send a request to the track changes api", ->
@request.post
.calledWith("#{@Settings.apis.trackchanges.url}/project/#{@project_id}/doc/#{@doc_id}/flush")
.should.equal true
return it("should send a request to the track changes api", function() {
return this.request.post
.calledWith(`${this.Settings.apis.trackchanges.url}/project/${this.project_id}/doc/${this.doc_id}/flush`)
.should.equal(true);
});
});
describe "when the project uses project history and double flush is not disabled", ->
beforeEach ->
@RedisManager.getHistoryType = sinon.stub().yields(null, 'project-history')
@HistoryManager.flushDocChangesAsync @project_id, @doc_id
describe("when the project uses project history and double flush is not disabled", function() {
beforeEach(function() {
this.RedisManager.getHistoryType = sinon.stub().yields(null, 'project-history');
return this.HistoryManager.flushDocChangesAsync(this.project_id, this.doc_id);
});
it "should send a request to the track changes api", ->
@request.post
return it("should send a request to the track changes api", function() {
return this.request.post
.called
.should.equal true
.should.equal(true);
});
});
describe "when the project uses project history and double flush is disabled", ->
beforeEach ->
@Settings.disableDoubleFlush = true
@RedisManager.getHistoryType = sinon.stub().yields(null, 'project-history')
@HistoryManager.flushDocChangesAsync @project_id, @doc_id
return describe("when the project uses project history and double flush is disabled", function() {
beforeEach(function() {
this.Settings.disableDoubleFlush = true;
this.RedisManager.getHistoryType = sinon.stub().yields(null, 'project-history');
return this.HistoryManager.flushDocChangesAsync(this.project_id, this.doc_id);
});
it "should not send a request to the track changes api", ->
@request.post
return it("should not send a request to the track changes api", function() {
return this.request.post
.called
.should.equal false
.should.equal(false);
});
});
});
describe "flushProjectChangesAsync", ->
beforeEach ->
@request.post = sinon.stub().callsArgWith(1, null, statusCode: 204)
describe("flushProjectChangesAsync", function() {
beforeEach(function() {
this.request.post = sinon.stub().callsArgWith(1, null, {statusCode: 204});
@HistoryManager.flushProjectChangesAsync @project_id
return this.HistoryManager.flushProjectChangesAsync(this.project_id);
});
it "should send a request to the project history api", ->
@request.post
.calledWith({url: "#{@Settings.apis.project_history.url}/project/#{@project_id}/flush", qs:{background:true}})
.should.equal true
return it("should send a request to the project history api", function() {
return this.request.post
.calledWith({url: `${this.Settings.apis.project_history.url}/project/${this.project_id}/flush`, qs:{background:true}})
.should.equal(true);
});
});
describe "flushProjectChanges", ->
describe("flushProjectChanges", function() {
describe "in the normal case", ->
beforeEach ->
@request.post = sinon.stub().callsArgWith(1, null, statusCode: 204)
@HistoryManager.flushProjectChanges @project_id, {background:true}
describe("in the normal case", function() {
beforeEach(function() {
this.request.post = sinon.stub().callsArgWith(1, null, {statusCode: 204});
return this.HistoryManager.flushProjectChanges(this.project_id, {background:true});});
it "should send a request to the project history api", ->
@request.post
.calledWith({url: "#{@Settings.apis.project_history.url}/project/#{@project_id}/flush", qs:{background:true}})
.should.equal true
return it("should send a request to the project history api", function() {
return this.request.post
.calledWith({url: `${this.Settings.apis.project_history.url}/project/${this.project_id}/flush`, qs:{background:true}})
.should.equal(true);
});
});
describe "with the skip_history_flush option", ->
beforeEach ->
@request.post = sinon.stub()
@HistoryManager.flushProjectChanges @project_id, {skip_history_flush:true}
return describe("with the skip_history_flush option", function() {
beforeEach(function() {
this.request.post = sinon.stub();
return this.HistoryManager.flushProjectChanges(this.project_id, {skip_history_flush:true});});
it "should not send a request to the project history api", ->
@request.post
return it("should not send a request to the project history api", function() {
return this.request.post
.called
.should.equal false
.should.equal(false);
});
});
});
describe "recordAndFlushHistoryOps", ->
beforeEach ->
@ops = [ 'mock-ops' ]
@project_ops_length = 10
@doc_ops_length = 5
describe("recordAndFlushHistoryOps", function() {
beforeEach(function() {
this.ops = [ 'mock-ops' ];
this.project_ops_length = 10;
this.doc_ops_length = 5;
@HistoryManager.flushProjectChangesAsync = sinon.stub()
@HistoryRedisManager.recordDocHasHistoryOps = sinon.stub().callsArg(3)
@HistoryManager.flushDocChangesAsync = sinon.stub()
this.HistoryManager.flushProjectChangesAsync = sinon.stub();
this.HistoryRedisManager.recordDocHasHistoryOps = sinon.stub().callsArg(3);
return this.HistoryManager.flushDocChangesAsync = sinon.stub();
});
describe "with no ops", ->
beforeEach ->
@HistoryManager.recordAndFlushHistoryOps(
@project_id, @doc_id, [], @doc_ops_length, @project_ops_length, @callback
)
describe("with no ops", function() {
beforeEach(function() {
return this.HistoryManager.recordAndFlushHistoryOps(
this.project_id, this.doc_id, [], this.doc_ops_length, this.project_ops_length, this.callback
);
});
it "should not flush project changes", ->
@HistoryManager.flushProjectChangesAsync.called.should.equal false
it("should not flush project changes", function() {
return this.HistoryManager.flushProjectChangesAsync.called.should.equal(false);
});
it "should not record doc has history ops", ->
@HistoryRedisManager.recordDocHasHistoryOps.called.should.equal false
it("should not record doc has history ops", function() {
return this.HistoryRedisManager.recordDocHasHistoryOps.called.should.equal(false);
});
it "should not flush doc changes", ->
@HistoryManager.flushDocChangesAsync.called.should.equal false
it("should not flush doc changes", function() {
return this.HistoryManager.flushDocChangesAsync.called.should.equal(false);
});
it "should call the callback", ->
@callback.called.should.equal true
return it("should call the callback", function() {
return this.callback.called.should.equal(true);
});
});
describe "with enough ops to flush project changes", ->
beforeEach ->
@HistoryManager.shouldFlushHistoryOps = sinon.stub()
@HistoryManager.shouldFlushHistoryOps.withArgs(@project_ops_length).returns(true)
@HistoryManager.shouldFlushHistoryOps.withArgs(@doc_ops_length).returns(false)
describe("with enough ops to flush project changes", function() {
beforeEach(function() {
this.HistoryManager.shouldFlushHistoryOps = sinon.stub();
this.HistoryManager.shouldFlushHistoryOps.withArgs(this.project_ops_length).returns(true);
this.HistoryManager.shouldFlushHistoryOps.withArgs(this.doc_ops_length).returns(false);
@HistoryManager.recordAndFlushHistoryOps(
@project_id, @doc_id, @ops, @doc_ops_length, @project_ops_length, @callback
)
return this.HistoryManager.recordAndFlushHistoryOps(
this.project_id, this.doc_id, this.ops, this.doc_ops_length, this.project_ops_length, this.callback
);
});
it "should flush project changes", ->
@HistoryManager.flushProjectChangesAsync
.calledWith(@project_id)
.should.equal true
it("should flush project changes", function() {
return this.HistoryManager.flushProjectChangesAsync
.calledWith(this.project_id)
.should.equal(true);
});
it "should record doc has history ops", ->
@HistoryRedisManager.recordDocHasHistoryOps
.calledWith(@project_id, @doc_id, @ops)
it("should record doc has history ops", function() {
return this.HistoryRedisManager.recordDocHasHistoryOps
.calledWith(this.project_id, this.doc_id, this.ops);
});
it "should not flush doc changes", ->
@HistoryManager.flushDocChangesAsync.called.should.equal false
it("should not flush doc changes", function() {
return this.HistoryManager.flushDocChangesAsync.called.should.equal(false);
});
it "should call the callback", ->
@callback.called.should.equal true
return it("should call the callback", function() {
return this.callback.called.should.equal(true);
});
});
describe "with enough ops to flush doc changes", ->
beforeEach ->
@HistoryManager.shouldFlushHistoryOps = sinon.stub()
@HistoryManager.shouldFlushHistoryOps.withArgs(@project_ops_length).returns(false)
@HistoryManager.shouldFlushHistoryOps.withArgs(@doc_ops_length).returns(true)
describe("with enough ops to flush doc changes", function() {
beforeEach(function() {
this.HistoryManager.shouldFlushHistoryOps = sinon.stub();
this.HistoryManager.shouldFlushHistoryOps.withArgs(this.project_ops_length).returns(false);
this.HistoryManager.shouldFlushHistoryOps.withArgs(this.doc_ops_length).returns(true);
@HistoryManager.recordAndFlushHistoryOps(
@project_id, @doc_id, @ops, @doc_ops_length, @project_ops_length, @callback
)
return this.HistoryManager.recordAndFlushHistoryOps(
this.project_id, this.doc_id, this.ops, this.doc_ops_length, this.project_ops_length, this.callback
);
});
it "should not flush project changes", ->
@HistoryManager.flushProjectChangesAsync.called.should.equal false
it("should not flush project changes", function() {
return this.HistoryManager.flushProjectChangesAsync.called.should.equal(false);
});
it "should record doc has history ops", ->
@HistoryRedisManager.recordDocHasHistoryOps
.calledWith(@project_id, @doc_id, @ops)
it("should record doc has history ops", function() {
return this.HistoryRedisManager.recordDocHasHistoryOps
.calledWith(this.project_id, this.doc_id, this.ops);
});
it "should flush doc changes", ->
@HistoryManager.flushDocChangesAsync
.calledWith(@project_id, @doc_id)
.should.equal true
it("should flush doc changes", function() {
return this.HistoryManager.flushDocChangesAsync
.calledWith(this.project_id, this.doc_id)
.should.equal(true);
});
it "should call the callback", ->
@callback.called.should.equal true
return it("should call the callback", function() {
return this.callback.called.should.equal(true);
});
});
describe "when recording doc has history ops errors", ->
beforeEach ->
@error = new Error("error")
@HistoryRedisManager.recordDocHasHistoryOps =
sinon.stub().callsArgWith(3, @error)
describe("when recording doc has history ops errors", function() {
beforeEach(function() {
this.error = new Error("error");
this.HistoryRedisManager.recordDocHasHistoryOps =
sinon.stub().callsArgWith(3, this.error);
@HistoryManager.recordAndFlushHistoryOps(
@project_id, @doc_id, @ops, @doc_ops_length, @project_ops_length, @callback
)
return this.HistoryManager.recordAndFlushHistoryOps(
this.project_id, this.doc_id, this.ops, this.doc_ops_length, this.project_ops_length, this.callback
);
});
it "should not flush doc changes", ->
@HistoryManager.flushDocChangesAsync.called.should.equal false
it("should not flush doc changes", function() {
return this.HistoryManager.flushDocChangesAsync.called.should.equal(false);
});
it "should call the callback with the error", ->
@callback.calledWith(@error).should.equal true
return it("should call the callback with the error", function() {
return this.callback.calledWith(this.error).should.equal(true);
});
});
describe "shouldFlushHistoryOps", ->
it "should return false if the number of ops is not known", ->
@HistoryManager.shouldFlushHistoryOps(null, ['a', 'b', 'c'].length, 1).should.equal false
return describe("shouldFlushHistoryOps", function() {
it("should return false if the number of ops is not known", function() {
return this.HistoryManager.shouldFlushHistoryOps(null, ['a', 'b', 'c'].length, 1).should.equal(false);
});
it "should return false if the updates didn't take us past the threshold", ->
# Currently there are 14 ops
# Previously we were on 11 ops
# We didn't pass over a multiple of 5
@HistoryManager.shouldFlushHistoryOps(14, ['a', 'b', 'c'].length, 5).should.equal false
it("should return false if the updates didn't take us past the threshold", function() {
// Currently there are 14 ops
// Previously we were on 11 ops
// We didn't pass over a multiple of 5
this.HistoryManager.shouldFlushHistoryOps(14, ['a', 'b', 'c'].length, 5).should.equal(false);
it "should return true if the updates took to the threshold", ->
# Currently there are 15 ops
# Previously we were on 12 ops
# We've reached a new multiple of 5
@HistoryManager.shouldFlushHistoryOps(15, ['a', 'b', 'c'].length, 5).should.equal true
it("should return true if the updates took to the threshold", function() {});
// Currently there are 15 ops
// Previously we were on 12 ops
// We've reached a new multiple of 5
return this.HistoryManager.shouldFlushHistoryOps(15, ['a', 'b', 'c'].length, 5).should.equal(true);
});
it "should return true if the updates took past the threshold", ->
# Currently there are 19 ops
# Previously we were on 16 ops
# We didn't pass over a multiple of 5
@HistoryManager.shouldFlushHistoryOps(17, ['a', 'b', 'c'].length, 5).should.equal true
return it("should return true if the updates took past the threshold", function() {
// Currently there are 19 ops
// Previously we were on 16 ops
// We didn't pass over a multiple of 5
return this.HistoryManager.shouldFlushHistoryOps(17, ['a', 'b', 'c'].length, 5).should.equal(true);
});
});
});
describe "resyncProjectHistory", ->
beforeEach ->
@projectHistoryId = 'history-id-1234'
@docs = [
doc: @doc_id
return describe("resyncProjectHistory", function() {
beforeEach(function() {
this.projectHistoryId = 'history-id-1234';
this.docs = [{
doc: this.doc_id,
path: 'main.tex'
]
@files = [
file: 'mock-file-id'
path: 'universe.png'
url: "www.filestore.test/#{@project_id}/mock-file-id"
]
@ProjectHistoryRedisManager.queueResyncProjectStructure = sinon.stub().yields()
@DocumentManager.resyncDocContentsWithLock = sinon.stub().yields()
@HistoryManager.resyncProjectHistory @project_id, @projectHistoryId, @docs, @files, @callback
}
];
this.files = [{
file: 'mock-file-id',
path: 'universe.png',
url: `www.filestore.test/${this.project_id}/mock-file-id`
}
];
this.ProjectHistoryRedisManager.queueResyncProjectStructure = sinon.stub().yields();
this.DocumentManager.resyncDocContentsWithLock = sinon.stub().yields();
return this.HistoryManager.resyncProjectHistory(this.project_id, this.projectHistoryId, this.docs, this.files, this.callback);
});
it "should queue a project structure reync", ->
@ProjectHistoryRedisManager.queueResyncProjectStructure
.calledWith(@project_id, @projectHistoryId, @docs, @files)
.should.equal true
it("should queue a project structure reync", function() {
return this.ProjectHistoryRedisManager.queueResyncProjectStructure
.calledWith(this.project_id, this.projectHistoryId, this.docs, this.files)
.should.equal(true);
});
it "should queue doc content reyncs", ->
@DocumentManager
it("should queue doc content reyncs", function() {
return this.DocumentManager
.resyncDocContentsWithLock
.calledWith(@project_id, @doc_id)
.should.equal true
.calledWith(this.project_id, this.doc_id)
.should.equal(true);
});
it "should call the callback", ->
@callback.called.should.equal true
return it("should call the callback", function() {
return this.callback.called.should.equal(true);
});
});
});

View file

@ -1,55 +1,82 @@
sinon = require('sinon')
chai = require('chai')
should = chai.should()
modulePath = "../../../../app/js/HistoryRedisManager.js"
SandboxedModule = require('sandboxed-module')
Errors = require "../../../../app/js/Errors"
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const sinon = require('sinon');
const chai = require('chai');
const should = chai.should();
const modulePath = "../../../../app/js/HistoryRedisManager.js";
const SandboxedModule = require('sandboxed-module');
const Errors = require("../../../../app/js/Errors");
describe "HistoryRedisManager", ->
beforeEach ->
@rclient =
auth: () ->
describe("HistoryRedisManager", function() {
beforeEach(function() {
this.rclient = {
auth() {},
exec: sinon.stub()
@rclient.multi = () => @rclient
@HistoryRedisManager = SandboxedModule.require modulePath, requires:
"redis-sharelatex": createClient: () => @rclient
"settings-sharelatex":
redis:
history: @settings =
key_schema:
uncompressedHistoryOps: ({doc_id}) -> "UncompressedHistoryOps:#{doc_id}"
docsWithHistoryOps: ({project_id}) -> "DocsWithHistoryOps:#{project_id}"
"logger-sharelatex": { log: () -> }
@doc_id = "doc-id-123"
@project_id = "project-id-123"
@callback = sinon.stub()
};
this.rclient.multi = () => this.rclient;
this.HistoryRedisManager = SandboxedModule.require(modulePath, { requires: {
"redis-sharelatex": { createClient: () => this.rclient
},
"settings-sharelatex": {
redis: {
history: (this.settings = {
key_schema: {
uncompressedHistoryOps({doc_id}) { return `UncompressedHistoryOps:${doc_id}`; },
docsWithHistoryOps({project_id}) { return `DocsWithHistoryOps:${project_id}`; }
}
})
}
},
"logger-sharelatex": { log() {} }
}
});
this.doc_id = "doc-id-123";
this.project_id = "project-id-123";
return this.callback = sinon.stub();
});
describe "recordDocHasHistoryOps", ->
beforeEach ->
@ops = [{ op: [{ i: "foo", p: 4 }] },{ op: [{ i: "bar", p: 56 }] }]
@rclient.sadd = sinon.stub().yields()
return describe("recordDocHasHistoryOps", function() {
beforeEach(function() {
this.ops = [{ op: [{ i: "foo", p: 4 }] },{ op: [{ i: "bar", p: 56 }] }];
return this.rclient.sadd = sinon.stub().yields();
});
describe "with ops", ->
beforeEach (done) ->
@HistoryRedisManager.recordDocHasHistoryOps @project_id, @doc_id, @ops, (args...) =>
@callback(args...)
done()
describe("with ops", function() {
beforeEach(function(done) {
return this.HistoryRedisManager.recordDocHasHistoryOps(this.project_id, this.doc_id, this.ops, (...args) => {
this.callback(...Array.from(args || []));
return done();
});
});
it "should add the doc_id to the set of which records the project docs", ->
@rclient.sadd
.calledWith("DocsWithHistoryOps:#{@project_id}", @doc_id)
.should.equal true
return it("should add the doc_id to the set of which records the project docs", function() {
return this.rclient.sadd
.calledWith(`DocsWithHistoryOps:${this.project_id}`, this.doc_id)
.should.equal(true);
});
});
describe "with no ops", ->
beforeEach (done) ->
@HistoryRedisManager.recordDocHasHistoryOps @project_id, @doc_id, [], (args...) =>
@callback(args...)
done()
return describe("with no ops", function() {
beforeEach(function(done) {
return this.HistoryRedisManager.recordDocHasHistoryOps(this.project_id, this.doc_id, [], (...args) => {
this.callback(...Array.from(args || []));
return done();
});
});
it "should not add the doc_id to the set of which records the project docs", ->
@rclient.sadd
it("should not add the doc_id to the set of which records the project docs", function() {
return this.rclient.sadd
.called
.should.equal false
.should.equal(false);
});
it "should call the callback with an error", ->
@callback.calledWith(new Error("cannot push no ops")).should.equal true
return it("should call the callback with an error", function() {
return this.callback.calledWith(new Error("cannot push no ops")).should.equal(true);
});
});
});
});

View file

@ -1,37 +1,62 @@
require('coffee-script')
sinon = require('sinon')
assert = require('assert')
path = require('path')
modulePath = path.join __dirname, '../../../../app/js/LockManager.js'
project_id = 1234
doc_id = 5678
blockingKey = "Blocking:#{doc_id}"
SandboxedModule = require('sandboxed-module')
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS206: Consider reworking classes to avoid initClass
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
require('coffee-script');
const sinon = require('sinon');
const assert = require('assert');
const path = require('path');
const modulePath = path.join(__dirname, '../../../../app/js/LockManager.js');
const project_id = 1234;
const doc_id = 5678;
const blockingKey = `Blocking:${doc_id}`;
const SandboxedModule = require('sandboxed-module');
describe 'LockManager - checking the lock', ()->
describe('LockManager - checking the lock', function(){
existsStub = sinon.stub()
let Profiler;
const existsStub = sinon.stub();
mocks =
"logger-sharelatex": log:->
"redis-sharelatex":
createClient : ()->
auth:->
exists: existsStub
"./Metrics": {inc: () ->}
"./Profiler": class Profiler
log: sinon.stub().returns { end: sinon.stub() }
end: sinon.stub()
LockManager = SandboxedModule.require(modulePath, requires: mocks)
const mocks = {
"logger-sharelatex": { log() {}
},
"redis-sharelatex": {
createClient(){
return {
auth() {},
exists: existsStub
};
}
},
"./Metrics": {inc() {}},
"./Profiler": (Profiler = (function() {
Profiler = class Profiler {
static initClass() {
this.prototype.log = sinon.stub().returns({ end: sinon.stub() });
this.prototype.end = sinon.stub();
}
};
Profiler.initClass();
return Profiler;
})())
};
const LockManager = SandboxedModule.require(modulePath, {requires: mocks});
it 'should return true if the key does not exists', (done)->
existsStub.yields(null, "0")
LockManager.checkLock doc_id, (err, free)->
free.should.equal true
done()
it('should return true if the key does not exists', function(done){
existsStub.yields(null, "0");
return LockManager.checkLock(doc_id, function(err, free){
free.should.equal(true);
return done();
});
});
it 'should return false if the key does exists', (done)->
existsStub.yields(null, "1")
LockManager.checkLock doc_id, (err, free)->
free.should.equal false
done()
return it('should return false if the key does exists', function(done){
existsStub.yields(null, "1");
return LockManager.checkLock(doc_id, function(err, free){
free.should.equal(false);
return done();
});
});
});

View file

@ -1,53 +1,82 @@
require('coffee-script')
sinon = require('sinon')
assert = require('assert')
path = require('path')
modulePath = path.join __dirname, '../../../../app/js/LockManager.js'
project_id = 1234
doc_id = 5678
SandboxedModule = require('sandboxed-module')
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS206: Consider reworking classes to avoid initClass
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
require('coffee-script');
const sinon = require('sinon');
const assert = require('assert');
const path = require('path');
const modulePath = path.join(__dirname, '../../../../app/js/LockManager.js');
const project_id = 1234;
const doc_id = 5678;
const SandboxedModule = require('sandboxed-module');
describe 'LockManager - releasing the lock', ()->
beforeEach ->
@client = {
auth: ->
describe('LockManager - releasing the lock', function(){
beforeEach(function() {
let Profiler;
this.client = {
auth() {},
eval: sinon.stub()
}
mocks =
"logger-sharelatex":
log:->
error:->
"redis-sharelatex":
createClient : () => @client
};
const mocks = {
"logger-sharelatex": {
log() {},
error() {}
},
"redis-sharelatex": {
createClient : () => this.client
},
"settings-sharelatex": {
redis:
lock:
key_schema:
blockingKey: ({doc_id}) -> "Blocking:#{doc_id}"
}
"./Metrics": {inc: () ->}
"./Profiler": class Profiler
log: sinon.stub().returns { end: sinon.stub() }
end: sinon.stub()
@LockManager = SandboxedModule.require(modulePath, requires: mocks)
@lockValue = "lock-value-stub"
@callback = sinon.stub()
redis: {
lock: {
key_schema: {
blockingKey({doc_id}) { return `Blocking:${doc_id}`; }
}
}
}
},
"./Metrics": {inc() {}},
"./Profiler": (Profiler = (function() {
Profiler = class Profiler {
static initClass() {
this.prototype.log = sinon.stub().returns({ end: sinon.stub() });
this.prototype.end = sinon.stub();
}
};
Profiler.initClass();
return Profiler;
})())
};
this.LockManager = SandboxedModule.require(modulePath, {requires: mocks});
this.lockValue = "lock-value-stub";
return this.callback = sinon.stub();
});
describe "when the lock is current", ->
beforeEach ->
@client.eval = sinon.stub().yields(null, 1)
@LockManager.releaseLock doc_id, @lockValue, @callback
describe("when the lock is current", function() {
beforeEach(function() {
this.client.eval = sinon.stub().yields(null, 1);
return this.LockManager.releaseLock(doc_id, this.lockValue, this.callback);
});
it 'should clear the data from redis', ->
@client.eval.calledWith(@LockManager.unlockScript, 1, "Blocking:#{doc_id}", @lockValue).should.equal true
it('should clear the data from redis', function() {
return this.client.eval.calledWith(this.LockManager.unlockScript, 1, `Blocking:${doc_id}`, this.lockValue).should.equal(true);
});
it 'should call the callback', ->
@callback.called.should.equal true
return it('should call the callback', function() {
return this.callback.called.should.equal(true);
});
});
describe "when the lock has expired", ->
beforeEach ->
@client.eval = sinon.stub().yields(null, 0)
@LockManager.releaseLock doc_id, @lockValue, @callback
return describe("when the lock has expired", function() {
beforeEach(function() {
this.client.eval = sinon.stub().yields(null, 0);
return this.LockManager.releaseLock(doc_id, this.lockValue, this.callback);
});
it 'should return an error if the lock has expired', ->
@callback.calledWith(new Error("tried to release timed out lock")).should.equal true
return it('should return an error if the lock has expired', function() {
return this.callback.calledWith(new Error("tried to release timed out lock")).should.equal(true);
});
});
});

View file

@ -1,79 +1,121 @@
sinon = require('sinon')
chai = require('chai')
should = chai.should()
modulePath = "../../../../app/js/LockManager.js"
SandboxedModule = require('sandboxed-module')
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS206: Consider reworking classes to avoid initClass
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const sinon = require('sinon');
const chai = require('chai');
const should = chai.should();
const modulePath = "../../../../app/js/LockManager.js";
const SandboxedModule = require('sandboxed-module');
describe 'LockManager - getting the lock', ->
beforeEach ->
@LockManager = SandboxedModule.require modulePath, requires:
"logger-sharelatex": log:->
"redis-sharelatex":
createClient : () =>
auth:->
"./Metrics": {inc: () ->}
"./Profiler": class Profiler
log: sinon.stub().returns { end: sinon.stub() }
end: sinon.stub()
@callback = sinon.stub()
@doc_id = "doc-id-123"
describe('LockManager - getting the lock', function() {
beforeEach(function() {
let Profiler;
this.LockManager = SandboxedModule.require(modulePath, { requires: {
"logger-sharelatex": { log() {}
},
"redis-sharelatex": {
createClient : () => {
return {auth() {}};
}
},
"./Metrics": {inc() {}},
"./Profiler": (Profiler = (function() {
Profiler = class Profiler {
static initClass() {
this.prototype.log = sinon.stub().returns({ end: sinon.stub() });
this.prototype.end = sinon.stub();
}
};
Profiler.initClass();
return Profiler;
})())
}
}
);
this.callback = sinon.stub();
return this.doc_id = "doc-id-123";
});
describe "when the lock is not set", ->
beforeEach (done) ->
@lockValue = "mock-lock-value"
@LockManager.tryLock = sinon.stub().callsArgWith(1, null, true, @lockValue)
@LockManager.getLock @doc_id, (args...) =>
@callback(args...)
done()
describe("when the lock is not set", function() {
beforeEach(function(done) {
this.lockValue = "mock-lock-value";
this.LockManager.tryLock = sinon.stub().callsArgWith(1, null, true, this.lockValue);
return this.LockManager.getLock(this.doc_id, (...args) => {
this.callback(...Array.from(args || []));
return done();
});
});
it "should try to get the lock", ->
@LockManager.tryLock
.calledWith(@doc_id)
.should.equal true
it("should try to get the lock", function() {
return this.LockManager.tryLock
.calledWith(this.doc_id)
.should.equal(true);
});
it "should only need to try once", ->
@LockManager.tryLock.callCount.should.equal 1
it("should only need to try once", function() {
return this.LockManager.tryLock.callCount.should.equal(1);
});
it "should return the callback with the lock value", ->
@callback.calledWith(null, @lockValue).should.equal true
return it("should return the callback with the lock value", function() {
return this.callback.calledWith(null, this.lockValue).should.equal(true);
});
});
describe "when the lock is initially set", ->
beforeEach (done) ->
@lockValue = "mock-lock-value"
startTime = Date.now()
tries = 0
@LockManager.LOCK_TEST_INTERVAL = 5
@LockManager.tryLock = (doc_id, callback = (error, isFree) ->) =>
if (Date.now() - startTime < 20) or (tries < 2)
tries = tries + 1
callback null, false
else
callback null, true, @lockValue
sinon.spy @LockManager, "tryLock"
describe("when the lock is initially set", function() {
beforeEach(function(done) {
this.lockValue = "mock-lock-value";
const startTime = Date.now();
let tries = 0;
this.LockManager.LOCK_TEST_INTERVAL = 5;
this.LockManager.tryLock = (doc_id, callback) => {
if (callback == null) { callback = function(error, isFree) {}; }
if (((Date.now() - startTime) < 20) || (tries < 2)) {
tries = tries + 1;
return callback(null, false);
} else {
return callback(null, true, this.lockValue);
}
};
sinon.spy(this.LockManager, "tryLock");
@LockManager.getLock @doc_id, (args...) =>
@callback(args...)
done()
return this.LockManager.getLock(this.doc_id, (...args) => {
this.callback(...Array.from(args || []));
return done();
});
});
it "should call tryLock multiple times until free", ->
(@LockManager.tryLock.callCount > 1).should.equal true
it("should call tryLock multiple times until free", function() {
return (this.LockManager.tryLock.callCount > 1).should.equal(true);
});
it "should return the callback with the lock value", ->
@callback.calledWith(null, @lockValue).should.equal true
return it("should return the callback with the lock value", function() {
return this.callback.calledWith(null, this.lockValue).should.equal(true);
});
});
describe "when the lock times out", ->
beforeEach (done) ->
time = Date.now()
@LockManager.MAX_LOCK_WAIT_TIME = 5
@LockManager.tryLock = sinon.stub().callsArgWith(1, null, false)
@LockManager.getLock @doc_id, (args...) =>
@callback(args...)
done()
return describe("when the lock times out", function() {
beforeEach(function(done) {
const time = Date.now();
this.LockManager.MAX_LOCK_WAIT_TIME = 5;
this.LockManager.tryLock = sinon.stub().callsArgWith(1, null, false);
return this.LockManager.getLock(this.doc_id, (...args) => {
this.callback(...Array.from(args || []));
return done();
});
});
it "should return the callback with an error", ->
e = new Error("Timeout")
e.doc_id = @doc_id
@callback.calledWith(e).should.equal true
return it("should return the callback with an error", function() {
const e = new Error("Timeout");
e.doc_id = this.doc_id;
return this.callback.calledWith(e).should.equal(true);
});
});
});

View file

@ -1,86 +1,132 @@
sinon = require('sinon')
chai = require('chai')
should = chai.should()
modulePath = "../../../../app/js/LockManager.js"
SandboxedModule = require('sandboxed-module')
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS206: Consider reworking classes to avoid initClass
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const sinon = require('sinon');
const chai = require('chai');
const should = chai.should();
const modulePath = "../../../../app/js/LockManager.js";
const SandboxedModule = require('sandboxed-module');
describe 'LockManager - trying the lock', ->
beforeEach ->
@LockManager = SandboxedModule.require modulePath, requires:
"logger-sharelatex": log:->
"redis-sharelatex":
createClient : () =>
auth:->
set: @set = sinon.stub()
"./Metrics": {inc: () ->}
describe('LockManager - trying the lock', function() {
beforeEach(function() {
let Profiler;
this.LockManager = SandboxedModule.require(modulePath, { requires: {
"logger-sharelatex": { log() {}
},
"redis-sharelatex": {
createClient : () => {
return {
auth() {},
set: (this.set = sinon.stub())
};
}
},
"./Metrics": {inc() {}},
"settings-sharelatex": {
redis:
lock:
key_schema:
blockingKey: ({doc_id}) -> "Blocking:#{doc_id}"
}
"./Profiler": @Profiler = class Profiler
log: sinon.stub().returns { end: sinon.stub() }
end: sinon.stub()
redis: {
lock: {
key_schema: {
blockingKey({doc_id}) { return `Blocking:${doc_id}`; }
}
}
}
},
"./Profiler": (this.Profiler = (Profiler = (function() {
Profiler = class Profiler {
static initClass() {
this.prototype.log = sinon.stub().returns({ end: sinon.stub() });
this.prototype.end = sinon.stub();
}
};
Profiler.initClass();
return Profiler;
})()))
}
}
);
@callback = sinon.stub()
@doc_id = "doc-id-123"
this.callback = sinon.stub();
return this.doc_id = "doc-id-123";
});
describe "when the lock is not set", ->
beforeEach ->
@lockValue = "mock-lock-value"
@LockManager.randomLock = sinon.stub().returns @lockValue
@set.callsArgWith(5, null, "OK")
@LockManager.tryLock @doc_id, @callback
describe("when the lock is not set", function() {
beforeEach(function() {
this.lockValue = "mock-lock-value";
this.LockManager.randomLock = sinon.stub().returns(this.lockValue);
this.set.callsArgWith(5, null, "OK");
return this.LockManager.tryLock(this.doc_id, this.callback);
});
it "should set the lock key with an expiry if it is not set", ->
@set.calledWith("Blocking:#{@doc_id}", @lockValue, "EX", 30, "NX")
.should.equal true
it("should set the lock key with an expiry if it is not set", function() {
return this.set.calledWith(`Blocking:${this.doc_id}`, this.lockValue, "EX", 30, "NX")
.should.equal(true);
});
it "should return the callback with true and the lock value", ->
@callback.calledWith(null, true, @lockValue).should.equal true
return it("should return the callback with true and the lock value", function() {
return this.callback.calledWith(null, true, this.lockValue).should.equal(true);
});
});
describe "when the lock is already set", ->
beforeEach ->
@set.callsArgWith(5, null, null)
@LockManager.tryLock @doc_id, @callback
describe("when the lock is already set", function() {
beforeEach(function() {
this.set.callsArgWith(5, null, null);
return this.LockManager.tryLock(this.doc_id, this.callback);
});
it "should return the callback with false", ->
@callback.calledWith(null, false).should.equal true
return it("should return the callback with false", function() {
return this.callback.calledWith(null, false).should.equal(true);
});
});
describe "when it takes a long time for redis to set the lock", ->
beforeEach ->
@Profiler.prototype.end = () -> 7000 # take a long time
@Profiler.prototype.log = sinon.stub().returns { end: @Profiler.prototype.end }
@lockValue = "mock-lock-value"
@LockManager.randomLock = sinon.stub().returns @lockValue
@LockManager.releaseLock = sinon.stub().callsArgWith(2,null)
@set.callsArgWith(5, null, "OK")
return describe("when it takes a long time for redis to set the lock", function() {
beforeEach(function() {
this.Profiler.prototype.end = () => 7000; // take a long time
this.Profiler.prototype.log = sinon.stub().returns({ end: this.Profiler.prototype.end });
this.lockValue = "mock-lock-value";
this.LockManager.randomLock = sinon.stub().returns(this.lockValue);
this.LockManager.releaseLock = sinon.stub().callsArgWith(2,null);
return this.set.callsArgWith(5, null, "OK");
});
describe "in all cases", ->
beforeEach ->
@LockManager.tryLock @doc_id, @callback
describe("in all cases", function() {
beforeEach(function() {
return this.LockManager.tryLock(this.doc_id, this.callback);
});
it "should set the lock key with an expiry if it is not set", ->
@set.calledWith("Blocking:#{@doc_id}", @lockValue, "EX", 30, "NX")
.should.equal true
it("should set the lock key with an expiry if it is not set", function() {
return this.set.calledWith(`Blocking:${this.doc_id}`, this.lockValue, "EX", 30, "NX")
.should.equal(true);
});
it "should try to release the lock", ->
@LockManager.releaseLock.calledWith(@doc_id, @lockValue).should.equal true
return it("should try to release the lock", function() {
return this.LockManager.releaseLock.calledWith(this.doc_id, this.lockValue).should.equal(true);
});
});
describe "if the lock is released successfully", ->
beforeEach ->
@LockManager.releaseLock = sinon.stub().callsArgWith(2,null)
@LockManager.tryLock @doc_id, @callback
describe("if the lock is released successfully", function() {
beforeEach(function() {
this.LockManager.releaseLock = sinon.stub().callsArgWith(2,null);
return this.LockManager.tryLock(this.doc_id, this.callback);
});
it "should return the callback with false", ->
@callback.calledWith(null, false).should.equal true
return it("should return the callback with false", function() {
return this.callback.calledWith(null, false).should.equal(true);
});
});
describe "if the lock has already timed out", ->
beforeEach ->
@LockManager.releaseLock = sinon.stub().callsArgWith(2, new Error("tried to release timed out lock"))
@LockManager.tryLock @doc_id, @callback
return describe("if the lock has already timed out", function() {
beforeEach(function() {
this.LockManager.releaseLock = sinon.stub().callsArgWith(2, new Error("tried to release timed out lock"));
return this.LockManager.tryLock(this.doc_id, this.callback);
});
it "should return the callback with an error", ->
e = new Error("tried to release timed out lock")
@callback.calledWith(e).should.equal true
return it("should return the callback with an error", function() {
const e = new Error("tried to release timed out lock");
return this.callback.calledWith(e).should.equal(true);
});
});
});
});

View file

@ -1,226 +1,304 @@
sinon = require('sinon')
chai = require('chai')
should = chai.should()
modulePath = "../../../../app/js/PersistenceManager.js"
SandboxedModule = require('sandboxed-module')
Errors = require "../../../../app/js/Errors"
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS206: Consider reworking classes to avoid initClass
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const sinon = require('sinon');
const chai = require('chai');
const should = chai.should();
const modulePath = "../../../../app/js/PersistenceManager.js";
const SandboxedModule = require('sandboxed-module');
const Errors = require("../../../../app/js/Errors");
describe "PersistenceManager", ->
beforeEach ->
@request = sinon.stub()
@request.defaults = () => @request
@PersistenceManager = SandboxedModule.require modulePath, requires:
"requestretry": @request
"settings-sharelatex": @Settings = {}
"./Metrics": @Metrics =
Timer: class Timer
done: sinon.stub()
describe("PersistenceManager", function() {
beforeEach(function() {
let Timer;
this.request = sinon.stub();
this.request.defaults = () => this.request;
this.PersistenceManager = SandboxedModule.require(modulePath, { requires: {
"requestretry": this.request,
"settings-sharelatex": (this.Settings = {}),
"./Metrics": (this.Metrics = {
Timer: (Timer = (function() {
Timer = class Timer {
static initClass() {
this.prototype.done = sinon.stub();
}
};
Timer.initClass();
return Timer;
})()),
inc: sinon.stub()
"logger-sharelatex": @logger = {log: sinon.stub(), err: sinon.stub()}
@project_id = "project-id-123"
@projectHistoryId = "history-id-123"
@doc_id = "doc-id-123"
@lines = ["one", "two", "three"]
@version = 42
@callback = sinon.stub()
@ranges = { comments: "mock", entries: "mock" }
@pathname = '/a/b/c.tex'
@lastUpdatedAt = Date.now()
@lastUpdatedBy = 'last-author-id'
@Settings.apis =
web:
url: @url = "www.example.com"
user: @user = "sharelatex"
pass: @pass = "password"
describe "getDoc", ->
beforeEach ->
@webResponse = {
lines: @lines,
version: @version,
ranges: @ranges
pathname: @pathname,
projectHistoryId: @projectHistoryId
}),
"logger-sharelatex": (this.logger = {log: sinon.stub(), err: sinon.stub()})
}
});
this.project_id = "project-id-123";
this.projectHistoryId = "history-id-123";
this.doc_id = "doc-id-123";
this.lines = ["one", "two", "three"];
this.version = 42;
this.callback = sinon.stub();
this.ranges = { comments: "mock", entries: "mock" };
this.pathname = '/a/b/c.tex';
this.lastUpdatedAt = Date.now();
this.lastUpdatedBy = 'last-author-id';
return this.Settings.apis = {
web: {
url: (this.url = "www.example.com"),
user: (this.user = "sharelatex"),
pass: (this.pass = "password")
}
};
});
describe "with a successful response from the web api", ->
beforeEach ->
@request.callsArgWith(1, null, {statusCode: 200}, JSON.stringify(@webResponse))
@PersistenceManager.getDoc(@project_id, @doc_id, @callback)
describe("getDoc", function() {
beforeEach(function() {
return this.webResponse = {
lines: this.lines,
version: this.version,
ranges: this.ranges,
pathname: this.pathname,
projectHistoryId: this.projectHistoryId
};});
it "should call the web api", ->
@request
describe("with a successful response from the web api", function() {
beforeEach(function() {
this.request.callsArgWith(1, null, {statusCode: 200}, JSON.stringify(this.webResponse));
return this.PersistenceManager.getDoc(this.project_id, this.doc_id, this.callback);
});
it("should call the web api", function() {
return this.request
.calledWith({
url: "#{@url}/project/#{@project_id}/doc/#{@doc_id}"
method: "GET"
headers:
url: `${this.url}/project/${this.project_id}/doc/${this.doc_id}`,
method: "GET",
headers: {
"accept": "application/json"
auth:
user: @user
pass: @pass
},
auth: {
user: this.user,
pass: this.pass,
sendImmediately: true
jar: false
},
jar: false,
timeout: 5000
})
.should.equal true
.should.equal(true);
});
it "should call the callback with the doc lines, version and ranges", ->
@callback
.calledWith(null, @lines, @version, @ranges, @pathname, @projectHistoryId)
.should.equal true
it("should call the callback with the doc lines, version and ranges", function() {
return this.callback
.calledWith(null, this.lines, this.version, this.ranges, this.pathname, this.projectHistoryId)
.should.equal(true);
});
it "should time the execution", ->
@Metrics.Timer::done.called.should.equal true
it("should time the execution", function() {
return this.Metrics.Timer.prototype.done.called.should.equal(true);
});
it "should increment the metric", ->
@Metrics.inc.calledWith("getDoc", 1, {status: 200}).should.equal true
return it("should increment the metric", function() {
return this.Metrics.inc.calledWith("getDoc", 1, {status: 200}).should.equal(true);
});
});
describe "when request returns an error", ->
beforeEach ->
@error = new Error("oops")
@error.code = "EOOPS"
@request.callsArgWith(1, @error, null, null)
@PersistenceManager.getDoc(@project_id, @doc_id, @callback)
describe("when request returns an error", function() {
beforeEach(function() {
this.error = new Error("oops");
this.error.code = "EOOPS";
this.request.callsArgWith(1, this.error, null, null);
return this.PersistenceManager.getDoc(this.project_id, this.doc_id, this.callback);
});
it "should return the error", ->
@callback.calledWith(@error).should.equal true
it("should return the error", function() {
return this.callback.calledWith(this.error).should.equal(true);
});
it "should time the execution", ->
@Metrics.Timer::done.called.should.equal true
it("should time the execution", function() {
return this.Metrics.Timer.prototype.done.called.should.equal(true);
});
it "should increment the metric", ->
@Metrics.inc.calledWith("getDoc", 1, {status: "EOOPS"}).should.equal true
return it("should increment the metric", function() {
return this.Metrics.inc.calledWith("getDoc", 1, {status: "EOOPS"}).should.equal(true);
});
});
describe "when the request returns 404", ->
beforeEach ->
@request.callsArgWith(1, null, {statusCode: 404}, "")
@PersistenceManager.getDoc(@project_id, @doc_id, @callback)
describe("when the request returns 404", function() {
beforeEach(function() {
this.request.callsArgWith(1, null, {statusCode: 404}, "");
return this.PersistenceManager.getDoc(this.project_id, this.doc_id, this.callback);
});
it "should return a NotFoundError", ->
@callback.calledWith(new Errors.NotFoundError("not found")).should.equal true
it("should return a NotFoundError", function() {
return this.callback.calledWith(new Errors.NotFoundError("not found")).should.equal(true);
});
it "should time the execution", ->
@Metrics.Timer::done.called.should.equal true
it("should time the execution", function() {
return this.Metrics.Timer.prototype.done.called.should.equal(true);
});
it "should increment the metric", ->
@Metrics.inc.calledWith("getDoc", 1, {status: 404}).should.equal true
return it("should increment the metric", function() {
return this.Metrics.inc.calledWith("getDoc", 1, {status: 404}).should.equal(true);
});
});
describe "when the request returns an error status code", ->
beforeEach ->
@request.callsArgWith(1, null, {statusCode: 500}, "")
@PersistenceManager.getDoc(@project_id, @doc_id, @callback)
describe("when the request returns an error status code", function() {
beforeEach(function() {
this.request.callsArgWith(1, null, {statusCode: 500}, "");
return this.PersistenceManager.getDoc(this.project_id, this.doc_id, this.callback);
});
it "should return an error", ->
@callback.calledWith(new Error("web api error")).should.equal true
it("should return an error", function() {
return this.callback.calledWith(new Error("web api error")).should.equal(true);
});
it "should time the execution", ->
@Metrics.Timer::done.called.should.equal true
it("should time the execution", function() {
return this.Metrics.Timer.prototype.done.called.should.equal(true);
});
it "should increment the metric", ->
@Metrics.inc.calledWith("getDoc", 1, {status: 500}).should.equal true
return it("should increment the metric", function() {
return this.Metrics.inc.calledWith("getDoc", 1, {status: 500}).should.equal(true);
});
});
describe "when request returns an doc without lines", ->
beforeEach ->
delete @webResponse.lines
@request.callsArgWith(1, null, {statusCode: 200}, JSON.stringify(@webResponse))
@PersistenceManager.getDoc(@project_id, @doc_id, @callback)
describe("when request returns an doc without lines", function() {
beforeEach(function() {
delete this.webResponse.lines;
this.request.callsArgWith(1, null, {statusCode: 200}, JSON.stringify(this.webResponse));
return this.PersistenceManager.getDoc(this.project_id, this.doc_id, this.callback);
});
it "should return and error", ->
@callback.calledWith(new Error("web API response had no doc lines")).should.equal true
return it("should return and error", function() {
return this.callback.calledWith(new Error("web API response had no doc lines")).should.equal(true);
});
});
describe "when request returns an doc without a version", ->
beforeEach ->
delete @webResponse.version
@request.callsArgWith(1, null, {statusCode: 200}, JSON.stringify(@webResponse))
@PersistenceManager.getDoc(@project_id, @doc_id, @callback)
describe("when request returns an doc without a version", function() {
beforeEach(function() {
delete this.webResponse.version;
this.request.callsArgWith(1, null, {statusCode: 200}, JSON.stringify(this.webResponse));
return this.PersistenceManager.getDoc(this.project_id, this.doc_id, this.callback);
});
it "should return and error", ->
@callback.calledWith(new Error("web API response had no valid doc version")).should.equal true
return it("should return and error", function() {
return this.callback.calledWith(new Error("web API response had no valid doc version")).should.equal(true);
});
});
describe "when request returns an doc without a pathname", ->
beforeEach ->
delete @webResponse.pathname
@request.callsArgWith(1, null, {statusCode: 200}, JSON.stringify(@webResponse))
@PersistenceManager.getDoc(@project_id, @doc_id, @callback)
return describe("when request returns an doc without a pathname", function() {
beforeEach(function() {
delete this.webResponse.pathname;
this.request.callsArgWith(1, null, {statusCode: 200}, JSON.stringify(this.webResponse));
return this.PersistenceManager.getDoc(this.project_id, this.doc_id, this.callback);
});
it "should return and error", ->
@callback.calledWith(new Error("web API response had no valid doc pathname")).should.equal true
return it("should return and error", function() {
return this.callback.calledWith(new Error("web API response had no valid doc pathname")).should.equal(true);
});
});
});
describe "setDoc", ->
describe "with a successful response from the web api", ->
beforeEach ->
@request.callsArgWith(1, null, {statusCode: 200})
@PersistenceManager.setDoc(@project_id, @doc_id, @lines, @version, @ranges, @lastUpdatedAt, @lastUpdatedBy, @callback)
return describe("setDoc", function() {
describe("with a successful response from the web api", function() {
beforeEach(function() {
this.request.callsArgWith(1, null, {statusCode: 200});
return this.PersistenceManager.setDoc(this.project_id, this.doc_id, this.lines, this.version, this.ranges, this.lastUpdatedAt, this.lastUpdatedBy, this.callback);
});
it "should call the web api", ->
@request
it("should call the web api", function() {
return this.request
.calledWith({
url: "#{@url}/project/#{@project_id}/doc/#{@doc_id}"
json:
lines: @lines
version: @version
ranges: @ranges
lastUpdatedAt: @lastUpdatedAt
lastUpdatedBy: @lastUpdatedBy
method: "POST"
auth:
user: @user
pass: @pass
url: `${this.url}/project/${this.project_id}/doc/${this.doc_id}`,
json: {
lines: this.lines,
version: this.version,
ranges: this.ranges,
lastUpdatedAt: this.lastUpdatedAt,
lastUpdatedBy: this.lastUpdatedBy
},
method: "POST",
auth: {
user: this.user,
pass: this.pass,
sendImmediately: true
jar: false
},
jar: false,
timeout: 5000
})
.should.equal true
.should.equal(true);
});
it "should call the callback without error", ->
@callback.calledWith(null).should.equal true
it("should call the callback without error", function() {
return this.callback.calledWith(null).should.equal(true);
});
it "should time the execution", ->
@Metrics.Timer::done.called.should.equal true
it("should time the execution", function() {
return this.Metrics.Timer.prototype.done.called.should.equal(true);
});
it "should increment the metric", ->
@Metrics.inc.calledWith("setDoc", 1, {status: 200}).should.equal true
return it("should increment the metric", function() {
return this.Metrics.inc.calledWith("setDoc", 1, {status: 200}).should.equal(true);
});
});
describe "when request returns an error", ->
beforeEach ->
@error = new Error("oops")
@error.code = "EOOPS"
@request.callsArgWith(1, @error, null, null)
@PersistenceManager.setDoc(@project_id, @doc_id, @lines, @version, @ranges, @lastUpdatedAt, @lastUpdatedBy, @callback)
describe("when request returns an error", function() {
beforeEach(function() {
this.error = new Error("oops");
this.error.code = "EOOPS";
this.request.callsArgWith(1, this.error, null, null);
return this.PersistenceManager.setDoc(this.project_id, this.doc_id, this.lines, this.version, this.ranges, this.lastUpdatedAt, this.lastUpdatedBy, this.callback);
});
it "should return the error", ->
@callback.calledWith(@error).should.equal true
it("should return the error", function() {
return this.callback.calledWith(this.error).should.equal(true);
});
it "should time the execution", ->
@Metrics.Timer::done.called.should.equal true
it("should time the execution", function() {
return this.Metrics.Timer.prototype.done.called.should.equal(true);
});
it "should increment the metric", ->
@Metrics.inc.calledWith("setDoc", 1, {status: "EOOPS"}).should.equal true
return it("should increment the metric", function() {
return this.Metrics.inc.calledWith("setDoc", 1, {status: "EOOPS"}).should.equal(true);
});
});
describe "when the request returns 404", ->
beforeEach ->
@request.callsArgWith(1, null, {statusCode: 404}, "")
@PersistenceManager.setDoc(@project_id, @doc_id, @lines, @version, @ranges, @lastUpdatedAt, @lastUpdatedBy, @callback)
describe("when the request returns 404", function() {
beforeEach(function() {
this.request.callsArgWith(1, null, {statusCode: 404}, "");
return this.PersistenceManager.setDoc(this.project_id, this.doc_id, this.lines, this.version, this.ranges, this.lastUpdatedAt, this.lastUpdatedBy, this.callback);
});
it "should return a NotFoundError", ->
@callback.calledWith(new Errors.NotFoundError("not found")).should.equal true
it("should return a NotFoundError", function() {
return this.callback.calledWith(new Errors.NotFoundError("not found")).should.equal(true);
});
it "should time the execution", ->
@Metrics.Timer::done.called.should.equal true
it("should time the execution", function() {
return this.Metrics.Timer.prototype.done.called.should.equal(true);
});
it "should increment the metric", ->
@Metrics.inc.calledWith("setDoc", 1, {status: 404}).should.equal true
return it("should increment the metric", function() {
return this.Metrics.inc.calledWith("setDoc", 1, {status: 404}).should.equal(true);
});
});
describe "when the request returns an error status code", ->
beforeEach ->
@request.callsArgWith(1, null, {statusCode: 500}, "")
@PersistenceManager.setDoc(@project_id, @doc_id, @lines, @version, @ranges, @lastUpdatedAt, @lastUpdatedBy, @callback)
return describe("when the request returns an error status code", function() {
beforeEach(function() {
this.request.callsArgWith(1, null, {statusCode: 500}, "");
return this.PersistenceManager.setDoc(this.project_id, this.doc_id, this.lines, this.version, this.ranges, this.lastUpdatedAt, this.lastUpdatedBy, this.callback);
});
it "should return an error", ->
@callback.calledWith(new Error("web api error")).should.equal true
it("should return an error", function() {
return this.callback.calledWith(new Error("web api error")).should.equal(true);
});
it "should time the execution", ->
@Metrics.Timer::done.called.should.equal true
it("should time the execution", function() {
return this.Metrics.Timer.prototype.done.called.should.equal(true);
});
it "should increment the metric", ->
@Metrics.inc.calledWith("setDoc", 1, {status: 500}).should.equal true
return it("should increment the metric", function() {
return this.Metrics.inc.calledWith("setDoc", 1, {status: 500}).should.equal(true);
});
});
});
});

View file

@ -1,122 +1,152 @@
sinon = require('sinon')
chai = require('chai')
should = chai.should()
modulePath = "../../../../app/js/ProjectHistoryRedisManager.js"
SandboxedModule = require('sandboxed-module')
tk = require "timekeeper"
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const sinon = require('sinon');
const chai = require('chai');
const should = chai.should();
const modulePath = "../../../../app/js/ProjectHistoryRedisManager.js";
const SandboxedModule = require('sandboxed-module');
const tk = require("timekeeper");
describe "ProjectHistoryRedisManager", ->
beforeEach ->
@project_id = "project-id-123"
@projectHistoryId = "history-id-123"
@user_id = "user-id-123"
@callback = sinon.stub()
@rclient = {}
tk.freeze(new Date())
@ProjectHistoryRedisManager = SandboxedModule.require modulePath,
requires:
"settings-sharelatex": @settings = {
redis:
project_history:
key_schema:
projectHistoryOps: ({project_id}) -> "ProjectHistory:Ops:#{project_id}"
projectHistoryFirstOpTimestamp: ({project_id}) -> "ProjectHistory:FirstOpTimestamp:#{project_id}"
}
"redis-sharelatex":
createClient: () => @rclient
"logger-sharelatex":
log:->
"./Metrics": @metrics = { summary: sinon.stub()}
globals:
JSON: @JSON = JSON
describe("ProjectHistoryRedisManager", function() {
beforeEach(function() {
this.project_id = "project-id-123";
this.projectHistoryId = "history-id-123";
this.user_id = "user-id-123";
this.callback = sinon.stub();
this.rclient = {};
tk.freeze(new Date());
return this.ProjectHistoryRedisManager = SandboxedModule.require(modulePath, {
requires: {
"settings-sharelatex": (this.settings = {
redis: {
project_history: {
key_schema: {
projectHistoryOps({project_id}) { return `ProjectHistory:Ops:${project_id}`; },
projectHistoryFirstOpTimestamp({project_id}) { return `ProjectHistory:FirstOpTimestamp:${project_id}`; }
}
}
}
}),
"redis-sharelatex": {
createClient: () => this.rclient
},
"logger-sharelatex": {
log() {}
},
"./Metrics": (this.metrics = { summary: sinon.stub()})
},
globals: {
JSON: (this.JSON = JSON)
}
}
);
});
afterEach ->
tk.reset()
afterEach(() => tk.reset());
describe "queueOps", ->
beforeEach ->
@ops = ["mock-op-1", "mock-op-2"]
@multi = exec: sinon.stub()
@multi.rpush = sinon.stub()
@multi.setnx = sinon.stub()
@rclient.multi = () => @multi
# @rclient = multi: () => @multi
@ProjectHistoryRedisManager.queueOps @project_id, @ops..., @callback
describe("queueOps", function() {
beforeEach(function() {
this.ops = ["mock-op-1", "mock-op-2"];
this.multi = {exec: sinon.stub()};
this.multi.rpush = sinon.stub();
this.multi.setnx = sinon.stub();
this.rclient.multi = () => this.multi;
// @rclient = multi: () => @multi
return this.ProjectHistoryRedisManager.queueOps(this.project_id, ...Array.from(this.ops), this.callback);
});
it "should queue an update", ->
@multi.rpush
it("should queue an update", function() {
return this.multi.rpush
.calledWithExactly(
"ProjectHistory:Ops:#{@project_id}"
@ops[0]
@ops[1]
).should.equal true
`ProjectHistory:Ops:${this.project_id}`,
this.ops[0],
this.ops[1]
).should.equal(true);
});
it "should set the queue timestamp if not present", ->
@multi.setnx
return it("should set the queue timestamp if not present", function() {
return this.multi.setnx
.calledWithExactly(
"ProjectHistory:FirstOpTimestamp:#{@project_id}"
`ProjectHistory:FirstOpTimestamp:${this.project_id}`,
Date.now()
).should.equal true
).should.equal(true);
});
});
describe "queueRenameEntity", ->
beforeEach () ->
@file_id = 1234
describe("queueRenameEntity", function() {
beforeEach(function() {
this.file_id = 1234;
@rawUpdate =
pathname: @pathname = '/old'
newPathname: @newPathname = '/new'
version: @version = 2
this.rawUpdate = {
pathname: (this.pathname = '/old'),
newPathname: (this.newPathname = '/new'),
version: (this.version = 2)
};
@ProjectHistoryRedisManager.queueOps = sinon.stub()
@ProjectHistoryRedisManager.queueRenameEntity @project_id, @projectHistoryId, 'file', @file_id, @user_id, @rawUpdate, @callback
this.ProjectHistoryRedisManager.queueOps = sinon.stub();
return this.ProjectHistoryRedisManager.queueRenameEntity(this.project_id, this.projectHistoryId, 'file', this.file_id, this.user_id, this.rawUpdate, this.callback);
});
it "should queue an update", ->
update =
pathname: @pathname
new_pathname: @newPathname
meta:
user_id: @user_id
return it("should queue an update", function() {
const update = {
pathname: this.pathname,
new_pathname: this.newPathname,
meta: {
user_id: this.user_id,
ts: new Date()
version: @version
projectHistoryId: @projectHistoryId
file: @file_id
},
version: this.version,
projectHistoryId: this.projectHistoryId,
file: this.file_id
};
@ProjectHistoryRedisManager.queueOps
.calledWithExactly(@project_id, @JSON.stringify(update), @callback)
.should.equal true
return this.ProjectHistoryRedisManager.queueOps
.calledWithExactly(this.project_id, this.JSON.stringify(update), this.callback)
.should.equal(true);
});
});
describe "queueAddEntity", ->
beforeEach () ->
@rclient.rpush = sinon.stub().yields()
@doc_id = 1234
return describe("queueAddEntity", function() {
beforeEach(function() {
this.rclient.rpush = sinon.stub().yields();
this.doc_id = 1234;
@rawUpdate =
pathname: @pathname = '/old'
docLines: @docLines = 'a\nb'
version: @version = 2
url: @url = 'filestore.example.com'
this.rawUpdate = {
pathname: (this.pathname = '/old'),
docLines: (this.docLines = 'a\nb'),
version: (this.version = 2),
url: (this.url = 'filestore.example.com')
};
@ProjectHistoryRedisManager.queueOps = sinon.stub()
@ProjectHistoryRedisManager.queueAddEntity @project_id, @projectHistoryId, 'doc', @doc_id, @user_id, @rawUpdate, @callback
this.ProjectHistoryRedisManager.queueOps = sinon.stub();
return this.ProjectHistoryRedisManager.queueAddEntity(this.project_id, this.projectHistoryId, 'doc', this.doc_id, this.user_id, this.rawUpdate, this.callback);
});
it "should queue an update", ->
update =
pathname: @pathname
docLines: @docLines
url: @url
meta:
user_id: @user_id
it("should queue an update", function() {
const update = {
pathname: this.pathname,
docLines: this.docLines,
url: this.url,
meta: {
user_id: this.user_id,
ts: new Date()
version: @version
projectHistoryId: @projectHistoryId
doc: @doc_id
},
version: this.version,
projectHistoryId: this.projectHistoryId,
doc: this.doc_id
};
@ProjectHistoryRedisManager.queueOps
.calledWithExactly(@project_id, @JSON.stringify(update), @callback)
.should.equal true
return this.ProjectHistoryRedisManager.queueOps
.calledWithExactly(this.project_id, this.JSON.stringify(update), this.callback)
.should.equal(true);
});
describe "queueResyncProjectStructure", ->
it "should queue an update", ->
describe("queueResyncProjectStructure", () => it("should queue an update", function() {}));
describe "queueResyncDocContent", ->
it "should queue an update", ->
return describe("queueResyncDocContent", () => it("should queue an update", function() {}));
});
});

View file

@ -1,86 +1,125 @@
sinon = require('sinon')
chai = require('chai')
should = chai.should()
modulePath = "../../../../app/js/ProjectManager.js"
SandboxedModule = require('sandboxed-module')
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS206: Consider reworking classes to avoid initClass
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const sinon = require('sinon');
const chai = require('chai');
const should = chai.should();
const modulePath = "../../../../app/js/ProjectManager.js";
const SandboxedModule = require('sandboxed-module');
describe "ProjectManager - flushAndDeleteProject", ->
beforeEach ->
@ProjectManager = SandboxedModule.require modulePath, requires:
"./RedisManager": @RedisManager = {}
"./ProjectHistoryRedisManager": @ProjectHistoryRedisManager = {}
"./DocumentManager": @DocumentManager = {}
"logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub() }
"./HistoryManager": @HistoryManager =
flushProjectChanges: sinon.stub().callsArg(2)
"./Metrics": @Metrics =
Timer: class Timer
done: sinon.stub()
@project_id = "project-id-123"
@callback = sinon.stub()
describe("ProjectManager - flushAndDeleteProject", function() {
beforeEach(function() {
let Timer;
this.ProjectManager = SandboxedModule.require(modulePath, { requires: {
"./RedisManager": (this.RedisManager = {}),
"./ProjectHistoryRedisManager": (this.ProjectHistoryRedisManager = {}),
"./DocumentManager": (this.DocumentManager = {}),
"logger-sharelatex": (this.logger = { log: sinon.stub(), error: sinon.stub() }),
"./HistoryManager": (this.HistoryManager =
{flushProjectChanges: sinon.stub().callsArg(2)}),
"./Metrics": (this.Metrics = {
Timer: (Timer = (function() {
Timer = class Timer {
static initClass() {
this.prototype.done = sinon.stub();
}
};
Timer.initClass();
return Timer;
})())
})
}
}
);
this.project_id = "project-id-123";
return this.callback = sinon.stub();
});
describe "successfully", ->
beforeEach (done) ->
@doc_ids = ["doc-id-1", "doc-id-2", "doc-id-3"]
@RedisManager.getDocIdsInProject = sinon.stub().callsArgWith(1, null, @doc_ids)
@DocumentManager.flushAndDeleteDocWithLock = sinon.stub().callsArg(3)
@ProjectManager.flushAndDeleteProjectWithLocks @project_id, {}, (error) =>
@callback(error)
done()
describe("successfully", function() {
beforeEach(function(done) {
this.doc_ids = ["doc-id-1", "doc-id-2", "doc-id-3"];
this.RedisManager.getDocIdsInProject = sinon.stub().callsArgWith(1, null, this.doc_ids);
this.DocumentManager.flushAndDeleteDocWithLock = sinon.stub().callsArg(3);
return this.ProjectManager.flushAndDeleteProjectWithLocks(this.project_id, {}, error => {
this.callback(error);
return done();
});
});
it "should get the doc ids in the project", ->
@RedisManager.getDocIdsInProject
.calledWith(@project_id)
.should.equal true
it("should get the doc ids in the project", function() {
return this.RedisManager.getDocIdsInProject
.calledWith(this.project_id)
.should.equal(true);
});
it "should delete each doc in the project", ->
for doc_id in @doc_ids
@DocumentManager.flushAndDeleteDocWithLock
.calledWith(@project_id, doc_id, {})
.should.equal true
it("should delete each doc in the project", function() {
return Array.from(this.doc_ids).map((doc_id) =>
this.DocumentManager.flushAndDeleteDocWithLock
.calledWith(this.project_id, doc_id, {})
.should.equal(true));
});
it "should flush project history", ->
@HistoryManager.flushProjectChanges
.calledWith(@project_id, {})
.should.equal true
it("should flush project history", function() {
return this.HistoryManager.flushProjectChanges
.calledWith(this.project_id, {})
.should.equal(true);
});
it "should call the callback without error", ->
@callback.calledWith(null).should.equal true
it("should call the callback without error", function() {
return this.callback.calledWith(null).should.equal(true);
});
it "should time the execution", ->
@Metrics.Timer::done.called.should.equal true
return it("should time the execution", function() {
return this.Metrics.Timer.prototype.done.called.should.equal(true);
});
});
describe "when a doc errors", ->
beforeEach (done) ->
@doc_ids = ["doc-id-1", "doc-id-2", "doc-id-3"]
@RedisManager.getDocIdsInProject = sinon.stub().callsArgWith(1, null, @doc_ids)
@DocumentManager.flushAndDeleteDocWithLock = sinon.spy (project_id, doc_id, options, callback) =>
if doc_id == "doc-id-1"
callback(@error = new Error("oops, something went wrong"))
else
callback()
@ProjectManager.flushAndDeleteProjectWithLocks @project_id, {}, (error) =>
@callback(error)
done()
return describe("when a doc errors", function() {
beforeEach(function(done) {
this.doc_ids = ["doc-id-1", "doc-id-2", "doc-id-3"];
this.RedisManager.getDocIdsInProject = sinon.stub().callsArgWith(1, null, this.doc_ids);
this.DocumentManager.flushAndDeleteDocWithLock = sinon.spy((project_id, doc_id, options, callback) => {
if (doc_id === "doc-id-1") {
return callback(this.error = new Error("oops, something went wrong"));
} else {
return callback();
}
});
return this.ProjectManager.flushAndDeleteProjectWithLocks(this.project_id, {}, error => {
this.callback(error);
return done();
});
});
it "should still flush each doc in the project", ->
for doc_id in @doc_ids
@DocumentManager.flushAndDeleteDocWithLock
.calledWith(@project_id, doc_id, {})
.should.equal true
it("should still flush each doc in the project", function() {
return Array.from(this.doc_ids).map((doc_id) =>
this.DocumentManager.flushAndDeleteDocWithLock
.calledWith(this.project_id, doc_id, {})
.should.equal(true));
});
it "should still flush project history", ->
@HistoryManager.flushProjectChanges
.calledWith(@project_id, {})
.should.equal true
it("should still flush project history", function() {
return this.HistoryManager.flushProjectChanges
.calledWith(this.project_id, {})
.should.equal(true);
});
it "should record the error", ->
@logger.error
.calledWith(err: @error, project_id: @project_id, doc_id: "doc-id-1", "error deleting doc")
.should.equal true
it("should record the error", function() {
return this.logger.error
.calledWith({err: this.error, project_id: this.project_id, doc_id: "doc-id-1"}, "error deleting doc")
.should.equal(true);
});
it "should call the callback with an error", ->
@callback.calledWith(new Error()).should.equal true
it("should call the callback with an error", function() {
return this.callback.calledWith(new Error()).should.equal(true);
});
it "should time the execution", ->
@Metrics.Timer::done.called.should.equal true
return it("should time the execution", function() {
return this.Metrics.Timer.prototype.done.called.should.equal(true);
});
});
});

View file

@ -1,75 +1,114 @@
sinon = require('sinon')
chai = require('chai')
should = chai.should()
modulePath = "../../../../app/js/ProjectManager.js"
SandboxedModule = require('sandboxed-module')
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS206: Consider reworking classes to avoid initClass
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const sinon = require('sinon');
const chai = require('chai');
const should = chai.should();
const modulePath = "../../../../app/js/ProjectManager.js";
const SandboxedModule = require('sandboxed-module');
describe "ProjectManager - flushProject", ->
beforeEach ->
@ProjectManager = SandboxedModule.require modulePath, requires:
"./RedisManager": @RedisManager = {}
"./ProjectHistoryRedisManager": @ProjectHistoryRedisManager = {}
"./DocumentManager": @DocumentManager = {}
"logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub() }
"./HistoryManager": @HistoryManager = {}
"./Metrics": @Metrics =
Timer: class Timer
done: sinon.stub()
@project_id = "project-id-123"
@callback = sinon.stub()
describe("ProjectManager - flushProject", function() {
beforeEach(function() {
let Timer;
this.ProjectManager = SandboxedModule.require(modulePath, { requires: {
"./RedisManager": (this.RedisManager = {}),
"./ProjectHistoryRedisManager": (this.ProjectHistoryRedisManager = {}),
"./DocumentManager": (this.DocumentManager = {}),
"logger-sharelatex": (this.logger = { log: sinon.stub(), error: sinon.stub() }),
"./HistoryManager": (this.HistoryManager = {}),
"./Metrics": (this.Metrics = {
Timer: (Timer = (function() {
Timer = class Timer {
static initClass() {
this.prototype.done = sinon.stub();
}
};
Timer.initClass();
return Timer;
})())
})
}
}
);
this.project_id = "project-id-123";
return this.callback = sinon.stub();
});
describe "successfully", ->
beforeEach (done) ->
@doc_ids = ["doc-id-1", "doc-id-2", "doc-id-3"]
@RedisManager.getDocIdsInProject = sinon.stub().callsArgWith(1, null, @doc_ids)
@DocumentManager.flushDocIfLoadedWithLock = sinon.stub().callsArg(2)
@ProjectManager.flushProjectWithLocks @project_id, (error) =>
@callback(error)
done()
describe("successfully", function() {
beforeEach(function(done) {
this.doc_ids = ["doc-id-1", "doc-id-2", "doc-id-3"];
this.RedisManager.getDocIdsInProject = sinon.stub().callsArgWith(1, null, this.doc_ids);
this.DocumentManager.flushDocIfLoadedWithLock = sinon.stub().callsArg(2);
return this.ProjectManager.flushProjectWithLocks(this.project_id, error => {
this.callback(error);
return done();
});
});
it "should get the doc ids in the project", ->
@RedisManager.getDocIdsInProject
.calledWith(@project_id)
.should.equal true
it("should get the doc ids in the project", function() {
return this.RedisManager.getDocIdsInProject
.calledWith(this.project_id)
.should.equal(true);
});
it "should flush each doc in the project", ->
for doc_id in @doc_ids
@DocumentManager.flushDocIfLoadedWithLock
.calledWith(@project_id, doc_id)
.should.equal true
it("should flush each doc in the project", function() {
return Array.from(this.doc_ids).map((doc_id) =>
this.DocumentManager.flushDocIfLoadedWithLock
.calledWith(this.project_id, doc_id)
.should.equal(true));
});
it "should call the callback without error", ->
@callback.calledWith(null).should.equal true
it("should call the callback without error", function() {
return this.callback.calledWith(null).should.equal(true);
});
it "should time the execution", ->
@Metrics.Timer::done.called.should.equal true
return it("should time the execution", function() {
return this.Metrics.Timer.prototype.done.called.should.equal(true);
});
});
describe "when a doc errors", ->
beforeEach (done) ->
@doc_ids = ["doc-id-1", "doc-id-2", "doc-id-3"]
@RedisManager.getDocIdsInProject = sinon.stub().callsArgWith(1, null, @doc_ids)
@DocumentManager.flushDocIfLoadedWithLock = sinon.spy (project_id, doc_id, callback = (error) ->) =>
if doc_id == "doc-id-1"
callback(@error = new Error("oops, something went wrong"))
else
callback()
@ProjectManager.flushProjectWithLocks @project_id, (error) =>
@callback(error)
done()
return describe("when a doc errors", function() {
beforeEach(function(done) {
this.doc_ids = ["doc-id-1", "doc-id-2", "doc-id-3"];
this.RedisManager.getDocIdsInProject = sinon.stub().callsArgWith(1, null, this.doc_ids);
this.DocumentManager.flushDocIfLoadedWithLock = sinon.spy((project_id, doc_id, callback) => {
if (callback == null) { callback = function(error) {}; }
if (doc_id === "doc-id-1") {
return callback(this.error = new Error("oops, something went wrong"));
} else {
return callback();
}
});
return this.ProjectManager.flushProjectWithLocks(this.project_id, error => {
this.callback(error);
return done();
});
});
it "should still flush each doc in the project", ->
for doc_id in @doc_ids
@DocumentManager.flushDocIfLoadedWithLock
.calledWith(@project_id, doc_id)
.should.equal true
it("should still flush each doc in the project", function() {
return Array.from(this.doc_ids).map((doc_id) =>
this.DocumentManager.flushDocIfLoadedWithLock
.calledWith(this.project_id, doc_id)
.should.equal(true));
});
it "should record the error", ->
@logger.error
.calledWith(err: @error, project_id: @project_id, doc_id: "doc-id-1", "error flushing doc")
.should.equal true
it("should record the error", function() {
return this.logger.error
.calledWith({err: this.error, project_id: this.project_id, doc_id: "doc-id-1"}, "error flushing doc")
.should.equal(true);
});
it "should call the callback with an error", ->
@callback.calledWith(new Error()).should.equal true
it("should call the callback with an error", function() {
return this.callback.calledWith(new Error()).should.equal(true);
});
it "should time the execution", ->
@Metrics.Timer::done.called.should.equal true
return it("should time the execution", function() {
return this.Metrics.Timer.prototype.done.called.should.equal(true);
});
});
});

View file

@ -1,118 +1,161 @@
sinon = require('sinon')
chai = require('chai')
should = chai.should()
modulePath = "../../../../app/js/ProjectManager.js"
SandboxedModule = require('sandboxed-module')
Errors = require "../../../../app/js/Errors.js"
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS206: Consider reworking classes to avoid initClass
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const sinon = require('sinon');
const chai = require('chai');
const should = chai.should();
const modulePath = "../../../../app/js/ProjectManager.js";
const SandboxedModule = require('sandboxed-module');
const Errors = require("../../../../app/js/Errors.js");
describe "ProjectManager - getProjectDocsAndFlushIfOld", ->
beforeEach ->
@ProjectManager = SandboxedModule.require modulePath, requires:
"./RedisManager": @RedisManager = {}
"./ProjectHistoryRedisManager": @ProjectHistoryRedisManager = {}
"./DocumentManager": @DocumentManager = {}
"logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub() }
"./HistoryManager": @HistoryManager = {}
"./Metrics": @Metrics =
Timer: class Timer
done: sinon.stub()
@project_id = "project-id-123"
@callback = sinon.stub()
@doc_versions = [111, 222, 333]
describe("ProjectManager - getProjectDocsAndFlushIfOld", function() {
beforeEach(function() {
let Timer;
this.ProjectManager = SandboxedModule.require(modulePath, { requires: {
"./RedisManager": (this.RedisManager = {}),
"./ProjectHistoryRedisManager": (this.ProjectHistoryRedisManager = {}),
"./DocumentManager": (this.DocumentManager = {}),
"logger-sharelatex": (this.logger = { log: sinon.stub(), error: sinon.stub() }),
"./HistoryManager": (this.HistoryManager = {}),
"./Metrics": (this.Metrics = {
Timer: (Timer = (function() {
Timer = class Timer {
static initClass() {
this.prototype.done = sinon.stub();
}
};
Timer.initClass();
return Timer;
})())
})
}
}
);
this.project_id = "project-id-123";
this.callback = sinon.stub();
return this.doc_versions = [111, 222, 333];});
describe "successfully", ->
beforeEach (done) ->
@doc_ids = ["doc-id-1", "doc-id-2", "doc-id-3"]
@doc_lines = [["aaa","aaa"],["bbb","bbb"],["ccc","ccc"]]
@docs = [
{_id: @doc_ids[0], lines: @doc_lines[0], v: @doc_versions[0]}
{_id: @doc_ids[1], lines: @doc_lines[1], v: @doc_versions[1]}
{_id: @doc_ids[2], lines: @doc_lines[2], v: @doc_versions[2]}
]
@RedisManager.checkOrSetProjectState = sinon.stub().callsArgWith(2, null)
@RedisManager.getDocIdsInProject = sinon.stub().callsArgWith(1, null, @doc_ids)
@DocumentManager.getDocAndFlushIfOldWithLock = sinon.stub()
@DocumentManager.getDocAndFlushIfOldWithLock.withArgs(@project_id, @doc_ids[0])
.callsArgWith(2, null, @doc_lines[0], @doc_versions[0])
@DocumentManager.getDocAndFlushIfOldWithLock.withArgs(@project_id, @doc_ids[1])
.callsArgWith(2, null, @doc_lines[1], @doc_versions[1])
@DocumentManager.getDocAndFlushIfOldWithLock.withArgs(@project_id, @doc_ids[2])
.callsArgWith(2, null, @doc_lines[2], @doc_versions[2])
@ProjectManager.getProjectDocsAndFlushIfOld @project_id, @projectStateHash, @excludeVersions, (error, docs) =>
@callback(error, docs)
done()
describe("successfully", function() {
beforeEach(function(done) {
this.doc_ids = ["doc-id-1", "doc-id-2", "doc-id-3"];
this.doc_lines = [["aaa","aaa"],["bbb","bbb"],["ccc","ccc"]];
this.docs = [
{_id: this.doc_ids[0], lines: this.doc_lines[0], v: this.doc_versions[0]},
{_id: this.doc_ids[1], lines: this.doc_lines[1], v: this.doc_versions[1]},
{_id: this.doc_ids[2], lines: this.doc_lines[2], v: this.doc_versions[2]}
];
this.RedisManager.checkOrSetProjectState = sinon.stub().callsArgWith(2, null);
this.RedisManager.getDocIdsInProject = sinon.stub().callsArgWith(1, null, this.doc_ids);
this.DocumentManager.getDocAndFlushIfOldWithLock = sinon.stub();
this.DocumentManager.getDocAndFlushIfOldWithLock.withArgs(this.project_id, this.doc_ids[0])
.callsArgWith(2, null, this.doc_lines[0], this.doc_versions[0]);
this.DocumentManager.getDocAndFlushIfOldWithLock.withArgs(this.project_id, this.doc_ids[1])
.callsArgWith(2, null, this.doc_lines[1], this.doc_versions[1]);
this.DocumentManager.getDocAndFlushIfOldWithLock.withArgs(this.project_id, this.doc_ids[2])
.callsArgWith(2, null, this.doc_lines[2], this.doc_versions[2]);
return this.ProjectManager.getProjectDocsAndFlushIfOld(this.project_id, this.projectStateHash, this.excludeVersions, (error, docs) => {
this.callback(error, docs);
return done();
});
});
it "should check the project state", ->
@RedisManager.checkOrSetProjectState
.calledWith(@project_id, @projectStateHash)
.should.equal true
it("should check the project state", function() {
return this.RedisManager.checkOrSetProjectState
.calledWith(this.project_id, this.projectStateHash)
.should.equal(true);
});
it "should get the doc ids in the project", ->
@RedisManager.getDocIdsInProject
.calledWith(@project_id)
.should.equal true
it("should get the doc ids in the project", function() {
return this.RedisManager.getDocIdsInProject
.calledWith(this.project_id)
.should.equal(true);
});
it "should call the callback without error", ->
@callback.calledWith(null, @docs).should.equal true
it("should call the callback without error", function() {
return this.callback.calledWith(null, this.docs).should.equal(true);
});
it "should time the execution", ->
@Metrics.Timer::done.called.should.equal true
return it("should time the execution", function() {
return this.Metrics.Timer.prototype.done.called.should.equal(true);
});
});
describe "when the state does not match", ->
beforeEach (done) ->
@doc_ids = ["doc-id-1", "doc-id-2", "doc-id-3"]
@RedisManager.checkOrSetProjectState = sinon.stub().callsArgWith(2, null, true)
@ProjectManager.getProjectDocsAndFlushIfOld @project_id, @projectStateHash, @excludeVersions, (error, docs) =>
@callback(error, docs)
done()
describe("when the state does not match", function() {
beforeEach(function(done) {
this.doc_ids = ["doc-id-1", "doc-id-2", "doc-id-3"];
this.RedisManager.checkOrSetProjectState = sinon.stub().callsArgWith(2, null, true);
return this.ProjectManager.getProjectDocsAndFlushIfOld(this.project_id, this.projectStateHash, this.excludeVersions, (error, docs) => {
this.callback(error, docs);
return done();
});
});
it "should check the project state", ->
@RedisManager.checkOrSetProjectState
.calledWith(@project_id, @projectStateHash)
.should.equal true
it("should check the project state", function() {
return this.RedisManager.checkOrSetProjectState
.calledWith(this.project_id, this.projectStateHash)
.should.equal(true);
});
it "should call the callback with an error", ->
@callback.calledWith(new Errors.ProjectStateChangedError("project state changed")).should.equal true
it("should call the callback with an error", function() {
return this.callback.calledWith(new Errors.ProjectStateChangedError("project state changed")).should.equal(true);
});
it "should time the execution", ->
@Metrics.Timer::done.called.should.equal true
return it("should time the execution", function() {
return this.Metrics.Timer.prototype.done.called.should.equal(true);
});
});
describe "when a doc errors", ->
beforeEach (done) ->
@doc_ids = ["doc-id-1", "doc-id-2", "doc-id-3"]
@RedisManager.checkOrSetProjectState = sinon.stub().callsArgWith(2, null)
@RedisManager.getDocIdsInProject = sinon.stub().callsArgWith(1, null, @doc_ids)
@DocumentManager.getDocAndFlushIfOldWithLock = sinon.stub()
@DocumentManager.getDocAndFlushIfOldWithLock.withArgs(@project_id, "doc-id-1")
.callsArgWith(2, null, ["test doc content"], @doc_versions[1])
@DocumentManager.getDocAndFlushIfOldWithLock.withArgs(@project_id, "doc-id-2")
.callsArgWith(2, @error = new Error("oops")) # trigger an error
@ProjectManager.getProjectDocsAndFlushIfOld @project_id, @projectStateHash, @excludeVersions, (error, docs) =>
@callback(error)
done()
describe("when a doc errors", function() {
beforeEach(function(done) {
this.doc_ids = ["doc-id-1", "doc-id-2", "doc-id-3"];
this.RedisManager.checkOrSetProjectState = sinon.stub().callsArgWith(2, null);
this.RedisManager.getDocIdsInProject = sinon.stub().callsArgWith(1, null, this.doc_ids);
this.DocumentManager.getDocAndFlushIfOldWithLock = sinon.stub();
this.DocumentManager.getDocAndFlushIfOldWithLock.withArgs(this.project_id, "doc-id-1")
.callsArgWith(2, null, ["test doc content"], this.doc_versions[1]);
this.DocumentManager.getDocAndFlushIfOldWithLock.withArgs(this.project_id, "doc-id-2")
.callsArgWith(2, (this.error = new Error("oops"))); // trigger an error
return this.ProjectManager.getProjectDocsAndFlushIfOld(this.project_id, this.projectStateHash, this.excludeVersions, (error, docs) => {
this.callback(error);
return done();
});
});
it "should record the error", ->
@logger.error
.calledWith(err: @error, project_id: @project_id, doc_id: "doc-id-2", "error getting project doc lines in getProjectDocsAndFlushIfOld")
.should.equal true
it("should record the error", function() {
return this.logger.error
.calledWith({err: this.error, project_id: this.project_id, doc_id: "doc-id-2"}, "error getting project doc lines in getProjectDocsAndFlushIfOld")
.should.equal(true);
});
it "should call the callback with an error", ->
@callback.calledWith(new Error("oops")).should.equal true
it("should call the callback with an error", function() {
return this.callback.calledWith(new Error("oops")).should.equal(true);
});
it "should time the execution", ->
@Metrics.Timer::done.called.should.equal true
return it("should time the execution", function() {
return this.Metrics.Timer.prototype.done.called.should.equal(true);
});
});
describe "clearing the project state with clearProjectState", ->
beforeEach (done) ->
@RedisManager.clearProjectState = sinon.stub().callsArg(1)
@ProjectManager.clearProjectState @project_id, (error) =>
@callback(error)
done()
return describe("clearing the project state with clearProjectState", function() {
beforeEach(function(done) {
this.RedisManager.clearProjectState = sinon.stub().callsArg(1);
return this.ProjectManager.clearProjectState(this.project_id, error => {
this.callback(error);
return done();
});
});
it "should clear the project state", ->
@RedisManager.clearProjectState
.calledWith(@project_id)
.should.equal true
it("should clear the project state", function() {
return this.RedisManager.clearProjectState
.calledWith(this.project_id)
.should.equal(true);
});
it "should call the callback", ->
@callback.called.should.equal true
return it("should call the callback", function() {
return this.callback.called.should.equal(true);
});
});
});

View file

@ -1,180 +1,242 @@
sinon = require('sinon')
chai = require('chai')
should = chai.should()
modulePath = "../../../../app/js/ProjectManager.js"
SandboxedModule = require('sandboxed-module')
_ = require('lodash')
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS206: Consider reworking classes to avoid initClass
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const sinon = require('sinon');
const chai = require('chai');
const should = chai.should();
const modulePath = "../../../../app/js/ProjectManager.js";
const SandboxedModule = require('sandboxed-module');
const _ = require('lodash');
describe "ProjectManager", ->
beforeEach ->
@ProjectManager = SandboxedModule.require modulePath, requires:
"./RedisManager": @RedisManager = {}
"./ProjectHistoryRedisManager": @ProjectHistoryRedisManager = {}
"./DocumentManager": @DocumentManager = {}
"logger-sharelatex": @logger = { log: sinon.stub(), error: sinon.stub() }
"./HistoryManager": @HistoryManager = {}
"./Metrics": @Metrics =
Timer: class Timer
done: sinon.stub()
describe("ProjectManager", function() {
beforeEach(function() {
let Timer;
this.ProjectManager = SandboxedModule.require(modulePath, { requires: {
"./RedisManager": (this.RedisManager = {}),
"./ProjectHistoryRedisManager": (this.ProjectHistoryRedisManager = {}),
"./DocumentManager": (this.DocumentManager = {}),
"logger-sharelatex": (this.logger = { log: sinon.stub(), error: sinon.stub() }),
"./HistoryManager": (this.HistoryManager = {}),
"./Metrics": (this.Metrics = {
Timer: (Timer = (function() {
Timer = class Timer {
static initClass() {
this.prototype.done = sinon.stub();
}
};
Timer.initClass();
return Timer;
})())
})
}
}
);
@project_id = "project-id-123"
@projectHistoryId = 'history-id-123'
@user_id = "user-id-123"
@version = 1234567
@HistoryManager.shouldFlushHistoryOps = sinon.stub().returns(false)
@HistoryManager.flushProjectChangesAsync = sinon.stub()
@callback = sinon.stub()
this.project_id = "project-id-123";
this.projectHistoryId = 'history-id-123';
this.user_id = "user-id-123";
this.version = 1234567;
this.HistoryManager.shouldFlushHistoryOps = sinon.stub().returns(false);
this.HistoryManager.flushProjectChangesAsync = sinon.stub();
return this.callback = sinon.stub();
});
describe "updateProjectWithLocks", ->
describe "rename operations", ->
beforeEach ->
@firstDocUpdate =
id: 1
pathname: 'foo'
return describe("updateProjectWithLocks", function() {
describe("rename operations", function() {
beforeEach(function() {
this.firstDocUpdate = {
id: 1,
pathname: 'foo',
newPathname: 'foo'
@secondDocUpdate =
id: 2
pathname: 'bar'
};
this.secondDocUpdate = {
id: 2,
pathname: 'bar',
newPathname: 'bar2'
@docUpdates = [ @firstDocUpdate, @secondDocUpdate ]
@firstFileUpdate =
id: 2
pathname: 'bar'
};
this.docUpdates = [ this.firstDocUpdate, this.secondDocUpdate ];
this.firstFileUpdate = {
id: 2,
pathname: 'bar',
newPathname: 'bar2'
@fileUpdates = [ @firstFileUpdate ]
@DocumentManager.renameDocWithLock = sinon.stub().yields()
@ProjectHistoryRedisManager.queueRenameEntity = sinon.stub().yields()
};
this.fileUpdates = [ this.firstFileUpdate ];
this.DocumentManager.renameDocWithLock = sinon.stub().yields();
return this.ProjectHistoryRedisManager.queueRenameEntity = sinon.stub().yields();
});
describe "successfully", ->
beforeEach ->
@ProjectManager.updateProjectWithLocks @project_id, @projectHistoryId, @user_id, @docUpdates, @fileUpdates, @version, @callback
describe("successfully", function() {
beforeEach(function() {
return this.ProjectManager.updateProjectWithLocks(this.project_id, this.projectHistoryId, this.user_id, this.docUpdates, this.fileUpdates, this.version, this.callback);
});
it "should rename the docs in the updates", ->
firstDocUpdateWithVersion = _.extend({}, @firstDocUpdate, {version: "#{@version}.0"})
secondDocUpdateWithVersion = _.extend({}, @secondDocUpdate, {version: "#{@version}.1"})
@DocumentManager.renameDocWithLock
.calledWith(@project_id, @firstDocUpdate.id, @user_id, firstDocUpdateWithVersion, @projectHistoryId)
.should.equal true
@DocumentManager.renameDocWithLock
.calledWith(@project_id, @secondDocUpdate.id, @user_id, secondDocUpdateWithVersion, @projectHistoryId)
.should.equal true
it("should rename the docs in the updates", function() {
const firstDocUpdateWithVersion = _.extend({}, this.firstDocUpdate, {version: `${this.version}.0`});
const secondDocUpdateWithVersion = _.extend({}, this.secondDocUpdate, {version: `${this.version}.1`});
this.DocumentManager.renameDocWithLock
.calledWith(this.project_id, this.firstDocUpdate.id, this.user_id, firstDocUpdateWithVersion, this.projectHistoryId)
.should.equal(true);
return this.DocumentManager.renameDocWithLock
.calledWith(this.project_id, this.secondDocUpdate.id, this.user_id, secondDocUpdateWithVersion, this.projectHistoryId)
.should.equal(true);
});
it "should rename the files in the updates", ->
firstFileUpdateWithVersion = _.extend({}, @firstFileUpdate, {version: "#{@version}.2"})
@ProjectHistoryRedisManager.queueRenameEntity
.calledWith(@project_id, @projectHistoryId, 'file', @firstFileUpdate.id, @user_id, firstFileUpdateWithVersion)
.should.equal true
it("should rename the files in the updates", function() {
const firstFileUpdateWithVersion = _.extend({}, this.firstFileUpdate, {version: `${this.version}.2`});
return this.ProjectHistoryRedisManager.queueRenameEntity
.calledWith(this.project_id, this.projectHistoryId, 'file', this.firstFileUpdate.id, this.user_id, firstFileUpdateWithVersion)
.should.equal(true);
});
it "should not flush the history", ->
@HistoryManager.flushProjectChangesAsync
.calledWith(@project_id)
.should.equal false
it("should not flush the history", function() {
return this.HistoryManager.flushProjectChangesAsync
.calledWith(this.project_id)
.should.equal(false);
});
it "should call the callback", ->
@callback.called.should.equal true
return it("should call the callback", function() {
return this.callback.called.should.equal(true);
});
});
describe "when renaming a doc fails", ->
beforeEach ->
@error = new Error('error')
@DocumentManager.renameDocWithLock = sinon.stub().yields(@error)
@ProjectManager.updateProjectWithLocks @project_id, @projectHistoryId, @user_id, @docUpdates, @fileUpdates, @version, @callback
describe("when renaming a doc fails", function() {
beforeEach(function() {
this.error = new Error('error');
this.DocumentManager.renameDocWithLock = sinon.stub().yields(this.error);
return this.ProjectManager.updateProjectWithLocks(this.project_id, this.projectHistoryId, this.user_id, this.docUpdates, this.fileUpdates, this.version, this.callback);
});
it "should call the callback with the error", ->
@callback.calledWith(@error).should.equal true
return it("should call the callback with the error", function() {
return this.callback.calledWith(this.error).should.equal(true);
});
});
describe "when renaming a file fails", ->
beforeEach ->
@error = new Error('error')
@ProjectHistoryRedisManager.queueRenameEntity = sinon.stub().yields(@error)
@ProjectManager.updateProjectWithLocks @project_id, @projectHistoryId, @user_id, @docUpdates, @fileUpdates, @version, @callback
describe("when renaming a file fails", function() {
beforeEach(function() {
this.error = new Error('error');
this.ProjectHistoryRedisManager.queueRenameEntity = sinon.stub().yields(this.error);
return this.ProjectManager.updateProjectWithLocks(this.project_id, this.projectHistoryId, this.user_id, this.docUpdates, this.fileUpdates, this.version, this.callback);
});
it "should call the callback with the error", ->
@callback.calledWith(@error).should.equal true
return it("should call the callback with the error", function() {
return this.callback.calledWith(this.error).should.equal(true);
});
});
describe "with enough ops to flush", ->
beforeEach ->
@HistoryManager.shouldFlushHistoryOps = sinon.stub().returns(true)
@ProjectManager.updateProjectWithLocks @project_id, @projectHistoryId, @user_id, @docUpdates, @fileUpdates, @version, @callback
return describe("with enough ops to flush", function() {
beforeEach(function() {
this.HistoryManager.shouldFlushHistoryOps = sinon.stub().returns(true);
return this.ProjectManager.updateProjectWithLocks(this.project_id, this.projectHistoryId, this.user_id, this.docUpdates, this.fileUpdates, this.version, this.callback);
});
it "should flush the history", ->
@HistoryManager.flushProjectChangesAsync
.calledWith(@project_id)
.should.equal true
return it("should flush the history", function() {
return this.HistoryManager.flushProjectChangesAsync
.calledWith(this.project_id)
.should.equal(true);
});
});
});
describe "add operations", ->
beforeEach ->
@firstDocUpdate =
id: 1
return describe("add operations", function() {
beforeEach(function() {
this.firstDocUpdate = {
id: 1,
docLines: "a\nb"
@secondDocUpdate =
id: 2
};
this.secondDocUpdate = {
id: 2,
docLines: "a\nb"
@docUpdates = [ @firstDocUpdate, @secondDocUpdate ]
@firstFileUpdate =
id: 3
};
this.docUpdates = [ this.firstDocUpdate, this.secondDocUpdate ];
this.firstFileUpdate = {
id: 3,
url: 'filestore.example.com/2'
@secondFileUpdate =
id: 4
};
this.secondFileUpdate = {
id: 4,
url: 'filestore.example.com/3'
@fileUpdates = [ @firstFileUpdate, @secondFileUpdate ]
@ProjectHistoryRedisManager.queueAddEntity = sinon.stub().yields()
};
this.fileUpdates = [ this.firstFileUpdate, this.secondFileUpdate ];
return this.ProjectHistoryRedisManager.queueAddEntity = sinon.stub().yields();
});
describe "successfully", ->
beforeEach ->
@ProjectManager.updateProjectWithLocks @project_id, @projectHistoryId, @user_id, @docUpdates, @fileUpdates, @version, @callback
describe("successfully", function() {
beforeEach(function() {
return this.ProjectManager.updateProjectWithLocks(this.project_id, this.projectHistoryId, this.user_id, this.docUpdates, this.fileUpdates, this.version, this.callback);
});
it "should add the docs in the updates", ->
firstDocUpdateWithVersion = _.extend({}, @firstDocUpdate, {version: "#{@version}.0"})
secondDocUpdateWithVersion = _.extend({}, @secondDocUpdate, {version: "#{@version}.1"})
@ProjectHistoryRedisManager.queueAddEntity.getCall(0)
.calledWith(@project_id, @projectHistoryId, 'doc', @firstDocUpdate.id, @user_id, firstDocUpdateWithVersion)
.should.equal true
@ProjectHistoryRedisManager.queueAddEntity.getCall(1)
.calledWith(@project_id, @projectHistoryId, 'doc', @secondDocUpdate.id, @user_id, secondDocUpdateWithVersion)
.should.equal true
it("should add the docs in the updates", function() {
const firstDocUpdateWithVersion = _.extend({}, this.firstDocUpdate, {version: `${this.version}.0`});
const secondDocUpdateWithVersion = _.extend({}, this.secondDocUpdate, {version: `${this.version}.1`});
this.ProjectHistoryRedisManager.queueAddEntity.getCall(0)
.calledWith(this.project_id, this.projectHistoryId, 'doc', this.firstDocUpdate.id, this.user_id, firstDocUpdateWithVersion)
.should.equal(true);
return this.ProjectHistoryRedisManager.queueAddEntity.getCall(1)
.calledWith(this.project_id, this.projectHistoryId, 'doc', this.secondDocUpdate.id, this.user_id, secondDocUpdateWithVersion)
.should.equal(true);
});
it "should add the files in the updates", ->
firstFileUpdateWithVersion = _.extend({}, @firstFileUpdate, {version: "#{@version}.2"})
secondFileUpdateWithVersion = _.extend({}, @secondFileUpdate, {version: "#{@version}.3"})
@ProjectHistoryRedisManager.queueAddEntity.getCall(2)
.calledWith(@project_id, @projectHistoryId, 'file', @firstFileUpdate.id, @user_id, firstFileUpdateWithVersion)
.should.equal true
@ProjectHistoryRedisManager.queueAddEntity.getCall(3)
.calledWith(@project_id, @projectHistoryId, 'file', @secondFileUpdate.id, @user_id, secondFileUpdateWithVersion)
.should.equal true
it("should add the files in the updates", function() {
const firstFileUpdateWithVersion = _.extend({}, this.firstFileUpdate, {version: `${this.version}.2`});
const secondFileUpdateWithVersion = _.extend({}, this.secondFileUpdate, {version: `${this.version}.3`});
this.ProjectHistoryRedisManager.queueAddEntity.getCall(2)
.calledWith(this.project_id, this.projectHistoryId, 'file', this.firstFileUpdate.id, this.user_id, firstFileUpdateWithVersion)
.should.equal(true);
return this.ProjectHistoryRedisManager.queueAddEntity.getCall(3)
.calledWith(this.project_id, this.projectHistoryId, 'file', this.secondFileUpdate.id, this.user_id, secondFileUpdateWithVersion)
.should.equal(true);
});
it "should not flush the history", ->
@HistoryManager.flushProjectChangesAsync
.calledWith(@project_id)
.should.equal false
it("should not flush the history", function() {
return this.HistoryManager.flushProjectChangesAsync
.calledWith(this.project_id)
.should.equal(false);
});
it "should call the callback", ->
@callback.called.should.equal true
return it("should call the callback", function() {
return this.callback.called.should.equal(true);
});
});
describe "when adding a doc fails", ->
beforeEach ->
@error = new Error('error')
@ProjectHistoryRedisManager.queueAddEntity = sinon.stub().yields(@error)
@ProjectManager.updateProjectWithLocks @project_id, @projectHistoryId, @user_id, @docUpdates, @fileUpdates, @version, @callback
describe("when adding a doc fails", function() {
beforeEach(function() {
this.error = new Error('error');
this.ProjectHistoryRedisManager.queueAddEntity = sinon.stub().yields(this.error);
return this.ProjectManager.updateProjectWithLocks(this.project_id, this.projectHistoryId, this.user_id, this.docUpdates, this.fileUpdates, this.version, this.callback);
});
it "should call the callback with the error", ->
@callback.calledWith(@error).should.equal true
return it("should call the callback with the error", function() {
return this.callback.calledWith(this.error).should.equal(true);
});
});
describe "when adding a file fails", ->
beforeEach ->
@error = new Error('error')
@ProjectHistoryRedisManager.queueAddEntity = sinon.stub().yields(@error)
@ProjectManager.updateProjectWithLocks @project_id, @projectHistoryId, @user_id, @docUpdates, @fileUpdates, @version, @callback
describe("when adding a file fails", function() {
beforeEach(function() {
this.error = new Error('error');
this.ProjectHistoryRedisManager.queueAddEntity = sinon.stub().yields(this.error);
return this.ProjectManager.updateProjectWithLocks(this.project_id, this.projectHistoryId, this.user_id, this.docUpdates, this.fileUpdates, this.version, this.callback);
});
it "should call the callback with the error", ->
@callback.calledWith(@error).should.equal true
return it("should call the callback with the error", function() {
return this.callback.calledWith(this.error).should.equal(true);
});
});
describe "with enough ops to flush", ->
beforeEach ->
@HistoryManager.shouldFlushHistoryOps = sinon.stub().returns(true)
@ProjectManager.updateProjectWithLocks @project_id, @projectHistoryId, @user_id, @docUpdates, @fileUpdates, @version, @callback
return describe("with enough ops to flush", function() {
beforeEach(function() {
this.HistoryManager.shouldFlushHistoryOps = sinon.stub().returns(true);
return this.ProjectManager.updateProjectWithLocks(this.project_id, this.projectHistoryId, this.user_id, this.docUpdates, this.fileUpdates, this.version, this.callback);
});
it "should flush the history", ->
@HistoryManager.flushProjectChangesAsync
.calledWith(@project_id)
.should.equal true
return it("should flush the history", function() {
return this.HistoryManager.flushProjectChangesAsync
.calledWith(this.project_id)
.should.equal(true);
});
});
});
});
});

View file

@ -1,316 +1,395 @@
sinon = require('sinon')
chai = require('chai')
should = chai.should()
expect = chai.expect
modulePath = "../../../../app/js/RangesManager.js"
SandboxedModule = require('sandboxed-module')
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const sinon = require('sinon');
const chai = require('chai');
const should = chai.should();
const {
expect
} = chai;
const modulePath = "../../../../app/js/RangesManager.js";
const SandboxedModule = require('sandboxed-module');
describe "RangesManager", ->
beforeEach ->
@RangesManager = SandboxedModule.require modulePath,
requires:
"logger-sharelatex": @logger = { error: sinon.stub(), log: sinon.stub(), warn: sinon.stub() }
describe("RangesManager", function() {
beforeEach(function() {
this.RangesManager = SandboxedModule.require(modulePath, {
requires: {
"logger-sharelatex": (this.logger = { error: sinon.stub(), log: sinon.stub(), warn: sinon.stub() })
}
});
@doc_id = "doc-id-123"
@project_id = "project-id-123"
@user_id = "user-id-123"
@callback = sinon.stub()
this.doc_id = "doc-id-123";
this.project_id = "project-id-123";
this.user_id = "user-id-123";
return this.callback = sinon.stub();
});
describe "applyUpdate", ->
beforeEach ->
@updates = [{
meta:
user_id: @user_id
describe("applyUpdate", function() {
beforeEach(function() {
this.updates = [{
meta: {
user_id: this.user_id
},
op: [{
i: "two "
i: "two ",
p: 4
}]
}]
@entries = {
}];
this.entries = {
comments: [{
op:
c: "three "
op: {
c: "three ",
p: 4
metadata:
user_id: @user_id
}]
},
metadata: {
user_id: this.user_id
}
}],
changes: [{
op:
i: "five"
op: {
i: "five",
p: 15
metadata:
user_id: @user_id
},
metadata: {
user_id: this.user_id
}
}]
}
@newDocLines = ["one two three four five"] # old is "one three four five"
};
return this.newDocLines = ["one two three four five"];}); // old is "one three four five"
describe "successfully", ->
beforeEach ->
@RangesManager.applyUpdate @project_id, @doc_id, @entries, @updates, @newDocLines, @callback
describe("successfully", function() {
beforeEach(function() {
return this.RangesManager.applyUpdate(this.project_id, this.doc_id, this.entries, this.updates, this.newDocLines, this.callback);
});
it "should return the modified the comments and changes", ->
@callback.called.should.equal true
[error, entries, ranges_were_collapsed] = @callback.args[0]
expect(error).to.be.null
expect(ranges_were_collapsed).to.equal false
entries.comments[0].op.should.deep.equal {
c: "three "
return it("should return the modified the comments and changes", function() {
this.callback.called.should.equal(true);
const [error, entries, ranges_were_collapsed] = Array.from(this.callback.args[0]);
expect(error).to.be.null;
expect(ranges_were_collapsed).to.equal(false);
entries.comments[0].op.should.deep.equal({
c: "three ",
p: 8
}
entries.changes[0].op.should.deep.equal {
i: "five"
});
return entries.changes[0].op.should.deep.equal({
i: "five",
p: 19
}
});
});
});
describe "with empty comments", ->
beforeEach ->
@entries.comments = []
@RangesManager.applyUpdate @project_id, @doc_id, @entries, @updates, @newDocLines, @callback
describe("with empty comments", function() {
beforeEach(function() {
this.entries.comments = [];
return this.RangesManager.applyUpdate(this.project_id, this.doc_id, this.entries, this.updates, this.newDocLines, this.callback);
});
it "should return an object with no comments", ->
# Save space in redis and don't store just {}
@callback.called.should.equal true
[error, entries] = @callback.args[0]
expect(error).to.be.null
expect(entries.comments).to.be.undefined
return it("should return an object with no comments", function() {
// Save space in redis and don't store just {}
this.callback.called.should.equal(true);
const [error, entries] = Array.from(this.callback.args[0]);
expect(error).to.be.null;
return expect(entries.comments).to.be.undefined;
});
});
describe "with empty changes", ->
beforeEach ->
@entries.changes = []
@RangesManager.applyUpdate @project_id, @doc_id, @entries, @updates, @newDocLines, @callback
describe("with empty changes", function() {
beforeEach(function() {
this.entries.changes = [];
return this.RangesManager.applyUpdate(this.project_id, this.doc_id, this.entries, this.updates, this.newDocLines, this.callback);
});
it "should return an object with no changes", ->
# Save space in redis and don't store just {}
@callback.called.should.equal true
[error, entries] = @callback.args[0]
expect(error).to.be.null
expect(entries.changes).to.be.undefined
return it("should return an object with no changes", function() {
// Save space in redis and don't store just {}
this.callback.called.should.equal(true);
const [error, entries] = Array.from(this.callback.args[0]);
expect(error).to.be.null;
return expect(entries.changes).to.be.undefined;
});
});
describe "with too many comments", ->
beforeEach ->
@RangesManager.MAX_COMMENTS = 2
@updates = [{
meta:
user_id: @user_id
describe("with too many comments", function() {
beforeEach(function() {
this.RangesManager.MAX_COMMENTS = 2;
this.updates = [{
meta: {
user_id: this.user_id
},
op: [{
c: "one"
p: 0
c: "one",
p: 0,
t: "thread-id-1"
}]
}]
@entries = {
}];
this.entries = {
comments: [{
op:
c: "three "
p: 4
op: {
c: "three ",
p: 4,
t: "thread-id-2"
metadata:
user_id: @user_id
},
metadata: {
user_id: this.user_id
}
}, {
op:
c: "four "
p: 10
op: {
c: "four ",
p: 10,
t: "thread-id-3"
metadata:
user_id: @user_id
}]
},
metadata: {
user_id: this.user_id
}
}],
changes: []
}
@RangesManager.applyUpdate @project_id, @doc_id, @entries, @updates, @newDocLines, @callback
};
return this.RangesManager.applyUpdate(this.project_id, this.doc_id, this.entries, this.updates, this.newDocLines, this.callback);
});
it "should return an error", ->
@callback.called.should.equal true
[error, entries] = @callback.args[0]
expect(error).to.not.be.null
expect(error.message).to.equal("too many comments or tracked changes")
return it("should return an error", function() {
this.callback.called.should.equal(true);
const [error, entries] = Array.from(this.callback.args[0]);
expect(error).to.not.be.null;
return expect(error.message).to.equal("too many comments or tracked changes");
});
});
describe "with too many changes", ->
beforeEach ->
@RangesManager.MAX_CHANGES = 2
@updates = [{
meta:
user_id: @user_id
describe("with too many changes", function() {
beforeEach(function() {
this.RangesManager.MAX_CHANGES = 2;
this.updates = [{
meta: {
user_id: this.user_id,
tc: "track-changes-id-yes"
},
op: [{
i: "one "
i: "one ",
p: 0
}]
}]
@entries = {
}];
this.entries = {
changes: [{
op:
i: "three"
op: {
i: "three",
p: 4
metadata:
user_id: @user_id
},
metadata: {
user_id: this.user_id
}
}, {
op:
i: "four"
op: {
i: "four",
p: 10
metadata:
user_id: @user_id
}]
},
metadata: {
user_id: this.user_id
}
}],
comments: []
}
@newDocLines = ["one two three four"]
@RangesManager.applyUpdate @project_id, @doc_id, @entries, @updates, @newDocLines, @callback
};
this.newDocLines = ["one two three four"];
return this.RangesManager.applyUpdate(this.project_id, this.doc_id, this.entries, this.updates, this.newDocLines, this.callback);
});
it "should return an error", ->
# Save space in redis and don't store just {}
@callback.called.should.equal true
[error, entries] = @callback.args[0]
expect(error).to.not.be.null
expect(error.message).to.equal("too many comments or tracked changes")
return it("should return an error", function() {
// Save space in redis and don't store just {}
this.callback.called.should.equal(true);
const [error, entries] = Array.from(this.callback.args[0]);
expect(error).to.not.be.null;
return expect(error.message).to.equal("too many comments or tracked changes");
});
});
describe "inconsistent changes", ->
beforeEach ->
@updates = [{
meta:
user_id: @user_id
describe("inconsistent changes", function() {
beforeEach(function() {
this.updates = [{
meta: {
user_id: this.user_id
},
op: [{
c: "doesn't match"
c: "doesn't match",
p: 0
}]
}]
@RangesManager.applyUpdate @project_id, @doc_id, @entries, @updates, @newDocLines, @callback
}];
return this.RangesManager.applyUpdate(this.project_id, this.doc_id, this.entries, this.updates, this.newDocLines, this.callback);
});
it "should return an error", ->
# Save space in redis and don't store just {}
@callback.called.should.equal true
[error, entries] = @callback.args[0]
expect(error).to.not.be.null
expect(error.message).to.equal("Change ({\"op\":{\"i\":\"five\",\"p\":15},\"metadata\":{\"user_id\":\"user-id-123\"}}) doesn't match text (\"our \")")
return it("should return an error", function() {
// Save space in redis and don't store just {}
this.callback.called.should.equal(true);
const [error, entries] = Array.from(this.callback.args[0]);
expect(error).to.not.be.null;
return expect(error.message).to.equal("Change ({\"op\":{\"i\":\"five\",\"p\":15},\"metadata\":{\"user_id\":\"user-id-123\"}}) doesn't match text (\"our \")");
});
});
describe "with an update that collapses a range", ->
beforeEach ->
@updates = [{
meta:
user_id: @user_id
return describe("with an update that collapses a range", function() {
beforeEach(function() {
this.updates = [{
meta: {
user_id: this.user_id
},
op: [{
d: "one"
p: 0
d: "one",
p: 0,
t: "thread-id-1"
}]
}]
@entries = {
}];
this.entries = {
comments: [{
op:
c: "n"
p: 1
op: {
c: "n",
p: 1,
t: "thread-id-2"
metadata:
user_id: @user_id
}]
},
metadata: {
user_id: this.user_id
}
}],
changes: []
};
return this.RangesManager.applyUpdate(this.project_id, this.doc_id, this.entries, this.updates, this.newDocLines, this.callback);
});
return it("should return ranges_were_collapsed == true", function() {
this.callback.called.should.equal(true);
const [error, entries, ranges_were_collapsed] = Array.from(this.callback.args[0]);
return expect(ranges_were_collapsed).to.equal(true);
});
});
});
return describe("acceptChanges", function() {
beforeEach(function() {
this.RangesManager = SandboxedModule.require(modulePath, {
requires: {
"logger-sharelatex": (this.logger = { error: sinon.stub(), log: sinon.stub(), warn: sinon.stub() }),
"./RangesTracker":(this.RangesTracker = SandboxedModule.require("../../../../app/js/RangesTracker.js"))
}
@RangesManager.applyUpdate @project_id, @doc_id, @entries, @updates, @newDocLines, @callback
it "should return ranges_were_collapsed == true", ->
@callback.called.should.equal true
[error, entries, ranges_were_collapsed] = @callback.args[0]
expect(ranges_were_collapsed).to.equal true
describe "acceptChanges", ->
beforeEach ->
@RangesManager = SandboxedModule.require modulePath,
requires:
"logger-sharelatex": @logger = { error: sinon.stub(), log: sinon.stub(), warn: sinon.stub() }
"./RangesTracker":@RangesTracker = SandboxedModule.require "../../../../app/js/RangesTracker.js"
@ranges = {
comments: []
changes: [{
id: "a1"
op:
i: "lorem"
p: 0
}, {
id: "a2"
op:
i: "ipsum"
p: 10
}, {
id: "a3"
op:
i: "dolor"
p: 20
}, {
id: "a4"
op:
i: "sit"
p: 30
}, {
id: "a5"
op:
i: "amet"
p: 40
}]
}
@removeChangeIdsSpy = sinon.spy @RangesTracker.prototype, "removeChangeIds"
);
describe "successfully with a single change", ->
beforeEach (done) ->
@change_ids = [ @ranges.changes[1].id ]
@RangesManager.acceptChanges @change_ids, @ranges, (err, ranges) =>
@rangesResponse = ranges
done()
this.ranges = {
comments: [],
changes: [{
id: "a1",
op: {
i: "lorem",
p: 0
}
}, {
id: "a2",
op: {
i: "ipsum",
p: 10
}
}, {
id: "a3",
op: {
i: "dolor",
p: 20
}
}, {
id: "a4",
op: {
i: "sit",
p: 30
}
}, {
id: "a5",
op: {
i: "amet",
p: 40
}
}]
};
return this.removeChangeIdsSpy = sinon.spy(this.RangesTracker.prototype, "removeChangeIds");
});
it "should log the call with the correct number of changes", ->
@logger.log
describe("successfully with a single change", function() {
beforeEach(function(done) {
this.change_ids = [ this.ranges.changes[1].id ];
return this.RangesManager.acceptChanges(this.change_ids, this.ranges, (err, ranges) => {
this.rangesResponse = ranges;
return done();
});
});
it("should log the call with the correct number of changes", function() {
return this.logger.log
.calledWith("accepting 1 changes in ranges")
.should.equal true
.should.equal(true);
});
it "should delegate the change removal to the ranges tracker", ->
@removeChangeIdsSpy
.calledWith(@change_ids)
.should.equal true
it("should delegate the change removal to the ranges tracker", function() {
return this.removeChangeIdsSpy
.calledWith(this.change_ids)
.should.equal(true);
});
it "should remove the change", ->
expect(@rangesResponse.changes
.find((change) => change.id == @ranges.changes[1].id))
.to.be.undefined
it("should remove the change", function() {
return expect(this.rangesResponse.changes
.find(change => change.id === this.ranges.changes[1].id))
.to.be.undefined;
});
it "should return the original number of changes minus 1", ->
@rangesResponse.changes.length
.should.equal @ranges.changes.length - 1
it("should return the original number of changes minus 1", function() {
return this.rangesResponse.changes.length
.should.equal(this.ranges.changes.length - 1);
});
it "should not touch other changes", ->
for i in [ 0, 2, 3, 4]
expect(@rangesResponse.changes
.find((change) => change.id == @ranges.changes[i].id))
.to.deep.equal @ranges.changes[i]
return it("should not touch other changes", function() {
return [ 0, 2, 3, 4].map((i) =>
expect(this.rangesResponse.changes
.find(change => change.id === this.ranges.changes[i].id))
.to.deep.equal(this.ranges.changes[i]));
});
});
describe "successfully with multiple changes", ->
beforeEach (done) ->
@change_ids = [ @ranges.changes[1].id, @ranges.changes[3].id, @ranges.changes[4].id ]
@RangesManager.acceptChanges @change_ids, @ranges, (err, ranges) =>
@rangesResponse = ranges
done()
return describe("successfully with multiple changes", function() {
beforeEach(function(done) {
this.change_ids = [ this.ranges.changes[1].id, this.ranges.changes[3].id, this.ranges.changes[4].id ];
return this.RangesManager.acceptChanges(this.change_ids, this.ranges, (err, ranges) => {
this.rangesResponse = ranges;
return done();
});
});
it "should log the call with the correct number of changes", ->
@logger.log
.calledWith("accepting #{ @change_ids.length } changes in ranges")
.should.equal true
it("should log the call with the correct number of changes", function() {
return this.logger.log
.calledWith(`accepting ${ this.change_ids.length } changes in ranges`)
.should.equal(true);
});
it "should delegate the change removal to the ranges tracker", ->
@removeChangeIdsSpy
.calledWith(@change_ids)
.should.equal true
it("should delegate the change removal to the ranges tracker", function() {
return this.removeChangeIdsSpy
.calledWith(this.change_ids)
.should.equal(true);
});
it "should remove the changes", ->
for i in [ 1, 3, 4]
expect(@rangesResponse.changes
.find((change) => change.id == @ranges.changes[1].id))
.to.be.undefined
it("should remove the changes", function() {
return [ 1, 3, 4].map((i) =>
expect(this.rangesResponse.changes
.find(change => change.id === this.ranges.changes[1].id))
.to.be.undefined);
});
it "should return the original number of changes minus the number of accepted changes", ->
@rangesResponse.changes.length
.should.equal @ranges.changes.length - 3
it("should return the original number of changes minus the number of accepted changes", function() {
return this.rangesResponse.changes.length
.should.equal(this.ranges.changes.length - 3);
});
it "should not touch other changes", ->
for i in [ 0, 2 ]
expect(@rangesResponse.changes
.find((change) => change.id == @ranges.changes[i].id))
.to.deep.equal @ranges.changes[i]
return it("should not touch other changes", function() {
return [ 0, 2 ].map((i) =>
expect(this.rangesResponse.changes
.find(change => change.id === this.ranges.changes[i].id))
.to.deep.equal(this.ranges.changes[i]));
});
});
});
});

View file

@ -1,88 +1,132 @@
sinon = require('sinon')
chai = require('chai')
should = chai.should()
expect = chai.expect
modulePath = "../../../../app/js/RateLimitManager.js"
SandboxedModule = require('sandboxed-module')
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS206: Consider reworking classes to avoid initClass
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const sinon = require('sinon');
const chai = require('chai');
const should = chai.should();
const {
expect
} = chai;
const modulePath = "../../../../app/js/RateLimitManager.js";
const SandboxedModule = require('sandboxed-module');
describe "RateLimitManager", ->
beforeEach ->
@RateLimitManager = SandboxedModule.require modulePath, requires:
"logger-sharelatex": @logger = { log: sinon.stub() }
"settings-sharelatex": @settings = {}
"./Metrics": @Metrics =
Timer: class Timer
done: sinon.stub()
describe("RateLimitManager", function() {
beforeEach(function() {
let Timer;
this.RateLimitManager = SandboxedModule.require(modulePath, { requires: {
"logger-sharelatex": (this.logger = { log: sinon.stub() }),
"settings-sharelatex": (this.settings = {}),
"./Metrics": (this.Metrics = {
Timer: (Timer = (function() {
Timer = class Timer {
static initClass() {
this.prototype.done = sinon.stub();
}
};
Timer.initClass();
return Timer;
})()),
gauge: sinon.stub()
@callback = sinon.stub()
@RateLimiter = new @RateLimitManager(1)
})
}
}
);
this.callback = sinon.stub();
return this.RateLimiter = new this.RateLimitManager(1);
});
describe "for a single task", ->
beforeEach ->
@task = sinon.stub()
@RateLimiter.run @task, @callback
describe("for a single task", function() {
beforeEach(function() {
this.task = sinon.stub();
return this.RateLimiter.run(this.task, this.callback);
});
it "should execute the task in the background", ->
@task.called.should.equal true
it("should execute the task in the background", function() {
return this.task.called.should.equal(true);
});
it "should call the callback", ->
@callback.called.should.equal true
it("should call the callback", function() {
return this.callback.called.should.equal(true);
});
it "should finish with a worker count of one", ->
# because it's in the background
expect(@RateLimiter.ActiveWorkerCount).to.equal 1
return it("should finish with a worker count of one", function() {
// because it's in the background
return expect(this.RateLimiter.ActiveWorkerCount).to.equal(1);
});
});
describe "for multiple tasks", ->
beforeEach (done) ->
@task = sinon.stub()
@finalTask = sinon.stub()
task = (cb) =>
@task()
setTimeout cb, 100
finalTask = (cb) =>
@finalTask()
setTimeout cb, 100
@RateLimiter.run task, @callback
@RateLimiter.run task, @callback
@RateLimiter.run task, @callback
@RateLimiter.run finalTask, (err) =>
@callback(err)
done()
describe("for multiple tasks", function() {
beforeEach(function(done) {
this.task = sinon.stub();
this.finalTask = sinon.stub();
const task = cb => {
this.task();
return setTimeout(cb, 100);
};
const finalTask = cb => {
this.finalTask();
return setTimeout(cb, 100);
};
this.RateLimiter.run(task, this.callback);
this.RateLimiter.run(task, this.callback);
this.RateLimiter.run(task, this.callback);
return this.RateLimiter.run(finalTask, err => {
this.callback(err);
return done();
});
});
it "should execute the first three tasks", ->
@task.calledThrice.should.equal true
it("should execute the first three tasks", function() {
return this.task.calledThrice.should.equal(true);
});
it "should execute the final task", ->
@finalTask.called.should.equal true
it("should execute the final task", function() {
return this.finalTask.called.should.equal(true);
});
it "should call the callback", ->
@callback.called.should.equal true
it("should call the callback", function() {
return this.callback.called.should.equal(true);
});
it "should finish with worker count of zero", ->
expect(@RateLimiter.ActiveWorkerCount).to.equal 0
return it("should finish with worker count of zero", function() {
return expect(this.RateLimiter.ActiveWorkerCount).to.equal(0);
});
});
describe "for a mixture of long-running tasks", ->
beforeEach (done) ->
@task = sinon.stub()
@finalTask = sinon.stub()
finalTask = (cb) =>
@finalTask()
setTimeout cb, 100
@RateLimiter.run @task, @callback
@RateLimiter.run @task, @callback
@RateLimiter.run @task, @callback
@RateLimiter.run finalTask, (err) =>
@callback(err)
done()
return describe("for a mixture of long-running tasks", function() {
beforeEach(function(done) {
this.task = sinon.stub();
this.finalTask = sinon.stub();
const finalTask = cb => {
this.finalTask();
return setTimeout(cb, 100);
};
this.RateLimiter.run(this.task, this.callback);
this.RateLimiter.run(this.task, this.callback);
this.RateLimiter.run(this.task, this.callback);
return this.RateLimiter.run(finalTask, err => {
this.callback(err);
return done();
});
});
it "should execute the first three tasks", ->
@task.calledThrice.should.equal true
it("should execute the first three tasks", function() {
return this.task.calledThrice.should.equal(true);
});
it "should execute the final task", ->
@finalTask.called.should.equal true
it("should execute the final task", function() {
return this.finalTask.called.should.equal(true);
});
it "should call the callback", ->
@callback.called.should.equal true
it("should call the callback", function() {
return this.callback.called.should.equal(true);
});
it "should finish with worker count of three", ->
expect(@RateLimiter.ActiveWorkerCount).to.equal 3
return it("should finish with worker count of three", function() {
return expect(this.RateLimiter.ActiveWorkerCount).to.equal(3);
});
});
});

View file

@ -1,95 +1,129 @@
sinon = require('sinon')
chai = require('chai')
should = chai.should()
modulePath = "../../../../app/js/RealTimeRedisManager.js"
SandboxedModule = require('sandboxed-module')
Errors = require "../../../../app/js/Errors"
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const sinon = require('sinon');
const chai = require('chai');
const should = chai.should();
const modulePath = "../../../../app/js/RealTimeRedisManager.js";
const SandboxedModule = require('sandboxed-module');
const Errors = require("../../../../app/js/Errors");
describe "RealTimeRedisManager", ->
beforeEach ->
@rclient =
auth: () ->
describe("RealTimeRedisManager", function() {
beforeEach(function() {
this.rclient = {
auth() {},
exec: sinon.stub()
@rclient.multi = () => @rclient
@pubsubClient =
publish: sinon.stub()
@RealTimeRedisManager = SandboxedModule.require modulePath, requires:
"redis-sharelatex": createClient: (config) => if (config.name is 'pubsub') then @pubsubClient else @rclient
"settings-sharelatex":
redis:
documentupdater: @settings =
key_schema:
pendingUpdates: ({doc_id}) -> "PendingUpdates:#{doc_id}"
pubsub:
};
this.rclient.multi = () => this.rclient;
this.pubsubClient =
{publish: sinon.stub()};
this.RealTimeRedisManager = SandboxedModule.require(modulePath, { requires: {
"redis-sharelatex": { createClient: config => (config.name === 'pubsub') ? this.pubsubClient : this.rclient
},
"settings-sharelatex": {
redis: {
documentupdater: (this.settings = {
key_schema: {
pendingUpdates({doc_id}) { return `PendingUpdates:${doc_id}`; }
}
}),
pubsub: {
name: "pubsub"
"logger-sharelatex": { log: () -> }
"crypto": @crypto = { randomBytes: sinon.stub().withArgs(4).returns(Buffer.from([0x1, 0x2, 0x3, 0x4])) }
"os": @os = {hostname: sinon.stub().returns("somehost")}
"./Metrics": @metrics = { summary: sinon.stub()}
}
}
},
"logger-sharelatex": { log() {} },
"crypto": (this.crypto = { randomBytes: sinon.stub().withArgs(4).returns(Buffer.from([0x1, 0x2, 0x3, 0x4])) }),
"os": (this.os = {hostname: sinon.stub().returns("somehost")}),
"./Metrics": (this.metrics = { summary: sinon.stub()})
}
});
@doc_id = "doc-id-123"
@project_id = "project-id-123"
@callback = sinon.stub()
this.doc_id = "doc-id-123";
this.project_id = "project-id-123";
return this.callback = sinon.stub();
});
describe "getPendingUpdatesForDoc", ->
beforeEach ->
@rclient.lrange = sinon.stub()
@rclient.ltrim = sinon.stub()
describe("getPendingUpdatesForDoc", function() {
beforeEach(function() {
this.rclient.lrange = sinon.stub();
return this.rclient.ltrim = sinon.stub();
});
describe "successfully", ->
beforeEach ->
@updates = [
describe("successfully", function() {
beforeEach(function() {
this.updates = [
{ op: [{ i: "foo", p: 4 }] },
{ op: [{ i: "foo", p: 4 }] }
{ op: [{ i: "foo", p: 4 }] }
]
@jsonUpdates = @updates.map (update) -> JSON.stringify update
@rclient.exec = sinon.stub().callsArgWith(0, null, [@jsonUpdates])
@RealTimeRedisManager.getPendingUpdatesForDoc @doc_id, @callback
];
this.jsonUpdates = this.updates.map(update => JSON.stringify(update));
this.rclient.exec = sinon.stub().callsArgWith(0, null, [this.jsonUpdates]);
return this.RealTimeRedisManager.getPendingUpdatesForDoc(this.doc_id, this.callback);
});
it "should get the pending updates", ->
@rclient.lrange
.calledWith("PendingUpdates:#{@doc_id}", 0, 7)
.should.equal true
it("should get the pending updates", function() {
return this.rclient.lrange
.calledWith(`PendingUpdates:${this.doc_id}`, 0, 7)
.should.equal(true);
});
it "should delete the pending updates", ->
@rclient.ltrim
.calledWith("PendingUpdates:#{@doc_id}", 8, -1)
.should.equal true
it("should delete the pending updates", function() {
return this.rclient.ltrim
.calledWith(`PendingUpdates:${this.doc_id}`, 8, -1)
.should.equal(true);
});
it "should call the callback with the updates", ->
@callback.calledWith(null, @updates).should.equal true
return it("should call the callback with the updates", function() {
return this.callback.calledWith(null, this.updates).should.equal(true);
});
});
describe "when the JSON doesn't parse", ->
beforeEach ->
@jsonUpdates = [
JSON.stringify { op: [{ i: "foo", p: 4 }] }
return describe("when the JSON doesn't parse", function() {
beforeEach(function() {
this.jsonUpdates = [
JSON.stringify({ op: [{ i: "foo", p: 4 }] }),
"broken json"
]
@rclient.exec = sinon.stub().callsArgWith(0, null, [@jsonUpdates])
@RealTimeRedisManager.getPendingUpdatesForDoc @doc_id, @callback
];
this.rclient.exec = sinon.stub().callsArgWith(0, null, [this.jsonUpdates]);
return this.RealTimeRedisManager.getPendingUpdatesForDoc(this.doc_id, this.callback);
});
it "should return an error to the callback", ->
@callback.calledWith(new Error("JSON parse error")).should.equal true
return it("should return an error to the callback", function() {
return this.callback.calledWith(new Error("JSON parse error")).should.equal(true);
});
});
});
describe "getUpdatesLength", ->
beforeEach ->
@rclient.llen = sinon.stub().yields(null, @length = 3)
@RealTimeRedisManager.getUpdatesLength @doc_id, @callback
describe("getUpdatesLength", function() {
beforeEach(function() {
this.rclient.llen = sinon.stub().yields(null, (this.length = 3));
return this.RealTimeRedisManager.getUpdatesLength(this.doc_id, this.callback);
});
it "should look up the length", ->
@rclient.llen.calledWith("PendingUpdates:#{@doc_id}").should.equal true
it("should look up the length", function() {
return this.rclient.llen.calledWith(`PendingUpdates:${this.doc_id}`).should.equal(true);
});
it "should return the length", ->
@callback.calledWith(null, @length).should.equal true
return it("should return the length", function() {
return this.callback.calledWith(null, this.length).should.equal(true);
});
});
describe "sendData", ->
beforeEach ->
@message_id = "doc:somehost:01020304-0"
@RealTimeRedisManager.sendData({op: "thisop"})
return describe("sendData", function() {
beforeEach(function() {
this.message_id = "doc:somehost:01020304-0";
return this.RealTimeRedisManager.sendData({op: "thisop"});
});
it "should send the op with a message id", ->
@pubsubClient.publish.calledWith("applied-ops", JSON.stringify({op:"thisop",_id:@message_id})).should.equal true
it("should send the op with a message id", function() {
return this.pubsubClient.publish.calledWith("applied-ops", JSON.stringify({op:"thisop",_id:this.message_id})).should.equal(true);
});
it "should track the payload size", ->
@metrics.summary.calledWith("redis.publish.applied-ops", JSON.stringify({op:"thisop",_id:@message_id}).length).should.equal true
return it("should track the payload size", function() {
return this.metrics.summary.calledWith("redis.publish.applied-ops", JSON.stringify({op:"thisop",_id:this.message_id}).length).should.equal(true);
});
});
});

View file

@ -1,283 +1,358 @@
text = require "../../../../app/js/sharejs/types/text"
require("chai").should()
RangesTracker = require "../../../../app/js/RangesTracker"
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS202: Simplify dynamic range loops
* DS205: Consider reworking code to avoid use of IIFEs
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const text = require("../../../../app/js/sharejs/types/text");
require("chai").should();
const RangesTracker = require("../../../../app/js/RangesTracker");
describe "ShareJS text type", ->
beforeEach ->
@t = "mock-thread-id"
describe("ShareJS text type", function() {
beforeEach(function() {
return this.t = "mock-thread-id";
});
describe "transform", ->
describe "insert / insert", ->
it "with an insert before", ->
dest = []
text._tc(dest, { i: "foo", p: 9 }, { i: "bar", p: 3 })
dest.should.deep.equal [{ i: "foo", p: 12 }]
describe("transform", function() {
describe("insert / insert", function() {
it("with an insert before", function() {
const dest = [];
text._tc(dest, { i: "foo", p: 9 }, { i: "bar", p: 3 });
return dest.should.deep.equal([{ i: "foo", p: 12 }]);
});
it "with an insert after", ->
dest = []
text._tc(dest, { i: "foo", p: 3 }, { i: "bar", p: 9 })
dest.should.deep.equal [{ i: "foo", p: 3 }]
it("with an insert after", function() {
const dest = [];
text._tc(dest, { i: "foo", p: 3 }, { i: "bar", p: 9 });
return dest.should.deep.equal([{ i: "foo", p: 3 }]);
});
it "with an insert at the same place with side == 'right'", ->
dest = []
text._tc(dest, { i: "foo", p: 3 }, { i: "bar", p: 3 }, 'right')
dest.should.deep.equal [{ i: "foo", p: 6 }]
it("with an insert at the same place with side == 'right'", function() {
const dest = [];
text._tc(dest, { i: "foo", p: 3 }, { i: "bar", p: 3 }, 'right');
return dest.should.deep.equal([{ i: "foo", p: 6 }]);
});
it "with an insert at the same place with side == 'left'", ->
dest = []
text._tc(dest, { i: "foo", p: 3 }, { i: "bar", p: 3 }, 'left')
dest.should.deep.equal [{ i: "foo", p: 3 }]
return it("with an insert at the same place with side == 'left'", function() {
const dest = [];
text._tc(dest, { i: "foo", p: 3 }, { i: "bar", p: 3 }, 'left');
return dest.should.deep.equal([{ i: "foo", p: 3 }]);
});
});
describe "insert / delete", ->
it "with a delete before", ->
dest = []
text._tc(dest, { i: "foo", p: 9 }, { d: "bar", p: 3 })
dest.should.deep.equal [{ i: "foo", p: 6 }]
describe("insert / delete", function() {
it("with a delete before", function() {
const dest = [];
text._tc(dest, { i: "foo", p: 9 }, { d: "bar", p: 3 });
return dest.should.deep.equal([{ i: "foo", p: 6 }]);
});
it "with a delete after", ->
dest = []
text._tc(dest, { i: "foo", p: 3 }, { d: "bar", p: 9 })
dest.should.deep.equal [{ i: "foo", p: 3 }]
it("with a delete after", function() {
const dest = [];
text._tc(dest, { i: "foo", p: 3 }, { d: "bar", p: 9 });
return dest.should.deep.equal([{ i: "foo", p: 3 }]);
});
it "with a delete at the same place with side == 'right'", ->
dest = []
text._tc(dest, { i: "foo", p: 3 }, { d: "bar", p: 3 }, 'right')
dest.should.deep.equal [{ i: "foo", p: 3 }]
it("with a delete at the same place with side == 'right'", function() {
const dest = [];
text._tc(dest, { i: "foo", p: 3 }, { d: "bar", p: 3 }, 'right');
return dest.should.deep.equal([{ i: "foo", p: 3 }]);
});
it "with a delete at the same place with side == 'left'", ->
dest = []
return it("with a delete at the same place with side == 'left'", function() {
const dest = [];
text._tc(dest, { i: "foo", p: 3 }, { d: "bar", p: 3 }, 'left')
dest.should.deep.equal [{ i: "foo", p: 3 }]
text._tc(dest, { i: "foo", p: 3 }, { d: "bar", p: 3 }, 'left');
return dest.should.deep.equal([{ i: "foo", p: 3 }]);
});
});
describe "delete / insert", ->
it "with an insert before", ->
dest = []
text._tc(dest, { d: "foo", p: 9 }, { i: "bar", p: 3 })
dest.should.deep.equal [{ d: "foo", p: 12 }]
describe("delete / insert", function() {
it("with an insert before", function() {
const dest = [];
text._tc(dest, { d: "foo", p: 9 }, { i: "bar", p: 3 });
return dest.should.deep.equal([{ d: "foo", p: 12 }]);
});
it "with an insert after", ->
dest = []
text._tc(dest, { d: "foo", p: 3 }, { i: "bar", p: 9 })
dest.should.deep.equal [{ d: "foo", p: 3 }]
it("with an insert after", function() {
const dest = [];
text._tc(dest, { d: "foo", p: 3 }, { i: "bar", p: 9 });
return dest.should.deep.equal([{ d: "foo", p: 3 }]);
});
it "with an insert at the same place with side == 'right'", ->
dest = []
text._tc(dest, { d: "foo", p: 3 }, { i: "bar", p: 3 }, 'right')
dest.should.deep.equal [{ d: "foo", p: 6 }]
it("with an insert at the same place with side == 'right'", function() {
const dest = [];
text._tc(dest, { d: "foo", p: 3 }, { i: "bar", p: 3 }, 'right');
return dest.should.deep.equal([{ d: "foo", p: 6 }]);
});
it "with an insert at the same place with side == 'left'", ->
dest = []
text._tc(dest, { d: "foo", p: 3 }, { i: "bar", p: 3 }, 'left')
dest.should.deep.equal [{ d: "foo", p: 6 }]
it("with an insert at the same place with side == 'left'", function() {
const dest = [];
text._tc(dest, { d: "foo", p: 3 }, { i: "bar", p: 3 }, 'left');
return dest.should.deep.equal([{ d: "foo", p: 6 }]);
});
it "with a delete that overlaps the insert location", ->
dest = []
text._tc(dest, { d: "foo", p: 3 }, { i: "bar", p: 4 })
dest.should.deep.equal [{ d: "f", p: 3 }, { d: "oo", p: 6 }]
return it("with a delete that overlaps the insert location", function() {
const dest = [];
text._tc(dest, { d: "foo", p: 3 }, { i: "bar", p: 4 });
return dest.should.deep.equal([{ d: "f", p: 3 }, { d: "oo", p: 6 }]);
});
});
describe "delete / delete", ->
it "with a delete before", ->
dest = []
text._tc(dest, { d: "foo", p: 9 }, { d: "bar", p: 3 })
dest.should.deep.equal [{ d: "foo", p: 6 }]
describe("delete / delete", function() {
it("with a delete before", function() {
const dest = [];
text._tc(dest, { d: "foo", p: 9 }, { d: "bar", p: 3 });
return dest.should.deep.equal([{ d: "foo", p: 6 }]);
});
it "with a delete after", ->
dest = []
text._tc(dest, { d: "foo", p: 3 }, { d: "bar", p: 9 })
dest.should.deep.equal [{ d: "foo", p: 3 }]
it("with a delete after", function() {
const dest = [];
text._tc(dest, { d: "foo", p: 3 }, { d: "bar", p: 9 });
return dest.should.deep.equal([{ d: "foo", p: 3 }]);
});
it "with deleting the same content", ->
dest = []
text._tc(dest, { d: "foo", p: 3 }, { d: "foo", p: 3 }, 'right')
dest.should.deep.equal []
it("with deleting the same content", function() {
const dest = [];
text._tc(dest, { d: "foo", p: 3 }, { d: "foo", p: 3 }, 'right');
return dest.should.deep.equal([]);
});
it "with the delete overlapping before", ->
dest = []
text._tc(dest, { d: "foobar", p: 3 }, { d: "abcfoo", p: 0 }, 'right')
dest.should.deep.equal [{ d: "bar", p: 0 }]
it("with the delete overlapping before", function() {
const dest = [];
text._tc(dest, { d: "foobar", p: 3 }, { d: "abcfoo", p: 0 }, 'right');
return dest.should.deep.equal([{ d: "bar", p: 0 }]);
});
it "with the delete overlapping after", ->
dest = []
text._tc(dest, { d: "abcfoo", p: 3 }, { d: "foobar", p: 6 })
dest.should.deep.equal [{ d: "abc", p: 3 }]
it("with the delete overlapping after", function() {
const dest = [];
text._tc(dest, { d: "abcfoo", p: 3 }, { d: "foobar", p: 6 });
return dest.should.deep.equal([{ d: "abc", p: 3 }]);
});
it "with the delete overlapping the whole delete", ->
dest = []
text._tc(dest, { d: "abcfoo123", p: 3 }, { d: "foo", p: 6 })
dest.should.deep.equal [{ d: "abc123", p: 3 }]
it("with the delete overlapping the whole delete", function() {
const dest = [];
text._tc(dest, { d: "abcfoo123", p: 3 }, { d: "foo", p: 6 });
return dest.should.deep.equal([{ d: "abc123", p: 3 }]);
});
it "with the delete inside the whole delete", ->
dest = []
text._tc(dest, { d: "foo", p: 6 }, { d: "abcfoo123", p: 3 })
dest.should.deep.equal []
return it("with the delete inside the whole delete", function() {
const dest = [];
text._tc(dest, { d: "foo", p: 6 }, { d: "abcfoo123", p: 3 });
return dest.should.deep.equal([]);
});
});
describe "comment / insert", ->
it "with an insert before", ->
dest = []
text._tc(dest, { c: "foo", p: 9, @t }, { i: "bar", p: 3 })
dest.should.deep.equal [{ c: "foo", p: 12, @t }]
describe("comment / insert", function() {
it("with an insert before", function() {
const dest = [];
text._tc(dest, { c: "foo", p: 9, t: this.t }, { i: "bar", p: 3 });
return dest.should.deep.equal([{ c: "foo", p: 12, t: this.t }]);
});
it "with an insert after", ->
dest = []
text._tc(dest, { c: "foo", p: 3, @t }, { i: "bar", p: 9 })
dest.should.deep.equal [{ c: "foo", p: 3, @t }]
it("with an insert after", function() {
const dest = [];
text._tc(dest, { c: "foo", p: 3, t: this.t }, { i: "bar", p: 9 });
return dest.should.deep.equal([{ c: "foo", p: 3, t: this.t }]);
});
it "with an insert at the left edge", ->
dest = []
text._tc(dest, { c: "foo", p: 3, @t }, { i: "bar", p: 3 })
# RangesTracker doesn't inject inserts into comments on edges, so neither should we
dest.should.deep.equal [{ c: "foo", p: 6, @t }]
it("with an insert at the left edge", function() {
const dest = [];
text._tc(dest, { c: "foo", p: 3, t: this.t }, { i: "bar", p: 3 });
// RangesTracker doesn't inject inserts into comments on edges, so neither should we
return dest.should.deep.equal([{ c: "foo", p: 6, t: this.t }]);
});
it "with an insert at the right edge", ->
dest = []
text._tc(dest, { c: "foo", p: 3, @t }, { i: "bar", p: 6 })
# RangesTracker doesn't inject inserts into comments on edges, so neither should we
dest.should.deep.equal [{ c: "foo", p: 3, @t }]
it("with an insert at the right edge", function() {
const dest = [];
text._tc(dest, { c: "foo", p: 3, t: this.t }, { i: "bar", p: 6 });
// RangesTracker doesn't inject inserts into comments on edges, so neither should we
return dest.should.deep.equal([{ c: "foo", p: 3, t: this.t }]);
});
it "with an insert in the middle", ->
dest = []
text._tc(dest, { c: "foo", p: 3, @t }, { i: "bar", p: 5 })
dest.should.deep.equal [{ c: "fobaro", p: 3, @t }]
return it("with an insert in the middle", function() {
const dest = [];
text._tc(dest, { c: "foo", p: 3, t: this.t }, { i: "bar", p: 5 });
return dest.should.deep.equal([{ c: "fobaro", p: 3, t: this.t }]);
});
});
describe "comment / delete", ->
it "with a delete before", ->
dest = []
text._tc(dest, { c: "foo", p: 9, @t }, { d: "bar", p: 3 })
dest.should.deep.equal [{ c: "foo", p: 6, @t }]
describe("comment / delete", function() {
it("with a delete before", function() {
const dest = [];
text._tc(dest, { c: "foo", p: 9, t: this.t }, { d: "bar", p: 3 });
return dest.should.deep.equal([{ c: "foo", p: 6, t: this.t }]);
});
it "with a delete after", ->
dest = []
text._tc(dest, { c: "foo", p: 3, @t }, { i: "bar", p: 9 })
dest.should.deep.equal [{ c: "foo", p: 3, @t }]
it("with a delete after", function() {
const dest = [];
text._tc(dest, { c: "foo", p: 3, t: this.t }, { i: "bar", p: 9 });
return dest.should.deep.equal([{ c: "foo", p: 3, t: this.t }]);
});
it "with a delete overlapping the comment content before", ->
dest = []
text._tc(dest, { c: "foobar", p: 6, @t }, { d: "123foo", p: 3 })
dest.should.deep.equal [{ c: "bar", p: 3, @t }]
it("with a delete overlapping the comment content before", function() {
const dest = [];
text._tc(dest, { c: "foobar", p: 6, t: this.t }, { d: "123foo", p: 3 });
return dest.should.deep.equal([{ c: "bar", p: 3, t: this.t }]);
});
it "with a delete overlapping the comment content after", ->
dest = []
text._tc(dest, { c: "foobar", p: 6, @t }, { d: "bar123", p: 9 })
dest.should.deep.equal [{ c: "foo", p: 6, @t }]
it("with a delete overlapping the comment content after", function() {
const dest = [];
text._tc(dest, { c: "foobar", p: 6, t: this.t }, { d: "bar123", p: 9 });
return dest.should.deep.equal([{ c: "foo", p: 6, t: this.t }]);
});
it "with a delete overlapping the comment content in the middle", ->
dest = []
text._tc(dest, { c: "foo123bar", p: 6, @t }, { d: "123", p: 9 })
dest.should.deep.equal [{ c: "foobar", p: 6, @t }]
it("with a delete overlapping the comment content in the middle", function() {
const dest = [];
text._tc(dest, { c: "foo123bar", p: 6, t: this.t }, { d: "123", p: 9 });
return dest.should.deep.equal([{ c: "foobar", p: 6, t: this.t }]);
});
it "with a delete overlapping the whole comment", ->
dest = []
text._tc(dest, { c: "foo", p: 6, @t }, { d: "123foo456", p: 3 })
dest.should.deep.equal [{ c: "", p: 3, @t }]
return it("with a delete overlapping the whole comment", function() {
const dest = [];
text._tc(dest, { c: "foo", p: 6, t: this.t }, { d: "123foo456", p: 3 });
return dest.should.deep.equal([{ c: "", p: 3, t: this.t }]);
});
});
describe "comment / insert", ->
it "should not do anything", ->
dest = []
text._tc(dest, { i: "foo", p: 6 }, { c: "bar", p: 3 })
dest.should.deep.equal [{ i: "foo", p: 6 }]
describe("comment / insert", () => it("should not do anything", function() {
const dest = [];
text._tc(dest, { i: "foo", p: 6 }, { c: "bar", p: 3 });
return dest.should.deep.equal([{ i: "foo", p: 6 }]);
}));
describe "comment / delete", ->
it "should not do anything", ->
dest = []
text._tc(dest, { d: "foo", p: 6 }, { c: "bar", p: 3 })
dest.should.deep.equal [{ d: "foo", p: 6 }]
describe("comment / delete", () => it("should not do anything", function() {
const dest = [];
text._tc(dest, { d: "foo", p: 6 }, { c: "bar", p: 3 });
return dest.should.deep.equal([{ d: "foo", p: 6 }]);
}));
describe "comment / comment", ->
it "should not do anything", ->
dest = []
text._tc(dest, { c: "foo", p: 6 }, { c: "bar", p: 3 })
dest.should.deep.equal [{ c: "foo", p: 6 }]
return describe("comment / comment", () => it("should not do anything", function() {
const dest = [];
text._tc(dest, { c: "foo", p: 6 }, { c: "bar", p: 3 });
return dest.should.deep.equal([{ c: "foo", p: 6 }]);
}));
});
describe "apply", ->
it "should apply an insert", ->
text.apply("foo", [{ i: "bar", p: 2 }]).should.equal "fobaro"
describe("apply", function() {
it("should apply an insert", () => text.apply("foo", [{ i: "bar", p: 2 }]).should.equal("fobaro"));
it "should apply a delete", ->
text.apply("foo123bar", [{ d: "123", p: 3 }]).should.equal "foobar"
it("should apply a delete", () => text.apply("foo123bar", [{ d: "123", p: 3 }]).should.equal("foobar"));
it "should do nothing with a comment", ->
text.apply("foo123bar", [{ c: "123", p: 3 }]).should.equal "foo123bar"
it("should do nothing with a comment", () => text.apply("foo123bar", [{ c: "123", p: 3 }]).should.equal("foo123bar"));
it "should throw an error when deleted content does not match", ->
(() ->
text.apply("foo123bar", [{ d: "456", p: 3 }])
).should.throw(Error)
it("should throw an error when deleted content does not match", () => ((() => text.apply("foo123bar", [{ d: "456", p: 3 }]))).should.throw(Error));
it "should throw an error when comment content does not match", ->
(() ->
text.apply("foo123bar", [{ c: "456", p: 3 }])
).should.throw(Error)
return it("should throw an error when comment content does not match", () => ((() => text.apply("foo123bar", [{ c: "456", p: 3 }]))).should.throw(Error));
});
describe "applying ops and comments in different orders", ->
it "should not matter which op or comment is applied first", ->
transform = (op1, op2, side) ->
d = []
text._tc(d, op1, op2, side)
return d
applySnapshot = (snapshot, op) ->
return text.apply(snapshot, op)
applyRanges = (rangesTracker, ops) ->
for op in ops
rangesTracker.applyOp(op, {})
return rangesTracker
commentsEqual = (comments1, comments2) ->
return false if comments1.length != comments2.length
comments1.sort (a,b) ->
if a.offset - b.offset == 0
return a.length - b.length
else
return a.offset - b.offset
comments2.sort (a,b) ->
if a.offset - b.offset == 0
return a.length - b.length
else
return a.offset - b.offset
for comment1, i in comments1
comment2 = comments2[i]
if comment1.offset != comment2.offset or comment1.length != comment2.length
return false
return true
SNAPSHOT = "123"
OPS = []
# Insert ops
for p in [0..SNAPSHOT.length]
OPS.push {i: "a", p: p}
OPS.push {i: "bc", p: p}
for p in [0..(SNAPSHOT.length-1)]
for length in [1..(SNAPSHOT.length - p)]
OPS.push {d: SNAPSHOT.slice(p, p+length), p}
for p in [0..(SNAPSHOT.length-1)]
for length in [1..(SNAPSHOT.length - p)]
OPS.push {c: SNAPSHOT.slice(p, p+length), p, @t}
return describe("applying ops and comments in different orders", () => it("should not matter which op or comment is applied first", function() {
let length, p;
let asc, end;
let asc1, end1;
let asc3, end3;
const transform = function(op1, op2, side) {
const d = [];
text._tc(d, op1, op2, side);
return d;
};
const applySnapshot = (snapshot, op) => text.apply(snapshot, op);
const applyRanges = function(rangesTracker, ops) {
for (let op of Array.from(ops)) {
rangesTracker.applyOp(op, {});
}
return rangesTracker;
};
const commentsEqual = function(comments1, comments2) {
if (comments1.length !== comments2.length) { return false; }
comments1.sort(function(a,b) {
if ((a.offset - b.offset) === 0) {
return a.length - b.length;
} else {
return a.offset - b.offset;
}
});
comments2.sort(function(a,b) {
if ((a.offset - b.offset) === 0) {
return a.length - b.length;
} else {
return a.offset - b.offset;
}
});
for (let i = 0; i < comments1.length; i++) {
const comment1 = comments1[i];
const comment2 = comments2[i];
if ((comment1.offset !== comment2.offset) || (comment1.length !== comment2.length)) {
return false;
}
}
return true;
};
const SNAPSHOT = "123";
const OPS = [];
// Insert ops
for (p = 0, end = SNAPSHOT.length, asc = 0 <= end; asc ? p <= end : p >= end; asc ? p++ : p--) {
OPS.push({i: "a", p});
OPS.push({i: "bc", p});
}
for (p = 0, end1 = SNAPSHOT.length-1, asc1 = 0 <= end1; asc1 ? p <= end1 : p >= end1; asc1 ? p++ : p--) {
var asc2, end2;
for (length = 1, end2 = SNAPSHOT.length - p, asc2 = 1 <= end2; asc2 ? length <= end2 : length >= end2; asc2 ? length++ : length--) {
OPS.push({d: SNAPSHOT.slice(p, p+length), p});
}
}
for (p = 0, end3 = SNAPSHOT.length-1, asc3 = 0 <= end3; asc3 ? p <= end3 : p >= end3; asc3 ? p++ : p--) {
var asc4, end4;
for (length = 1, end4 = SNAPSHOT.length - p, asc4 = 1 <= end4; asc4 ? length <= end4 : length >= end4; asc4 ? length++ : length--) {
OPS.push({c: SNAPSHOT.slice(p, p+length), p, t: this.t});
}
}
for op1 in OPS
for op2 in OPS
op1_t = transform(op1, op2, "left")
op2_t = transform(op2, op1, "right")
rt12 = new RangesTracker()
snapshot12 = applySnapshot(applySnapshot(SNAPSHOT, [op1]), op2_t)
applyRanges(rt12, [op1])
applyRanges(rt12, op2_t)
rt21 = new RangesTracker()
snapshot21 = applySnapshot(applySnapshot(SNAPSHOT, [op2]), op1_t)
applyRanges(rt21, [op2])
applyRanges(rt21, op1_t)
if snapshot12 != snapshot21
console.error {op1, op2, op1_t, op2_t, snapshot12, snapshot21}, "Ops are not consistent"
throw new Error("OT is inconsistent")
if !commentsEqual(rt12.comments, rt21.comments)
console.log rt12.comments
console.log rt21.comments
console.error {op1, op2, op1_t, op2_t, rt12_comments: rt12.comments, rt21_comments: rt21.comments}, "Comments are not consistent"
throw new Error("OT is inconsistent")
return (() => {
const result = [];
for (var op1 of Array.from(OPS)) {
result.push((() => {
const result1 = [];
for (let op2 of Array.from(OPS)) {
const op1_t = transform(op1, op2, "left");
const op2_t = transform(op2, op1, "right");
const rt12 = new RangesTracker();
const snapshot12 = applySnapshot(applySnapshot(SNAPSHOT, [op1]), op2_t);
applyRanges(rt12, [op1]);
applyRanges(rt12, op2_t);
const rt21 = new RangesTracker();
const snapshot21 = applySnapshot(applySnapshot(SNAPSHOT, [op2]), op1_t);
applyRanges(rt21, [op2]);
applyRanges(rt21, op1_t);
if (snapshot12 !== snapshot21) {
console.error({op1, op2, op1_t, op2_t, snapshot12, snapshot21}, "Ops are not consistent");
throw new Error("OT is inconsistent");
}
if (!commentsEqual(rt12.comments, rt21.comments)) {
console.log(rt12.comments);
console.log(rt21.comments);
console.error({op1, op2, op1_t, op2_t, rt12_comments: rt12.comments, rt21_comments: rt21.comments}, "Comments are not consistent");
throw new Error("OT is inconsistent");
} else {
result1.push(undefined);
}
}
return result1;
})());
}
return result;
})();
}));
});

View file

@ -1,93 +1,129 @@
sinon = require('sinon')
chai = require('chai')
should = chai.should()
expect = chai.expect
modulePath = "../../../../app/js/ShareJsDB.js"
SandboxedModule = require('sandboxed-module')
Errors = require "../../../../app/js/Errors"
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const sinon = require('sinon');
const chai = require('chai');
const should = chai.should();
const {
expect
} = chai;
const modulePath = "../../../../app/js/ShareJsDB.js";
const SandboxedModule = require('sandboxed-module');
const Errors = require("../../../../app/js/Errors");
describe "ShareJsDB", ->
beforeEach ->
@doc_id = "document-id"
@project_id = "project-id"
@doc_key = "#{@project_id}:#{@doc_id}"
@callback = sinon.stub()
@ShareJsDB = SandboxedModule.require modulePath, requires:
"./RedisManager": @RedisManager = {}
describe("ShareJsDB", function() {
beforeEach(function() {
this.doc_id = "document-id";
this.project_id = "project-id";
this.doc_key = `${this.project_id}:${this.doc_id}`;
this.callback = sinon.stub();
this.ShareJsDB = SandboxedModule.require(modulePath, { requires: {
"./RedisManager": (this.RedisManager = {})
}
});
@version = 42
@lines = ["one", "two", "three"]
@db = new @ShareJsDB(@project_id, @doc_id, @lines, @version)
this.version = 42;
this.lines = ["one", "two", "three"];
return this.db = new this.ShareJsDB(this.project_id, this.doc_id, this.lines, this.version);
});
describe "getSnapshot", ->
describe "successfully", ->
beforeEach ->
@db.getSnapshot @doc_key, @callback
describe("getSnapshot", function() {
describe("successfully", function() {
beforeEach(function() {
return this.db.getSnapshot(this.doc_key, this.callback);
});
it "should return the doc lines", ->
@callback.args[0][1].snapshot.should.equal @lines.join("\n")
it("should return the doc lines", function() {
return this.callback.args[0][1].snapshot.should.equal(this.lines.join("\n"));
});
it "should return the doc version", ->
@callback.args[0][1].v.should.equal @version
it("should return the doc version", function() {
return this.callback.args[0][1].v.should.equal(this.version);
});
it "should return the type as text", ->
@callback.args[0][1].type.should.equal "text"
return it("should return the type as text", function() {
return this.callback.args[0][1].type.should.equal("text");
});
});
describe "when the key does not match", ->
beforeEach ->
@db.getSnapshot "bad:key", @callback
return describe("when the key does not match", function() {
beforeEach(function() {
return this.db.getSnapshot("bad:key", this.callback);
});
it "should return the callback with a NotFoundError", ->
@callback.calledWith(new Errors.NotFoundError("not found")).should.equal true
return it("should return the callback with a NotFoundError", function() {
return this.callback.calledWith(new Errors.NotFoundError("not found")).should.equal(true);
});
});
});
describe "getOps", ->
describe "with start == end", ->
beforeEach ->
@start = @end = 42
@db.getOps @doc_key, @start, @end, @callback
describe("getOps", function() {
describe("with start == end", function() {
beforeEach(function() {
this.start = (this.end = 42);
return this.db.getOps(this.doc_key, this.start, this.end, this.callback);
});
it "should return an empty array", ->
@callback.calledWith(null, []).should.equal true
return it("should return an empty array", function() {
return this.callback.calledWith(null, []).should.equal(true);
});
});
describe "with a non empty range", ->
beforeEach ->
@start = 35
@end = 42
@RedisManager.getPreviousDocOps = sinon.stub().callsArgWith(3, null, @ops)
@db.getOps @doc_key, @start, @end, @callback
describe("with a non empty range", function() {
beforeEach(function() {
this.start = 35;
this.end = 42;
this.RedisManager.getPreviousDocOps = sinon.stub().callsArgWith(3, null, this.ops);
return this.db.getOps(this.doc_key, this.start, this.end, this.callback);
});
it "should get the range from redis", ->
@RedisManager.getPreviousDocOps
.calledWith(@doc_id, @start, @end-1)
.should.equal true
it("should get the range from redis", function() {
return this.RedisManager.getPreviousDocOps
.calledWith(this.doc_id, this.start, this.end-1)
.should.equal(true);
});
it "should return the ops", ->
@callback.calledWith(null, @ops).should.equal true
return it("should return the ops", function() {
return this.callback.calledWith(null, this.ops).should.equal(true);
});
});
describe "with no specified end", ->
beforeEach ->
@start = 35
@end = null
@RedisManager.getPreviousDocOps = sinon.stub().callsArgWith(3, null, @ops)
@db.getOps @doc_key, @start, @end, @callback
return describe("with no specified end", function() {
beforeEach(function() {
this.start = 35;
this.end = null;
this.RedisManager.getPreviousDocOps = sinon.stub().callsArgWith(3, null, this.ops);
return this.db.getOps(this.doc_key, this.start, this.end, this.callback);
});
it "should get until the end of the list", ->
@RedisManager.getPreviousDocOps
.calledWith(@doc_id, @start, -1)
.should.equal true
return it("should get until the end of the list", function() {
return this.RedisManager.getPreviousDocOps
.calledWith(this.doc_id, this.start, -1)
.should.equal(true);
});
});
});
describe "writeOps", ->
describe "writing an op", ->
beforeEach ->
@opData =
op: {p: 20, t: "foo"}
meta: {source: "bar"}
v: @version
@db.writeOp @doc_key, @opData, @callback
return describe("writeOps", () => describe("writing an op", function() {
beforeEach(function() {
this.opData = {
op: {p: 20, t: "foo"},
meta: {source: "bar"},
v: this.version
};
return this.db.writeOp(this.doc_key, this.opData, this.callback);
});
it "should write into appliedOps", ->
expect(@db.appliedOps[@doc_key]).to.deep.equal [@opData]
it("should write into appliedOps", function() {
return expect(this.db.appliedOps[this.doc_key]).to.deep.equal([this.opData]);
});
it "should call the callback without an error", ->
@callback.called.should.equal true
(@callback.args[0][0]?).should.equal false
return it("should call the callback without an error", function() {
this.callback.called.should.equal(true);
return (this.callback.args[0][0] != null).should.equal(false);
});
}));
});

View file

@ -1,131 +1,181 @@
sinon = require('sinon')
chai = require('chai')
should = chai.should()
modulePath = "../../../../app/js/ShareJsUpdateManager.js"
SandboxedModule = require('sandboxed-module')
crypto = require('crypto')
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const sinon = require('sinon');
const chai = require('chai');
const should = chai.should();
const modulePath = "../../../../app/js/ShareJsUpdateManager.js";
const SandboxedModule = require('sandboxed-module');
const crypto = require('crypto');
describe "ShareJsUpdateManager", ->
beforeEach ->
@project_id = "project-id-123"
@doc_id = "document-id-123"
@callback = sinon.stub()
@ShareJsUpdateManager = SandboxedModule.require modulePath,
requires:
describe("ShareJsUpdateManager", function() {
beforeEach(function() {
let Model;
this.project_id = "project-id-123";
this.doc_id = "document-id-123";
this.callback = sinon.stub();
return this.ShareJsUpdateManager = SandboxedModule.require(modulePath, {
requires: {
"./sharejs/server/model":
class Model
constructor: (@db) ->
"./ShareJsDB" : @ShareJsDB = { mockDB: true }
"redis-sharelatex" : createClient: () => @rclient = auth:->
"logger-sharelatex": @logger = { log: sinon.stub() }
"./RealTimeRedisManager": @RealTimeRedisManager = {}
"./Metrics": @metrics = { inc: sinon.stub() }
globals:
clearTimeout: @clearTimeout = sinon.stub()
(Model = class Model {
constructor(db) {
this.db = db;
}
}),
"./ShareJsDB" : (this.ShareJsDB = { mockDB: true }),
"redis-sharelatex" : { createClient: () => { return this.rclient = {auth() {}}; }
},
"logger-sharelatex": (this.logger = { log: sinon.stub() }),
"./RealTimeRedisManager": (this.RealTimeRedisManager = {}),
"./Metrics": (this.metrics = { inc: sinon.stub() })
},
globals: {
clearTimeout: (this.clearTimeout = sinon.stub())
}
}
);
});
describe "applyUpdate", ->
beforeEach ->
@lines = ["one", "two"]
@version = 34
@updatedDocLines = ["onefoo", "two"]
content = @updatedDocLines.join("\n")
@hash = crypto.createHash('sha1').update("blob " + content.length + "\x00").update(content, 'utf8').digest('hex')
@update = {p: 4, t: "foo", v:@version, hash:@hash}
@model =
applyOp: sinon.stub().callsArg(2)
getSnapshot: sinon.stub()
db:
describe("applyUpdate", function() {
beforeEach(function() {
this.lines = ["one", "two"];
this.version = 34;
this.updatedDocLines = ["onefoo", "two"];
const content = this.updatedDocLines.join("\n");
this.hash = crypto.createHash('sha1').update("blob " + content.length + "\x00").update(content, 'utf8').digest('hex');
this.update = {p: 4, t: "foo", v:this.version, hash:this.hash};
this.model = {
applyOp: sinon.stub().callsArg(2),
getSnapshot: sinon.stub(),
db: {
appliedOps: {}
@ShareJsUpdateManager.getNewShareJsModel = sinon.stub().returns(@model)
@ShareJsUpdateManager._listenForOps = sinon.stub()
@ShareJsUpdateManager.removeDocFromCache = sinon.stub().callsArg(1)
}
};
this.ShareJsUpdateManager.getNewShareJsModel = sinon.stub().returns(this.model);
this.ShareJsUpdateManager._listenForOps = sinon.stub();
return this.ShareJsUpdateManager.removeDocFromCache = sinon.stub().callsArg(1);
});
describe "successfully", ->
beforeEach (done) ->
@model.getSnapshot.callsArgWith(1, null, {snapshot: @updatedDocLines.join("\n"), v: @version})
@model.db.appliedOps["#{@project_id}:#{@doc_id}"] = @appliedOps = ["mock-ops"]
@ShareJsUpdateManager.applyUpdate @project_id, @doc_id, @update, @lines, @version, (err, docLines, version, appliedOps) =>
@callback(err, docLines, version, appliedOps)
done()
describe("successfully", function() {
beforeEach(function(done) {
this.model.getSnapshot.callsArgWith(1, null, {snapshot: this.updatedDocLines.join("\n"), v: this.version});
this.model.db.appliedOps[`${this.project_id}:${this.doc_id}`] = (this.appliedOps = ["mock-ops"]);
return this.ShareJsUpdateManager.applyUpdate(this.project_id, this.doc_id, this.update, this.lines, this.version, (err, docLines, version, appliedOps) => {
this.callback(err, docLines, version, appliedOps);
return done();
});
});
it "should create a new ShareJs model", ->
@ShareJsUpdateManager.getNewShareJsModel
.calledWith(@project_id, @doc_id, @lines, @version)
.should.equal true
it("should create a new ShareJs model", function() {
return this.ShareJsUpdateManager.getNewShareJsModel
.calledWith(this.project_id, this.doc_id, this.lines, this.version)
.should.equal(true);
});
it "should listen for ops on the model", ->
@ShareJsUpdateManager._listenForOps
.calledWith(@model)
.should.equal true
it("should listen for ops on the model", function() {
return this.ShareJsUpdateManager._listenForOps
.calledWith(this.model)
.should.equal(true);
});
it "should send the update to ShareJs", ->
@model.applyOp
.calledWith("#{@project_id}:#{@doc_id}", @update)
.should.equal true
it("should send the update to ShareJs", function() {
return this.model.applyOp
.calledWith(`${this.project_id}:${this.doc_id}`, this.update)
.should.equal(true);
});
it "should get the updated doc lines", ->
@model.getSnapshot
.calledWith("#{@project_id}:#{@doc_id}")
.should.equal true
it("should get the updated doc lines", function() {
return this.model.getSnapshot
.calledWith(`${this.project_id}:${this.doc_id}`)
.should.equal(true);
});
it "should return the updated doc lines, version and ops", ->
@callback.calledWith(null, @updatedDocLines, @version, @appliedOps).should.equal true
return it("should return the updated doc lines, version and ops", function() {
return this.callback.calledWith(null, this.updatedDocLines, this.version, this.appliedOps).should.equal(true);
});
});
describe "when applyOp fails", ->
beforeEach (done) ->
@error = new Error("Something went wrong")
@model.applyOp = sinon.stub().callsArgWith(2, @error)
@ShareJsUpdateManager.applyUpdate @project_id, @doc_id, @update, @lines, @version, (err, docLines, version) =>
@callback(err, docLines, version)
done()
describe("when applyOp fails", function() {
beforeEach(function(done) {
this.error = new Error("Something went wrong");
this.model.applyOp = sinon.stub().callsArgWith(2, this.error);
return this.ShareJsUpdateManager.applyUpdate(this.project_id, this.doc_id, this.update, this.lines, this.version, (err, docLines, version) => {
this.callback(err, docLines, version);
return done();
});
});
it "should call the callback with the error", ->
@callback.calledWith(@error).should.equal true
return it("should call the callback with the error", function() {
return this.callback.calledWith(this.error).should.equal(true);
});
});
describe "when getSnapshot fails", ->
beforeEach (done) ->
@error = new Error("Something went wrong")
@model.getSnapshot.callsArgWith(1, @error)
@ShareJsUpdateManager.applyUpdate @project_id, @doc_id, @update, @lines, @version, (err, docLines, version) =>
@callback(err, docLines, version)
done()
describe("when getSnapshot fails", function() {
beforeEach(function(done) {
this.error = new Error("Something went wrong");
this.model.getSnapshot.callsArgWith(1, this.error);
return this.ShareJsUpdateManager.applyUpdate(this.project_id, this.doc_id, this.update, this.lines, this.version, (err, docLines, version) => {
this.callback(err, docLines, version);
return done();
});
});
it "should call the callback with the error", ->
@callback.calledWith(@error).should.equal true
return it("should call the callback with the error", function() {
return this.callback.calledWith(this.error).should.equal(true);
});
});
describe "with an invalid hash", ->
beforeEach (done) ->
@error = new Error("invalid hash")
@model.getSnapshot.callsArgWith(1, null, {snapshot: "unexpected content", v: @version})
@model.db.appliedOps["#{@project_id}:#{@doc_id}"] = @appliedOps = ["mock-ops"]
@ShareJsUpdateManager.applyUpdate @project_id, @doc_id, @update, @lines, @version, (err, docLines, version, appliedOps) =>
@callback(err, docLines, version, appliedOps)
done()
return describe("with an invalid hash", function() {
beforeEach(function(done) {
this.error = new Error("invalid hash");
this.model.getSnapshot.callsArgWith(1, null, {snapshot: "unexpected content", v: this.version});
this.model.db.appliedOps[`${this.project_id}:${this.doc_id}`] = (this.appliedOps = ["mock-ops"]);
return this.ShareJsUpdateManager.applyUpdate(this.project_id, this.doc_id, this.update, this.lines, this.version, (err, docLines, version, appliedOps) => {
this.callback(err, docLines, version, appliedOps);
return done();
});
});
it "should call the callback with the error", ->
@callback.calledWith(@error).should.equal true
return it("should call the callback with the error", function() {
return this.callback.calledWith(this.error).should.equal(true);
});
});
});
describe "_listenForOps", ->
beforeEach ->
@model = on: (event, callback) =>
@callback = callback
sinon.spy @model, "on"
@ShareJsUpdateManager._listenForOps(@model)
return describe("_listenForOps", function() {
beforeEach(function() {
this.model = { on: (event, callback) => {
return this.callback = callback;
}
};
sinon.spy(this.model, "on");
return this.ShareJsUpdateManager._listenForOps(this.model);
});
it "should listen to the model for updates", ->
@model.on.calledWith("applyOp")
.should.equal true
it("should listen to the model for updates", function() {
return this.model.on.calledWith("applyOp")
.should.equal(true);
});
describe "the callback", ->
beforeEach ->
@opData =
op: {t: "foo", p: 1}
meta: source: "bar"
@RealTimeRedisManager.sendData = sinon.stub()
@callback("#{@project_id}:#{@doc_id}", @opData)
return describe("the callback", function() {
beforeEach(function() {
this.opData = {
op: {t: "foo", p: 1},
meta: { source: "bar"
}
};
this.RealTimeRedisManager.sendData = sinon.stub();
return this.callback(`${this.project_id}:${this.doc_id}`, this.opData);
});
it "should publish the op to redis", ->
@RealTimeRedisManager.sendData
.calledWith({project_id: @project_id, doc_id: @doc_id, op: @opData})
.should.equal true
return it("should publish the op to redis", function() {
return this.RealTimeRedisManager.sendData
.calledWith({project_id: this.project_id, doc_id: this.doc_id, op: this.opData})
.should.equal(true);
});
});
});
});

View file

@ -1,369 +1,481 @@
sinon = require('sinon')
chai = require('chai')
should = chai.should()
modulePath = "../../../../app/js/UpdateManager.js"
SandboxedModule = require('sandboxed-module')
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS206: Consider reworking classes to avoid initClass
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const sinon = require('sinon');
const chai = require('chai');
const should = chai.should();
const modulePath = "../../../../app/js/UpdateManager.js";
const SandboxedModule = require('sandboxed-module');
describe "UpdateManager", ->
beforeEach ->
@project_id = "project-id-123"
@projectHistoryId = "history-id-123"
@doc_id = "document-id-123"
@callback = sinon.stub()
@UpdateManager = SandboxedModule.require modulePath, requires:
"./LockManager" : @LockManager = {}
"./RedisManager" : @RedisManager = {}
"./RealTimeRedisManager" : @RealTimeRedisManager = {}
"./ShareJsUpdateManager" : @ShareJsUpdateManager = {}
"./HistoryManager" : @HistoryManager = {}
"logger-sharelatex": @logger = { log: sinon.stub() }
"./Metrics": @Metrics =
Timer: class Timer
done: sinon.stub()
"settings-sharelatex": @Settings = {}
"./DocumentManager": @DocumentManager = {}
"./RangesManager": @RangesManager = {}
"./SnapshotManager": @SnapshotManager = {}
"./Profiler": class Profiler
log: sinon.stub().returns { end: sinon.stub() }
end: sinon.stub()
describe("UpdateManager", function() {
beforeEach(function() {
let Profiler, Timer;
this.project_id = "project-id-123";
this.projectHistoryId = "history-id-123";
this.doc_id = "document-id-123";
this.callback = sinon.stub();
return this.UpdateManager = SandboxedModule.require(modulePath, { requires: {
"./LockManager" : (this.LockManager = {}),
"./RedisManager" : (this.RedisManager = {}),
"./RealTimeRedisManager" : (this.RealTimeRedisManager = {}),
"./ShareJsUpdateManager" : (this.ShareJsUpdateManager = {}),
"./HistoryManager" : (this.HistoryManager = {}),
"logger-sharelatex": (this.logger = { log: sinon.stub() }),
"./Metrics": (this.Metrics = {
Timer: (Timer = (function() {
Timer = class Timer {
static initClass() {
this.prototype.done = sinon.stub();
}
};
Timer.initClass();
return Timer;
})())
}),
"settings-sharelatex": (this.Settings = {}),
"./DocumentManager": (this.DocumentManager = {}),
"./RangesManager": (this.RangesManager = {}),
"./SnapshotManager": (this.SnapshotManager = {}),
"./Profiler": (Profiler = (function() {
Profiler = class Profiler {
static initClass() {
this.prototype.log = sinon.stub().returns({ end: sinon.stub() });
this.prototype.end = sinon.stub();
}
};
Profiler.initClass();
return Profiler;
})())
}
}
);
});
describe "processOutstandingUpdates", ->
beforeEach ->
@UpdateManager.fetchAndApplyUpdates = sinon.stub().callsArg(2)
@UpdateManager.processOutstandingUpdates @project_id, @doc_id, @callback
describe("processOutstandingUpdates", function() {
beforeEach(function() {
this.UpdateManager.fetchAndApplyUpdates = sinon.stub().callsArg(2);
return this.UpdateManager.processOutstandingUpdates(this.project_id, this.doc_id, this.callback);
});
it "should apply the updates", ->
@UpdateManager.fetchAndApplyUpdates.calledWith(@project_id, @doc_id).should.equal true
it("should apply the updates", function() {
return this.UpdateManager.fetchAndApplyUpdates.calledWith(this.project_id, this.doc_id).should.equal(true);
});
it "should call the callback", ->
@callback.called.should.equal true
it("should call the callback", function() {
return this.callback.called.should.equal(true);
});
it "should time the execution", ->
@Metrics.Timer::done.called.should.equal true
return it("should time the execution", function() {
return this.Metrics.Timer.prototype.done.called.should.equal(true);
});
});
describe "processOutstandingUpdatesWithLock", ->
describe "when the lock is free", ->
beforeEach ->
@LockManager.tryLock = sinon.stub().callsArgWith(1, null, true, @lockValue = "mock-lock-value")
@LockManager.releaseLock = sinon.stub().callsArg(2)
@UpdateManager.continueProcessingUpdatesWithLock = sinon.stub().callsArg(2)
@UpdateManager.processOutstandingUpdates = sinon.stub().callsArg(2)
describe("processOutstandingUpdatesWithLock", function() {
describe("when the lock is free", function() {
beforeEach(function() {
this.LockManager.tryLock = sinon.stub().callsArgWith(1, null, true, (this.lockValue = "mock-lock-value"));
this.LockManager.releaseLock = sinon.stub().callsArg(2);
this.UpdateManager.continueProcessingUpdatesWithLock = sinon.stub().callsArg(2);
return this.UpdateManager.processOutstandingUpdates = sinon.stub().callsArg(2);
});
describe "successfully", ->
beforeEach ->
@UpdateManager.processOutstandingUpdatesWithLock @project_id, @doc_id, @callback
describe("successfully", function() {
beforeEach(function() {
return this.UpdateManager.processOutstandingUpdatesWithLock(this.project_id, this.doc_id, this.callback);
});
it "should acquire the lock", ->
@LockManager.tryLock.calledWith(@doc_id).should.equal true
it("should acquire the lock", function() {
return this.LockManager.tryLock.calledWith(this.doc_id).should.equal(true);
});
it "should free the lock", ->
@LockManager.releaseLock.calledWith(@doc_id, @lockValue).should.equal true
it("should free the lock", function() {
return this.LockManager.releaseLock.calledWith(this.doc_id, this.lockValue).should.equal(true);
});
it "should process the outstanding updates", ->
@UpdateManager.processOutstandingUpdates.calledWith(@project_id, @doc_id).should.equal true
it("should process the outstanding updates", function() {
return this.UpdateManager.processOutstandingUpdates.calledWith(this.project_id, this.doc_id).should.equal(true);
});
it "should do everything with the lock acquired", ->
@UpdateManager.processOutstandingUpdates.calledAfter(@LockManager.tryLock).should.equal true
@UpdateManager.processOutstandingUpdates.calledBefore(@LockManager.releaseLock).should.equal true
it("should do everything with the lock acquired", function() {
this.UpdateManager.processOutstandingUpdates.calledAfter(this.LockManager.tryLock).should.equal(true);
return this.UpdateManager.processOutstandingUpdates.calledBefore(this.LockManager.releaseLock).should.equal(true);
});
it "should continue processing new updates that may have come in", ->
@UpdateManager.continueProcessingUpdatesWithLock.calledWith(@project_id, @doc_id).should.equal true
it("should continue processing new updates that may have come in", function() {
return this.UpdateManager.continueProcessingUpdatesWithLock.calledWith(this.project_id, this.doc_id).should.equal(true);
});
it "should return the callback", ->
@callback.called.should.equal true
return it("should return the callback", function() {
return this.callback.called.should.equal(true);
});
});
describe "when processOutstandingUpdates returns an error", ->
beforeEach ->
@UpdateManager.processOutstandingUpdates = sinon.stub().callsArgWith(2, @error = new Error("Something went wrong"))
@UpdateManager.processOutstandingUpdatesWithLock @project_id, @doc_id, @callback
return describe("when processOutstandingUpdates returns an error", function() {
beforeEach(function() {
this.UpdateManager.processOutstandingUpdates = sinon.stub().callsArgWith(2, (this.error = new Error("Something went wrong")));
return this.UpdateManager.processOutstandingUpdatesWithLock(this.project_id, this.doc_id, this.callback);
});
it "should free the lock", ->
@LockManager.releaseLock.calledWith(@doc_id, @lockValue).should.equal true
it("should free the lock", function() {
return this.LockManager.releaseLock.calledWith(this.doc_id, this.lockValue).should.equal(true);
});
it "should return the error in the callback", ->
@callback.calledWith(@error).should.equal true
return it("should return the error in the callback", function() {
return this.callback.calledWith(this.error).should.equal(true);
});
});
});
describe "when the lock is taken", ->
beforeEach ->
@LockManager.tryLock = sinon.stub().callsArgWith(1, null, false)
@UpdateManager.processOutstandingUpdates = sinon.stub().callsArg(2)
@UpdateManager.processOutstandingUpdatesWithLock @project_id, @doc_id, @callback
return describe("when the lock is taken", function() {
beforeEach(function() {
this.LockManager.tryLock = sinon.stub().callsArgWith(1, null, false);
this.UpdateManager.processOutstandingUpdates = sinon.stub().callsArg(2);
return this.UpdateManager.processOutstandingUpdatesWithLock(this.project_id, this.doc_id, this.callback);
});
it "should return the callback", ->
@callback.called.should.equal true
it("should return the callback", function() {
return this.callback.called.should.equal(true);
});
it "should not process the updates", ->
@UpdateManager.processOutstandingUpdates.called.should.equal false
return it("should not process the updates", function() {
return this.UpdateManager.processOutstandingUpdates.called.should.equal(false);
});
});
});
describe "continueProcessingUpdatesWithLock", ->
describe "when there are outstanding updates", ->
beforeEach ->
@RealTimeRedisManager.getUpdatesLength = sinon.stub().callsArgWith(1, null, 3)
@UpdateManager.processOutstandingUpdatesWithLock = sinon.stub().callsArg(2)
@UpdateManager.continueProcessingUpdatesWithLock @project_id, @doc_id, @callback
describe("continueProcessingUpdatesWithLock", function() {
describe("when there are outstanding updates", function() {
beforeEach(function() {
this.RealTimeRedisManager.getUpdatesLength = sinon.stub().callsArgWith(1, null, 3);
this.UpdateManager.processOutstandingUpdatesWithLock = sinon.stub().callsArg(2);
return this.UpdateManager.continueProcessingUpdatesWithLock(this.project_id, this.doc_id, this.callback);
});
it "should process the outstanding updates", ->
@UpdateManager.processOutstandingUpdatesWithLock.calledWith(@project_id, @doc_id).should.equal true
it("should process the outstanding updates", function() {
return this.UpdateManager.processOutstandingUpdatesWithLock.calledWith(this.project_id, this.doc_id).should.equal(true);
});
it "should return the callback", ->
@callback.called.should.equal true
return it("should return the callback", function() {
return this.callback.called.should.equal(true);
});
});
describe "when there are no outstanding updates", ->
beforeEach ->
@RealTimeRedisManager.getUpdatesLength = sinon.stub().callsArgWith(1, null, 0)
@UpdateManager.processOutstandingUpdatesWithLock = sinon.stub().callsArg(2)
@UpdateManager.continueProcessingUpdatesWithLock @project_id, @doc_id, @callback
return describe("when there are no outstanding updates", function() {
beforeEach(function() {
this.RealTimeRedisManager.getUpdatesLength = sinon.stub().callsArgWith(1, null, 0);
this.UpdateManager.processOutstandingUpdatesWithLock = sinon.stub().callsArg(2);
return this.UpdateManager.continueProcessingUpdatesWithLock(this.project_id, this.doc_id, this.callback);
});
it "should not try to process the outstanding updates", ->
@UpdateManager.processOutstandingUpdatesWithLock.called.should.equal false
it("should not try to process the outstanding updates", function() {
return this.UpdateManager.processOutstandingUpdatesWithLock.called.should.equal(false);
});
it "should return the callback", ->
@callback.called.should.equal true
return it("should return the callback", function() {
return this.callback.called.should.equal(true);
});
});
});
describe "fetchAndApplyUpdates", ->
describe "with updates", ->
beforeEach ->
@updates = [{p: 1, t: "foo"}]
@updatedDocLines = ["updated", "lines"]
@version = 34
@RealTimeRedisManager.getPendingUpdatesForDoc = sinon.stub().callsArgWith(1, null, @updates)
@UpdateManager.applyUpdate = sinon.stub().callsArgWith(3, null, @updatedDocLines, @version)
@UpdateManager.fetchAndApplyUpdates @project_id, @doc_id, @callback
describe("fetchAndApplyUpdates", function() {
describe("with updates", function() {
beforeEach(function() {
this.updates = [{p: 1, t: "foo"}];
this.updatedDocLines = ["updated", "lines"];
this.version = 34;
this.RealTimeRedisManager.getPendingUpdatesForDoc = sinon.stub().callsArgWith(1, null, this.updates);
this.UpdateManager.applyUpdate = sinon.stub().callsArgWith(3, null, this.updatedDocLines, this.version);
return this.UpdateManager.fetchAndApplyUpdates(this.project_id, this.doc_id, this.callback);
});
it "should get the pending updates", ->
@RealTimeRedisManager.getPendingUpdatesForDoc.calledWith(@doc_id).should.equal true
it("should get the pending updates", function() {
return this.RealTimeRedisManager.getPendingUpdatesForDoc.calledWith(this.doc_id).should.equal(true);
});
it "should apply the updates", ->
for update in @updates
@UpdateManager.applyUpdate
.calledWith(@project_id, @doc_id, update)
.should.equal true
it("should apply the updates", function() {
return Array.from(this.updates).map((update) =>
this.UpdateManager.applyUpdate
.calledWith(this.project_id, this.doc_id, update)
.should.equal(true));
});
it "should call the callback", ->
@callback.called.should.equal true
return it("should call the callback", function() {
return this.callback.called.should.equal(true);
});
});
describe "when there are no updates", ->
beforeEach ->
@updates = []
@RealTimeRedisManager.getPendingUpdatesForDoc = sinon.stub().callsArgWith(1, null, @updates)
@UpdateManager.applyUpdate = sinon.stub()
@RedisManager.setDocument = sinon.stub()
@UpdateManager.fetchAndApplyUpdates @project_id, @doc_id, @callback
return describe("when there are no updates", function() {
beforeEach(function() {
this.updates = [];
this.RealTimeRedisManager.getPendingUpdatesForDoc = sinon.stub().callsArgWith(1, null, this.updates);
this.UpdateManager.applyUpdate = sinon.stub();
this.RedisManager.setDocument = sinon.stub();
return this.UpdateManager.fetchAndApplyUpdates(this.project_id, this.doc_id, this.callback);
});
it "should not call applyUpdate", ->
@UpdateManager.applyUpdate.called.should.equal false
it("should not call applyUpdate", function() {
return this.UpdateManager.applyUpdate.called.should.equal(false);
});
it "should call the callback", ->
@callback.called.should.equal true
return it("should call the callback", function() {
return this.callback.called.should.equal(true);
});
});
});
describe "applyUpdate", ->
beforeEach ->
@updateMeta = { user_id: 'last-author-fake-id' }
@update = {op: [{p: 42, i: "foo"}], meta: @updateMeta}
@updatedDocLines = ["updated", "lines"]
@version = 34
@lines = ["original", "lines"]
@ranges = { entries: "mock", comments: "mock" }
@updated_ranges = { entries: "updated", comments: "updated" }
@appliedOps = [ {v: 42, op: "mock-op-42"}, { v: 45, op: "mock-op-45" }]
@doc_ops_length = sinon.stub()
@project_ops_length = sinon.stub()
@pathname = '/a/b/c.tex'
@DocumentManager.getDoc = sinon.stub().yields(null, @lines, @version, @ranges, @pathname, @projectHistoryId)
@RangesManager.applyUpdate = sinon.stub().yields(null, @updated_ranges, false)
@ShareJsUpdateManager.applyUpdate = sinon.stub().yields(null, @updatedDocLines, @version, @appliedOps)
@RedisManager.updateDocument = sinon.stub().yields(null, @doc_ops_length, @project_ops_length)
@RealTimeRedisManager.sendData = sinon.stub()
@UpdateManager._addProjectHistoryMetadataToOps = sinon.stub()
@HistoryManager.recordAndFlushHistoryOps = sinon.stub().callsArg(5)
describe("applyUpdate", function() {
beforeEach(function() {
this.updateMeta = { user_id: 'last-author-fake-id' };
this.update = {op: [{p: 42, i: "foo"}], meta: this.updateMeta};
this.updatedDocLines = ["updated", "lines"];
this.version = 34;
this.lines = ["original", "lines"];
this.ranges = { entries: "mock", comments: "mock" };
this.updated_ranges = { entries: "updated", comments: "updated" };
this.appliedOps = [ {v: 42, op: "mock-op-42"}, { v: 45, op: "mock-op-45" }];
this.doc_ops_length = sinon.stub();
this.project_ops_length = sinon.stub();
this.pathname = '/a/b/c.tex';
this.DocumentManager.getDoc = sinon.stub().yields(null, this.lines, this.version, this.ranges, this.pathname, this.projectHistoryId);
this.RangesManager.applyUpdate = sinon.stub().yields(null, this.updated_ranges, false);
this.ShareJsUpdateManager.applyUpdate = sinon.stub().yields(null, this.updatedDocLines, this.version, this.appliedOps);
this.RedisManager.updateDocument = sinon.stub().yields(null, this.doc_ops_length, this.project_ops_length);
this.RealTimeRedisManager.sendData = sinon.stub();
this.UpdateManager._addProjectHistoryMetadataToOps = sinon.stub();
return this.HistoryManager.recordAndFlushHistoryOps = sinon.stub().callsArg(5);
});
describe "normally", ->
beforeEach ->
@UpdateManager.applyUpdate @project_id, @doc_id, @update, @callback
describe("normally", function() {
beforeEach(function() {
return this.UpdateManager.applyUpdate(this.project_id, this.doc_id, this.update, this.callback);
});
it "should apply the updates via ShareJS", ->
@ShareJsUpdateManager.applyUpdate
.calledWith(@project_id, @doc_id, @update, @lines, @version)
.should.equal true
it("should apply the updates via ShareJS", function() {
return this.ShareJsUpdateManager.applyUpdate
.calledWith(this.project_id, this.doc_id, this.update, this.lines, this.version)
.should.equal(true);
});
it "should update the ranges", ->
@RangesManager.applyUpdate
.calledWith(@project_id, @doc_id, @ranges, @appliedOps, @updatedDocLines)
.should.equal true
it("should update the ranges", function() {
return this.RangesManager.applyUpdate
.calledWith(this.project_id, this.doc_id, this.ranges, this.appliedOps, this.updatedDocLines)
.should.equal(true);
});
it "should save the document", ->
@RedisManager.updateDocument
.calledWith(@project_id, @doc_id, @updatedDocLines, @version, @appliedOps, @updated_ranges, @updateMeta)
.should.equal true
it("should save the document", function() {
return this.RedisManager.updateDocument
.calledWith(this.project_id, this.doc_id, this.updatedDocLines, this.version, this.appliedOps, this.updated_ranges, this.updateMeta)
.should.equal(true);
});
it "should add metadata to the ops" , ->
@UpdateManager._addProjectHistoryMetadataToOps
.calledWith(@appliedOps, @pathname, @projectHistoryId, @lines)
.should.equal true
it("should add metadata to the ops" , function() {
return this.UpdateManager._addProjectHistoryMetadataToOps
.calledWith(this.appliedOps, this.pathname, this.projectHistoryId, this.lines)
.should.equal(true);
});
it "should push the applied ops into the history queue", ->
@HistoryManager.recordAndFlushHistoryOps
.calledWith(@project_id, @doc_id, @appliedOps, @doc_ops_length, @project_ops_length)
.should.equal true
it("should push the applied ops into the history queue", function() {
return this.HistoryManager.recordAndFlushHistoryOps
.calledWith(this.project_id, this.doc_id, this.appliedOps, this.doc_ops_length, this.project_ops_length)
.should.equal(true);
});
it "should call the callback", ->
@callback.called.should.equal true
return it("should call the callback", function() {
return this.callback.called.should.equal(true);
});
});
describe "with UTF-16 surrogate pairs in the update", ->
beforeEach ->
@update = {op: [{p: 42, i: "\uD835\uDC00"}]}
@UpdateManager.applyUpdate @project_id, @doc_id, @update, @callback
describe("with UTF-16 surrogate pairs in the update", function() {
beforeEach(function() {
this.update = {op: [{p: 42, i: "\uD835\uDC00"}]};
return this.UpdateManager.applyUpdate(this.project_id, this.doc_id, this.update, this.callback);
});
it "should apply the update but with surrogate pairs removed", ->
@ShareJsUpdateManager.applyUpdate
.calledWith(@project_id, @doc_id, @update)
.should.equal true
return it("should apply the update but with surrogate pairs removed", function() {
this.ShareJsUpdateManager.applyUpdate
.calledWith(this.project_id, this.doc_id, this.update)
.should.equal(true);
# \uFFFD is 'replacement character'
@update.op[0].i.should.equal "\uFFFD\uFFFD"
// \uFFFD is 'replacement character'
return this.update.op[0].i.should.equal("\uFFFD\uFFFD");
});
});
describe "with an error", ->
beforeEach ->
@error = new Error("something went wrong")
@ShareJsUpdateManager.applyUpdate = sinon.stub().yields(@error)
@UpdateManager.applyUpdate @project_id, @doc_id, @update, @callback
describe("with an error", function() {
beforeEach(function() {
this.error = new Error("something went wrong");
this.ShareJsUpdateManager.applyUpdate = sinon.stub().yields(this.error);
return this.UpdateManager.applyUpdate(this.project_id, this.doc_id, this.update, this.callback);
});
it "should call RealTimeRedisManager.sendData with the error", ->
@RealTimeRedisManager.sendData
it("should call RealTimeRedisManager.sendData with the error", function() {
return this.RealTimeRedisManager.sendData
.calledWith({
project_id: @project_id,
doc_id: @doc_id,
error: @error.message
project_id: this.project_id,
doc_id: this.doc_id,
error: this.error.message
})
.should.equal true
.should.equal(true);
});
it "should call the callback with the error", ->
@callback.calledWith(@error).should.equal true
return it("should call the callback with the error", function() {
return this.callback.calledWith(this.error).should.equal(true);
});
});
describe "when ranges get collapsed", ->
beforeEach ->
@RangesManager.applyUpdate = sinon.stub().yields(null, @updated_ranges, true)
@SnapshotManager.recordSnapshot = sinon.stub().yields()
@UpdateManager.applyUpdate @project_id, @doc_id, @update, @callback
return describe("when ranges get collapsed", function() {
beforeEach(function() {
this.RangesManager.applyUpdate = sinon.stub().yields(null, this.updated_ranges, true);
this.SnapshotManager.recordSnapshot = sinon.stub().yields();
return this.UpdateManager.applyUpdate(this.project_id, this.doc_id, this.update, this.callback);
});
it "should call SnapshotManager.recordSnapshot", ->
@SnapshotManager.recordSnapshot
return it("should call SnapshotManager.recordSnapshot", function() {
return this.SnapshotManager.recordSnapshot
.calledWith(
@project_id,
@doc_id,
@version,
@pathname,
@lines,
@ranges
this.project_id,
this.doc_id,
this.version,
this.pathname,
this.lines,
this.ranges
)
.should.equal true
.should.equal(true);
});
});
});
describe "_addProjectHistoryMetadataToOps", ->
it "should add projectHistoryId, pathname and doc_length metadata to the ops", ->
lines = [
'some'
'test'
'data'
]
appliedOps = [
{ v: 42, op: [{i: "foo", p: 4}, { i: "bar", p: 6 }] },
{ v: 45, op: [{d: "qux", p: 4}, { i: "bazbaz", p: 14 }] },
{ v: 49, op: [{i: "penguin", p: 18}] }
]
@UpdateManager._addProjectHistoryMetadataToOps(appliedOps, @pathname, @projectHistoryId, lines)
appliedOps.should.deep.equal [{
projectHistoryId: @projectHistoryId
v: 42
op: [{i: "foo", p: 4}, { i: "bar", p: 6 }]
meta:
pathname: @pathname
doc_length: 14
}, {
projectHistoryId: @projectHistoryId
v: 45
op: [{d: "qux", p: 4}, { i: "bazbaz", p: 14 }]
meta:
pathname: @pathname
doc_length: 20 # 14 + 'foo' + 'bar'
}, {
projectHistoryId: @projectHistoryId
v: 49
op: [{i: "penguin", p: 18}]
meta:
pathname: @pathname
doc_length: 23 # 14 - 'qux' + 'bazbaz'
}]
describe("_addProjectHistoryMetadataToOps", () => it("should add projectHistoryId, pathname and doc_length metadata to the ops", function() {
const lines = [
'some',
'test',
'data'
];
const appliedOps = [
{ v: 42, op: [{i: "foo", p: 4}, { i: "bar", p: 6 }] },
{ v: 45, op: [{d: "qux", p: 4}, { i: "bazbaz", p: 14 }] },
{ v: 49, op: [{i: "penguin", p: 18}] }
];
this.UpdateManager._addProjectHistoryMetadataToOps(appliedOps, this.pathname, this.projectHistoryId, lines);
return appliedOps.should.deep.equal([{
projectHistoryId: this.projectHistoryId,
v: 42,
op: [{i: "foo", p: 4}, { i: "bar", p: 6 }],
meta: {
pathname: this.pathname,
doc_length: 14
}
}, {
projectHistoryId: this.projectHistoryId,
v: 45,
op: [{d: "qux", p: 4}, { i: "bazbaz", p: 14 }],
meta: {
pathname: this.pathname,
doc_length: 20
} // 14 + 'foo' + 'bar'
}, {
projectHistoryId: this.projectHistoryId,
v: 49,
op: [{i: "penguin", p: 18}],
meta: {
pathname: this.pathname,
doc_length: 23
} // 14 - 'qux' + 'bazbaz'
}]);
}));
describe "lockUpdatesAndDo", ->
beforeEach ->
@method = sinon.stub().callsArgWith(3, null, @response_arg1)
@callback = sinon.stub()
@arg1 = "argument 1"
@response_arg1 = "response argument 1"
@lockValue = "mock-lock-value"
@LockManager.getLock = sinon.stub().callsArgWith(1, null, @lockValue)
@LockManager.releaseLock = sinon.stub().callsArg(2)
return describe("lockUpdatesAndDo", function() {
beforeEach(function() {
this.method = sinon.stub().callsArgWith(3, null, this.response_arg1);
this.callback = sinon.stub();
this.arg1 = "argument 1";
this.response_arg1 = "response argument 1";
this.lockValue = "mock-lock-value";
this.LockManager.getLock = sinon.stub().callsArgWith(1, null, this.lockValue);
return this.LockManager.releaseLock = sinon.stub().callsArg(2);
});
describe "successfully", ->
beforeEach ->
@UpdateManager.continueProcessingUpdatesWithLock = sinon.stub()
@UpdateManager.processOutstandingUpdates = sinon.stub().callsArg(2)
@UpdateManager.lockUpdatesAndDo @method, @project_id, @doc_id, @arg1, @callback
describe("successfully", function() {
beforeEach(function() {
this.UpdateManager.continueProcessingUpdatesWithLock = sinon.stub();
this.UpdateManager.processOutstandingUpdates = sinon.stub().callsArg(2);
return this.UpdateManager.lockUpdatesAndDo(this.method, this.project_id, this.doc_id, this.arg1, this.callback);
});
it "should lock the doc", ->
@LockManager.getLock
.calledWith(@doc_id)
.should.equal true
it("should lock the doc", function() {
return this.LockManager.getLock
.calledWith(this.doc_id)
.should.equal(true);
});
it "should process any outstanding updates", ->
@UpdateManager.processOutstandingUpdates
.calledWith(@project_id, @doc_id)
.should.equal true
it("should process any outstanding updates", function() {
return this.UpdateManager.processOutstandingUpdates
.calledWith(this.project_id, this.doc_id)
.should.equal(true);
});
it "should call the method", ->
@method
.calledWith(@project_id, @doc_id, @arg1)
.should.equal true
it("should call the method", function() {
return this.method
.calledWith(this.project_id, this.doc_id, this.arg1)
.should.equal(true);
});
it "should return the method response to the callback", ->
@callback
.calledWith(null, @response_arg1)
.should.equal true
it("should return the method response to the callback", function() {
return this.callback
.calledWith(null, this.response_arg1)
.should.equal(true);
});
it "should release the lock", ->
@LockManager.releaseLock
.calledWith(@doc_id, @lockValue)
.should.equal true
it("should release the lock", function() {
return this.LockManager.releaseLock
.calledWith(this.doc_id, this.lockValue)
.should.equal(true);
});
it "should continue processing updates", ->
@UpdateManager.continueProcessingUpdatesWithLock
.calledWith(@project_id, @doc_id)
.should.equal true
return it("should continue processing updates", function() {
return this.UpdateManager.continueProcessingUpdatesWithLock
.calledWith(this.project_id, this.doc_id)
.should.equal(true);
});
});
describe "when processOutstandingUpdates returns an error", ->
beforeEach ->
@UpdateManager.processOutstandingUpdates = sinon.stub().callsArgWith(2, @error = new Error("Something went wrong"))
@UpdateManager.lockUpdatesAndDo @method, @project_id, @doc_id, @arg1, @callback
describe("when processOutstandingUpdates returns an error", function() {
beforeEach(function() {
this.UpdateManager.processOutstandingUpdates = sinon.stub().callsArgWith(2, (this.error = new Error("Something went wrong")));
return this.UpdateManager.lockUpdatesAndDo(this.method, this.project_id, this.doc_id, this.arg1, this.callback);
});
it "should free the lock", ->
@LockManager.releaseLock.calledWith(@doc_id, @lockValue).should.equal true
it("should free the lock", function() {
return this.LockManager.releaseLock.calledWith(this.doc_id, this.lockValue).should.equal(true);
});
it "should return the error in the callback", ->
@callback.calledWith(@error).should.equal true
return it("should return the error in the callback", function() {
return this.callback.calledWith(this.error).should.equal(true);
});
});
describe "when the method returns an error", ->
beforeEach ->
@UpdateManager.processOutstandingUpdates = sinon.stub().callsArg(2)
@method = sinon.stub().callsArgWith(3, @error = new Error("something went wrong"), @response_arg1)
@UpdateManager.lockUpdatesAndDo @method, @project_id, @doc_id, @arg1, @callback
return describe("when the method returns an error", function() {
beforeEach(function() {
this.UpdateManager.processOutstandingUpdates = sinon.stub().callsArg(2);
this.method = sinon.stub().callsArgWith(3, (this.error = new Error("something went wrong")), this.response_arg1);
return this.UpdateManager.lockUpdatesAndDo(this.method, this.project_id, this.doc_id, this.arg1, this.callback);
});
it "should free the lock", ->
@LockManager.releaseLock.calledWith(@doc_id, @lockValue).should.equal true
it("should free the lock", function() {
return this.LockManager.releaseLock.calledWith(this.doc_id, this.lockValue).should.equal(true);
});
it "should return the error in the callback", ->
@callback.calledWith(@error).should.equal true
return it("should return the error in the callback", function() {
return this.callback.calledWith(this.error).should.equal(true);
});
});
});
});