(#3946 #3758) - add buffer.type, test types
diff --git a/lib/adapters/leveldb/index.js b/lib/adapters/leveldb/index.js
index b95757c..33bcce4 100644
--- a/lib/adapters/leveldb/index.js
+++ b/lib/adapters/leveldb/index.js
@@ -28,6 +28,7 @@
 var readAsBinaryString = require('../../deps/binary/readAsBinaryString');
 var binStringToBluffer =
   require('../../deps/binary/binaryStringToBlobOrBuffer');
+var typedBuffer = require('../../deps/binary/typedBuffer');
 
 var LevelTransaction = require('./transaction');
 
@@ -79,6 +80,7 @@
         } else if (isBrowser) { // browser, it's a binary string
           data = binStringToBluffer(buffer, type);
         } else { // node, it's already a buffer
+          buffer.type = type; // non-standard, but used for consistency
           data = buffer;
         }
       }
@@ -367,6 +369,7 @@
   // method...
   api._getAttachment = function (attachment, opts, callback) {
     var digest = attachment.digest;
+    var type = attachment.content_type;
 
     stores.binaryStore.get(digest, function (err, attach) {
       var data;
@@ -374,8 +377,8 @@
       if (err && err.name === 'NotFoundError') {
         // Empty attachment
         data = !opts.binary ? '' : isBrowser ?
-          utils.createBlob([''], {type: attachment.content_type}) :
-          new Buffer('');
+          utils.createBlob([''], {type: type}) :
+          typedBuffer('', 'binary', type);
         return callback(null, data);
       }
 
@@ -385,11 +388,13 @@
 
       if (isBrowser) {
         if (opts.binary) {
-          data = binStringToBluffer(attach, attachment.content_type);
+          data = binStringToBluffer(attach, type);
         } else {
           data = utils.btoa(attach);
         }
       } else {
+        // non-standard, but used for consistency with the browser
+        attach.type = type;
         data = opts.binary ? attach : utils.btoa(attach);
       }
       callback(null, data);
diff --git a/lib/deps/ajax.js b/lib/deps/ajax.js
index 704b14c..17ea6f1 100644
--- a/lib/deps/ajax.js
+++ b/lib/deps/ajax.js
@@ -2,9 +2,10 @@
 
 var request = require('request');
 
-var buffer = require('./buffer');
+var buffer = require('./binary/buffer');
 var errors = require('./errors');
 var utils = require('../utils');
+var isBrowser = typeof process === 'undefined' || process.browser;
 
 function ajax(options, adapterCallback) {
 
@@ -57,6 +58,10 @@
         }
       });
     }
+    if (!isBrowser && options.binary) {
+      // non-standard buffer.type for consistency with the browser
+      obj.type = resp.headers['content-type'];
+    }
     cb(null, obj, resp);
   }
 
diff --git a/lib/deps/binary/base64.js b/lib/deps/binary/base64.js
index 572e301..9550d09 100644
--- a/lib/deps/binary/base64.js
+++ b/lib/deps/binary/base64.js
@@ -1,6 +1,6 @@
 'use strict';
 
