Merge pull request #1187 from strongloop/feature/replication-conflict-tests

Add replication tests for conflict resolution; run replication tests in the browser too
This commit is contained in:
Miroslav Bajtoš 2015-03-06 18:23:40 +01:00
commit b08861a1b1
3 changed files with 131 additions and 17 deletions

View File

@ -3,7 +3,11 @@ var loopback = require('../');
// create a unique Checkpoint model
var Checkpoint = loopback.Checkpoint.extend('TestCheckpoint');
Checkpoint.attachTo(loopback.memory());
var memory = loopback.createDataSource({
connector: loopback.Memory
});
Checkpoint.attachTo(memory);
describe('Checkpoint', function() {
describe('current()', function() {

View File

@ -20,6 +20,9 @@ module.exports = function(config) {
'test/model.test.js',
'test/model.application.test.js',
'test/geo-point.test.js',
'test/replication.test.js',
'test/change.test.js',
'test/checkpoint.test.js',
'test/app.test.js'
],

View File

@ -1,6 +1,5 @@
var async = require('async');
var loopback = require('../');
var ACL = loopback.ACL;
var Change = loopback.Change;
var defineModelTestsWithDataSource = require('./util/model-tests');
var PersistedModel = loopback.PersistedModel;
@ -850,8 +849,85 @@ describe('Replication / Change APIs', function() {
sync(ClientA, Server)
], done);
});
it('handles conflict resolved using "ours"', function(done) {
testResolvedConflictIsHandledWithNoMoreConflicts(
function resolveUsingOurs(conflict, cb) {
conflict.resolve(cb);
},
done);
});
it('handles conflict resolved using "theirs"', function(done) {
testResolvedConflictIsHandledWithNoMoreConflicts(
function resolveUsingTheirs(conflict, cb) {
conflict.models(function(err, source, target) {
if (err) return cb(err);
// We sync ClientA->Server first
expect(conflict.SourceModel.modelName)
.to.equal(ClientB.modelName);
var m = new conflict.SourceModel(target);
m.save(cb);
});
},
done);
});
it('handles conflict resolved manually', function(done) {
testResolvedConflictIsHandledWithNoMoreConflicts(
function resolveManually(conflict, cb) {
conflict.models(function(err, source, target) {
if (err) return cb(err);
var m = new conflict.SourceModel(source || target);
m.name = 'manual';
m.save(function(err) {
if (err) return cb(err);
conflict.resolve(function(err) {
if (err) return cb(err);
cb();
});
});
});
},
done);
});
});
function testResolvedConflictIsHandledWithNoMoreConflicts(resolver, cb) {
async.series([
// sync the new model to ClientB
sync(ClientB, Server),
verifyInstanceWasReplicated(ClientA, ClientB),
// ClientA makes a change
updateSourceInstanceNameTo('a'),
sync(ClientA, Server),
// ClientB changes the same instance
updateClientB('b'),
function syncAndResolveConflict(next) {
replicate(ClientB, Server, function(err, conflicts, cps) {
if (err) return next(err);
expect(conflicts).to.have.length(1);
expect(conflicts[0].SourceModel.modelName)
.to.equal(ClientB.modelName);
debug('Resolving the conflict %j', conflicts[0]);
resolver(conflicts[0], next);
});
},
// repeat the last sync, it should pass now
sync(ClientB, Server),
// and sync back to ClientA too
sync(ClientA, Server),
verifyInstanceWasReplicated(ClientB, ClientA)
], cb);
}
function updateClientB(name) {
return function updateInstanceB(next) {
ClientB.findById(sourceInstanceId, function(err, instance) {
@ -865,8 +941,10 @@ describe('Replication / Change APIs', function() {
function sync(client, server) {
return function syncBothWays(next) {
async.series([
replicateExpectingSuccess(server, client),
replicateExpectingSuccess(client, server)
// NOTE(bajtos) It's important to replicate from the client to the
// server first, so that we can resolve any conflicts at the client
replicateExpectingSuccess(client, server),
replicateExpectingSuccess(server, client)
], next);
};
}
@ -900,31 +978,60 @@ describe('Replication / Change APIs', function() {
});
};
}
function verifyInstanceWasReplicated(source, target) {
return function verify(next) {
source.findById(sourceInstanceId, function(err, expected) {
if (err) return next(err);
target.findById(sourceInstanceId, function(err, actual) {
if (err) return next(err);
expect(actual && actual.toObject())
.to.eql(expected && expected.toObject());
debug('replicated instance: %j', actual);
next();
});
});
};
}
});
var _since = {};
function replicate(source, target, since, next) {
if (typeof since === 'function') {
next = since;
since = undefined;
}
var sinceIx = source.modelName + ':to:' + target.modelName;
if (since === undefined) {
since = useSinceFilter ?
_since[sinceIx] || -1 :
-1;
}
debug('replicate from %s to %s since %j',
source.modelName, target.modelName, since);
source.replicate(since, target, function(err, conflicts, cps) {
if (err) return next(err);
if (conflicts.length === 0) {
_since[sinceIx] = cps;
}
next(err, conflicts, cps);
});
}
function replicateExpectingSuccess(source, target, since) {
if (!source) source = SourceModel;
if (!target) target = TargetModel;
return function replicate(next) {
var sinceIx = source.modelName + ':to:' + target.modelName;
if (since === undefined) {
since = useSinceFilter ?
_since[sinceIx] || -1 :
-1;
}
debug('replicateExpectingSuccess from %s to %s since %j',
source.modelName, target.modelName, since);
source.replicate(since, target, function(err, conflicts, cps) {
return function doReplicate(next) {
replicate(source, target, since, function(err, conflicts, cps) {
if (err) return next(err);
if (conflicts.length) {
return next(new Error('Unexpected conflicts\n' +
conflicts.map(JSON.stringify).join('\n')));
}
_since[sinceIx] = cps;
next();
});
};