├── .gitignore ├── .travis.yml ├── README.md ├── index.js ├── package.json └── test ├── features.js ├── http.js ├── signatures.js └── utils.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | coverage 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | language: node_js 3 | 4 | cache: 5 | directories: 6 | - node_modules 7 | 8 | node_js: 9 | - "0.10" 10 | 11 | services: 12 | - couchdb 13 | 14 | before_install: 15 | - npm i -g npm@^2.0.0 16 | 17 | before_script: 18 | - npm prune 19 | 20 | script: npm run $COMMAND 21 | 22 | env: 23 | matrix: 24 | - COMMAND='helper -- lint' 25 | - COMMAND='helper -- js-test' 26 | - COMMAND='build' 27 | 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | pouchdb-validation 2 | ================== 3 | 4 | [![Build Status](https://travis-ci.org/pouchdb/pouchdb-validation.svg?branch=master)](https://travis-ci.org/pouchdb/pouchdb-validation) 5 | [![Dependency Status](https://david-dm.org/pouchdb/pouchdb-validation.svg)](https://david-dm.org/pouchdb/pouchdb-validation) 6 | [![devDependency Status](https://david-dm.org/pouchdb/pouchdb-validation/dev-status.svg)](https://david-dm.org/pouchdb/pouchdb-validation#info=devDependencies) 7 | 8 | > A PouchDB plug-in that allows you to re-use your CouchDB validate_doc_update functions on the client side. 9 | 10 | A browser version is available. 11 | 12 | - [NodeJS package](https://www.npmjs.org/package/pouchdb-validation) 13 | - Browser object name: ``window.validation`` 14 | 15 | First, make sure you understand how validation functions work in CouchDB. A good 16 | start is [the CouchDB guide entry on validation functions](http://guide.couchdb.org/draft/validation.html). 17 | 18 | Usage 19 | ----- 20 | 21 | First, you need to register the plug-in with PouchDB. That can be done using the 22 | ``PouchDB.plugin()`` function. In NodeJS, you can just pass in the result of the 23 | ``require()`` function. In the browser, you pass in the browser object name 24 | given above. 25 | 26 | An example (using the list plug-in): 27 | 28 | ```javascript 29 | //NodeJS (and Browserify) 30 | PouchDB.plugin(require("pouchdb-validation")); 31 | 32 | //Browser - after the JavaScript file containing the plug-in has been 33 | //included via a script tag (or something similar). 34 | PouchDB.plugin(Validation); 35 | ``` 36 | 37 | All functions have two ways of returning the output to the user. One is 38 | a callback parameter, which should have the signature ``(err, resp)``. 39 | The other is the Promise all functions return. PouchDB itself uses the 40 | same system. 41 | 42 | ### db.validatingPut(doc[, options[, callback]]) 43 | Exactly the same as the ``db.put`` function, but checks with all validation 44 | functions ('validate_doc_update') in all design documents of the current 45 | database if it is ok to save ``doc``. In short, this method acts more like its 46 | CouchDB equivalent than the original PouchDB version does. The only thing you 47 | get to see of it is a few extra errors, i.e. of the 'unauthorized' or the 48 | 'forbidden' type. It also has a few extra ``options`` (defaults are shown): 49 | 50 | - ``secObj``: e.g.: 51 | 52 | ```javascript 53 | { 54 | admins: { 55 | names: [], 56 | roles: [] 57 | }, 58 | members: { 59 | names: [], 60 | roles: [] 61 | } 62 | } 63 | ``` 64 | 65 | - ``userCtx``: e.g.: 66 | 67 | ```javascript: 68 | { 69 | db: "test_db", 70 | name: "username", 71 | roles: [ 72 | "_admin" 73 | ] 74 | } 75 | ``` 76 | 77 | - ``checkHttp``: Set this to ``true`` if you want to validate HTTP database 78 | documents offline too. Unnecessary for CouchDB, but handy for e.g. 79 | pouchdb-express-router, which doesn't validate itself. 80 | 81 | ### db.validatingPost(doc[, options[, callback]]) 82 | 83 | See the ``db.validatingPut()`` function. 84 | 85 | ### db.validatingRemove(doc[, options[, callback]]) 86 | 87 | See the ``db.validatingPut()`` function. 88 | 89 | ### db.validatingBulkDocs(bulkDocs[, options[, callback]]) 90 | 91 | See the ``db.validatingPut()`` function. Returns an array, like 92 | ``db.bulkDocs()``. The ``all_or_nothing`` attribute on ``bulkDocs`` is 93 | unsupported. Also, the result array might not be in the same order as 94 | the passed in documents. 95 | 96 | ### db.validatingPutAttachment(docId, attachmentId, rev, attachment, type[, options[, callback]]) 97 | 98 | See the ``db.validatingPut()`` function. Output is the same as 99 | ``db.putAttachment()`` (except for a few extra errors being possible.) 100 | 101 | ### db.validatingRemoveAttachment(docId, attachmentId, rev[, options[, callback]]) 102 | 103 | See the ``db.validatingPut()`` function. Output is the same as 104 | ``db.removeAttachment()`` (except for a few extra errors being possible.) 105 | 106 | ### db.installValidationMethods() 107 | 108 | Installs the validation methods on this database. In other words, the ``db.*`` 109 | methods are replaced by their ``db.validating*`` counterparts. This method is 110 | always synchronous. 111 | 112 | **Throws**: an error if the methods are already installed. 113 | **Returns**: nothing 114 | 115 | ### db.uninstallValidationMethods() 116 | 117 | Undoes what ``db.installValidationMethods`` did. This method is always 118 | synchronous. 119 | 120 | **Throws**: an error if the methods aren't currently installed. 121 | **Returns**: nothing 122 | 123 | License 124 | ------- 125 | 126 | Apache-2.0 127 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2013-2015, Marten de Vries 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | "use strict"; 18 | 19 | var coucheval = require("couchdb-eval"); 20 | var couchdb_objects = require("couchdb-objects"); 21 | var wrappers = require("pouchdb-wrappers"); 22 | var createBulkDocsWrapper = require("pouchdb-bulkdocs-wrapper"); 23 | var PouchPluginError = require("pouchdb-plugin-error"); 24 | 25 | var uuid = require("random-uuid-v4"); 26 | var Promise = require("pouchdb-promise"); 27 | 28 | function oldDoc(db, id) { 29 | return db.get(id, {revs: true}).catch(function () { 30 | return null; 31 | }); 32 | } 33 | 34 | function validate(validationFuncs, newDoc, oldDoc, options) { 35 | newDoc._revisions = (oldDoc || {})._revisions; 36 | 37 | try { 38 | validationFuncs.forEach(function (validationFuncInfo) { 39 | var func = validationFuncInfo.func; 40 | var designDoc = validationFuncInfo.designDoc; 41 | func.call(designDoc, newDoc, oldDoc, options.userCtx, options.secObj); 42 | }); 43 | } catch (e) { 44 | if (typeof e.unauthorized !== "undefined") { 45 | throw new PouchPluginError({ 46 | name: "unauthorized", 47 | message: e.unauthorized, 48 | status: 401 49 | }); 50 | } else if (typeof e.forbidden !== "undefined") { 51 | throw new PouchPluginError({ 52 | name: "forbidden", 53 | message: e.forbidden, 54 | status: 403 55 | }); 56 | } else { 57 | throw coucheval.wrapExecutionError(e); 58 | } 59 | } 60 | //passed all validation functions (no errors thrown) -> success 61 | } 62 | 63 | function doValidation(db, newDoc, options) { 64 | var isHttp = ["http", "https"].indexOf(db.type()) !== -1; 65 | if (isHttp && !options.checkHttp) { 66 | //CouchDB does the checking for itself. Validate succesful. 67 | return Promise.resolve(); 68 | } 69 | if (String(newDoc._id).indexOf("_design/") === 0 || String(newDoc._id).indexOf("_local") === 0) { 70 | //a design document -> always validates succesful. 71 | return Promise.resolve(); 72 | } 73 | return getValidationFunctions(db).then(function (validationFuncs) { 74 | if (!validationFuncs.length) { 75 | //no validation functions, so valid! 76 | return; 77 | } 78 | var completeOptionsPromise = completeValidationOptions(db, options); 79 | var oldDocPromise = oldDoc(db, newDoc._id); 80 | 81 | return Promise.all([completeOptionsPromise, oldDocPromise]) 82 | .then(Function.prototype.apply.bind(function (completeOptions, oldDoc) { 83 | return validate(validationFuncs, newDoc, oldDoc, completeOptions); 84 | }, null)); 85 | }); 86 | } 87 | 88 | function completeValidationOptions(db, options) { 89 | if (!options.secObj) { 90 | options.secObj = {}; 91 | } 92 | 93 | var userCtxPromise; 94 | if (options.userCtx) { 95 | userCtxPromise = Promise.resolve(options.userCtx); 96 | } else { 97 | var buildUserContext = couchdb_objects.buildUserContextObject; 98 | userCtxPromise = db.info().then(buildUserContext); 99 | } 100 | return userCtxPromise.then(function (userCtx) { 101 | options.userCtx = userCtx; 102 | return options; 103 | }); 104 | } 105 | 106 | function getValidationFunctions(db) { 107 | return db.allDocs({ 108 | startkey: "_design/", 109 | endkey: "_design0", 110 | include_docs: true 111 | }).then(parseValidationFunctions); 112 | } 113 | 114 | function parseValidationFunctions(resp) { 115 | var validationFuncs = resp.rows.map(function (row) { 116 | return { 117 | designDoc: row.doc, 118 | code: row.doc.validate_doc_update 119 | }; 120 | }); 121 | validationFuncs = validationFuncs.filter(function (info) { 122 | return typeof info.code !== "undefined"; 123 | }); 124 | validationFuncs.forEach(function (info) { 125 | //convert str -> function 126 | info.func = coucheval.evaluate(info.designDoc, {}, info.code); 127 | }); 128 | return validationFuncs; 129 | } 130 | 131 | var wrapperApi = {}; 132 | 133 | wrapperApi.put = function (orig, args) { 134 | return doValidation(args.db, args.doc, args.options).then(orig); 135 | }; 136 | 137 | wrapperApi.post = function (orig, args) { 138 | args.doc._id = args.doc._id || uuid(); 139 | return doValidation(args.db, args.doc, args.options).then(orig); 140 | }; 141 | 142 | wrapperApi.remove = function (orig, args) { 143 | args.doc._deleted = true; 144 | return doValidation(args.db, args.doc, args.options).then(orig); 145 | }; 146 | 147 | wrapperApi.bulkDocs = createBulkDocsWrapper(function (doc, args) { 148 | doc._id = doc._id || uuid(); 149 | return doValidation(args.db, doc, args.options); 150 | }); 151 | 152 | wrapperApi.putAttachment = function (orig, args) { 153 | return args.db.get(args.docId, {rev: args.rev, revs: true}) 154 | .catch(function () { 155 | return {_id: args.docId}; 156 | }) 157 | .then(function (doc) { 158 | //validate the doc + attachment 159 | doc._attachments = doc._attachments || {}; 160 | doc._attachments[args.attachmentId] = { 161 | content_type: args.type, 162 | data: args.doc 163 | }; 164 | 165 | return doValidation(args.db, doc, args.options); 166 | }) 167 | .then(orig); 168 | }; 169 | 170 | wrapperApi.removeAttachment = function (orig, args) { 171 | return args.db.get(args.docId, {rev: args.rev, revs: true}) 172 | .then(function (doc) { 173 | //validate the doc without attachment 174 | delete doc._attachments[args.attachmentId]; 175 | 176 | return doValidation(args.db, doc, args.options); 177 | }) 178 | .then(orig); 179 | }; 180 | 181 | Object.keys(wrapperApi).forEach(function (name) { 182 | var exportName = "validating" + name[0].toUpperCase() + name.substr(1); 183 | var orig = function () { 184 | return this[name].apply(this, arguments); 185 | }; 186 | exports[exportName] = wrappers.createWrapperMethod(name, orig, wrapperApi[name]); 187 | }); 188 | 189 | exports.installValidationMethods = function () { 190 | var db = this; 191 | 192 | try { 193 | wrappers.installWrapperMethods(db, wrapperApi); 194 | } catch (err) { 195 | throw new PouchPluginError({ 196 | status: 500, 197 | name: "already_installed", 198 | message: "Validation methods are already installed on this database." 199 | }); 200 | } 201 | }; 202 | 203 | exports.uninstallValidationMethods = function () { 204 | var db = this; 205 | 206 | try { 207 | wrappers.uninstallWrapperMethods(db, wrapperApi); 208 | } catch (err) { 209 | throw new PouchPluginError({ 210 | status: 500, 211 | name: "already_not_installed", 212 | message: "Validation methods are already not installed on this database." 213 | }); 214 | } 215 | }; 216 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "pouchdb-validation", 3 | "version": "1.2.1", 4 | "main": "index.js", 5 | "description": "A PouchDB plug-in that allows you to re-use your CouchDB validate_doc_update functions on the client side.", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/pouchdb/pouchdb-validation.git" 9 | }, 10 | "homepage": "http://python-pouchdb.marten-de-vries.nl/plugins.html", 11 | "keywords": [ 12 | "pouch", 13 | "pouchdb", 14 | "couch", 15 | "couchdb", 16 | "validation", 17 | "validate", 18 | "validate_doc_update" 19 | ], 20 | "license": "Apache-2.0", 21 | "author": "Marten de Vries", 22 | "dependencies": { 23 | "couchdb-objects": "^1.0.0", 24 | "couchdb-eval": "^1.0.0", 25 | "pouchdb-promise": "^0.0.0", 26 | "random-uuid-v4": "^0.0.6", 27 | "pouchdb-plugin-error": "^1.0.0", 28 | "pouchdb-wrappers": "^1.0.0", 29 | "pouchdb-bulkdocs-wrapper": "^1.0.0" 30 | }, 31 | "devDependencies": { 32 | "pouchdb-plugin-helper": "^3.0.0" 33 | }, 34 | "scripts": { 35 | "helper": "./node_modules/.bin/pouchdb-plugin-helper", 36 | "test": "npm run helper -- test", 37 | "build": "npm run helper -- build Validation" 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /test/features.js: -------------------------------------------------------------------------------- 1 | import {setup, setupWithDoc, setupWithDocAndAttachment, teardown, should, shouldThrowError, onlyTestValidationDoc} from './utils'; 2 | 3 | let db; 4 | 5 | describe('basic validation tests', () => { 6 | beforeEach(async () => { 7 | db = setup(); 8 | await db.put(onlyTestValidationDoc); 9 | }); 10 | 11 | afterEach(teardown); 12 | 13 | it('should allow put', async () => { 14 | const doc = await db.validatingPut({_id: 'test'}); 15 | doc.ok.should.be.ok; 16 | }); 17 | it('should allow post', async () => { 18 | const doc = await db.validatingPost({_id: 'test'}); 19 | doc.ok.should.be.ok; 20 | }); 21 | it('should allow remove', async() => { 22 | const info = await db.put({_id: 'test'}); 23 | const rmInfo = await db.validatingRemove({ 24 | _id: 'test', 25 | _rev: info.rev 26 | }); 27 | rmInfo.ok.should.be.ok; 28 | }); 29 | it('should allow bulkDocs', async () => { 30 | const resp = await db.validatingBulkDocs([ 31 | { 32 | _id: 'test' 33 | } 34 | ]); 35 | resp[0].should.be.ok; 36 | }); 37 | it('should allow putAttachment', (cb) => { 38 | function getCb(resp) { 39 | resp.toString('ascii').should.equal('Hello world!'); 40 | cb(); 41 | } 42 | function putCb(err, resp) { 43 | resp.ok.should.be.ok; 44 | db.getAttachment('test', 'test').then(getCb) 45 | } 46 | const blob = new Buffer('Hello world!', 'ascii'); 47 | db.validatingPutAttachment('test', 'test', blob, "text/plain", putCb); 48 | }); 49 | it('should fail', async () => { 50 | //setup - put an attachment 51 | const blob = new Buffer('Hello world!', 'ascii'); 52 | const resp = await db.putAttachment('mytest', 'test', blob, 'text/plain'); 53 | const error = await shouldThrowError(async () => { 54 | await db.validatingRemoveAttachment('mytest', 'test', resp.rev); 55 | }); 56 | error.status.should.equal(403); 57 | error.name.should.equal('forbidden'); 58 | }); 59 | }); 60 | 61 | describe('unauthorized validation tests', () => { 62 | let rev; 63 | beforeEach(async () => { 64 | const data = await setupWithDoc(); 65 | db = data.db; 66 | rev = data.rev; 67 | await db.put({ 68 | _id: '_design/test', 69 | validate_doc_update: `function (newDoc, oldDoc, userCtx, secObj) { 70 | if (newDoc._id !== "test") { 71 | throw({unauthorized: "only a document named 'test' is allowed."}); 72 | } 73 | }` 74 | }); 75 | }); 76 | afterEach(teardown); 77 | 78 | function checkError(err) { 79 | err.name.should.equal('unauthorized'); 80 | err.message.should.equal("only a document named 'test' is allowed.") 81 | } 82 | 83 | it('should fail an invalid put', async () => { 84 | checkError(await shouldThrowError(async () => { 85 | await db.validatingPut({_id: 'test_invalid'}); 86 | })); 87 | }); 88 | it('should fail an invalid post', async () => { 89 | checkError(await shouldThrowError(async () => { 90 | await db.validatingPost({}); 91 | })); 92 | }); 93 | it('should fail an invalid remove', async () => { 94 | checkError(await shouldThrowError(async () => { 95 | await db.validatingRemove({ 96 | _id: 'mytest', 97 | _rev: rev 98 | }) 99 | })); 100 | }) 101 | it('should fail an invalid bulkDocs', async () => { 102 | // Also tests validatingBulkDocs with docs: [] property (which is 103 | // deprecated, but still supported). 104 | const resp = await db.validatingBulkDocs({ 105 | docs: [ 106 | { 107 | _id: 'test_invalid' 108 | } 109 | ] 110 | }); 111 | checkError(resp[0]); 112 | }); 113 | }); 114 | 115 | describe('forbidden validation tests', () => { 116 | let rev; 117 | beforeEach(async () => { 118 | const data = await setupWithDoc(); 119 | db = data.db; 120 | rev = data.rev; 121 | 122 | await db.put(onlyTestValidationDoc); 123 | }); 124 | afterEach(teardown); 125 | 126 | function checkError(err) { 127 | err.name.should.equal('forbidden'); 128 | err.message.should.equal("only a document named 'test' is allowed."); 129 | } 130 | 131 | it('should fail an invalid put', async () => { 132 | checkError(await shouldThrowError(async () => { 133 | await db.validatingPut({_id: 'test_invalid'}); 134 | })); 135 | }); 136 | it('should fail an invalid post', async () => { 137 | checkError(await shouldThrowError(async () => { 138 | await db.validatingPost({}); 139 | })); 140 | }); 141 | it('should fail an invalid remove', async () => { 142 | checkError(await shouldThrowError(async () => { 143 | await db.validatingRemove({ 144 | _id: 'mytest', 145 | _rev: rev 146 | }) 147 | })); 148 | }); 149 | it('should fail an invalid bulk docs', async () => { 150 | const resp = await db.validatingBulkDocs([ 151 | { 152 | _id: 'test_invalid' 153 | }, 154 | {} 155 | ]); 156 | checkError(resp[0]); 157 | checkError(resp[1]); 158 | }); 159 | it('should never fail a design doc', async () => { 160 | // A design doc is always valid, so no matter the validate_doc_update 161 | // function, the stuff below should succeed. 162 | (await db.validatingPut({ 163 | _id: '_design/mytest' 164 | })).ok.should.be.ok; 165 | }); 166 | it('should never fail a local doc', async () => { 167 | // A local doc is always valid, so no matter the validate_doc_update 168 | // function, the stuff below should succeed. 169 | await db.validatingPut({ 170 | _id: '_local/mytest' 171 | }); 172 | }) 173 | }); 174 | 175 | describe('compilation error validation tests', () => { 176 | beforeEach(() => { 177 | db = setup(); 178 | }); 179 | afterEach(teardown); 180 | 181 | function checkError(err) { 182 | err.name.should.equal('compilation_error') 183 | err.message.should.contain('Expression does not eval to a function.'); 184 | } 185 | 186 | it('should fail syntax error', async () => { 187 | await db.put({ 188 | "_id": "_design/test", 189 | "validate_doc_update": `function (newDoc, oldDoc, userCtx, secObj) { 190 | return; 191 | }324j3lkl;` 192 | }); 193 | checkError(await shouldThrowError(async () => { 194 | await db.validatingPut({_id: 'test'}); 195 | })); 196 | }); 197 | 198 | it('should fail a non-function', async () => { 199 | await db.put({ 200 | _id: '_design/test', 201 | validate_doc_update: "'a string instead of a function'" 202 | }); 203 | 204 | checkError(await shouldThrowError(async () => { 205 | await db.validatingPut({_id: 'test'}); 206 | })); 207 | }); 208 | }); 209 | 210 | describe('exception validation tests', () => { 211 | beforeEach(async () => { 212 | db = setup(); 213 | 214 | await db.put({ 215 | _id: '_design/test', 216 | validate_doc_update: `function (newDoc, oldDoc, userCtx, secObj) { 217 | //reference error 218 | test; 219 | }` 220 | }) 221 | }); 222 | afterEach(teardown); 223 | 224 | it('should fail for put()', async () => { 225 | const err = await shouldThrowError(async () => { 226 | await db.validatingPut({_id: 'test'}); 227 | }); 228 | err.name.should.equal('ReferenceError'); 229 | //'test' is the name of the missing variable. 230 | err.message.should.contain('test') 231 | }); 232 | }); 233 | 234 | describe('attachment validation tests', () => { 235 | let rev; 236 | const forbiddenDesignDoc = { 237 | _id: '_design/test', 238 | validate_doc_update: `function (newDoc, oldDoc, userCtx, secObj) { 239 | throw({forbidden: JSON.stringify(newDoc)}); 240 | }` 241 | } 242 | beforeEach(async () => { 243 | const info = await setupWithDocAndAttachment(); 244 | db = info.db; 245 | rev = info.attRev; 246 | }); 247 | afterEach(teardown); 248 | 249 | it('should succesfully remove an attachment', async () => { 250 | await db.validatingRemoveAttachment('attachment_test', 'text', rev); 251 | }); 252 | it("shouldn't remove the attachment when forbidden", async () => { 253 | await db.put(forbiddenDesignDoc); 254 | const err = await shouldThrowError(async () => { 255 | await db.validatingRemoveAttachment('attachment_test', 'text', rev); 256 | }); 257 | err.name.should.equal('forbidden'); 258 | // checks if the newDoc argument is filled in correctly 259 | err.message.should.contain('"_attachments":{}'); 260 | }); 261 | it('should succesfully put an attachment', async () => { 262 | await db.validatingPutAttachment('attachment_test2', 'text', new Buffer('tést', 'UTF-8'), 'text/plain'); 263 | }); 264 | it("shouldn't put an attachment when forbidden", async () => { 265 | await db.put(forbiddenDesignDoc); 266 | const err = await shouldThrowError(async () => { 267 | await db.validatingPutAttachment('attachment_test2', 'text', new Buffer('tést', 'UTF-8'), 'text/plain'); 268 | }); 269 | err.name.should.equal('forbidden'); 270 | // checks if the newDoc argument is filled in correctly 271 | err.message.should.contain('text/plain'); 272 | }); 273 | }); 274 | 275 | describe('validation args tests', () => { 276 | let rev; 277 | beforeEach(async () => { 278 | const info = await setupWithDoc(); 279 | db = info.db; 280 | rev = info.rev; 281 | await db.put({ 282 | _id: '_design/test', 283 | validate_doc_update: `function (newDoc, oldDoc, userCtx, secObj) { 284 | throw({forbidden: JSON.stringify({ 285 | newDoc: newDoc, 286 | oldDoc: oldDoc, 287 | userCtx: userCtx, 288 | secObj: secObj 289 | })}); 290 | }` 291 | }); 292 | }); 293 | afterEach(teardown); 294 | 295 | it.skip('should have the right args with a new doc', async () => { 296 | const doc = {_id: 'test'}; 297 | const err = await shouldThrowError(async () => { 298 | await db.validatingPut(doc); 299 | }); 300 | const i = JSON.parse(err.message); 301 | i.newDoc.should.eql(doc); 302 | should.not.exist(i.oldDoc); 303 | 304 | i.userCtx.should.eql({ 305 | db: 'test', 306 | name: null, 307 | roles: ['_admin'] 308 | }); 309 | i.secObj.should.eql({}); 310 | }); 311 | it('should have the right args with an existing doc', async () => { 312 | const doc = {_id: 'mytest', _rev: rev}; 313 | const err = await shouldThrowError(async () => { 314 | await db.validatingPut(doc); 315 | }); 316 | const i = JSON.parse(err.message); 317 | i.oldDoc.test.should.be.ok; 318 | i.oldDoc._revisions.should.have.property('ids'); 319 | i.newDoc._revisions.should.have.property('ids'); 320 | }); 321 | it('should support changing the userCtx', async () => { 322 | const theUserCtx = { 323 | db: 'test', 324 | name: 'pypouchtest', 325 | roles: ['the_boss'] 326 | } 327 | 328 | const err = await shouldThrowError(async () => { 329 | await db.validatingPost({}, {userCtx: theUserCtx}); 330 | }); 331 | const i = JSON.parse(err.message); 332 | i.userCtx.should.eql(theUserCtx); 333 | }); 334 | it('should support changing the security object', async () => { 335 | const theSecObj = { 336 | admins: { 337 | names: ['the_boss'], 338 | roles: [] 339 | }, 340 | members: { 341 | names: [], 342 | roles: [] 343 | } 344 | }; 345 | 346 | const err = await shouldThrowError(async () => { 347 | await db.validatingPost({}, {secObj: theSecObj}); 348 | }); 349 | const i = JSON.parse(err.message); 350 | 351 | i.secObj.should.eql(theSecObj); 352 | }); 353 | }); 354 | 355 | describe('install validation methods tests', () => { 356 | beforeEach(async () => { 357 | db = setup(); 358 | await db.put(onlyTestValidationDoc); 359 | }); 360 | afterEach(teardown); 361 | 362 | it('basics should work', async () => { 363 | db.installValidationMethods(); 364 | 365 | const err = await shouldThrowError(async () => { 366 | await db.put({_id: 'mytest'}); 367 | }); 368 | err.status.should.equal(403); 369 | 370 | db.uninstallValidationMethods(); 371 | 372 | const resp = await db.put({_id: 'mytest'}); 373 | resp.ok.should.be.ok; 374 | }); 375 | it('should fail when installing twice', async () => { 376 | db.installValidationMethods(); 377 | const err = await shouldThrowError(async () => { 378 | await db.installValidationMethods(); 379 | }); 380 | err.name.should.equal('already_installed'); 381 | }); 382 | it('should fail uninstalling when not installed', async () => { 383 | const err = await shouldThrowError(async () => { 384 | await db.uninstallValidationMethods(); 385 | }); 386 | err.name.should.equal('already_not_installed'); 387 | }); 388 | it('should support reinstalling methods', async () => { 389 | for (let i = 0; i < 2; i++) { 390 | db.installValidationMethods(); 391 | db.uninstallValidationMethods(); 392 | } 393 | }); 394 | }); 395 | -------------------------------------------------------------------------------- /test/http.js: -------------------------------------------------------------------------------- 1 | import {setupHTTP, teardown, shouldThrowError, onlyTestValidationDoc} from './utils'; 2 | 3 | let db; 4 | function before() { 5 | db = setupHTTP(); 6 | } 7 | 8 | describe('signature http tests', () => { 9 | beforeEach(before); 10 | afterEach(teardown); 11 | it('should work with post', async () => { 12 | // Tests one special validation case to complete JS coverage 13 | await db.validatingPost({}); 14 | }); 15 | }); 16 | 17 | describe('http tests', () => { 18 | beforeEach(before); 19 | afterEach(teardown); 20 | //FIXME: re-enable (related to bug report) 21 | it.skip('should work', async () => { 22 | await db.put(onlyTestValidationDoc); 23 | const error = await shouldThrowError(async () => { 24 | await db.validatingPost({}); 25 | }); 26 | error.status.should.equal(403); 27 | error.name.should.equal('forbidden'); 28 | error.message.should.equal("only a document named 'test' is allowed."); 29 | 30 | const resp = await db.validatingPut({_id: 'test'}); 31 | resp.ok.should.be.ok; 32 | }); 33 | }); 34 | -------------------------------------------------------------------------------- /test/signatures.js: -------------------------------------------------------------------------------- 1 | import {setup, teardown} from './utils'; 2 | 3 | describe('callback usage', () => { 4 | it('should allow passing in a callback', async () => { 5 | const db = setup(); 6 | await db.validatingPost({}, () => {}); 7 | await teardown(); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /test/utils.js: -------------------------------------------------------------------------------- 1 | import Validation from '../'; 2 | import stuff from 'pouchdb-plugin-helper/testutils'; 3 | 4 | stuff.PouchDB.plugin(Validation); 5 | stuff.onlyTestValidationDoc = { 6 | _id: '_design/test', 7 | validate_doc_update: `function (newDoc, oldDoc, userCtx, secObj) { 8 | if (newDoc._id !== "test") { 9 | throw({forbidden: "only a document named 'test' is allowed."}); 10 | } 11 | }` 12 | }; 13 | 14 | module.exports = stuff; 15 | --------------------------------------------------------------------------------