-var buffer = require('./../buffer');
+var buffer = require('./buffer');
 
 if (typeof atob === 'function') {
   exports.atob = function (str) {
diff --git a/lib/deps/binary/base64StringToBlobOrBuffer.js b/lib/deps/binary/base64StringToBlobOrBuffer.js
index d0e835d..375ab22 100644
--- a/lib/deps/binary/base64StringToBlobOrBuffer.js
+++ b/lib/deps/binary/base64StringToBlobOrBuffer.js
@@ -3,14 +3,14 @@
 var isBrowser = typeof process === 'undefined' || process.browser;
 var atob = require('./base64').atob;
 var binaryStringToBlobOrBuffer = require('./binaryStringToBlobOrBuffer');
-var buffer = require('../buffer');
+var typedBuffer = require('./typedBuffer');
 
 if (isBrowser) {
   module.exports = function (b64, type) {
     return binaryStringToBlobOrBuffer(atob(b64), type);
   };
 } else {
-  module.exports = function (b64) {
-    return new buffer(b64, 'base64');
+  module.exports = function (b64, type) {
+    return typedBuffer(b64, 'base64', type);
   };
 }
\ No newline at end of file
diff --git a/lib/deps/binary/binaryStringToBlobOrBuffer.js b/lib/deps/binary/binaryStringToBlobOrBuffer.js
index c0e539a..4cf4962 100644
--- a/lib/deps/binary/binaryStringToBlobOrBuffer.js
+++ b/lib/deps/binary/binaryStringToBlobOrBuffer.js
@@ -1,7 +1,7 @@
 'use strict';
 
 var isBrowser = typeof process === 'undefined' || process.browser;
-var buffer = require('../buffer');
+var typedBuffer = require('./typedBuffer');
 
 var createBlob = require('./blob');
 var binaryStringToArrayBuffer = require('./binaryStringToArrayBuffer');
@@ -11,7 +11,7 @@
     return createBlob([binaryStringToArrayBuffer(binString)], {type: type});
   };
 } else {
-  module.exports = function (binString) {
-    return new buffer(binString, 'binary');
+  module.exports = function (binString, type) {
+    return typedBuffer(binString, 'binary', type);
   };
 }
\ No newline at end of file
diff --git a/lib/deps/buffer-browser.js b/lib/deps/binary/buffer-browser.js
similarity index 100%
rename from lib/deps/buffer-browser.js
rename to lib/deps/binary/buffer-browser.js
diff --git a/lib/deps/buffer.js b/lib/deps/binary/buffer.js
similarity index 100%
rename from lib/deps/buffer.js
rename to lib/deps/binary/buffer.js
diff --git a/lib/deps/binary/typedBuffer.js b/lib/deps/binary/typedBuffer.js
new file mode 100644
index 0000000..9939bd5
--- /dev/null
+++ b/lib/deps/binary/typedBuffer.js
@@ -0,0 +1,12 @@
+'use strict';
+
+var buffer = require('./buffer');
+
+function typedBuffer(binString, buffType, type) {
+  // buffType is either 'binary' or 'base64'
+  var buff = new buffer(binString, buffType);
+  buff.type = type; // non-standard, but used for consistency with the browser
+  return buff;
+}
+
+module.exports = typedBuffer;
\ No newline at end of file
diff --git a/lib/deps/multipart.js b/lib/deps/multipart.js
index 8adc0a2..a2de8a0 100644
--- a/lib/deps/multipart.js
+++ b/lib/deps/multipart.js
@@ -14,7 +14,7 @@
 var utils = require('../utils');
 var clone = utils.clone;
 var isBrowser = typeof process === 'undefined' || process.browser;
-var buffer = require('./buffer');
+var buffer = require('./binary/buffer');
 
 function createBlobOrBuffer(parts, type) {
   if (isBrowser) {
diff --git a/package.json b/package.json
index 2f6387d..db2e870 100644
--- a/package.json
+++ b/package.json
@@ -111,7 +111,7 @@
     "./adapters/levelalt": false,
     "./lib/version.js": "./lib/version-browser.js",
     "./lib/adapters/preferredAdapters.js": "./lib/adapters/preferredAdapters-browser.js",
-    "./lib/deps/buffer.js": "./lib/deps/buffer-browser.js",
+    "./lib/deps/binary/buffer.js": "./lib/deps/binary/buffer-browser.js",
     "./lib/deps/migrate.js": "./lib/deps/migrate-browser.js",
     "bluebird": "lie",
     "fs": false,
diff --git a/tests/integration/test.attachments.js b/tests/integration/test.attachments.js
index f615612..e54825b 100644
--- a/tests/integration/test.attachments.js
+++ b/tests/integration/test.attachments.js
@@ -187,9 +187,10 @@
             should.exist(att.digest);
             att.content_type.should.equal(expected.content_type);
             att.data.should.not.be.a('string');
+            att.data.type.should.equal(expected.content_type);
             return testUtils.readBlobPromise(att.data);
-          }).then(function (b64) {
-            testUtils.btoa(b64).should.equal(expected.data);
+          }).then(function (bin) {
+            testUtils.btoa(bin).should.equal(expected.data);
           });
         }));
       });
