You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by ga...@apache.org on 2017/03/02 13:41:15 UTC

couchdb-nano git commit: Adding replication using the "_replicator" database

Repository: couchdb-nano
Updated Branches:
  refs/heads/master 32cc15c8f -> a20fb9561


Adding replication using the "_replicator" database

Starting with 1.2.0 CouchDB added a new system database called
"_replicator" to handle replications jobs.

Replications are now created as entries on that database and
the server will schedule and perform the replication
accordingly. Entries in the "_replicator" db will be updated.

This means that replication now is a completely asynchronous job
that is not guaranteed to run right after the replication was started.

This commit adds three new object with three object to handle this new
type of replication:

- replication.enable: To enable the replication of a database.
- replication.query: To query the status of a replication job.
- replication.disable: To disable the replication of a database.

More information on this type of replication can be found:
- https://wiki.apache.org/couchdb/Replication#from_1.2.0_onward
- http://guide.couchdb.org/draft/replication.html
- https://gist.github.com/fdmanana/832610

[Addendum after merging with the new repo]
Fixing tests for uuids, since they were not passing.


Project: http://git-wip-us.apache.org/repos/asf/couchdb-nano/repo
Commit: http://git-wip-us.apache.org/repos/asf/couchdb-nano/commit/a20fb956
Tree: http://git-wip-us.apache.org/repos/asf/couchdb-nano/tree/a20fb956
Diff: http://git-wip-us.apache.org/repos/asf/couchdb-nano/diff/a20fb956

Branch: refs/heads/master
Commit: a20fb956143f3160af05868a8732e01b52b0d8fe
Parents: 32cc15c
Author: Carlos Manuel Duclos Vergara <ca...@tieto.com>
Authored: Thu Feb 2 14:25:50 2017 +0100
Committer: Garren Smith <ga...@gmail.com>
Committed: Thu Mar 2 15:40:42 2017 +0200

----------------------------------------------------------------------
 README.md                                |  52 +++++++++++
 lib/nano.js                              |  52 ++++++++++-
 tests/fixtures/database/replicator.json  | 117 +++++++++++++++++++++++
 tests/fixtures/util/uuid.json            |  16 +++-
 tests/helpers/unit.js                    |  13 ++-
 tests/integration/database/replicator.js | 129 ++++++++++++++++++++++++++
 tests/integration/util/uuid.js           |   4 +-
 tests/unit/database/replicator.js        |  38 ++++++++
 8 files changed, 414 insertions(+), 7 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/couchdb-nano/blob/a20fb956/README.md
