blob: 5e85847e324dff29b5b39ba054357270331c6acf [file] [log] [blame]
// 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.
couchTests.replication = function(debug) {
if (debug) debugger;
function waitForSeq(sourceDb, targetDb) {
var targetSeq,
sourceSeq = sourceDb.info().update_seq,
t0 = new Date(),
t1,
ms = 3000;
do {
targetSeq = targetDb.info().update_seq;
t1 = new Date();
} while (((t1 - t0) <= ms) && targetSeq < sourceSeq);
}
var host = CouchDB.host;
var dbPairs = [
{source:"test_suite_db_a",
target:"test_suite_db_b"},
{source:"test_suite_db_a",
target:CouchDB.protocol + host + "/test_suite_db_b"},
{source:CouchDB.protocol + host + "/test_suite_db_a",
target:"test_suite_db_b"},
{source:CouchDB.protocol + host + "/test_suite_db_a",
target:CouchDB.protocol + host + "/test_suite_db_b"}
];
var dbA = new CouchDB("test_suite_db_a", {"X-Couch-Full-Commit":"false"});
var dbB = new CouchDB("test_suite_db_b", {"X-Couch-Full-Commit":"false"});
var numDocs = 10;
var xhr;
for (var testPair = 0; testPair < dbPairs.length; testPair++) {
var A = dbPairs[testPair].source;
var B = dbPairs[testPair].target;
dbA.deleteDb();
dbA.createDb();
dbB.deleteDb();
dbB.createDb();
var repTests = {
// copy and paste and put your code in. delete unused steps.
test_template: new function () {
this.init = function(dbA, dbB) {
// before anything has happened
};
this.afterAB1 = function(dbA, dbB) {
// called after replicating src=A tgt=B first time.
};
this.afterBA1 = function(dbA, dbB) {
// called after replicating src=B tgt=A first time.
};
this.afterAB2 = function(dbA, dbB) {
// called after replicating src=A tgt=B second time.
};
this.afterBA2 = function(dbA, dbB) {
// etc...
};
},
simple_test: new function () {
this.init = function(dbA, dbB) {
var docs = makeDocs(0, numDocs);
dbA.bulkSave(docs);
};
this.afterAB1 = function(dbA, dbB) {
for (var j = 0; j < numDocs; j++) {
var docA = dbA.open("" + j);
var docB = dbB.open("" + j);
T(docA._rev == docB._rev);
}
};
},
deletes_test: new function () {
// make sure deletes are replicated
this.init = function(dbA, dbB) {
T(dbA.save({_id:"foo1",value:"a"}).ok);
};
this.afterAB1 = function(dbA, dbB) {
var docA = dbA.open("foo1");
var docB = dbB.open("foo1");
T(docA._rev == docB._rev);
dbA.deleteDoc(docA);
};
this.afterAB2 = function(dbA, dbB) {
T(dbA.open("foo1") == null);
T(dbB.open("foo1") == null);
};
},
deleted_test : new function() {
// docs created and deleted on a single node are also replicated
this.init = function(dbA, dbB) {
T(dbA.save({_id:"del1",value:"a"}).ok);
var docA = dbA.open("del1");
dbA.deleteDoc(docA);
};
this.afterAB1 = function(dbA, dbB) {
var rows = dbB.changes().results;
var rowCnt = 0;
for (var i=0; i < rows.length; i++) {
if (rows[i].id == "del1") {
rowCnt += 1;
T(rows[i].deleted == true);
}
};
T(rowCnt == 1);
};
},
slashes_in_ids_test: new function () {
// make sure docs with slashes in id replicate properly
this.init = function(dbA, dbB) {
dbA.save({ _id:"abc/def", val:"one" });
};
this.afterAB1 = function(dbA, dbB) {
var docA = dbA.open("abc/def");
var docB = dbB.open("abc/def");
T(docA._rev == docB._rev);
};
},
design_docs_test: new function() {
// make sure design docs replicate properly
this.init = function(dbA, dbB) {
dbA.save({ _id:"_design/test" });
};
this.afterAB1 = function() {
var docA = dbA.open("_design/test");
var docB = dbB.open("_design/test");
T(docA._rev == docB._rev);
};
},
attachments_test: new function () {
// Test attachments
this.init = function(dbA, dbB) {
dbA.save({
_id:"bin_doc",
_attachments:{
"foo+bar.txt": {
"type":"base64",
"data": "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ="
}
}
});
// make sure on design docs as well
dbA.save({
_id:"_design/with_bin",
_attachments:{
"foo+bar.txt": {
"type":"base64",
"data": "VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ="
}
}
});
};
this.afterAB1 = function(dbA, dbB) {
var xhr = CouchDB.request("GET",
"/test_suite_db_a/bin_doc/foo%2Bbar.txt");
T(xhr.responseText == "This is a base64 encoded text");
xhr = CouchDB.request("GET",
"/test_suite_db_b/bin_doc/foo%2Bbar.txt");
T(xhr.responseText == "This is a base64 encoded text");
// and the design-doc
xhr = CouchDB.request("GET",
"/test_suite_db_a/_design/with_bin/foo%2Bbar.txt");
T(xhr.responseText == "This is a base64 encoded text");
xhr = CouchDB.request("GET",
"/test_suite_db_b/_design/with_bin/foo%2Bbar.txt");
T(xhr.responseText == "This is a base64 encoded text");
};
},
conflicts_test: new function () {
// test conflicts
this.init = function(dbA, dbB) {
dbA.save({_id:"foo",value:"a"});
dbB.save({_id:"foo",value:"b"});
};
this.afterBA1 = function(dbA, dbB) {
var docA = dbA.open("foo", {conflicts: true});
var docB = dbB.open("foo", {conflicts: true});
// make sure the same rev is in each db
T(docA._rev === docB._rev);
// make sure the conflicts are the same in each db
T(docA._conflicts[0] === docB._conflicts[0]);
// delete a conflict.
dbA.deleteDoc({_id:"foo", _rev:docA._conflicts[0]});
};
this.afterBA2 = function(dbA, dbB) {
// open documents and include the conflict meta data
var docA = dbA.open("foo", {conflicts: true, deleted_conflicts: true});
var docB = dbB.open("foo", {conflicts: true, deleted_conflicts: true});
// We should have no conflicts this time
T(typeof docA._conflicts === "undefined");
T(typeof docB._conflicts === "undefined");
// They show up as deleted conflicts instead
T(docA._deleted_conflicts[0] == docB._deleted_conflicts[0]);
};
}
};
var test;
for(test in repTests) {
if(repTests[test].init) {
repTests[test].init(dbA, dbB);
}
}
var result = CouchDB.replicate(A, B);
var seqA = result.source_last_seq;
T(0 == result.history[0].start_last_seq);
T(typeof result.history[1] === "undefined");
for(test in repTests) {
if(repTests[test].afterAB1) repTests[test].afterAB1(dbA, dbB);
}
result = CouchDB.replicate(B, A);
var seqB = result.source_last_seq;
T(0 == result.history[0].start_last_seq);
T(typeof result.history[1] === "undefined");
for(test in repTests) {
if(repTests[test].afterBA1) repTests[test].afterBA1(dbA, dbB);
}
var result2 = CouchDB.replicate(A, B);
// each successful replication produces a new session id
T(result2.session_id != result.session_id);
T(seqA < result2.source_last_seq);
T(seqA == result2.history[0].start_last_seq);
T(result2.history[1].end_last_seq == seqA);
seqA = result2.source_last_seq;
for(test in repTests) {
if(repTests[test].afterAB2) repTests[test].afterAB2(dbA, dbB);
}
result = CouchDB.replicate(B, A);
T(seqB < result.source_last_seq);
T(seqB == result.history[0].start_last_seq);
T(result.history[1].end_last_seq == seqB);
seqB = result.source_last_seq;
for(test in repTests) {
if(repTests[test].afterBA2) repTests[test].afterBA2(dbA, dbB);
}
// do an replication where nothing has changed
result2 = CouchDB.replicate(B, A);
T(result2.no_changes == true);
T(result2.session_id == result.session_id);
}
// test optional automatic creation of the target db
var dbA = new CouchDB("test_suite_db_a", {"X-Couch-Full-Commit":"false"});
var dbB = new CouchDB("test_suite_db_b", {"X-Couch-Full-Commit":"false"});
dbA.deleteDb();
dbA.createDb();
dbB.deleteDb();
// local
CouchDB.replicate(dbA.name, "test_suite_db_b", {
body: {"create_target": true}
});
TEquals("test_suite_db_b", dbB.info().db_name,
"Target database should exist");
// remote
dbB.deleteDb();
CouchDB.replicate(dbA.name, CouchDB.protocol + CouchDB.host + "/test_suite_db_b", {
body: {"create_target": true}
});
TEquals("test_suite_db_b", dbB.info().db_name,
"Target database should exist");
// continuous
var continuousResult = CouchDB.replicate(dbA.name, "test_suite_db_b", {
body: {"continuous": true}
});
T(continuousResult.ok);
T(continuousResult._local_id);
var cancelResult = CouchDB.replicate(dbA.name, "test_suite_db_b", {
body: {"cancel": true}
});
T(cancelResult.ok);
T(continuousResult._local_id == cancelResult._local_id);
try {
var cancelResult2 = CouchDB.replicate(dbA.name, "test_suite_db_b", {
body: {"cancel": true}
});
} catch (e) {
T(e.error == "not_found");
}
// test replication object option doc_ids
var dbA = new CouchDB("test_suite_rep_docs_db_a", {"X-Couch-Full-Commit":"false"});
var dbB = new CouchDB("test_suite_rep_docs_db_b", {"X-Couch-Full-Commit":"false"});
dbA.deleteDb();
dbA.createDb();
var all_docs = [
{
_id: "foo1",
value: "a"
},
{
_id: "foo2",
value: "b"
},
{
_id: "foo3",
value: "c"
},
{
_id: "slashed/foo",
value: "s"
},
{
_id: "_design/foobar",
language: "javascript",
value: "I am a design doc",
filters: {
idfilter: (function(doc, req) {
return doc.value == Number(req.filter_value);
}).toString()
},
views: {
countview: (function(doc) {
emit(doc.value, 1);
}).toString()
}
}
];
for (var i = 0; i < all_docs.length; i++) {
T(dbA.save(all_docs[i]).ok);
}
var dbPairs = [
{source:"test_suite_rep_docs_db_a",
target:"test_suite_rep_docs_db_b"},
{source:"test_suite_rep_docs_db_a",
target:CouchDB.protocol + host + "/test_suite_rep_docs_db_b"},
{source:CouchDB.protocol + host + "/test_suite_rep_docs_db_a",
target:"test_suite_rep_docs_db_b"},
{source:CouchDB.protocol + host + "/test_suite_rep_docs_db_a",
target:CouchDB.protocol + host + "/test_suite_rep_docs_db_b"}
];
var target_doc_ids = [
["foo1", "foo3", "foo666"],
["foo1", "foo666"],
["foo666", "foo2"],
["foo2", "foo9999", "foo1"],
["foo3", "slashed/foo"],
["foo3", "slashed%2Ffoo"],
["foo1", "_design/foobar"],
["foo1", "foo1001", "_design%2Ffoobar"]
];
for (var i = 0; i < dbPairs.length; i++) {
var src_db = dbPairs[i].source;
var tgt_db = dbPairs[i].target;
for (var j = 0; j < target_doc_ids.length; j++) {
var doc_ids = target_doc_ids[j];
var valid_doc_ids = [];
var invalid_doc_ids = [];
for (var p = 0; p < doc_ids.length; p++) {
var id = doc_ids[p];
var found = false;
for (var k = 0; k < all_docs.length; k++) {
var doc = all_docs[k];
if (id === doc._id) {
found = true;
break;
}
}
if (found) {
valid_doc_ids.push(id);
} else {
invalid_doc_ids.push(id);
}
};
dbB.deleteDb();
dbB.createDb();
var repResult = CouchDB.replicate(src_db, tgt_db, {
body: {"doc_ids": doc_ids}
});
T(repResult.ok);
T(repResult.docs_written === valid_doc_ids.length);
T(repResult.docs_read === valid_doc_ids.length);
T(repResult.doc_write_failures === 0);
for (var k = 0; k < all_docs.length; k++) {
var doc = all_docs[k];
var tgt_doc = dbB.open(doc._id);
if (doc_ids.indexOf(doc._id) >= 0) {
T(tgt_doc !== null);
T(tgt_doc.value === doc.value);
} else {
T(tgt_doc === null);
}
}
for (var k = 0; k < invalid_doc_ids.length; k++) {
var tgt_doc = dbB.open(invalid_doc_ids[k]);
T(tgt_doc === null);
}
}
}
// test filtered replication
var filterFun1 = (function(doc, req) {
if (doc.value < Number(req.query.maxvalue)) {
return true;
} else {
return false;
}
}).toString();
var filterFun2 = (function(doc, req) {
return true;
}).toString();
var dbPairs = [
{source:"test_suite_filtered_rep_db_a",
target:"test_suite_filtered_rep_db_b"},
{source:"test_suite_filtered_rep_db_a",
target:CouchDB.protocol + host + "/test_suite_filtered_rep_db_b"},
{source:CouchDB.protocol + host + "/test_suite_filtered_rep_db_a",
target:"test_suite_filtered_rep_db_b"},
{source:CouchDB.protocol + host + "/test_suite_filtered_rep_db_a",
target:CouchDB.protocol + host + "/test_suite_filtered_rep_db_b"}
];
var sourceDb = new CouchDB("test_suite_filtered_rep_db_a");
var targetDb = new CouchDB("test_suite_filtered_rep_db_b");
for (var i = 0; i < dbPairs.length; i++) {
sourceDb.deleteDb();
sourceDb.createDb();
T(sourceDb.save({_id: "foo1", value: 1}).ok);
T(sourceDb.save({_id: "foo2", value: 2}).ok);
T(sourceDb.save({_id: "foo3", value: 3}).ok);
T(sourceDb.save({_id: "foo4", value: 4}).ok);
var ddoc = {
"_id": "_design/mydesign",
"language": "javascript",
"filters": {
"myfilter": filterFun1
}
};
T(sourceDb.save(ddoc).ok);
targetDb.deleteDb();
targetDb.createDb();
var dbA = dbPairs[i].source;
var dbB = dbPairs[i].target;
var repResult = CouchDB.replicate(dbA, dbB, {
body: {
"filter" : "mydesign/myfilter",
"query_params" : {
"maxvalue": "3"
}
}
});
T(repResult.ok);
T(repResult.history instanceof Array);
T(repResult.history.length === 1);
T(repResult.history[0].docs_written === 2);
T(repResult.history[0].docs_read === 2);
T(repResult.history[0].doc_write_failures === 0);
var docFoo1 = targetDb.open("foo1");
T(docFoo1 !== null);
T(docFoo1.value === 1);
var docFoo2 = targetDb.open("foo2");
T(docFoo2 !== null);
T(docFoo2.value === 2);
var docFoo3 = targetDb.open("foo3");
T(docFoo3 === null);
var docFoo4 = targetDb.open("foo4");
T(docFoo4 === null);
// replication should start from scratch after the filter's code changed
ddoc.filters.myfilter = filterFun2;
T(sourceDb.save(ddoc).ok);
repResult = CouchDB.replicate(dbA, dbB, {
body: {
"filter" : "mydesign/myfilter",
"query_params" : {
"maxvalue": "3"
}
}
});
T(repResult.ok);
T(repResult.history instanceof Array);
T(repResult.history.length === 1);
T(repResult.history[0].docs_written === 3);
T(repResult.history[0].docs_read === 3);
T(repResult.history[0].doc_write_failures === 0);
docFoo1 = targetDb.open("foo1");
T(docFoo1 !== null);
T(docFoo1.value === 1);
docFoo2 = targetDb.open("foo2");
T(docFoo2 !== null);
T(docFoo2.value === 2);
docFoo3 = targetDb.open("foo3");
T(docFoo3 !== null);
T(docFoo3.value === 3);
docFoo4 = targetDb.open("foo4");
T(docFoo4 !== null);
T(docFoo4.value === 4);
T(targetDb.open("_design/mydesign") !== null);
}
// test for COUCHDB-868 - design docs' attachments not getting replicated
// when doing a pull replication with HTTP basic auth
dbA = new CouchDB("test_suite_db_a");
dbB = new CouchDB("test_suite_db_b");
var usersDb = new CouchDB("test_suite_auth");
var lorem = CouchDB.request(
"GET", "/_utils/script/test/lorem.txt").responseText;
var lorem_b64 = CouchDB.request(
"GET", "/_utils/script/test/lorem_b64.txt").responseText;
usersDb.deleteDb();
usersDb.createDb();
dbA.deleteDb();
dbA.createDb();
dbB.deleteDb();
dbB.createDb();
var atts_ddoc = {
_id: "_design/i_have_atts",
language: "javascript"
};
T(dbA.save(atts_ddoc).ok);
var rev = atts_ddoc._rev;
var att_1_name = "lorem.txt";
var att_2_name = "lorem.dat";
var xhr = CouchDB.request(
"PUT", "/" + dbA.name + "/" + atts_ddoc._id + "/" + att_1_name + "?rev=" + rev, {
headers: {"Content-Type": "text/plain;charset=utf-8"},
body: lorem
});
rev = JSON.parse(xhr.responseText).rev;
T(xhr.status === 201);
xhr = CouchDB.request(
"PUT", "/" + dbA.name + "/" + atts_ddoc._id + "/" + att_2_name + "?rev=" + rev, {
headers: {"Content-Type": "application/data"},
body: lorem_b64
});
T(xhr.status === 201);
var fdmananaUserDoc = CouchDB.prepareUserDoc({
name: "fdmanana",
roles: ["reader"]
}, "qwerty");
T(usersDb.save(fdmananaUserDoc).ok);
T(dbA.setSecObj({
admins: {
names: [],
roles: ["admin"]
},
readers: {
names: [],
roles: ["reader"]
}
}).ok);
T(dbB.setSecObj({
admins: {
names: ["fdmanana"],
roles: []
}
}).ok);
var server_config = [
{
section: "couch_httpd_auth",
key: "authentication_db",
value: usersDb.name
},
// to prevent admin party mode
{
section: "admins",
key: "joe",
value: "erlang"
}
];
var test_fun = function() {
T(CouchDB.login("fdmanana", "qwerty").ok);
T(CouchDB.session().userCtx.name === "fdmanana");
T(CouchDB.session().userCtx.roles.indexOf("_admin") === -1);
var repResult = CouchDB.replicate(
CouchDB.protocol + "fdmanana:qwerty@" + host + "/" + dbA.name,
dbB.name
);
T(repResult.ok === true);
T(repResult.history instanceof Array);
T(repResult.history.length === 1);
T(repResult.history[0].docs_written === 1);
T(repResult.history[0].docs_read === 1);
T(repResult.history[0].doc_write_failures === 0);
var atts_ddoc_copy = dbB.open(atts_ddoc._id);
T(atts_ddoc_copy !== null);
T(typeof atts_ddoc_copy._attachments === "object");
T(atts_ddoc_copy._attachments !== null);
T(att_1_name in atts_ddoc_copy._attachments);
T(att_2_name in atts_ddoc_copy._attachments);
var xhr = CouchDB.request("GET", "/" + dbB.name + "/" + atts_ddoc._id + "/" + att_1_name);
T(xhr.status === 200);
T(xhr.responseText === lorem);
xhr = CouchDB.request("GET", "/" + dbB.name + "/" + atts_ddoc._id + "/" + att_2_name);
T(xhr.status === 200);
T(xhr.responseText === lorem_b64);
CouchDB.logout();
T(CouchDB.login("joe", "erlang").ok);
T(dbA.setSecObj({
admins: {
names: [],
roles: ["bar"]
},
readers: {
names: [],
roles: ["foo"]
}
}).ok);
T(dbB.deleteDb().ok === true);
T(dbB.createDb().ok === true);
T(dbB.setSecObj({
admins: {
names: ["fdmanana"],
roles: []
}
}).ok);
CouchDB.logout();
T(CouchDB.login("fdmanana", "qwerty").ok);
T(CouchDB.session().userCtx.name === "fdmanana");
T(CouchDB.session().userCtx.roles.indexOf("_admin") === -1);
try {
repResult = CouchDB.replicate(
CouchDB.protocol + "fdmanana:qwerty@" + host + "/" + dbA.name,
dbB.name
);
T(false, "replication should have failed");
} catch(x) {
T(x.error === "unauthorized");
}
atts_ddoc_copy = dbB.open(atts_ddoc._id);
T(atts_ddoc_copy === null);
CouchDB.logout();
T(CouchDB.login("joe", "erlang").ok);
};
run_on_modified_server(server_config, test_fun);
// COUCHDB-1093 - filtered and continuous _changes feed dies when the
// database is compacted
dbA = new CouchDB("test_suite_db_a");
dbB = new CouchDB("test_suite_db_b");
dbA.deleteDb();
dbA.createDb();
dbB.deleteDb();
dbB.createDb();
var docs = makeDocs(1, 10);
docs.push({
_id: "_design/foo",
language: "javascript",
filters: {
myfilter: (function(doc, req) { return true; }).toString()
}
});
dbA.bulkSave(docs).ok;
var repResult = CouchDB.replicate(
CouchDB.protocol + host + "/" + dbA.name,
dbB.name,
{
body: {
continuous: true,
filter: "foo/myfilter"
}
}
);
TEquals(true, repResult.ok);
TEquals('string', typeof repResult._local_id);
var xhr = CouchDB.request("GET", "/_active_tasks");
var tasks = JSON.parse(xhr.responseText);
TEquals(true, dbA.compact().ok);
while (dbA.info().compact_running) {};
TEquals(true, dbA.save(makeDocs(30, 31)[0]).ok);
xhr = CouchDB.request("GET", "/_active_tasks");
var tasksAfter = JSON.parse(xhr.responseText);
TEquals(tasks.length, tasksAfter.length);
waitForSeq(dbA, dbB);
T(dbB.open("30") !== null);
repResult = CouchDB.replicate(
CouchDB.protocol + host + "/" + dbA.name,
dbB.name,
{
body: {
continuous: true,
filter: "foo/myfilter",
cancel: true
}
}
);
TEquals(true, repResult.ok);
TEquals('string', typeof repResult._local_id);
// COUCHDB-885 - push replication of a doc with attachment causes a
// conflict in the target.
dbA = new CouchDB("test_suite_db_a");
dbB = new CouchDB("test_suite_db_b");
dbA.deleteDb();
dbA.createDb();
dbB.deleteDb();
dbB.createDb();
var doc = {
_id: "doc1"
};
TEquals(true, dbA.save(doc).ok);
repResult = CouchDB.replicate(
dbA.name,
CouchDB.protocol + host + "/" + dbB.name
);
TEquals(true, repResult.ok);
TEquals(true, repResult.history instanceof Array);
TEquals(1, repResult.history.length);
TEquals(1, repResult.history[0].docs_written);
TEquals(1, repResult.history[0].docs_read);
TEquals(0, repResult.history[0].doc_write_failures);
doc["_attachments"] = {
"hello.txt": {
"content_type": "text/plain",
"data": "aGVsbG8gd29ybGQ=" // base64:encode("hello world")
},
"foo.dat": {
"content_type": "not/compressible",
"data": "aSBhbSBub3QgZ3ppcGVk" // base64:encode("i am not gziped")
}
};
TEquals(true, dbA.save(doc).ok);
repResult = CouchDB.replicate(
dbA.name,
CouchDB.protocol + host + "/" + dbB.name
);
TEquals(true, repResult.ok);
TEquals(true, repResult.history instanceof Array);
TEquals(2, repResult.history.length);
TEquals(1, repResult.history[0].docs_written);
TEquals(1, repResult.history[0].docs_read);
TEquals(0, repResult.history[0].doc_write_failures);
var copy = dbB.open(doc._id, {
conflicts: true, deleted_conflicts: true, attachments: true,
att_encoding_info: true});
T(copy !== null);
TEquals("undefined", typeof copy._conflicts);
TEquals("undefined", typeof copy._deleted_conflicts);
TEquals("text/plain", copy._attachments["hello.txt"]["content_type"]);
TEquals("aGVsbG8gd29ybGQ=", copy._attachments["hello.txt"]["data"]);
TEquals("gzip", copy._attachments["hello.txt"]["encoding"]);
TEquals("not/compressible", copy._attachments["foo.dat"]["content_type"]);
TEquals("aSBhbSBub3QgZ3ppcGVk", copy._attachments["foo.dat"]["data"]);
TEquals("undefined", typeof copy._attachments["foo.dat"]["encoding"]);
// end of test for COUCHDB-885
// cleanup
dbA.deleteDb();
dbB.deleteDb();
usersDb.deleteDb();
};