recoder-code
Version:
Complete AI-powered development platform with ML model training, plugin registry, real-time collaboration, monitoring, infrastructure automation, and enterprise deployment capabilities
634 lines (547 loc) • 21 kB
JavaScript
var Backend = require('../../lib/backend');
var expect = require('chai').expect;
var MemoryDb = require('../../lib/db/memory');
var MemoryMilestoneDb = require('../../lib/milestone-db/memory');
var sinon = require('sinon');
var async = require('async');
var json0v2 = require('ot-json0-v2').type;
var types = require('../../lib/types');
var clone = require('../../lib/util').clone;
describe('SnapshotVersionRequest', function() {
var backend;
beforeEach(function() {
backend = new Backend();
});
afterEach(function(done) {
backend.close(done);
});
describe('a document with some simple versions', function() {
var v0 = {
id: 'don-quixote',
v: 0,
type: null,
data: undefined,
m: null
};
var v1 = {
id: 'don-quixote',
v: 1,
type: 'http://sharejs.org/types/JSONv0',
data: {
title: 'Don Quixote'
},
m: null
};
var v2 = {
id: 'don-quixote',
v: 2,
type: 'http://sharejs.org/types/JSONv0',
data: {
title: 'Don Quixote',
author: 'Miguel de Cervante'
},
m: null
};
var v3 = {
id: 'don-quixote',
v: 3,
type: 'http://sharejs.org/types/JSONv0',
data: {
title: 'Don Quixote',
author: 'Miguel de Cervantes'
},
m: null
};
beforeEach(function(done) {
var doc = backend.connect().get('books', 'don-quixote');
doc.create({title: 'Don Quixote'}, function(error) {
if (error) return done(error);
doc.submitOp({p: ['author'], oi: 'Miguel de Cervante'}, function(error) {
if (error) return done(error);
doc.submitOp({p: ['author'], od: 'Miguel de Cervante', oi: 'Miguel de Cervantes'}, done);
});
});
});
it('fetches v1', function(done) {
backend.connect().fetchSnapshot('books', 'don-quixote', 1, function(error, snapshot) {
if (error) return done(error);
expect(snapshot).to.eql(v1);
done();
});
});
it('fetches v2', function(done) {
backend.connect().fetchSnapshot('books', 'don-quixote', 2, function(error, snapshot) {
if (error) return done(error);
expect(snapshot).to.eql(v2);
done();
});
});
it('fetches v3', function(done) {
backend.connect().fetchSnapshot('books', 'don-quixote', 3, function(error, snapshot) {
if (error) return done(error);
expect(snapshot).to.eql(v3);
done();
});
});
it('returns an empty snapshot if the version is 0', function(done) {
backend.connect().fetchSnapshot('books', 'don-quixote', 0, function(error, snapshot) {
if (error) return done(error);
expect(snapshot).to.eql(v0);
done();
});
});
it('throws if the version is undefined', function() {
var fetch = function() {
backend.connect().fetchSnapshot('books', 'don-quixote', undefined, function() {});
};
expect(fetch).to.throw(Error);
});
it('fetches the latest version when the optional version is not provided', function(done) {
backend.connect().fetchSnapshot('books', 'don-quixote', function(error, snapshot) {
if (error) return done(error);
expect(snapshot).to.eql(v3);
done();
});
});
it('throws without a callback', function() {
var fetch = function() {
backend.connect().fetchSnapshot('books', 'don-quixote');
};
expect(fetch).to.throw(Error);
});
it('throws if the version is -1', function() {
var fetch = function() {
backend.connect().fetchSnapshot('books', 'don-quixote', -1, function() {});
};
expect(fetch).to.throw(Error);
});
it('errors if the version is a string', function() {
var fetch = function() {
backend.connect().fetchSnapshot('books', 'don-quixote', 'foo', function() { });
};
expect(fetch).to.throw(Error);
});
it('errors if asking for a version that does not exist', function(done) {
backend.connect().fetchSnapshot('books', 'don-quixote', 4, function(error, snapshot) {
expect(error.code).to.equal('ERR_OP_VERSION_NEWER_THAN_CURRENT_SNAPSHOT');
expect(snapshot).to.equal(undefined);
done();
});
});
it('returns an empty snapshot if trying to fetch a non-existent document', function(done) {
backend.connect().fetchSnapshot('books', 'does-not-exist', 0, function(error, snapshot) {
if (error) return done(error);
expect(snapshot).to.eql({
id: 'does-not-exist',
v: 0,
type: null,
data: undefined,
m: null
});
done();
});
});
it('starts pending, and finishes not pending', function(done) {
var connection = backend.connect();
connection.fetchSnapshot('books', 'don-quixote', null, function(error) {
if (error) return done(error);
expect(connection.hasPending()).to.equal(false);
done();
});
expect(connection.hasPending()).to.equal(true);
});
it('deletes the request from the connection', function(done) {
var connection = backend.connect();
connection.fetchSnapshot('books', 'don-quixote', function(error) {
if (error) return done(error);
expect(connection._snapshotRequests).to.eql({});
done();
});
expect(connection._snapshotRequests).to.not.eql({});
});
it('emits a ready event when done', function(done) {
var connection = backend.connect();
connection.fetchSnapshot('books', 'don-quixote', function(error) {
if (error) return done(error);
});
var snapshotRequest = connection._snapshotRequests[1];
snapshotRequest.on('ready', done);
});
it('fires the connection.whenNothingPending', function(done) {
var connection = backend.connect();
var snapshotFetched = false;
connection.fetchSnapshot('books', 'don-quixote', function(error) {
if (error) return done(error);
snapshotFetched = true;
});
connection.whenNothingPending(function() {
expect(snapshotFetched).to.equal(true);
done();
});
});
it('can drop its connection and reconnect, and the callback is just called once', function(done) {
var connection = backend.connect();
// Here we hook into middleware to make sure that we get the following flow:
// - Connection established
// - Connection attempts to fetch a snapshot
// - Snapshot is about to be returned
// - Connection is dropped before the snapshot is returned
// - Connection is re-established
// - Connection re-requests the snapshot
// - This time the fetch operation is allowed to complete (because of the connectionInterrupted flag)
// - The done callback is called just once (if it's called twice, then mocha will complain)
var connectionInterrupted = false;
backend.use(backend.MIDDLEWARE_ACTIONS.readSnapshots, function(request, callback) {
if (!connectionInterrupted) {
connection.close();
backend.connect(connection);
connectionInterrupted = true;
}
callback();
});
connection.fetchSnapshot('books', 'don-quixote', done);
});
it('cannot send the same request twice over a connection', function(done) {
var connection = backend.connect();
// Here we hook into the middleware to make sure that we get the following flow:
// - Attempt to fetch a snapshot
// - The snapshot request is temporarily stored on the Connection
// - Snapshot is about to be returned (ie the request was already successfully sent)
// - We attempt to resend the request again
// - The done callback is call just once, because the second request does not get sent
// (if the done callback is called twice, then mocha will complain)
var hasResent = false;
backend.use(backend.MIDDLEWARE_ACTIONS.readSnapshots, function(request, callback) {
if (!hasResent) {
connection._snapshotRequests[1]._onConnectionStateChanged();
hasResent = true;
}
callback();
});
connection.fetchSnapshot('books', 'don-quixote', done);
});
describe('readSnapshots middleware', function() {
it('triggers the middleware', function(done) {
backend.use(backend.MIDDLEWARE_ACTIONS.readSnapshots,
function(request) {
expect(request.snapshots[0]).to.eql(v3);
expect(request.snapshotType).to.equal(backend.SNAPSHOT_TYPES.byVersion);
done();
}
);
backend.connect().fetchSnapshot('books', 'don-quixote', 3, function() { });
});
it('can have its snapshot manipulated in the middleware', function(done) {
backend.middleware[backend.MIDDLEWARE_ACTIONS.readSnapshots] = [
function(request, callback) {
request.snapshots[0].data.title = 'Alice in Wonderland';
callback();
}
];
backend.connect().fetchSnapshot('books', 'don-quixote', function(error, snapshot) {
if (error) return done(error);
expect(snapshot.data.title).to.equal('Alice in Wonderland');
done();
});
});
it('respects errors thrown in the middleware', function(done) {
backend.middleware[backend.MIDDLEWARE_ACTIONS.readSnapshots] = [
function(request, callback) {
callback({message: 'foo'});
}
];
backend.connect().fetchSnapshot('books', 'don-quixote', 0, function(error) {
expect(error.message).to.equal('foo');
done();
});
});
});
describe('with a registered projection', function() {
beforeEach(function() {
backend.addProjection('bookTitles', 'books', {title: true});
});
it('applies the projection to a snapshot', function(done) {
backend.connect().fetchSnapshot('bookTitles', 'don-quixote', 2, function(error, snapshot) {
if (error) return done(error);
expect(snapshot.data.title).to.equal('Don Quixote');
expect(snapshot.data.author).to.equal(undefined);
done();
});
});
});
});
describe('a document that is currently deleted', function() {
beforeEach(function(done) {
var doc = backend.connect().get('books', 'catch-22');
doc.create({title: 'Catch 22'}, function(error) {
if (error) return done(error);
doc.del(function(error) {
done(error);
});
});
});
it('returns a null type', function(done) {
backend.connect().fetchSnapshot('books', 'catch-22', null, function(error, snapshot) {
expect(snapshot).to.eql({
id: 'catch-22',
v: 2,
type: null,
data: undefined,
m: null
});
done();
});
});
it('fetches v1', function(done) {
backend.connect().fetchSnapshot('books', 'catch-22', 1, function(error, snapshot) {
if (error) return done(error);
expect(snapshot).to.eql({
id: 'catch-22',
v: 1,
type: 'http://sharejs.org/types/JSONv0',
data: {
title: 'Catch 22'
},
m: null
});
done();
});
});
});
describe('a document that was deleted and then created again', function() {
beforeEach(function(done) {
var doc = backend.connect().get('books', 'hitchhikers-guide');
doc.create({title: 'Hitchhiker\'s Guide to the Galaxy'}, function(error) {
if (error) return done(error);
doc.del(function(error) {
if (error) return done(error);
doc.create({title: 'The Restaurant at the End of the Universe'}, function(error) {
done(error);
});
});
});
});
it('fetches the latest version of the document', function(done) {
backend.connect().fetchSnapshot('books', 'hitchhikers-guide', null, function(error, snapshot) {
if (error) return done(error);
expect(snapshot).to.eql({
id: 'hitchhikers-guide',
v: 3,
type: 'http://sharejs.org/types/JSONv0',
data: {
title: 'The Restaurant at the End of the Universe'
},
m: null
});
done();
});
});
});
describe('milestone snapshots enabled for every other version', function() {
var milestoneDb;
var db;
var backendWithMilestones;
beforeEach(function() {
var options = {interval: 2};
db = new MemoryDb();
milestoneDb = new MemoryMilestoneDb(options);
backendWithMilestones = new Backend({
db: db,
milestoneDb: milestoneDb
});
});
afterEach(function(done) {
backendWithMilestones.close(done);
});
it('fetches a snapshot using the milestone', function(done) {
var doc = backendWithMilestones.connect().get('books', 'mocking-bird');
async.waterfall([
doc.create.bind(doc, {title: 'To Kill a Mocking Bird'}),
doc.submitOp.bind(doc, {p: ['author'], oi: 'Harper Lea'}),
doc.submitOp.bind(doc, {p: ['author'], od: 'Harper Lea', oi: 'Harper Lee'}),
function(next) {
sinon.spy(milestoneDb, 'getMilestoneSnapshot');
sinon.spy(db, 'getOps');
backendWithMilestones.connect().fetchSnapshot('books', 'mocking-bird', 3, next);
},
function(snapshot, next) {
expect(milestoneDb.getMilestoneSnapshot.calledOnce).to.equal(true);
expect(db.getOps.calledWith('books', 'mocking-bird', 2, 3)).to.equal(true);
expect(snapshot.v).to.equal(3);
expect(snapshot.data).to.eql({title: 'To Kill a Mocking Bird', author: 'Harper Lee'});
next();
}
], done);
});
it('does not mutate the milestone snapshot', function(done) {
var connection = backendWithMilestones.connect();
var doc = connection.get('books', 'mocking-bird');
var milestoneDb = backendWithMilestones.milestoneDb;
var milestone;
async.waterfall([
doc.create.bind(doc, {title: 'To Kill a Mocking Bird'}),
doc.submitOp.bind(doc, {p: ['author'], oi: 'Harper Lea'}),
doc.submitOp.bind(doc, {p: ['author'], od: 'Harper Lea', oi: 'Harper Lee'}),
milestoneDb.getMilestoneSnapshot.bind(milestoneDb, 'books', 'mocking-bird', 3),
function(snapshot, next) {
milestone = clone(snapshot);
next();
},
function(next) {
// Internally, this calls ot.applyOps(), which mutates the snapshot it's passed.
// This call shouldn't cause the in-memory milestone snapshot to be mutated.
connection.fetchSnapshot('books', 'mocking-bird', 3, next);
},
function(snapshot, next) {
expect(snapshot.v).to.equal(3);
expect(snapshot.data).to.eql({title: 'To Kill a Mocking Bird', author: 'Harper Lee'});
next();
},
milestoneDb.getMilestoneSnapshot.bind(milestoneDb, 'books', 'mocking-bird', 3),
function(snapshot, next) {
expect(snapshot).to.eql(milestone);
next();
}
], done);
});
describe('with version null', function() {
it('fetches a latest version', function(done) {
var doc = backendWithMilestones.connect().get('books', 'mocking-bird');
async.waterfall([
doc.create.bind(doc, {title: 'To Kill a Mocking Bird'}),
doc.submitOp.bind(doc, {p: ['author'], oi: 'Harper Lea'}),
doc.submitOp.bind(doc, {p: ['author'], od: 'Harper Lea', oi: 'Harper Lee'}),
function(next) {
sinon.spy(milestoneDb, 'getMilestoneSnapshot');
sinon.spy(db, 'getOps');
backendWithMilestones.connect().fetchSnapshot('books', 'mocking-bird', null, next);
},
function(snapshot, next) {
expect(milestoneDb.getMilestoneSnapshot.called).to.be.false;
expect(db.getOps.called).to.be.false;
expect(snapshot.v).to.equal(3);
expect(snapshot.data).to.eql({
title: 'To Kill a Mocking Bird',
author: 'Harper Lee'
});
next();
}
], done);
});
it('return error if getSnapshot fails', function(done) {
var doc = backendWithMilestones.connect().get('books', 'mocking-bird');
async.waterfall([
doc.create.bind(doc, {title: 'To Kill a Mocking Bird'}),
doc.submitOp.bind(doc, {p: ['author'], oi: 'Harper Lea'}),
doc.submitOp.bind(doc, {p: ['author'], od: 'Harper Lea', oi: 'Harper Lee'}),
function(next) {
sinon.spy(milestoneDb, 'getMilestoneSnapshot');
sinon.spy(db, 'getOps');
sinon.stub(db, 'getSnapshot').callsFake(function(_collection, _id, _fields, _options, callback) {
callback(new Error('TEST_ERROR'));
});
backendWithMilestones.connect().fetchSnapshot('books', 'mocking-bird', null, function(error) {
expect(error.message).to.be.equal('TEST_ERROR');
next(null, null);
});
},
function(snapshot, next) {
expect(milestoneDb.getMilestoneSnapshot.called).to.be.false;
expect(db.getOps.called).to.be.false;
expect(db.getSnapshot.called).to.be.true;
next();
}
], done);
});
});
});
describe('invalid json0v2 path', function() {
beforeEach(function(done) {
var doc = backend.connect().get('series', 'his-dark-materials');
async.series([
doc.create.bind(doc, [{title: 'Golden Compass'}]),
doc.submitOp.bind(doc, {p: ['0', 'title'], od: 'Golden Compass', oi: 'Northern Lights'}),
doc.submitOp.bind(doc, {p: ['1'], li: {title: 'Subtle Knife'}}),
doc.submitOp.bind(doc, {p: ['1'], lm: '0'})
], done);
});
describe('json0v1', function() {
it('fetches v2 with json0v1', function(done) {
backend.connect().fetchSnapshot('series', 'his-dark-materials', 2, function(error, snapshot) {
if (error) return done(error);
expect(snapshot.data).to.eql([{title: 'Northern Lights'}]);
done();
});
});
});
describe('json0v2', function() {
var defaultType;
beforeEach(function() {
defaultType = types.defaultType;
types.defaultType = json0v2;
types.register(json0v2);
});
afterEach(function() {
types.defaultType = defaultType;
types.register(defaultType);
});
it('fetches a string-indexed list insertion with json0v2', function(done) {
backend.connect().fetchSnapshot('series', 'his-dark-materials', 2, function(error, snapshot) {
if (error) return done(error);
expect(snapshot.data).to.eql([{title: 'Northern Lights'}]);
done();
});
});
it('fetches a list move using a string target', function(done) {
backend.connect().fetchSnapshot('series', 'his-dark-materials', 4, function(error, snapshot) {
if (error) return done(error);
expect(snapshot.data).to.eql([
{title: 'Subtle Knife'},
{title: 'Northern Lights'}
]);
done();
});
});
});
});
describe('invalid json0v2 path with multiple components', function() {
beforeEach(function(done) {
var doc = backend.connect().get('series', 'his-dark-materials');
async.series([
doc.create.bind(doc, [{}]),
doc.submitOp.bind(doc, [
{p: ['0', 'title'], oi: ''},
{p: ['0', 'title', 0], si: 'Northern Lights'}
])
], done);
});
describe('json0v1', function() {
it('fetches v2 with json0v1', function(done) {
backend.connect().fetchSnapshot('series', 'his-dark-materials', 2, function(error, snapshot) {
if (error) return done(error);
expect(snapshot.data).to.eql([{title: 'Northern Lights'}]);
done();
});
});
});
describe('json0v2', function() {
var defaultType;
beforeEach(function() {
defaultType = types.defaultType;
types.defaultType = json0v2;
types.register(json0v2);
});
afterEach(function() {
types.defaultType = defaultType;
types.register(defaultType);
});
it('fetches v2 with json0v2', function(done) {
backend.connect().fetchSnapshot('series', 'his-dark-materials', 2, function(error, snapshot) {
if (error) return done(error);
expect(snapshot.data).to.eql([{title: 'Northern Lights'}]);
done();
});
});
});
});
});