----------------------------------------------------------------------
diff --git a/README.md b/README.md
index af69a68..21b66d1 100644
--- a/README.md
+++ b/README.md
@@ -29,6 +29,9 @@ minimalistic couchdb driver for node.js
   - [nano.db.list([callback])](#nanodblistcallback)
   - [nano.db.compact(name, [designname], [callback])](#nanodbcompactname-designname-callback)
   - [nano.db.replicate(source, target, [opts], [callback])](#nanodbreplicatesource-target-opts-callback)
+  - [nano.db.replication.enable(source, target, [opts], [callback])](#nanodbreplicatorenablesource-target-opts-callback)
+  - [nano.db.replication.query(id, [opts], [callback])](#nanodbreplicatorquery-id-opts-callback)
+  - [nano.db.replication.disable(id, [opts], [callback])](#nanodbreplicatordisable-id-opts-callback)
   - [nano.db.changes(name, [params], [callback])](#nanodbchangesname-params-callback)
   - [nano.db.follow(name, [params], [callback])](#nanodbfollowname-params-callback)
   - [nano.db.info([callback])](#nanodbinfocallback)
@@ -274,6 +277,55 @@ nano.db.replicate('alice', 'http://admin:password@otherhost.com:5984/alice',
 });
 ```
 
+### nano.db.replication.enable(source, target, [opts], [callback])
+
+enables replication using the new couchdb api from `source` to `target`
+with options `opts`. `target` has to exist, add `create_target:true` to
+`opts` to create it prior to replication.
+replication will survive server restarts.
+
+``` js
+nano.db.replication.enable('alice', 'http://admin:password@otherhost.com:5984/alice',
+                  { create_target:true }, function(err, body) {
+    if (!err)
+      console.log(body);
+});
+```
+
+### nano.db.replication.query(id, [opts], [callback])
+
+queries the state of replication using the new couchdb api. `id` comes from the response
+given by the call to enable.
+
+``` js
+nano.db.replication.enable('alice', 'http://admin:password@otherhost.com:5984/alice',
+                   { create_target:true }, function(err, body) {
+    if (!err) {
+      nano.db.replication.query(body.id, function(error, reply) {
+        if (!err)
+          console.log(reply);
+      }
+    }
+});
+```
+
+### nano.db.replication.disable(id, [opts], [callback])
+
+disables replication using the new couchdb api. `id` comes from the response given
+by the call to enable.
+
+``` js
+nano.db.replication.enable('alice', 'http://admin:password@otherhost.com:5984/alice',
+                   { create_target:true }, function(err, body) {
+    if (!err) {
+      nano.db.replication.disable(body.id, function(error, reply) {
+        if (!err)
+          console.log(reply);
+      }
+    }
+});
+```
+
 ### nano.db.changes(name, [params], [callback])
 
 asks for the changes feed of `name`, `params` contains additions

http://git-wip-us.apache.org/repos/asf/couchdb-nano/blob/a20fb956/lib/nano.js
----------------------------------------------------------------------
diff --git a/lib/nano.js b/lib/nano.js
index 1dfd648..86ceba8 100644
--- a/lib/nano.js
+++ b/lib/nano.js
@@ -25,6 +25,7 @@ var nano;
 
 module.exports = exports = nano = function dbScope(cfg) {
   var serverScope = {};
+  var replications = {};
 
   if (typeof cfg === 'string') {
     cfg = {url: cfg};
@@ -354,7 +355,7 @@ module.exports = exports = nano = function dbScope(cfg) {
       callback = opts;
       opts = {};
     }
-
+    // _replicate
     opts.source = _serializeAsUrl(source);
     opts.target = _serializeAsUrl(target);
 
@@ -371,6 +372,37 @@ module.exports = exports = nano = function dbScope(cfg) {
     return relax({ method: 'GET', path: '_uuids', qs: {count: count}}, callback);
   }
 
+  // http://guide.couchdb.org/draft/replication.html
+  function enableReplication(source, target, opts, callback) {
+    if (typeof opts === 'function') {
+      callback = opts;
+      opts = {};
+    }
+    // _replicator
+    opts.source = _serializeAsUrl(source);
+    opts.target = _serializeAsUrl(target);
+
+    return relax({db: '_replicator', body: opts, method: 'POST'}, callback);
+  }
+
+  // http://guide.couchdb.org/draft/replication.html
+  function queryReplication(id, opts, callback) {
+    if (typeof opts === 'function') {
+      callback = opts;
+      opts = {};
+    }
+    return relax({db: '_replicator', method: 'GET', path: id}, callback);
+  }
+
+  // http://guide.couchdb.org/draft/replication.html
+  function disableReplication(id, rev, opts, callback) {
+    if (typeof opts === 'function') {
+      callback = opts;
+      opts = {};
+    }
+    return relax({db: '_replicator', method: 'DELETE', path: id, qs: {rev: rev}}, callback);
+  }
+
   function docModule(dbName) {
     var docScope = {};
     dbName = decodeURIComponent(dbName);
@@ -765,7 +797,18 @@ module.exports = exports = nano = function dbScope(cfg) {
       spatial: viewSpatial,
       view: viewDocs,
       viewWithList: viewWithList,
-      server: serverScope
+      server: serverScope,
+      replication: {
+        enable: function(target, opts, cb) {
+          return enableReplication(dbName, target, opts, cb);
+        },
+        disable: function(id, revision, opts, cb) {
+          return disableReplication(id, revision, opts, cb);
+        },
+        query: function(id, opts, cb) {
+          return queryReplication(id, opts, cb);
+        }
+      }
     };
 
     docScope.view.compact = function(ddoc, cb) {
@@ -786,6 +829,11 @@ module.exports = exports = nano = function dbScope(cfg) {
       scope: docModule,
       compact: compactDb,
       replicate: replicateDb,
+      replication: {
+        enable: enableReplication,
+        disable: disableReplication,
+        query: queryReplication
+      },
       changes: changesDb,
       follow: followDb,
       followUpdates: followUpdates,

http://git-wip-us.apache.org/repos/asf/couchdb-nano/blob/a20fb956/tests/fixtures/database/replicator.json
----------------------------------------------------------------------
diff --git a/tests/fixtures/database/replicator.json b/tests/fixtures/database/replicator.json
new file mode 100644
index 0000000..f1f76d6
--- /dev/null
+++ b/tests/fixtures/database/replicator.json
@@ -0,0 +1,117 @@
+[
+  { "method"   : "put"
+  , "path"     : "/database_replicator"
+  , "status"   : 201
+  , "response" : "{ \"ok\": true }"
+  }
+, { "method"   : "put"
+  , "path"     : "/database_replica"
+  , "status"   : 201
+  , "response" : "{ \"ok\": true }"
+  }
+, { "method"   : "put"
+  , "path"     : "/database_replica2"
+  , "status"   : 201
+  , "response" : "{ \"ok\": true }"
+  }
+, { "method"   : "put"
+  , "path"     : "/database_replica3"
+  , "status"   : 201
+  , "response" : "{ \"ok\": true }"
+  }
+, { "method"   : "put"
+  , "status"   : 201
+  , "path"     : "/database_replicator/foobar"
+  , "body"     : "{\"foo\":\"bar\"}"
+  , "response" : "{\"ok\":true,\"id\":\"foobar\",\"rev\":\"1-4c6114\"}"
+  }
+, { "method"   : "put"
+  , "status"   : 201
+  , "path"     : "/database_replicator/foobaz"
+  , "body"     : "{\"foo\":\"baz\"}"
+  , "response" : "{\"ok\":true,\"id\":\"foobaz\",\"rev\":\"1-611488\"}"
+  }
+, { "method"   : "put"
+  , "status"   : 201
+  , "path"     : "/database_replicator/barfoo"
+  , "body"     : "{\"bar\":\"foo\"}"
+  , "response" : "{\"ok\":true,\"id\":\"barfoo\",\"rev\":\"1-3cde10\"}"
+  }
+, { "method"   : "post"
+  , "status"   : 201
+  , "path"     : "/_replicator"
+  , "body"     : "{\"source\":\"database_replicator\",\"target\":\"database_replica\"}"
+  , "response" : "{\"ok\":true, \"id\": \"632c186d2c10497410f8b46ef300016e\"}"
+  }
+, { "path"     : "/_replicator/632c186d2c10497410f8b46ef300016e"
+  , "status"   : 200
+  , "response" : "{ \"_id\": \"632c186d2c10497410f8b46ef300016e\", \"_rev\": \"3-c83884542204db29b34cd9ed9e5364e1\", \"source\": \"database_replicator\", \"target\": \"database_replica\", \"owner\": null, \"_replication_state\": \"triggered\", \"_replication_state_time\": \"2017-02-07T11:42:25+01:00\", \"_replication_id\": \"c1ed194ee95788f1fcade8cf5489bce9\", \"_replication_stats\": { \"revisions_checked\": 3, \"missing_revisions_found\": 3, \"docs_read\": 3, \"docs_written\": 3, \"doc_write_failures\": 0, \"checkpointed_source_seq\": 3 } }"
+  }
+, { "path"     : "/database_replica/_all_docs"
+  , "status"   : 200
+  , "response" : "{\"total_rows\":3,\"offset\":0,\"rows\":[{\"id\":\"barfoo\",\"key\":\"barfoo\",\"value\":{\"rev\":\"1-41412c293dade3fe73279cba8b4cece4\"}},{\"id\":\"foobar\",\"key\":\"foobar\",\"value\":{\"rev\":\"1-4c6114c65e295552ab1019e2b046b10e\"}},{\"id\":\"foobaz\",\"key\":\"foobaz\",\"value\":{\"rev\":\"1-cfa20dddac397da5bf0be2b50fb472fe\"}}]}"
+  }
+, { "method"   : "delete"
+  , "status"   : 200
+  , "path"     : "/_replicator/632c186d2c10497410f8b46ef300016e?rev=3-c83884542204db29b34cd9ed9e5364e1"
+  , "response" : "{\"ok\":true, \"id\": \"632c186d2c10497410f8b46ef300016e\"}"
+  }
+, { "method"   : "post"
+  , "status"   : 201
+  , "path"     : "/_replicator"
+  , "body"     : "{\"source\":\"http://localhost:5984/database_replicator\",\"target\":\"database_replica2\"}"
+  , "response" : "{\"ok\":true, \"id\": \"632c186d2c10497410f8b46ef300018f\"}"
+  }
+, { "path"     : "/_replicator/632c186d2c10497410f8b46ef300018f"
+  , "status"   : 200
+  , "response" : "{ \"_id\": \"632c186d2c10497410f8b46ef300018f\", \"_rev\": \"3-c83884542204db29b34cd9ed9e5364e1\", \"source\": \"database_replicator\", \"target\": \"database_replica2\", \"owner\": null, \"_replication_state\": \"triggered\", \"_replication_state_time\": \"2017-02-07T11:42:25+01:00\", \"_replication_id\": \"c1ed194ee95788f1fcade8cf5489bce9\", \"_replication_stats\": { \"revisions_checked\": 3, \"missing_revisions_found\": 3, \"docs_read\": 3, \"docs_written\": 3, \"doc_write_failures\": 0, \"checkpointed_source_seq\": 3 } }"
+  }
+, { "path"     : "/database_replica2/_all_docs"
+  , "status"   : 200
+  , "response" : "{\"total_rows\":3,\"offset\":0,\"rows\":[{\"id\":\"barfoo\",\"key\":\"barfoo\",\"value\":{\"rev\":\"1-41412c293dade3fe73279cba8b4cece4\"}},{\"id\":\"foobar\",\"key\":\"foobar\",\"value\":{\"rev\":\"1-4c6114c65e295552ab1019e2b046b10e\"}},{\"id\":\"foobaz\",\"key\":\"foobaz\",\"value\":{\"rev\":\"1-cfa20dddac397da5bf0be2b50fb472fe\"}}]}"
+  }
+, { "method"   : "delete"
+  , "status"   : 200
+  , "path"     : "/_replicator/632c186d2c10497410f8b46ef300018f?rev=3-c83884542204db29b34cd9ed9e5364e1"
+  , "response" : "{\"ok\":true, \"id\": \"632c186d2c10497410f8b46ef300018f\"}"
+  }
+, { "method"   : "post"
+  , "status"   : 201
+  , "path"     : "/_replicator"
+  , "body"     : "{\"source\":\"database_replicator\",\"target\":\"database_replica3\"}"
+  , "response" : "{\"ok\":true, \"id\": \"632c186d2c10497410f8b46ef3000200\"}"
+  }
+, { "path"     : "/_replicator/632c186d2c10497410f8b46ef3000200"
+  , "status"   : 200
+  , "response" : "{ \"_id\": \"632c186d2c10497410f8b46ef3000200\", \"_rev\": \"3-c83884542204db29b34cd9ed9e5364e1\", \"source\": \"database_replicator\", \"target\": \"database_replica3\", \"owner\": null, \"_replication_state\": \"triggered\", \"_replication_state_time\": \"2017-02-07T11:42:25+01:00\", \"_replication_id\": \"c1ed194ee95788f1fcade8cf5489bce9\", \"_replication_stats\": { \"revisions_checked\": 3, \"missing_revisions_found\": 3, \"docs_read\": 3, \"docs_written\": 3, \"doc_write_failures\": 0, \"checkpointed_source_seq\": 3 } }"
+  }
+, { "path"     : "/database_replica3/_all_docs"
+  , "status"   : 200
+  , "response" : "{\"total_rows\":3,\"offset\":0,\"rows\":[{\"id\":\"barfoo\",\"key\":\"barfoo\",\"value\":{\"rev\":\"1-41412c293dade3fe73279cba8b4cece4\"}},{\"id\":\"foobar\",\"key\":\"foobar\",\"value\":{\"rev\":\"1-4c6114c65e295552ab1019e2b046b10e\"}},{\"id\":\"foobaz\",\"key\":\"foobaz\",\"value\":{\"rev\":\"1-cfa20dddac397da5bf0be2b50fb472fe\"}}]}"
+  }
+, { "method"   : "delete"
+  , "status"   : 200
+  , "path"     : "/_replicator/632c186d2c10497410f8b46ef3000200?rev=3-c83884542204db29b34cd9ed9e5364e1"
+  , "response" : "{\"ok\":true, \"id\": \"632c186d2c10497410f8b46ef3000200\"}"
+  }
+, { "method"   : "delete"
+  , "path"     : "/database_replicator"
+  , "status"   : 200
+  , "response" : "{ \"ok\": true }"
+  }
+, { "method"   : "delete"
+  , "path"     : "/database_replica"
+  , "status"   : 200
+  , "response" : "{ \"ok\": true }"
+  }
+, { "method"   : "delete"
+  , "path"     : "/database_replica2"
+  , "status"   : 200
+  , "response" : "{ \"ok\": true }"
+  }
+, { "method"   : "delete"
+  , "path"     : "/database_replica3"
+  , "status"   : 200
+  , "response" : "{ \"ok\": true }"
+  }
+]

http://git-wip-us.apache.org/repos/asf/couchdb-nano/blob/a20fb956/tests/fixtures/util/uuid.json
----------------------------------------------------------------------
diff --git a/tests/fixtures/util/uuid.json b/tests/fixtures/util/uuid.json
index b14ccc5..0a64655 100644
--- a/tests/fixtures/util/uuid.json
+++ b/tests/fixtures/util/uuid.json
@@ -1,12 +1,22 @@
 [
-  { "method"   : "get"
-  , "path"     : "/uuids?count=3"
+  { "method"   : "put"
+  , "path"     : "/util_uuid"
+  , "status"   : 201
+  , "response" : "{ \"ok\": true }"
+  }
+, { "method"   : "get"
+  , "path"     : "/_uuids?count=3"
   , "status"   : 200
   , "response" : "{ \"uuids\": [\"1\",\"2\",\"3\"] }"
   }
 , { "method"   : "get"
-  , "path"     : "/uuids"
+  , "path"     : "/_uuids?count=1"
   , "status"   : 200
   , "response" : "{ \"uuids\": [\"1\"] }"
   }
+, { "method"   : "delete"
+  , "path"     : "/util_uuid"
+  , "status"   : 200
+  , "response" : "{ \"ok\": true }"
+  }
 ]

http://git-wip-us.apache.org/repos/asf/couchdb-nano/blob/a20fb956/tests/helpers/unit.js
----------------------------------------------------------------------
diff --git a/tests/helpers/unit.js b/tests/helpers/unit.js
index 8cf09fa..bf9f21a 100644
--- a/tests/helpers/unit.js
+++ b/tests/helpers/unit.js
@@ -56,7 +56,18 @@ helpers.unit = function(method, error) {
       // are at the top level in nano
       //
       if(method[0] === 'database') {
-        f = cli.server.db[method[1]];
+        //
+        // Due to the way this harness is designed we cannot differentiate between different methods
+        // when those methods are embedded on an object.
+        // We have two options, either we hardcode the resolution or we write a full harness that
+        // can differentiate between methods embedded on an object.
+        // I go the hardcoded route for now.
+        //
+        if (method[1] === 'replicator') {
+          f = cli.server.db.replication.enable;
+        } else {
+          f = cli.server.db[method[1]];
+        }
       } else if(method[0] === 'view' && method[1] === 'compact') {
         f = cli.view.compact;
       } else if(!~['multipart', 'attachment'].indexOf(method[0])) {

http://git-wip-us.apache.org/repos/asf/couchdb-nano/blob/a20fb956/tests/integration/database/replicator.js
----------------------------------------------------------------------
diff --git a/tests/integration/database/replicator.js b/tests/integration/database/replicator.js
new file mode 100644
index 0000000..be77071
--- /dev/null
+++ b/tests/integration/database/replicator.js
@@ -0,0 +1,129 @@
+// Licensed under the Apache License, Version 2.0 (the 'License'); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+'use strict';
+
+var async = require('async');
+var helpers = require('../../helpers/integration');
+var harness = helpers.harness(__filename);
+var it = harness.it;
+var db = harness.locals.db;
+var nano = harness.locals.nano;
+
+var replica;
+var replica2;
+var replica3;
+
+it('should insert a bunch of items', helpers.insertThree);
+
+it('creates a bunch of database replicas', function(assert) {
+    async.forEach(['database_replica', 'database_replica2', 'database_replica3'],
+    nano.db.create, function(error) {
+        assert.equal(error, null, 'created database(s)');
+        assert.end();
+    });
+});
+
+it('should be able to replicate (replicator) three docs', function(assert) {
+  replica = nano.use('database_replica');
+  db.replication.enable('database_replica', function(error, data) {
+    assert.equal(error, null, 'replication should not fail');
+    assert.true(data, 'replication should be scheduled');
+    assert.true(data.ok, 'replication should be scheduled');
+    assert.true(data.id, 'replication should return the id to query back');
+    function waitForReplication() {
+      setTimeout(function() {
+        db.replication.query(data.id, function(error, reply) {
+          assert.equal(reply.target, 'database_replica', 'target db should match');
+          assert.equal(reply._replication_state, 'triggered', 'replication should have triggered');
+          replica.list(function(error, list) {
+            assert.equal(error, null, 'should be able to invoke list');
+            assert.equal(list['total_rows'], 3, 'and have three documents');
+            db.replication.disable(reply._id, reply._rev, function(error, disabled) {
+              assert.true(disabled, 'should not be null');
+              assert.true(disabled.ok, 'should have stopped the replication');
+              assert.end();
+            });  
+          })
+        })
+      }, 
+      3000)
+    };
+    waitForReplication();
+  });
+});
+
+it('should be able to replicate (replicator) to a `nano` object', function(assert) {
+  replica2 = nano.use('database_replica2');
+  nano.db.replication.enable(db, 'database_replica2', function(error, data) {
+    assert.equal(error, null, 'replication should not fail');
+    assert.true(data, 'replication should be scheduled');
+    assert.true(data.ok, 'replication should be scheduled');
+    assert.true(data.id, 'replication should return the id to query back');
+    function waitForReplication() {
+      setTimeout(function() {
+        nano.db.replication.query(data.id, function(error, reply) {
+          assert.equal(reply.target, 'database_replica2', 'target db should match');
+          assert.equal(reply._replication_state, 'triggered', 'replication should have triggered');
+          replica2.list(function(error, list) {
+            assert.equal(error, null, 'should be able to invoke list');
+            assert.equal(list['total_rows'], 3, 'and have three documents');
+            nano.db.replication.disable(reply._id, reply._rev, function(error, disabled) {
+              assert.true(disabled, 'should not be null');
+              assert.true(disabled.ok, 'should have stopped the replication');
+              assert.end();
+            });
+          });
+        })
+      }, 
+      3000)
+    };
+    waitForReplication();
+  });
+});
+
+it('should be able to replicate (replicator) with params', function(assert) {
+  replica3 = nano.use('database_replica3');
+  db.replication.enable('database_replica3', {}, function(error, data) {
+    assert.equal(error, null, 'replication should not fail');
+    assert.true(data, 'replication should be scheduled');
+    assert.true(data.ok, 'replication should be scheduled');
+    assert.true(data.id, 'replication should return the id to query back');
+    function waitForReplication() {
+      setTimeout(function() {
+        db.replication.query(data.id, function(error, reply) {
+          assert.equal(reply.target, 'database_replica3', 'target db should match');
+          assert.equal(reply._replication_state, 'triggered', 'replication should have triggered');
+          replica3.list(function(error, list) {
+            assert.equal(error, null, 'should be able to invoke list');
+            assert.equal(list['total_rows'], 3, 'and have three documents');
+            db.replication.disable(reply._id, reply._rev, function(error, disabled) {
+              assert.true(disabled, 'should not be null');
+              assert.true(disabled.ok, 'should have stopped the replication');
+              assert.end();
+            });
+          });  
+        })
+      }, 
+      3000)
+    };
+    waitForReplication();
+  });
+});
+
+it('should destroy the extra databases', function(assert) {
+ async.forEach(['database_replica', 'database_replica2', 'database_replica3'],
+ nano.db.destroy, function(error) {
+   assert.equal(error, null, 'deleted databases');
+   assert.end();
+ });
+});

http://git-wip-us.apache.org/repos/asf/couchdb-nano/blob/a20fb956/tests/integration/util/uuid.js
----------------------------------------------------------------------
diff --git a/tests/integration/util/uuid.js b/tests/integration/util/uuid.js
index 2faed02..0cb4360 100644
--- a/tests/integration/util/uuid.js
+++ b/tests/integration/util/uuid.js
@@ -23,6 +23,7 @@ it('should insert a one item', helpers.insertOne);
 it('should generate three uuids', function(assert) {
   nano.uuids(3, function(error, data) {
     assert.equal(error, null, 'should generate uuids');
+    assert.ok(data, 'got response');
     assert.ok(data.uuids, 'got uuids');
     assert.equal(data.uuids.count, 3, 'got 3');
     assert.end();
@@ -32,7 +33,8 @@ it('should generate three uuids', function(assert) {
 it('should generate one uuid', function(assert) {
   nano.uuids(function(error, data) {
     assert.equal(error, null, 'should generate uuids');
-    assert.ok(data.uuids, 'got uuids');
+    assert.ok(data, 'got response');
+    assert.ok(data.uuids, 'got uuid');
     assert.equal(data.uuids.count, 1, 'got 1');
     assert.end();
   });

http://git-wip-us.apache.org/repos/asf/couchdb-nano/blob/a20fb956/tests/unit/database/replicator.js
----------------------------------------------------------------------
diff --git a/tests/unit/database/replicator.js b/tests/unit/database/replicator.js
new file mode 100644
index 0000000..dd4abbe
--- /dev/null
+++ b/tests/unit/database/replicator.js
@@ -0,0 +1,38 @@
+// Licensed under the Apache License, Version 2.0 (the 'License'); you may not
+// use this file except in compliance with the License. You may obtain a copy of
+// the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an 'AS IS' BASIS, WITHOUT
+// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+// License for the specific language governing permissions and limitations under
+// the License.
+
+'use strict';
+
+var replicator = require('../../helpers/unit').unit([
+  'database',
+  'replicator'
+]);
+
+replicator('baa', 'baashep', {
+  body: '{"source":"baa","target":"baashep"}',
+  headers: {
+    accept: 'application/json',
+    'content-type': 'application/json'
+  },
+  method: 'POST',
+  uri: '/_replicator'
+});
+
+replicator('molly', 'anne', {some: 'params'}, {
+  body: '{"some":"params","source":"molly","target":"anne"}',
+  headers: {
+    accept: 'application/json',
+    'content-type': 'application/json'
+  },
+  method: 'POST',
+  uri: '/_replicator'
+});