sharedb
Version:
JSON OT database backend
491 lines (413 loc) • 14.2 kB
JavaScript
var expect = require('chai').expect;
var ot = require('../lib/ot');
var types = require('../lib/types');
var type = types.defaultType;
var presenceType = require('./client/presence/presence-test-type').type;
var ShareDBError = require('../lib/error');
var ERROR_CODE = ShareDBError.CODES;
types.register(presenceType);
describe('ot', function() {
describe('checkOp', function() {
it('fails if op is not an object', function() {
expect(ot.checkOp('hi')).ok;
expect(ot.checkOp()).ok;
expect(ot.checkOp(123)).ok;
expect(ot.checkOp([])).ok;
});
it('fails if op data is missing op, create and del', function() {
expect(ot.checkOp({v: 5})).ok;
});
it('fails if src/seq data is invalid', function() {
expect(ot.checkOp({del: true, v: 5, src: 'hi'})).ok;
expect(ot.checkOp({del: true, v: 5, seq: 123})).ok;
expect(ot.checkOp({del: true, v: 5, src: 'hi', seq: 'there'})).ok;
});
it('fails if a create operation is missing its type', function() {
expect(ot.checkOp({create: {}})).ok;
expect(ot.checkOp({create: 123})).ok;
});
it('fails if the type is missing', function() {
expect(ot.checkOp({create: {type: 'something that does not exist'}})).ok;
});
it('accepts valid create operations', function() {
expect(ot.checkOp({create: {type: type.uri}})).equal();
expect(ot.checkOp({create: {type: type.uri, data: 'hi there'}})).equal();
});
it('accepts valid delete operations', function() {
expect(ot.checkOp({del: true})).equal();
});
it('accepts valid ops', function() {
expect(ot.checkOp({op: [1, 2, 3]})).equal();
});
});
describe('normalize', function() {
it('expands type names in normalizeType', function() {
expect(ot.normalizeType(type.name)).equal(type.uri);
expect(ot.normalizeType(type.uri)).equal(type.uri);
expect(ot.normalizeType('foo')).equal();
});
});
describe('apply', function() {
it('fails if the versions dont match', function() {
expect(ot.apply({v: 0}, {v: 1, create: {type: type.uri}})).ok;
expect(ot.apply({v: 0}, {v: 1, del: true})).ok;
expect(ot.apply({v: 0}, {v: 1, op: []})).ok;
expect(ot.apply({v: 5}, {v: 4, create: {type: type.uri}})).ok;
expect(ot.apply({v: 5}, {v: 4, del: true})).ok;
expect(ot.apply({v: 5}, {v: 4, op: []})).ok;
});
it('allows the version field to be missing', function() {
expect(ot.apply({v: 5}, {create: {type: type.uri}})).equal();
expect(ot.apply({}, {v: 6, create: {type: type.uri}})).equal();
});
});
describe('create', function() {
it('fails if the document already exists', function() {
var doc = {v: 6, create: {type: type.uri}};
expect(ot.apply({v: 6, type: type.uri, data: 'hi'}, doc)).ok;
// The doc should be unmodified
expect(doc).eql({v: 6, create: {type: type.uri}});
});
it('creates doc data correctly when no initial data is passed', function() {
var doc = {v: 5};
expect(ot.apply(doc, {v: 5, create: {type: type.uri}})).equal();
expect(doc).eql({v: 6, type: type.uri, data: type.create()});
});
it('creates doc data when it is given initial data', function() {
var doc = {v: 5};
expect(ot.apply(doc, {v: 5, create: {type: type.uri, data: 'Hi there'}})).equal();
expect(doc).eql({v: 6, type: type.uri, data: 'Hi there'});
});
});
describe('del', function() {
it('deletes the document data and type', function() {
var doc = {v: 6, type: type.uri, data: 'Hi there'};
expect(ot.apply(doc, {v: 6, del: true})).equal();
expect(doc).eql({v: 7, type: null, data: undefined});
});
it('still works if the document doesnt exist anyway', function() {
var doc = {v: 6};
expect(ot.apply(doc, {v: 6, del: true})).equal();
expect(doc).eql({v: 7, type: null, data: undefined});
});
});
describe('op', function() {
it('fails if the document does not exist', function() {
expect(ot.apply({v: 6}, {v: 6, op: [1, 2, 3]})).ok;
});
it('fails if the type is missing', function() {
expect(ot.apply({v: 6, type: 'some non existant type'}, {v: 6, op: [1, 2, 3]})).ok;
});
it('applies the operation to the document data', function() {
var doc = {v: 6, type: type.uri, data: 'Hi'};
expect(ot.apply(doc, {v: 6, op: [{p: [2], si: ' there'}]})).equal();
expect(doc).eql({v: 7, type: type.uri, data: 'Hi there'});
});
});
describe('no-op', function() {
it('works on existing docs', function() {
var doc = {v: 6, type: type.uri, data: 'Hi'};
expect(ot.apply(doc, {v: 6})).equal();
// same, but with v+1.
expect(doc).eql({v: 7, type: type.uri, data: 'Hi'});
});
it('works on nonexistant docs', function() {
var doc = {v: 0};
expect(ot.apply(doc, {v: 0})).equal();
expect(doc).eql({v: 1});
});
});
describe('transform', function() {
it('fails if the version is specified on both and does not match', function() {
var op1 = {v: 5, op: [{p: [10], si: 'hi'}]};
var op2 = {v: 6, op: [{p: [5], si: 'abcde'}]};
expect(ot.transform(type.uri, op1, op2)).ok;
expect(op1).eql({v: 5, op: [{p: [10], si: 'hi'}]});
});
// There's 9 cases here.
it('create by create fails', function() {
expect(ot.transform(null, {v: 10, create: {type: type.uri}}, {v: 10, create: {type: type.uri}})).ok;
});
it('create by delete fails', function() {
expect(ot.transform(null, {create: {type: type.uri}}, {del: true})).ok;
});
it('create by op fails', function() {
expect(ot.transform(null, {v: 10, create: {type: type.uri}}, {v: 10, op: [15, 'hi']})).ok;
});
it('create by noop ok', function() {
var op = {create: {type: type.uri}, v: 6};
expect(ot.transform(null, op, {v: 6})).equal();
expect(op).eql({create: {type: type.uri}, v: 7});
});
it('delete by create fails', function() {
expect(ot.transform(null, {del: true}, {create: {type: type.uri}})).ok;
});
it('delete by delete ok', function() {
var op = {del: true, v: 6};
expect(ot.transform(type.uri, op, {del: true, v: 6})).equal();
expect(op).eql({del: true, v: 7});
// And with no version specified should work, too
var op = {del: true};
expect(ot.transform(type.uri, op, {del: true, v: 6})).equal();
expect(op).eql({del: true});
});
it('delete by op ok', function() {
var op = {del: true, v: 8};
expect(ot.transform(type.uri, op, {op: [], v: 8}));
expect(op).eql({del: true, v: 9});
// And with no version specified should work, too
var op = {del: true};
expect(ot.transform(type.uri, op, {op: [], v: 8}));
expect(op).eql({del: true});
});
it('delete by noop ok', function() {
var op = {del: true, v: 6};
expect(ot.transform(null, op, {v: 6})).equal();
expect(op).eql({del: true, v: 7});
var op = {del: true};
expect(ot.transform(null, op, {v: 6})).equal();
expect(op).eql({del: true});
});
it('op by create fails', function() {
expect(ot.transform(null, {op: {}}, {create: {type: type.uri}})).ok;
});
it('op by delete fails', function() {
expect(ot.transform(type.uri, {v: 10, op: []}, {v: 10, del: true})).ok;
});
it('op by op ok', function() {
var op1 = {v: 6, op: [{p: [10], si: 'hi'}]};
var op2 = {v: 6, op: [{p: [5], si: 'abcde'}]};
expect(ot.transform(type.uri, op1, op2)).equal();
expect(op1).eql({v: 7, op: [{p: [15], si: 'hi'}]});
// No version specified
var op1 = {op: [{p: [10], si: 'hi'}]};
var op2 = {v: 6, op: [{p: [5], si: 'abcde'}]};
expect(ot.transform(type.uri, op1, op2)).equal();
expect(op1).eql({op: [{p: [15], si: 'hi'}]});
});
it('op by noop ok', function() {
// I don't think this is ever used, but whatever.
var op = {v: 6, op: [{p: [10], si: 'hi'}]};
expect(ot.transform(type.uri, op, {v: 6})).equal();
expect(op).eql({v: 7, op: [{p: [10], si: 'hi'}]});
});
it('noop by anything is ok', function() {
var op = {};
expect(ot.transform(type.uri, op, {v: 6, op: [{p: [10], si: 'hi'}]})).equal();
expect(op).eql({});
expect(ot.transform(type.uri, op, {del: true})).equal();
expect(op).eql({});
expect(ot.transform(null, op, {create: {type: type.uri}})).equal();
expect(op).eql({});
expect(ot.transform(null, op, {})).equal();
expect(op).eql({});
});
});
describe('transformPresence', function() {
it('transforms a presence by an op', function() {
var presence = {
p: {index: 5},
t: presenceType.uri,
v: 1
};
var op = {
op: {index: 2, value: 'foo'}
};
var error = ot.transformPresence(presence, op);
expect(error).to.be.undefined;
expect(presence).to.eql({
p: {index: 8},
t: presenceType.uri,
v: 2
});
});
it('nulls presence for a create op', function() {
var presence = {
p: {index: 5},
t: presenceType.uri,
v: 1
};
var op = {
create: {type: presenceType.uri, data: 'foo'}
};
var error = ot.transformPresence(presence, op);
expect(error).to.be.undefined;
expect(presence).to.eql({
p: null,
t: presenceType.uri,
v: 2
});
});
it('nulls presence for a delete op', function() {
var presence = {
p: {index: 5},
t: presenceType.uri,
v: 1
};
var op = {del: true};
var error = ot.transformPresence(presence, op);
expect(error).to.be.undefined;
expect(presence).to.eql({
p: null,
t: presenceType.uri,
v: 2
});
});
it('returns an error for an invalid op', function() {
var presence = {
p: {index: 5},
t: presenceType.uri,
v: 1
};
var op = {};
var error = ot.transformPresence(presence, op);
expect(error.code).to.eql('ERR_OT_OP_BADLY_FORMED');
});
it('considers isOwnOp', function() {
var presence = {
p: {index: 5},
t: presenceType.uri,
v: 1
};
var op = {
op: {index: 5, value: 'foo'}
};
var error = ot.transformPresence(presence, op, true);
expect(error).to.be.undefined;
expect(presence).to.eql({
p: {index: 8},
t: presenceType.uri,
v: 2
});
});
it('checks that the type supports presence', function() {
var presence = {
p: {index: 5},
t: type.uri,
v: 1
};
var op = {
op: {index: 5, value: 'foo'}
};
var error = ot.transformPresence(presence, op);
expect(error.code).to.eql('ERR_TYPE_DOES_NOT_SUPPORT_PRESENCE');
});
it('leaves a null presence untransformed', function() {
var presence = {
p: null,
t: presenceType.uri,
v: 2
};
var op = {
op: {index: 5, value: 'foo'}
};
var error = ot.transformPresence(presence, op);
expect(error).to.be.undefined;
expect(presence).to.eql({
p: null,
t: presenceType.uri,
v: 3
});
});
});
describe('applyOps', function() {
it('applies an op to a snapshot', function() {
var snapshot = {
type: 'json0',
data: {title: 'Wee Free Men'}
};
var ops = [
{
v: 1,
op: [{p: ['title', 0], si: 'The '}]
}
];
var error = ot.applyOps(snapshot, ops);
expect(error).to.be.undefined;
expect(snapshot.data).to.eql({title: 'The Wee Free Men'});
expect(snapshot.v).to.equal(2);
});
it('applies multiple ops', function() {
var snapshot = {
type: 'json0',
data: {title: 'Wee Free Men'}
};
var ops = [
{
v: 1,
op: [{p: ['title', 0], si: 'The '}]
},
{
v: 2,
op: [{p: ['author'], oi: 'Terry Pratchett'}]
}
];
ot.applyOps(snapshot, ops);
expect(snapshot.data).to.eql({
author: 'Terry Pratchett',
title: 'The Wee Free Men'
});
expect(snapshot.v).to.equal(3);
});
it('applies a del to a snapshot', function() {
var snapshot = {
type: 'json0',
data: {title: 'Wee Free Men'}
};
var ops = [{v: 1, del: true}];
ot.applyOps(snapshot, ops);
expect(snapshot.data).to.be.undefined;
});
it('applies a create to a snapshot', function() {
var snapshot = {};
var ops = [
{
v: 1,
create: {
type: 'json0',
data: {title: 'Wee Free Men'}
}
}
];
ot.applyOps(snapshot, ops);
expect(snapshot.data).to.eql({title: 'Wee Free Men'});
});
it('returns an error if the snapshot has an unknown type', function() {
var snapshot = {type: 'unknown-type', data: {}};
var ops = [
{
v: 1,
op: [{p: ['title'], oi: 'Title'}]
}
];
var error = ot.applyOps(snapshot, ops);
expect(error.code).to.equal(ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNIZED);
});
it('returns an error if a create op has an unknown type', function() {
var snapshot = {};
var ops = [
{
v: 1,
create: {
type: 'unknown-type',
data: {}
}
}
];
var error = ot.applyOps(snapshot, ops);
expect(error.code).to.equal(ERROR_CODE.ERR_DOC_TYPE_NOT_RECOGNIZED);
});
it('catches and returns an error thrown by type.apply', function() {
var snapshot = {
type: 'json0',
data: {title: 'Wee Free Men'}
};
var ops = [{
v: 1,
op: [{p: ['title'], li: 'not a list'}]
}];
var error = ot.applyOps(snapshot, ops);
expect(error.code).to.equal(ERROR_CODE.ERR_OT_OP_NOT_APPLIED);
});
});
});