@@ -220,8 +221,9 @@
             should.exist(att.digest);
             att.content_type.should.equal(expected.content_type);
             att.data.should.not.be.a('string');
-            return testUtils.readBlobPromise(att.data).then(function (b64) {
-              testUtils.btoa(b64).should.equal(expected.data);
+            att.data.type.should.equal(expected.content_type);
+            return testUtils.readBlobPromise(att.data).then(function (bin) {
+              testUtils.btoa(bin).should.equal(expected.data);
             });
           });
         }));
@@ -256,8 +258,9 @@
             should.exist(att.digest);
             att.content_type.should.equal(expected.content_type);
             att.data.should.not.be.a('string');
-            return testUtils.readBlobPromise(att.data).then(function (b64) {
-              testUtils.btoa(b64).should.equal(expected.data);
+            att.data.type.should.equal(expected.content_type);
+            return testUtils.readBlobPromise(att.data).then(function (bin) {
+              testUtils.btoa(bin).should.equal(expected.data);
             });
           }));
         });
@@ -297,8 +300,9 @@
             should.exist(att.digest);
             att.content_type.should.equal(expected.content_type);
             att.data.should.not.be.a('string');
-            return testUtils.readBlobPromise(att.data).then(function (b64) {
-              testUtils.btoa(b64).should.equal(expected.data);
+            att.data.type.should.equal(expected.content_type);
+            return testUtils.readBlobPromise(att.data).then(function (bin) {
+              testUtils.btoa(bin).should.equal(expected.data);
             });
           }));
         });
@@ -365,8 +369,9 @@
             should.exist(att.digest);
             att.content_type.should.equal(expected.content_type);
             att.data.should.not.be.a('string');
-            return testUtils.readBlobPromise(att.data).then(function (b64) {
-              testUtils.btoa(b64).should.equal(expected.data);
+            att.data.type.should.equal(expected.content_type);
+            return testUtils.readBlobPromise(att.data).then(function (bin) {
+              testUtils.btoa(bin).should.equal(expected.data);
             });
           }));
         });
@@ -441,8 +446,9 @@
               should.exist(att.digest);
               att.content_type.should.equal(expected.content_type);
               att.data.should.not.be.a('string');
-              return testUtils.readBlobPromise(att.data).then(function (b64) {
-                testUtils.btoa(b64).should.equal(expected.data);
+              att.data.type.should.equal(expected.content_type);
+              return testUtils.readBlobPromise(att.data).then(function (bin) {
+                testUtils.btoa(bin).should.equal(expected.data);
               });
             }));
           }));
@@ -536,8 +542,9 @@
               should.exist(att.digest);
               att.content_type.should.equal(expected.content_type);
               att.data.should.not.be.a('string');
-              return testUtils.readBlobPromise(att.data).then(function (b64) {
-                testUtils.btoa(b64).should.equal(expected.data);
+              att.data.type.should.equal(expected.content_type);
+              return testUtils.readBlobPromise(att.data).then(function (bin) {
+                testUtils.btoa(bin).should.equal(expected.data);
               });
             }));
           }));
@@ -579,8 +586,9 @@
             should.exist(att.digest);
             att.content_type.should.equal(expected.content_type);
             att.data.should.not.be.a('string');
-            return testUtils.readBlobPromise(att.data).then(function (b64) {
-              testUtils.btoa(b64).should.equal(expected.data);
+            att.data.type.should.equal(expected.content_type);
+            return testUtils.readBlobPromise(att.data).then(function (bin) {
+              testUtils.btoa(bin).should.equal(expected.data);
             });
           }));
         });
@@ -655,8 +663,9 @@
               should.exist(att.digest);
               att.content_type.should.equal(expected.content_type);
               att.data.should.not.be.a('string');
-              return testUtils.readBlobPromise(att.data).then(function (b64) {
-                testUtils.btoa(b64).should.equal(expected.data);
+              att.data.type.should.equal(expected.content_type);
+              return testUtils.readBlobPromise(att.data).then(function (bin) {
+                testUtils.btoa(bin).should.equal(expected.data);
               });
             }));
           }));
@@ -746,8 +755,9 @@
               should.exist(att.digest);
               att.content_type.should.equal(expected.content_type);
               att.data.should.not.be.a('string');
