From da7ca1123e50549df5c828ec39a6dc78db585271 Mon Sep 17 00:00:00 2001 From: Geoff Cox Date: Tue, 18 Jul 2017 07:45:32 -0700 Subject: [PATCH] 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 --- README.md | 22 +- TESTING.md | 13 +- package.json | 10 +- reset-db.js | 9 + scripts/attachment.js | 54 ++++ scripts/auth.js | 24 -- scripts/config.js | 29 +- scripts/db.js | 83 +---- scripts/doc.js | 138 +++----- scripts/index.js | 4 +- scripts/request-class.js | 3 +- scripts/security.js | 17 + scripts/system.js | 86 ++++- scripts/user.js | 38 ++- test/couch.png | Bin 0 -> 20041 bytes test/node-and-browser.js | 9 +- test/spec/attachment.js | 90 ++++++ test/spec/config.js | 117 +++++++ test/spec/db.js | 184 ++++++++++- test/spec/doc.js | 374 +++++++++++++++++++++- test/spec/exclude-design-docs-iterator.js | 15 +- test/spec/faked-stream-iterator.js | 21 ++ test/spec/index.js | 4 + test/spec/request-class.js | 94 ++++++ test/spec/request.js | 2 +- test/spec/security.js | 15 +- test/spec/system.js | 281 ++++++++++++++++ test/spec/user.js | 139 +++++++- test/utils.js | 24 +- 29 files changed, 1619 insertions(+), 280 deletions(-) create mode 100755 reset-db.js create mode 100644 scripts/attachment.js delete mode 100644 scripts/auth.js create mode 100644 test/couch.png create mode 100644 test/spec/attachment.js create mode 100644 test/spec/config.js create mode 100644 test/spec/faked-stream-iterator.js create mode 100644 test/spec/request-class.js create mode 100644 test/spec/system.js diff --git a/README.md b/README.md index 7805795..b5792fe 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/TESTING.md b/TESTING.md index eb0ad0e..0e6b4aa 100644 --- a/TESTING.md +++ b/TESTING.md @@ -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 diff --git a/package.json b/package.json index ee8aea7..aa1c593 100644 --- a/package.json +++ b/package.json @@ -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": { diff --git a/reset-db.js b/reset-db.js new file mode 100755 index 0000000..694afc8 --- /dev/null +++ b/reset-db.js @@ -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(); diff --git a/scripts/attachment.js b/scripts/attachment.js new file mode 100644 index 0000000..a1fb9fe --- /dev/null +++ b/scripts/attachment.js @@ -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; diff --git a/scripts/auth.js b/scripts/auth.js deleted file mode 100644 index e139f8a..0000000 --- a/scripts/auth.js +++ /dev/null @@ -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; diff --git a/scripts/config.js b/scripts/config.js index 5b0b0f2..558af13 100644 --- a/scripts/config.js +++ b/scripts/config.js @@ -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); }; diff --git a/scripts/db.js b/scripts/db.js index bf36ba9..4db21ab 100644 --- a/scripts/db.js +++ b/scripts/db.js @@ -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; diff --git a/scripts/doc.js b/scripts/doc.js index c4a94a8..cb6d96d 100644 --- a/scripts/doc.js +++ b/scripts/doc.js @@ -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; diff --git a/scripts/index.js b/scripts/index.js index d48142a..1c9d128 100644 --- a/scripts/index.js +++ b/scripts/index.js @@ -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); diff --git a/scripts/request-class.js b/scripts/request-class.js index 70a4c52..fc4420e 100644 --- a/scripts/request-class.js +++ b/scripts/request-class.js @@ -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; diff --git a/scripts/security.js b/scripts/security.js index d716de9..57eefc4 100644 --- a/scripts/security.js +++ b/scripts/security.js @@ -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; diff --git a/scripts/system.js b/scripts/system.js index b25ff28..0df9524 100644 --- a/scripts/system.js +++ b/scripts/system.js @@ -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; diff --git a/scripts/user.js b/scripts/user.js index 83ab76a..32d7fb0 100644 --- a/scripts/user.js +++ b/scripts/user.js @@ -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: { diff --git a/test/couch.png b/test/couch.png new file mode 100644 index 0000000000000000000000000000000000000000..e5e312a10d9a501fb6d4ee4a833a26b2a8e8c4ea GIT binary patch literal 20041 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 literal 0 HcmV?d00001 diff --git a/test/node-and-browser.js b/test/node-and-browser.js index 81702d0..c8e860e 100644 --- a/test/node-and-browser.js +++ b/test/node-and-browser.js @@ -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'); + +}); diff --git a/test/spec/attachment.js b/test/spec/attachment.js new file mode 100644 index 0000000..3dd285e --- /dev/null +++ b/test/spec/attachment.js @@ -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); + }); + }); + +}); diff --git a/test/spec/config.js b/test/spec/config.js new file mode 100644 index 0000000..8bc6c04 --- /dev/null +++ b/test/spec/config.js @@ -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); + }); + +}); diff --git a/test/spec/db.js b/test/spec/db.js index ec4bd57..0d9269c 100644 --- a/test/spec/db.js +++ b/test/spec/db.js @@ -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'); + }); + }); + }); diff --git a/test/spec/doc.js b/test/spec/doc.js index e3a12bd..c4e4459 100644 --- a/test/spec/doc.js +++ b/test/spec/doc.js @@ -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); + }); + }); diff --git a/test/spec/exclude-design-docs-iterator.js b/test/spec/exclude-design-docs-iterator.js index ec4c736..486bd6d 100644 --- a/test/spec/exclude-design-docs-iterator.js +++ b/test/spec/exclude-design-docs-iterator.js @@ -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({ diff --git a/test/spec/faked-stream-iterator.js b/test/spec/faked-stream-iterator.js new file mode 100644 index 0000000..904c0b9 --- /dev/null +++ b/test/spec/faked-stream-iterator.js @@ -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; diff --git a/test/spec/index.js b/test/spec/index.js index ce91d3f..e3c99b6 100644 --- a/test/spec/index.js +++ b/test/spec/index.js @@ -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'); diff --git a/test/spec/request-class.js b/test/spec/request-class.js new file mode 100644 index 0000000..8501864 --- /dev/null +++ b/test/spec/request-class.js @@ -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); + }); + +}); diff --git a/test/spec/request.js b/test/spec/request.js index a811ba6..ea76c21 100644 --- a/test/spec/request.js +++ b/test/spec/request.js @@ -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 () { diff --git a/test/spec/security.js b/test/spec/security.js index f33ecd0..7d34152 100644 --- a/test/spec/security.js +++ b/test/spec/security.js @@ -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); + }); + }); diff --git a/test/spec/system.js b/test/spec/system.js new file mode 100644 index 0000000..595681f --- /dev/null +++ b/test/spec/system.js @@ -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); + }); + +}); diff --git a/test/spec/user.js b/test/spec/user.js index 505749b..00dfa3a 100644 --- a/test/spec/user.js +++ b/test/spec/user.js @@ -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); + }); + }); diff --git a/test/utils.js b/test/utils.js index 996dbcc..7b6109a 100644 --- a/test/utils.js +++ b/test/utils.js @@ -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();