100% coverage (#4)

* doc(readme): clean up reasons

* doc(motto)

* test(db-and-doc): more coverage

* test(create-or-update-ignore-conflict)

* test(upsert)

* test(ignore-missing)

* test(post-and-ignore-conflict)

* test(get-merge-put)

* refactor(all): rename post and put

* test(get-merge-create-or-update)

* test(get-merge-update-ignore-conflict)

* test(get-merge-upsert)

* test(get-modify-upsert)

* refactor(doc): redundant code

* test(destroy-ignore-conflict)

* test(get-and-destroy)

* test(mark-as-destroyed)

* test(set-destroyed)

* refactor(attachment)

* test(doc): 100% coverage

* test(attachment): create with base 64

* test(attachment): clean up binary code

* test(attachment): get

* test(attachment): destroy

* test(system): is couchdb 1

* test(system): get

* test(system): reset

* test(updates)

* test(updates)

* test(all): unique DB names

* test(system): reactivate tests

* test(user): add role

* test(user): downsert role

* feat(stream-iterator): indefinite

* test(user): 100% coverage

* test(request-class)

* test(request-class): 100% coverage

* test(config)

* test(config): more coverage

* test(config): more coverage

* test(config): 100% coverage

* test(all): 100% coverage

* refactor(beautify)

* test(coverage): enforce 100%

* test(system): fix race condition

* test(user): shortcut for browser

* test(updates): test continuous stream in phantomjs

* test(updates): test continuous stream in phantomjs

* test(continuous): mock for phantomjs

* test(system): abort iterators

* test(system): fake abort
This commit is contained in:
Geoff Cox 2017-07-18 07:45:32 -07:00 committed by GitHub
parent 559b1a750c
commit da7ca1123e
29 changed files with 1619 additions and 280 deletions

View file

@ -2,18 +2,22 @@
[![Circle CI](https://circleci.com/gh/redgeoff/slouch.svg?style=svg&circle-token=ae7548ebc7e23a051ed03dbc3209c5e0529e260a)](https://circleci.com/gh/redgeoff/slouch)
## Install
$ npm install couch-slouch
An API for CouchDB that does the heavy lifting
## Slouch is a good alternative to nano as:
- Don't have to create an instance for each DB
- Native promises
- Support for iterators
- More CouchDB support, e.g. admin functions
- You don't have to create an instance for each DB
- Supports native promises
- Supports iterators
- Automatically throttles connections to DB to avoid max_dbs_open errors
- Automatically persists connections with exponential backoff in case DB restarts or connection is dropped
- Also works in the browser
- Upserts, "get and put", support for optionally ignoring conflicts, missing docs, etc...
- Works in node and in the browser
- Provides upserts and "get and update" functions
- Support for optionally ignoring conflicts, missing docs, etc...
- Designed for both CouchDB 1 and CouchDB 2
## Install
$ npm install couch-slouch

View file

@ -7,7 +7,18 @@ Install CouchDB locally. You can easily run CouchDB via docker with:
$ ./run-couchdb-docker.sh
Note: if you are not running ubuntu, you will probably have to configure the `common` variable
Notes:
- If you are not running ubuntu, you will probably have to configure the `common` variable
- If you are running the tests against a CouchDB instance on another box then you will need to enable CORs, e.g. ./enable-cors.sh and you will also need to change the `host` entry in test/spec/config.json
## Resetting the DB
If your DB accumulates a lot of junk data and you want to clear it, you can do so with:
$ npm run reset-db
Warning: this will delete all your databases!
## Test in node

View file

@ -1,7 +1,7 @@
{
"name": "couch-slouch",
"version": "0.0.1",
"description": "Interface with CouchDB without the heavy lifting",
"description": "An API for CouchDB that does the heavy lifting",
"main": "index.js",
"repository": {
"type": "git",
@ -21,7 +21,7 @@
"beautify": "beautify-proj -i test -o . -c beautify.json && beautify-proj -i scripts -o . -c beautify.json",
"jshint": "jshint -c .jshintrc test scripts",
"node-test": "istanbul test --dir cache/coverage/node node_modules/mocha/bin/_mocha test/node.js",
"node-full-test": "npm run node-test --coverage && istanbul check-coverage --lines 0 --function 0 --statements 0 --branches 0",
"node-full-test": "npm run node-test --coverage && istanbul check-coverage --lines 100 --function 100 --statements 100 --branches 100",
"browser-server": "node_modules/gofur/scripts/browser/serve.js -c cache -t test/browser.js",
"browser-test": "node_modules/gofur/scripts/browser/test.js -c cache -t test/browser.js",
"browser-test-firefox": "node_modules/gofur/scripts/browser/test.js -c cache -t test/browser.js -b selenium:firefox",
@ -30,11 +30,12 @@
"browser-coverage-server": "node_modules/gofur/scripts/browser-coverage/serve.js -c cache -t test/browser.js",
"browser-coverage-test": "node_modules/gofur/scripts/browser-coverage/test.js -c cache -t test/browser.js",
"browser-coverage-report": "istanbul report --dir cache/coverage/browser --root cache/coverage/browser lcov",
"browser-coverage-check": "istanbul check-coverage --lines 0 --function 0 --statements 0 --branches 0 cache/coverage/browser/coverage.json",
"browser-coverage-check": "istanbul check-coverage --lines 100 --function 100 --statements 100 --branches 100 cache/coverage/browser/coverage.json",
"browser-coverage-full-test": "npm run browser-coverage-test && npm run browser-coverage-report && npm run browser-coverage-check",
"test": "npm run assert-beautified && npm run jshint && npm run node-full-test && npm run browser-coverage-full-test",
"TODO-build": "",
"TODO-test-in-sauce": ""
"TODO-test-in-sauce": "",
"reset-db": "./reset-db.js"
},
"dependencies": {
"backoff-promise": "0.0.2",
@ -53,6 +54,7 @@
"gofur": "^0.0.7",
"istanbul": "^0.4.5",
"jshint": "^2.9.4",
"memorystream": "^0.3.1",
"mocha": "^3.4.2"
},
"greenkeeper": {

9
reset-db.js Executable file
View file

@ -0,0 +1,9 @@
#!/usr/bin/env node
'use strict';
var Slouch = require('./scripts'),
utils = require('./test/utils'),
slouch = new Slouch(utils.couchDBURL());
slouch.system.reset();

54
scripts/attachment.js Normal file
View file

@ -0,0 +1,54 @@
'use strict';
var promisedRequest = require('./request');
var Attachment = function (slouch) {
this._slouch = slouch;
};
// TODO
// Attachment.prototype.create = function (dbName, docId, attachmentName, data, contentType, rev) {
// var formData = {
// custom_file: {
// value: data,
// options: {
// filename: attachmentName,
// contentType: contentType
// }
// }
// };
//
// return promisedRequest.request({
// uri: this._slouch._url + '/' + dbName + '/' + docId + '/' + attachmentName +
// '?rev=' + encodeURIComponent(rev),
// method: 'PUT',
// // raw: true,
// // encoding: null,
// formData: formData
// }).then(function (response) {
// return response.body;
// });
// };
Attachment.prototype.get = function (dbName, docId, attachmentName) {
return promisedRequest.request({
uri: this._slouch._url + '/' + dbName + '/' + docId + '/' + attachmentName,
method: 'GET',
raw: true,
encoding: null
}).then(function (response) {
return response.body;
});
};
Attachment.prototype.destroy = function (dbName, docId, attachmentName, rev) {
return promisedRequest.request({
uri: this._slouch._url + '/' + dbName + '/' + docId + '/' + attachmentName,
method: 'DELETE',
qs: {
rev: rev
}
});
};
module.exports = Attachment;

View file

@ -1,24 +0,0 @@
'use strict';
var Auth = function (slouch) {
this._slouch = slouch;
};
Auth.prototype.onlyRoleCanView = function (dbName, role) {
return this._slouch.security.set(dbName, {
admins: {
names: ['_admin'],
roles: []
},
members: {
names: [],
roles: [role]
}
});
};
Auth.prototype.onlyAdminCanView = function (dbName) {
return this.onlyRoleCanView(dbName, '_admin');
};
module.exports = Auth;

View file

@ -5,11 +5,12 @@ var request = require('./request'),
var Config = function (slouch) {
this._slouch = slouch;
this._req = request;
};
Config.prototype._couchDB2Request = function (node, path, opts, parseBody) {
opts.uri = this._url + '/_node/' + node + '/_config/' + path;
return request.request(opts, parseBody);
opts.uri = this._slouch._url + '/_node/' + node + '/_config/' + path;
return this._req.request(opts, parseBody);
};
// Warning: as per https://github.com/klaemo/docker-couchdb/issues/42#issuecomment-169610897, this
@ -34,13 +35,13 @@ Config.prototype._couchDB2Requests = function (path, opts, parseBody, maxNumNode
};
Config.prototype._couchDB1Request = function (path, opts, parseBody) {
opts.uri = this._url + '/_config/' + path;
return request.request(opts, parseBody);
opts.uri = this._slouch._url + '/_config/' + path;
return this._req.request(opts, parseBody);
};
Config.prototype._request = function (path, opts, parseBody, maxNumNodes) {
var self = this;
return self._slouch._system.isCouchDB1().then(function (isCouchDB1) {
return self._slouch.system.isCouchDB1().then(function (isCouchDB1) {
if (isCouchDB1) {
return self._couchDB1Request(path, opts, parseBody);
} else {
@ -49,10 +50,16 @@ Config.prototype._request = function (path, opts, parseBody, maxNumNodes) {
});
};
Config.prototype.get = function (path) {
return this._request(path, {
method: 'GET'
}, true);
};
Config.prototype.set = function (path, value) {
return this._request(path, {
method: 'PUT',
body: JSON.stringify(value)
body: JSON.stringify(this._toString(value))
});
};
@ -74,6 +81,16 @@ Config.prototype.setCouchHttpdAuthTimeout = function (timeoutSecs) {
return this.set('couch_httpd_auth/timeout', timeoutSecs + '');
};
Config.prototype._toString = function (value) {
if (typeof value === 'boolean') {
return value ? 'true' : 'false';
} else if (typeof value === 'string') {
return value;
} else {
return value + ''; // convert to string
}
};
Config.prototype.setCouchHttpdAuthAllowPersistentCookies = function (allow) {
return this.set('couch_httpd_auth/allow_persistent_cookies', allow);
};

View file

@ -1,13 +1,12 @@
'use strict';
var promisedRequest = require('./request'),
FilteredStreamIterator = require('quelle').FilteredStreamIterator,
PersistentStreamIterator = require('quelle').PersistentStreamIterator,
StreamIterator = require('quelle').StreamIterator,
sporks = require('sporks');
request = require('request');
var DB = function (slouch) {
this._slouch = slouch;
this._request = request;
};
DB.prototype._create = function (dbName) {
@ -74,22 +73,22 @@ DB.prototype.changes = function (dbName, params) {
url: this._slouch._url + '/' + dbName + '/_changes',
method: 'GET',
qs: params
}, jsonStreamParseStr, indefinite);
}, jsonStreamParseStr, indefinite, this._request);
};
DB.prototype.view = function (dbName, viewDocId, view, params) {
return new PersistentStreamIterator({
url: this._slouch_url + '/' + dbName + '/' + viewDocId + '/_view/' + view,
url: this._slouch._url + '/' + dbName + '/' + viewDocId + '/_view/' + view,
qs: params
}, 'rows.*');
};
DB.prototype.viewArray = function (dbName, viewDocId, view, params) {
return promisedRequest.request({
url: this._slouch_url + '/' + dbName + '/' + viewDocId + '/_view/' + view,
url: this._slouch._url + '/' + dbName + '/' + viewDocId + '/_view/' + view,
qs: params
});
}, true);
};
// Use a JSONStream so that we don't have to load a large JSON structure into memory
@ -101,12 +100,13 @@ DB.prototype.all = function () {
DB.prototype.replicate = function (params) {
return promisedRequest.request({
url: this._url + '/_replicate',
url: this._slouch._url + '/_replicate',
method: 'POST',
json: params
});
};
// TODO: support fromDBName and toDbName also being URLs
DB.prototype.copy = function (fromDBName, toDBName) {
var self = this;
return self.create(toDBName).then(function () {
@ -121,71 +121,4 @@ DB.prototype.copy = function (fromDBName, toDBName) {
});
};
// Use a JSONStream so that we don't have to load a large JSON structure into memory
DB.prototype.updates = function (params) {
var indefinite = false,
jsonStreamParseStr = null;
if (params && params.feed === 'continuous') {
indefinite = true;
jsonStreamParseStr = undefined;
} else {
jsonStreamParseStr = 'results.*';
}
return new PersistentStreamIterator({
url: this._slouch._url + '/_db_updates',
method: 'GET',
qs: params
}, jsonStreamParseStr, indefinite);
};
DB.prototype.updatesViaGlobalChanges = function (params) {
var self = this,
iterator = new StreamIterator();
self.get('_global_changes').then(function (dbDoc) {
var clonedParams = sporks.clone(params);
clonedParams.since = dbDoc.update_seq;
// We pipe to the returned iterator so that the function can return an iterator who's content is
// deferred.
self.changes('_global_changes', clonedParams).pipe(iterator);
});
return new FilteredStreamIterator(iterator, function (item) {
// Repackage the item so that it is compatible with _db_updates.
var parts = item.id.split(':');
return {
db_name: parts[1],
type: parts[0]
};
});
};
// The _db_updates feed in CouchDB does not include any history, i.e. any updates before when we
// start listening to the feed. CouchDB 2 on the other hand stores the complete history in the
// _global_changes database. We use the _changes feed on the _global_changes database to provide a
// backwards compatible API.
DB.prototype.updatesNoHistory = function (params) {
var self = this,
iterator = new StreamIterator();
self._slouch._system.isCouchDB1().then(function (isCouchDB1) {
if (isCouchDB1) {
return self.updates(params);
} else {
return self.updatesViaGlobalChanges(params);
}
}).then(function (_iterator) {
// We pipe to the returned iterator so that the function can return an iterator who's content is
// deferred.
_iterator.pipe(iterator);
});
return iterator;
};
module.exports = DB;

View file

@ -35,8 +35,7 @@ Doc.prototype.ignoreMissing = function (promiseFactory) {
});
};
// Use to create doc
Doc.prototype.post = function (dbName, doc) {
Doc.prototype.create = function (dbName, doc) {
return promisedRequest.request({
uri: this._slouch._url + '/' + dbName,
method: 'POST',
@ -46,30 +45,29 @@ Doc.prototype.post = function (dbName, doc) {
});
};
Doc.prototype.postAndIgnoreConflict = function (dbName, doc) {
Doc.prototype.createAndIgnoreConflict = function (dbName, doc) {
var self = this;
return self.ignoreConflict(function () {
return self.post(dbName, doc);
return self.create(dbName, doc);
});
};
// Use to update doc
Doc.prototype.put = function (dbName, doc) {
Doc.prototype.update = function (dbName, doc) {
return promisedRequest.request({
uri: this._slouch._url + '/' + dbName + '/' + doc._id,
method: 'PUT',
body: JSON.stringify(doc)
}).then(function () {
// Return doc so that callers like getMergePut have an automatic way to get the data that was
// put
// Return doc so that callers like getMergeUpdate have an automatic way to get the data that was
// update
return doc;
});
};
Doc.prototype.putIgnoreConflict = function (dbName, doc) {
Doc.prototype.updateIgnoreConflict = function (dbName, doc) {
var self = this;
return self.ignoreConflict(function () {
return self.put(dbName, doc);
return self.update(dbName, doc);
});
};
@ -87,6 +85,14 @@ Doc.prototype.getIgnoreMissing = function (dbName, id) {
});
};
Doc.prototype.exists = function (dbName, id) {
return this.get(dbName, id).then(function () {
return true;
}).catch(function () {
return false;
});
};
Doc.prototype.createOrUpdate = function (dbName, doc) {
var self = this,
@ -97,14 +103,14 @@ Doc.prototype.createOrUpdate = function (dbName, doc) {
// Use the latest rev so that we can attempt to update the doc without a conflict
clonedDoc._rev = _doc._rev;
return self.put(dbName, clonedDoc);
return self.update(dbName, clonedDoc);
}).catch(function (err) {
if (self.isMissingError(err)) { // missing? This can be expected on the first put
if (self.isMissingError(err)) { // missing? This can be expected on the first update
// The doc is missing so we attempt to create the doc w/o a rev number
return self.post(dbName, doc);
return self.create(dbName, doc);
} else {
@ -123,18 +129,19 @@ Doc.prototype.createOrUpdateIgnoreConflict = function (dbName, doc) {
});
};
Doc.prototype.upsert = function (dbName, doc) {
Doc.prototype._persistThroughConflicts = function (promiseFactory) {
var self = this,
i = 0;
var _upsert = function () {
return self.createOrUpdate(dbName, doc).catch(function (err) {
var run = function () {
return promiseFactory().catch(function (err) {
if (err.error === 'conflict' && i++ < self.maxRetries) { // conflict?
// Retry
return _upsert();
return run();
} else {
@ -144,12 +151,20 @@ Doc.prototype.upsert = function (dbName, doc) {
}
});
};
return _upsert();
return run();
};
Doc.prototype.getMergePut = function (dbName, doc) {
Doc.prototype.upsert = function (dbName, doc) {
var self = this;
return self._persistThroughConflicts(function () {
return self.createOrUpdate(dbName, doc);
});
};
Doc.prototype.getMergeUpdate = function (dbName, doc) {
var self = this;
@ -159,7 +174,7 @@ Doc.prototype.getMergePut = function (dbName, doc) {
clonedDoc = sporks.merge(clonedDoc, doc);
return self.put(dbName, clonedDoc);
return self.update(dbName, clonedDoc);
});
};
@ -184,68 +199,29 @@ Doc.prototype.getMergeCreateOrUpdate = function (dbName, doc) {
});
};
Doc.prototype.getMergePutIgnoreConflict = function (dbName, doc) {
Doc.prototype.getMergeUpdateIgnoreConflict = function (dbName, doc) {
var self = this;
return self.ignoreConflict(function () {
return self.getMergePut(dbName, doc);
return self.getMergeUpdate(dbName, doc);
});
};
Doc.prototype.getMergeUpsert = function (dbName, doc) {
var self = this,
i = 0;
var _upsert = function () {
return self.getMergeCreateOrUpdate(dbName, doc).catch(function (err) {
if (err.error === 'conflict' && i++ < self.maxRetries) { // conflict?
// Retry
return _upsert();
} else {
// Unexpected error
throw err;
}
});
};
return _upsert();
var self = this;
return self._persistThroughConflicts(function () {
return self.getMergeCreateOrUpdate(dbName, doc);
});
};
Doc.prototype.getModifyUpsert = function (dbName, docId, onGetPromiseFactory) {
var self = this,
i = 0;
var _upsert = function () {
var self = this;
return self._persistThroughConflicts(function () {
return self.get(dbName, docId).then(function (doc) {
return onGetPromiseFactory(doc);
}).then(function (modifiedDoc) {
return self.put(dbName, modifiedDoc);
}).catch(function (err) {
if (err.error === 'conflict' && i++ < self.maxRetries) { // conflict?
// Retry
return _upsert();
} else {
// Unexpected error
throw err;
}
return self.update(dbName, modifiedDoc);
});
};
return _upsert();
});
};
Doc.prototype.allArray = function (dbName, params) {
@ -269,12 +245,11 @@ Doc.prototype.destroyAllNonDesign = function (dbName) {
return this.destroyAll(dbName, true);
};
Doc.prototype.destroyAll = function (dbName, keepDesignDocs, exceptDBNames) {
Doc.prototype.destroyAll = function (dbName, keepDesignDocs) {
var self = this;
return self.all(dbName).each(function (doc) {
if ((!keepDesignDocs || doc.id.indexOf('_design') === -1) &&
(!exceptDBNames || exceptDBNames.indexOf(doc.id) !== -1)) {
if (!keepDesignDocs || doc.id.indexOf('_design') === -1) {
return self.destroy(dbName, doc.id, doc.value.rev);
}
});
@ -297,34 +272,23 @@ Doc.prototype.destroyIgnoreConflict = function (dbName, docId, docRev) {
});
};
Doc.prototype.getAnddestroy = function (dbName, docId) {
Doc.prototype.getAndDestroy = function (dbName, docId) {
var self = this;
return self.get(dbName, docId).then(function (doc) {
return self.destroy(dbName, docId, doc._rev);
});
};
Doc.prototype.markDocAsDestroyed = function (dbName, docId) {
return this.getMergePut(dbName, {
Doc.prototype.markAsDestroyed = function (dbName, docId) {
return this.getMergeUpdate(dbName, {
_id: docId,
_deleted: true
});
};
// Just for formalizing the setting of the _deleted flag
Doc.prototype.setDeleted = function (doc) {
Doc.prototype.setDestroyed = function (doc) {
doc._deleted = true;
};
Doc.prototype.getAttachment = function (dbName, docId, attachmentName) {
return promisedRequest.request({
uri: this._slouch._url + '/' + dbName + '/' + docId + '/' + attachmentName,
method: 'GET',
raw: true,
encoding: null
}).then(function (response) {
return response.body;
});
};
module.exports = Doc;

View file

@ -1,6 +1,6 @@
'use strict';
var Auth = require('./auth'),
var Attachment = require('./attachment'),
Config = require('./config'),
DB = require('./db'),
Doc = require('./doc'),
@ -14,7 +14,7 @@ var Auth = require('./auth'),
var Slouch = function (url) {
this._url = url;
this.auth = new Auth(this);
this.attachment = new Attachment(this);
this.config = new Config(this);
this.db = new DB(this);
this.doc = new Doc(this);

View file

@ -14,6 +14,7 @@ QueryString.prototype.unescape = function (s) {
var RequestClass = function () {
this._throttler = new Throttler(RequestClass.DEFAULT_CONNECTIONS);
this._req = req;
};
// For debugging all traffic
@ -49,7 +50,7 @@ RequestClass.prototype._request = function (opts, parseBody) {
var self = this,
selfArguments = arguments;
return req.apply(this, arguments).then(function (response) {
return self._req.apply(this, arguments).then(function (response) {
var err = null;

View file

@ -32,4 +32,21 @@ Security.prototype.get = function (dbName) {
}, true);
};
Security.prototype.onlyRoleCanView = function (dbName, role) {
return this.set(dbName, {
admins: {
names: ['_admin'],
roles: []
},
members: {
names: [],
roles: [role]
}
});
};
Security.prototype.onlyAdminCanView = function (dbName) {
return this.onlyRoleCanView(dbName, '_admin');
};
module.exports = Security;

View file

@ -1,11 +1,17 @@
'use strict';
var promisedRequest = require('./request'),
FilteredStreamIterator = require('quelle').FilteredStreamIterator,
PersistentStreamIterator = require('quelle').PersistentStreamIterator,
StreamIterator = require('quelle').StreamIterator,
sporks = require('sporks'),
Promise = require('sporks/scripts/promise');
Promise = require('sporks/scripts/promise'),
request = require('request');
var System = function (slouch) {
this._slouch = slouch;
this._couchDB1 = null;
this._request = request;
};
System.prototype._isCouchDB1 = function () {
@ -64,4 +70,82 @@ System.prototype.reset = function (exceptDBNames) {
});
};
// Use a JSONStream so that we don't have to load a large JSON structure into memory
System.prototype.updates = function (params) {
var indefinite = false,
jsonStreamParseStr = null;
if (params && params.feed === 'continuous') {
indefinite = true;
jsonStreamParseStr = undefined;
} else {
jsonStreamParseStr = 'results.*';
}
return new PersistentStreamIterator({
url: this._slouch._url + '/_db_updates',
method: 'GET',
qs: params
}, jsonStreamParseStr, indefinite, this._request);
};
System.prototype._cloneParams = function (params) {
return params ? sporks.clone(params) : {};
};
System.prototype._itemToUpdate = function (item) {
if (item.id) {
// Repackage the item so that it is compatible with _db_updates.
var parts = item.id.split(':');
return {
db_name: parts[1],
type: parts[0]
};
} else {
// Ignore items that don't have ids
return undefined;
}
};
System.prototype.updatesViaGlobalChanges = function (params) {
var self = this,
iterator = new StreamIterator();
self._slouch.db.get('_global_changes').then(function (dbDoc) {
var clonedParams = self._cloneParams(params);
clonedParams.since = dbDoc.update_seq;
// We pipe to the returned iterator so that the function can return an iterator who's content is
// deferred.
self._slouch.db.changes('_global_changes', clonedParams).pipe(iterator);
});
return new FilteredStreamIterator(iterator, function (item) {
return self._itemToUpdate(item);
});
};
// The _db_updates feed in CouchDB does not include any history, i.e. any updates before when we
// start listening to the feed. CouchDB 2 on the other hand stores the complete history in the
// _global_changes database. We use the _changes feed on the _global_changes database to provide a
// backwards compatible API.
System.prototype.updatesNoHistory = function (params) {
var self = this,
iterator = new StreamIterator();
self._slouch.system.isCouchDB1().then(function (isCouchDB1) {
if (isCouchDB1) {
return self.updates(params);
} else {
return self.updatesViaGlobalChanges(params);
}
}).then(function (_iterator) {
// We pipe to the returned iterator so that the function can return an iterator who's content is
// deferred.
_iterator.pipe(iterator);
});
return iterator;
};
module.exports = System;

View file

@ -8,6 +8,7 @@ var NotAuthenticatedError = require('./not-authenticated-error'),
var User = function (slouch) {
this._slouch = slouch;
this._dbName = '_users';
this._request = request;
};
User.prototype.toUserId = function (username) {
@ -20,7 +21,7 @@ User.prototype.toUsername = function (userId) {
User.prototype._insert = function (username, user) {
user._id = this.toUserId(username);
return this._slouch.doc.put(this._dbName, user);
return this._slouch.doc.update(this._dbName, user);
};
User.prototype.create = function (username, password, roles, metadata) {
@ -48,13 +49,14 @@ User.prototype.addRole = function (username, role) {
var self = this;
return self.get(username).then(function (user) {
user.roles.push(role);
return self._update(username, user).catch(function (err) {
if (err.statusCode === 409) { // conflict? Try again
return self.addRole(username, role);
} else {
throw err;
}
});
return self._update(username, user);
});
};
User.prototype.upsertRole = function (username, role) {
var self = this;
return self._slouch.doc._persistThroughConflicts(function () {
return self.addRole(username, role);
});
};
@ -66,6 +68,13 @@ User.prototype.removeRole = function (username, role) {
});
};
User.prototype.downsertRole = function (username, role) {
var self = this;
return self._slouch.doc._persistThroughConflicts(function () {
return self.removeRole(username, role);
});
};
User.prototype.setPassword = function (username, password) {
var self = this;
return self.get(username).then(function (user) {
@ -94,7 +103,7 @@ User.prototype.destroy = function (username) {
};
User.prototype.authenticate = function (username, password) {
return this.postSession({
return this.createSession({
name: username,
password: password
}).then(function (response) {
@ -106,21 +115,24 @@ User.prototype.authenticate = function (username, password) {
});
};
User.prototype.postSession = function (doc) {
return request.request({
uri: this._slouch_url + '/_session',
User.prototype.createSession = function (doc) {
return this._request.request({
uri: this._slouch._url + '/_session',
method: 'POST',
json: doc
});
};
// TODO: get authenticate() and authenticated() working properly in the browser. For now, we
// have to fake the responses as it appears that the session cookie is not being propogated from
// the session post to the session get.
User.prototype.authenticated = function (cookie) {
// Specify a URL w/o a username and password as we want to check to make sure that the cookie is
// for a current session
var parts = url.parse(this._slouch._url);
var _url = parts.protocol + '//' + parts.host + parts.pathname;
return request.request({
return this._request.request({
uri: _url + '/_session',
method: 'GET',
headers: {

BIN
test/couch.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

View file

@ -4,4 +4,11 @@ var chai = require('chai');
chai.use(require('chai-as-promised'));
chai.should();
require('./spec');
describe('slouch', function () {
// Sometimes the DB gets a little backed up so we need more time for our tests
this.timeout(10000);
require('./spec');
});

90
test/spec/attachment.js Normal file
View file

@ -0,0 +1,90 @@
'use strict';
var Slouch = require('../../scripts'),
utils = require('../utils');
describe('attachment', function () {
var slouch = null,
db = null;
// Base64 encoded 10px by 10px black PNG. Source: http://png-pixel.com
var base64Data = [
'iVBORw0KGgoAAAANSUhEUgAAAAoAAAAKCAQAAAAnOwc2AAAAEUlEQVR42mNk+M+AARiHsiAAcCIKAYwFoQ8AAAAASUVO',
'RK5CYII='
].join('');
// TODO: will be needed when support binary attachments
// // As per https://stackoverflow.com/a/14573049/2831606, we need an abstraction as the API can
// // differ
// var bufferFrom = function () {
// if (typeof Buffer.from === 'function') {
// // Node 5.10+
// return Buffer.from(base64Data, 'base64');
// } else {
// // older Node versions
// return new Buffer(base64Data, 'base64');
// }
// };
beforeEach(function () {
slouch = new Slouch(utils.couchDBURL());
db = slouch.db;
return utils.createDB();
});
afterEach(function () {
return utils.destroyDB();
});
var createBase64Attachment = function () {
return slouch.doc.update(utils.createdDB, {
_id: 'foo',
_attachments: {
'my_image.png': {
data: base64Data,
content_type: 'image/png'
}
}
});
};
// TODO
// it('should create attachment', function () {
// var data = bufferFrom(base64Data);
// return slouch.doc.create(utils.createdDB, {
// _id: 'foo'
// }).then(function () {
// return slouch.doc.get(utils.createdDB, 'foo');
// }).then(function (doc) {
// return slouch.attachment.create(utils.createdDB, 'foo', 'my_file.png', data, 'image/png',
// doc._rev);
// });
// });
it('should create attachment from base 64 data', function () {
return createBase64Attachment().then(function () {
return slouch.doc.get(utils.createdDB, 'foo');
}).then(function (doc) {
doc._attachments['my_image.png'].content_type.should.eql('image/png');
return slouch.attachment.get(utils.createdDB, 'foo', 'my_image.png');
}).then(function (attachment) {
var base64Attach = new Buffer(attachment).toString('base64');
base64Attach.should.eql(base64Data);
});
});
it('should destroy attachment', function () {
return createBase64Attachment().then(function () {
return slouch.doc.get(utils.createdDB, 'foo');
}).then(function (doc) {
return slouch.attachment.destroy(utils.createdDB, 'foo', 'my_image.png', doc._rev);
}).then(function () {
return slouch.doc.get(utils.createdDB, 'foo');
}).then(function (doc) {
(doc._attachments === undefined).should.eql(true);
});
});
});

117
test/spec/config.js Normal file
View file

@ -0,0 +1,117 @@
'use strict';
// Note: we need to make sure that we have complete test coverage on both CouchDB 1 and CouchDB 2,
// but also want to test against the actual DB.
var Slouch = require('../../scripts'),
utils = require('../utils'),
sporks = require('sporks'),
Promise = require('sporks/scripts/promise');
describe('config', function () {
var slouch = null,
config = null,
requests = null;
beforeEach(function () {
slouch = new Slouch(utils.couchDBURL());
config = slouch.config;
requests = [];
});
var fakeIsCouchDB1 = function (isCouchDB1) {
slouch.system.isCouchDB1 = function () {
return Promise.resolve(isCouchDB1);
};
};
var mockRequest = function (nodes) {
config._req = {
request: function (opts) {
requests.push(opts);
return Promise.resolve();
}
};
slouch.membership.get = function () {
return Promise.resolve({
cluster_nodes: nodes
});
};
};
var shouldSet = function (isCouchDB1, nodes) {
fakeIsCouchDB1(isCouchDB1);
mockRequest(nodes);
return config.set('foo', 'bar').then(function () {
// Sanity test
JSON.parse(requests[0].body).should.eql('bar');
});
};
it('should set for couchdb 1', function () {
return shouldSet(true);
});
it('should set for couchdb 2', function () {
return shouldSet(false, ['node1']);
});
it('should set for couchdb 2 when multiple nodes', function () {
return shouldSet(false, ['node1', 'node2']);
});
it('should request for max nodes', function () {
fakeIsCouchDB1(false);
mockRequest(['node1', 'node2', 'node3']);
return config._request('path', {}, true, 2);
});
it('should couch_httpd_auth/timeout', function () {
return config.setCouchHttpdAuthTimeout(3600);
});
it('should get and unset', function () {
return config.setLogLevel('warning').then(function () {
return config.get('log/level');
}).then(function (value) {
value.should.eql('warning');
return config.unset('log/level');
}).then(function () {
return sporks.shouldThrow(function () {
return config.get('log/level');
});
});
});
it('should unset ignore missing', function () {
return config.setLogLevel('info').then(function () {
return config.unset('log/level');
}).then(function () {
return config.unsetIgnoreMissing('log/level');
});
});
it('should set couch_httpd_auth/allow_persistent_cookies', function () {
return config.setCouchHttpdAuthAllowPersistentCookies(false).then(function () {
return config.setCouchHttpdAuthAllowPersistentCookies(true);
});
});
it('should set compaction rule', function () {
return config.setCompactionRule('_default',
'[{db_fragmentation, "70%"}, {view_fragmentation, "60%"}, {from, "06:00"}, {to, "10:00"}]'
);
});
it('should set max_dbs_open', function () {
return config.setCouchDBMaxDBsOpen(500);
});
it('should set httpd/max_connections', function () {
return config.setHttpdMaxConnections(2048);
});
});

View file

@ -1,25 +1,85 @@
'use strict';
var Slouch = require('../../scripts'),
utils = require('../utils');
utils = require('../utils'),
sporks = require('sporks'),
Promise = require('sporks/scripts/promise');
describe('db', function () {
var db = new Slouch(utils.couchDBURL()).db;
var slouch = null,
db = null,
dbsToDestroy = null;
beforeEach(function () {
return db.create('testdb');
slouch = new Slouch(utils.couchDBURL());
db = slouch.db;
dbsToDestroy = [];
return utils.createDB().then(function () {
dbsToDestroy.push(utils.createdDB);
});
});
afterEach(function () {
return db.destroy('testdb');
var promises = [];
dbsToDestroy.forEach(function (name) {
promises.push(db.destroy(name));
});
return Promise.all(promises);
});
var createDocs = function () {
return slouch.doc.create(utils.createdDB, {
thing: 'jam'
}).then(function () {
return slouch.doc.create(utils.createdDB, {
thing: 'clean',
fun: false
});
}).then(function () {
return slouch.doc.create(utils.createdDB, {
thing: 'code'
});
});
};
var createView = function () {
return slouch.doc.create(utils.createdDB, {
_id: '_design/myview',
views: {
fun: {
map: [
'function(doc) {',
'if (doc.fun !== false) {',
'emit(doc._id, null);',
'}',
'}'
].join(' ')
}
}
});
};
var verifyAllDocs = function (dbName) {
var docs = {};
return slouch.doc.all(dbName, {
include_docs: true
}).each(function (doc) {
docs[doc.doc.thing] = true;
}).then(function () {
docs.should.eql({
jam: true,
clean: true,
code: true
});
});
};
it('should check if exists', function () {
return db.exists('testdb').then(function (exists) {
return db.exists(utils.createdDB).then(function (exists) {
exists.should.eql(true);
}).then(function () {
return db.exists('testdb2');
return db.exists(utils.createdDB + '_2');
}).then(function (exists) {
exists.should.eql(false);
});
@ -45,4 +105,116 @@ describe('db', function () {
});
});
it('create should not throw if DB is created', function () {
var err = new Error();
// Fake error
db._create = function () {
return sporks.promiseError(err);
};
return sporks.shouldThrow(function () {
return db.create('missing-db');
}, err);
});
it('create should throw if DB is not created', function () {
var err = new Error();
// Fake error
db._create = function () {
return sporks.promiseError(err);
};
return utils.createDB();
});
it('should get db', function () {
return db.get(utils.createdDB).then(function (_db) {
_db.db_name.should.eql(utils.createdDB);
});
});
it('should get changes', function () {
var changes = {};
return createDocs().then(function () {
return db.changes(utils.createdDB, {
include_docs: true
}).each(function (change) {
// Use associative array as order is not guaranteed
changes[change.doc.thing] = true;
});
}).then(function () {
changes.should.eql({
jam: true,
clean: true,
code: true
});
});
});
it('should get view', function () {
var docs = {};
return createDocs().then(function () {
return createView();
}).then(function () {
return db.view(utils.createdDB, '_design/myview', 'fun', {
include_docs: true
}).each(function (doc) {
// Use associative array as order is not guaranteed
docs[doc.doc.thing] = true;
});
}).then(function () {
docs.should.eql({
jam: true,
code: true
});
});
});
it('should get view array', function () {
var docs = {};
return createDocs().then(function () {
return createView();
}).then(function () {
return db.viewArray(utils.createdDB, '_design/myview', 'fun', {
include_docs: true
}).then(function (_docs) {
_docs.rows.forEach(function (_doc) {
// Use associative array as order is not guaranteed
docs[_doc.doc.thing] = true;
});
});
}).then(function () {
docs.should.eql({
jam: true,
code: true
});
});
});
it('should replicate', function () {
return createDocs().then(function () {
return db.create(utils.createdDB + '_2');
}).then(function () {
dbsToDestroy.push(utils.createdDB + '_2');
return db.replicate({
source: slouch._url + '/' + utils.createdDB,
target: slouch._url + '/' + utils.createdDB + '_2'
});
}).then(function () {
return verifyAllDocs(utils.createdDB + '_2');
});
});
it('should copy', function () {
return createDocs().then(function () {
return db.create(utils.createdDB + '_2');
}).then(function () {
return db.copy(utils.createdDB, utils.createdDB + '_2');
}).then(function () {
return verifyAllDocs(utils.createdDB + '_2');
});
});
});

View file

@ -2,35 +2,99 @@
var Slouch = require('../../scripts'),
utils = require('../utils'),
sporks = require('sporks');
sporks = require('sporks'),
Promise = require('sporks/scripts/promise');
describe('doc', function () {
var slouch = new Slouch(utils.couchDBURL()),
db = slouch.db;
var slouch = null,
db = null,
defaultGet = null,
defaultUpdate = null,
conflictDoc = null;
beforeEach(function () {
return db.create('testdb');
slouch = new Slouch(utils.couchDBURL());
db = slouch.db;
return utils.createDB();
});
afterEach(function () {
return db.destroy('testdb');
return utils.destroyDB();
});
it('should post, put and get doc', function () {
var createDocs = function () {
return slouch.doc.create(utils.createdDB, {
_id: '1',
thing: 'jam'
}).then(function () {
return slouch.doc.create(utils.createdDB, {
thing: 'clean',
fun: false
});
}).then(function () {
return slouch.doc.create(utils.createdDB, {
thing: 'code'
});
});
};
var fakeConflict = function (numConflicts) {
defaultUpdate = slouch.doc.update;
var i = 0;
// Fake resolution of conflict
slouch.doc.update = function () {
if (numConflicts && i++ > numConflicts) {
// Resolve after a few attempts
slouch.doc.get = defaultGet;
}
return defaultUpdate.apply(this, arguments);
};
return createDocs().then(function () {
return slouch.doc.get(utils.createdDB, '1');
}).then(function (doc) {
conflictDoc = doc;
return slouch.doc.createOrUpdate(utils.createdDB, {
_id: '1',
thing: 'dance'
}).then(function () {
defaultGet = slouch.doc.get;
// Fake conflict
slouch.doc.get = function () {
return Promise.resolve({
_id: '1',
_rev: doc._rev,
thing: 'dance'
});
};
});
});
};
it('should create, update and get doc', function () {
var doc = {
thing: 'play'
};
return slouch.doc.post('testdb', doc).then(function (_doc) {
return slouch.doc.create(utils.createdDB, doc).then(function (_doc) {
doc._id = _doc.id;
return slouch.doc.get('testdb', doc._id);
return slouch.doc.get(utils.createdDB, doc._id);
}).then(function (body) {
doc._rev = body._rev;
doc.priority = 'medium';
return slouch.doc.put('testdb', doc);
return slouch.doc.update(utils.createdDB, doc);
}).then(function () {
return slouch.doc.get('testdb', doc._id);
return slouch.doc.get(utils.createdDB, doc._id);
}).then(function (body) {
doc._rev = body._rev;
body.should.eql(doc);
@ -38,21 +102,21 @@ describe('doc', function () {
});
it('should destroy all non-design docs', function () {
return slouch.doc.post('testdb', {
return slouch.doc.create(utils.createdDB, {
thing: 'play'
}).then(function () {
return slouch.doc.post('testdb', {
return slouch.doc.create(utils.createdDB, {
thing: 'write'
});
}).then(function () {
return slouch.doc.post('testdb', {
return slouch.doc.create(utils.createdDB, {
_id: '_design/mydesign',
foo: 'bar'
});
}).then(function () {
return slouch.doc.destroyAllNonDesign('testdb');
return slouch.doc.destroyAllNonDesign(utils.createdDB);
}).then(function () {
return slouch.doc.allArray('testdb');
return slouch.doc.allArray(utils.createdDB);
}).then(function (body) {
body.total_rows.should.eql(1);
});
@ -63,13 +127,289 @@ describe('doc', function () {
var doc = {
thing: 'play'
};
return slouch.doc.post('testdb', doc).then(function (_doc) {
return slouch.doc.create(utils.createdDB, doc).then(function (_doc) {
doc._id = _doc.id;
doc.priority = 'medium';
// Generates conflict as no rev provided
return slouch.doc.put('testdb', doc);
return slouch.doc.update(utils.createdDB, doc);
});
});
});
it('should ignore conflict when updateting', function () {
return slouch.doc.create(utils.createdDB, {
_id: '1',
thing: 'jam'
}).then(function () {
return slouch.doc.updateIgnoreConflict(utils.createdDB, {
_id: '1',
thing: 'clean'
});
});
});
it('should only ignore conflicts', function () {
return sporks.shouldThrow(function () {
return slouch.doc.updateIgnoreConflict('missingdb', {
thing: 'clean'
});
});
});
it('should ignore missing docs', function () {
return slouch.doc.getIgnoreMissing(utils.createdDB, 'missingid');
});
it('should check if doc exists', function () {
return createDocs().then(function () {
return slouch.doc.exists(utils.createdDB, '1');
}).then(function (exists) {
exists.should.eql(true);
});
});
it('should throw if not missing', function () {
return sporks.shouldThrow(function () {
return slouch.doc.ignoreMissing(function () {
return sporks.promiseError(new Error());
});
});
});
it('should create and ignore conflict', function () {
return fakeConflict().then(function () {
return slouch.doc.createAndIgnoreConflict(utils.createdDB, {
_id: '1',
thing: 'dance'
});
});
});
it('should create when creating or updating', function () {
return slouch.doc.createOrUpdate(utils.createdDB, {
_id: '1',
thing: 'jam'
}).then(function () {
return slouch.doc.get(utils.createdDB, '1');
}).then(function (doc) {
doc._id.should.eql('1');
doc.thing.should.eql('jam');
});
});
it('should update when creating or updating', function () {
return createDocs().then(function () {
return slouch.doc.createOrUpdate(utils.createdDB, {
_id: '1',
thing: 'dance'
});
}).then(function () {
return slouch.doc.get(utils.createdDB, '1');
}).then(function (doc) {
doc._id.should.eql('1');
doc.thing.should.eql('dance');
});
});
it('should throw when creating or updating', function () {
// Fake error
slouch.doc.get = function () {
return Promise.resolve({
_rev: 'bad-rev'
});
};
return createDocs().then(function () {
return sporks.shouldThrow(function () {
return slouch.doc.createOrUpdate(utils.createdDB, {
_id: '1',
thing: 'jam'
});
});
});
});
it('should ignore conflict when creating or updating', function () {
return fakeConflict().then(function () {
return slouch.doc.createOrUpdateIgnoreConflict(utils.createdDB, {
_id: '1',
thing: 'sing'
});
});
});
it('should upsert when conflict', function () {
return fakeConflict(3).then(function () {
return slouch.doc.upsert(utils.createdDB, {
_id: '1',
thing: 'dance'
});
}).then(function () {
return slouch.doc.get(utils.createdDB, '1');
}).then(function (doc) {
doc._id.should.eql('1');
doc.thing.should.eql('dance');
});
});
it('upsert should fail after max retries', function () {
slouch.maxRetries = 3;
return fakeConflict().then(function () {
return sporks.shouldThrow(function () {
return slouch.doc.upsert(utils.createdDB, {
_id: '1',
thing: 'dance'
});
});
});
});
it('should get, merge and update', function () {
return createDocs().then(function () {
return slouch.doc.getMergeUpdate(utils.createdDB, {
_id: '1',
priority: 'high'
});
}).then(function () {
return slouch.doc.get(utils.createdDB, '1');
}).then(function (doc) {
doc._id.should.eql('1');
doc.thing.should.eql('jam');
doc.priority.should.eql('high');
});
});
it('should get, merge, create or update when doc existing', function () {
return createDocs().then(function () {
return slouch.doc.getMergeCreateOrUpdate(utils.createdDB, {
_id: '1',
priority: 'high'
});
}).then(function () {
return slouch.doc.get(utils.createdDB, '1');
}).then(function (doc) {
doc._id.should.eql('1');
doc.thing.should.eql('jam');
doc.priority.should.eql('high');
});
});
it('should get, merge, create or update when missing', function () {
return slouch.doc.getMergeCreateOrUpdate(utils.createdDB, {
_id: '1',
priority: 'high'
}).then(function () {
return slouch.doc.get(utils.createdDB, '1');
}).then(function (doc) {
doc._id.should.eql('1');
doc.priority.should.eql('high');
});
});
it('should ignore conflict when getting, merging and updating', function () {
return fakeConflict().then(function () {
return slouch.doc.getMergeUpdateIgnoreConflict(utils.createdDB, {
_id: '1',
thing: 'sing'
});
});
});
it('should get, merge and upsert when conflict', function () {
return fakeConflict(3).then(function () {
return slouch.doc.getMergeUpsert(utils.createdDB, {
_id: '1',
priority: 'high'
});
}).then(function () {
return slouch.doc.get(utils.createdDB, '1');
}).then(function (doc) {
doc._id.should.eql('1');
doc.thing.should.eql('dance');
doc.priority.should.eql('high');
});
});
it('get, merge and upsert should fail after max retries', function () {
slouch.maxRetries = 3;
return fakeConflict().then(function () {
return sporks.shouldThrow(function () {
return slouch.doc.getMergeUpsert(utils.createdDB, {
_id: '1',
thing: 'dance'
});
});
});
});
it('should get, modify and upsert when conflict', function () {
return fakeConflict(3).then(function () {
return slouch.doc.getModifyUpsert(utils.createdDB, '1', function (doc) {
return Promise.resolve({
_id: '1',
_rev: doc._rev,
thing: doc.thing,
priority: 'high'
});
});
}).then(function () {
return slouch.doc.get(utils.createdDB, '1');
}).then(function (doc) {
doc._id.should.eql('1');
doc.thing.should.eql('dance');
doc.priority.should.eql('high');
});
});
it('get, modify and upsert should fail after max retries', function () {
slouch.maxRetries = 3;
return fakeConflict().then(function () {
return sporks.shouldThrow(function () {
return slouch.doc.getModifyUpsert(utils.createdDB, '1', function (doc) {
return Promise.resolve({
_id: '1',
_rev: doc._rev,
thing: doc.thing,
priority: 'high'
});
});
});
});
});
it('should ignore conflicts when destroying', function () {
return fakeConflict().then(function () {
return slouch.doc.destroyIgnoreConflict(utils.createdDB, '1', conflictDoc._rev);
});
});
it('should get and destroy', function () {
return createDocs().then(function () {
return slouch.doc.getAndDestroy(utils.createdDB, '1');
}).then(function () {
return slouch.doc.exists();
}).then(function (exists) {
exists.should.eql(false);
});
});
it('should mark as destroyed', function () {
return createDocs().then(function () {
return slouch.doc.markAsDestroyed(utils.createdDB, '1');
}).then(function () {
return slouch.doc.exists();
}).then(function (exists) {
exists.should.eql(false);
});
});
it('should set destroyed', function () {
var doc = {};
slouch.doc.setDestroyed(doc);
doc._deleted.should.eql(true);
});
});

View file

@ -5,26 +5,25 @@ var Slouch = require('../../scripts'),
describe('exclude-design-docs-iterator', function () {
var slouch = new Slouch(utils.couchDBURL()),
db = slouch.db;
var slouch = new Slouch(utils.couchDBURL());
beforeEach(function () {
return db.create('testdb');
return utils.createDB();
});
afterEach(function () {
return db.destroy('testdb');
return utils.destroyDB();
});
var createDocs = function () {
return slouch.doc.post('testdb', {
return slouch.doc.create(utils.createdDB, {
thing: 'play'
}).then(function () {
return slouch.doc.post('testdb', {
return slouch.doc.create(utils.createdDB, {
thing: 'write'
});
}).then(function () {
return slouch.doc.post('testdb', {
return slouch.doc.create(utils.createdDB, {
_id: '_design/mydesign',
thing: 'design'
});
@ -34,7 +33,7 @@ describe('exclude-design-docs-iterator', function () {
it('should filter', function () {
var docs = [];
return createDocs().then(function () {
return new slouch.ExcludeDesignDocsIterator(slouch.doc.all('testdb', {
return new slouch.ExcludeDesignDocsIterator(slouch.doc.all(utils.createdDB, {
include_docs: true
})).each(function (doc) {
docs.push({

View file

@ -0,0 +1,21 @@
'use strict';
var Promise = require('sporks/scripts/promise');
var FakedStreamIterator = function (items) {
this._items = items;
this._i = 0;
};
FakedStreamIterator.prototype.each = function (onItem) {
var self = this;
if (self._i < self._items.length) {
return onItem(self._items[self._i++]).then(function () {
return self.each(onItem);
});
} else {
return Promise.resolve();
}
};
module.exports = FakedStreamIterator;

View file

@ -1,8 +1,12 @@
'use strict';
require('./attachment');
require('./config');
require('./db');
require('./doc');
require('./exclude-design-docs-iterator');
require('./request-class');
require('./request');
require('./security');
require('./system');
require('./user');

View file

@ -0,0 +1,94 @@
'use strict';
var RequestClass = require('../../scripts/request-class'),
sporks = require('sporks'),
Backoff = require('backoff-promise'),
Promise = require('sporks/scripts/promise');
describe('request-class', function () {
var consoleLog = console.log,
request = null;
beforeEach(function () {
request = new RequestClass();
// Shorten the backoff
request._newBackoff = function () {
return new Backoff(10);
};
});
afterEach(function () {
// Restore console log
console.log = consoleLog;
RequestClass.LOG_EVERYTHING = false;
});
it('should log', function () {
RequestClass.LOG_EVERYTHING = true;
var logged = null;
// Spy
console.log = function (str) {
logged = str;
};
request._log('foo');
logged.should.eql('foo');
});
it('should get 404 status code', function () {
request._getStatusCode({
reason: 'Could not open source database'
}).should.eql(404);
});
it('should handle malformed error', function () {
// Fake
request._req = function () {
return Promise.resolve(null);
};
return sporks.shouldThrow(function () {
return request._request();
});
});
it('should reconnect when all DBs active', function () {
var err = new Error('all_dbs_active');
request._shouldReconnect(err).should.eql(true);
});
it('should throw error reach max retries', function () {
var err = new Error('all_dbs_active');
// Fake
request._request = function () {
return sporks.promiseError(err);
};
return sporks.shouldThrow(function () {
return request.request();
}, err);
});
it('should ignore default_authentication_handler errors when requesting', function () {
var err = new Error('default_authentication_handler');
// Fake
request._request = function () {
return sporks.promiseError(err);
};
return request.request();
});
it('should set max connections', function () {
request.setMaxConnections(2);
request._throttler.getMaxConcurrentProcesses().should.eql(2);
});
});

View file

@ -20,7 +20,7 @@ describe('request', function () {
it('should handle ENOTFOUND errors', function () {
// Shorten the backoff as in a browser we just a "Failed to Fetch" error which triggers a retry
request._newBackoff = function () {
return new Backoff(10);
return new Backoff(1);
};
return sporks.shouldThrow(function () {

View file

@ -5,15 +5,14 @@ var Slouch = require('../../scripts'),
describe('security', function () {
var slouch = new Slouch(utils.couchDBURL()),
db = slouch.db;
var slouch = new Slouch(utils.couchDBURL());
beforeEach(function () {
return db.create('testdb');
return utils.createDB();
});
afterEach(function () {
return db.destroy('testdb');
return utils.destroyDB();
});
it('should set and get security', function () {
@ -27,11 +26,15 @@ describe('security', function () {
'roles': ['producer', 'consumer']
}
};
return slouch.security.set('testdb', security).then(function () {
return slouch.security.get('testdb');
return slouch.security.set(utils.createdDB, security).then(function () {
return slouch.security.get(utils.createdDB);
}).then(function (_security) {
_security.should.eql(security);
});
});
it('only admin can view', function () {
return slouch.security.onlyAdminCanView(utils.createdDB);
});
});

281
test/spec/system.js Normal file
View file

@ -0,0 +1,281 @@
'use strict';
var Slouch = require('../../scripts'),
utils = require('../utils'),
FakedStreamIterator = require('./faked-stream-iterator'),
Promise = require('sporks/scripts/promise'),
sporks = require('sporks'),
MemoryStream = require('memorystream');
// FakedJSONRequest = require('quelle/test/spec/faked-json-request');
// events = require('events');
describe('system', function () {
var slouch = null,
db = null,
system = null,
destroyed = null,
created = null,
defaultGet = null,
iteratorToAbort = null;
beforeEach(function () {
slouch = new Slouch(utils.couchDBURL());
db = slouch.db;
system = slouch.system;
destroyed = [];
created = [];
iteratorToAbort = null;
return utils.createDB();
});
afterEach(function () {
if (iteratorToAbort) {
iteratorToAbort.abort();
}
system.get = defaultGet;
return utils.destroyDB();
});
var fakeCouchDBVersion = function (version) {
defaultGet = system.get;
system.get = function () {
return Promise.resolve({
version: [version]
});
};
};
var fakeDBAll = function (dbs) {
slouch.db.all = function () {
return new FakedStreamIterator(dbs);
};
};
var fakeCreateAndDestroy = function () {
slouch.db.create = function (dbName) {
created.push(dbName);
return Promise.resolve();
};
slouch.db.destroy = function (dbName) {
destroyed.push(dbName);
return Promise.resolve();
};
};
var isPhantomJS = function () {
return global.navigator && global.navigator.userAgent.indexOf('PhantomJS') !== -1;
};
// TODO: why do continuous requests just hang in PhantomJS, but not in any other browser?
var fakeContinuousUpdatesIfPhantomJS = function (item) {
if (isPhantomJS()) {
system._request = function () {
var stream = new MemoryStream();
stream.write(JSON.stringify(item));
stream.abort = function () {};
return stream;
};
db._request = system._request;
}
};
it('should clone params when falsy', function () {
system._cloneParams().should.eql({});
});
it('should check if couchdb 1', function () {
// We run the tests on both CouchDB 1 and 2 and so we don't care about the version. In the
// future, we could pass a paramter to our test scripts that would allow us to test this better.
return system.isCouchDB1();
});
it('should detect couchdb 1', function () {
fakeCouchDBVersion('1');
return system.isCouchDB1().then(function (is1) {
is1.should.eql(true);
});
});
it('should cache if couchdb 1', function () {
fakeCouchDBVersion('1');
return system.isCouchDB1().then(function () {
return system.isCouchDB1();
}).then(function (is1) {
is1.should.eql(true);
});
});
it('should reset when couchdb 1', function () {
fakeCouchDBVersion('1');
fakeDBAll(['_replicator', 'testa', 'testb']);
fakeCreateAndDestroy();
return system.reset().then(function () {
created.should.eql(['_replicator']);
destroyed.should.eql(['_replicator', 'testa', 'testb']);
created = [];
destroyed = [];
// Now try the exceptDBNames param
return system.reset(['testb']);
}).then(function () {
created.should.eql(['_replicator']);
destroyed.should.eql(['_replicator', 'testa']);
});
});
it('should reset when not couchdb', function () {
fakeCouchDBVersion('2');
fakeDBAll(['_replicator', '_global_changes', '_users', 'testa', 'testb']);
fakeCreateAndDestroy();
return system.reset().then(function () {
created.should.eql(['_replicator', '_global_changes', '_users']);
destroyed.should.eql(['_replicator', '_global_changes', '_users', 'testa',
'testb'
]);
});
});
it('should listen for updates', function () {
var hasUpdate = false;
return slouch.doc.create(utils.createdDB, {
foo: 'bar'
}).then(function () {
return system.updates().each(function (update) {
if (update.db_name === utils.createdDB) {
hasUpdate = true;
}
});
}).then(function () {
hasUpdate.should.eql(true);
});
});
it('should listen for updates continuously', function () {
fakeContinuousUpdatesIfPhantomJS({
db_name: utils.createdDB,
type: 'updated'
});
var promise = new Promise(function (resolve, reject) {
iteratorToAbort = system.updates({
feed: 'continuous'
});
iteratorToAbort.each(function (update) {
if (update.db_name === utils.createdDB && update.type === 'updated') {
resolve();
}
}).catch(function (err) {
reject(err);
});
});
// Use timeout to create on the next click, after we start listening to the updates
return sporks.timeout().then(function () {
return slouch.doc.create(utils.createdDB, {
foo: 'bar'
});
}).then(function () {
return promise;
});
});
it('should listen for updates no history when couchdb 1', function () {
fakeCouchDBVersion('1');
fakeContinuousUpdatesIfPhantomJS({
db_name: utils.createdDB,
type: 'updated'
});
var promise = new Promise(function (resolve, reject) {
iteratorToAbort = system.updatesNoHistory({
feed: 'continuous'
});
iteratorToAbort.each(function (update) {
if (update.db_name === utils.createdDB && update.type === 'updated') {
resolve();
}
}).catch(function (err) {
reject(err);
});
});
// Use timeout to create on the next click, after we start listening to the updates
return sporks.timeout().then(function () {
return slouch.doc.create(utils.createdDB, {
foo: 'bar'
});
}).then(function () {
return promise;
});
});
it('should listen for updates no history when couchdb 2', function () {
fakeCouchDBVersion('2');
fakeContinuousUpdatesIfPhantomJS({
id: 'updated:' + utils.createdDB
});
// Mock get regardless of version of CouchDB
var defaultGet = db.get;
db.get = function (name) {
var args = sporks.toArgsArray(arguments);
if (name === '_global_changes') {
args[0] = utils.createdDB;
}
return defaultGet.apply(this, args);
};
// Mock changes regardless of version of CouchDB
var defaultChanges = db.changes;
db.changes = function (name) {
var args = sporks.toArgsArray(arguments);
if (name === '_global_changes') {
args[0] = utils.createdDB;
}
return defaultChanges.apply(this, args);
};
var promise = new Promise(function (resolve, reject) {
iteratorToAbort = system.updatesNoHistory({
feed: 'continuous'
});
iteratorToAbort.each(function (update) {
if (update.db_name === utils.createdDB && update.type === 'updated') {
resolve();
}
}).catch(function (err) {
reject(err);
});
});
// Use timeout to create on the next click, after we start listening to the updates
return sporks.timeout().then(function () {
return slouch.doc.create(utils.createdDB, {
_id: 'updated:' + utils.createdDB
});
}).then(function () {
return promise;
});
});
it('should clone params', function () {
var params = {
foo: 'bar'
};
system._cloneParams(params).should.eql(params);
});
it('should return undefined if item missing id', function () {
(system._itemToUpdate({}) === undefined).should.eql(true);
});
});

View file

@ -1,29 +1,53 @@
'use strict';
var Slouch = require('../../scripts'),
utils = require('../utils');
utils = require('../utils'),
sporks = require('sporks'),
Promise = require('sporks/scripts/promise');
describe('user', function () {
var slouch = new Slouch(utils.couchDBURL()),
user = slouch.user;
var slouch = null,
user = null,
defaultUpdate = null,
username = null,
defaultRequest = null;
beforeEach(function () {
return user.create('testusername', 'testpassword', ['testrole1'], {
slouch = new Slouch(utils.couchDBURL());
user = slouch.user;
username = 'test_' + utils.nextId();
defaultRequest = user._request.request;
return user.create(username, 'testpassword', ['testrole1'], {
firstName: 'Jill',
email: 'test@example.com'
});
});
afterEach(function () {
return user.destroy('testusername');
user._request.request = defaultRequest;
return user.destroy(username);
});
var fakeConflict = function () {
var i = 0;
defaultUpdate = user._update;
user._update = function () {
if (i++ < 3) {
var err = new Error();
err.error = 'conflict';
return sporks.promiseError(err);
} else {
return defaultUpdate.apply(this, arguments);
}
};
};
// NOTE: create and destroy tested by beforeEach() and afterEach()
it('should not create when already exists', function () {
var err = null;
return user.create('testusername', 'testpassword').catch(function (_err) {
return user.create(username, 'testpassword').catch(function (_err) {
err = _err;
}).then(function () {
(err === null).should.eql(false);
@ -31,33 +55,64 @@ describe('user', function () {
});
it('should get', function () {
return user.get('testusername').then(function (_user) {
_user._id.should.eql('org.couchdb.user:testusername');
_user.name.should.eql('testusername');
return user.get(username).then(function (_user) {
_user._id.should.eql('org.couchdb.user:' + username);
_user.name.should.eql(username);
_user.roles.should.eql(['testrole1']);
_user.type.should.eql('user');
_user.metadata.should.eql({
firstName: 'Jill',
email: 'test@example.com'
});
user.toUsername(_user._id).should.eql(username);
});
});
it('should add role', function () {
return user.addRole('testusername', 'testrole2').then(function () {
return user.get('testusername');
return user.addRole(username, 'testrole2').then(function () {
return user.get(username);
}).then(function (_user) {
_user.roles.should.eql(['testrole1', 'testrole2']);
});
});
it('should upsert role', function () {
fakeConflict();
return user.upsertRole(username, 'testrole2').then(function () {
return user.get(username);
}).then(function (_user) {
_user.roles.should.eql(['testrole1', 'testrole2']);
});
});
it('should remove role', function () {
return user.addRole(username, 'testrole2').then(function () {
return user.removeRole(username, 'testrole1');
}).then(function () {
return user.get(username);
}).then(function (_user) {
_user.roles.should.eql(['testrole2']);
});
});
it('should downsert role', function () {
return user.addRole(username, 'testrole2').then(function () {
fakeConflict();
return user.downsertRole(username, 'testrole1');
}).then(function () {
return user.get(username);
}).then(function (_user) {
_user.roles.should.eql(['testrole2']);
});
});
it('should set password', function () {
var origUser = null;
return user.get('testusername').then(function (_user) {
return user.get(username).then(function (_user) {
origUser = _user;
return user.setPassword('testusername', 'testpassword2');
return user.setPassword(username, 'testpassword2');
}).then(function () {
return user.get('testusername');
return user.get(username);
}).then(function (_user) {
// Make sure password changed
(_user.derived_key === origUser.derived_key).should.eql(false);
@ -65,13 +120,65 @@ describe('user', function () {
});
it('should set metadata', function () {
return user.setMetadata('testusername', {
return user.setMetadata(username, {
firstName: 'Jack'
}).then(function () {
return user.get('testusername');
return user.get(username);
}).then(function (_user) {
_user.metadata.firstName.should.eql('Jack');
});
});
it('should authenticate and get session', function () {
// TODO: get authenticate() and authenticated() working properly in the browser. For now, we
// have to fake the responses as it appears that the session cookie is not being propogated from
// the session post to the session get.
if (global.window) { // in browser?
user._request.request = function () {
if (arguments['0'].uri.indexOf('_session') !== -1) {
return Promise.resolve({
headers: {
'set-cookie': [
'some-cookie'
]
},
body: JSON.stringify({
userCtx: {
name: username,
roles: ['testrole1']
},
cookie: 'some-cookie'
})
});
} else {
return defaultRequest.apply(this, arguments);
}
};
}
return user.authenticateAndGetSession(username, 'testpassword').then(function (session) {
// Sanity check
session.userCtx.name.should.eql(username);
session.userCtx.roles.should.eql(['testrole1']);
(session.cookie === undefined).should.eql(false);
});
});
it('should not authenticate and get session', function () {
var err = new Error();
err.name = 'NotAuthenticatedError';
return sporks.shouldThrow(function () {
return user.authenticateAndGetSession(username, 'bad-password');
}, err);
});
it('should not be authenticated when cookie missing', function () {
var err = new Error();
err.name = 'NotAuthenticatedError';
return sporks.shouldThrow(function () {
return user.authenticated('bad-cookie');
}, err);
});
});

View file

@ -1,12 +1,32 @@
'use strict';
var config = require('./config.json');
var config = require('./config.json'),
Slouch = require('../scripts');
var Utils = function () {};
var Utils = function () {
this._dbId = 0;
this.createdDB = null;
this._slouch = new Slouch(this.couchDBURL());
};
Utils.prototype.couchDBURL = function () {
return config.couchdb.scheme + '://' + config.couchdb.username + ':' +
config.couchdb.password + '@' + config.couchdb.host + ':' + config.couchdb.port;
};
Utils.prototype.nextId = function () {
return this._dbId++;
};
// Use unique DB names for each tests as there can be race conditions where a DB is destroyed, but
// has not yet been fully released.
Utils.prototype.createDB = function () {
this.createdDB = 'test_' + this.nextId();
return this._slouch.db.create(this.createdDB);
};
Utils.prototype.destroyDB = function () {
return this._slouch.db.destroy(this.createdDB);
};
module.exports = new Utils();