-              return testUtils.readBlobPromise(att.data).then(function (b64) {
-                testUtils.btoa(b64).should.equal(expected.data);
+              att.data.type.should.equal(expected.content_type);
+              return testUtils.readBlobPromise(att.data).then(function (bin) {
+                testUtils.btoa(bin).should.equal(expected.data);
               });
             }));
           }));
@@ -841,8 +851,9 @@
                 should.exist(att.digest);
                 att.content_type.should.equal(expected.content_type);
                 att.data.should.not.be.a('string');
-                return testUtils.readBlobPromise(att.data).then(function (b64) {
-                  testUtils.btoa(b64).should.equal(expected.data);
+                att.data.type.should.equal(expected.content_type);
+                return testUtils.readBlobPromise(att.data).then(function (bin) {
+                  testUtils.btoa(bin).should.equal(expected.data);
                 });
               }));
             }));
@@ -899,8 +910,9 @@
               should.exist(att.digest);
               att.content_type.should.equal(expected.content_type);
               att.data.should.not.be.a('string');
-              return testUtils.readBlobPromise(att.data).then(function (b64) {
-                testUtils.btoa(b64).should.equal(expected.data);
+              att.data.type.should.equal(expected.content_type);
+              return testUtils.readBlobPromise(att.data).then(function (bin) {
+                testUtils.btoa(bin).should.equal(expected.data);
                 doneWithDoc();
               });
             }).catch(reject);
@@ -1010,8 +1022,9 @@
                 should.exist(att.digest);
                 att.content_type.should.equal(expected.content_type);
                 att.data.should.not.be.a('string');
-                return testUtils.readBlobPromise(att.data).then(function (b64) {
-                  testUtils.btoa(b64).should.equal(expected.data);
+                att.data.type.should.equal(expected.content_type);
+                return testUtils.readBlobPromise(att.data).then(function (bin) {
+                  testUtils.btoa(bin).should.equal(expected.data);
                 });
               })).then(doneWithDoc);
             }).catch(reject);
@@ -1069,8 +1082,9 @@
               should.exist(att.digest);
               att.content_type.should.equal(expected.content_type);
               att.data.should.not.be.a('string');
-              return testUtils.readBlobPromise(att.data).then(function (b64) {
-                testUtils.btoa(b64).should.equal(expected.data);
+              att.data.type.should.equal(expected.content_type);
+              return testUtils.readBlobPromise(att.data).then(function (bin) {
+                testUtils.btoa(bin).should.equal(expected.data);
                 doneWithDoc();
               });
             }).catch(reject);
