decaffeinate: Convert ApplyingUpdatesToADocTests.coffee and 14 other files to JS

This commit is contained in:
decaffeinate
2020-05-06 12:12:17 +02:00
committed by Tim Alby
parent f46fe5be7f
commit adffde3059
15 changed files with 2477 additions and 1801 deletions

View File

@@ -1,394 +1,499 @@
sinon = require "sinon"
chai = require("chai")
chai.should()
expect = chai.expect
async = require "async"
Settings = require('settings-sharelatex')
rclient_history = require("redis-sharelatex").createClient(Settings.redis.history) # note: this is track changes, not project-history
rclient_project_history = require("redis-sharelatex").createClient(Settings.redis.project_history)
rclient_du = require("redis-sharelatex").createClient(Settings.redis.documentupdater)
Keys = Settings.redis.documentupdater.key_schema
HistoryKeys = Settings.redis.history.key_schema
ProjectHistoryKeys = Settings.redis.project_history.key_schema
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* 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");
chai.should();
const {
expect
} = chai;
const async = require("async");
const Settings = require('settings-sharelatex');
const rclient_history = require("redis-sharelatex").createClient(Settings.redis.history); // note: this is track changes, not project-history
const rclient_project_history = require("redis-sharelatex").createClient(Settings.redis.project_history);
const rclient_du = require("redis-sharelatex").createClient(Settings.redis.documentupdater);
const Keys = Settings.redis.documentupdater.key_schema;
const HistoryKeys = Settings.redis.history.key_schema;
const ProjectHistoryKeys = Settings.redis.project_history.key_schema;
MockTrackChangesApi = require "./helpers/MockTrackChangesApi"
MockWebApi = require "./helpers/MockWebApi"
DocUpdaterClient = require "./helpers/DocUpdaterClient"
DocUpdaterApp = require "./helpers/DocUpdaterApp"
const MockTrackChangesApi = require("./helpers/MockTrackChangesApi");
const MockWebApi = require("./helpers/MockWebApi");
const DocUpdaterClient = require("./helpers/DocUpdaterClient");
const DocUpdaterApp = require("./helpers/DocUpdaterApp");
describe "Applying updates to a doc", ->
before (done) ->
@lines = ["one", "two", "three"]
@version = 42
@update =
doc: @doc_id
describe("Applying updates to a doc", function() {
before(function(done) {
this.lines = ["one", "two", "three"];
this.version = 42;
this.update = {
doc: this.doc_id,
op: [{
i: "one and a half\n"
i: "one and a half\n",
p: 4
}]
v: @version
@result = ["one", "one and a half", "two", "three"]
DocUpdaterApp.ensureRunning(done)
}],
v: this.version
};
this.result = ["one", "one and a half", "two", "three"];
return DocUpdaterApp.ensureRunning(done);
});
describe "when the document is not loaded", ->
before (done) ->
[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
sinon.spy MockWebApi, "getDocument"
@startTime = Date.now()
MockWebApi.insertDoc @project_id, @doc_id, {lines: @lines, version: @version}
DocUpdaterClient.sendUpdate @project_id, @doc_id, @update, (error) ->
throw error if error?
setTimeout done, 200
return null
describe("when the document is not loaded", function() {
before(function(done) {
[this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
sinon.spy(MockWebApi, "getDocument");
this.startTime = Date.now();
MockWebApi.insertDoc(this.project_id, this.doc_id, {lines: this.lines, version: this.version});
DocUpdaterClient.sendUpdate(this.project_id, this.doc_id, this.update, function(error) {
if (error != null) { throw error; }
return setTimeout(done, 200);
});
return null;
});
after ->
MockWebApi.getDocument.restore()
after(() => MockWebApi.getDocument.restore());
it "should load the document from the web API", ->
MockWebApi.getDocument
.calledWith(@project_id, @doc_id)
.should.equal true
it("should load the document from the web API", function() {
return MockWebApi.getDocument
.calledWith(this.project_id, this.doc_id)
.should.equal(true);
});
it "should update the doc", (done) ->
DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, doc) =>
doc.lines.should.deep.equal @result
done()
return null
it("should update the doc", function(done) {
DocUpdaterClient.getDoc(this.project_id, this.doc_id, (error, res, doc) => {
doc.lines.should.deep.equal(this.result);
return done();
});
return null;
});
it "should push the applied updates to the track changes api", (done) ->
rclient_history.lrange HistoryKeys.uncompressedHistoryOps({@doc_id}), 0, -1, (error, updates) =>
throw error if error?
JSON.parse(updates[0]).op.should.deep.equal @update.op
rclient_history.sismember HistoryKeys.docsWithHistoryOps({@project_id}), @doc_id, (error, result) =>
throw error if error?
result.should.equal 1
done()
return null
it("should push the applied updates to the track changes api", function(done) {
rclient_history.lrange(HistoryKeys.uncompressedHistoryOps({doc_id: this.doc_id}), 0, -1, (error, updates) => {
if (error != null) { throw error; }
JSON.parse(updates[0]).op.should.deep.equal(this.update.op);
return rclient_history.sismember(HistoryKeys.docsWithHistoryOps({project_id: this.project_id}), this.doc_id, (error, result) => {
if (error != null) { throw error; }
result.should.equal(1);
return done();
});
});
return null;
});
it "should push the applied updates to the project history changes api", (done) ->
rclient_project_history.lrange ProjectHistoryKeys.projectHistoryOps({@project_id}), 0, -1, (error, updates) =>
throw error if error?
JSON.parse(updates[0]).op.should.deep.equal @update.op
done()
return null
it("should push the applied updates to the project history changes api", function(done) {
rclient_project_history.lrange(ProjectHistoryKeys.projectHistoryOps({project_id: this.project_id}), 0, -1, (error, updates) => {
if (error != null) { throw error; }
JSON.parse(updates[0]).op.should.deep.equal(this.update.op);
return done();
});
return null;
});
it "should set the first op timestamp", (done) ->
rclient_project_history.get ProjectHistoryKeys.projectHistoryFirstOpTimestamp({@project_id}), (error, result) =>
throw error if error?
result.should.be.within(@startTime, Date.now())
@firstOpTimestamp = result
done()
return null
it("should set the first op timestamp", function(done) {
rclient_project_history.get(ProjectHistoryKeys.projectHistoryFirstOpTimestamp({project_id: this.project_id}), (error, result) => {
if (error != null) { throw error; }
result.should.be.within(this.startTime, Date.now());
this.firstOpTimestamp = result;
return done();
});
return null;
});
describe "when sending another update", ->
before (done) ->
@timeout = 10000
@second_update = Object.create(@update)
@second_update.v = @version + 1
DocUpdaterClient.sendUpdate @project_id, @doc_id, @second_update, (error) ->
throw error if error?
setTimeout done, 200
return null
return describe("when sending another update", function() {
before(function(done) {
this.timeout = 10000;
this.second_update = Object.create(this.update);
this.second_update.v = this.version + 1;
DocUpdaterClient.sendUpdate(this.project_id, this.doc_id, this.second_update, function(error) {
if (error != null) { throw error; }
return setTimeout(done, 200);
});
return null;
});
it "should not change the first op timestamp", (done) ->
rclient_project_history.get ProjectHistoryKeys.projectHistoryFirstOpTimestamp({@project_id}), (error, result) =>
throw error if error?
result.should.equal @firstOpTimestamp
done()
return null
return it("should not change the first op timestamp", function(done) {
rclient_project_history.get(ProjectHistoryKeys.projectHistoryFirstOpTimestamp({project_id: this.project_id}), (error, result) => {
if (error != null) { throw error; }
result.should.equal(this.firstOpTimestamp);
return done();
});
return null;
});
});
});
describe "when the document is loaded", ->
before (done) ->
[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
describe("when the document is loaded", function() {
before(function(done) {
[this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
MockWebApi.insertDoc @project_id, @doc_id, {lines: @lines, version: @version}
DocUpdaterClient.preloadDoc @project_id, @doc_id, (error) =>
throw error if error?
sinon.spy MockWebApi, "getDocument"
DocUpdaterClient.sendUpdate @project_id, @doc_id, @update, (error) ->
throw error if error?
setTimeout done, 200
return null
MockWebApi.insertDoc(this.project_id, this.doc_id, {lines: this.lines, version: this.version});
DocUpdaterClient.preloadDoc(this.project_id, this.doc_id, error => {
if (error != null) { throw error; }
sinon.spy(MockWebApi, "getDocument");
return DocUpdaterClient.sendUpdate(this.project_id, this.doc_id, this.update, function(error) {
if (error != null) { throw error; }
return setTimeout(done, 200);
});
});
return null;
});
after ->
MockWebApi.getDocument.restore()
after(() => MockWebApi.getDocument.restore());
it "should not need to call the web api", ->
MockWebApi.getDocument.called.should.equal false
it("should not need to call the web api", () => MockWebApi.getDocument.called.should.equal(false));
it "should update the doc", (done) ->
DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, doc) =>
doc.lines.should.deep.equal @result
done()
return null
it("should update the doc", function(done) {
DocUpdaterClient.getDoc(this.project_id, this.doc_id, (error, res, doc) => {
doc.lines.should.deep.equal(this.result);
return done();
});
return null;
});
it "should push the applied updates to the track changes api", (done) ->
rclient_history.lrange HistoryKeys.uncompressedHistoryOps({@doc_id}), 0, -1, (error, updates) =>
JSON.parse(updates[0]).op.should.deep.equal @update.op
rclient_history.sismember HistoryKeys.docsWithHistoryOps({@project_id}), @doc_id, (error, result) =>
result.should.equal 1
done()
return null
it("should push the applied updates to the track changes api", function(done) {
rclient_history.lrange(HistoryKeys.uncompressedHistoryOps({doc_id: this.doc_id}), 0, -1, (error, updates) => {
JSON.parse(updates[0]).op.should.deep.equal(this.update.op);
return rclient_history.sismember(HistoryKeys.docsWithHistoryOps({project_id: this.project_id}), this.doc_id, (error, result) => {
result.should.equal(1);
return done();
});
});
return null;
});
it "should push the applied updates to the project history changes api", (done) ->
rclient_project_history.lrange ProjectHistoryKeys.projectHistoryOps({@project_id}), 0, -1, (error, updates) =>
JSON.parse(updates[0]).op.should.deep.equal @update.op
done()
return null
return it("should push the applied updates to the project history changes api", function(done) {
rclient_project_history.lrange(ProjectHistoryKeys.projectHistoryOps({project_id: this.project_id}), 0, -1, (error, updates) => {
JSON.parse(updates[0]).op.should.deep.equal(this.update.op);
return done();
});
return null;
});
});
describe "when the document is loaded and is using project-history only", ->
before (done) ->
[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
describe("when the document is loaded and is using project-history only", function() {
before(function(done) {
[this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
MockWebApi.insertDoc @project_id, @doc_id, {lines: @lines, version: @version, projectHistoryType: 'project-history'}
DocUpdaterClient.preloadDoc @project_id, @doc_id, (error) =>
throw error if error?
sinon.spy MockWebApi, "getDocument"
DocUpdaterClient.sendUpdate @project_id, @doc_id, @update, (error) ->
throw error if error?
setTimeout done, 200
return null
MockWebApi.insertDoc(this.project_id, this.doc_id, {lines: this.lines, version: this.version, projectHistoryType: 'project-history'});
DocUpdaterClient.preloadDoc(this.project_id, this.doc_id, error => {
if (error != null) { throw error; }
sinon.spy(MockWebApi, "getDocument");
return DocUpdaterClient.sendUpdate(this.project_id, this.doc_id, this.update, function(error) {
if (error != null) { throw error; }
return setTimeout(done, 200);
});
});
return null;
});
after ->
MockWebApi.getDocument.restore()
after(() => MockWebApi.getDocument.restore());
it "should update the doc", (done) ->
DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, doc) =>
doc.lines.should.deep.equal @result
done()
return null
it("should update the doc", function(done) {
DocUpdaterClient.getDoc(this.project_id, this.doc_id, (error, res, doc) => {
doc.lines.should.deep.equal(this.result);
return done();
});
return null;
});
it "should not push any applied updates to the track changes api", (done) ->
rclient_history.lrange HistoryKeys.uncompressedHistoryOps({@doc_id}), 0, -1, (error, updates) =>
updates.length.should.equal 0
done()
return null
it("should not push any applied updates to the track changes api", function(done) {
rclient_history.lrange(HistoryKeys.uncompressedHistoryOps({doc_id: this.doc_id}), 0, -1, (error, updates) => {
updates.length.should.equal(0);
return done();
});
return null;
});
it "should push the applied updates to the project history changes api", (done) ->
rclient_project_history.lrange ProjectHistoryKeys.projectHistoryOps({@project_id}), 0, -1, (error, updates) =>
JSON.parse(updates[0]).op.should.deep.equal @update.op
done()
return null
return it("should push the applied updates to the project history changes api", function(done) {
rclient_project_history.lrange(ProjectHistoryKeys.projectHistoryOps({project_id: this.project_id}), 0, -1, (error, updates) => {
JSON.parse(updates[0]).op.should.deep.equal(this.update.op);
return done();
});
return null;
});
});
describe "when the document has been deleted", ->
describe "when the ops come in a single linear order", ->
before (done) ->
[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
lines = ["", "", ""]
MockWebApi.insertDoc @project_id, @doc_id, {lines: lines, version: 0}
@updates = [
{ doc_id: @doc_id, v: 0, op: [i: "h", p: 0 ] }
{ doc_id: @doc_id, v: 1, op: [i: "e", p: 1 ] }
{ doc_id: @doc_id, v: 2, op: [i: "l", p: 2 ] }
{ doc_id: @doc_id, v: 3, op: [i: "l", p: 3 ] }
{ doc_id: @doc_id, v: 4, op: [i: "o", p: 4 ] }
{ doc_id: @doc_id, v: 5, op: [i: " ", p: 5 ] }
{ doc_id: @doc_id, v: 6, op: [i: "w", p: 6 ] }
{ doc_id: @doc_id, v: 7, op: [i: "o", p: 7 ] }
{ doc_id: @doc_id, v: 8, op: [i: "r", p: 8 ] }
{ doc_id: @doc_id, v: 9, op: [i: "l", p: 9 ] }
{ doc_id: @doc_id, v: 10, op: [i: "d", p: 10] }
]
@my_result = ["hello world", "", ""]
done()
describe("when the document has been deleted", function() {
describe("when the ops come in a single linear order", function() {
before(function(done) {
[this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
const lines = ["", "", ""];
MockWebApi.insertDoc(this.project_id, this.doc_id, {lines, version: 0});
this.updates = [
{ doc_id: this.doc_id, v: 0, op: [{i: "h", p: 0} ] },
{ doc_id: this.doc_id, v: 1, op: [{i: "e", p: 1} ] },
{ doc_id: this.doc_id, v: 2, op: [{i: "l", p: 2} ] },
{ doc_id: this.doc_id, v: 3, op: [{i: "l", p: 3} ] },
{ doc_id: this.doc_id, v: 4, op: [{i: "o", p: 4} ] },
{ doc_id: this.doc_id, v: 5, op: [{i: " ", p: 5} ] },
{ doc_id: this.doc_id, v: 6, op: [{i: "w", p: 6} ] },
{ doc_id: this.doc_id, v: 7, op: [{i: "o", p: 7} ] },
{ doc_id: this.doc_id, v: 8, op: [{i: "r", p: 8} ] },
{ doc_id: this.doc_id, v: 9, op: [{i: "l", p: 9} ] },
{ doc_id: this.doc_id, v: 10, op: [{i: "d", p: 10}] }
];
this.my_result = ["hello world", "", ""];
return done();
});
it "should be able to continue applying updates when the project has been deleted", (done) ->
actions = []
for update in @updates.slice(0,6)
do (update) =>
actions.push (callback) => DocUpdaterClient.sendUpdate @project_id, @doc_id, update, callback
actions.push (callback) => DocUpdaterClient.deleteDoc @project_id, @doc_id, callback
for update in @updates.slice(6)
do (update) =>
actions.push (callback) => DocUpdaterClient.sendUpdate @project_id, @doc_id, update, callback
it("should be able to continue applying updates when the project has been deleted", function(done) {
let update;
const actions = [];
for (update of Array.from(this.updates.slice(0,6))) {
(update => {
return actions.push(callback => DocUpdaterClient.sendUpdate(this.project_id, this.doc_id, update, callback));
})(update);
}
actions.push(callback => DocUpdaterClient.deleteDoc(this.project_id, this.doc_id, callback));
for (update of Array.from(this.updates.slice(6))) {
(update => {
return actions.push(callback => DocUpdaterClient.sendUpdate(this.project_id, this.doc_id, update, callback));
})(update);
}
async.series actions, (error) =>
throw error if error?
DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, doc) =>
doc.lines.should.deep.equal @my_result
done()
return null
async.series(actions, error => {
if (error != null) { throw error; }
return DocUpdaterClient.getDoc(this.project_id, this.doc_id, (error, res, doc) => {
doc.lines.should.deep.equal(this.my_result);
return done();
});
});
return null;
});
it "should push the applied updates to the track changes api", (done) ->
rclient_history.lrange HistoryKeys.uncompressedHistoryOps({@doc_id}), 0, -1, (error, updates) =>
updates = (JSON.parse(u) for u in updates)
for appliedUpdate, i in @updates
appliedUpdate.op.should.deep.equal updates[i].op
it("should push the applied updates to the track changes api", function(done) {
rclient_history.lrange(HistoryKeys.uncompressedHistoryOps({doc_id: this.doc_id}), 0, -1, (error, updates) => {
updates = (Array.from(updates).map((u) => JSON.parse(u)));
for (let i = 0; i < this.updates.length; i++) {
const appliedUpdate = this.updates[i];
appliedUpdate.op.should.deep.equal(updates[i].op);
}
rclient_history.sismember HistoryKeys.docsWithHistoryOps({@project_id}), @doc_id, (error, result) =>
result.should.equal 1
done()
return null
return rclient_history.sismember(HistoryKeys.docsWithHistoryOps({project_id: this.project_id}), this.doc_id, (error, result) => {
result.should.equal(1);
return done();
});
});
return null;
});
it "should store the doc ops in the correct order", (done) ->
rclient_du.lrange Keys.docOps({doc_id: @doc_id}), 0, -1, (error, updates) =>
updates = (JSON.parse(u) for u in updates)
for appliedUpdate, i in @updates
appliedUpdate.op.should.deep.equal updates[i].op
done()
return null
return it("should store the doc ops in the correct order", function(done) {
rclient_du.lrange(Keys.docOps({doc_id: this.doc_id}), 0, -1, (error, updates) => {
updates = (Array.from(updates).map((u) => JSON.parse(u)));
for (let i = 0; i < this.updates.length; i++) {
const appliedUpdate = this.updates[i];
appliedUpdate.op.should.deep.equal(updates[i].op);
}
return done();
});
return null;
});
});
describe "when older ops come in after the delete", ->
before (done) ->
[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
lines = ["", "", ""]
MockWebApi.insertDoc @project_id, @doc_id, {lines: lines, version: 0}
@updates = [
{ doc_id: @doc_id, v: 0, op: [i: "h", p: 0 ] }
{ doc_id: @doc_id, v: 1, op: [i: "e", p: 1 ] }
{ doc_id: @doc_id, v: 2, op: [i: "l", p: 2 ] }
{ doc_id: @doc_id, v: 3, op: [i: "l", p: 3 ] }
{ doc_id: @doc_id, v: 4, op: [i: "o", p: 4 ] }
{ doc_id: @doc_id, v: 0, op: [i: "world", p: 1 ] }
]
@my_result = ["hello", "world", ""]
done()
return describe("when older ops come in after the delete", function() {
before(function(done) {
[this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
const lines = ["", "", ""];
MockWebApi.insertDoc(this.project_id, this.doc_id, {lines, version: 0});
this.updates = [
{ doc_id: this.doc_id, v: 0, op: [{i: "h", p: 0} ] },
{ doc_id: this.doc_id, v: 1, op: [{i: "e", p: 1} ] },
{ doc_id: this.doc_id, v: 2, op: [{i: "l", p: 2} ] },
{ doc_id: this.doc_id, v: 3, op: [{i: "l", p: 3} ] },
{ doc_id: this.doc_id, v: 4, op: [{i: "o", p: 4} ] },
{ doc_id: this.doc_id, v: 0, op: [{i: "world", p: 1} ] }
];
this.my_result = ["hello", "world", ""];
return done();
});
it "should be able to continue applying updates when the project has been deleted", (done) ->
actions = []
for update in @updates.slice(0,5)
do (update) =>
actions.push (callback) => DocUpdaterClient.sendUpdate @project_id, @doc_id, update, callback
actions.push (callback) => DocUpdaterClient.deleteDoc @project_id, @doc_id, callback
for update in @updates.slice(5)
do (update) =>
actions.push (callback) => DocUpdaterClient.sendUpdate @project_id, @doc_id, update, callback
return it("should be able to continue applying updates when the project has been deleted", function(done) {
let update;
const actions = [];
for (update of Array.from(this.updates.slice(0,5))) {
(update => {
return actions.push(callback => DocUpdaterClient.sendUpdate(this.project_id, this.doc_id, update, callback));
})(update);
}
actions.push(callback => DocUpdaterClient.deleteDoc(this.project_id, this.doc_id, callback));
for (update of Array.from(this.updates.slice(5))) {
(update => {
return actions.push(callback => DocUpdaterClient.sendUpdate(this.project_id, this.doc_id, update, callback));
})(update);
}
async.series actions, (error) =>
throw error if error?
DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, doc) =>
doc.lines.should.deep.equal @my_result
done()
return null
async.series(actions, error => {
if (error != null) { throw error; }
return DocUpdaterClient.getDoc(this.project_id, this.doc_id, (error, res, doc) => {
doc.lines.should.deep.equal(this.my_result);
return done();
});
});
return null;
});
});
});
describe "with a broken update", ->
before (done) ->
[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
@broken_update = { doc_id: @doc_id, v: @version, op: [d: "not the correct content", p: 0 ] }
MockWebApi.insertDoc @project_id, @doc_id, {lines: @lines, version: @version}
describe("with a broken update", function() {
before(function(done) {
[this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
this.broken_update = { doc_id: this.doc_id, v: this.version, op: [{d: "not the correct content", p: 0} ] };
MockWebApi.insertDoc(this.project_id, this.doc_id, {lines: this.lines, version: this.version});
DocUpdaterClient.subscribeToAppliedOps @messageCallback = sinon.stub()
DocUpdaterClient.subscribeToAppliedOps(this.messageCallback = sinon.stub());
DocUpdaterClient.sendUpdate @project_id, @doc_id, @broken_update, (error) ->
throw error if error?
setTimeout done, 200
return null
DocUpdaterClient.sendUpdate(this.project_id, this.doc_id, this.broken_update, function(error) {
if (error != null) { throw error; }
return setTimeout(done, 200);
});
return null;
});
it "should not update the doc", (done) ->
DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, doc) =>
doc.lines.should.deep.equal @lines
done()
return null
it("should not update the doc", function(done) {
DocUpdaterClient.getDoc(this.project_id, this.doc_id, (error, res, doc) => {
doc.lines.should.deep.equal(this.lines);
return done();
});
return null;
});
it "should send a message with an error", ->
@messageCallback.called.should.equal true
[channel, message] = @messageCallback.args[0]
channel.should.equal "applied-ops"
JSON.parse(message).should.deep.include {
project_id: @project_id,
doc_id: @doc_id,
return it("should send a message with an error", function() {
this.messageCallback.called.should.equal(true);
const [channel, message] = Array.from(this.messageCallback.args[0]);
channel.should.equal("applied-ops");
return JSON.parse(message).should.deep.include({
project_id: this.project_id,
doc_id: this.doc_id,
error:'Delete component does not match'
});
});
});
describe("with enough updates to flush to the track changes api", function() {
before(function(done) {
[this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
const updates = [];
for (let v = 0; v <= 199; v++) { // Should flush after 100 ops
updates.push({
doc_id: this.doc_id,
op: [{i: v.toString(), p: 0}],
v
});
}
describe "with enough updates to flush to the track changes api", ->
before (done) ->
[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
updates = []
for v in [0..199] # Should flush after 100 ops
updates.push
doc_id: @doc_id,
op: [i: v.toString(), p: 0]
v: v
sinon.spy(MockTrackChangesApi, "flushDoc");
sinon.spy MockTrackChangesApi, "flushDoc"
MockWebApi.insertDoc(this.project_id, this.doc_id, {lines: this.lines, version: 0});
MockWebApi.insertDoc @project_id, @doc_id, {lines: @lines, version: 0}
# Send updates in chunks to causes multiple flushes
actions = []
for i in [0..19]
do (i) =>
actions.push (cb) =>
DocUpdaterClient.sendUpdates @project_id, @doc_id, updates.slice(i*10, (i+1)*10), cb
async.series actions, (error) =>
throw error if error?
setTimeout done, 2000
return null
after ->
MockTrackChangesApi.flushDoc.restore()
it "should flush the doc twice", ->
MockTrackChangesApi.flushDoc.calledTwice.should.equal true
describe "when there is no version in Mongo", ->
before (done) ->
[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
MockWebApi.insertDoc @project_id, @doc_id, {
lines: @lines
// Send updates in chunks to causes multiple flushes
const actions = [];
for (let i = 0; i <= 19; i++) {
(i => {
return actions.push(cb => {
return DocUpdaterClient.sendUpdates(this.project_id, this.doc_id, updates.slice(i*10, (i+1)*10), cb);
});
})(i);
}
async.series(actions, error => {
if (error != null) { throw error; }
return setTimeout(done, 2000);
});
return null;
});
update =
doc: @doc_id
op: @update.op
after(() => MockTrackChangesApi.flushDoc.restore());
return it("should flush the doc twice", () => MockTrackChangesApi.flushDoc.calledTwice.should.equal(true));
});
describe("when there is no version in Mongo", function() {
before(function(done) {
[this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
MockWebApi.insertDoc(this.project_id, this.doc_id, {
lines: this.lines
});
const update = {
doc: this.doc_id,
op: this.update.op,
v: 0
DocUpdaterClient.sendUpdate @project_id, @doc_id, update, (error) ->
throw error if error?
setTimeout done, 200
return null
};
DocUpdaterClient.sendUpdate(this.project_id, this.doc_id, update, function(error) {
if (error != null) { throw error; }
return setTimeout(done, 200);
});
return null;
});
it "should update the doc (using version = 0)", (done) ->
DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, doc) =>
doc.lines.should.deep.equal @result
done()
return null
return it("should update the doc (using version = 0)", function(done) {
DocUpdaterClient.getDoc(this.project_id, this.doc_id, (error, res, doc) => {
doc.lines.should.deep.equal(this.result);
return done();
});
return null;
});
});
describe "when the sending duplicate ops", ->
before (done) ->
[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
MockWebApi.insertDoc @project_id, @doc_id, {lines: @lines, version: @version}
return describe("when the sending duplicate ops", function() {
before(function(done) {
[this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
MockWebApi.insertDoc(this.project_id, this.doc_id, {lines: this.lines, version: this.version});
DocUpdaterClient.subscribeToAppliedOps @messageCallback = sinon.stub()
DocUpdaterClient.subscribeToAppliedOps(this.messageCallback = sinon.stub());
# One user delete 'one', the next turns it into 'once'. The second becomes a NOP.
DocUpdaterClient.sendUpdate @project_id, @doc_id, {
doc: @doc_id
// One user delete 'one', the next turns it into 'once'. The second becomes a NOP.
DocUpdaterClient.sendUpdate(this.project_id, this.doc_id, {
doc: this.doc_id,
op: [{
i: "one and a half\n"
i: "one and a half\n",
p: 4
}]
v: @version
meta:
}],
v: this.version,
meta: {
source: "ikHceq3yfAdQYzBo4-xZ"
}, (error) =>
throw error if error?
setTimeout () =>
DocUpdaterClient.sendUpdate @project_id, @doc_id, {
doc: @doc_id
}
}, error => {
if (error != null) { throw error; }
return setTimeout(() => {
return DocUpdaterClient.sendUpdate(this.project_id, this.doc_id, {
doc: this.doc_id,
op: [{
i: "one and a half\n"
i: "one and a half\n",
p: 4
}]
v: @version
dupIfSource: ["ikHceq3yfAdQYzBo4-xZ"]
meta:
}],
v: this.version,
dupIfSource: ["ikHceq3yfAdQYzBo4-xZ"],
meta: {
source: "ikHceq3yfAdQYzBo4-xZ"
}, (error) =>
throw error if error?
setTimeout done, 200
, 200
return null
}
}, error => {
if (error != null) { throw error; }
return setTimeout(done, 200);
});
}
, 200);
});
return null;
});
it "should update the doc", (done) ->
DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, doc) =>
doc.lines.should.deep.equal @result
done()
return null
it("should update the doc", function(done) {
DocUpdaterClient.getDoc(this.project_id, this.doc_id, (error, res, doc) => {
doc.lines.should.deep.equal(this.result);
return done();
});
return null;
});
it "should return a message about duplicate ops", ->
@messageCallback.calledTwice.should.equal true
@messageCallback.args[0][0].should.equal "applied-ops"
expect(JSON.parse(@messageCallback.args[0][1]).op.dup).to.be.undefined
@messageCallback.args[1][0].should.equal "applied-ops"
expect(JSON.parse(@messageCallback.args[1][1]).op.dup).to.equal true
return it("should return a message about duplicate ops", function() {
this.messageCallback.calledTwice.should.equal(true);
this.messageCallback.args[0][0].should.equal("applied-ops");
expect(JSON.parse(this.messageCallback.args[0][1]).op.dup).to.be.undefined;
this.messageCallback.args[1][0].should.equal("applied-ops");
return expect(JSON.parse(this.messageCallback.args[1][1]).op.dup).to.equal(true);
});
});
});

View File

@@ -1,300 +1,363 @@
sinon = require "sinon"
chai = require("chai")
chai.should()
Settings = require('settings-sharelatex')
rclient_project_history = require("redis-sharelatex").createClient(Settings.redis.project_history)
ProjectHistoryKeys = Settings.redis.project_history.key_schema
/*
* 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");
chai.should();
const Settings = require('settings-sharelatex');
const rclient_project_history = require("redis-sharelatex").createClient(Settings.redis.project_history);
const ProjectHistoryKeys = Settings.redis.project_history.key_schema;
MockProjectHistoryApi = require "./helpers/MockProjectHistoryApi"
MockWebApi = require "./helpers/MockWebApi"
DocUpdaterClient = require "./helpers/DocUpdaterClient"
DocUpdaterApp = require "./helpers/DocUpdaterApp"
const MockProjectHistoryApi = require("./helpers/MockProjectHistoryApi");
const MockWebApi = require("./helpers/MockWebApi");
const DocUpdaterClient = require("./helpers/DocUpdaterClient");
const DocUpdaterApp = require("./helpers/DocUpdaterApp");
describe "Applying updates to a project's structure", ->
before ->
@user_id = 'user-id-123'
@version = 1234
describe("Applying updates to a project's structure", function() {
before(function() {
this.user_id = 'user-id-123';
return this.version = 1234;
});
describe "renaming a file", ->
before (done) ->
@project_id = DocUpdaterClient.randomId()
@fileUpdate =
id: DocUpdaterClient.randomId()
pathname: '/file-path'
describe("renaming a file", function() {
before(function(done) {
this.project_id = DocUpdaterClient.randomId();
this.fileUpdate = {
id: DocUpdaterClient.randomId(),
pathname: '/file-path',
newPathname: '/new-file-path'
@fileUpdates = [ @fileUpdate ]
DocUpdaterApp.ensureRunning (error) =>
throw error if error?
DocUpdaterClient.sendProjectUpdate @project_id, @user_id, [], @fileUpdates, @version, (error) ->
throw error if error?
setTimeout done, 200
};
this.fileUpdates = [ this.fileUpdate ];
return DocUpdaterApp.ensureRunning(error => {
if (error != null) { throw error; }
return DocUpdaterClient.sendProjectUpdate(this.project_id, this.user_id, [], this.fileUpdates, this.version, function(error) {
if (error != null) { throw error; }
return setTimeout(done, 200);
});
});
});
it "should push the applied file renames to the project history api", (done) ->
rclient_project_history.lrange ProjectHistoryKeys.projectHistoryOps({@project_id}), 0, -1, (error, updates) =>
throw error if error?
return it("should push the applied file renames to the project history api", function(done) {
rclient_project_history.lrange(ProjectHistoryKeys.projectHistoryOps({project_id: this.project_id}), 0, -1, (error, updates) => {
if (error != null) { throw error; }
update = JSON.parse(updates[0])
update.file.should.equal @fileUpdate.id
update.pathname.should.equal '/file-path'
update.new_pathname.should.equal '/new-file-path'
update.meta.user_id.should.equal @user_id
update.meta.ts.should.be.a('string')
update.version.should.equal "#{@version}.0"
const update = JSON.parse(updates[0]);
update.file.should.equal(this.fileUpdate.id);
update.pathname.should.equal('/file-path');
update.new_pathname.should.equal('/new-file-path');
update.meta.user_id.should.equal(this.user_id);
update.meta.ts.should.be.a('string');
update.version.should.equal(`${this.version}.0`);
done()
return null
return done();
});
return null;
});
});
describe "renaming a document", ->
before ->
@docUpdate =
id: DocUpdaterClient.randomId()
pathname: '/doc-path'
describe("renaming a document", function() {
before(function() {
this.docUpdate = {
id: DocUpdaterClient.randomId(),
pathname: '/doc-path',
newPathname: '/new-doc-path'
@docUpdates = [ @docUpdate ]
};
return this.docUpdates = [ this.docUpdate ];});
describe "when the document is not loaded", ->
before (done) ->
@project_id = DocUpdaterClient.randomId()
DocUpdaterClient.sendProjectUpdate @project_id, @user_id, @docUpdates, [], @version, (error) ->
throw error if error?
setTimeout done, 200
return null
describe("when the document is not loaded", function() {
before(function(done) {
this.project_id = DocUpdaterClient.randomId();
DocUpdaterClient.sendProjectUpdate(this.project_id, this.user_id, this.docUpdates, [], this.version, function(error) {
if (error != null) { throw error; }
return setTimeout(done, 200);
});
return null;
});
it "should push the applied doc renames to the project history api", (done) ->
rclient_project_history.lrange ProjectHistoryKeys.projectHistoryOps({@project_id}), 0, -1, (error, updates) =>
throw error if error?
return it("should push the applied doc renames to the project history api", function(done) {
rclient_project_history.lrange(ProjectHistoryKeys.projectHistoryOps({project_id: this.project_id}), 0, -1, (error, updates) => {
if (error != null) { throw error; }
update = JSON.parse(updates[0])
update.doc.should.equal @docUpdate.id
update.pathname.should.equal '/doc-path'
update.new_pathname.should.equal '/new-doc-path'
update.meta.user_id.should.equal @user_id
update.meta.ts.should.be.a('string')
update.version.should.equal "#{@version}.0"
const update = JSON.parse(updates[0]);
update.doc.should.equal(this.docUpdate.id);
update.pathname.should.equal('/doc-path');
update.new_pathname.should.equal('/new-doc-path');
update.meta.user_id.should.equal(this.user_id);
update.meta.ts.should.be.a('string');
update.version.should.equal(`${this.version}.0`);
done()
return null
return done();
});
return null;
});
});
describe "when the document is loaded", ->
before (done) ->
@project_id = DocUpdaterClient.randomId()
MockWebApi.insertDoc @project_id, @docUpdate.id, {}
DocUpdaterClient.preloadDoc @project_id, @docUpdate.id, (error) =>
throw error if error?
sinon.spy MockWebApi, "getDocument"
DocUpdaterClient.sendProjectUpdate @project_id, @user_id, @docUpdates, [], @version, (error) ->
throw error if error?
setTimeout done, 200
return null
return describe("when the document is loaded", function() {
before(function(done) {
this.project_id = DocUpdaterClient.randomId();
MockWebApi.insertDoc(this.project_id, this.docUpdate.id, {});
DocUpdaterClient.preloadDoc(this.project_id, this.docUpdate.id, error => {
if (error != null) { throw error; }
sinon.spy(MockWebApi, "getDocument");
return DocUpdaterClient.sendProjectUpdate(this.project_id, this.user_id, this.docUpdates, [], this.version, function(error) {
if (error != null) { throw error; }
return setTimeout(done, 200);
});
});
return null;
});
after ->
MockWebApi.getDocument.restore()
after(() => MockWebApi.getDocument.restore());
it "should update the doc", (done) ->
DocUpdaterClient.getDoc @project_id, @docUpdate.id, (error, res, doc) =>
doc.pathname.should.equal @docUpdate.newPathname
done()
return null
it("should update the doc", function(done) {
DocUpdaterClient.getDoc(this.project_id, this.docUpdate.id, (error, res, doc) => {
doc.pathname.should.equal(this.docUpdate.newPathname);
return done();
});
return null;
});
it "should push the applied doc renames to the project history api", (done) ->
rclient_project_history.lrange ProjectHistoryKeys.projectHistoryOps({@project_id}), 0, -1, (error, updates) =>
throw error if error?
return it("should push the applied doc renames to the project history api", function(done) {
rclient_project_history.lrange(ProjectHistoryKeys.projectHistoryOps({project_id: this.project_id}), 0, -1, (error, updates) => {
if (error != null) { throw error; }
update = JSON.parse(updates[0])
update.doc.should.equal @docUpdate.id
update.pathname.should.equal '/doc-path'
update.new_pathname.should.equal '/new-doc-path'
update.meta.user_id.should.equal @user_id
update.meta.ts.should.be.a('string')
update.version.should.equal "#{@version}.0"
const update = JSON.parse(updates[0]);
update.doc.should.equal(this.docUpdate.id);
update.pathname.should.equal('/doc-path');
update.new_pathname.should.equal('/new-doc-path');
update.meta.user_id.should.equal(this.user_id);
update.meta.ts.should.be.a('string');
update.version.should.equal(`${this.version}.0`);
done()
return null
return done();
});
return null;
});
});
});
describe "renaming multiple documents and files", ->
before ->
@docUpdate0 =
id: DocUpdaterClient.randomId()
pathname: '/doc-path0'
describe("renaming multiple documents and files", function() {
before(function() {
this.docUpdate0 = {
id: DocUpdaterClient.randomId(),
pathname: '/doc-path0',
newPathname: '/new-doc-path0'
@docUpdate1 =
id: DocUpdaterClient.randomId()
pathname: '/doc-path1'
};
this.docUpdate1 = {
id: DocUpdaterClient.randomId(),
pathname: '/doc-path1',
newPathname: '/new-doc-path1'
@docUpdates = [ @docUpdate0, @docUpdate1 ]
@fileUpdate0 =
id: DocUpdaterClient.randomId()
pathname: '/file-path0'
};
this.docUpdates = [ this.docUpdate0, this.docUpdate1 ];
this.fileUpdate0 = {
id: DocUpdaterClient.randomId(),
pathname: '/file-path0',
newPathname: '/new-file-path0'
@fileUpdate1 =
id: DocUpdaterClient.randomId()
pathname: '/file-path1'
};
this.fileUpdate1 = {
id: DocUpdaterClient.randomId(),
pathname: '/file-path1',
newPathname: '/new-file-path1'
@fileUpdates = [ @fileUpdate0, @fileUpdate1 ]
};
return this.fileUpdates = [ this.fileUpdate0, this.fileUpdate1 ];});
describe "when the documents are not loaded", ->
before (done) ->
@project_id = DocUpdaterClient.randomId()
DocUpdaterClient.sendProjectUpdate @project_id, @user_id, @docUpdates, @fileUpdates, @version, (error) ->
throw error if error?
setTimeout done, 200
return null
return describe("when the documents are not loaded", function() {
before(function(done) {
this.project_id = DocUpdaterClient.randomId();
DocUpdaterClient.sendProjectUpdate(this.project_id, this.user_id, this.docUpdates, this.fileUpdates, this.version, function(error) {
if (error != null) { throw error; }
return setTimeout(done, 200);
});
return null;
});
it "should push the applied doc renames to the project history api", (done) ->
rclient_project_history.lrange ProjectHistoryKeys.projectHistoryOps({@project_id}), 0, -1, (error, updates) =>
throw error if error?
return it("should push the applied doc renames to the project history api", function(done) {
rclient_project_history.lrange(ProjectHistoryKeys.projectHistoryOps({project_id: this.project_id}), 0, -1, (error, updates) => {
if (error != null) { throw error; }
update = JSON.parse(updates[0])
update.doc.should.equal @docUpdate0.id
update.pathname.should.equal '/doc-path0'
update.new_pathname.should.equal '/new-doc-path0'
update.meta.user_id.should.equal @user_id
update.meta.ts.should.be.a('string')
update.version.should.equal "#{@version}.0"
let update = JSON.parse(updates[0]);
update.doc.should.equal(this.docUpdate0.id);
update.pathname.should.equal('/doc-path0');
update.new_pathname.should.equal('/new-doc-path0');
update.meta.user_id.should.equal(this.user_id);
update.meta.ts.should.be.a('string');
update.version.should.equal(`${this.version}.0`);
update = JSON.parse(updates[1])
update.doc.should.equal @docUpdate1.id
update.pathname.should.equal '/doc-path1'
update.new_pathname.should.equal '/new-doc-path1'
update.meta.user_id.should.equal @user_id
update.meta.ts.should.be.a('string')
update.version.should.equal "#{@version}.1"
update = JSON.parse(updates[1]);
update.doc.should.equal(this.docUpdate1.id);
update.pathname.should.equal('/doc-path1');
update.new_pathname.should.equal('/new-doc-path1');
update.meta.user_id.should.equal(this.user_id);
update.meta.ts.should.be.a('string');
update.version.should.equal(`${this.version}.1`);
update = JSON.parse(updates[2])
update.file.should.equal @fileUpdate0.id
update.pathname.should.equal '/file-path0'
update.new_pathname.should.equal '/new-file-path0'
update.meta.user_id.should.equal @user_id
update.meta.ts.should.be.a('string')
update.version.should.equal "#{@version}.2"
update = JSON.parse(updates[2]);
update.file.should.equal(this.fileUpdate0.id);
update.pathname.should.equal('/file-path0');
update.new_pathname.should.equal('/new-file-path0');
update.meta.user_id.should.equal(this.user_id);
update.meta.ts.should.be.a('string');
update.version.should.equal(`${this.version}.2`);
update = JSON.parse(updates[3])
update.file.should.equal @fileUpdate1.id
update.pathname.should.equal '/file-path1'
update.new_pathname.should.equal '/new-file-path1'
update.meta.user_id.should.equal @user_id
update.meta.ts.should.be.a('string')
update.version.should.equal "#{@version}.3"
update = JSON.parse(updates[3]);
update.file.should.equal(this.fileUpdate1.id);
update.pathname.should.equal('/file-path1');
update.new_pathname.should.equal('/new-file-path1');
update.meta.user_id.should.equal(this.user_id);
update.meta.ts.should.be.a('string');
update.version.should.equal(`${this.version}.3`);
done()
return null
return done();
});
return null;
});
});
});
describe "adding a file", ->
before (done) ->
@project_id = DocUpdaterClient.randomId()
@fileUpdate =
id: DocUpdaterClient.randomId()
pathname: '/file-path'
describe("adding a file", function() {
before(function(done) {
this.project_id = DocUpdaterClient.randomId();
this.fileUpdate = {
id: DocUpdaterClient.randomId(),
pathname: '/file-path',
url: 'filestore.example.com'
@fileUpdates = [ @fileUpdate ]
DocUpdaterClient.sendProjectUpdate @project_id, @user_id, [], @fileUpdates, @version, (error) ->
throw error if error?
setTimeout done, 200
return null
};
this.fileUpdates = [ this.fileUpdate ];
DocUpdaterClient.sendProjectUpdate(this.project_id, this.user_id, [], this.fileUpdates, this.version, function(error) {
if (error != null) { throw error; }
return setTimeout(done, 200);
});
return null;
});
it "should push the file addition to the project history api", (done) ->
rclient_project_history.lrange ProjectHistoryKeys.projectHistoryOps({@project_id}), 0, -1, (error, updates) =>
throw error if error?
return it("should push the file addition to the project history api", function(done) {
rclient_project_history.lrange(ProjectHistoryKeys.projectHistoryOps({project_id: this.project_id}), 0, -1, (error, updates) => {
if (error != null) { throw error; }
update = JSON.parse(updates[0])
update.file.should.equal @fileUpdate.id
update.pathname.should.equal '/file-path'
update.url.should.equal 'filestore.example.com'
update.meta.user_id.should.equal @user_id
update.meta.ts.should.be.a('string')
update.version.should.equal "#{@version}.0"
const update = JSON.parse(updates[0]);
update.file.should.equal(this.fileUpdate.id);
update.pathname.should.equal('/file-path');
update.url.should.equal('filestore.example.com');
update.meta.user_id.should.equal(this.user_id);
update.meta.ts.should.be.a('string');
update.version.should.equal(`${this.version}.0`);
done()
return null
return done();
});
return null;
});
});
describe "adding a doc", ->
before (done) ->
@project_id = DocUpdaterClient.randomId()
@docUpdate =
id: DocUpdaterClient.randomId()
pathname: '/file-path'
describe("adding a doc", function() {
before(function(done) {
this.project_id = DocUpdaterClient.randomId();
this.docUpdate = {
id: DocUpdaterClient.randomId(),
pathname: '/file-path',
docLines: 'a\nb'
@docUpdates = [ @docUpdate ]
DocUpdaterClient.sendProjectUpdate @project_id, @user_id, @docUpdates, [], @version, (error) ->
throw error if error?
setTimeout done, 200
return null
};
this.docUpdates = [ this.docUpdate ];
DocUpdaterClient.sendProjectUpdate(this.project_id, this.user_id, this.docUpdates, [], this.version, function(error) {
if (error != null) { throw error; }
return setTimeout(done, 200);
});
return null;
});
it "should push the doc addition to the project history api", (done) ->
rclient_project_history.lrange ProjectHistoryKeys.projectHistoryOps({@project_id}), 0, -1, (error, updates) =>
throw error if error?
return it("should push the doc addition to the project history api", function(done) {
rclient_project_history.lrange(ProjectHistoryKeys.projectHistoryOps({project_id: this.project_id}), 0, -1, (error, updates) => {
if (error != null) { throw error; }
update = JSON.parse(updates[0])
update.doc.should.equal @docUpdate.id
update.pathname.should.equal '/file-path'
update.docLines.should.equal 'a\nb'
update.meta.user_id.should.equal @user_id
update.meta.ts.should.be.a('string')
update.version.should.equal "#{@version}.0"
const update = JSON.parse(updates[0]);
update.doc.should.equal(this.docUpdate.id);
update.pathname.should.equal('/file-path');
update.docLines.should.equal('a\nb');
update.meta.user_id.should.equal(this.user_id);
update.meta.ts.should.be.a('string');
update.version.should.equal(`${this.version}.0`);
done()
return null
return done();
});
return null;
});
});
describe "with enough updates to flush to the history service", ->
before (done) ->
@project_id = DocUpdaterClient.randomId()
@user_id = DocUpdaterClient.randomId()
@version0 = 12345
@version1 = @version0 + 1
updates = []
for v in [0..599] # Should flush after 500 ops
updates.push
describe("with enough updates to flush to the history service", function() {
before(function(done) {
this.project_id = DocUpdaterClient.randomId();
this.user_id = DocUpdaterClient.randomId();
this.version0 = 12345;
this.version1 = this.version0 + 1;
const updates = [];
for (let v = 0; v <= 599; v++) { // Should flush after 500 ops
updates.push({
id: DocUpdaterClient.randomId(),
pathname: '/file-' + v
pathname: '/file-' + v,
docLines: 'a\nb'
});
}
sinon.spy MockProjectHistoryApi, "flushProject"
sinon.spy(MockProjectHistoryApi, "flushProject");
# Send updates in chunks to causes multiple flushes
projectId = @project_id
userId = @project_id
DocUpdaterClient.sendProjectUpdate projectId, userId, updates.slice(0, 250), [], @version0, (error) ->
throw error if error?
DocUpdaterClient.sendProjectUpdate projectId, userId, updates.slice(250), [], @version1, (error) ->
throw error if error?
setTimeout done, 2000
return null
// Send updates in chunks to causes multiple flushes
const projectId = this.project_id;
const userId = this.project_id;
DocUpdaterClient.sendProjectUpdate(projectId, userId, updates.slice(0, 250), [], this.version0, function(error) {
if (error != null) { throw error; }
return DocUpdaterClient.sendProjectUpdate(projectId, userId, updates.slice(250), [], this.version1, function(error) {
if (error != null) { throw error; }
return setTimeout(done, 2000);
});
});
return null;
});
after ->
MockProjectHistoryApi.flushProject.restore()
after(() => MockProjectHistoryApi.flushProject.restore());
it "should flush project history", ->
MockProjectHistoryApi.flushProject.calledWith(@project_id).should.equal true
return it("should flush project history", function() {
return MockProjectHistoryApi.flushProject.calledWith(this.project_id).should.equal(true);
});
});
describe "with too few updates to flush to the history service", ->
before (done) ->
@project_id = DocUpdaterClient.randomId()
@user_id = DocUpdaterClient.randomId()
@version0 = 12345
@version1 = @version0 + 1
return describe("with too few updates to flush to the history service", function() {
before(function(done) {
this.project_id = DocUpdaterClient.randomId();
this.user_id = DocUpdaterClient.randomId();
this.version0 = 12345;
this.version1 = this.version0 + 1;
updates = []
for v in [0..42] # Should flush after 500 ops
updates.push
const updates = [];
for (let v = 0; v <= 42; v++) { // Should flush after 500 ops
updates.push({
id: DocUpdaterClient.randomId(),
pathname: '/file-' + v
pathname: '/file-' + v,
docLines: 'a\nb'
});
}
sinon.spy MockProjectHistoryApi, "flushProject"
sinon.spy(MockProjectHistoryApi, "flushProject");
# Send updates in chunks
projectId = @project_id
userId = @project_id
DocUpdaterClient.sendProjectUpdate projectId, userId, updates.slice(0, 10), [], @version0, (error) ->
throw error if error?
DocUpdaterClient.sendProjectUpdate projectId, userId, updates.slice(10), [], @version1, (error) ->
throw error if error?
setTimeout done, 2000
return null
// Send updates in chunks
const projectId = this.project_id;
const userId = this.project_id;
DocUpdaterClient.sendProjectUpdate(projectId, userId, updates.slice(0, 10), [], this.version0, function(error) {
if (error != null) { throw error; }
return DocUpdaterClient.sendProjectUpdate(projectId, userId, updates.slice(10), [], this.version1, function(error) {
if (error != null) { throw error; }
return setTimeout(done, 2000);
});
});
return null;
});
after ->
MockProjectHistoryApi.flushProject.restore()
after(() => MockProjectHistoryApi.flushProject.restore());
it "should not flush project history", ->
MockProjectHistoryApi.flushProject.calledWith(@project_id).should.equal false
return it("should not flush project history", function() {
return MockProjectHistoryApi.flushProject.calledWith(this.project_id).should.equal(false);
});
});
});

View File

@@ -1,109 +1,141 @@
sinon = require "sinon"
chai = require("chai")
chai.should()
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* 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");
chai.should();
MockTrackChangesApi = require "./helpers/MockTrackChangesApi"
MockProjectHistoryApi = require "./helpers/MockProjectHistoryApi"
MockWebApi = require "./helpers/MockWebApi"
DocUpdaterClient = require "./helpers/DocUpdaterClient"
DocUpdaterApp = require "./helpers/DocUpdaterApp"
const MockTrackChangesApi = require("./helpers/MockTrackChangesApi");
const MockProjectHistoryApi = require("./helpers/MockProjectHistoryApi");
const MockWebApi = require("./helpers/MockWebApi");
const DocUpdaterClient = require("./helpers/DocUpdaterClient");
const DocUpdaterApp = require("./helpers/DocUpdaterApp");
describe "Deleting a document", ->
before (done) ->
@lines = ["one", "two", "three"]
@version = 42
@update =
doc: @doc_id
describe("Deleting a document", function() {
before(function(done) {
this.lines = ["one", "two", "three"];
this.version = 42;
this.update = {
doc: this.doc_id,
op: [{
i: "one and a half\n"
i: "one and a half\n",
p: 4
}]
v: @version
@result = ["one", "one and a half", "two", "three"]
}],
v: this.version
};
this.result = ["one", "one and a half", "two", "three"];
sinon.spy MockTrackChangesApi, "flushDoc"
sinon.spy MockProjectHistoryApi, "flushProject"
DocUpdaterApp.ensureRunning(done)
sinon.spy(MockTrackChangesApi, "flushDoc");
sinon.spy(MockProjectHistoryApi, "flushProject");
return DocUpdaterApp.ensureRunning(done);
});
after ->
MockTrackChangesApi.flushDoc.restore()
MockProjectHistoryApi.flushProject.restore()
after(function() {
MockTrackChangesApi.flushDoc.restore();
return MockProjectHistoryApi.flushProject.restore();
});
describe "when the updated doc exists in the doc updater", ->
before (done) ->
[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
sinon.spy MockWebApi, "setDocument"
sinon.spy MockWebApi, "getDocument"
describe("when the updated doc exists in the doc updater", function() {
before(function(done) {
[this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
sinon.spy(MockWebApi, "setDocument");
sinon.spy(MockWebApi, "getDocument");
MockWebApi.insertDoc @project_id, @doc_id, {lines: @lines, version: @version}
DocUpdaterClient.preloadDoc @project_id, @doc_id, (error) =>
throw error if error?
DocUpdaterClient.sendUpdate @project_id, @doc_id, @update, (error) =>
throw error if error?
setTimeout () =>
DocUpdaterClient.deleteDoc @project_id, @doc_id, (error, res, body) =>
@statusCode = res.statusCode
setTimeout done, 200
, 200
MockWebApi.insertDoc(this.project_id, this.doc_id, {lines: this.lines, version: this.version});
return DocUpdaterClient.preloadDoc(this.project_id, this.doc_id, error => {
if (error != null) { throw error; }
return DocUpdaterClient.sendUpdate(this.project_id, this.doc_id, this.update, error => {
if (error != null) { throw error; }
return setTimeout(() => {
return DocUpdaterClient.deleteDoc(this.project_id, this.doc_id, (error, res, body) => {
this.statusCode = res.statusCode;
return setTimeout(done, 200);
});
}
, 200);
});
});
});
after ->
MockWebApi.setDocument.restore()
MockWebApi.getDocument.restore()
after(function() {
MockWebApi.setDocument.restore();
return MockWebApi.getDocument.restore();
});
it "should return a 204 status code", ->
@statusCode.should.equal 204
it("should return a 204 status code", function() {
return this.statusCode.should.equal(204);
});
it "should send the updated document and version to the web api", ->
MockWebApi.setDocument
.calledWith(@project_id, @doc_id, @result, @version + 1)
.should.equal true
it("should send the updated document and version to the web api", function() {
return MockWebApi.setDocument
.calledWith(this.project_id, this.doc_id, this.result, this.version + 1)
.should.equal(true);
});
it "should need to reload the doc if read again", (done) ->
MockWebApi.getDocument.called.should.equal.false
DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, doc) =>
it("should need to reload the doc if read again", function(done) {
MockWebApi.getDocument.called.should.equal.false;
return DocUpdaterClient.getDoc(this.project_id, this.doc_id, (error, res, doc) => {
MockWebApi.getDocument
.calledWith(@project_id, @doc_id)
.should.equal true
done()
.calledWith(this.project_id, this.doc_id)
.should.equal(true);
return done();
});
});
it "should flush track changes", ->
MockTrackChangesApi.flushDoc.calledWith(@doc_id).should.equal true
it("should flush track changes", function() {
return MockTrackChangesApi.flushDoc.calledWith(this.doc_id).should.equal(true);
});
it "should flush project history", ->
MockProjectHistoryApi.flushProject.calledWith(@project_id).should.equal true
return it("should flush project history", function() {
return MockProjectHistoryApi.flushProject.calledWith(this.project_id).should.equal(true);
});
});
describe "when the doc is not in the doc updater", ->
before (done) ->
[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
MockWebApi.insertDoc @project_id, @doc_id, {
lines: @lines
}
sinon.spy MockWebApi, "setDocument"
sinon.spy MockWebApi, "getDocument"
DocUpdaterClient.deleteDoc @project_id, @doc_id, (error, res, body) =>
@statusCode = res.statusCode
setTimeout done, 200
return describe("when the doc is not in the doc updater", function() {
before(function(done) {
[this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
MockWebApi.insertDoc(this.project_id, this.doc_id, {
lines: this.lines
});
sinon.spy(MockWebApi, "setDocument");
sinon.spy(MockWebApi, "getDocument");
return DocUpdaterClient.deleteDoc(this.project_id, this.doc_id, (error, res, body) => {
this.statusCode = res.statusCode;
return setTimeout(done, 200);
});
});
after ->
MockWebApi.setDocument.restore()
MockWebApi.getDocument.restore()
after(function() {
MockWebApi.setDocument.restore();
return MockWebApi.getDocument.restore();
});
it "should return a 204 status code", ->
@statusCode.should.equal 204
it("should return a 204 status code", function() {
return this.statusCode.should.equal(204);
});
it "should not need to send the updated document to the web api", ->
MockWebApi.setDocument.called.should.equal false
it("should not need to send the updated document to the web api", () => MockWebApi.setDocument.called.should.equal(false));
it "should need to reload the doc if read again", (done) ->
MockWebApi.getDocument.called.should.equal.false
DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, doc) =>
it("should need to reload the doc if read again", function(done) {
MockWebApi.getDocument.called.should.equal.false;
return DocUpdaterClient.getDoc(this.project_id, this.doc_id, (error, res, doc) => {
MockWebApi.getDocument
.calledWith(@project_id, @doc_id)
.should.equal true
done()
.calledWith(this.project_id, this.doc_id)
.should.equal(true);
return done();
});
});
it "should flush track changes", ->
MockTrackChangesApi.flushDoc.calledWith(@doc_id).should.equal true
it("should flush track changes", function() {
return MockTrackChangesApi.flushDoc.calledWith(this.doc_id).should.equal(true);
});
it "should flush project history", ->
MockProjectHistoryApi.flushProject.calledWith(@project_id).should.equal true
return it("should flush project history", function() {
return MockProjectHistoryApi.flushProject.calledWith(this.project_id).should.equal(true);
});
});
});

View File

@@ -1,174 +1,217 @@
sinon = require "sinon"
chai = require("chai")
chai.should()
async = require "async"
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* 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");
chai.should();
const async = require("async");
MockTrackChangesApi = require "./helpers/MockTrackChangesApi"
MockProjectHistoryApi = require "./helpers/MockProjectHistoryApi"
MockWebApi = require "./helpers/MockWebApi"
DocUpdaterClient = require "./helpers/DocUpdaterClient"
DocUpdaterApp = require "./helpers/DocUpdaterApp"
const MockTrackChangesApi = require("./helpers/MockTrackChangesApi");
const MockProjectHistoryApi = require("./helpers/MockProjectHistoryApi");
const MockWebApi = require("./helpers/MockWebApi");
const DocUpdaterClient = require("./helpers/DocUpdaterClient");
const DocUpdaterApp = require("./helpers/DocUpdaterApp");
describe "Deleting a project", ->
before (done) ->
@project_id = DocUpdaterClient.randomId()
@docs = [{
id: doc_id0 = DocUpdaterClient.randomId()
lines: ["one", "two", "three"]
update:
doc: doc_id0
describe("Deleting a project", function() {
before(function(done) {
let doc_id0, doc_id1;
this.project_id = DocUpdaterClient.randomId();
this.docs = [{
id: (doc_id0 = DocUpdaterClient.randomId()),
lines: ["one", "two", "three"],
update: {
doc: doc_id0,
op: [{
i: "one and a half\n"
i: "one and a half\n",
p: 4
}]
}],
v: 0
},
updatedLines: ["one", "one and a half", "two", "three"]
}, {
id: doc_id1 = DocUpdaterClient.randomId()
lines: ["four", "five", "six"]
update:
doc: doc_id1
id: (doc_id1 = DocUpdaterClient.randomId()),
lines: ["four", "five", "six"],
update: {
doc: doc_id1,
op: [{
i: "four and a half\n"
i: "four and a half\n",
p: 5
}]
}],
v: 0
},
updatedLines: ["four", "four and a half", "five", "six"]
}]
for doc in @docs
MockWebApi.insertDoc @project_id, doc.id, {
lines: doc.lines
}];
for (let doc of Array.from(this.docs)) {
MockWebApi.insertDoc(this.project_id, doc.id, {
lines: doc.lines,
version: doc.update.v
}
});
}
DocUpdaterApp.ensureRunning(done)
return DocUpdaterApp.ensureRunning(done);
});
describe "with documents which have been updated", ->
before (done) ->
sinon.spy MockWebApi, "setDocument"
sinon.spy MockTrackChangesApi, "flushDoc"
sinon.spy MockProjectHistoryApi, "flushProject"
describe("with documents which have been updated", function() {
before(function(done) {
sinon.spy(MockWebApi, "setDocument");
sinon.spy(MockTrackChangesApi, "flushDoc");
sinon.spy(MockProjectHistoryApi, "flushProject");
async.series @docs.map((doc) =>
(callback) =>
DocUpdaterClient.preloadDoc @project_id, doc.id, (error) =>
return callback(error) if error?
DocUpdaterClient.sendUpdate @project_id, doc.id, doc.update, (error) =>
callback(error)
), (error) =>
throw error if error?
setTimeout () =>
DocUpdaterClient.deleteProject @project_id, (error, res, body) =>
@statusCode = res.statusCode
done()
, 200
return async.series(this.docs.map(doc => {
return callback => {
return DocUpdaterClient.preloadDoc(this.project_id, doc.id, error => {
if (error != null) { return callback(error); }
return DocUpdaterClient.sendUpdate(this.project_id, doc.id, doc.update, error => {
return callback(error);
});
});
};
}), error => {
if (error != null) { throw error; }
return setTimeout(() => {
return DocUpdaterClient.deleteProject(this.project_id, (error, res, body) => {
this.statusCode = res.statusCode;
return done();
});
}
, 200);
});
});
after ->
MockWebApi.setDocument.restore()
MockTrackChangesApi.flushDoc.restore()
MockProjectHistoryApi.flushProject.restore()
after(function() {
MockWebApi.setDocument.restore();
MockTrackChangesApi.flushDoc.restore();
return MockProjectHistoryApi.flushProject.restore();
});
it "should return a 204 status code", ->
@statusCode.should.equal 204
it("should return a 204 status code", function() {
return this.statusCode.should.equal(204);
});
it "should send each document to the web api", ->
for doc in @docs
it("should send each document to the web api", function() {
return Array.from(this.docs).map((doc) =>
MockWebApi.setDocument
.calledWith(@project_id, doc.id, doc.updatedLines)
.should.equal true
.calledWith(this.project_id, doc.id, doc.updatedLines)
.should.equal(true));
});
it "should need to reload the docs if read again", (done) ->
sinon.spy MockWebApi, "getDocument"
async.series @docs.map((doc) =>
(callback) =>
MockWebApi.getDocument.calledWith(@project_id, doc.id).should.equal false
DocUpdaterClient.getDoc @project_id, doc.id, (error, res, returnedDoc) =>
MockWebApi.getDocument.calledWith(@project_id, doc.id).should.equal true
callback()
), () ->
MockWebApi.getDocument.restore()
done()
it("should need to reload the docs if read again", function(done) {
sinon.spy(MockWebApi, "getDocument");
return async.series(this.docs.map(doc => {
return callback => {
MockWebApi.getDocument.calledWith(this.project_id, doc.id).should.equal(false);
return DocUpdaterClient.getDoc(this.project_id, doc.id, (error, res, returnedDoc) => {
MockWebApi.getDocument.calledWith(this.project_id, doc.id).should.equal(true);
return callback();
});
};
}), function() {
MockWebApi.getDocument.restore();
return done();
});
});
it "should flush each doc in track changes", ->
for doc in @docs
MockTrackChangesApi.flushDoc.calledWith(doc.id).should.equal true
it("should flush each doc in track changes", function() {
return Array.from(this.docs).map((doc) =>
MockTrackChangesApi.flushDoc.calledWith(doc.id).should.equal(true));
});
it "should flush each doc in project history", ->
MockProjectHistoryApi.flushProject.calledWith(@project_id).should.equal true
return it("should flush each doc in project history", function() {
return MockProjectHistoryApi.flushProject.calledWith(this.project_id).should.equal(true);
});
});
describe "with the background=true parameter from realtime and no request to flush the queue", ->
before (done) ->
sinon.spy MockWebApi, "setDocument"
sinon.spy MockTrackChangesApi, "flushDoc"
sinon.spy MockProjectHistoryApi, "flushProject"
describe("with the background=true parameter from realtime and no request to flush the queue", function() {
before(function(done) {
sinon.spy(MockWebApi, "setDocument");
sinon.spy(MockTrackChangesApi, "flushDoc");
sinon.spy(MockProjectHistoryApi, "flushProject");
async.series @docs.map((doc) =>
(callback) =>
DocUpdaterClient.preloadDoc @project_id, doc.id, callback
), (error) =>
throw error if error?
setTimeout () =>
DocUpdaterClient.deleteProjectOnShutdown @project_id, (error, res, body) =>
@statusCode = res.statusCode
done()
, 200
return async.series(this.docs.map(doc => {
return callback => {
return DocUpdaterClient.preloadDoc(this.project_id, doc.id, callback);
};
}), error => {
if (error != null) { throw error; }
return setTimeout(() => {
return DocUpdaterClient.deleteProjectOnShutdown(this.project_id, (error, res, body) => {
this.statusCode = res.statusCode;
return done();
});
}
, 200);
});
});
after ->
MockWebApi.setDocument.restore()
MockTrackChangesApi.flushDoc.restore()
MockProjectHistoryApi.flushProject.restore()
after(function() {
MockWebApi.setDocument.restore();
MockTrackChangesApi.flushDoc.restore();
return MockProjectHistoryApi.flushProject.restore();
});
it "should return a 204 status code", ->
@statusCode.should.equal 204
it("should return a 204 status code", function() {
return this.statusCode.should.equal(204);
});
it "should not send any documents to the web api", ->
MockWebApi.setDocument.called.should.equal false
it("should not send any documents to the web api", () => MockWebApi.setDocument.called.should.equal(false));
it "should not flush any docs in track changes", ->
MockTrackChangesApi.flushDoc.called.should.equal false
it("should not flush any docs in track changes", () => MockTrackChangesApi.flushDoc.called.should.equal(false));
it "should not flush to project history", ->
MockProjectHistoryApi.flushProject.called.should.equal false
return it("should not flush to project history", () => MockProjectHistoryApi.flushProject.called.should.equal(false));
});
describe "with the background=true parameter from realtime and a request to flush the queue", ->
before (done) ->
sinon.spy MockWebApi, "setDocument"
sinon.spy MockTrackChangesApi, "flushDoc"
sinon.spy MockProjectHistoryApi, "flushProject"
return describe("with the background=true parameter from realtime and a request to flush the queue", function() {
before(function(done) {
sinon.spy(MockWebApi, "setDocument");
sinon.spy(MockTrackChangesApi, "flushDoc");
sinon.spy(MockProjectHistoryApi, "flushProject");
async.series @docs.map((doc) =>
(callback) =>
DocUpdaterClient.preloadDoc @project_id, doc.id, callback
), (error) =>
throw error if error?
setTimeout () =>
DocUpdaterClient.deleteProjectOnShutdown @project_id, (error, res, body) =>
@statusCode = res.statusCode
# after deleting the project and putting it in the queue, flush the queue
setTimeout () ->
DocUpdaterClient.flushOldProjects done
, 2000
, 200
return async.series(this.docs.map(doc => {
return callback => {
return DocUpdaterClient.preloadDoc(this.project_id, doc.id, callback);
};
}), error => {
if (error != null) { throw error; }
return setTimeout(() => {
return DocUpdaterClient.deleteProjectOnShutdown(this.project_id, (error, res, body) => {
this.statusCode = res.statusCode;
// after deleting the project and putting it in the queue, flush the queue
return setTimeout(() => DocUpdaterClient.flushOldProjects(done)
, 2000);
});
}
, 200);
});
});
after ->
MockWebApi.setDocument.restore()
MockTrackChangesApi.flushDoc.restore()
MockProjectHistoryApi.flushProject.restore()
after(function() {
MockWebApi.setDocument.restore();
MockTrackChangesApi.flushDoc.restore();
return MockProjectHistoryApi.flushProject.restore();
});
it "should return a 204 status code", ->
@statusCode.should.equal 204
it("should return a 204 status code", function() {
return this.statusCode.should.equal(204);
});
it "should send each document to the web api", ->
for doc in @docs
it("should send each document to the web api", function() {
return Array.from(this.docs).map((doc) =>
MockWebApi.setDocument
.calledWith(@project_id, doc.id, doc.updatedLines)
.should.equal true
.calledWith(this.project_id, doc.id, doc.updatedLines)
.should.equal(true));
});
it "should flush each doc in track changes", ->
for doc in @docs
MockTrackChangesApi.flushDoc.calledWith(doc.id).should.equal true
it("should flush each doc in track changes", function() {
return Array.from(this.docs).map((doc) =>
MockTrackChangesApi.flushDoc.calledWith(doc.id).should.equal(true));
});
it "should flush to project history", ->
MockProjectHistoryApi.flushProject.called.should.equal true
return it("should flush to project history", () => MockProjectHistoryApi.flushProject.called.should.equal(true));
});
});

View File

@@ -1,80 +1,105 @@
sinon = require "sinon"
chai = require("chai")
chai.should()
async = require "async"
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* 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");
chai.should();
const async = require("async");
MockWebApi = require "./helpers/MockWebApi"
DocUpdaterClient = require "./helpers/DocUpdaterClient"
DocUpdaterApp = require "./helpers/DocUpdaterApp"
const MockWebApi = require("./helpers/MockWebApi");
const DocUpdaterClient = require("./helpers/DocUpdaterClient");
const DocUpdaterApp = require("./helpers/DocUpdaterApp");
describe "Flushing a project", ->
before (done) ->
@project_id = DocUpdaterClient.randomId()
@docs = [{
id: doc_id0 = DocUpdaterClient.randomId()
lines: ["one", "two", "three"]
update:
doc: doc_id0
describe("Flushing a project", function() {
before(function(done) {
let doc_id0, doc_id1;
this.project_id = DocUpdaterClient.randomId();
this.docs = [{
id: (doc_id0 = DocUpdaterClient.randomId()),
lines: ["one", "two", "three"],
update: {
doc: doc_id0,
op: [{
i: "one and a half\n"
i: "one and a half\n",
p: 4
}]
}],
v: 0
},
updatedLines: ["one", "one and a half", "two", "three"]
}, {
id: doc_id1 = DocUpdaterClient.randomId()
lines: ["four", "five", "six"]
update:
doc: doc_id1
id: (doc_id1 = DocUpdaterClient.randomId()),
lines: ["four", "five", "six"],
update: {
doc: doc_id1,
op: [{
i: "four and a half\n"
i: "four and a half\n",
p: 5
}]
}],
v: 0
},
updatedLines: ["four", "four and a half", "five", "six"]
}]
for doc in @docs
MockWebApi.insertDoc @project_id, doc.id, {
lines: doc.lines
}];
for (let doc of Array.from(this.docs)) {
MockWebApi.insertDoc(this.project_id, doc.id, {
lines: doc.lines,
version: doc.update.v
}
DocUpdaterApp.ensureRunning(done)
});
}
return DocUpdaterApp.ensureRunning(done);
});
describe "with documents which have been updated", ->
before (done) ->
sinon.spy MockWebApi, "setDocument"
return describe("with documents which have been updated", function() {
before(function(done) {
sinon.spy(MockWebApi, "setDocument");
async.series @docs.map((doc) =>
(callback) =>
DocUpdaterClient.preloadDoc @project_id, doc.id, (error) =>
return callback(error) if error?
DocUpdaterClient.sendUpdate @project_id, doc.id, doc.update, (error) =>
callback(error)
), (error) =>
throw error if error?
setTimeout () =>
DocUpdaterClient.flushProject @project_id, (error, res, body) =>
@statusCode = res.statusCode
done()
, 200
return async.series(this.docs.map(doc => {
return callback => {
return DocUpdaterClient.preloadDoc(this.project_id, doc.id, error => {
if (error != null) { return callback(error); }
return DocUpdaterClient.sendUpdate(this.project_id, doc.id, doc.update, error => {
return callback(error);
});
});
};
}), error => {
if (error != null) { throw error; }
return setTimeout(() => {
return DocUpdaterClient.flushProject(this.project_id, (error, res, body) => {
this.statusCode = res.statusCode;
return done();
});
}
, 200);
});
});
after ->
MockWebApi.setDocument.restore()
after(() => MockWebApi.setDocument.restore());
it "should return a 204 status code", ->
@statusCode.should.equal 204
it("should return a 204 status code", function() {
return this.statusCode.should.equal(204);
});
it "should send each document to the web api", ->
for doc in @docs
it("should send each document to the web api", function() {
return Array.from(this.docs).map((doc) =>
MockWebApi.setDocument
.calledWith(@project_id, doc.id, doc.updatedLines)
.should.equal true
.calledWith(this.project_id, doc.id, doc.updatedLines)
.should.equal(true));
});
it "should update the lines in the doc updater", (done) ->
async.series @docs.map((doc) =>
(callback) =>
DocUpdaterClient.getDoc @project_id, doc.id, (error, res, returnedDoc) =>
returnedDoc.lines.should.deep.equal doc.updatedLines
callback()
), done
return it("should update the lines in the doc updater", function(done) {
return async.series(this.docs.map(doc => {
return callback => {
return DocUpdaterClient.getDoc(this.project_id, doc.id, (error, res, returnedDoc) => {
returnedDoc.lines.should.deep.equal(doc.updatedLines);
return callback();
});
};
}), done);
});
});
});

View File

@@ -1,90 +1,112 @@
sinon = require "sinon"
chai = require("chai")
chai.should()
expect = chai.expect
async = require "async"
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* 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");
chai.should();
const {
expect
} = chai;
const async = require("async");
MockWebApi = require "./helpers/MockWebApi"
DocUpdaterClient = require "./helpers/DocUpdaterClient"
DocUpdaterApp = require "./helpers/DocUpdaterApp"
const MockWebApi = require("./helpers/MockWebApi");
const DocUpdaterClient = require("./helpers/DocUpdaterClient");
const DocUpdaterApp = require("./helpers/DocUpdaterApp");
describe "Flushing a doc to Mongo", ->
before (done) ->
@lines = ["one", "two", "three"]
@version = 42
@update =
doc: @doc_id
meta: { user_id: 'last-author-fake-id' }
describe("Flushing a doc to Mongo", function() {
before(function(done) {
this.lines = ["one", "two", "three"];
this.version = 42;
this.update = {
doc: this.doc_id,
meta: { user_id: 'last-author-fake-id' },
op: [{
i: "one and a half\n"
i: "one and a half\n",
p: 4
}]
v: @version
@result = ["one", "one and a half", "two", "three"]
DocUpdaterApp.ensureRunning(done)
}],
v: this.version
};
this.result = ["one", "one and a half", "two", "three"];
return DocUpdaterApp.ensureRunning(done);
});
describe "when the updated doc exists in the doc updater", ->
before (done) ->
[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
sinon.spy MockWebApi, "setDocument"
describe("when the updated doc exists in the doc updater", function() {
before(function(done) {
[this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
sinon.spy(MockWebApi, "setDocument");
MockWebApi.insertDoc @project_id, @doc_id, {lines: @lines, version: @version}
DocUpdaterClient.sendUpdates @project_id, @doc_id, [@update], (error) =>
throw error if error?
setTimeout () =>
DocUpdaterClient.flushDoc @project_id, @doc_id, done
, 200
MockWebApi.insertDoc(this.project_id, this.doc_id, {lines: this.lines, version: this.version});
return DocUpdaterClient.sendUpdates(this.project_id, this.doc_id, [this.update], error => {
if (error != null) { throw error; }
return setTimeout(() => {
return DocUpdaterClient.flushDoc(this.project_id, this.doc_id, done);
}
, 200);
});
});
after ->
MockWebApi.setDocument.restore()
after(() => MockWebApi.setDocument.restore());
it "should flush the updated doc lines and version to the web api", ->
MockWebApi.setDocument
.calledWith(@project_id, @doc_id, @result, @version + 1)
.should.equal true
it("should flush the updated doc lines and version to the web api", function() {
return MockWebApi.setDocument
.calledWith(this.project_id, this.doc_id, this.result, this.version + 1)
.should.equal(true);
});
it "should flush the last update author and time to the web api", ->
lastUpdatedAt = MockWebApi.setDocument.lastCall.args[5]
parseInt(lastUpdatedAt).should.be.closeTo((new Date()).getTime(), 30000)
return it("should flush the last update author and time to the web api", function() {
const lastUpdatedAt = MockWebApi.setDocument.lastCall.args[5];
parseInt(lastUpdatedAt).should.be.closeTo((new Date()).getTime(), 30000);
lastUpdatedBy = MockWebApi.setDocument.lastCall.args[6]
lastUpdatedBy.should.equal 'last-author-fake-id'
const lastUpdatedBy = MockWebApi.setDocument.lastCall.args[6];
return lastUpdatedBy.should.equal('last-author-fake-id');
});
});
describe "when the doc does not exist in the doc updater", ->
before (done) ->
[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
MockWebApi.insertDoc @project_id, @doc_id, {
lines: @lines
}
sinon.spy MockWebApi, "setDocument"
DocUpdaterClient.flushDoc @project_id, @doc_id, done
describe("when the doc does not exist in the doc updater", function() {
before(function(done) {
[this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
MockWebApi.insertDoc(this.project_id, this.doc_id, {
lines: this.lines
});
sinon.spy(MockWebApi, "setDocument");
return DocUpdaterClient.flushDoc(this.project_id, this.doc_id, done);
});
after ->
MockWebApi.setDocument.restore()
after(() => MockWebApi.setDocument.restore());
it "should not flush the doc to the web api", ->
MockWebApi.setDocument.called.should.equal false
return it("should not flush the doc to the web api", () => MockWebApi.setDocument.called.should.equal(false));
});
describe "when the web api http request takes a long time on first request", ->
before (done) ->
[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
MockWebApi.insertDoc @project_id, @doc_id, {
lines: @lines
version: @version
}
t = 30000
sinon.stub MockWebApi, "setDocument", (project_id, doc_id, lines, version, ranges, lastUpdatedAt, lastUpdatedBy, callback = (error) ->) ->
setTimeout callback, t
t = 0
DocUpdaterClient.preloadDoc @project_id, @doc_id, done
return describe("when the web api http request takes a long time on first request", function() {
before(function(done) {
[this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
MockWebApi.insertDoc(this.project_id, this.doc_id, {
lines: this.lines,
version: this.version
});
let t = 30000;
sinon.stub(MockWebApi, "setDocument", function(project_id, doc_id, lines, version, ranges, lastUpdatedAt, lastUpdatedBy, callback) {
if (callback == null) { callback = function(error) {}; }
setTimeout(callback, t);
return t = 0;
});
return DocUpdaterClient.preloadDoc(this.project_id, this.doc_id, done);
});
after ->
MockWebApi.setDocument.restore()
after(() => MockWebApi.setDocument.restore());
it "should still work", (done) ->
start = Date.now()
DocUpdaterClient.flushDoc @project_id, @doc_id, (error, res, doc) =>
res.statusCode.should.equal 204
delta = Date.now() - start
expect(delta).to.be.below 20000
done()
return it("should still work", function(done) {
const start = Date.now();
return DocUpdaterClient.flushDoc(this.project_id, this.doc_id, (error, res, doc) => {
res.statusCode.should.equal(204);
const delta = Date.now() - start;
expect(delta).to.be.below(20000);
return done();
});
});
});
});

View File

@@ -1,138 +1,188 @@
sinon = require "sinon"
chai = require("chai")
chai.should()
expect = chai.expect
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* 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");
chai.should();
const {
expect
} = chai;
MockWebApi = require "./helpers/MockWebApi"
DocUpdaterClient = require "./helpers/DocUpdaterClient"
DocUpdaterApp = require "./helpers/DocUpdaterApp"
const MockWebApi = require("./helpers/MockWebApi");
const DocUpdaterClient = require("./helpers/DocUpdaterClient");
const DocUpdaterApp = require("./helpers/DocUpdaterApp");
describe "Getting a document", ->
before (done) ->
@lines = ["one", "two", "three"]
@version = 42
DocUpdaterApp.ensureRunning(done)
describe("Getting a document", function() {
before(function(done) {
this.lines = ["one", "two", "three"];
this.version = 42;
return DocUpdaterApp.ensureRunning(done);
});
describe "when the document is not loaded", ->
before (done) ->
[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
sinon.spy MockWebApi, "getDocument"
describe("when the document is not loaded", function() {
before(function(done) {
[this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
sinon.spy(MockWebApi, "getDocument");
MockWebApi.insertDoc @project_id, @doc_id, {lines: @lines, version: @version}
MockWebApi.insertDoc(this.project_id, this.doc_id, {lines: this.lines, version: this.version});
DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, @returnedDoc) => done()
return DocUpdaterClient.getDoc(this.project_id, this.doc_id, (error, res, returnedDoc) => { this.returnedDoc = returnedDoc; return done(); });
});
after ->
MockWebApi.getDocument.restore()
after(() => MockWebApi.getDocument.restore());
it "should load the document from the web API", ->
MockWebApi.getDocument
.calledWith(@project_id, @doc_id)
.should.equal true
it("should load the document from the web API", function() {
return MockWebApi.getDocument
.calledWith(this.project_id, this.doc_id)
.should.equal(true);
});
it "should return the document lines", ->
@returnedDoc.lines.should.deep.equal @lines
it("should return the document lines", function() {
return this.returnedDoc.lines.should.deep.equal(this.lines);
});
it "should return the document at its current version", ->
@returnedDoc.version.should.equal @version
return it("should return the document at its current version", function() {
return this.returnedDoc.version.should.equal(this.version);
});
});
describe "when the document is already loaded", ->
before (done) ->
[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
describe("when the document is already loaded", function() {
before(function(done) {
[this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
MockWebApi.insertDoc @project_id, @doc_id, {lines: @lines, version: @version}
DocUpdaterClient.preloadDoc @project_id, @doc_id, (error) =>
throw error if error?
sinon.spy MockWebApi, "getDocument"
DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, @returnedDoc) => done()
MockWebApi.insertDoc(this.project_id, this.doc_id, {lines: this.lines, version: this.version});
return DocUpdaterClient.preloadDoc(this.project_id, this.doc_id, error => {
if (error != null) { throw error; }
sinon.spy(MockWebApi, "getDocument");
return DocUpdaterClient.getDoc(this.project_id, this.doc_id, (error, res, returnedDoc) => { this.returnedDoc = returnedDoc; return done(); });
});
});
after ->
MockWebApi.getDocument.restore()
after(() => MockWebApi.getDocument.restore());
it "should not load the document from the web API", ->
MockWebApi.getDocument.called.should.equal false
it("should not load the document from the web API", () => MockWebApi.getDocument.called.should.equal(false));
it "should return the document lines", ->
@returnedDoc.lines.should.deep.equal @lines
return it("should return the document lines", function() {
return this.returnedDoc.lines.should.deep.equal(this.lines);
});
});
describe "when the request asks for some recent ops", ->
before (done) ->
[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
MockWebApi.insertDoc @project_id, @doc_id, {
lines: @lines = ["one", "two", "three"]
}
describe("when the request asks for some recent ops", function() {
before(function(done) {
[this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
MockWebApi.insertDoc(this.project_id, this.doc_id, {
lines: (this.lines = ["one", "two", "three"])
});
@updates = for v in [0..199]
doc_id: @doc_id,
op: [i: v.toString(), p: 0]
v: v
this.updates = __range__(0, 199, true).map((v) => ({
doc_id: this.doc_id,
op: [{i: v.toString(), p: 0}],
v
}));
DocUpdaterClient.sendUpdates @project_id, @doc_id, @updates, (error) =>
throw error if error?
sinon.spy MockWebApi, "getDocument"
done()
return DocUpdaterClient.sendUpdates(this.project_id, this.doc_id, this.updates, error => {
if (error != null) { throw error; }
sinon.spy(MockWebApi, "getDocument");
return done();
});
});
after ->
MockWebApi.getDocument.restore()
after(() => MockWebApi.getDocument.restore());
describe "when the ops are loaded", ->
before (done) ->
DocUpdaterClient.getDocAndRecentOps @project_id, @doc_id, 190, (error, res, @returnedDoc) => done()
describe("when the ops are loaded", function() {
before(function(done) {
return DocUpdaterClient.getDocAndRecentOps(this.project_id, this.doc_id, 190, (error, res, returnedDoc) => { this.returnedDoc = returnedDoc; return done(); });
});
it "should return the recent ops", ->
@returnedDoc.ops.length.should.equal 10
for update, i in @updates.slice(190, -1)
@returnedDoc.ops[i].op.should.deep.equal update.op
return it("should return the recent ops", function() {
this.returnedDoc.ops.length.should.equal(10);
return Array.from(this.updates.slice(190, -1)).map((update, i) =>
this.returnedDoc.ops[i].op.should.deep.equal(update.op));
});
});
describe "when the ops are not all loaded", ->
before (done) ->
# We only track 100 ops
DocUpdaterClient.getDocAndRecentOps @project_id, @doc_id, 10, (error, @res, @returnedDoc) => done()
return describe("when the ops are not all loaded", function() {
before(function(done) {
// We only track 100 ops
return DocUpdaterClient.getDocAndRecentOps(this.project_id, this.doc_id, 10, (error, res, returnedDoc) => { this.res = res; this.returnedDoc = returnedDoc; return done(); });
});
it "should return UnprocessableEntity", ->
@res.statusCode.should.equal 422
return it("should return UnprocessableEntity", function() {
return this.res.statusCode.should.equal(422);
});
});
});
describe "when the document does not exist", ->
before (done) ->
[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, doc) =>
@statusCode = res.statusCode
done()
describe("when the document does not exist", function() {
before(function(done) {
[this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
return DocUpdaterClient.getDoc(this.project_id, this.doc_id, (error, res, doc) => {
this.statusCode = res.statusCode;
return done();
});
});
it "should return 404", ->
@statusCode.should.equal 404
return it("should return 404", function() {
return this.statusCode.should.equal(404);
});
});
describe "when the web api returns an error", ->
before (done) ->
[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
sinon.stub MockWebApi, "getDocument", (project_id, doc_id, callback = (error, doc) ->) ->
callback new Error("oops")
DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, doc) =>
@statusCode = res.statusCode
done()
describe("when the web api returns an error", function() {
before(function(done) {
[this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
sinon.stub(MockWebApi, "getDocument", function(project_id, doc_id, callback) {
if (callback == null) { callback = function(error, doc) {}; }
return callback(new Error("oops"));
});
return DocUpdaterClient.getDoc(this.project_id, this.doc_id, (error, res, doc) => {
this.statusCode = res.statusCode;
return done();
});
});
after ->
MockWebApi.getDocument.restore()
after(() => MockWebApi.getDocument.restore());
it "should return 500", ->
@statusCode.should.equal 500
return it("should return 500", function() {
return this.statusCode.should.equal(500);
});
});
describe "when the web api http request takes a long time", ->
before (done) ->
@timeout = 10000
[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
sinon.stub MockWebApi, "getDocument", (project_id, doc_id, callback = (error, doc) ->) ->
setTimeout callback, 30000
done()
return describe("when the web api http request takes a long time", function() {
before(function(done) {
this.timeout = 10000;
[this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
sinon.stub(MockWebApi, "getDocument", function(project_id, doc_id, callback) {
if (callback == null) { callback = function(error, doc) {}; }
return setTimeout(callback, 30000);
});
return done();
});
after ->
MockWebApi.getDocument.restore()
after(() => MockWebApi.getDocument.restore());
it "should return quickly(ish)", (done) ->
start = Date.now()
DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, doc) =>
res.statusCode.should.equal 500
delta = Date.now() - start
expect(delta).to.be.below 20000
done()
return it("should return quickly(ish)", function(done) {
const start = Date.now();
return DocUpdaterClient.getDoc(this.project_id, this.doc_id, (error, res, doc) => {
res.statusCode.should.equal(500);
const delta = Date.now() - start;
expect(delta).to.be.below(20000);
return done();
});
});
});
});
function __range__(left, right, inclusive) {
let range = [];
let ascending = left < right;
let end = !inclusive ? right : ascending ? right + 1 : right - 1;
for (let i = left; ascending ? i < end : i > end; ascending ? i++ : i--) {
range.push(i);
}
return range;
}

View File

@@ -1,69 +1,109 @@
sinon = require "sinon"
chai = require("chai")
chai.should()
expect = chai.expect
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* 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");
chai.should();
const {
expect
} = chai;
MockWebApi = require "./helpers/MockWebApi"
DocUpdaterClient = require "./helpers/DocUpdaterClient"
DocUpdaterApp = require "./helpers/DocUpdaterApp"
const MockWebApi = require("./helpers/MockWebApi");
const DocUpdaterClient = require("./helpers/DocUpdaterClient");
const DocUpdaterApp = require("./helpers/DocUpdaterApp");
describe "Getting documents for project", ->
before (done) ->
@lines = ["one", "two", "three"]
@version = 42
DocUpdaterApp.ensureRunning(done)
describe("Getting documents for project", function() {
before(function(done) {
this.lines = ["one", "two", "three"];
this.version = 42;
return DocUpdaterApp.ensureRunning(done);
});
describe "when project state hash does not match", ->
before (done) ->
@projectStateHash = DocUpdaterClient.randomId()
[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
describe("when project state hash does not match", function() {
before(function(done) {
this.projectStateHash = DocUpdaterClient.randomId();
[this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
MockWebApi.insertDoc @project_id, @doc_id, {lines: @lines, version: @version}
DocUpdaterClient.preloadDoc @project_id, @doc_id, (error) =>
throw error if error?
DocUpdaterClient.getProjectDocs @project_id, @projectStateHash, (error, @res, @returnedDocs) =>
done()
MockWebApi.insertDoc(this.project_id, this.doc_id, {lines: this.lines, version: this.version});
return DocUpdaterClient.preloadDoc(this.project_id, this.doc_id, error => {
if (error != null) { throw error; }
return DocUpdaterClient.getProjectDocs(this.project_id, this.projectStateHash, (error, res, returnedDocs) => {
this.res = res;
this.returnedDocs = returnedDocs;
return done();
});
});
});
it "should return a 409 Conflict response", ->
@res.statusCode.should.equal 409
return it("should return a 409 Conflict response", function() {
return this.res.statusCode.should.equal(409);
});
});
describe "when project state hash matches", ->
before (done) ->
@projectStateHash = DocUpdaterClient.randomId()
[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
describe("when project state hash matches", function() {
before(function(done) {
this.projectStateHash = DocUpdaterClient.randomId();
[this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
MockWebApi.insertDoc @project_id, @doc_id, {lines: @lines, version: @version}
DocUpdaterClient.preloadDoc @project_id, @doc_id, (error) =>
throw error if error?
DocUpdaterClient.getProjectDocs @project_id, @projectStateHash, (error, @res0, @returnedDocs0) =>
# set the hash
DocUpdaterClient.getProjectDocs @project_id, @projectStateHash, (error, @res, @returnedDocs) =>
# the hash should now match
done()
MockWebApi.insertDoc(this.project_id, this.doc_id, {lines: this.lines, version: this.version});
return DocUpdaterClient.preloadDoc(this.project_id, this.doc_id, error => {
if (error != null) { throw error; }
return DocUpdaterClient.getProjectDocs(this.project_id, this.projectStateHash, (error, res0, returnedDocs0) => {
// set the hash
this.res0 = res0;
this.returnedDocs0 = returnedDocs0;
return DocUpdaterClient.getProjectDocs(this.project_id, this.projectStateHash, (error, res, returnedDocs) => {
// the hash should now match
this.res = res;
this.returnedDocs = returnedDocs;
return done();
});
});
});
});
it "should return a 200 response", ->
@res.statusCode.should.equal 200
it("should return a 200 response", function() {
return this.res.statusCode.should.equal(200);
});
it "should return the documents", ->
@returnedDocs.should.deep.equal [ {_id: @doc_id, lines: @lines, v: @version} ]
return it("should return the documents", function() {
return this.returnedDocs.should.deep.equal([ {_id: this.doc_id, lines: this.lines, v: this.version} ]);
});
});
describe "when the doc has been removed", ->
before (done) ->
@projectStateHash = DocUpdaterClient.randomId()
[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
return describe("when the doc has been removed", function() {
before(function(done) {
this.projectStateHash = DocUpdaterClient.randomId();
[this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
MockWebApi.insertDoc @project_id, @doc_id, {lines: @lines, version: @version}
DocUpdaterClient.preloadDoc @project_id, @doc_id, (error) =>
throw error if error?
DocUpdaterClient.getProjectDocs @project_id, @projectStateHash, (error, @res0, @returnedDocs0) =>
# set the hash
DocUpdaterClient.deleteDoc @project_id, @doc_id, (error, res, body) =>
# delete the doc
DocUpdaterClient.getProjectDocs @project_id, @projectStateHash, (error, @res, @returnedDocs) =>
# the hash would match, but the doc has been deleted
done()
MockWebApi.insertDoc(this.project_id, this.doc_id, {lines: this.lines, version: this.version});
return DocUpdaterClient.preloadDoc(this.project_id, this.doc_id, error => {
if (error != null) { throw error; }
return DocUpdaterClient.getProjectDocs(this.project_id, this.projectStateHash, (error, res0, returnedDocs0) => {
// set the hash
this.res0 = res0;
this.returnedDocs0 = returnedDocs0;
return DocUpdaterClient.deleteDoc(this.project_id, this.doc_id, (error, res, body) => {
// delete the doc
return DocUpdaterClient.getProjectDocs(this.project_id, this.projectStateHash, (error, res1, returnedDocs) => {
// the hash would match, but the doc has been deleted
this.res = res1;
this.returnedDocs = returnedDocs;
return done();
});
});
});
});
});
it "should return a 409 Conflict response", ->
@res.statusCode.should.equal 409
return it("should return a 409 Conflict response", function() {
return this.res.statusCode.should.equal(409);
});
});
});

View File

@@ -1,372 +1,467 @@
sinon = require "sinon"
chai = require("chai")
chai.should()
expect = chai.expect
async = require "async"
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* 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");
chai.should();
const {
expect
} = chai;
const async = require("async");
{db, ObjectId} = require "../../../app/js/mongojs"
MockWebApi = require "./helpers/MockWebApi"
DocUpdaterClient = require "./helpers/DocUpdaterClient"
DocUpdaterApp = require "./helpers/DocUpdaterApp"
const {db, ObjectId} = require("../../../app/js/mongojs");
const MockWebApi = require("./helpers/MockWebApi");
const DocUpdaterClient = require("./helpers/DocUpdaterClient");
const DocUpdaterApp = require("./helpers/DocUpdaterApp");
describe "Ranges", ->
before (done) ->
DocUpdaterApp.ensureRunning done
describe("Ranges", function() {
before(done => DocUpdaterApp.ensureRunning(done));
describe "tracking changes from ops", ->
before (done) ->
@project_id = DocUpdaterClient.randomId()
@user_id = DocUpdaterClient.randomId()
@id_seed = "587357bd35e64f6157"
@doc = {
id: DocUpdaterClient.randomId()
describe("tracking changes from ops", function() {
before(function(done) {
this.project_id = DocUpdaterClient.randomId();
this.user_id = DocUpdaterClient.randomId();
this.id_seed = "587357bd35e64f6157";
this.doc = {
id: DocUpdaterClient.randomId(),
lines: ["aaa"]
}
@updates = [{
doc: @doc.id
op: [{ i: "123", p: 1 }]
v: 0
meta: { user_id: @user_id }
};
this.updates = [{
doc: this.doc.id,
op: [{ i: "123", p: 1 }],
v: 0,
meta: { user_id: this.user_id }
}, {
doc: @doc.id
op: [{ i: "456", p: 5 }]
v: 1
meta: { user_id: @user_id, tc: @id_seed }
doc: this.doc.id,
op: [{ i: "456", p: 5 }],
v: 1,
meta: { user_id: this.user_id, tc: this.id_seed }
}, {
doc: @doc.id
op: [{ d: "12", p: 1 }]
v: 2
meta: { user_id: @user_id }
}]
MockWebApi.insertDoc @project_id, @doc.id, {
lines: @doc.lines
doc: this.doc.id,
op: [{ d: "12", p: 1 }],
v: 2,
meta: { user_id: this.user_id }
}];
MockWebApi.insertDoc(this.project_id, this.doc.id, {
lines: this.doc.lines,
version: 0
});
const jobs = [];
for (let update of Array.from(this.updates)) {
(update => {
return jobs.push(callback => DocUpdaterClient.sendUpdate(this.project_id, this.doc.id, update, callback));
})(update);
}
jobs = []
for update in @updates
do (update) =>
jobs.push (callback) => DocUpdaterClient.sendUpdate @project_id, @doc.id, update, callback
DocUpdaterApp.ensureRunning (error) =>
throw error if error?
DocUpdaterClient.preloadDoc @project_id, @doc.id, (error) =>
throw error if error?
async.series jobs, (error) ->
throw error if error?
done()
return DocUpdaterApp.ensureRunning(error => {
if (error != null) { throw error; }
return DocUpdaterClient.preloadDoc(this.project_id, this.doc.id, error => {
if (error != null) { throw error; }
return async.series(jobs, function(error) {
if (error != null) { throw error; }
return done();
});
});
});
});
it "should update the ranges", (done) ->
DocUpdaterClient.getDoc @project_id, @doc.id, (error, res, data) =>
throw error if error?
ranges = data.ranges
change = ranges.changes[0]
change.op.should.deep.equal { i: "456", p: 3 }
change.id.should.equal @id_seed + "000001"
change.metadata.user_id.should.equal @user_id
done()
it("should update the ranges", function(done) {
return DocUpdaterClient.getDoc(this.project_id, this.doc.id, (error, res, data) => {
if (error != null) { throw error; }
const {
ranges
} = data;
const change = ranges.changes[0];
change.op.should.deep.equal({ i: "456", p: 3 });
change.id.should.equal(this.id_seed + "000001");
change.metadata.user_id.should.equal(this.user_id);
return done();
});
});
describe "Adding comments", ->
describe "standalone", ->
before (done) ->
@project_id = DocUpdaterClient.randomId()
@user_id = DocUpdaterClient.randomId()
@doc = {
id: DocUpdaterClient.randomId()
return describe("Adding comments", function() {
describe("standalone", function() {
before(function(done) {
this.project_id = DocUpdaterClient.randomId();
this.user_id = DocUpdaterClient.randomId();
this.doc = {
id: DocUpdaterClient.randomId(),
lines: ["foo bar baz"]
}
@updates = [{
doc: @doc.id
op: [{ c: "bar", p: 4, t: @tid = DocUpdaterClient.randomId() }]
};
this.updates = [{
doc: this.doc.id,
op: [{ c: "bar", p: 4, t: (this.tid = DocUpdaterClient.randomId()) }],
v: 0
}]
MockWebApi.insertDoc @project_id, @doc.id, {
lines: @doc.lines
}];
MockWebApi.insertDoc(this.project_id, this.doc.id, {
lines: this.doc.lines,
version: 0
});
const jobs = [];
for (let update of Array.from(this.updates)) {
(update => {
return jobs.push(callback => DocUpdaterClient.sendUpdate(this.project_id, this.doc.id, update, callback));
})(update);
}
jobs = []
for update in @updates
do (update) =>
jobs.push (callback) => DocUpdaterClient.sendUpdate @project_id, @doc.id, update, callback
DocUpdaterClient.preloadDoc @project_id, @doc.id, (error) =>
throw error if error?
async.series jobs, (error) ->
throw error if error?
setTimeout done, 200
return DocUpdaterClient.preloadDoc(this.project_id, this.doc.id, error => {
if (error != null) { throw error; }
return async.series(jobs, function(error) {
if (error != null) { throw error; }
return setTimeout(done, 200);
});
});
});
it "should update the ranges", (done) ->
DocUpdaterClient.getDoc @project_id, @doc.id, (error, res, data) =>
throw error if error?
ranges = data.ranges
comment = ranges.comments[0]
comment.op.should.deep.equal { c: "bar", p: 4, t: @tid }
comment.id.should.equal @tid
done()
return it("should update the ranges", function(done) {
return DocUpdaterClient.getDoc(this.project_id, this.doc.id, (error, res, data) => {
if (error != null) { throw error; }
const {
ranges
} = data;
const comment = ranges.comments[0];
comment.op.should.deep.equal({ c: "bar", p: 4, t: this.tid });
comment.id.should.equal(this.tid);
return done();
});
});
});
describe "with conflicting ops needing OT", ->
before (done) ->
@project_id = DocUpdaterClient.randomId()
@user_id = DocUpdaterClient.randomId()
@doc = {
id: DocUpdaterClient.randomId()
return describe("with conflicting ops needing OT", function() {
before(function(done) {
this.project_id = DocUpdaterClient.randomId();
this.user_id = DocUpdaterClient.randomId();
this.doc = {
id: DocUpdaterClient.randomId(),
lines: ["foo bar baz"]
}
@updates = [{
doc: @doc.id
op: [{ i: "ABC", p: 3 }]
v: 0
meta: { user_id: @user_id }
};
this.updates = [{
doc: this.doc.id,
op: [{ i: "ABC", p: 3 }],
v: 0,
meta: { user_id: this.user_id }
}, {
doc: @doc.id
op: [{ c: "bar", p: 4, t: @tid = DocUpdaterClient.randomId() }]
doc: this.doc.id,
op: [{ c: "bar", p: 4, t: (this.tid = DocUpdaterClient.randomId()) }],
v: 0
}]
MockWebApi.insertDoc @project_id, @doc.id, {
lines: @doc.lines
}];
MockWebApi.insertDoc(this.project_id, this.doc.id, {
lines: this.doc.lines,
version: 0
});
const jobs = [];
for (let update of Array.from(this.updates)) {
(update => {
return jobs.push(callback => DocUpdaterClient.sendUpdate(this.project_id, this.doc.id, update, callback));
})(update);
}
jobs = []
for update in @updates
do (update) =>
jobs.push (callback) => DocUpdaterClient.sendUpdate @project_id, @doc.id, update, callback
DocUpdaterClient.preloadDoc @project_id, @doc.id, (error) =>
throw error if error?
async.series jobs, (error) ->
throw error if error?
setTimeout done, 200
return DocUpdaterClient.preloadDoc(this.project_id, this.doc.id, error => {
if (error != null) { throw error; }
return async.series(jobs, function(error) {
if (error != null) { throw error; }
return setTimeout(done, 200);
});
});
});
it "should update the comments with the OT shifted comment", (done) ->
DocUpdaterClient.getDoc @project_id, @doc.id, (error, res, data) =>
throw error if error?
ranges = data.ranges
comment = ranges.comments[0]
comment.op.should.deep.equal { c: "bar", p: 7, t: @tid }
done()
return it("should update the comments with the OT shifted comment", function(done) {
return DocUpdaterClient.getDoc(this.project_id, this.doc.id, (error, res, data) => {
if (error != null) { throw error; }
const {
ranges
} = data;
const comment = ranges.comments[0];
comment.op.should.deep.equal({ c: "bar", p: 7, t: this.tid });
return done();
});
});
});
});
});
describe "Loading ranges from persistence layer", ->
before (done) ->
@project_id = DocUpdaterClient.randomId()
@user_id = DocUpdaterClient.randomId()
@id_seed = "587357bd35e64f6157"
@doc = {
id: DocUpdaterClient.randomId()
describe("Loading ranges from persistence layer", function() {
before(function(done) {
this.project_id = DocUpdaterClient.randomId();
this.user_id = DocUpdaterClient.randomId();
this.id_seed = "587357bd35e64f6157";
this.doc = {
id: DocUpdaterClient.randomId(),
lines: ["a123aa"]
}
@update = {
doc: @doc.id
op: [{ i: "456", p: 5 }]
v: 0
meta: { user_id: @user_id, tc: @id_seed }
}
MockWebApi.insertDoc @project_id, @doc.id, {
lines: @doc.lines
version: 0
};
this.update = {
doc: this.doc.id,
op: [{ i: "456", p: 5 }],
v: 0,
meta: { user_id: this.user_id, tc: this.id_seed }
};
MockWebApi.insertDoc(this.project_id, this.doc.id, {
lines: this.doc.lines,
version: 0,
ranges: {
changes: [{
op: { i: "123", p: 1 }
metadata:
user_id: @user_id
op: { i: "123", p: 1 },
metadata: {
user_id: this.user_id,
ts: new Date()
}
}]
}
}
DocUpdaterClient.preloadDoc @project_id, @doc.id, (error) =>
throw error if error?
DocUpdaterClient.sendUpdate @project_id, @doc.id, @update, (error) ->
throw error if error?
setTimeout done, 200
});
return DocUpdaterClient.preloadDoc(this.project_id, this.doc.id, error => {
if (error != null) { throw error; }
return DocUpdaterClient.sendUpdate(this.project_id, this.doc.id, this.update, function(error) {
if (error != null) { throw error; }
return setTimeout(done, 200);
});
});
});
it "should have preloaded the existing ranges", (done) ->
DocUpdaterClient.getDoc @project_id, @doc.id, (error, res, data) =>
throw error if error?
{changes} = data.ranges
changes[0].op.should.deep.equal { i: "123", p: 1 }
changes[1].op.should.deep.equal { i: "456", p: 5 }
done()
it("should have preloaded the existing ranges", function(done) {
return DocUpdaterClient.getDoc(this.project_id, this.doc.id, (error, res, data) => {
if (error != null) { throw error; }
const {changes} = data.ranges;
changes[0].op.should.deep.equal({ i: "123", p: 1 });
changes[1].op.should.deep.equal({ i: "456", p: 5 });
return done();
});
});
it "should flush the ranges to the persistence layer again", (done) ->
DocUpdaterClient.flushDoc @project_id, @doc.id, (error) =>
throw error if error?
MockWebApi.getDocument @project_id, @doc.id, (error, doc) =>
{changes} = doc.ranges
changes[0].op.should.deep.equal { i: "123", p: 1 }
changes[1].op.should.deep.equal { i: "456", p: 5 }
done()
return it("should flush the ranges to the persistence layer again", function(done) {
return DocUpdaterClient.flushDoc(this.project_id, this.doc.id, error => {
if (error != null) { throw error; }
return MockWebApi.getDocument(this.project_id, this.doc.id, (error, doc) => {
const {changes} = doc.ranges;
changes[0].op.should.deep.equal({ i: "123", p: 1 });
changes[1].op.should.deep.equal({ i: "456", p: 5 });
return done();
});
});
});
});
describe "accepting a change", ->
before (done) ->
@project_id = DocUpdaterClient.randomId()
@user_id = DocUpdaterClient.randomId()
@id_seed = "587357bd35e64f6157"
@doc = {
id: DocUpdaterClient.randomId()
describe("accepting a change", function() {
before(function(done) {
this.project_id = DocUpdaterClient.randomId();
this.user_id = DocUpdaterClient.randomId();
this.id_seed = "587357bd35e64f6157";
this.doc = {
id: DocUpdaterClient.randomId(),
lines: ["aaa"]
}
@update = {
doc: @doc.id
op: [{ i: "456", p: 1 }]
v: 0
meta: { user_id: @user_id, tc: @id_seed }
}
MockWebApi.insertDoc @project_id, @doc.id, {
lines: @doc.lines
};
this.update = {
doc: this.doc.id,
op: [{ i: "456", p: 1 }],
v: 0,
meta: { user_id: this.user_id, tc: this.id_seed }
};
MockWebApi.insertDoc(this.project_id, this.doc.id, {
lines: this.doc.lines,
version: 0
}
DocUpdaterClient.preloadDoc @project_id, @doc.id, (error) =>
throw error if error?
DocUpdaterClient.sendUpdate @project_id, @doc.id, @update, (error) =>
throw error if error?
setTimeout () =>
DocUpdaterClient.getDoc @project_id, @doc.id, (error, res, data) =>
throw error if error?
ranges = data.ranges
change = ranges.changes[0]
change.op.should.deep.equal { i: "456", p: 1 }
change.id.should.equal @id_seed + "000001"
change.metadata.user_id.should.equal @user_id
done()
, 200
});
return DocUpdaterClient.preloadDoc(this.project_id, this.doc.id, error => {
if (error != null) { throw error; }
return DocUpdaterClient.sendUpdate(this.project_id, this.doc.id, this.update, error => {
if (error != null) { throw error; }
return setTimeout(() => {
return DocUpdaterClient.getDoc(this.project_id, this.doc.id, (error, res, data) => {
if (error != null) { throw error; }
const {
ranges
} = data;
const change = ranges.changes[0];
change.op.should.deep.equal({ i: "456", p: 1 });
change.id.should.equal(this.id_seed + "000001");
change.metadata.user_id.should.equal(this.user_id);
return done();
});
}
, 200);
});
});
});
it "should remove the change after accepting", (done) ->
DocUpdaterClient.acceptChange @project_id, @doc.id, @id_seed + "000001", (error) =>
throw error if error?
DocUpdaterClient.getDoc @project_id, @doc.id, (error, res, data) =>
throw error if error?
expect(data.ranges.changes).to.be.undefined
done()
return it("should remove the change after accepting", function(done) {
return DocUpdaterClient.acceptChange(this.project_id, this.doc.id, this.id_seed + "000001", error => {
if (error != null) { throw error; }
return DocUpdaterClient.getDoc(this.project_id, this.doc.id, (error, res, data) => {
if (error != null) { throw error; }
expect(data.ranges.changes).to.be.undefined;
return done();
});
});
});
});
describe "deleting a comment range", ->
before (done) ->
@project_id = DocUpdaterClient.randomId()
@user_id = DocUpdaterClient.randomId()
@doc = {
id: DocUpdaterClient.randomId()
describe("deleting a comment range", function() {
before(function(done) {
this.project_id = DocUpdaterClient.randomId();
this.user_id = DocUpdaterClient.randomId();
this.doc = {
id: DocUpdaterClient.randomId(),
lines: ["foo bar"]
}
@update = {
doc: @doc.id
op: [{ c: "bar", p: 4, t: @tid = DocUpdaterClient.randomId() }]
};
this.update = {
doc: this.doc.id,
op: [{ c: "bar", p: 4, t: (this.tid = DocUpdaterClient.randomId()) }],
v: 0
}
MockWebApi.insertDoc @project_id, @doc.id, {
lines: @doc.lines
};
MockWebApi.insertDoc(this.project_id, this.doc.id, {
lines: this.doc.lines,
version: 0
}
DocUpdaterClient.preloadDoc @project_id, @doc.id, (error) =>
throw error if error?
DocUpdaterClient.sendUpdate @project_id, @doc.id, @update, (error) =>
throw error if error?
setTimeout () =>
DocUpdaterClient.getDoc @project_id, @doc.id, (error, res, data) =>
throw error if error?
ranges = data.ranges
change = ranges.comments[0]
change.op.should.deep.equal { c: "bar", p: 4, t: @tid }
change.id.should.equal @tid
done()
, 200
});
return DocUpdaterClient.preloadDoc(this.project_id, this.doc.id, error => {
if (error != null) { throw error; }
return DocUpdaterClient.sendUpdate(this.project_id, this.doc.id, this.update, error => {
if (error != null) { throw error; }
return setTimeout(() => {
return DocUpdaterClient.getDoc(this.project_id, this.doc.id, (error, res, data) => {
if (error != null) { throw error; }
const {
ranges
} = data;
const change = ranges.comments[0];
change.op.should.deep.equal({ c: "bar", p: 4, t: this.tid });
change.id.should.equal(this.tid);
return done();
});
}
, 200);
});
});
});
it "should remove the comment range", (done) ->
DocUpdaterClient.removeComment @project_id, @doc.id, @tid, (error, res) =>
throw error if error?
expect(res.statusCode).to.equal 204
DocUpdaterClient.getDoc @project_id, @doc.id, (error, res, data) =>
throw error if error?
expect(data.ranges.comments).to.be.undefined
done()
return it("should remove the comment range", function(done) {
return DocUpdaterClient.removeComment(this.project_id, this.doc.id, this.tid, (error, res) => {
if (error != null) { throw error; }
expect(res.statusCode).to.equal(204);
return DocUpdaterClient.getDoc(this.project_id, this.doc.id, (error, res, data) => {
if (error != null) { throw error; }
expect(data.ranges.comments).to.be.undefined;
return done();
});
});
});
});
describe "tripping range size limit", ->
before (done) ->
@project_id = DocUpdaterClient.randomId()
@user_id = DocUpdaterClient.randomId()
@id_seed = DocUpdaterClient.randomId()
@doc = {
id: DocUpdaterClient.randomId()
describe("tripping range size limit", function() {
before(function(done) {
this.project_id = DocUpdaterClient.randomId();
this.user_id = DocUpdaterClient.randomId();
this.id_seed = DocUpdaterClient.randomId();
this.doc = {
id: DocUpdaterClient.randomId(),
lines: ["aaa"]
}
@i = new Array(3 * 1024 * 1024).join("a")
@updates = [{
doc: @doc.id
op: [{ i: @i, p: 1 }]
v: 0
meta: { user_id: @user_id, tc: @id_seed }
}]
MockWebApi.insertDoc @project_id, @doc.id, {
lines: @doc.lines
};
this.i = new Array(3 * 1024 * 1024).join("a");
this.updates = [{
doc: this.doc.id,
op: [{ i: this.i, p: 1 }],
v: 0,
meta: { user_id: this.user_id, tc: this.id_seed }
}];
MockWebApi.insertDoc(this.project_id, this.doc.id, {
lines: this.doc.lines,
version: 0
});
const jobs = [];
for (let update of Array.from(this.updates)) {
(update => {
return jobs.push(callback => DocUpdaterClient.sendUpdate(this.project_id, this.doc.id, update, callback));
})(update);
}
jobs = []
for update in @updates
do (update) =>
jobs.push (callback) => DocUpdaterClient.sendUpdate @project_id, @doc.id, update, callback
DocUpdaterClient.preloadDoc @project_id, @doc.id, (error) =>
throw error if error?
async.series jobs, (error) ->
throw error if error?
setTimeout done, 200
return DocUpdaterClient.preloadDoc(this.project_id, this.doc.id, error => {
if (error != null) { throw error; }
return async.series(jobs, function(error) {
if (error != null) { throw error; }
return setTimeout(done, 200);
});
});
});
it "should not update the ranges", (done) ->
DocUpdaterClient.getDoc @project_id, @doc.id, (error, res, data) =>
throw error if error?
ranges = data.ranges
expect(ranges.changes).to.be.undefined
done()
return it("should not update the ranges", function(done) {
return DocUpdaterClient.getDoc(this.project_id, this.doc.id, (error, res, data) => {
if (error != null) { throw error; }
const {
ranges
} = data;
expect(ranges.changes).to.be.undefined;
return done();
});
});
});
describe "deleting text surrounding a comment", ->
before (done) ->
@project_id = DocUpdaterClient.randomId()
@user_id = DocUpdaterClient.randomId()
@doc_id = DocUpdaterClient.randomId()
MockWebApi.insertDoc @project_id, @doc_id, {
lines: ["foo bar baz"]
version: 0
return describe("deleting text surrounding a comment", function() {
before(function(done) {
this.project_id = DocUpdaterClient.randomId();
this.user_id = DocUpdaterClient.randomId();
this.doc_id = DocUpdaterClient.randomId();
MockWebApi.insertDoc(this.project_id, this.doc_id, {
lines: ["foo bar baz"],
version: 0,
ranges: {
comments: [{
op: { c: "a", p: 5, tid: @tid = DocUpdaterClient.randomId() }
metadata:
user_id: @user_id
op: { c: "a", p: 5, tid: (this.tid = DocUpdaterClient.randomId()) },
metadata: {
user_id: this.user_id,
ts: new Date()
}
}]
}
}
@updates = [{
doc: @doc_id
op: [{ d: "foo ", p: 0 }]
v: 0
meta: { user_id: @user_id }
});
this.updates = [{
doc: this.doc_id,
op: [{ d: "foo ", p: 0 }],
v: 0,
meta: { user_id: this.user_id }
}, {
doc: @doc_id
op: [{ d: "bar ", p: 0 }]
v: 1
meta: { user_id: @user_id }
}]
jobs = []
for update in @updates
do (update) =>
jobs.push (callback) => DocUpdaterClient.sendUpdate @project_id, @doc_id, update, callback
DocUpdaterClient.preloadDoc @project_id, @doc_id, (error) =>
throw error if error?
async.series jobs, (error) ->
throw error if error?
setTimeout () =>
DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, data) =>
throw error if error?
done()
, 200
doc: this.doc_id,
op: [{ d: "bar ", p: 0 }],
v: 1,
meta: { user_id: this.user_id }
}];
const jobs = [];
for (let update of Array.from(this.updates)) {
(update => {
return jobs.push(callback => DocUpdaterClient.sendUpdate(this.project_id, this.doc_id, update, callback));
})(update);
}
return DocUpdaterClient.preloadDoc(this.project_id, this.doc_id, error => {
if (error != null) { throw error; }
return async.series(jobs, function(error) {
if (error != null) { throw error; }
return setTimeout(() => {
return DocUpdaterClient.getDoc(this.project_id, this.doc_id, (error, res, data) => {
if (error != null) { throw error; }
return done();
});
}
, 200);
});
});
});
it "should write a snapshot from before the destructive change", (done) ->
DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, data) =>
return done(error) if error?
db.docSnapshots.find {
project_id: ObjectId(@project_id),
doc_id: ObjectId(@doc_id)
}, (error, docSnapshots) =>
return done(error) if error?
expect(docSnapshots.length).to.equal 1
expect(docSnapshots[0].version).to.equal 1
expect(docSnapshots[0].lines).to.deep.equal ["bar baz"]
expect(docSnapshots[0].ranges.comments[0].op).to.deep.equal {
return it("should write a snapshot from before the destructive change", function(done) {
return DocUpdaterClient.getDoc(this.project_id, this.doc_id, (error, res, data) => {
if (error != null) { return done(error); }
return db.docSnapshots.find({
project_id: ObjectId(this.project_id),
doc_id: ObjectId(this.doc_id)
}, (error, docSnapshots) => {
if (error != null) { return done(error); }
expect(docSnapshots.length).to.equal(1);
expect(docSnapshots[0].version).to.equal(1);
expect(docSnapshots[0].lines).to.deep.equal(["bar baz"]);
expect(docSnapshots[0].ranges.comments[0].op).to.deep.equal({
c: "a",
p: 1,
tid: @tid
}
done()
tid: this.tid
});
return done();
});
});
});
});
});

View File

@@ -1,248 +1,320 @@
sinon = require "sinon"
chai = require("chai")
chai.should()
expect = require("chai").expect
Settings = require('settings-sharelatex')
rclient_du = require("redis-sharelatex").createClient(Settings.redis.documentupdater)
Keys = Settings.redis.documentupdater.key_schema
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* 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");
chai.should();
const {
expect
} = require("chai");
const Settings = require('settings-sharelatex');
const rclient_du = require("redis-sharelatex").createClient(Settings.redis.documentupdater);
const Keys = Settings.redis.documentupdater.key_schema;
MockTrackChangesApi = require "./helpers/MockTrackChangesApi"
MockProjectHistoryApi = require "./helpers/MockProjectHistoryApi"
MockWebApi = require "./helpers/MockWebApi"
DocUpdaterClient = require "./helpers/DocUpdaterClient"
DocUpdaterApp = require "./helpers/DocUpdaterApp"
const MockTrackChangesApi = require("./helpers/MockTrackChangesApi");
const MockProjectHistoryApi = require("./helpers/MockProjectHistoryApi");
const MockWebApi = require("./helpers/MockWebApi");
const DocUpdaterClient = require("./helpers/DocUpdaterClient");
const DocUpdaterApp = require("./helpers/DocUpdaterApp");
describe "Setting a document", ->
before (done) ->
@lines = ["one", "two", "three"]
@version = 42
@update =
doc: @doc_id
describe("Setting a document", function() {
before(function(done) {
this.lines = ["one", "two", "three"];
this.version = 42;
this.update = {
doc: this.doc_id,
op: [{
i: "one and a half\n"
i: "one and a half\n",
p: 4
}]
v: @version
@result = ["one", "one and a half", "two", "three"]
@newLines = ["these", "are", "the", "new", "lines"]
@source = "dropbox"
@user_id = "user-id-123"
}],
v: this.version
};
this.result = ["one", "one and a half", "two", "three"];
this.newLines = ["these", "are", "the", "new", "lines"];
this.source = "dropbox";
this.user_id = "user-id-123";
sinon.spy MockTrackChangesApi, "flushDoc"
sinon.spy MockProjectHistoryApi, "flushProject"
sinon.spy MockWebApi, "setDocument"
DocUpdaterApp.ensureRunning(done)
sinon.spy(MockTrackChangesApi, "flushDoc");
sinon.spy(MockProjectHistoryApi, "flushProject");
sinon.spy(MockWebApi, "setDocument");
return DocUpdaterApp.ensureRunning(done);
});
after ->
MockTrackChangesApi.flushDoc.restore()
MockProjectHistoryApi.flushProject.restore()
MockWebApi.setDocument.restore()
after(function() {
MockTrackChangesApi.flushDoc.restore();
MockProjectHistoryApi.flushProject.restore();
return MockWebApi.setDocument.restore();
});
describe "when the updated doc exists in the doc updater", ->
before (done) ->
[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
MockWebApi.insertDoc @project_id, @doc_id, lines: @lines, version: @version
DocUpdaterClient.preloadDoc @project_id, @doc_id, (error) =>
throw error if error?
DocUpdaterClient.sendUpdate @project_id, @doc_id, @update, (error) =>
throw error if error?
setTimeout () =>
DocUpdaterClient.setDocLines @project_id, @doc_id, @newLines, @source, @user_id, false, (error, res, body) =>
@statusCode = res.statusCode
done()
, 200
return null
describe("when the updated doc exists in the doc updater", function() {
before(function(done) {
[this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
MockWebApi.insertDoc(this.project_id, this.doc_id, {lines: this.lines, version: this.version});
DocUpdaterClient.preloadDoc(this.project_id, this.doc_id, error => {
if (error != null) { throw error; }
return DocUpdaterClient.sendUpdate(this.project_id, this.doc_id, this.update, error => {
if (error != null) { throw error; }
return setTimeout(() => {
return DocUpdaterClient.setDocLines(this.project_id, this.doc_id, this.newLines, this.source, this.user_id, false, (error, res, body) => {
this.statusCode = res.statusCode;
return done();
});
}
, 200);
});
});
return null;
});
after ->
MockTrackChangesApi.flushDoc.reset()
MockProjectHistoryApi.flushProject.reset()
MockWebApi.setDocument.reset()
after(function() {
MockTrackChangesApi.flushDoc.reset();
MockProjectHistoryApi.flushProject.reset();
return MockWebApi.setDocument.reset();
});
it "should return a 204 status code", ->
@statusCode.should.equal 204
it("should return a 204 status code", function() {
return this.statusCode.should.equal(204);
});
it "should send the updated doc lines and version to the web api", ->
MockWebApi.setDocument
.calledWith(@project_id, @doc_id, @newLines)
.should.equal true
it("should send the updated doc lines and version to the web api", function() {
return MockWebApi.setDocument
.calledWith(this.project_id, this.doc_id, this.newLines)
.should.equal(true);
});
it "should update the lines in the doc updater", (done) ->
DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, doc) =>
doc.lines.should.deep.equal @newLines
done()
return null
it("should update the lines in the doc updater", function(done) {
DocUpdaterClient.getDoc(this.project_id, this.doc_id, (error, res, doc) => {
doc.lines.should.deep.equal(this.newLines);
return done();
});
return null;
});
it "should bump the version in the doc updater", (done) ->
DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, doc) =>
doc.version.should.equal @version + 2
done()
return null
it("should bump the version in the doc updater", function(done) {
DocUpdaterClient.getDoc(this.project_id, this.doc_id, (error, res, doc) => {
doc.version.should.equal(this.version + 2);
return done();
});
return null;
});
it "should leave the document in redis", (done) ->
rclient_du.get Keys.docLines({doc_id: @doc_id}), (error, lines) =>
throw error if error?
expect(JSON.parse(lines)).to.deep.equal @newLines
done()
return null
return it("should leave the document in redis", function(done) {
rclient_du.get(Keys.docLines({doc_id: this.doc_id}), (error, lines) => {
if (error != null) { throw error; }
expect(JSON.parse(lines)).to.deep.equal(this.newLines);
return done();
});
return null;
});
});
describe "when the updated doc does not exist in the doc updater", ->
before (done) ->
[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
MockWebApi.insertDoc @project_id, @doc_id, {lines: @lines, version: @version}
DocUpdaterClient.setDocLines @project_id, @doc_id, @newLines, @source, @user_id, false, (error, res, body) =>
@statusCode = res.statusCode
setTimeout done, 200
return null
describe("when the updated doc does not exist in the doc updater", function() {
before(function(done) {
[this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
MockWebApi.insertDoc(this.project_id, this.doc_id, {lines: this.lines, version: this.version});
DocUpdaterClient.setDocLines(this.project_id, this.doc_id, this.newLines, this.source, this.user_id, false, (error, res, body) => {
this.statusCode = res.statusCode;
return setTimeout(done, 200);
});
return null;
});
after ->
MockTrackChangesApi.flushDoc.reset()
MockProjectHistoryApi.flushProject.reset()
MockWebApi.setDocument.reset()
after(function() {
MockTrackChangesApi.flushDoc.reset();
MockProjectHistoryApi.flushProject.reset();
return MockWebApi.setDocument.reset();
});
it "should return a 204 status code", ->
@statusCode.should.equal 204
it("should return a 204 status code", function() {
return this.statusCode.should.equal(204);
});
it "should send the updated doc lines to the web api", ->
MockWebApi.setDocument
.calledWith(@project_id, @doc_id, @newLines)
.should.equal true
it("should send the updated doc lines to the web api", function() {
return MockWebApi.setDocument
.calledWith(this.project_id, this.doc_id, this.newLines)
.should.equal(true);
});
it "should flush track changes", ->
MockTrackChangesApi.flushDoc.calledWith(@doc_id).should.equal true
it("should flush track changes", function() {
return MockTrackChangesApi.flushDoc.calledWith(this.doc_id).should.equal(true);
});
it "should flush project history", ->
MockProjectHistoryApi.flushProject.calledWith(@project_id).should.equal true
it("should flush project history", function() {
return MockProjectHistoryApi.flushProject.calledWith(this.project_id).should.equal(true);
});
it "should remove the document from redis", (done) ->
rclient_du.get Keys.docLines({doc_id: @doc_id}), (error, lines) =>
throw error if error?
expect(lines).to.not.exist
done()
return null
return it("should remove the document from redis", function(done) {
rclient_du.get(Keys.docLines({doc_id: this.doc_id}), (error, lines) => {
if (error != null) { throw error; }
expect(lines).to.not.exist;
return done();
});
return null;
});
});
describe "when the updated doc is too large for the body parser", ->
before (done) ->
[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
MockWebApi.insertDoc @project_id, @doc_id, {lines: @lines, version: @version}
@newLines = []
while JSON.stringify(@newLines).length < Settings.max_doc_length + 64 * 1024
@newLines.push("(a long line of text)".repeat(10000))
DocUpdaterClient.setDocLines @project_id, @doc_id, @newLines, @source, @user_id, false, (error, res, body) =>
@statusCode = res.statusCode
setTimeout done, 200
return null
describe("when the updated doc is too large for the body parser", function() {
before(function(done) {
[this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
MockWebApi.insertDoc(this.project_id, this.doc_id, {lines: this.lines, version: this.version});
this.newLines = [];
while (JSON.stringify(this.newLines).length < (Settings.max_doc_length + (64 * 1024))) {
this.newLines.push("(a long line of text)".repeat(10000));
}
DocUpdaterClient.setDocLines(this.project_id, this.doc_id, this.newLines, this.source, this.user_id, false, (error, res, body) => {
this.statusCode = res.statusCode;
return setTimeout(done, 200);
});
return null;
});
after ->
MockTrackChangesApi.flushDoc.reset()
MockProjectHistoryApi.flushProject.reset()
MockWebApi.setDocument.reset()
after(function() {
MockTrackChangesApi.flushDoc.reset();
MockProjectHistoryApi.flushProject.reset();
return MockWebApi.setDocument.reset();
});
it "should return a 413 status code", ->
@statusCode.should.equal 413
it("should return a 413 status code", function() {
return this.statusCode.should.equal(413);
});
it "should not send the updated doc lines to the web api", ->
MockWebApi.setDocument.called.should.equal false
it("should not send the updated doc lines to the web api", () => MockWebApi.setDocument.called.should.equal(false));
it "should not flush track changes", ->
MockTrackChangesApi.flushDoc.called.should.equal false
it("should not flush track changes", () => MockTrackChangesApi.flushDoc.called.should.equal(false));
it "should not flush project history", ->
MockProjectHistoryApi.flushProject.called.should.equal false
return it("should not flush project history", () => MockProjectHistoryApi.flushProject.called.should.equal(false));
});
describe "when the updated doc is large but under the bodyParser and HTTPController size limit", ->
before (done) ->
[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
MockWebApi.insertDoc @project_id, @doc_id, {lines: @lines, version: @version}
describe("when the updated doc is large but under the bodyParser and HTTPController size limit", function() {
before(function(done) {
[this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
MockWebApi.insertDoc(this.project_id, this.doc_id, {lines: this.lines, version: this.version});
@newLines = []
while JSON.stringify(@newLines).length < 2 * 1024 * 1024 # limit in HTTPController
@newLines.push("(a long line of text)".repeat(10000))
@newLines.pop() # remove the line which took it over the limit
DocUpdaterClient.setDocLines @project_id, @doc_id, @newLines, @source, @user_id, false, (error, res, body) =>
@statusCode = res.statusCode
setTimeout done, 200
return null
this.newLines = [];
while (JSON.stringify(this.newLines).length < (2 * 1024 * 1024)) { // limit in HTTPController
this.newLines.push("(a long line of text)".repeat(10000));
}
this.newLines.pop(); // remove the line which took it over the limit
DocUpdaterClient.setDocLines(this.project_id, this.doc_id, this.newLines, this.source, this.user_id, false, (error, res, body) => {
this.statusCode = res.statusCode;
return setTimeout(done, 200);
});
return null;
});
after ->
MockTrackChangesApi.flushDoc.reset()
MockProjectHistoryApi.flushProject.reset()
MockWebApi.setDocument.reset()
after(function() {
MockTrackChangesApi.flushDoc.reset();
MockProjectHistoryApi.flushProject.reset();
return MockWebApi.setDocument.reset();
});
it "should return a 204 status code", ->
@statusCode.should.equal 204
it("should return a 204 status code", function() {
return this.statusCode.should.equal(204);
});
it "should send the updated doc lines to the web api", ->
MockWebApi.setDocument
.calledWith(@project_id, @doc_id, @newLines)
.should.equal true
return it("should send the updated doc lines to the web api", function() {
return MockWebApi.setDocument
.calledWith(this.project_id, this.doc_id, this.newLines)
.should.equal(true);
});
});
describe "with track changes", ->
before ->
@lines = ["one", "one and a half", "two", "three"]
@id_seed = "587357bd35e64f6157"
@update =
doc: @doc_id
return describe("with track changes", function() {
before(function() {
this.lines = ["one", "one and a half", "two", "three"];
this.id_seed = "587357bd35e64f6157";
return this.update = {
doc: this.doc_id,
op: [{
d: "one and a half\n"
d: "one and a half\n",
p: 4
}]
meta:
tc: @id_seed
user_id: @user_id
v: @version
}],
meta: {
tc: this.id_seed,
user_id: this.user_id
},
v: this.version
};
});
describe "with the undo flag", ->
before (done) ->
[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
MockWebApi.insertDoc @project_id, @doc_id, {lines: @lines, version: @version}
DocUpdaterClient.preloadDoc @project_id, @doc_id, (error) =>
throw error if error?
DocUpdaterClient.sendUpdate @project_id, @doc_id, @update, (error) =>
throw error if error?
# Go back to old lines, with undo flag
DocUpdaterClient.setDocLines @project_id, @doc_id, @lines, @source, @user_id, true, (error, res, body) =>
@statusCode = res.statusCode
setTimeout done, 200
return null
describe("with the undo flag", function() {
before(function(done) {
[this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
MockWebApi.insertDoc(this.project_id, this.doc_id, {lines: this.lines, version: this.version});
DocUpdaterClient.preloadDoc(this.project_id, this.doc_id, error => {
if (error != null) { throw error; }
return DocUpdaterClient.sendUpdate(this.project_id, this.doc_id, this.update, error => {
if (error != null) { throw error; }
// Go back to old lines, with undo flag
return DocUpdaterClient.setDocLines(this.project_id, this.doc_id, this.lines, this.source, this.user_id, true, (error, res, body) => {
this.statusCode = res.statusCode;
return setTimeout(done, 200);
});
});
});
return null;
});
after ->
MockTrackChangesApi.flushDoc.reset()
MockProjectHistoryApi.flushProject.reset()
MockWebApi.setDocument.reset()
after(function() {
MockTrackChangesApi.flushDoc.reset();
MockProjectHistoryApi.flushProject.reset();
return MockWebApi.setDocument.reset();
});
it "should undo the tracked changes", (done) ->
DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, data) =>
throw error if error?
ranges = data.ranges
expect(ranges.changes).to.be.undefined
done()
return null
return it("should undo the tracked changes", function(done) {
DocUpdaterClient.getDoc(this.project_id, this.doc_id, (error, res, data) => {
if (error != null) { throw error; }
const {
ranges
} = data;
expect(ranges.changes).to.be.undefined;
return done();
});
return null;
});
});
describe "without the undo flag", ->
before (done) ->
[@project_id, @doc_id] = [DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]
MockWebApi.insertDoc @project_id, @doc_id, {lines: @lines, version: @version}
DocUpdaterClient.preloadDoc @project_id, @doc_id, (error) =>
throw error if error?
DocUpdaterClient.sendUpdate @project_id, @doc_id, @update, (error) =>
throw error if error?
# Go back to old lines, without undo flag
DocUpdaterClient.setDocLines @project_id, @doc_id, @lines, @source, @user_id, false, (error, res, body) =>
@statusCode = res.statusCode
setTimeout done, 200
return null
return describe("without the undo flag", function() {
before(function(done) {
[this.project_id, this.doc_id] = Array.from([DocUpdaterClient.randomId(), DocUpdaterClient.randomId()]);
MockWebApi.insertDoc(this.project_id, this.doc_id, {lines: this.lines, version: this.version});
DocUpdaterClient.preloadDoc(this.project_id, this.doc_id, error => {
if (error != null) { throw error; }
return DocUpdaterClient.sendUpdate(this.project_id, this.doc_id, this.update, error => {
if (error != null) { throw error; }
// Go back to old lines, without undo flag
return DocUpdaterClient.setDocLines(this.project_id, this.doc_id, this.lines, this.source, this.user_id, false, (error, res, body) => {
this.statusCode = res.statusCode;
return setTimeout(done, 200);
});
});
});
return null;
});
after ->
MockTrackChangesApi.flushDoc.reset()
MockProjectHistoryApi.flushProject.reset()
MockWebApi.setDocument.reset()
after(function() {
MockTrackChangesApi.flushDoc.reset();
MockProjectHistoryApi.flushProject.reset();
return MockWebApi.setDocument.reset();
});
it "should not undo the tracked changes", (done) ->
DocUpdaterClient.getDoc @project_id, @doc_id, (error, res, data) =>
throw error if error?
ranges = data.ranges
expect(ranges.changes.length).to.equal 1
done()
return null
return it("should not undo the tracked changes", function(done) {
DocUpdaterClient.getDoc(this.project_id, this.doc_id, (error, res, data) => {
if (error != null) { throw error; }
const {
ranges
} = data;
expect(ranges.changes.length).to.equal(1);
return done();
});
return null;
});
});
});
});

View File

@@ -1,20 +1,38 @@
app = require('../../../../app')
require("logger-sharelatex").logger.level("fatal")
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* DS102: Remove unnecessary code created because of implicit returns
* DS205: Consider reworking code to avoid use of IIFEs
* DS207: Consider shorter variations of null checks
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const app = require('../../../../app');
require("logger-sharelatex").logger.level("fatal");
module.exports =
running: false
initing: false
callbacks: []
ensureRunning: (callback = (error) ->) ->
if @running
return callback()
else if @initing
@callbacks.push callback
else
@initing = true
@callbacks.push callback
app.listen 3003, "localhost", (error) =>
throw error if error?
@running = true
for callback in @callbacks
callback()
module.exports = {
running: false,
initing: false,
callbacks: [],
ensureRunning(callback) {
if (callback == null) { callback = function(error) {}; }
if (this.running) {
return callback();
} else if (this.initing) {
return this.callbacks.push(callback);
} else {
this.initing = true;
this.callbacks.push(callback);
return app.listen(3003, "localhost", error => {
if (error != null) { throw error; }
this.running = true;
return (() => {
const result = [];
for (callback of Array.from(this.callbacks)) {
result.push(callback());
}
return result;
})();
});
}
}
};

View File

@@ -1,111 +1,171 @@
Settings = require('settings-sharelatex')
rclient = require("redis-sharelatex").createClient(Settings.redis.documentupdater)
keys = Settings.redis.documentupdater.key_schema
request = require("request").defaults(jar: false)
async = require "async"
/*
* decaffeinate suggestions:
* DS101: Remove unnecessary use of Array.from
* 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
*/
let DocUpdaterClient;
const Settings = require('settings-sharelatex');
const rclient = require("redis-sharelatex").createClient(Settings.redis.documentupdater);
const keys = Settings.redis.documentupdater.key_schema;
const request = require("request").defaults({jar: false});
const async = require("async");
rclient_sub = require("redis-sharelatex").createClient(Settings.redis.pubsub)
rclient_sub.subscribe "applied-ops"
rclient_sub.setMaxListeners(0)
const rclient_sub = require("redis-sharelatex").createClient(Settings.redis.pubsub);
rclient_sub.subscribe("applied-ops");
rclient_sub.setMaxListeners(0);
module.exports = DocUpdaterClient =
randomId: () ->
chars = for i in [1..24]
Math.random().toString(16)[2]
return chars.join("")
module.exports = (DocUpdaterClient = {
randomId() {
const chars = __range__(1, 24, true).map((i) =>
Math.random().toString(16)[2]);
return chars.join("");
},
subscribeToAppliedOps: (callback = (message) ->) ->
rclient_sub.on "message", callback
subscribeToAppliedOps(callback) {
if (callback == null) { callback = function(message) {}; }
return rclient_sub.on("message", callback);
},
sendUpdate: (project_id, doc_id, update, callback = (error) ->) ->
rclient.rpush keys.pendingUpdates({doc_id}), JSON.stringify(update), (error)->
return callback(error) if error?
doc_key = "#{project_id}:#{doc_id}"
rclient.sadd "DocsWithPendingUpdates", doc_key, (error) ->
return callback(error) if error?
rclient.rpush "pending-updates-list", doc_key, callback
sendUpdate(project_id, doc_id, update, callback) {
if (callback == null) { callback = function(error) {}; }
return rclient.rpush(keys.pendingUpdates({doc_id}), JSON.stringify(update), function(error){
if (error != null) { return callback(error); }
const doc_key = `${project_id}:${doc_id}`;
return rclient.sadd("DocsWithPendingUpdates", doc_key, function(error) {
if (error != null) { return callback(error); }
return rclient.rpush("pending-updates-list", doc_key, callback);
});
});
},
sendUpdates: (project_id, doc_id, updates, callback = (error) ->) ->
DocUpdaterClient.preloadDoc project_id, doc_id, (error) ->
return callback(error) if error?
jobs = []
for update in updates
do (update) ->
jobs.push (callback) ->
DocUpdaterClient.sendUpdate project_id, doc_id, update, callback
async.series jobs, (err) ->
DocUpdaterClient.waitForPendingUpdates project_id, doc_id, callback
sendUpdates(project_id, doc_id, updates, callback) {
if (callback == null) { callback = function(error) {}; }
return DocUpdaterClient.preloadDoc(project_id, doc_id, function(error) {
if (error != null) { return callback(error); }
const jobs = [];
for (let update of Array.from(updates)) {
((update => jobs.push(callback => DocUpdaterClient.sendUpdate(project_id, doc_id, update, callback))))(update);
}
return async.series(jobs, err => DocUpdaterClient.waitForPendingUpdates(project_id, doc_id, callback));
});
},
waitForPendingUpdates: (project_id, doc_id, callback) ->
async.retry {times: 30, interval: 100}, (cb) ->
rclient.llen keys.pendingUpdates({doc_id}), (err, length) ->
if length > 0
cb(new Error("updates still pending"))
else
cb()
, callback
waitForPendingUpdates(project_id, doc_id, callback) {
return async.retry({times: 30, interval: 100}, cb => rclient.llen(keys.pendingUpdates({doc_id}), function(err, length) {
if (length > 0) {
return cb(new Error("updates still pending"));
} else {
return cb();
}
})
, callback);
},
getDoc: (project_id, doc_id, callback = (error, res, body) ->) ->
request.get "http://localhost:3003/project/#{project_id}/doc/#{doc_id}", (error, res, body) ->
if body? and res.statusCode >= 200 and res.statusCode < 300
body = JSON.parse(body)
callback error, res, body
getDoc(project_id, doc_id, callback) {
if (callback == null) { callback = function(error, res, body) {}; }
return request.get(`http://localhost:3003/project/${project_id}/doc/${doc_id}`, function(error, res, body) {
if ((body != null) && (res.statusCode >= 200) && (res.statusCode < 300)) {
body = JSON.parse(body);
}
return callback(error, res, body);
});
},
getDocAndRecentOps: (project_id, doc_id, fromVersion, callback = (error, res, body) ->) ->
request.get "http://localhost:3003/project/#{project_id}/doc/#{doc_id}?fromVersion=#{fromVersion}", (error, res, body) ->
if body? and res.statusCode >= 200 and res.statusCode < 300
body = JSON.parse(body)
callback error, res, body
getDocAndRecentOps(project_id, doc_id, fromVersion, callback) {
if (callback == null) { callback = function(error, res, body) {}; }
return request.get(`http://localhost:3003/project/${project_id}/doc/${doc_id}?fromVersion=${fromVersion}`, function(error, res, body) {
if ((body != null) && (res.statusCode >= 200) && (res.statusCode < 300)) {
body = JSON.parse(body);
}
return callback(error, res, body);
});
},
preloadDoc: (project_id, doc_id, callback = (error) ->) ->
DocUpdaterClient.getDoc project_id, doc_id, callback
preloadDoc(project_id, doc_id, callback) {
if (callback == null) { callback = function(error) {}; }
return DocUpdaterClient.getDoc(project_id, doc_id, callback);
},
flushDoc: (project_id, doc_id, callback = (error) ->) ->
request.post "http://localhost:3003/project/#{project_id}/doc/#{doc_id}/flush", (error, res, body) ->
callback error, res, body
flushDoc(project_id, doc_id, callback) {
if (callback == null) { callback = function(error) {}; }
return request.post(`http://localhost:3003/project/${project_id}/doc/${doc_id}/flush`, (error, res, body) => callback(error, res, body));
},
setDocLines: (project_id, doc_id, lines, source, user_id, undoing, callback = (error) ->) ->
request.post {
url: "http://localhost:3003/project/#{project_id}/doc/#{doc_id}"
json:
lines: lines
source: source
user_id: user_id
undoing: undoing
}, (error, res, body) ->
callback error, res, body
setDocLines(project_id, doc_id, lines, source, user_id, undoing, callback) {
if (callback == null) { callback = function(error) {}; }
return request.post({
url: `http://localhost:3003/project/${project_id}/doc/${doc_id}`,
json: {
lines,
source,
user_id,
undoing
}
}, (error, res, body) => callback(error, res, body));
},
deleteDoc: (project_id, doc_id, callback = (error) ->) ->
request.del "http://localhost:3003/project/#{project_id}/doc/#{doc_id}", (error, res, body) ->
callback error, res, body
deleteDoc(project_id, doc_id, callback) {
if (callback == null) { callback = function(error) {}; }
return request.del(`http://localhost:3003/project/${project_id}/doc/${doc_id}`, (error, res, body) => callback(error, res, body));
},
flushProject: (project_id, callback = () ->) ->
request.post "http://localhost:3003/project/#{project_id}/flush", callback
flushProject(project_id, callback) {
if (callback == null) { callback = function() {}; }
return request.post(`http://localhost:3003/project/${project_id}/flush`, callback);
},
deleteProject: (project_id, callback = () ->) ->
request.del "http://localhost:3003/project/#{project_id}", callback
deleteProject(project_id, callback) {
if (callback == null) { callback = function() {}; }
return request.del(`http://localhost:3003/project/${project_id}`, callback);
},
deleteProjectOnShutdown: (project_id, callback = () ->) ->
request.del "http://localhost:3003/project/#{project_id}?background=true&shutdown=true", callback
deleteProjectOnShutdown(project_id, callback) {
if (callback == null) { callback = function() {}; }
return request.del(`http://localhost:3003/project/${project_id}?background=true&shutdown=true`, callback);
},
flushOldProjects: (callback = () ->) ->
request.get "http://localhost:3003/flush_queued_projects?min_delete_age=1", callback
flushOldProjects(callback) {
if (callback == null) { callback = function() {}; }
return request.get("http://localhost:3003/flush_queued_projects?min_delete_age=1", callback);
},
acceptChange: (project_id, doc_id, change_id, callback = () ->) ->
request.post "http://localhost:3003/project/#{project_id}/doc/#{doc_id}/change/#{change_id}/accept", callback
acceptChange(project_id, doc_id, change_id, callback) {
if (callback == null) { callback = function() {}; }
return request.post(`http://localhost:3003/project/${project_id}/doc/${doc_id}/change/${change_id}/accept`, callback);
},
removeComment: (project_id, doc_id, comment, callback = () ->) ->
request.del "http://localhost:3003/project/#{project_id}/doc/#{doc_id}/comment/#{comment}", callback
removeComment(project_id, doc_id, comment, callback) {
if (callback == null) { callback = function() {}; }
return request.del(`http://localhost:3003/project/${project_id}/doc/${doc_id}/comment/${comment}`, callback);
},
getProjectDocs: (project_id, projectStateHash, callback = () ->) ->
request.get "http://localhost:3003/project/#{project_id}/doc?state=#{projectStateHash}", (error, res, body) ->
if body? and res.statusCode >= 200 and res.statusCode < 300
body = JSON.parse(body)
callback error, res, body
getProjectDocs(project_id, projectStateHash, callback) {
if (callback == null) { callback = function() {}; }
return request.get(`http://localhost:3003/project/${project_id}/doc?state=${projectStateHash}`, function(error, res, body) {
if ((body != null) && (res.statusCode >= 200) && (res.statusCode < 300)) {
body = JSON.parse(body);
}
return callback(error, res, body);
});
},
sendProjectUpdate: (project_id, userId, docUpdates, fileUpdates, version, callback = (error) ->) ->
request.post {
url: "http://localhost:3003/project/#{project_id}"
sendProjectUpdate(project_id, userId, docUpdates, fileUpdates, version, callback) {
if (callback == null) { callback = function(error) {}; }
return request.post({
url: `http://localhost:3003/project/${project_id}`,
json: { userId, docUpdates, fileUpdates, version }
}, (error, res, body) ->
callback error, res, body
}, (error, res, body) => callback(error, res, body));
}
});
function __range__(left, right, inclusive) {
let range = [];
let ascending = left < right;
let end = !inclusive ? right : ascending ? right + 1 : right - 1;
for (let i = left; ascending ? i < end : i > end; ascending ? i++ : i--) {
range.push(i);
}
return range;
}

View File

@@ -1,19 +1,34 @@
express = require("express")
app = express()
/*
* 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
*/
let MockProjectHistoryApi;
const express = require("express");
const app = express();
module.exports = MockProjectHistoryApi =
flushProject: (doc_id, callback = (error) ->) ->
callback()
module.exports = (MockProjectHistoryApi = {
flushProject(doc_id, callback) {
if (callback == null) { callback = function(error) {}; }
return callback();
},
run: () ->
app.post "/project/:project_id/flush", (req, res, next) =>
@flushProject req.params.project_id, (error) ->
if error?
res.sendStatus 500
else
res.sendStatus 204
run() {
app.post("/project/:project_id/flush", (req, res, next) => {
return this.flushProject(req.params.project_id, function(error) {
if (error != null) {
return res.sendStatus(500);
} else {
return res.sendStatus(204);
}
});
});
app.listen 3054, (error) ->
throw error if error?
return app.listen(3054, function(error) {
if (error != null) { throw error; }
});
}
});
MockProjectHistoryApi.run()
MockProjectHistoryApi.run();

View File

@@ -1,23 +1,38 @@
express = require("express")
app = express()
/*
* 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
*/
let MockTrackChangesApi;
const express = require("express");
const app = express();
module.exports = MockTrackChangesApi =
flushDoc: (doc_id, callback = (error) ->) ->
callback()
module.exports = (MockTrackChangesApi = {
flushDoc(doc_id, callback) {
if (callback == null) { callback = function(error) {}; }
return callback();
},
run: () ->
app.post "/project/:project_id/doc/:doc_id/flush", (req, res, next) =>
@flushDoc req.params.doc_id, (error) ->
if error?
res.sendStatus 500
else
res.sendStatus 204
run() {
app.post("/project/:project_id/doc/:doc_id/flush", (req, res, next) => {
return this.flushDoc(req.params.doc_id, function(error) {
if (error != null) {
return res.sendStatus(500);
} else {
return res.sendStatus(204);
}
});
});
app.listen 3015, (error) ->
throw error if error?
.on "error", (error) ->
console.error "error starting MockTrackChangesApi:", error.message
process.exit(1)
return app.listen(3015, function(error) {
if (error != null) { throw error; }
}).on("error", function(error) {
console.error("error starting MockTrackChangesApi:", error.message);
return process.exit(1);
});
}
});
MockTrackChangesApi.run()
MockTrackChangesApi.run();

View File

@@ -1,54 +1,75 @@
express = require("express")
bodyParser = require("body-parser")
app = express()
MAX_REQUEST_SIZE = 2*(2*1024*1024 + 64*1024)
/*
* 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
*/
let MockWebApi;
const express = require("express");
const bodyParser = require("body-parser");
const app = express();
const MAX_REQUEST_SIZE = 2*((2*1024*1024) + (64*1024));
module.exports = MockWebApi =
docs: {}
module.exports = (MockWebApi = {
docs: {},
clearDocs: () -> @docs = {}
clearDocs() { return this.docs = {}; },
insertDoc: (project_id, doc_id, doc) ->
doc.version ?= 0
doc.lines ?= []
doc.pathname = '/a/b/c.tex'
@docs["#{project_id}:#{doc_id}"] = doc
insertDoc(project_id, doc_id, doc) {
if (doc.version == null) { doc.version = 0; }
if (doc.lines == null) { doc.lines = []; }
doc.pathname = '/a/b/c.tex';
return this.docs[`${project_id}:${doc_id}`] = doc;
},
setDocument: (project_id, doc_id, lines, version, ranges, lastUpdatedAt, lastUpdatedBy, callback = (error) ->) ->
doc = @docs["#{project_id}:#{doc_id}"] ||= {}
doc.lines = lines
doc.version = version
doc.ranges = ranges
doc.pathname = '/a/b/c.tex'
doc.lastUpdatedAt = lastUpdatedAt
doc.lastUpdatedBy = lastUpdatedBy
callback null
setDocument(project_id, doc_id, lines, version, ranges, lastUpdatedAt, lastUpdatedBy, callback) {
if (callback == null) { callback = function(error) {}; }
const doc = this.docs[`${project_id}:${doc_id}`] || (this.docs[`${project_id}:${doc_id}`] = {});
doc.lines = lines;
doc.version = version;
doc.ranges = ranges;
doc.pathname = '/a/b/c.tex';
doc.lastUpdatedAt = lastUpdatedAt;
doc.lastUpdatedBy = lastUpdatedBy;
return callback(null);
},
getDocument: (project_id, doc_id, callback = (error, doc) ->) ->
callback null, @docs["#{project_id}:#{doc_id}"]
getDocument(project_id, doc_id, callback) {
if (callback == null) { callback = function(error, doc) {}; }
return callback(null, this.docs[`${project_id}:${doc_id}`]);
},
run: () ->
app.get "/project/:project_id/doc/:doc_id", (req, res, next) =>
@getDocument req.params.project_id, req.params.doc_id, (error, doc) ->
if error?
res.sendStatus 500
else if doc?
res.send JSON.stringify doc
else
res.sendStatus 404
run() {
app.get("/project/:project_id/doc/:doc_id", (req, res, next) => {
return this.getDocument(req.params.project_id, req.params.doc_id, function(error, doc) {
if (error != null) {
return res.sendStatus(500);
} else if (doc != null) {
return res.send(JSON.stringify(doc));
} else {
return res.sendStatus(404);
}
});
});
app.post "/project/:project_id/doc/:doc_id", bodyParser.json({limit: MAX_REQUEST_SIZE}), (req, res, next) =>
MockWebApi.setDocument req.params.project_id, req.params.doc_id, req.body.lines, req.body.version, req.body.ranges, req.body.lastUpdatedAt, req.body.lastUpdatedBy, (error) ->
if error?
res.sendStatus 500
else
res.sendStatus 204
app.post("/project/:project_id/doc/:doc_id", bodyParser.json({limit: MAX_REQUEST_SIZE}), (req, res, next) => {
return MockWebApi.setDocument(req.params.project_id, req.params.doc_id, req.body.lines, req.body.version, req.body.ranges, req.body.lastUpdatedAt, req.body.lastUpdatedBy, function(error) {
if (error != null) {
return res.sendStatus(500);
} else {
return res.sendStatus(204);
}
});
});
app.listen 3000, (error) ->
throw error if error?
.on "error", (error) ->
console.error "error starting MockWebApi:", error.message
process.exit(1)
return app.listen(3000, function(error) {
if (error != null) { throw error; }
}).on("error", function(error) {
console.error("error starting MockWebApi:", error.message);
return process.exit(1);
});
}
});
MockWebApi.run()
MockWebApi.run();