├── .gitignore ├── .npmignore ├── CHANGELOG.md ├── conf.json ├── lib ├── assertions │ ├── comprise.js │ ├── index.js │ ├── statuscode.js │ ├── compression.js │ ├── json.js │ ├── header.js │ ├── schema.js │ └── cookie.js ├── utils │ └── objectPath.js ├── chakram.js ├── plugins.js └── methods.js ├── .jshintrc ├── test ├── assertions │ ├── statuscode.js │ ├── compression.js │ ├── cookie.js │ ├── header.js │ ├── json.js │ └── schema.js ├── warnings.js ├── methods.js ├── documentation-examples.js ├── plugins.js └── base.js ├── .travis.yml ├── LICENSE ├── package.json ├── examples ├── randomuser.js ├── dweet.js └── spotify.js └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | out -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | coverage 3 | out -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | #Changelog 2 | 3 | ###1.0.0 4 | - Added "comprise" and "comprised" chain elements which should be used instead of "include" for subset JSON assertions 5 | - Simplified plugin interface 6 | 7 | ###0.1.0 8 | - Added callback support for JSON and header assertions 9 | -------------------------------------------------------------------------------- /conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "templates": { 3 | "applicationName": "Chakram", 4 | "meta": { 5 | "title": "Chakram: BDD REST API test tool", 6 | "description": "Chakram: BDD REST API test tool", 7 | "keyword": "E2E REST API JSON chai mocha nodejs test BDD" 8 | }, 9 | "linenums": false 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /lib/assertions/comprise.js: -------------------------------------------------------------------------------- 1 | module.exports = function (chai, utils) { 2 | 3 | var setContainsFlag = function () { 4 | utils.flag(this,'contains',true); 5 | }; 6 | 7 | utils.addProperty(chai.Assertion.prototype, "comprise", setContainsFlag); 8 | utils.addProperty(chai.Assertion.prototype, "comprised", setContainsFlag); 9 | }; 10 | -------------------------------------------------------------------------------- /lib/utils/objectPath.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | get : function (chaiUtils, obj, path) { 3 | var subObject = chaiUtils.getPathValue(path, obj); 4 | if(subObject === undefined || subObject === null) { 5 | throw new Error("could not find path '"+path+"' in object "+JSON.stringify(obj)); 6 | } 7 | return subObject; 8 | } 9 | }; -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "camelcase": true, 3 | "curly": true, 4 | "eqeqeq": true, 5 | "indent": 2, 6 | "latedef": true, 7 | "unused": "vars", 8 | "undef": true, 9 | "node": true, 10 | "expr": true, 11 | "globals": { 12 | "before": true, 13 | "beforeEach": true, 14 | "after": true, 15 | "afterEach": true, 16 | "describe": true, 17 | "it": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /lib/assertions/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | Chakram Expectation 3 | @module chakram-expectation 4 | @desc Extends the {@link http://chaijs.com/api/bdd/ chai.expect} object with additional HTTP matchers. 5 | */ 6 | 7 | module.exports = [ 8 | require('./statuscode.js'), 9 | require('./header.js'), 10 | require('./cookie.js'), 11 | require('./schema.js'), 12 | require('./json.js'), 13 | require('./compression.js'), 14 | require('./comprise.js') 15 | ]; -------------------------------------------------------------------------------- /test/assertions/statuscode.js: -------------------------------------------------------------------------------- 1 | var chakram = require('./../../lib/chakram.js'), 2 | expect = chakram.expect; 3 | 4 | describe("Chakram Assertions", function() { 5 | 6 | describe("Status code", function() { 7 | it("should assert return status code", function() { 8 | var exists = chakram.get("http://httpbin.org/status/200"); 9 | var missing = chakram.get("http://httpbin.org/status/404"); 10 | return chakram.waitFor([ 11 | expect(exists).to.have.status(200), 12 | expect(missing).to.have.status(404) 13 | ]); 14 | }); 15 | }); 16 | 17 | }); -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "0.10" 4 | 5 | addons: 6 | code_climate: 7 | repo_token: 1ab8d32d74f270b9feaf3a47edce2c8d569cf164b2f7615374542952400d2e02 8 | 9 | after_script: 10 | - if [ $TRAVIS_BRANCH = 'master' ]; cat coverage/lcov.info | codeclimate; fi 11 | 12 | deploy: 13 | provider: npm 14 | email: danielallenreid@gmail.com 15 | api_key: 16 | secure: WKXhgXectE9pqwxUMXDmNF7hC8ZatjInaPoO2XpqNZ4ggFeBT7YFuBPeZ4tqGo0RAgsSRC+/USENuMUZPlJjuVir2hdK9lX0iUxmXboUIPGNzCt7+cY/g0/ZVLtSBMsBmoRjZlPLYxoyqXJJCw9yiz41vOBk6dSQzdgqu9J4R78= 17 | on: 18 | tags: true 19 | repo: dareid/chakram 20 | branch: master -------------------------------------------------------------------------------- /lib/assertions/statuscode.js: -------------------------------------------------------------------------------- 1 | /** 2 | Checks the status code of the response 3 | @alias module:chakram-expectation.status 4 | @param {Number} code - the expected status code from the response 5 | @example 6 | it("should allow checking of the response's status code", function () { 7 | var response = chakram.get("http://httpbin.org/get"); 8 | return expect(response).to.have.status(200); 9 | }); 10 | */ 11 | 12 | module.exports = function (chai, utils) { 13 | 14 | utils.addMethod(chai.Assertion.prototype, 'status', function (status) { 15 | var respStatus = this._obj.response.statusCode; 16 | this.assert( 17 | respStatus === status, 18 | 'expected status code ' + respStatus + ' to equal ' + status, 19 | 'expected status code ' + respStatus + ' not to equal ' + status 20 | ); 21 | }); 22 | }; -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Daniel Reid 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /lib/assertions/compression.js: -------------------------------------------------------------------------------- 1 | module.exports = function (chai, utils) { 2 | 3 | var confirmCompression = function(expectedCompression) { 4 | this.to.have.header('content-encoding', expectedCompression); 5 | }; 6 | 7 | /** 8 | Checks that the response is gzip compressed 9 | @alias module:chakram-expectation.gzip 10 | @example 11 | it("should detect gzip compression", function () { 12 | var gzip = chakram.get("http://httpbin.org/gzip"); 13 | return expect(gzip).to.be.encoded.with.gzip; 14 | }); 15 | */ 16 | var gzipAssertion = function () { 17 | confirmCompression.call(this, 'gzip'); 18 | }; 19 | 20 | /** 21 | Checks that the response is deflate compressed 22 | @alias module:chakram-expectation.deflate 23 | @example 24 | it("should detect deflate compression", function () { 25 | var deflate = chakram.get("http://httpbin.org/deflate"); 26 | return expect(deflate).to.be.encoded.with.deflate; 27 | }); 28 | */ 29 | var deflateAssertion = function () { 30 | confirmCompression.call(this, 'deflate'); 31 | }; 32 | 33 | utils.addProperty(chai.Assertion.prototype, 'encoded', function () {}); 34 | utils.addProperty(chai.Assertion.prototype, 'gzip', gzipAssertion); 35 | utils.addProperty(chai.Assertion.prototype, 'deflate', deflateAssertion); 36 | }; -------------------------------------------------------------------------------- /test/assertions/compression.js: -------------------------------------------------------------------------------- 1 | var chakram = require('./../../lib/chakram.js'), 2 | expect = chakram.expect; 3 | 4 | describe("Chakram Assertions", function() { 5 | 6 | describe("Compression", function() { 7 | 8 | it("should allow assertions on uncompressed responses", function () { 9 | var noncompressed = chakram.get("http://httpbin.org/get"); 10 | expect(noncompressed).not.to.be.encoded.with.gzip; 11 | expect(noncompressed).not.to.be.encoded.with.deflate; 12 | return chakram.wait(); 13 | }); 14 | 15 | it("should detect gzip compression", function () { 16 | var gzip = chakram.get("http://httpbin.org/gzip"); 17 | expect(gzip).to.be.encoded.with.gzip; 18 | expect(gzip).not.to.be.encoded.with.deflate; 19 | return chakram.wait(); 20 | }); 21 | 22 | it("should detect deflate compression", function () { 23 | var deflate = chakram.get("http://httpbin.org/deflate"); 24 | expect(deflate).not.to.be.encoded.with.gzip; 25 | expect(deflate).to.be.encoded.with.deflate; 26 | return chakram.wait(); 27 | }); 28 | 29 | it("should support shorter language chains", function () { 30 | var deflate = chakram.get("http://httpbin.org/deflate"); 31 | expect(deflate).not.to.be.gzip; 32 | expect(deflate).to.be.deflate; 33 | return chakram.wait(); 34 | }); 35 | }); 36 | 37 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chakram", 3 | "version": "1.0.0", 4 | "description": "Chakram is an API testing framework designed to test JSON REST endpoints. The library offers a BDD testing style and fully exploits javascript promises", 5 | "main": "lib/chakram.js", 6 | "license": { 7 | "type": "MIT" 8 | }, 9 | "contributors": [{ 10 | "name": "Daniel Reid", 11 | "email": "danielallenreid@gmail.com", 12 | "url": "https://twitter.com/danielallenreid" 13 | }, { 14 | "name": "Harry Rose" 15 | } 16 | ], 17 | "repository": { 18 | "type": "git", 19 | "url": "https://github.com/dareid/chakram" 20 | }, 21 | "keywords": [ 22 | "test", 23 | "API", 24 | "REST", 25 | "HTTP", 26 | "JSON", 27 | "mocha", 28 | "chai", 29 | "BDD" 30 | ], 31 | "dependencies": { 32 | "request": "2.55.x", 33 | "extend-object": "1.0.x", 34 | "q": "1.4.x", 35 | "chai": "2.3.x", 36 | "chai-as-promised": "5.0.x", 37 | "tv4": "1.1.x", 38 | "chai-subset": "1.0.x" 39 | }, 40 | "devDependencies": { 41 | "mocha": "2.x.x", 42 | "istanbul": "0.x.x", 43 | "codeclimate-test-reporter": "0.x.x", 44 | "rewire": "2.x.x", 45 | "sinon": "1.x.x", 46 | "jsdoc": "latest", 47 | "jaguarjs-jsdoc": "dareid/jaguarjs-jsdoc" 48 | }, 49 | "scripts": { 50 | "pretest": "npm install", 51 | "test": "istanbul cover _mocha test/* test/**/* examples/*", 52 | "predoc": "npm install", 53 | "doc": "jsdoc -t node_modules/jaguarjs-jsdoc -c conf.json -R README.md -r lib" 54 | }, 55 | "engines": { 56 | "node": ">= 0.10.0" 57 | } 58 | } -------------------------------------------------------------------------------- /test/warnings.js: -------------------------------------------------------------------------------- 1 | var rewire = require('rewire'), 2 | sinon = require('sinon'), 3 | chakram = rewire('./../lib/chakram.js'), 4 | expect = chakram.expect; 5 | 6 | 7 | describe("User Warnings", function() { 8 | var warningStub, revertWarning; 9 | 10 | before(function () { 11 | warningStub = sinon.stub(); 12 | revertWarning = chakram.__set__("warnUser", warningStub); 13 | }); 14 | 15 | it("should warn user about unrun tests", function () { 16 | var request = chakram.get("http://httpbin.org/status/200"); 17 | expect(request).to.have.status(400); 18 | return request; 19 | }); 20 | 21 | it("should warn user about unrun tests even when 'it' is synchronous", function() { 22 | var request = chakram.get("http://httpbin.org/status/200"); 23 | expect(request).to.have.status(400); 24 | }); 25 | 26 | it("should set the test as an error on warning the user", function () { 27 | revertWarning(); 28 | var thisObj = { 29 | test: { 30 | error : sinon.stub() 31 | }, 32 | currentTest: { 33 | state : "passed" 34 | } 35 | }; 36 | var warning = chakram.__get__("warnUser"); 37 | warning.call(thisObj, "test error"); 38 | expect(thisObj.test.error.callCount).to.equal(1); 39 | }); 40 | 41 | it("should not warn the user if the test has failed anyway", function () { 42 | revertWarning(); 43 | var thisObj = { 44 | test: { 45 | error : sinon.stub() 46 | }, 47 | currentTest: { 48 | state : "failed" 49 | } 50 | }; 51 | var warning = chakram.__get__("warnUser"); 52 | warning.call(thisObj, "test error"); 53 | expect(thisObj.test.error.callCount).to.equal(0); 54 | }); 55 | 56 | after(function() { 57 | expect(warningStub.callCount).to.equal(2); 58 | }); 59 | }); -------------------------------------------------------------------------------- /lib/assertions/json.js: -------------------------------------------------------------------------------- 1 | var path = require('./../utils/objectPath.js'); 2 | 3 | /** 4 | Checks the content of a JSON object within the return body. By default this will check the body JSON exactly matches the given object. If the 'comprise' chain element is used, it checks that the object specified is contained within the body JSON. An additional first argument allows sub object checks. 5 | @alias module:chakram-expectation.json 6 | @param {String} [subelement] - if specified a subelement of the JSON body is checked, specified using dot notation 7 | @param {*|function} expectedValue - a JSON serializable object which should match the JSON body or the JSON body's subelement OR a custom function which is called with the JSON body or the JSON body's subelement 8 | @example 9 | it("should allow checking of JSON return bodies", function () { 10 | var response = chakram.get("http://httpbin.org/get"); 11 | expect(response).to.comprise.of.json({ 12 | url: "http://httpbin.org/get", 13 | headers: { 14 | Host: "httpbin.org", 15 | } 16 | }); 17 | expect(response).to.have.json('url', "http://httpbin.org/get"); 18 | expect(response).to.have.json('url', function (url) { 19 | expect(url).to.equal("http://httpbin.org/get"); 20 | }); 21 | return chakram.wait(); 22 | }); 23 | */ 24 | 25 | module.exports = function (chai, utils) { 26 | var flag = utils.flag; 27 | utils.addMethod(chai.Assertion.prototype, 'json', function () { 28 | 29 | var object = this._obj.body; 30 | var toMatch = arguments[arguments.length-1]; 31 | 32 | if(arguments.length === 2) { 33 | object = path.get(utils, object, arguments[0]); 34 | } 35 | 36 | if(typeof(toMatch) === 'function') { 37 | toMatch(object); 38 | } else { 39 | var assert = new chai.Assertion(object); 40 | utils.transferFlags(this, assert, false); 41 | 42 | if(flag(this, 'contains')) { 43 | assert.to.containSubset(toMatch); 44 | } else { 45 | assert.to.deep.equal(toMatch); 46 | } 47 | } 48 | }); 49 | }; -------------------------------------------------------------------------------- /test/assertions/cookie.js: -------------------------------------------------------------------------------- 1 | var chakram = require('./../../lib/chakram.js'), 2 | expect = chakram.expect; 3 | 4 | describe("Chakram Assertions", function() { 5 | describe("Cookies", function() { 6 | 7 | var cookieSet; 8 | 9 | before(function() { 10 | cookieSet = chakram.get("http://httpbin.org/cookies/set?chakram=testval"); 11 | }); 12 | 13 | it("should check existance of a cookie", function () { 14 | expect(cookieSet).to.have.cookie('chakram'); 15 | expect(cookieSet).not.to.have.cookie('nonexistantcookie'); 16 | return chakram.wait(); 17 | }); 18 | 19 | it("should check that the cookie value matches a given string", function () { 20 | expect(cookieSet).to.have.cookie('chakram', 'testval'); 21 | 22 | expect(cookieSet).not.to.have.cookie('Chakram', 'testval'); 23 | expect(cookieSet).not.to.have.cookie('chakram', 'est'); 24 | expect(cookieSet).not.to.have.cookie('chakram', 'testva'); 25 | expect(cookieSet).not.to.have.cookie('chakram', 'Testval'); 26 | expect(cookieSet).not.to.have.cookie('chakram', ''); 27 | 28 | expect(cookieSet).not.to.have.cookie('nonexistantcookie', 'testval'); 29 | return chakram.wait(); 30 | }); 31 | 32 | it("should check that the cookie value satisifies regex", function () { 33 | expect(cookieSet).to.have.cookie('chakram', /testval/); 34 | expect(cookieSet).to.have.cookie('chakram', /TESTVAL/i); 35 | expect(cookieSet).to.have.cookie('chakram', /test.*/); 36 | expect(cookieSet).to.have.cookie('chakram', /te.*val/); 37 | expect(cookieSet).to.have.cookie('chakram', /est/); 38 | 39 | expect(cookieSet).not.to.have.cookie('chakram', /\s/); 40 | expect(cookieSet).not.to.have.cookie('chakram', /t[s]/); 41 | expect(cookieSet).not.to.have.cookie('chakram', /TESTVAL/); 42 | 43 | expect(cookieSet).not.to.have.cookie('nonexistantcookie', /testval/); 44 | return chakram.wait(); 45 | }); 46 | }); 47 | }); -------------------------------------------------------------------------------- /lib/assertions/header.js: -------------------------------------------------------------------------------- 1 | /** 2 | Either checks that a header exists or ensures the header matches a given value 3 | @alias module:chakram-expectation.header 4 | @param {String} name - checks a header with this name exists 5 | @param {String | RegExp | function} [value] - if specified, checks the header matches the given string or regular expression OR calls the provided function passing the header's value 6 | @example 7 | it("should allow checking of HTTP headers", function () { 8 | var response = chakram.get("http://httpbin.org/get"); 9 | expect(response).to.have.header('content-type'); 10 | expect(response).to.have.header('content-type', 'application/json'); 11 | expect(response).to.have.header('content-type', /json/); 12 | expect(response).to.have.header('content-type', function(contentType) { 13 | expect(contentType).to.equal('application/json'); 14 | }); 15 | return chakram.wait(); 16 | }); 17 | */ 18 | 19 | module.exports = function (chai, utils) { 20 | 21 | utils.addMethod(chai.Assertion.prototype, 'header', function (key, expected) { 22 | 23 | var headerValue = this._obj.response.headers[key.toLowerCase()]; 24 | 25 | if(arguments.length === 1) { 26 | this.assert( 27 | headerValue !== undefined && headerValue !== null, 28 | 'expected header '+ key +' to exist', 29 | 'expected header '+ key +' not to exist' 30 | ); 31 | } else if (expected instanceof RegExp) { 32 | this.assert( 33 | expected.test(headerValue), 34 | 'expected header '+ key + ' with value ' + headerValue + ' to match regex '+expected, 35 | 'expected header '+ key + ' with value ' + headerValue + ' not to match regex '+expected 36 | ); 37 | } else if (typeof(expected) === 'function') { 38 | expected(headerValue); 39 | } else { 40 | this.assert( 41 | headerValue === expected, 42 | 'expected header '+ key + ' with value ' + headerValue + ' to match '+expected, 43 | 'expected header '+ key + ' with value ' + headerValue + ' not to match '+expected 44 | ); 45 | } 46 | }); 47 | }; -------------------------------------------------------------------------------- /lib/assertions/schema.js: -------------------------------------------------------------------------------- 1 | var tv4 = require('tv4'), 2 | path = require('./../utils/objectPath.js'); 3 | 4 | /** 5 | Checks the schema of the returned JSON object against a provided {@link http://json-schema.org/ JSON Schema}. This assertion utilizes the brilliant {@link https://github.com/geraintluff/tv4 tv4 library}. An optional dot notation argument allows a subelement of the JSON object to checked against a JSON schema. Amoungst others, this can confirm types, array lengths, required fields, min and max of numbers and string lengths. For more examples see the test/assertions/schema.js tests. 6 | @alias module:chakram-expectation.schema 7 | @param {String} [subelement] - if specified a subelement of the JSON body is checked, specified using dot notation 8 | @param {*} expectedSchema - a JSON schema object which should match the JSON body or the JSON body's subelement. For more details on format see {@link http://json-schema.org/ the JSON schema website} 9 | @example 10 | it("should check that the returned JSON object satisifies a JSON schema", function () { 11 | var response = chakram.get("http://httpbin.org/get"); 12 | expect(response).to.have.schema('headers', {"required": ["Host", "Accept"]}); 13 | expect(response).to.have.schema({ 14 | "type": "object", 15 | properties: { 16 | url: { 17 | type: "string" 18 | }, 19 | headers: { 20 | type: "object" 21 | } 22 | } 23 | }); 24 | return chakram.wait(); 25 | }); 26 | */ 27 | 28 | module.exports = function (chai, utils) { 29 | 30 | utils.addMethod(chai.Assertion.prototype, 'schema', function () { 31 | 32 | var object = this._obj.body; 33 | var schema = arguments[arguments.length-1]; 34 | 35 | if(arguments.length === 2) { 36 | object = path.get(utils, object, arguments[0]); 37 | } 38 | 39 | var valid = tv4.validate(object, schema); 40 | 41 | this.assert( 42 | valid, 43 | 'expected body to match JSON schema '+JSON.stringify(schema)+'. error: ' + tv4.error, 44 | 'expected body to not match JSON schema ' + JSON.stringify(schema) 45 | ); 46 | }); 47 | }; -------------------------------------------------------------------------------- /lib/assertions/cookie.js: -------------------------------------------------------------------------------- 1 | /** 2 | Either checks that a cookie exists or ensures the cookie matches a given value 3 | @alias module:chakram-expectation.cookie 4 | @param {String} name - checks a cookie with this name exists 5 | @param {String | RegExp} [value] - if specified, checks the cookie matches the given string or regular expression 6 | @example 7 | it("should allow checking of HTTP cookies", function () { 8 | var response = chakram.get("http://httpbin.org/cookies/set?chakram=testval"); 9 | expect(response).to.have.cookie('chakram'); 10 | expect(response).to.have.cookie('chakram', 'testval'); 11 | expect(response).to.have.cookie('chakram', /val/); 12 | return chakram.wait(); 13 | }); 14 | */ 15 | module.exports = function (chai, utils) { 16 | 17 | var getCookie = function (jar, url, key) { 18 | var cookies = jar.getCookies(url); 19 | for(var ct = 0; ct < cookies.length; ct++) { 20 | if(cookies[ct].key === key) { 21 | return cookies[ct]; 22 | } 23 | } 24 | return null; 25 | }; 26 | 27 | var getCookieValue = function (jar, url, key) { 28 | var cookie = getCookie(jar, url, key); 29 | return (cookie === null ? null : cookie.value); 30 | }; 31 | 32 | utils.addMethod(chai.Assertion.prototype, 'cookie', function (key, value) { 33 | var cookieValue = getCookieValue(this._obj.jar, this._obj.url, key); 34 | if(arguments.length === 1) { 35 | this.assert( 36 | cookieValue !== undefined && cookieValue !== null, 37 | 'expected cookie '+ key +' to exist', 38 | 'expected cookie '+ key +' not to exist' 39 | ); 40 | } else if (value instanceof RegExp) { 41 | this.assert( 42 | value.test(cookieValue), 43 | 'expected cookie '+ key + ' with value ' + cookieValue + ' to match regex '+value, 44 | 'expected cookie '+ key + ' with value ' + cookieValue + ' not to match regex '+value 45 | ); 46 | } else { 47 | this.assert( 48 | cookieValue === value, 49 | 'expected cookie '+ key + ' with value ' + cookieValue + ' to match '+value, 50 | 'expected cookie '+ key + ' with value ' + cookieValue + ' not to match '+value 51 | ); 52 | } 53 | }); 54 | }; -------------------------------------------------------------------------------- /examples/randomuser.js: -------------------------------------------------------------------------------- 1 | var chakram = require('./../lib/chakram.js'), 2 | expect = chakram.expect; 3 | 4 | describe("Random User API", function() { 5 | var apiResponse; 6 | 7 | before(function () { 8 | apiResponse = chakram.get("http://api.randomuser.me/?gender=female"); 9 | }); 10 | 11 | it("should return 200 on success", function () { 12 | return expect(apiResponse).to.have.status(200); 13 | }); 14 | 15 | it("should return content type and server headers", function () { 16 | expect(apiResponse).to.have.header("server"); 17 | expect(apiResponse).to.have.header("content-type", /json/); 18 | return chakram.wait(); 19 | }); 20 | 21 | it("should include email, username, password and phone number", function () { 22 | return expect(apiResponse).to.have.schema('results[0].user', { 23 | "required": [ 24 | "email", 25 | "username", 26 | "password", 27 | "phone" 28 | ] 29 | }); 30 | }); 31 | 32 | it("should return a female user", function () { 33 | return expect(apiResponse).to.have.json('results[0].user.gender', 'female'); 34 | }); 35 | 36 | it("should return a valid email address", function () { 37 | return expect(apiResponse).to.have.json(function(json) { 38 | var email = json.results[0].user.email; 39 | expect(/\S+@\S+\.\S+/.test(email)).to.be.true; 40 | }); 41 | }); 42 | 43 | it("should return a single random user", function () { 44 | return expect(apiResponse).to.have.schema('results', {minItems: 1, maxItems: 1}); 45 | }); 46 | 47 | it("should not be gzip compressed", function () { 48 | return expect(apiResponse).not.to.be.encoded.with.gzip; 49 | }); 50 | 51 | it("should return a different username on each request", function () { 52 | this.timeout(10000); 53 | var multipleResponses = []; 54 | for(var ct = 0; ct < 5; ct++) { 55 | multipleResponses.push(chakram.get("http://api.randomuser.me/?gender=female")); 56 | } 57 | return chakram.all(multipleResponses).then(function(responses) { 58 | var returnedUsernames = responses.map(function(response) { 59 | return response.body.results[0].user.username; 60 | }); 61 | while (returnedUsernames.length > 0) { 62 | var username = returnedUsernames.pop(); 63 | expect(returnedUsernames.indexOf(username)).to.equal(-1); 64 | } 65 | }); 66 | }); 67 | }); -------------------------------------------------------------------------------- /test/assertions/header.js: -------------------------------------------------------------------------------- 1 | var chakram = require('./../../lib/chakram.js'), 2 | expect = chakram.expect; 3 | 4 | describe("Chakram Assertions", function() { 5 | describe("Header", function() { 6 | 7 | var headerRequest; 8 | 9 | before(function() { 10 | headerRequest = chakram.get("http://httpbin.org/response-headers?testheader=true123"); 11 | }); 12 | 13 | it("should check existance of a header", function () { 14 | expect(headerRequest).to.have.header('testheader'); 15 | expect(headerRequest).to.have.header('testHeaDer'); 16 | expect(headerRequest).not.to.have.header('notpresentheader'); 17 | return chakram.wait(); 18 | }); 19 | 20 | it("should check that header matches string", function () { 21 | expect(headerRequest).to.have.header('testheader', "true123"); 22 | expect(headerRequest).to.have.header('TESTHEADER', "true123"); 23 | 24 | expect(headerRequest).not.to.have.header('testheader', "123"); 25 | expect(headerRequest).not.to.have.header('testheader', "TRUE"); 26 | expect(headerRequest).not.to.have.header('testheader', "true"); 27 | expect(headerRequest).not.to.have.header('testheader', "tru"); 28 | 29 | expect(headerRequest).not.to.have.header('notpresentheader', "true123"); 30 | return chakram.wait(); 31 | }); 32 | 33 | it("should check that header satisfies regex", function () { 34 | expect(headerRequest).to.have.header('testheader', /true/); 35 | expect(headerRequest).to.have.header('testheader', /ru/); 36 | expect(headerRequest).to.have.header('testheader', /\d/); 37 | expect(headerRequest).to.have.header('testheader', /[t][r]/); 38 | expect(headerRequest).to.have.header('Testheader', /TRUE/i); 39 | 40 | expect(headerRequest).not.to.have.header('testheader', /tree/); 41 | expect(headerRequest).not.to.have.header('testheader', /\s/); 42 | expect(headerRequest).not.to.have.header('testheader', /[t][w|y|j]/); 43 | expect(headerRequest).not.to.have.header('testheader', /TRUE/); 44 | 45 | expect(headerRequest).not.to.have.header('notpresentheader', "/true123/"); 46 | return chakram.wait(); 47 | }); 48 | 49 | it("should call provided functions with the header value", function () { 50 | expect(headerRequest).to.have.header('testheader', function (headerValue) { 51 | expect(headerValue).to.equal("true123"); 52 | }); 53 | expect(headerRequest).to.have.header('notpresentheader', function (headerValue) { 54 | expect(headerValue).to.be.undefined; 55 | }); 56 | return chakram.wait(); 57 | }); 58 | }); 59 | }); -------------------------------------------------------------------------------- /test/methods.js: -------------------------------------------------------------------------------- 1 | var chakram = require('./../lib/chakram.js'), 2 | expect = chakram.expect; 3 | 4 | describe("Methods", function() { 5 | 6 | var testWriteMethods = function (testMethod, testUrl) { 7 | it("should support JSON requests", function () { 8 | var json = {"num": 2,"str": "test"}; 9 | var response = testMethod(testUrl, json); 10 | return response.then(function(resp) { 11 | expect(resp.body).to.be.an('object'); 12 | expect(resp.body.json).to.deep.equal(json); 13 | expect(resp.body.headers['Content-Type']).to.be.equal('application/json'); 14 | }); 15 | }); 16 | 17 | it("should support non-JSON requests", function () { 18 | var stringPost = "testing with a string post"; 19 | var response = testMethod(testUrl, stringPost, {json:false}); 20 | return response.then(function(resp) { 21 | expect(resp.body).to.be.a('string'); 22 | expect(JSON.parse(resp.body).data).to.be.equal(stringPost); 23 | expect(JSON.parse(resp.body).headers['Content-Type']).not.to.be.equal('application/json'); 24 | }); 25 | }); 26 | 27 | it("should support sending custom headers", function () { 28 | var customHeaders = { 29 | "Token": "dummy token value" 30 | }; 31 | var response = testMethod(testUrl, {}, { 32 | headers: customHeaders 33 | }); 34 | return expect(response).to.include.json('headers', customHeaders); 35 | }); 36 | }; 37 | 38 | describe("POST", function () { 39 | testWriteMethods(chakram.post, "http://httpbin.org/post"); 40 | }); 41 | 42 | describe("PUT", function () { 43 | testWriteMethods(chakram.put, "http://httpbin.org/put"); 44 | }); 45 | 46 | describe("DELETE", function () { 47 | testWriteMethods(chakram.delete, "http://httpbin.org/delete"); 48 | }); 49 | 50 | describe("PATCH", function () { 51 | testWriteMethods(chakram.patch, "http://httpbin.org/patch"); 52 | }); 53 | 54 | it("should allow GET requests", function () { 55 | return chakram.get("http://httpbin.org/get?test=str") 56 | .then(function(obj) { 57 | expect(obj.body).to.be.an('object'); 58 | expect(obj.body.args.test).to.equal('str'); 59 | }); 60 | }); 61 | 62 | it("should allow HEAD requests", function () { 63 | var request = chakram.head("http://httpbin.org/get?test=str"); 64 | expect(request).to.have.status(200); 65 | expect(request).to.have.header('content-length'); 66 | return chakram.wait().then(function(obj) { 67 | expect(obj.body).to.be.undefined; 68 | }); 69 | }); 70 | 71 | it("should allow OPTIONS requests", function () { 72 | var request = chakram.options("http://httpbin.org/get?test=str"); 73 | expect(request).to.have.header('Access-Control-Allow-Credentials'); 74 | expect(request).to.have.header('Access-Control-Allow-Methods'); 75 | expect(request).to.have.header('Access-Control-Allow-Origin'); 76 | expect(request).to.have.header('Access-Control-Max-Age'); 77 | return chakram.wait(); 78 | }); 79 | }); -------------------------------------------------------------------------------- /examples/dweet.js: -------------------------------------------------------------------------------- 1 | var chakram = require('./../lib/chakram.js'), 2 | expect = chakram.expect; 3 | 4 | describe("Dweet API", function() { 5 | 6 | var namedDweetPost, initialDweetData, specifiedThingName; 7 | 8 | before("Initialize a new dweet thing for the tests", function () { 9 | specifiedThingName = 'chakram-test-thing'; 10 | initialDweetData = { 11 | description: "test sending a string", 12 | sensorValue: 0.2222, 13 | alert: true 14 | }; 15 | namedDweetPost = chakram.post("https://dweet.io/dweet/for/"+specifiedThingName, initialDweetData); 16 | }); 17 | 18 | it("should return 200 on success", function () { 19 | return expect(namedDweetPost).to.have.status(200); 20 | }); 21 | 22 | it("should specify success in the response 'this' field", function () { 23 | return expect(namedDweetPost).to.have.json('this', 'succeeded'); 24 | }); 25 | 26 | it("should respond with the created dweet's data", function () { 27 | return expect(namedDweetPost).to.have.json('with.content', initialDweetData); 28 | }); 29 | 30 | it("should use a dweet thing name if provided", function () { 31 | return expect(namedDweetPost).to.have.json('with.thing', specifiedThingName); 32 | }); 33 | 34 | it("should allow retrieval of the last data point", function () { 35 | var dataRetrieval = chakram.get("https://dweet.io/get/latest/dweet/for/"+specifiedThingName); 36 | return expect(dataRetrieval).to.have.json('with[0].content', initialDweetData); 37 | }); 38 | 39 | it("should respond with data matching the dweet schema", function () { 40 | var expectedSchema = { 41 | type: "object", 42 | properties: { 43 | this: {type: "string"}, 44 | by: {type: "string"}, 45 | the: {type: "string"}, 46 | with: { 47 | type: "object", 48 | properties: { 49 | thing: {type: "string"}, 50 | created: {type: "string"}, 51 | content: {type: "object"} 52 | }, 53 | required: ["thing", "created", "content"] 54 | } 55 | }, 56 | required: ["this", "by", "the", "with"] 57 | }; 58 | return expect(namedDweetPost).to.have.schema(expectedSchema); 59 | }); 60 | 61 | describe("anonymous thing name", function () { 62 | 63 | var generatedThingName, anonymousDweetPost; 64 | 65 | before(function () { 66 | anonymousDweetPost = chakram.post("https://dweet.io/dweet", initialDweetData); 67 | return anonymousDweetPost.then(function(respObj) { 68 | generatedThingName = respObj.body.with.thing; 69 | }); 70 | }); 71 | 72 | it("should succeed without a specified thing name, generating a random dweet thing name", function () { 73 | expect(anonymousDweetPost).to.have.status(200); 74 | expect(anonymousDweetPost).to.have.json('this', 'succeeded'); 75 | return chakram.wait(); 76 | }); 77 | 78 | it("should allow data retrieval using the generated thing name", function () { 79 | var data = chakram.get("https://dweet.io/get/latest/dweet/for/"+generatedThingName); 80 | return expect(data).to.have.json('with', function (dweetArray) { 81 | expect(dweetArray).to.have.length(1); 82 | var dweet = dweetArray[0]; 83 | expect(dweet.content).to.deep.equal(initialDweetData); 84 | expect(dweet.thing).to.equal(generatedThingName); 85 | }); 86 | }); 87 | }); 88 | 89 | 90 | }); 91 | -------------------------------------------------------------------------------- /test/documentation-examples.js: -------------------------------------------------------------------------------- 1 | var chakram = require('./../lib/chakram.js'), 2 | expect = chakram.expect; 3 | 4 | describe("Documentation examples", function() { 5 | 6 | it("should support chakram and chai assertions", function () { 7 | var google = chakram.get("http://google.com"); 8 | expect(true).to.be.true; 9 | expect(google).to.have.status(200); 10 | expect(1).to.be.below(10); 11 | expect("teststring").to.be.a('string'); 12 | return chakram.wait(); 13 | }); 14 | 15 | it("should support grouping multiple tests", function () { 16 | var response = chakram.get("http://httpbin.org/get"); 17 | return chakram.waitFor([ 18 | expect(response).to.have.status(200), 19 | expect(response).not.to.have.status(404) 20 | ]); 21 | }); 22 | 23 | it("should support auto waiting for tests", function() { 24 | var response = chakram.get("http://httpbin.org/get"); 25 | expect(response).to.have.status(200); 26 | expect(response).not.to.have.status(404); 27 | return chakram.wait(); 28 | }); 29 | 30 | it("should detect deflate compression", function () { 31 | var deflate = chakram.get("http://httpbin.org/deflate"); 32 | return expect(deflate).to.be.encoded.with.deflate; 33 | }); 34 | 35 | it("should detect gzip compression", function () { 36 | var gzip = chakram.get("http://httpbin.org/gzip"); 37 | return expect(gzip).to.be.encoded.with.gzip; 38 | }); 39 | 40 | it("should allow checking of HTTP cookies", function () { 41 | var response = chakram.get("http://httpbin.org/cookies/set?chakram=testval"); 42 | expect(response).to.have.cookie('chakram'); 43 | expect(response).to.have.cookie('chakram', 'testval'); 44 | expect(response).to.have.cookie('chakram', /val/); 45 | return chakram.wait(); 46 | }); 47 | 48 | it("should allow checking of HTTP headers", function () { 49 | var response = chakram.get("http://httpbin.org/get"); 50 | expect(response).to.have.header('content-type'); 51 | expect(response).to.have.header('content-type', 'application/json'); 52 | expect(response).to.have.header('content-type', /json/); 53 | expect(response).to.have.header('content-type', function(contentType) { 54 | expect(contentType).to.equal('application/json'); 55 | }); 56 | return chakram.wait(); 57 | }); 58 | 59 | it("should allow checking of JSON return bodies", function () { 60 | var response = chakram.get("http://httpbin.org/get"); 61 | expect(response).to.comprise.of.json({ 62 | url: "http://httpbin.org/get", 63 | headers: { 64 | Host: "httpbin.org", 65 | } 66 | }); 67 | expect(response).to.have.json('url', "http://httpbin.org/get"); 68 | expect(response).to.have.json('url', function (url) { 69 | expect(url).to.equal("http://httpbin.org/get"); 70 | }); 71 | return chakram.wait(); 72 | }); 73 | 74 | it("should check that the returned JSON object satisifies a JSON schema", function () { 75 | var response = chakram.get("http://httpbin.org/get"); 76 | expect(response).to.have.schema('headers', {"required": ["Host", "Accept"]}); 77 | expect(response).to.have.schema({ 78 | "type": "object", 79 | properties: { 80 | url: { 81 | type: "string" 82 | }, 83 | headers: { 84 | type: "object" 85 | } 86 | } 87 | }); 88 | return chakram.wait(); 89 | }); 90 | 91 | it("should allow checking of the response's status code", function () { 92 | var response = chakram.get("http://httpbin.org/get"); 93 | return expect(response).to.have.status(200); 94 | }); 95 | 96 | }); -------------------------------------------------------------------------------- /test/plugins.js: -------------------------------------------------------------------------------- 1 | var chakram = require('./../lib/chakram.js'), 2 | expect = chakram.expect; 3 | 4 | describe("Plugins", function() { 5 | 6 | it("should allow new properties to be defined", function () { 7 | chakram.addProperty("httpbin", function (respObj) { 8 | var hostMatches = /httpbin.org/.test(respObj.url); 9 | this.assert(hostMatches, 'expected '+respObj.url+' to contain httpbin.org', 'expected '+respObj.url+' to not contain httpbin.org'); 10 | }); 11 | var httpbin = chakram.get("http://httpbin.org/status/200"); 12 | expect(httpbin).to.be.at.httpbin; 13 | var senseye = chakram.get("http://senseye.io"); 14 | expect(senseye).not.to.be.at.httpbin; 15 | return chakram.wait(); 16 | }); 17 | 18 | it("should allow new methods to be defined", function () { 19 | chakram.addMethod("statusRange", function (respObj, low, high) { 20 | var inRange = respObj.response.statusCode >= low && respObj.response.statusCode <= high; 21 | this.assert(inRange, 'expected '+respObj.response.statusCode+' to be between '+low+' and '+high, 'expected '+respObj.response.statusCode+' not to be between '+low+' and '+high); 22 | }); 23 | var twohundred = chakram.get("http://httpbin.org/status/200"); 24 | expect(twohundred).to.have.statusRange(0, 200); 25 | expect(twohundred).to.have.statusRange(200, 200); 26 | expect(twohundred).not.to.have.statusRange(300, 500); 27 | return chakram.wait(); 28 | }); 29 | 30 | 31 | describe("raw chai plugin registration", function () { 32 | before(function() { 33 | chakram.addRawPlugin("unavailable", function (chai, utils) { 34 | utils.addProperty(chai.Assertion.prototype, 'unavailable', function () { 35 | var statusCode = this._obj.response.statusCode; 36 | this.assert(statusCode === 503, 'expected status code '+statusCode+' to equal 503', 'expected '+statusCode+' to not be equal to 503'); 37 | }); 38 | }); 39 | }); 40 | it("should allow low level chai plugins to be registered", function() { 41 | var unavailableReq = chakram.get("http://httpbin.org/status/503"); 42 | return expect(unavailableReq).to.be.unavailable; 43 | }); 44 | }); 45 | 46 | 47 | describe("pre 0.2.0 plugin registration", function () { 48 | before(function() { 49 | var customProperty = function (chai, utils) { 50 | utils.addProperty(chai.Assertion.prototype, 'teapot', function () { 51 | var statusCode = this._obj.response.statusCode; 52 | this.assert(statusCode === 418, 'expected status code '+statusCode+' to equal 418', 'expected '+statusCode+' to not be equal to 418'); 53 | }); 54 | }; 55 | var customMethod = function (chai, utils) { 56 | utils.addMethod(chai.Assertion.prototype, 'httpVersion', function (ver) { 57 | var version = this._obj.response.httpVersion; 58 | this.assert(version === ver, 'expected '+version+' to equal #{exp}', 'expected '+version+' to not be equal to #{exp}', ver); 59 | }); 60 | }; 61 | chakram.initialize(customProperty, customMethod); 62 | }); 63 | 64 | it("should support adding custom properties", function () { 65 | var notATeapot = chakram.get("http://httpbin.org/status/200"); 66 | var aTeapot = chakram.get("http://httpbin.org/status/418"); 67 | return chakram.waitFor([ 68 | expect(notATeapot).to.not.be.a.teapot, 69 | expect(aTeapot).to.be.a.teapot 70 | ]); 71 | }); 72 | 73 | it("should support adding custom methods", function () { 74 | var aTeapot = chakram.get("http://httpbin.org/status/418"); 75 | return expect(aTeapot).to.have.httpVersion("1.1"); 76 | }); 77 | }); 78 | 79 | }); -------------------------------------------------------------------------------- /lib/chakram.js: -------------------------------------------------------------------------------- 1 | /** 2 | Chakram Module 3 | @module chakram 4 | @example 5 | var chakram = require("chakram"); 6 | */ 7 | 8 | var Q = require('q'), 9 | extend = require('extend-object'), 10 | methods = require('./methods.js'), 11 | plugins = require('./plugins.js'); 12 | 13 | var exports = module.exports = {}; 14 | extend(exports, methods, plugins); 15 | 16 | var recordedExpects = []; 17 | 18 | /** 19 | Chakram assertion constructor. Extends chai's extend method with Chakram's HTTP assertions. 20 | Please see {@link http://chaijs.com/api/bdd/ chai's API documentation} for details on the default chai assertions and the {@link ChakramExpectation} documentation for the Chakram HTTP assertions. 21 | @param {*} value - The variable to run assertions on, can be a {@link ChakramResponse} promise 22 | @returns {chakram-expectation} A Chakram expectation object 23 | @alias module:chakram.expect 24 | @example 25 | var expect = chakram.expect; 26 | it("should support chakram and chai assertions", function () { 27 | var google = chakram.get("http://google.com"); 28 | expect(true).to.be.true; 29 | expect(google).to.have.status(200); 30 | expect(1).to.be.below(10); 31 | expect("teststring").to.be.a('string'); 32 | return chakram.wait(); 33 | }); 34 | */ 35 | exports.expect = function(value) { 36 | if(plugins.chai === null) { 37 | exports.initialize(); 38 | } 39 | if (value !== undefined && value !== null && value.then !== undefined) { 40 | var test = plugins.chai.expect(value).eventually; 41 | recordedExpects.push(test); 42 | return test; 43 | } else { 44 | return plugins.chai.expect(value); 45 | } 46 | }; 47 | 48 | /** 49 | Returns a promise which is fulfilled once all promises in the provided array are fulfilled. 50 | Identical to {@link https://github.com/kriskowal/q/wiki/API-Reference#promiseall Q.all}. 51 | @method 52 | @param {Promise[]} promiseArray - An array of promises to wait for 53 | @returns {Promise} 54 | @alias module:chakram.all 55 | */ 56 | exports.all = Q.all; 57 | 58 | /** 59 | Returns a promise which is fulfilled once all promises in the provided array are fulfilled. 60 | Similar to {@link https://github.com/kriskowal/q/wiki/API-Reference#promiseall Q.all}, however, instead of being fulfilled with an array containing the fulfillment value of each promise, it is fulfilled with the fulfillment value of the last promise in the provided array. This allows chaining of HTTP calls. 61 | @param {Promise[]} promiseArray - An array of promises to wait for 62 | @returns {Promise} 63 | @alias module:chakram.waitFor 64 | @example 65 | it("should support grouping multiple tests", function () { 66 | var response = chakram.get("http://httpbin.org/get"); 67 | return chakram.waitFor([ 68 | expect(response).to.have.status(200), 69 | expect(response).not.to.have.status(404) 70 | ]); 71 | }); 72 | */ 73 | exports.waitFor = function(promiseArray) { 74 | return Q.all(promiseArray).then(function(resolvedArray) { 75 | var deferred = Q.defer(); 76 | deferred.resolve(resolvedArray[resolvedArray.length - 1]); 77 | return deferred.promise; 78 | }); 79 | }; 80 | 81 | /** 82 | Returns a promise which is fulfilled once all chakram expectations are fulfilled. 83 | This works by recording all chakram expectations called within an 'it' and waits for all the expectations to finish before resolving the returned promise. 84 | @returns {Promise} 85 | @alias module:chakram.wait 86 | @example 87 | it("should support auto waiting for tests", function() { 88 | var response = chakram.get("http://httpbin.org/get"); 89 | expect(response).to.have.status(200); 90 | expect(response).not.to.have.status(404); 91 | return chakram.wait(); 92 | }); 93 | */ 94 | exports.wait = function() { 95 | return exports.waitFor(recordedExpects); 96 | }; 97 | 98 | var warnUser = function (message) { 99 | if (this.currentTest.state !== 'failed') { 100 | this.test.error(new Error(message)); 101 | } 102 | }; 103 | 104 | var checkForUnfulfilledExpectations = function () { 105 | for(var ct = 0; ct < recordedExpects.length; ct++) { 106 | if(recordedExpects[ct].isFulfilled !== undefined && recordedExpects[ct].isFulfilled() === false) { 107 | warnUser.call(this, "Some expectation promises were not fulfilled before the test finished. Ensure you are waiting for all the expectations to run"); 108 | break; 109 | } 110 | } 111 | }; 112 | 113 | afterEach(function() { 114 | checkForUnfulfilledExpectations.call(this); 115 | recordedExpects = []; 116 | }); -------------------------------------------------------------------------------- /test/base.js: -------------------------------------------------------------------------------- 1 | var chakram = require('./../lib/chakram.js'), 2 | expect = chakram.expect; 3 | 4 | describe("Chakram", function() { 5 | 6 | it("should support chai's built in expectations", function () { 7 | expect(true).not.to.equal(false); 8 | expect(1).to.be.below(10); 9 | expect("teststring").to.be.a('string'); 10 | expect([1,2,3]).not.to.contain(4); 11 | expect(undefined).to.be.undefined; 12 | expect(null).to.be.null; 13 | }); 14 | 15 | describe("Async support", function () { 16 | 17 | describe("Async it", function() { 18 | var delayedResponse; 19 | this.timeout(11000); 20 | 21 | beforeEach(function() { 22 | delayedResponse = chakram.get("http://httpbin.org/delay/1"); 23 | }); 24 | 25 | it("should support mocha's promise returns", function () { 26 | return expect(delayedResponse).to.have.status(200); 27 | }); 28 | 29 | it("should support mocha's done callback", function (done) { 30 | expect(delayedResponse).to.have.status(200).then(function(){done();}); 31 | }); 32 | }); 33 | }); 34 | 35 | 36 | describe("Response Object", function () { 37 | 38 | var request; 39 | 40 | before(function () { 41 | request = chakram.get("http://httpbin.org/get"); 42 | }); 43 | 44 | it("should expose any errors in the chakram response object", function () { 45 | return chakram.get("not-valid") 46 | .then(function(obj) { 47 | expect(obj.error).to.exist.and.to.be.a("object"); 48 | }); 49 | }); 50 | 51 | it("should include the original URL in the chakram response object", function () { 52 | return chakram.get("not-valid") 53 | .then(function(obj) { 54 | expect(obj.url).to.exist.and.to.equal("not-valid"); 55 | }); 56 | }); 57 | 58 | var assertChakramResponseObject = function (obj) { 59 | expect(obj.body).to.exist; 60 | expect(obj.response).to.exist; 61 | expect(obj.error).to.be.null; 62 | expect(obj.url).to.exist; 63 | expect(obj.jar).to.exist; 64 | }; 65 | 66 | it("should resolve chakram request promises to a chakram response object", function () { 67 | return request.then(assertChakramResponseObject); 68 | }); 69 | 70 | it("should resolve chakram expect promises to a chakram response object", function () { 71 | var expectPromise = expect(request).to.have.status(200); 72 | return expectPromise.then(assertChakramResponseObject); 73 | }); 74 | 75 | it("should resolve chakram.waitFor promises to a chakram response object", function () { 76 | var waitPromise = chakram.waitFor([ 77 | expect(request).to.have.status(200), 78 | expect(request).not.to.have.status(400) 79 | ]); 80 | return waitPromise.then(assertChakramResponseObject); 81 | }); 82 | 83 | it("should resolve chakram.wait promises to a chakram response object", function () { 84 | expect(request).to.have.status(200); 85 | expect(request).not.to.have.status(400); 86 | return chakram.wait().then(assertChakramResponseObject); 87 | }); 88 | }); 89 | 90 | describe("Multiple expects", function () { 91 | var request; 92 | 93 | beforeEach(function() { 94 | request = chakram.get("http://httpbin.org/status/200"); 95 | }); 96 | 97 | it("should support grouping multiple tests", function () { 98 | return chakram.waitFor([ 99 | expect(request).to.have.status(200), 100 | expect(request).not.to.have.status(404) 101 | ]); 102 | }); 103 | 104 | it("should support chaining of tests", function () { 105 | return expect(request).to.have.status(200).and.not.to.have.status(404); 106 | }); 107 | 108 | it("should support auto waiting for tests", function() { 109 | expect(request).to.have.status(200); 110 | expect(request).not.to.have.status(404); 111 | return chakram.wait(); 112 | }); 113 | }); 114 | 115 | describe("Chained requests", function () { 116 | it("should allow multiple chained requests", function () { 117 | this.timeout(4000); 118 | return expect(chakram.get("http://httpbin.org/status/200")).to.have.status(200) 119 | .then(function(obj) { 120 | var postRequest = chakram.post("http://httpbin.org/post", {"url": obj.url}); 121 | expect(postRequest).to.have.status(200); 122 | expect(postRequest).to.have.header('content-length'); 123 | return chakram.wait(); 124 | }).then(function(obj) { 125 | expect(obj.body.json.url).to.be.equal("http://httpbin.org/status/200"); 126 | }); 127 | }); 128 | }); 129 | }); -------------------------------------------------------------------------------- /test/assertions/json.js: -------------------------------------------------------------------------------- 1 | var chakram = require('./../../lib/chakram.js'), 2 | expect = chakram.expect; 3 | 4 | describe("Chakram Assertions", function() { 5 | describe("JSON", function() { 6 | 7 | var postRequest; 8 | 9 | before(function() { 10 | postRequest = chakram.post("http://httpbin.org/post", { 11 | stringArray: ["test1", "test2", "test3"], 12 | number: 20, 13 | str: "test str", 14 | obj: { 15 | test: "str" 16 | } 17 | }); 18 | }); 19 | 20 | it("should throw an error if path does not exist", function () { 21 | return postRequest.then(function (obj) { 22 | expect(function() { 23 | expect(obj).to.have.json('headers.non.existant', {}); 24 | }).to.throw(Error); 25 | }); 26 | }); 27 | 28 | it("should support checking that a path does not exist", function () { 29 | return expect(postRequest).not.to.have.json('headers.non.existant'); 30 | }); 31 | 32 | describe("Equals", function () { 33 | it("should ensure matches json exactly", function () { 34 | return chakram.waitFor([ 35 | expect(postRequest).to.have.json('json.stringArray', ["test1", "test2", "test3"]), 36 | expect(postRequest).to.have.json('json.number', 20), 37 | expect(postRequest).not.to.have.json('json.number', 22), 38 | expect(postRequest).to.have.json('json.obj', { 39 | test: "str" 40 | }) 41 | ]); 42 | }); 43 | }); 44 | 45 | var testChainedCompriseProperty = function(description, buildChain) { 46 | describe(description, function () { 47 | it("should ensure body includes given json", function() { 48 | return chakram.waitFor([ 49 | buildChain(expect(postRequest).to).json({ 50 | json: { 51 | number: 20, 52 | str: "test str", 53 | stringArray: { 54 | 1:"test2" 55 | } 56 | } 57 | }), 58 | buildChain(expect(postRequest).to.not).json({ 59 | json: { number: 22 } 60 | }), 61 | buildChain(expect(postRequest).to).json({ 62 | json: { 63 | obj: { 64 | test: "str" 65 | } 66 | } 67 | }) 68 | ]); 69 | }); 70 | 71 | it("should support negated include JSON assertions", function () { 72 | return postRequest.then(function (resp) { 73 | expect(function() { 74 | buildChain(expect(resp).to.not).json({ 75 | json: { number: 20 } 76 | }); 77 | }).to.throw(Error); 78 | }); 79 | }); 80 | 81 | it("should be able to specify json path", function () { 82 | return chakram.waitFor([ 83 | buildChain(expect(postRequest).to).json('json', { 84 | number: 20, 85 | str: "test str", 86 | stringArray: {1:"test2"} 87 | }), 88 | buildChain(expect(postRequest).to).json('json.obj', { 89 | test: "str" 90 | }), 91 | buildChain(expect(postRequest).to.not).json('json.obj', { 92 | doesnt: "exist" 93 | }) 94 | ]); 95 | }); 96 | }); 97 | }; 98 | 99 | testChainedCompriseProperty("Comprise", function(assertion){ return assertion.comprise.of; }); 100 | testChainedCompriseProperty("Comprised", function(assertion){ return assertion.be.comprised.of; }); 101 | 102 | describe("Callbacks", function () { 103 | it("should allow custom callbacks to be used to run assertions", function () { 104 | return expect(postRequest).to.have.json('json.stringArray', function (data) { 105 | expect(data).to.deep.equal(["test1", "test2", "test3"]); 106 | }); 107 | }); 108 | 109 | it("should allow the whole JSON body to be checked", function () { 110 | return expect(postRequest).to.have.json(function (data) { 111 | expect(data.json.number).to.be.above(19).and.below(21); 112 | expect(data.json.number).not.to.equal(211); 113 | }); 114 | }); 115 | }); 116 | }); 117 | }); -------------------------------------------------------------------------------- /lib/plugins.js: -------------------------------------------------------------------------------- 1 | var chakramMatchers = require("./assertions/index.js"), 2 | chaiSubset = require('chai-subset'), 3 | chaiAsPromised, 4 | plugins = {}; 5 | 6 | var exports = module.exports = {}; 7 | exports.chai = null; 8 | 9 | var extendChaiPromise = function () { 10 | chaiAsPromised.transferPromiseness = function (assertion, promise) { 11 | assertion.then = promise.then.bind(promise); 12 | assertion.isFulfilled = promise.isFulfilled.bind(promise); 13 | }; 14 | }; 15 | 16 | var loadChai = function () { 17 | if (exports.chai !== null) { 18 | //need to remove to reinitialise with new plugins 19 | delete require.cache[require.resolve('chai-as-promised')]; 20 | delete require.cache[require.resolve('chai')]; 21 | } 22 | exports.chai = require('chai'); 23 | chaiAsPromised = require("chai-as-promised"); 24 | extendChaiPromise(); 25 | }; 26 | 27 | /** 28 | Initialise the chakram package with custom chai plugins. This is no longer recommended, instead use either addMethod, addProperty or addRawPlugin. 29 | @deprecated since 0.2.0 30 | @param {...ChaiPlugin} customChaiPlugin - One or multiple chai plugins 31 | @alias module:chakram.initialize 32 | @example 33 | var customProperty = function (chai, utils) { 34 | utils.addProperty(chai.Assertion.prototype, 'teapot', function () { 35 | var statusCode = this._obj.response.statusCode; 36 | this.assert( 37 | statusCode === 418, 38 | 'expected status code '+statusCode+' to equal 418', 39 | 'expected '+statusCode+' to not be equal to 418' 40 | ); 41 | }); 42 | }; 43 | chakram.initialise(customProperty); 44 | */ 45 | exports.initialize = function (customChaiPlugin) { 46 | loadChai(); 47 | for (var ct = 0; ct < arguments.length; ct++) { 48 | exports.chai.use(arguments[ct]); 49 | } 50 | for(var key in plugins) { 51 | exports.chai.use(plugins[key]); 52 | } 53 | chakramMatchers.map(function (matcher) { 54 | exports.chai.use(matcher); 55 | }); 56 | exports.chai.use(chaiSubset); 57 | exports.chai.use(chaiAsPromised); 58 | }; 59 | 60 | /** 61 | Add a raw chai plugin to Chakram. See Chai's documentation for more details. 62 | @param {String} name - The plugin's name, used as an identifier 63 | @param {function} plugin - A Chai plugin function, function should accept two arguments, the chai object and the chai utils object 64 | @alias module:chakram.addRawPlugin 65 | @example 66 | chakram.addRawPlugin("unavailable", function (chai, utils) { 67 | utils.addProperty(chai.Assertion.prototype, 'unavailable', function () { 68 | var statusCode = this._obj.response.statusCode; 69 | this.assert(statusCode === 503, 70 | 'expected status code '+statusCode+' to equal 503', 71 | 'expected '+statusCode+' to not be equal to 503'); 72 | }); 73 | }); 74 | var unavailableReq = chakram.get("http://httpbin.org/status/503"); 75 | return expect(unavailableReq).to.be.unavailable; 76 | */ 77 | exports.addRawPlugin = function (name, plugin) { 78 | plugins[name] = plugin; 79 | exports.initialize(); 80 | }; 81 | 82 | /** 83 | Add a new property assertion to Chakram. Properties should be used over methods when there is no arguments required for the assertion. 84 | @param {String} name - The plugin's name, used as an identifier 85 | @param {function} plugin - A function which should accept one argument; a {@link ChakramResponse} object 86 | @alias module:chakram.addProperty 87 | @example 88 | chakram.addProperty("httpbin", function (respObj) { 89 | var hostMatches = /httpbin.org/.test(respObj.url); 90 | this.assert(hostMatches, 91 | 'expected '+respObj.url+' to contain httpbin.org', 92 | 'expected '+respObj.url+' to not contain httpbin.org'); 93 | }); 94 | var httpbin = chakram.get("http://httpbin.org/status/200"); 95 | return expect(httpbin).to.be.at.httpbin; 96 | */ 97 | exports.addProperty = function (name, callback) { 98 | exports.addRawPlugin(name, function (chai, utils) { 99 | utils.addProperty(chai.Assertion.prototype, name, function () { 100 | callback.call(this, this._obj); 101 | }); 102 | }); 103 | }; 104 | 105 | /** 106 | Add a new method assertion to Chakram. Methods should be used when the assertion requires parameters. 107 | @param {String} name - The plugin's name, used as an identifier 108 | @param {function} plugin - A function which should accept one or more arguments. The first argument will be a {@link ChakramResponse} object, followed by any arguments passed into the assertion. 109 | @alias module:chakram.addMethod 110 | @example 111 | chakram.addMethod("statusRange", function (respObj, low, high) { 112 | var inRange = respObj.response.statusCode >= low && respObj.response.statusCode <= high; 113 | this.assert(inRange, 'expected '+respObj.response.statusCode+' to be between '+low+' and '+high, 'expected '+respObj.response.statusCode+' not to be between '+low+' and '+high); 114 | }); 115 | var twohundred = chakram.get("http://httpbin.org/status/200"); 116 | return expect(twohundred).to.have.statusRange(0, 200); 117 | */ 118 | exports.addMethod = function (name, callback) { 119 | exports.addRawPlugin(name, function (chai, utils) { 120 | utils.addMethod(chai.Assertion.prototype, name, function () { 121 | var args = Array.prototype.slice.call(arguments); 122 | args.unshift(this._obj); 123 | callback.apply(this, args); 124 | }); 125 | }); 126 | }; -------------------------------------------------------------------------------- /examples/spotify.js: -------------------------------------------------------------------------------- 1 | var chakram = require('./../lib/chakram.js'), 2 | expect = chakram.expect; 3 | 4 | describe("Spotify API", function() { 5 | 6 | before(function () { 7 | var spotifyErrorSchema = { 8 | properties: { 9 | error: { 10 | properties: { 11 | status: {type: "integer"}, 12 | message: {type: "string"} 13 | }, 14 | required: ["status", "message"] 15 | } 16 | }, 17 | required: ["error"] 18 | }; 19 | 20 | chakram.addProperty("spotify", function(){}); 21 | chakram.addMethod("error", function (respObj, status, message) { 22 | expect(respObj).to.have.schema(spotifyErrorSchema); 23 | expect(respObj).to.have.status(status); 24 | expect(respObj).to.have.json('error.message', message); 25 | expect(respObj).to.have.json('error.status', status); 26 | }); 27 | chakram.addMethod("limit", function (respObj, topLevelObjectName, limit) { 28 | expect(respObj).to.have.schema(topLevelObjectName, { 29 | required: ["limit", "items"], 30 | properties: { 31 | limit: {minimum:limit, maximum:limit}, 32 | items: {minItems: limit, maxItems: limit} 33 | } 34 | }); 35 | }); 36 | }); 37 | 38 | 39 | describe("Search Endpoint", function () { 40 | var randomArtistSearch; 41 | 42 | before(function () { 43 | randomArtistSearch = chakram.get("https://api.spotify.com/v1/search?q=random&type=artist"); 44 | }); 45 | 46 | 47 | it("should require a search query", function () { 48 | var missingQuery = chakram.get("https://api.spotify.com/v1/search?type=artist"); 49 | return expect(missingQuery).to.be.a.spotify.error(400, "No search query"); 50 | }); 51 | 52 | it("should require an item type", function () { 53 | var missingType = chakram.get("https://api.spotify.com/v1/search?q=random"); 54 | return expect(missingType).to.be.a.spotify.error(400, "Missing parameter type"); 55 | }); 56 | 57 | var shouldSupportItemType = function (type) { 58 | it("should support item type "+type, function () { 59 | var typeCheck = chakram.get("https://api.spotify.com/v1/search?q=random&type="+type); 60 | return expect(typeCheck).to.have.status(200); 61 | }); 62 | }; 63 | 64 | shouldSupportItemType('artist'); 65 | shouldSupportItemType('track'); 66 | shouldSupportItemType('album'); 67 | shouldSupportItemType('playlist'); 68 | 69 | it("should throw an error if an unknown item type is used", function () { 70 | var missingType = chakram.get("https://api.spotify.com/v1/search?q=random&type=invalid"); 71 | return expect(missingType).to.be.a.spotify.error(400, "Bad search type field"); 72 | }); 73 | 74 | it("should by default return 20 search results", function () { 75 | return expect(randomArtistSearch).to.have.limit("artists", 20); 76 | }); 77 | 78 | it("should support a limit parameter", function () { 79 | var one = chakram.get("https://api.spotify.com/v1/search?q=random&type=artist&limit=1"); 80 | expect(one).to.have.limit("artists", 1); 81 | var fifty = chakram.get("https://api.spotify.com/v1/search?q=random&type=artist&limit=50"); 82 | expect(fifty).to.have.limit("artists", 50); 83 | return chakram.wait(); 84 | }); 85 | 86 | it("should support an offset parameter", function () { 87 | var first = chakram.get("https://api.spotify.com/v1/search?q=random&type=artist&limit=1"); 88 | var second = chakram.get("https://api.spotify.com/v1/search?q=random&type=artist&limit=1&offset=1"); 89 | expect(first).to.have.json("artists.offset", 0); 90 | expect(second).to.have.json("artists.offset", 1); 91 | return chakram.all([first,second]).then(function(responses) { 92 | expect(responses[0].body.artists.items[0].id).not.to.equal(responses[1].body.artists.items[0].id); 93 | return chakram.wait(); 94 | }); 95 | }); 96 | 97 | it("should only support GET calls", function () { 98 | this.timeout(4000); 99 | expect(chakram.post("https://api.spotify.com/v1/search")).to.have.status(405); 100 | expect(chakram.put("https://api.spotify.com/v1/search")).to.have.status(405); 101 | expect(chakram.delete("https://api.spotify.com/v1/search")).to.have.status(405); 102 | expect(chakram.patch("https://api.spotify.com/v1/search")).to.have.status(405); 103 | return chakram.wait(); 104 | }); 105 | 106 | it("should return href, id, name and popularity for all found artists", function () { 107 | return expect(randomArtistSearch).to.have.schema('artists.items', { 108 | type: "array", 109 | items: { 110 | type: "object", 111 | properties: { 112 | href: {type: "string"}, 113 | id: {type: "string"}, 114 | name: {type: "string"}, 115 | popularity: {type: "integer"} 116 | }, 117 | required: ["href", "id", "name", "popularity"] 118 | } 119 | }); 120 | }); 121 | }); 122 | }); -------------------------------------------------------------------------------- /lib/methods.js: -------------------------------------------------------------------------------- 1 | var request = require('request'), 2 | extend = require('extend-object'), 3 | Q = require('q'); 4 | 5 | var exports = module.exports = {}; 6 | 7 | /** 8 | Perform HTTP request 9 | @param {string} method - the HTTP method to use 10 | @param {string} url - fully qualified url 11 | @param {Object} [params] - additional request options, see the popular {@link https://github.com/request/request#requestoptions-callback request library} for options 12 | @returns {Promise} Promise which will resolve to a {@link ChakramResponse} object 13 | @alias module:chakram.request 14 | @example 15 | var request = chakram.request("GET", "http://httpbin.org/get", { 16 | 'auth': {'user': 'username','pass': 'password'} 17 | }); 18 | expect(request).to.have.status(200); 19 | */ 20 | exports.request = function (method, url, params) { 21 | var options = extend({ 22 | url: url, 23 | method: method, 24 | json: true, 25 | jar: request.jar() 26 | }, params || {} ); 27 | var deferred = Q.defer(); 28 | request(options, function (error, response, body) { 29 | /** 30 | Chakram Response Object 31 | @desc Encapsulates the results of a HTTP call into a single object 32 | @typedef {Object} ChakramResponse 33 | @property {Error} error - An error when applicable 34 | @property {Object} response - An {@link http://nodejs.org/api/http.html#http_http_incomingmessage http.IncomingMessage} object 35 | @property {String|Buffer|Object} body - The response body. Typically a JSON object unless the json option has been set to false, in which case will be either a String or Buffer 36 | @property {Object} jar - A {@link https://github.com/goinstant/tough-cookie tough cookie} jar 37 | @property {String} url - The request's original URL 38 | */ 39 | deferred.resolve({ 40 | error : error, 41 | response: response, 42 | body: body, 43 | jar: options.jar, 44 | url: url 45 | }); 46 | }); 47 | return deferred.promise; 48 | }; 49 | 50 | /** 51 | Perform HTTP GET request 52 | @param {string} url - fully qualified url 53 | @param {Object} [params] - additional request options, see the popular {@link https://github.com/request/request#requestoptions-callback request library} for options 54 | @returns {Promise} Promise which will resolve to a {@link ChakramResponse} object 55 | @alias module:chakram.get 56 | */ 57 | exports.get = function(url, params) { 58 | return exports.request('GET', url, params); 59 | }; 60 | 61 | /** 62 | Perform HTTP HEAD request 63 | @param {string} url - fully qualified url 64 | @param {Object} [params] - additional request options, see the popular {@link https://github.com/request/request#requestoptions-callback request library} for options 65 | @returns {Promise} Promise which will resolve to a {@link ChakramResponse} object 66 | @alias module:chakram.head 67 | */ 68 | exports.head = function(url, params) { 69 | return exports.request('HEAD', url, params); 70 | }; 71 | 72 | /** 73 | Perform HTTP OPTIONS request 74 | @param {string} url - fully qualified url 75 | @param {Object} [params] - additional request options, see the popular {@link https://github.com/request/request#requestoptions-callback request library} for options 76 | @returns {Promise} Promise which will resolve to a {@link ChakramResponse} object 77 | @alias module:chakram.options 78 | */ 79 | exports.options = function(url, params) { 80 | return exports.request('OPTIONS', url, params); 81 | }; 82 | 83 | var extendWithData = function (data, params) { 84 | return extend({body: data}, params); 85 | }; 86 | 87 | 88 | /** 89 | Perform HTTP POST request 90 | @param {string} url - fully qualified url 91 | @param {Object} data - a JSON serializable object (unless json is set to false in params, in which case this should be a buffer or string) 92 | @param {Object} [params] - additional request options, see the popular {@link https://github.com/request/request#requestoptions-callback request library} for options 93 | @returns {Promise} Promise which will resolve to a {@link ChakramResponse} object 94 | @alias module:chakram.post 95 | */ 96 | exports.post = function (url, data, params) { 97 | return exports.request('POST', url, extendWithData(data, params)); 98 | }; 99 | 100 | /** 101 | Perform HTTP PATCH request 102 | @param {string} url - fully qualified url 103 | @param {Object} data - a JSON serializable object (unless json is set to false in params, in which case this should be a buffer or string) 104 | @param {Object} [params] - additional request options, see the popular {@link https://github.com/request/request#requestoptions-callback request library} for options 105 | @returns {Promise} Promise which will resolve to a {@link ChakramResponse} object 106 | @alias module:chakram.patch 107 | */ 108 | exports.patch = function (url, data, params) { 109 | return exports.request('PATCH', url, extendWithData(data, params)); 110 | }; 111 | 112 | 113 | /** 114 | Perform HTTP PUT request 115 | @param {string} url - fully qualified url 116 | @param {Object} data - a JSON serializable object (unless json is set to false in params, in which case this should be a buffer or string) 117 | @param {Object} [params] - additional request options, see the popular {@link https://github.com/request/request#requestoptions-callback request library} for options 118 | @returns {Promise} Promise which will resolve to a {@link ChakramResponse} object 119 | @alias module:chakram.put 120 | */ 121 | exports.put = function (url, data, params) { 122 | return exports.request('PUT', url, extendWithData(data, params)); 123 | }; 124 | 125 | /** 126 | Perform HTTP DELETE request 127 | @param {string} url - fully qualified url 128 | @param {Object} [data] - a JSON serializable object (unless json is set to false in params, in which case this should be a buffer or string) 129 | @param {Object} [params] - additional request options, see the popular {@link https://github.com/request/request#requestoptions-callback request library} for options 130 | @returns {Promise} Promise which will resolve to a {@link ChakramResponse} object 131 | @alias module:chakram.delete 132 | */ 133 | exports.delete = function(url, data, params) { 134 | return exports.request('DELETE', url, extendWithData(data, params)); 135 | }; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Chakram 2 | 3 | [![Build Status](https://travis-ci.org/dareid/chakram.svg?branch=master)](https://travis-ci.org/dareid/chakram) [![Test Coverage](https://codeclimate.com/github/dareid/chakram/badges/coverage.svg)](https://codeclimate.com/github/dareid/chakram) [![Code Climate](https://codeclimate.com/github/dareid/chakram/badges/gpa.svg)](https://codeclimate.com/github/dareid/chakram) 4 | 5 | Chakram is an API testing framework designed to perform end to end tests on JSON REST endpoints. The library offers a BDD testing style and fully exploits javascript promises - the resulting tests are simple, clear and expressive. The library is built on [node.js](https://nodejs.org/), [mocha](http://mochajs.org/) and [chai](http://chaijs.com/). 6 | 7 | More information is available in the [library's documentation](http://dareid.github.io/chakram/) and its [own tests](https://github.com/dareid/chakram/tree/master/test) which demonstrate all of Chakram's capabilities. Example API tests of publically accessable APIs are available in the [examples directory](https://github.com/dareid/chakram/tree/master/examples). 8 | 9 | ## Install Chakram 10 | Chakram requires nodejs and NPM to be installed, it is available as an NPM module. Ideally, Chakram should be added to your testing project's devDependancies. This can be achieved with the following command: 11 | ```js 12 | npm install chakram --save-dev 13 | ``` 14 | 15 | ## Introduction 16 | Chakram builds on top of the mocha testing framework, as such, the tests follow the [BDD style](http://mochajs.org/#getting-started). As this library focuses on testing REST APIs, the tests are naturally asynchronous. Mocha has [native support for promises](http://mochajs.org/#asynchronous-code) which Chakram exploits. All requests and expectations return promises which fulfill to [Chakram response objects](http://dareid.github.io/chakram/jsdoc/global.html#ChakramResponse). 17 | 18 | The example below demonstrates a GET request and an assertion of the returned status code. The assertion of the status code returns a promise which is fulfilled once the status code has been checked. 19 | 20 | ```js 21 | var chakram = require('chakram'), 22 | expect = chakram.expect; 23 | 24 | describe("Minimal example", function() { 25 | it("should provide a simple async testing framework", function () { 26 | var response = chakram.get("http://httpbin.org/get"); 27 | return expect(response).to.have.status(200); 28 | }); 29 | }); 30 | ``` 31 | Below is a larger example testing the [Random User Generator API](https://randomuser.me/). 32 | 33 | ```js 34 | var chakram = require('chakram'), 35 | expect = chakram.expect; 36 | 37 | describe("Random User API", function() { 38 | var apiResponse; 39 | 40 | before(function () { 41 | apiResponse = chakram.get("http://api.randomuser.me/?gender=female"); 42 | }); 43 | 44 | it("should return 200 on success", function () { 45 | return expect(apiResponse).to.have.status(200); 46 | }); 47 | 48 | it("should return content type and server headers", function () { 49 | expect(apiResponse).to.have.header("server"); 50 | expect(apiResponse).to.have.header("content-type", /json/); 51 | return chakram.wait(); 52 | }); 53 | 54 | it("should include email, username, password and phone number", function () { 55 | return expect(apiResponse).to.have.schema('results[0].user', { 56 | "required": [ 57 | "email", 58 | "username", 59 | "password", 60 | "phone" 61 | ] 62 | }); 63 | }); 64 | 65 | it("should return a female user", function () { 66 | return expect(apiResponse).to.have.json('results[0].user.gender', 'female'); 67 | }); 68 | 69 | it("should return a valid email address", function () { 70 | return expect(apiResponse).to.have.json(function(json) { 71 | var email = json.results[0].user.email; 72 | expect(/\S+@\S+\.\S+/.test(email)).to.be.true; 73 | }); 74 | }); 75 | 76 | it("should return a single random user", function () { 77 | return expect(apiResponse).to.have.schema('results', {minItems: 1, maxItems: 1}); 78 | }); 79 | 80 | it("should not be gzip compressed", function () { 81 | return expect(apiResponse).not.to.be.encoded.with.gzip; 82 | }); 83 | 84 | it("should return a different username on each request", function () { 85 | this.timeout(10000); 86 | var multipleResponses = []; 87 | for(var ct = 0; ct < 5; ct++) { 88 | multipleResponses.push(chakram.get("http://api.randomuser.me/?gender=female")); 89 | } 90 | return chakram.all(multipleResponses).then(function(responses) { 91 | var returnedUsernames = responses.map(function(response) { 92 | return response.body.results[0].user.username; 93 | }); 94 | while (returnedUsernames.length > 0) { 95 | var username = returnedUsernames.pop(); 96 | expect(returnedUsernames.indexOf(username)).to.equal(-1); 97 | } 98 | }); 99 | }); 100 | }); 101 | 102 | ``` 103 | It is important that tests wait for all requests and assertions to be completed. To help, chakram includes a wait method, this returns a promise which will be fulfilled once all assertions have been performed. In addition, Chakram will fail any tests which do not wait for assertions to complete. Below is a test using the wait method. 104 | 105 | ```js 106 | var chakram = require('chakram'), 107 | expect = chakram.expect; 108 | 109 | describe("Minimal example", function() { 110 | it("should provide a simple async testing framework", function () { 111 | var response = chakram.get("http://httpbin.org/get"); 112 | expect(response).to.have.status(200); 113 | expect(response).not.to.have.header('non-existing-header'); 114 | return chakram.wait(); 115 | }); 116 | }); 117 | ``` 118 | 119 | 120 | ## Run Tests 121 | To run Chakram tests, install the Mocha testing framework globally (or as a dev dependancy): 122 | ``` 123 | npm install -g mocha 124 | ``` 125 | Once installed, run the tests using the [Mocha command line](http://mochajs.org/#usage), which in its simplest form is: 126 | ``` 127 | mocha path/to/tests 128 | ``` -------------------------------------------------------------------------------- /test/assertions/schema.js: -------------------------------------------------------------------------------- 1 | var chakram = require('./../../lib/chakram.js'), 2 | expect = chakram.expect; 3 | 4 | describe("Chakram Assertions", function() { 5 | describe("JSON Schema", function() { 6 | 7 | var getRequest, postRequest; 8 | 9 | before(function() { 10 | getRequest = chakram.get("http://httpbin.org/get"); 11 | postRequest = chakram.post("http://httpbin.org/post", { 12 | stringArray: ["test1", "test2", "test3"], 13 | mixedArray: ["str", true, 20, 1.2222, true], 14 | objectArray: [{ 15 | name: "bob", 16 | age: 20 17 | }, { 18 | name: "jim", 19 | age: 72 20 | }], 21 | number: 20, 22 | str: "test str" 23 | }); 24 | }); 25 | 26 | describe("dot notation access", function() { 27 | 28 | it("should perform assertions on subelements if first argument is a dot notation string", function () { 29 | var expectedSchema = {"required": ["Host", "Accept"]}; 30 | expect(getRequest).to.have.schema('headers', expectedSchema); 31 | expect(getRequest).not.to.have.schema(expectedSchema); 32 | return chakram.wait(); 33 | }); 34 | 35 | it("should thrown an error if dot notation is not valid", function () { 36 | return getRequest.then(function (obj) { 37 | expect(function() { 38 | expect(obj).to.have.schema('headers.non.existant', {}); 39 | }).to.throw(Error); 40 | }); 41 | }); 42 | 43 | it("should be case sensitive", function () { 44 | return getRequest.then(function (obj) { 45 | expect(function() { 46 | expect(obj).to.have.schema('Headers', {}); 47 | }).to.throw(Error); 48 | }); 49 | }); 50 | 51 | }); 52 | 53 | describe("objects", function () { 54 | 55 | it("should be able to specify required object values", function () { 56 | var expectedSchema = {"required": ["args", "headers", "origin"]}; 57 | var incorrectSchema = {"required": ["not", "existing"]}; 58 | return chakram.waitFor([ 59 | expect(getRequest).to.have.schema(expectedSchema), 60 | expect(getRequest).not.to.have.schema(incorrectSchema) 61 | ]); 62 | }); 63 | 64 | it("should allow exact matching of an object's properties", function () { 65 | var missingUrlSchema = { 66 | properties: { 67 | url: {}, 68 | headers: {}, 69 | origin: {}, 70 | args: {} 71 | }, 72 | additionalProperties: false 73 | }; 74 | return expect(getRequest).to.have.schema(missingUrlSchema); 75 | }); 76 | 77 | it("should assert types in json object", function () { 78 | var expectedTypes = { 79 | "type": "object", 80 | properties: { 81 | url: { 82 | type: "string" 83 | }, 84 | headers: { 85 | type: "object" 86 | } 87 | } 88 | }; 89 | return expect(getRequest).to.have.schema(expectedTypes); 90 | }); 91 | 92 | it("should allow assertions on object's properties", function () { 93 | var expectedTypes = { 94 | properties: { 95 | url: { 96 | type: "string" 97 | } 98 | } 99 | }; 100 | return expect(getRequest).to.have.schema(expectedTypes); 101 | }); 102 | 103 | }); 104 | 105 | describe("arrays", function () { 106 | 107 | it("should assert types in json arrays", function () { 108 | var mixedArray = { 109 | items: { 110 | type: ["string", "boolean", "number"] 111 | } 112 | }; 113 | var stringArray = { 114 | items: { 115 | type: "string" 116 | } 117 | }; 118 | return chakram.waitFor([ 119 | expect(postRequest).to.have.schema('json.stringArray', stringArray), 120 | expect(postRequest).to.have.schema('json.stringArray', mixedArray), 121 | expect(postRequest).not.to.have.schema('json.mixedArray', stringArray), 122 | expect(postRequest).to.have.schema('json.mixedArray', mixedArray), 123 | ]); 124 | }); 125 | 126 | it("should allow assertions on array's items", function () { 127 | var expectStringsToBeTestWithNumber = { 128 | items: { 129 | pattern: /test\d/ 130 | } 131 | }; 132 | var expectPersonArray = { 133 | items: { 134 | properties: { 135 | name: { type: "string" }, 136 | age: { 137 | type: "integer", 138 | minimum: 0 139 | } 140 | } 141 | } 142 | }; 143 | return chakram.waitFor([ 144 | expect(postRequest).to.have.schema('json.stringArray', expectStringsToBeTestWithNumber), 145 | expect(postRequest).not.to.have.schema('json.mixedArray', expectStringsToBeTestWithNumber), 146 | expect(postRequest).to.have.schema('json.objectArray', expectPersonArray) 147 | ]); 148 | }); 149 | 150 | it("should assert array length", function () { 151 | expect(postRequest).to.have.schema('json.stringArray', {minItems: 0, maxItems: 5}); 152 | expect(postRequest).to.have.schema('json.stringArray', {maxItems: 5}); 153 | expect(postRequest).to.have.schema('json.stringArray', {minItems: 3, maxItems: 5}); 154 | expect(postRequest).not.to.have.schema('json.stringArray', {minItems: 4, maxItems: 5}); 155 | expect(postRequest).not.to.have.schema('json.stringArray', {minItems: 1, maxItems: 2}); 156 | return chakram.wait(); 157 | }); 158 | 159 | it("should assert unique items in array", function () { 160 | expect(postRequest).to.have.schema('json.stringArray', {uniqueItems:true}); 161 | expect(postRequest).to.have.schema('json.mixedArray', {uniqueItems:false}); 162 | expect(postRequest).not.to.have.schema('json.mixedArray', {uniqueItems:true}); 163 | return chakram.wait(); 164 | }); 165 | 166 | }); 167 | 168 | describe("numbers", function() { 169 | 170 | it("should assert number min and max values", function () { 171 | expect(postRequest).to.have.schema('json.number', {minimum:0, maximum:100}); 172 | expect(postRequest).to.have.schema('json.number', {maximum:100}); 173 | expect(postRequest).to.have.schema('json.number', {minimum:19, maximum:21}); 174 | expect(postRequest).to.have.schema('json.number', {minimum:20, maximum:21}); 175 | expect(postRequest).not.to.have.schema('json.number', {minimum:20, maximum:21, exclusiveMinimum:true}); 176 | expect(postRequest).to.have.schema('json.number', {minimum:19, maximum:20}); 177 | expect(postRequest).not.to.have.schema('json.number', {minimum:19, maximum:20, exclusiveMaximum:true}); 178 | expect(postRequest).not.to.have.schema('json.number', {minimum:1, maximum:5}); 179 | return chakram.wait(); 180 | }); 181 | 182 | }); 183 | 184 | describe("strings", function() { 185 | 186 | it("should assert string matches regex", function () { 187 | expect(postRequest).to.have.schema('json.str', {pattern: /test/}); 188 | expect(postRequest).to.have.schema('json.str', {pattern: /str/}); 189 | expect(postRequest).to.have.schema('json.str', {pattern: /est\sst/}); 190 | expect(postRequest).not.to.have.schema('json.str', {pattern: /string/}); 191 | expect(postRequest).not.to.have.schema('json.str', {pattern: /\d/}); 192 | return chakram.wait(); 193 | }); 194 | 195 | it("should assert string length", function () { 196 | expect(postRequest).to.have.schema('json.str', {minLength: 0, maxLength: 100}); 197 | expect(postRequest).not.to.have.schema('json.str', {maxLength: 5}); 198 | expect(postRequest).not.to.have.schema('json.str', {minLength: 50}); 199 | return chakram.wait(); 200 | }); 201 | 202 | }); 203 | 204 | }); 205 | }); --------------------------------------------------------------------------------