@@ -1839,11 +1853,14 @@
           doc._attachments['foo.txt'].content_type.should.equal('text/plain');
           db.getAttachment('bin_doc', 'foo.txt', function (err, res) {
             should.not.exist(err, 'fetched attachment');
+            res.type.should.equal('text/plain');
             testUtils.readBlob(res, function (data) {
               data.should.equal('This is a base64 encoded text');
               db.put(binAttDoc2, function (err, rev) {
                 db.getAttachment('bin_doc2', 'foo.txt',
-                  function (err, res, xhr) {
+                  function (err, res) {
+                  should.not.exist(err);
+                  res.type.should.equal('text/plain');
                   testUtils.readBlob(res, function (data) {
                     data.should.equal('', 'Correct data returned');
                     moreTests(rev.rev);
@@ -1861,10 +1878,13 @@
                          function (err, info) {
           info.ok.should.equal(true);
           db.getAttachment('bin_doc2', 'foo2.txt', function (err, res, xhr) {
+            should.not.exist(err);
+            res.type.should.equal('text/plain');
             testUtils.readBlob(res, function (data) {
               should.exist(data);
               db.get('bin_doc2', { attachments: true },
                 function (err, res, xhr) {
+                should.not.exist(err);
                 should.exist(res._attachments, 'Result has attachments field');
                 should.not
                   .exist(res._attachments['foo2.txt'].stub, 'stub is false');
@@ -1884,10 +1904,12 @@
     it('Test getAttachment', function (done) {
       var db = new PouchDB(dbs.name);
       db.put(binAttDoc, function (err, res) {
+        should.not.exist(err);
         db.getAttachment('bin_doc', 'foo.txt', function (err, res) {
           if (err) {
             return done(err);
           }
+          res.type.should.equal('text/plain');
           testUtils.readBlob(res, function (data) {
             data.should.equal('This is a base64 encoded text', 'correct data');
             done();
@@ -1896,6 +1918,74 @@
       });
     });
 
+    it('Test getAttachment with stubs', function () {
+      var db = new PouchDB(dbs.name);
+      return db.put({
+        _id: 'doc',
+        _attachments: {
+          '1': {
+            content_type: 'application/octet-stream',
+            data: testUtils.btoa('1\u00002\u00013\u0002')
+          }
+        }
+      }).then(function (res) {
+        return db.get('doc');
+      }).then(function (doc) {
+        doc._attachments['2'] = {
+          content_type: 'application/octet-stream',
+          data: testUtils.btoa('3\u00002\u00011\u0002')
+        };
+        return db.put(doc);
+      }).then(function () {
+        return db.getAttachment('doc', '1');
+      }).then(function (att) {
+        att.type.should.equal('application/octet-stream');
+        return testUtils.readBlobPromise(att);
+      }).then(function (bin) {
+        bin.should.equal('1\u00002\u00013\u0002');
+        return db.getAttachment('doc', '2');
+      }).then(function (att) {
+        att.type.should.equal('application/octet-stream');
+        return testUtils.readBlobPromise(att);
+      }).then(function (bin) {
+        bin.should.equal('3\u00002\u00011\u0002');
+      });
+    });
+
+    it('Test get() with binary:true and stubs', function () {
+      var db = new PouchDB(dbs.name);
+      return db.put({
+        _id: 'doc',
+        _attachments: {
+          '1': {
+            content_type: 'application/octet-stream',
+            data: testUtils.btoa('1\u00002\u00013\u0002')
+          }
+        }
+      }).then(function (res) {
+        return db.get('doc');
+      }).then(function (doc) {
+        doc._attachments['2'] = {
+          content_type: 'application/octet-stream',
+          data: testUtils.btoa('3\u00002\u00011\u0002')
+        };
+        return db.put(doc);
+      }).then(function () {
+        return db.get('doc', {attachments: true, binary: true});
+      }).then(function (doc) {
+        var att1 = doc._attachments['1'].data;
+        var att2 = doc._attachments['2'].data;
+        att1.type.should.equal('application/octet-stream');
+        att2.type.should.equal('application/octet-stream');
+        return testUtils.readBlobPromise(att1).then(function (bin) {
+          bin.should.equal('1\u00002\u00013\u0002');
+          return testUtils.readBlobPromise(att2);
+        }).then(function (bin) {
+          bin.should.equal('3\u00002\u00011\u0002');
+        });
+      });
+    });
+
     it('Test attachments in allDocs/changes', function (done) {
       var db = new PouchDB(dbs.name);
       var docs = [
@@ -2264,6 +2354,8 @@
             should.exist(res._attachments['my/text?@']);
 
             db.getAttachment('mydoc', 'my/text?@', function (err, attachment) {
+              should.not.exist(err);
+              attachment.type.should.equal('text/plain');
               testUtils.readBlob(attachment, function (data) {
                 data.should.eql('Mytext');
 
@@ -2371,6 +2463,8 @@
           doc._attachments['foo.json'].content_type.should
             .equal('application/json', 'doc has correct content type');
           db.getAttachment(results.id, 'foo.json', function (err, attachment) {
+            should.not.exist(err);
+            attachment.type.should.equal('application/json');
             testUtils.readBlob(attachment, function (data) {
               jsonDoc._attachments['foo.json'].data.should
                 .equal('eyJIZWxsbyI6IndvcmxkIn0=', 'correct data');
@@ -2632,7 +2726,10 @@
         return PouchDB.utils.Promise.all(testCases.map(function (testCase) {
           var promise = testCase[0];
           var expected = testCase[1];
-          return promise.then(testUtils.readBlobPromise).then(function (bin) {
+          return promise.then(function (blob) {
+            blob.type.should.equal('text/plain');
+            return testUtils.readBlobPromise(blob);
+          }).then(function (bin) {
             bin.should.equal(expected, 'didn\'t get blob we expected for rev');
           });
         }));
@@ -2672,6 +2769,7 @@
             should.not.exist(err, 'attachment inserted');
             db.getAttachment('foo', 'foo.txt', function (err, blob) {
               should.not.exist(err, 'attachment gotten');
+              blob.type.should.equal('text/plain');
               testUtils.readBlob(blob, function (returnedData) {
                 testUtils.btoa(returnedData).should.equal(data);
                 db.get('foo', function (err, doc) {
@@ -2718,6 +2816,7 @@
             should.not.exist(err, 'attachment inserted');
             db.getAttachment('foo', 'foo.png', function (err, blob) {
               should.not.exist(err, 'attachment gotten');
+              blob.type.should.equal('image/png');
               testUtils.readBlob(blob, function (returnedData) {
                 testUtils.btoa(returnedData).should.equal(data);
                 db.get('foo', function (err, doc) {
@@ -2909,9 +3008,8 @@
       return db.putAttachment('foo', 'foo.bin', base64, 'image/png').then(function () {
         return db.getAttachment('foo', 'foo.bin');
       }).then(function (blob) {
-        return new PouchDB.utils.Promise(function (resolve) {
-          testUtils.readBlob(blob, resolve);
-        });
+        blob.type.should.equal('image/png');
+        return testUtils.readBlobPromise(blob);
       }).then(function (bin) {
         testUtils.btoa(bin).should.equal(base64);
       });
@@ -2954,6 +3052,7 @@
                 should.not.exist(err, 'attachment inserted');
                 db.getAttachment('foo', 'foo.png', function (err, blob) {
                   should.not.exist(err, 'attachment gotten');
+                  blob.type.should.equal('image/png');
                   testUtils.readBlob(blob, function (returnedData) {
                     testUtils.btoa(returnedData).should.equal(data);
                     db.get('foo', function (err, doc) {
@@ -3167,6 +3266,41 @@
       });
     });
 
+    it('Attachment types replicate', function () {
+      var binAttDoc = {
+        _id: 'bin_doc',
+        _attachments: {
+          'foo.txt': {
+            content_type: 'text/plain',
+            data: 'VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ='
+          }
+        }
+      };
+      var docs1 = [
+        binAttDoc,
+        {_id: '0', integer: 0},
+        {_id: '1', integer: 1},
+        {_id: '2', integer: 2},
+        {_id: '3', integer: 3}
+      ];
+
+      var db = new PouchDB(dbs.name);
+      var remote = new PouchDB(dbs.remote);
+
+      return remote.bulkDocs({ docs: docs1 }).then(function(info) {
+        return db.replicate.from(remote);
+      }).then(function () {
+        return db.get('bin_doc', {attachments: true, binary: true});
+      }).then(function (doc) {
+        var blob = doc._attachments['foo.txt'].data;
+        blob.type.should.equal('text/plain');
+        return testUtils.readBlobPromise(blob);
+      }).then(function (bin) {
+        bin.should.equal(testUtils.atob(
+          'VGhpcyBpcyBhIGJhc2U2NCBlbmNvZGVkIHRleHQ='));
+      });
+    });
+
     it('Many many attachments replicate', function () {
       var doc = {_id: 'foo'};
 
@@ -3323,7 +3457,7 @@
           }
         });
       }
-      return remote.bulkDocs(docs).then(function (info) {
+      return remote.bulkDocs(docs).then(function () {
         return remote.replicate.to(db);
       }).then(function () {
         return db.allDocs();
diff --git a/tests/integration/utils.js b/tests/integration/utils.js
index 1f88e58..7d76ee0 100644
--- a/tests/integration/utils.js
+++ b/tests/integration/utils.js
@@ -55,7 +55,9 @@
   if (typeof module !== 'undefined' && module.exports) {
     return new Buffer(data, 'binary');
   } else {
-    return PouchDB.utils.createBlob([data], { type: type });
+    return PouchDB.utils.createBlob([data], {
+      type: (type || 'text/plain')
+    });
   }
 };