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 // create a unique Checkpoint model
var Checkpoint = loopback.Checkpoint.extend('TestCheckpoint'); var Checkpoint = loopback.Checkpoint.extend('TestCheckpoint');
Checkpoint.attachTo(loopback.memory());
var memory = loopback.createDataSource({
connector: loopback.Memory
});
Checkpoint.attachTo(memory);
describe('Checkpoint', function() { describe('Checkpoint', function() {
describe('current()', function() { describe('current()', function() {

View File

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

View File

@ -1,6 +1,5 @@
var async = require('async'); var async = require('async');
var loopback = require('../'); var loopback = require('../');
var ACL = loopback.ACL;
var Change = loopback.Change; var Change = loopback.Change;
var defineModelTestsWithDataSource = require('./util/model-tests'); var defineModelTestsWithDataSource = require('./util/model-tests');
var PersistedModel = loopback.PersistedModel; var PersistedModel = loopback.PersistedModel;
@ -850,8 +849,85 @@ describe('Replication / Change APIs', function() {
sync(ClientA, Server) sync(ClientA, Server)
], done); ], 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) { function updateClientB(name) {
return function updateInstanceB(next) { return function updateInstanceB(next) {
ClientB.findById(sourceInstanceId, function(err, instance) { ClientB.findById(sourceInstanceId, function(err, instance) {
@ -865,8 +941,10 @@ describe('Replication / Change APIs', function() {
function sync(client, server) { function sync(client, server) {
return function syncBothWays(next) { return function syncBothWays(next) {
async.series([ async.series([
replicateExpectingSuccess(server, client), // NOTE(bajtos) It's important to replicate from the client to the
replicateExpectingSuccess(client, server) // server first, so that we can resolve any conflicts at the client
replicateExpectingSuccess(client, server),
replicateExpectingSuccess(server, client)
], next); ], 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 = {}; 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) { function replicateExpectingSuccess(source, target, since) {
if (!source) source = SourceModel; if (!source) source = SourceModel;
if (!target) target = TargetModel; if (!target) target = TargetModel;
return function replicate(next) { return function doReplicate(next) {
var sinceIx = source.modelName + ':to:' + target.modelName; replicate(source, target, since, function(err, conflicts, cps) {
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) {
if (err) return next(err); if (err) return next(err);
if (conflicts.length) { if (conflicts.length) {
return next(new Error('Unexpected conflicts\n' + return next(new Error('Unexpected conflicts\n' +
conflicts.map(JSON.stringify).join('\n'))); conflicts.map(JSON.stringify).join('\n')));
} }
_since[sinceIx] = cps;
next(); next();
}); });
}; };