diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7bbe1a3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/node_modules +/coverage diff --git a/.jscsfilter b/.jscsfilter new file mode 100644 index 0000000..a5c4af4 --- /dev/null +++ b/.jscsfilter @@ -0,0 +1,12 @@ +'use strict'; + +var _ = require('lodash'); + +module.exports = function(e) { + var report = true; + var isTestFile = _.startsWith(e.filename, './test/'); + if (isTestFile && e.message === 'jsdoc definition required') { + report = false; + } + return report; +}; diff --git a/.jscsrc b/.jscsrc new file mode 100644 index 0000000..6795bbd --- /dev/null +++ b/.jscsrc @@ -0,0 +1,47 @@ +{ + "preset": "airbnb", + "excludeFiles": ["node_modules/**", "coverage/**"], + "errorFilter": "./.jscsfilter", + "disallowEmptyBlocks": null, + "disallowFunctionDeclarations": true, + "disallowKeywordsOnNewLine": null, + "disallowMultipleLineBreaks": null, + "disallowMultipleVarDecl": "exceptUndefined", + "disallowSpacesInAnonymousFunctionExpression": { + "beforeOpeningRoundBrace": true + }, + "disallowSpacesInFunctionExpression": { + "beforeOpeningRoundBrace": true + }, + "disallowTrailingComma": null, + "maximumLineLength": 80, + "maximumNumberOfLines": 1000, + "requireBlocksOnNewline": 2, + "requireKeywordsOnNewLine": ["else"], + "requirePaddingNewLinesAfterBlocks": null, + "requirePaddingNewLinesAfterUseStrict": true, + "requirePaddingNewLinesBeforeExport": true, + "requireSemicolons": true, + "requireSpaceAfterLineComment": true, + "requireSpacesInsideObjectBrackets": "all", + "requireTrailingComma": { + "ignoreSingleLine": true + }, + "safeContextKeyword": "self", + "plugins": [ + "jscs-jsdoc" + ], + "jsDoc": { + "checkAnnotations": { + "preset": "jsdoc3", + "extra": { + "scope": "some" + } + }, + "checkReturnTypes": true, + "checkTypes": "capitalizedNativeCase", + "enforceExistence": true, + "requireNewlineAfterDescription": true, + "requireParamTypes": true + } +} diff --git a/.jshintignore b/.jshintignore new file mode 100644 index 0000000..7bbe1a3 --- /dev/null +++ b/.jshintignore @@ -0,0 +1,2 @@ +/node_modules +/coverage diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..1fba9f9 --- /dev/null +++ b/.jshintrc @@ -0,0 +1,21 @@ +{ + "node": true, + "bitwise": true, + "camelcase": true, + "curly": true, + "eqeqeq": true, + "immed": true, + "indent": 2, + "latedef": true, + "newcap": true, + "noarg": true, + "quotmark": "single", + "regexp": true, + "undef": true, + "unused": true, + "strict": true, + "trailing": true, + "smarttabs": true, + "globals": { + } +} diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..d5beb98 --- /dev/null +++ b/.travis.yml @@ -0,0 +1,7 @@ +node_js: +- '0.10' +- '0.12' +sudo: false +language: node_js +script: npm run-script test-travis +after_script: npm install coveralls@2 && cat ./coverage/lcov.info | coveralls diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..8477c43 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2015 Whitney Young + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..0a9ec82 --- /dev/null +++ b/README.md @@ -0,0 +1,32 @@ +# Azul Chai + +[![NPM version][npm-image]][npm-url] [![Build status][travis-image]][travis-url] [![Code Climate][codeclimate-image]][codeclimate-url] [![Coverage Status][coverage-image]][coverage-url] [![Dependencies][david-image]][david-url] [![devDependencies][david-dev-image]][david-dev-url] + +Chai assertions & test helpers for [Azul.js][azul]. + +```js +chai.use(require('azul-chai')); + +item.should.be.a.model('article').with.json({ id: 5, title: 'Azul.js' }); +``` + + +## License + +This project is distributed under the MIT license. + + +[azul]: https://github.com/wbyoung/azul + +[travis-image]: http://img.shields.io/travis/wbyoung/azul-chai.svg?style=flat +[travis-url]: http://travis-ci.org/wbyoung/azul-chai +[npm-image]: http://img.shields.io/npm/v/azul-chai.svg?style=flat +[npm-url]: https://npmjs.org/package/azul-chai +[codeclimate-image]: http://img.shields.io/codeclimate/github/wbyoung/azul-chai.svg?style=flat +[codeclimate-url]: https://codeclimate.com/github/wbyoung/azul-chai +[coverage-image]: http://img.shields.io/coveralls/wbyoung/azul-chai.svg?style=flat +[coverage-url]: https://coveralls.io/r/wbyoung/azul-chai +[david-image]: http://img.shields.io/david/wbyoung/azul-chai.svg?style=flat +[david-url]: https://david-dm.org/wbyoung/azul-chai +[david-dev-image]: http://img.shields.io/david/dev/wbyoung/azul-chai.svg?style=flat +[david-dev-url]: https://david-dm.org/wbyoung/azul-chai#info=devDependencies diff --git a/index.js b/index.js new file mode 100644 index 0000000..288ce9a --- /dev/null +++ b/index.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = require('./lib'); diff --git a/lib/index.js b/lib/index.js new file mode 100644 index 0000000..39a26a3 --- /dev/null +++ b/lib/index.js @@ -0,0 +1,3 @@ +'use strict'; + +module.exports = require('./plugin'); diff --git a/lib/plugin.js b/lib/plugin.js new file mode 100644 index 0000000..22727e7 --- /dev/null +++ b/lib/plugin.js @@ -0,0 +1,93 @@ +'use strict'; + +var _ = require('lodash'); + +/** + * This is the main chai plugin. + * + * @public + * @function plugin + * @param {Object} chai + * @param {Object} utils + */ +module.exports = function(chai, utils) { + + var Assertion = chai.Assertion; + var addMethod = Assertion.addMethod.bind(Assertion); + + /** + * Overwrite a method, but throw if super is called when there wasn't really + * anything to override. + * + * We should be able to remove this when the following is addressed: + * https://github.com/chaijs/chai/issues/467 + * + * @private + * @function + */ + var overwriteMethod = function(name, fn) { + if (typeof Assertion.prototype[name] === 'function') { + Assertion.overwriteMethod(name, fn); + } + else { + /** local */ + var _super = function() { + throw new Error('Method \'' + name + + '\' cannot be used in this context'); + }; + Assertion.addMethod(name, fn(_super)); + } + }; + + /** + * Check that a model is a specific type. + * + * .should.be.a.model('person') + * .should.be.a.model(Person) + * + * @public + * @function json + * @param {Object} json + */ + addMethod('model', function(model) { + new Assertion(this._obj).to.have.deep.property('__identity__.db'); + if (_.isString(model)) { + model = this._obj.__identity__.db.model(model); + } + + this.assert(this._obj instanceof model.__class__, + 'expected #{this} to be a #{exp} model but was #{act}', + 'expected #{this} to not be a #{act}', + model.__name__, this._obj.__identity__.__name__); + + utils.flag(this, 'azul.model', true); + }); + + + /** + * Check that a model's JSON matches an expected value. + * + * .should.be.a.model('person') + * .with.json({ username: 'wbyoung' }) + * + * @public + * @function json + * @param {Object} json + */ + overwriteMethod('json', function(_super) { + /** local */ + return function assertJSON(json) { + if (utils.flag(this, 'azul.model')) { + new Assertion(this._obj).to.have.property('json'); + this.assert(utils.eql(json, this._obj.json), + 'expected #{this} to have JSON #{exp} but got #{act}', + 'expected #{this} to not have JSON of #{act}', + json, this._obj.json, true); + } + else { + _super.apply(this, arguments); + } + }; + }); + +}; diff --git a/package.json b/package.json new file mode 100644 index 0000000..2f0d293 --- /dev/null +++ b/package.json @@ -0,0 +1,51 @@ +{ + "name": "azul-chai", + "version": "0.0.0", + "description": "Simple database queries for Node.js", + "homepage": "https://github.com/wbyoung/azul-chai", + "main": "index.js", + "bugs": { + "url": "https://github.com/wbyoung/azul-chai/issues" + }, + "scripts": { + "test": "jshint . && jscs . && istanbul cover node_modules/.bin/_mocha --report html --", + "test-travis": "jshint . && jscs . && istanbul cover node_modules/.bin/_mocha --report lcovonly --" + }, + "files": [ + "index.js", + "lib", + "LICENSE" + ], + "repository": { + "type": "git", + "url": "https://github.com/wbyoung/azul-chai.git" + }, + "keywords": [ + "postgres", + "mysql", + "sqlite", + "pg", + "database", + "chai", + "sql" + ], + "author": "Whitney Young", + "license": "MIT", + "dependencies": { + "lodash": "^3.9.3" + }, + "peerDependencies": { + "azul": "^0.0.1-alpha.14", + "chai": ">= 2.3.0 < 4" + }, + "devDependencies": { + "azul": "^0.0.1-alpha.14", + "bluebird": "^2.9.27", + "chai": "^3.0.0", + "istanbul": "^0.3.14", + "jscs": "^1.13.1", + "jscs-jsdoc": "^1.0.1", + "jshint": "^2.8.0", + "mocha": "^2.2.5" + } +} diff --git a/test/.jshintrc b/test/.jshintrc new file mode 100644 index 0000000..6220dd0 --- /dev/null +++ b/test/.jshintrc @@ -0,0 +1,17 @@ +{ + "extends": "../.jshintrc", + "globals": { + "describe": true, + "it": true, + "before": true, + "beforeEach": true, + "after": true, + "afterEach": true, + "chai": true, + "expect": true, + "should": true, + "sinon": true, + "__db": true + }, + "expr": true +} diff --git a/test/_helpers.js b/test/_helpers.js new file mode 100644 index 0000000..623d15b --- /dev/null +++ b/test/_helpers.js @@ -0,0 +1,35 @@ +'use strict'; + +var _ = require('lodash'); +var chai = require('chai'); +var azul = require('azul'); +var Promise = require('bluebird'); + +var Adapter = azul.Adapter.extend({ + _connect: Promise.method(function() { return 1; }), + _disconnect: Promise.method(function() { }), + _execute: Promise.method(function() { }), +}); + +// reset chai.Assertion's prototype. this is required to support mocha +// re-running the test suite in watch mode. +var prototype = chai.Assertion.prototype; +var initial = chai.hold = chai.hold || _.clone(Object.keys(prototype)); +_.difference(Object.keys(prototype), initial).forEach(function(key) { + delete prototype[key]; +}); + +chai.use(require('..')); + +global.chai = chai; +global.expect = chai.expect; +global.should = chai.should(); + +global.__db = function(fn) { + return function() { + beforeEach(function() { + global.db = azul({ adapter: Adapter.create() }); + }); + fn.call(this); + }; +}; diff --git a/test/assertion_tests.js b/test/assertion_tests.js new file mode 100644 index 0000000..96247da --- /dev/null +++ b/test/assertion_tests.js @@ -0,0 +1,58 @@ +'use strict'; + +require('./_helpers'); + +var _ = require('lodash'); + +describe('assertions', __db(function() { + /* global db */ + + beforeEach(require('./common').models); + + it('can validate a model', function() { + db.model('article').create({ title: 'Azul Chai' }) + .should.be.a.model('article') + .with.json({ id: undefined, title: 'Azul Chai' }); + }); + + it('can validate a model with the class', function() { + db.model('article').create({ title: 'Azul Chai' }) + .should.be.a.model(db.model('article')); + }); + + it('produces an error for mistyped model', function() { + expect(function() { + db.model('article').create({ title: '' }).should.be.a.model('post'); + }) + .to.throw(/expected.*Article.*to be.*Post.*but was.*Article/); + }); + + it('produces an error for mismatched json', function() { + expect(function() { + db.model('article').create({ title: '' }) + .should.be.a.model('article') + .with.json({ id: undefined, title: 'Azul Chai' }); + }) + .to.throw(/expected.*Article.*to have.*Azul Chai.*but got.*''/); + }); + + it('produces an error when json is used on non-model', function() { + expect(function() { + [].should.be.json({}); + }) + .to.throw(/method.*json.*cannot be used.*context/i); + }); + + it('would be compatible with other json methods', function() { + chai.use(function(chai) { + chai.Assertion.addMethod('json', function() {}); + }); + chai.use(_.partial(require('..'))); // install copy of azul-chai + + expect(function() { + [].should.be.json({}); + }) + .to.not.throw(); + }); + +})); diff --git a/test/common.js b/test/common.js new file mode 100644 index 0000000..7458dc2 --- /dev/null +++ b/test/common.js @@ -0,0 +1,21 @@ +'use strict'; + +require('./_helpers'); + +exports.models = function() { + /* global db */ + + var attr = db.attr; + var hasMany = db.hasMany; + var belongsTo = db.belongsTo; + + db.model('article', { + title: attr(), + comments: hasMany(), + }); + db.model('comment', { + body: attr(), + article: belongsTo('article'), + }); + +}; diff --git a/test/mocha.opts b/test/mocha.opts new file mode 100644 index 0000000..4a52320 --- /dev/null +++ b/test/mocha.opts @@ -0,0 +1 @@ +--recursive