overleaf/services/document-updater/test/unit/js/RedisManager/RedisManagerTests.js

1008 lines
35 KiB
JavaScript
Raw Normal View History

/* eslint-disable
camelcase,
mocha/no-identical-title,
no-return-assign,
no-unused-vars,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* decaffeinate suggestions:
* DS102: Remove unnecessary code created because of implicit returns
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
*/
const sinon = require('sinon');
const chai = require('chai');
const should = chai.should();
const modulePath = "../../../../app/js/RedisManager.js";
const SandboxedModule = require('sandboxed-module');
const Errors = require("../../../../app/js/Errors");
const crypto = require("crypto");
const tk = require("timekeeper");
describe("RedisManager", function() {
beforeEach(function() {
let Timer;
this.multi = {exec: sinon.stub()};
this.rclient = {multi: () => this.multi};
tk.freeze(new Date());
this.RedisManager = SandboxedModule.require(modulePath, {
requires: {
"logger-sharelatex": (this.logger = { error: sinon.stub(), log: sinon.stub(), warn: sinon.stub() }),
"./ProjectHistoryRedisManager": (this.ProjectHistoryRedisManager = {}),
"settings-sharelatex": (this.settings = {
documentupdater: {logHashErrors: {write:true, read:true}},
apis: {
project_history: {enabled: true}
},
redis: {
documentupdater: {
key_schema: {
blockingKey({doc_id}) { return `Blocking:${doc_id}`; },
docLines({doc_id}) { return `doclines:${doc_id}`; },
docOps({doc_id}) { return `DocOps:${doc_id}`; },
docVersion({doc_id}) { return `DocVersion:${doc_id}`; },
docHash({doc_id}) { return `DocHash:${doc_id}`; },
projectKey({doc_id}) { return `ProjectId:${doc_id}`; },
pendingUpdates({doc_id}) { return `PendingUpdates:${doc_id}`; },
docsInProject({project_id}) { return `DocsIn:${project_id}`; },
ranges({doc_id}) { return `Ranges:${doc_id}`; },
pathname({doc_id}) { return `Pathname:${doc_id}`; },
projectHistoryId({doc_id}) { return `ProjectHistoryId:${doc_id}`; },
projectHistoryType({doc_id}) { return `ProjectHistoryType:${doc_id}`; },
projectState({project_id}) { return `ProjectState:${project_id}`; },
unflushedTime({doc_id}) { return `UnflushedTime:${doc_id}`; },
lastUpdatedBy({doc_id}) { return `lastUpdatedBy:${doc_id}`; },
lastUpdatedAt({doc_id}) { return `lastUpdatedAt:${doc_id}`; }
}
},
history: {
key_schema: {
uncompressedHistoryOps({doc_id}) { return `UncompressedHistoryOps:${doc_id}`; },
docsWithHistoryOps({project_id}) { return `DocsWithHistoryOps:${project_id}`; }
}
}
}
}),
"redis-sharelatex": {
createClient: () => this.rclient
},
"./Metrics": (this.metrics = {
inc: sinon.stub(),
summary: sinon.stub(),
Timer: (Timer = class Timer {
constructor() {
this.start = new Date();
}
done() {
const timeSpan = new Date - this.start;
return timeSpan;
}
})
}),
"./Errors": Errors
},
globals: {
JSON: (this.JSON = JSON)
}
}
);
this.doc_id = "doc-id-123";
this.project_id = "project-id-123";
this.projectHistoryId = 123;
return this.callback = sinon.stub();
});
afterEach(function() { return tk.reset(); });
describe("getDoc", function() {
beforeEach(function() {
this.lines = ["one", "two", "three", "これは"]; // include some utf8
this.jsonlines = JSON.stringify(this.lines);
this.version = 42;
this.hash = crypto.createHash('sha1').update(this.jsonlines,'utf8').digest('hex');
this.ranges = { comments: "mock", entries: "mock" };
this.json_ranges = JSON.stringify(this.ranges);
this.unflushed_time = 12345;
this.pathname = '/a/b/c.tex';
this.multi.get = sinon.stub();
this.multi.exec = sinon.stub().callsArgWith(0, null, [this.jsonlines, this.version, this.hash, this.project_id, this.json_ranges, this.pathname, this.projectHistoryId.toString(), this.unflushed_time]);
return this.rclient.sadd = sinon.stub().yields(null, 0);
});
describe("successfully", function() {
beforeEach(function() {
return this.RedisManager.getDoc(this.project_id, this.doc_id, this.callback);
});
it("should get the lines from redis", function() {
return this.multi.get
.calledWith(`doclines:${this.doc_id}`)
.should.equal(true);
});
it("should get the version from", function() {
return this.multi.get
.calledWith(`DocVersion:${this.doc_id}`)
.should.equal(true);
});
it('should get the hash', function() {
return this.multi.get
.calledWith(`DocHash:${this.doc_id}`)
.should.equal(true);
});
it("should get the ranges", function() {
return this.multi.get
.calledWith(`Ranges:${this.doc_id}`)
.should.equal(true);
});
it("should get the unflushed time", function() {
return this.multi.get
.calledWith(`UnflushedTime:${this.doc_id}`)
.should.equal(true);
});
it("should get the pathname", function() {
return this.multi.get
.calledWith(`Pathname:${this.doc_id}`)
.should.equal(true);
});
it("should get the projectHistoryId as an integer", function() {
return this.multi.get
.calledWith(`ProjectHistoryId:${this.doc_id}`)
.should.equal(true);
});
it("should get lastUpdatedAt", function() {
return this.multi.get
.calledWith(`lastUpdatedAt:${this.doc_id}`)
.should.equal(true);
});
it("should get lastUpdatedBy", function() {
return this.multi.get
.calledWith(`lastUpdatedBy:${this.doc_id}`)
.should.equal(true);
});
it("should check if the document is in the DocsIn set", function() {
return this.rclient.sadd
.calledWith(`DocsIn:${this.project_id}`)
.should.equal(true);
});
it('should return the document', function() {
return this.callback
.calledWithExactly(null, this.lines, this.version, this.ranges, this.pathname, this.projectHistoryId, this.unflushed_time, this.lastUpdatedAt, this.lastUpdatedBy)
.should.equal(true);
});
return it('should not log any errors', function() {
return this.logger.error.calledWith()
.should.equal(false);
});
});
describe("when the document is not present", function() {
beforeEach(function() {
this.multi.exec = sinon.stub().callsArgWith(0, null, [null, null, null, null, null, null, null, null, null, null]);
this.rclient.sadd = sinon.stub().yields();
return this.RedisManager.getDoc(this.project_id, this.doc_id, this.callback);
});
it("should not check if the document is in the DocsIn set", function() {
return this.rclient.sadd
.calledWith(`DocsIn:${this.project_id}`)
.should.equal(false);
});
it('should return an empty result', function() {
return this.callback
.calledWithExactly(null, null, 0, {}, null, null, null, null, null)
.should.equal(true);
});
return it('should not log any errors', function() {
return this.logger.error.calledWith()
.should.equal(false);
});
});
describe("when the document is missing from the DocsIn set", function() {
beforeEach(function() {
this.rclient.sadd = sinon.stub().yields(null, 1);
return this.RedisManager.getDoc(this.project_id, this.doc_id, this.callback);
});
it('should log an error', function() {
return this.logger.error.calledWith()
.should.equal(true);
});
return it('should return the document', function() {
return this.callback
.calledWithExactly(null, this.lines, this.version, this.ranges, this.pathname, this.projectHistoryId, this.unflushed_time, this.lastUpdatedAt, this.lastUpdatedBy)
.should.equal(true);
});
});
describe("with a corrupted document", function() {
beforeEach(function() {
this.badHash = "INVALID-HASH-VALUE";
this.multi.exec = sinon.stub().callsArgWith(0, null, [this.jsonlines, this.version, this.badHash, this.project_id, this.json_ranges]);
return this.RedisManager.getDoc(this.project_id, this.doc_id, this.callback);
});
it('should log a hash error', function() {
return this.logger.error.calledWith()
.should.equal(true);
});
return it('should return the document', function() {
return this.callback
.calledWith(null, this.lines, this.version, this.ranges)
.should.equal(true);
});
});
describe("with a slow request to redis", function() {
beforeEach(function() {
this.multi.exec = sinon.stub().callsArgWith(0, null, [this.jsonlines, this.version, this.badHash, this.project_id, this.json_ranges, this.pathname, this.unflushed_time]);
this.clock = sinon.useFakeTimers();
this.multi.exec = cb => {
this.clock.tick(6000);
return cb(null, [this.jsonlines, this.version, this.another_project_id, this.json_ranges, this.pathname, this.unflushed_time]);
};
return this.RedisManager.getDoc(this.project_id, this.doc_id, this.callback);
});
afterEach(function() {
return this.clock.restore();
});
return it('should return an error', function() {
return this.callback
.calledWith(new Error("redis getDoc exceeded timeout"))
.should.equal(true);
});
});
return describe("getDoc with an invalid project id", function() {
beforeEach(function() {
this.another_project_id = "project-id-456";
this.multi.exec = sinon.stub().callsArgWith(0, null, [this.jsonlines, this.version, this.hash, this.another_project_id, this.json_ranges, this.pathname, this.unflushed_time]);
return this.RedisManager.getDoc(this.project_id, this.doc_id, this.callback);
});
return it('should return an error', function() {
return this.callback
2016-12-01 11:49:53 -05:00
.calledWith(new Errors.NotFoundError("not found"))
.should.equal(true);
});
});
});
describe("getPreviousDocOpsTests", function() {
describe("with a start and an end value", function() {
beforeEach(function() {
this.first_version_in_redis = 30;
this.version = 70;
this.length = this.version - this.first_version_in_redis;
this.start = 50;
this.end = 60;
this.ops = [
{ "mock": "op-1" },
{ "mock": "op-2" }
];
this.jsonOps = this.ops.map(op => JSON.stringify(op));
this.rclient.llen = sinon.stub().callsArgWith(1, null, this.length);
this.rclient.get = sinon.stub().callsArgWith(1, null, this.version.toString());
this.rclient.lrange = sinon.stub().callsArgWith(3, null, this.jsonOps);
return this.RedisManager.getPreviousDocOps(this.doc_id, this.start, this.end, this.callback);
});
it("should get the length of the existing doc ops", function() {
return this.rclient.llen
.calledWith(`DocOps:${this.doc_id}`)
.should.equal(true);
});
it("should get the current version of the doc", function() {
return this.rclient.get
.calledWith(`DocVersion:${this.doc_id}`)
.should.equal(true);
});
it("should get the appropriate docs ops", function() {
return this.rclient.lrange
.calledWith(`DocOps:${this.doc_id}`, this.start - this.first_version_in_redis, this.end - this.first_version_in_redis)
.should.equal(true);
});
return it("should return the docs with the doc ops deserialized", function() {
return this.callback.calledWith(null, this.ops).should.equal(true);
});
});
describe("with an end value of -1", function() {
beforeEach(function() {
this.first_version_in_redis = 30;
this.version = 70;
this.length = this.version - this.first_version_in_redis;
this.start = 50;
this.end = -1;
this.ops = [
{ "mock": "op-1" },
{ "mock": "op-2" }
];
this.jsonOps = this.ops.map(op => JSON.stringify(op));
this.rclient.llen = sinon.stub().callsArgWith(1, null, this.length);
this.rclient.get = sinon.stub().callsArgWith(1, null, this.version.toString());
this.rclient.lrange = sinon.stub().callsArgWith(3, null, this.jsonOps);
return this.RedisManager.getPreviousDocOps(this.doc_id, this.start, this.end, this.callback);
});
it("should get the appropriate docs ops to the end of list", function() {
return this.rclient.lrange
.calledWith(`DocOps:${this.doc_id}`, this.start - this.first_version_in_redis, -1)
.should.equal(true);
});
return it("should return the docs with the doc ops deserialized", function() {
return this.callback.calledWith(null, this.ops).should.equal(true);
});
});
describe("when the requested range is not in Redis", function() {
beforeEach(function() {
this.first_version_in_redis = 30;
this.version = 70;
this.length = this.version - this.first_version_in_redis;
this.start = 20;
this.end = -1;
this.ops = [
{ "mock": "op-1" },
{ "mock": "op-2" }
];
this.jsonOps = this.ops.map(op => JSON.stringify(op));
this.rclient.llen = sinon.stub().callsArgWith(1, null, this.length);
this.rclient.get = sinon.stub().callsArgWith(1, null, this.version.toString());
this.rclient.lrange = sinon.stub().callsArgWith(3, null, this.jsonOps);
return this.RedisManager.getPreviousDocOps(this.doc_id, this.start, this.end, this.callback);
});
it("should return an error", function() {
return this.callback.calledWith(new Errors.OpRangeNotAvailableError("doc ops range is not loaded in redis")).should.equal(true);
});
return it("should log out the problem", function() {
return this.logger.warn.called.should.equal(true);
});
});
return describe("with a slow request to redis", function() {
beforeEach(function() {
this.first_version_in_redis = 30;
this.version = 70;
this.length = this.version - this.first_version_in_redis;
this.start = 50;
this.end = 60;
this.ops = [
{ "mock": "op-1" },
{ "mock": "op-2" }
];
this.jsonOps = this.ops.map(op => JSON.stringify(op));
this.rclient.llen = sinon.stub().callsArgWith(1, null, this.length);
this.rclient.get = sinon.stub().callsArgWith(1, null, this.version.toString());
this.clock = sinon.useFakeTimers();
this.rclient.lrange = (key, start, end, cb) => {
this.clock.tick(6000);
return cb(null, this.jsonOps);
};
return this.RedisManager.getPreviousDocOps(this.doc_id, this.start, this.end, this.callback);
});
afterEach(function() {
return this.clock.restore();
});
return it('should return an error', function() {
return this.callback
.calledWith(new Error("redis getPreviousDocOps exceeded timeout"))
.should.equal(true);
});
});
});
describe("updateDocument", function() {
beforeEach(function() {
this.lines = ["one", "two", "three", "これは"];
this.ops = [{ op: [{ i: "foo", p: 4 }] },{ op: [{ i: "bar", p: 8 }] }];
this.version = 42;
this.hash = crypto.createHash('sha1').update(JSON.stringify(this.lines),'utf8').digest('hex');
this.ranges = { comments: "mock", entries: "mock" };
this.updateMeta = { user_id: 'last-author-fake-id' };
this.doc_update_list_length = sinon.stub();
this.project_update_list_length = sinon.stub();
this.RedisManager.getDocVersion = sinon.stub();
this.multi.set = sinon.stub();
this.multi.rpush = sinon.stub();
this.multi.expire = sinon.stub();
this.multi.ltrim = sinon.stub();
this.multi.del = sinon.stub();
this.multi.exec = sinon.stub().callsArgWith(0, null,
[this.hash, null, null, null, null, null, null, this.doc_update_list_length, null, null]
);
return this.ProjectHistoryRedisManager.queueOps = sinon.stub().callsArgWith(
this.ops.length + 1, null, this.project_update_list_length
);
});
describe("with a consistent version", function() {
beforeEach(function() {});
describe("with project history enabled", function() {
beforeEach(function() {
this.settings.apis.project_history.enabled = true;
this.RedisManager.getDocVersion.withArgs(this.doc_id).yields(null, this.version - this.ops.length);
return this.RedisManager.updateDocument(this.project_id, this.doc_id, this.lines, this.version, this.ops, this.ranges, this.updateMeta, this.callback);
});
it("should get the current doc version to check for consistency", function() {
return this.RedisManager.getDocVersion
.calledWith(this.doc_id)
.should.equal(true);
});
it("should set the doclines", function() {
return this.multi.set
.calledWith(`doclines:${this.doc_id}`, JSON.stringify(this.lines))
.should.equal(true);
});
it("should set the version", function() {
return this.multi.set
.calledWith(`DocVersion:${this.doc_id}`, this.version)
.should.equal(true);
});
it("should set the hash", function() {
return this.multi.set
.calledWith(`DocHash:${this.doc_id}`, this.hash)
.should.equal(true);
});
it("should set the ranges", function() {
return this.multi.set
.calledWith(`Ranges:${this.doc_id}`, JSON.stringify(this.ranges))
.should.equal(true);
});
it("should set the unflushed time", function() {
return this.multi.set
.calledWith(`UnflushedTime:${this.doc_id}`, Date.now(), "NX")
.should.equal(true);
});
it("should set the last updated time", function() {
return this.multi.set
.calledWith(`lastUpdatedAt:${this.doc_id}`, Date.now())
.should.equal(true);
});
it("should set the last updater", function() {
return this.multi.set
.calledWith(`lastUpdatedBy:${this.doc_id}`, 'last-author-fake-id')
.should.equal(true);
});
it("should push the doc op into the doc ops list", function() {
return this.multi.rpush
.calledWith(`DocOps:${this.doc_id}`, JSON.stringify(this.ops[0]), JSON.stringify(this.ops[1]))
.should.equal(true);
});
it("should renew the expiry ttl on the doc ops array", function() {
return this.multi.expire
.calledWith(`DocOps:${this.doc_id}`, this.RedisManager.DOC_OPS_TTL)
.should.equal(true);
});
it("should truncate the list to 100 members", function() {
return this.multi.ltrim
.calledWith(`DocOps:${this.doc_id}`, -this.RedisManager.DOC_OPS_MAX_LENGTH, -1)
.should.equal(true);
});
it("should push the updates into the history ops list", function() {
return this.multi.rpush
.calledWith(`UncompressedHistoryOps:${this.doc_id}`, JSON.stringify(this.ops[0]), JSON.stringify(this.ops[1]))
.should.equal(true);
});
it("should push the updates into the project history ops list", function() {
return this.ProjectHistoryRedisManager.queueOps
.calledWith(this.project_id, JSON.stringify(this.ops[0]))
.should.equal(true);
});
it("should call the callback", function() {
return this.callback
.calledWith(null, this.doc_update_list_length, this.project_update_list_length)
.should.equal(true);
});
return it('should not log any errors', function() {
return this.logger.error.calledWith()
.should.equal(false);
});
});
describe("with project history disabled", function() {
beforeEach(function() {
this.settings.apis.project_history.enabled = false;
this.RedisManager.getDocVersion.withArgs(this.doc_id).yields(null, this.version - this.ops.length);
return this.RedisManager.updateDocument(this.project_id, this.doc_id, this.lines, this.version, this.ops, this.ranges, this.updateMeta, this.callback);
});
it("should not push the updates into the project history ops list", function() {
return this.ProjectHistoryRedisManager.queueOps.called.should.equal(false);
});
return it("should call the callback", function() {
return this.callback
.calledWith(null, this.doc_update_list_length)
.should.equal(true);
});
});
return describe("with a doc using project history only", function() {
beforeEach(function() {
this.RedisManager.getDocVersion.withArgs(this.doc_id).yields(null, this.version - this.ops.length, 'project-history');
return this.RedisManager.updateDocument(this.project_id, this.doc_id, this.lines, this.version, this.ops, this.ranges, this.updateMeta, this.callback);
});
it("should not push the updates to the track-changes ops list", function() {
return this.multi.rpush
.calledWith(`UncompressedHistoryOps:${this.doc_id}`)
.should.equal(false);
});
it("should push the updates into the project history ops list", function() {
return this.ProjectHistoryRedisManager.queueOps
.calledWith(this.project_id, JSON.stringify(this.ops[0]))
.should.equal(true);
});
return it("should call the callback with the project update count only", function() {
return this.callback
.calledWith(null, undefined, this.project_update_list_length)
.should.equal(true);
});
});
});
describe("with an inconsistent version", function() {
beforeEach(function() {
this.RedisManager.getDocVersion.withArgs(this.doc_id).yields(null, this.version - this.ops.length - 1);
return this.RedisManager.updateDocument(this.project_id, this.doc_id, this.lines, this.version, this.ops, this.ranges, this.updateMeta, this.callback);
});
it("should not call multi.exec", function() {
return this.multi.exec.called.should.equal(false);
});
return it("should call the callback with an error", function() {
return this.callback
.calledWith(new Error(`Version mismatch. '${this.doc_id}' is corrupted.`))
.should.equal(true);
});
});
describe("with no updates", function() {
beforeEach(function() {
this.RedisManager.getDocVersion.withArgs(this.doc_id).yields(null, this.version);
return this.RedisManager.updateDocument(this.project_id, this.doc_id, this.lines, this.version, [], this.ranges, this.updateMeta, this.callback);
});
it("should not try to enqueue doc updates", function() {
return this.multi.rpush
.called
.should.equal(false);
});
2017-09-29 05:54:48 -04:00
it("should not try to enqueue project updates", function() {
return this.ProjectHistoryRedisManager.queueOps
.called
.should.equal(false);
});
return it("should still set the doclines", function() {
return this.multi.set
.calledWith(`doclines:${this.doc_id}`, JSON.stringify(this.lines))
.should.equal(true);
});
});
describe("with empty ranges", function() {
beforeEach(function() {
this.RedisManager.getDocVersion.withArgs(this.doc_id).yields(null, this.version - this.ops.length);
return this.RedisManager.updateDocument(this.project_id, this.doc_id, this.lines, this.version, this.ops, {}, this.updateMeta, this.callback);
});
it("should not set the ranges", function() {
return this.multi.set
.calledWith(`Ranges:${this.doc_id}`, JSON.stringify(this.ranges))
.should.equal(false);
});
return it("should delete the ranges key", function() {
return this.multi.del
.calledWith(`Ranges:${this.doc_id}`)
.should.equal(true);
});
});
describe("with null bytes in the serialized doc lines", function() {
beforeEach(function() {
this.RedisManager.getDocVersion.withArgs(this.doc_id).yields(null, this.version - this.ops.length);
this._stringify = JSON.stringify;
this.JSON.stringify = () => '["bad bytes! \u0000 <- here"]';
return this.RedisManager.updateDocument(this.project_id, this.doc_id, this.lines, this.version, this.ops, this.ranges, this.updateMeta, this.callback);
});
afterEach(function() {
return this.JSON.stringify = this._stringify;
});
it("should log an error", function() {
return this.logger.error.called.should.equal(true);
});
return it("should call the callback with an error", function() {
return this.callback.calledWith(new Error("null bytes found in doc lines")).should.equal(true);
});
});
describe("with ranges that are too big", function() {
beforeEach(function() {
this.RedisManager.getDocVersion.withArgs(this.doc_id).yields(null, this.version - this.ops.length);
this.RedisManager._serializeRanges = sinon.stub().yields(new Error("ranges are too large"));
return this.RedisManager.updateDocument(this.project_id, this.doc_id, this.lines, this.version, this.ops, this.ranges, this.updateMeta, this.callback);
});
it('should log an error', function() {
return this.logger.error.called.should.equal(true);
});
return it("should call the callback with the error", function() {
return this.callback.calledWith(new Error("ranges are too large")).should.equal(true);
});
});
return describe("without user id from meta", function() {
beforeEach(function() {
this.RedisManager.getDocVersion.withArgs(this.doc_id).yields(null, this.version - this.ops.length);
return this.RedisManager.updateDocument(this.project_id, this.doc_id, this.lines, this.version, this.ops, this.ranges, {}, this.callback);
});
it("should set the last updater to null", function() {
return this.multi.del
.calledWith(`lastUpdatedBy:${this.doc_id}`)
.should.equal(true);
});
return it("should still set the last updated time", function() {
return this.multi.set
.calledWith(`lastUpdatedAt:${this.doc_id}`, Date.now())
.should.equal(true);
});
});
});
describe("putDocInMemory", function() {
beforeEach(function() {
this.multi.set = sinon.stub();
this.rclient.sadd = sinon.stub().yields();
this.multi.del = sinon.stub();
this.lines = ["one", "two", "three", "これは"];
this.version = 42;
this.hash = crypto.createHash('sha1').update(JSON.stringify(this.lines),'utf8').digest('hex');
this.multi.exec = sinon.stub().callsArgWith(0, null, [this.hash]);
this.ranges = { comments: "mock", entries: "mock" };
return this.pathname = '/a/b/c.tex';
});
describe("with non-empty ranges", function() {
beforeEach(function(done) {
return this.RedisManager.putDocInMemory(this.project_id, this.doc_id, this.lines, this.version, this.ranges, this.pathname, this.projectHistoryId, done);
});
it("should set the lines", function() {
return this.multi.set
.calledWith(`doclines:${this.doc_id}`, JSON.stringify(this.lines))
.should.equal(true);
});
it("should set the version", function() {
return this.multi.set
.calledWith(`DocVersion:${this.doc_id}`, this.version)
.should.equal(true);
});
it("should set the hash", function() {
return this.multi.set
.calledWith(`DocHash:${this.doc_id}`, this.hash)
.should.equal(true);
});
it("should set the ranges", function() {
return this.multi.set
.calledWith(`Ranges:${this.doc_id}`, JSON.stringify(this.ranges))
.should.equal(true);
});
it("should set the project_id for the doc", function() {
return this.multi.set
.calledWith(`ProjectId:${this.doc_id}`, this.project_id)
.should.equal(true);
});
it("should set the pathname for the doc", function() {
return this.multi.set
.calledWith(`Pathname:${this.doc_id}`, this.pathname)
.should.equal(true);
});
it("should set the projectHistoryId for the doc", function() {
return this.multi.set
.calledWith(`ProjectHistoryId:${this.doc_id}`, this.projectHistoryId)
.should.equal(true);
});
it("should add the doc_id to the project set", function() {
return this.rclient.sadd
.calledWith(`DocsIn:${this.project_id}`, this.doc_id)
.should.equal(true);
});
return it('should not log any errors', function() {
return this.logger.error.calledWith()
.should.equal(false);
});
});
describe("with empty ranges", function() {
beforeEach(function(done) {
return this.RedisManager.putDocInMemory(this.project_id, this.doc_id, this.lines, this.version, {}, this.pathname, this.projectHistoryId, done);
});
it("should delete the ranges key", function() {
return this.multi.del
.calledWith(`Ranges:${this.doc_id}`)
.should.equal(true);
});
return it("should not set the ranges", function() {
return this.multi.set
.calledWith(`Ranges:${this.doc_id}`, JSON.stringify(this.ranges))
.should.equal(false);
});
});
describe("with null bytes in the serialized doc lines", function() {
beforeEach(function() {
this._stringify = JSON.stringify;
this.JSON.stringify = () => '["bad bytes! \u0000 <- here"]';
return this.RedisManager.putDocInMemory(this.project_id, this.doc_id, this.lines, this.version, this.ranges, this.pathname, this.projectHistoryId, this.callback);
});
afterEach(function() {
return this.JSON.stringify = this._stringify;
});
it("should log an error", function() {
return this.logger.error.called.should.equal(true);
});
return it("should call the callback with an error", function() {
return this.callback.calledWith(new Error("null bytes found in doc lines")).should.equal(true);
});
});
return describe("with ranges that are too big", function() {
beforeEach(function() {
this.RedisManager._serializeRanges = sinon.stub().yields(new Error("ranges are too large"));
return this.RedisManager.putDocInMemory(this.project_id, this.doc_id, this.lines, this.version, this.ranges, this.pathname, this.projectHistoryId, this.callback);
});
it('should log an error', function() {
return this.logger.error.called.should.equal(true);
});
return it("should call the callback with the error", function() {
return this.callback.calledWith(new Error("ranges are too large")).should.equal(true);
});
});
});
describe("removeDocFromMemory", function() {
beforeEach(function(done) {
this.multi.strlen = sinon.stub();
this.multi.del = sinon.stub();
this.multi.srem = sinon.stub();
this.multi.exec.yields();
return this.RedisManager.removeDocFromMemory(this.project_id, this.doc_id, done);
});
it("should check the length of the current doclines", function() {
return this.multi.strlen
.calledWith(`doclines:${this.doc_id}`)
.should.equal(true);
});
it("should delete the lines", function() {
return this.multi.del
.calledWith(`doclines:${this.doc_id}`)
.should.equal(true);
});
it("should delete the version", function() {
return this.multi.del
.calledWith(`DocVersion:${this.doc_id}`)
.should.equal(true);
});
it("should delete the hash", function() {
return this.multi.del
.calledWith(`DocHash:${this.doc_id}`)
.should.equal(true);
});
it("should delete the unflushed time", function() {
return this.multi.del
.calledWith(`UnflushedTime:${this.doc_id}`)
.should.equal(true);
});
it("should delete the project_id for the doc", function() {
return this.multi.del
.calledWith(`ProjectId:${this.doc_id}`)
.should.equal(true);
});
it("should remove the doc_id from the project set", function() {
return this.multi.srem
.calledWith(`DocsIn:${this.project_id}`, this.doc_id)
.should.equal(true);
});
it("should delete the pathname for the doc", function() {
return this.multi.del
.calledWith(`Pathname:${this.doc_id}`)
.should.equal(true);
});
it("should delete the pathname for the doc", function() {
return this.multi.del
.calledWith(`ProjectHistoryId:${this.doc_id}`)
.should.equal(true);
});
it("should delete lastUpdatedAt", function() {
return this.multi.del
.calledWith(`lastUpdatedAt:${this.doc_id}`)
.should.equal(true);
});
return it("should delete lastUpdatedBy", function() {
return this.multi.del
.calledWith(`lastUpdatedBy:${this.doc_id}`)
.should.equal(true);
});
});
describe("clearProjectState", function() {
beforeEach(function(done) {
this.rclient.del = sinon.stub().callsArg(1);
return this.RedisManager.clearProjectState(this.project_id, done);
});
return it("should delete the project state", function() {
return this.rclient.del
.calledWith(`ProjectState:${this.project_id}`)
.should.equal(true);
});
});
return describe("renameDoc", function() {
beforeEach(function() {
this.rclient.rpush = sinon.stub().yields();
this.rclient.set = sinon.stub().yields();
return this.update = {
id: this.doc_id,
pathname: (this.pathname = 'pathname'),
newPathname: (this.newPathname = 'new-pathname')
};
});
describe("the document is cached in redis", function() {
beforeEach(function() {
this.RedisManager.getDoc = sinon.stub().callsArgWith(2, null, 'lines', 'version');
this.ProjectHistoryRedisManager.queueRenameEntity = sinon.stub().yields();
return this.RedisManager.renameDoc(this.project_id, this.doc_id, this.userId, this.update, this.projectHistoryId, this.callback);
});
it("update the cached pathname", function() {
return this.rclient.set
.calledWith(`Pathname:${this.doc_id}`, this.newPathname)
.should.equal(true);
});
return it("should queue an update", function() {
return this.ProjectHistoryRedisManager.queueRenameEntity
.calledWithExactly(this.project_id, this.projectHistoryId, 'doc', this.doc_id, this.userId, this.update, this.callback)
.should.equal(true);
});
});
describe("the document is not cached in redis", function() {
beforeEach(function() {
this.RedisManager.getDoc = sinon.stub().callsArgWith(2, null, null, null);
this.ProjectHistoryRedisManager.queueRenameEntity = sinon.stub().yields();
return this.RedisManager.renameDoc(this.project_id, this.doc_id, this.userId, this.update, this.projectHistoryId, this.callback);
});
it("does not update the cached pathname", function() {
return this.rclient.set.called.should.equal(false);
});
return it("should queue an update", function() {
return this.ProjectHistoryRedisManager.queueRenameEntity
.calledWithExactly(this.project_id, this.projectHistoryId, 'doc', this.doc_id, this.userId, this.update, this.callback)
.should.equal(true);
});
});
return describe("getDocVersion", function() {
beforeEach(function() {
return this.version = 12345;
});
describe("when the document does not have a project history type set", function() {
beforeEach(function() {
this.rclient.mget = sinon.stub().withArgs(`DocVersion:${this.doc_id}`, `ProjectHistoryType:${this.doc_id}`).callsArgWith(2, null, [`${this.version}`]);
return this.RedisManager.getDocVersion(this.doc_id, this.callback);
});
return it("should return the document version and an undefined history type", function() {
return this.callback.calledWithExactly(null, this.version, undefined).should.equal(true);
});
});
return describe("when the document has a project history type set", function() {
beforeEach(function() {
this.rclient.mget = sinon.stub().withArgs(`DocVersion:${this.doc_id}`, `ProjectHistoryType:${this.doc_id}`).callsArgWith(2, null, [`${this.version}`, 'project-history']);
return this.RedisManager.getDocVersion(this.doc_id, this.callback);
});
return it("should return the document version and history type", function() {
return this.callback.calledWithExactly(null, this.version, 'project-history').should.equal(true);
});
});
});
});
});