├── .gitignore ├── index.js ├── package.json ├── LICENSE ├── README.md ├── test ├── test.js └── test-data-builder.test.js ├── CHANGES.md ├── lib ├── test-data-builder.js └── helpers.js └── CONTRIBUTING.md /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .project 3 | .DS_Store 4 | *.sublime* 5 | *.seed 6 | *.log 7 | *.csv 8 | *.dat 9 | *.out 10 | *.pid 11 | *.swp 12 | *.swo 13 | node_modules 14 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2013. All Rights Reserved. 2 | // Node module: loopback-testing 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | var helpers = require('./lib/helpers'); 7 | exports.describe = helpers.describe; 8 | exports.it = helpers.it; 9 | exports.beforeEach = helpers.beforeEach; 10 | exports.TestDataBuilder = require('./lib/test-data-builder'); 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "loopback-testing", 3 | "version": "1.4.0", 4 | "description": "Utilities for testing LoopBack applications", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "mocha" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/strongloop/loopback-testing" 12 | }, 13 | "author": "Ritchie Martori", 14 | "dependencies": { 15 | "async": "^2.0.1", 16 | "chai": "^3.5.0", 17 | "supertest": "^2.0.0" 18 | }, 19 | "devDependencies": { 20 | "loopback": "^2.34.0", 21 | "mocha": "^3.0.2" 22 | }, 23 | "license": "MIT" 24 | } 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) IBM Corp. 2013,2015. All Rights Reserved. 2 | Node module: loopback-testing 3 | This project is licensed under the MIT License, full text below. 4 | 5 | -------- 6 | 7 | MIT license 8 | 9 | Permission is hereby granted, free of charge, to any person obtaining a copy 10 | of this software and associated documentation files (the "Software"), to deal 11 | in the Software without restriction, including without limitation the rights 12 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 13 | copies of the Software, and to permit persons to whom the Software is 14 | furnished to do so, subject to the following conditions: 15 | 16 | The above copyright notice and this permission notice shall be included in 17 | all copies or substantial portions of the Software. 18 | 19 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 20 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 21 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 22 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 23 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 24 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 25 | THE SOFTWARE. 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DEPRECATED 2 | 3 | This module is no longer maintained by StrongLoop/IBM. 4 | 5 | We are encouraging the community to take the ownership of this module. 6 | Please open an issue in https://github.com/strongloop/loopback if you 7 | are willing to become the new maintainer. 8 | 9 | See also https://groups.google.com/forum/#!topic/loopbackjs/w038RvqHeYI 10 | 11 | --- 12 | 13 | #loopback-testing 14 | 15 | **Utilities for testing LoopBack apps** 16 | 17 | ## overview 18 | 19 | The following helpers are designed to generate [mocha] tests against 20 | [LoopBack](http://strongloop.com/loopback) apps. 21 | 22 | ## install 23 | 24 | 1. `npm install loopback-testing --save-dev` 25 | 2. Assuming you started with a clean template/project generated by `slc loopback` 26 | 1. If you have mocha installed as a global npm module that's great! Simply update `/package.json` with: 27 | 28 | ``` 29 | { 30 | ... 31 | "scripts": { 32 | ... 33 | "test": "mocha" 34 | } 35 | } 36 | ``` 37 | 2. Otherwise, you can utilize the mocha library within the `loopback-testing` testing module: 38 | 39 | ``` 40 | { 41 | ... 42 | "scripts": { 43 | ... 44 | "test": "./node_modules/loopback-testing/node_modules/.bin/mocha" 45 | } 46 | } 47 | ``` 48 | 3. Run `npm test` to execute any tests under the `test` directory. 49 | 50 | ## basic usage 51 | 52 | Below is a simple LoopBack app. 53 | 54 | ```js 55 | var loopback = require('loopback'); 56 | var app = loopback(); 57 | var Product = app.model('product'); 58 | Product.attachTo(loopback.memory()); 59 | ``` 60 | 61 | Use the `loopback-testing` module to generate `mocha` tests. 62 | 63 | ```js 64 | var lt = require('loopback-testing'); 65 | var assert = require('assert'); 66 | var app = require('../server/server.js'); //path to app.js or server.js 67 | 68 | describe('/products', function() { 69 | lt.beforeEach.withApp(app); 70 | lt.describe.whenCalledRemotely('GET', '/products', function() { 71 | lt.it.shouldBeAllowed(); 72 | it('should have statusCode 200', function() { 73 | assert.equal(this.res.statusCode, 200); 74 | }); 75 | 76 | lt.beforeEach.givenModel('product'); 77 | it('should respond with an array of products', function() { 78 | assert(Array.isArray(this.res.body)); 79 | }); 80 | }); 81 | }); 82 | ``` 83 | 84 | ## building test data 85 | 86 | Use TestDataBuilder to build many Model instances in one async call. Specify 87 | only properties relevant to your test, the builder will pre-fill remaining 88 | required properties with sensible defaults. 89 | 90 | ```js 91 | var TestDataBuilder = require('loopback-testing').TestDataBuilder; 92 | var ref = TestDataBuilder.ref; 93 | 94 | // The context object to hold the created models. 95 | // You can use `this` in mocha test instead. 96 | var context = {}; 97 | 98 | var ref = TestDataBuilder.ref; 99 | new TestDataBuilder() 100 | .define('application', Application, { 101 | pushSettings: { stub: { } } 102 | }) 103 | .define('device', Device, { 104 | // use the value of application's id 105 | // the value is resolved at build time 106 | appId: ref('application.id'), 107 | deviceType: 'android' 108 | }) 109 | .define('notification', Notification) 110 | .buildTo(context, function(err) { 111 | // test models are available as 112 | // context.application 113 | // context.device 114 | // context.notification 115 | }); 116 | ``` 117 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2013,2015. All Rights Reserved. 2 | // Node module: loopback-testing 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | var loopback = require('loopback'); 7 | var helpers = require('../'); 8 | var assert = require('assert'); 9 | 10 | describe('helpers', function () { 11 | var testApp = loopback(); 12 | var db = testApp.dataSource('db', {connector: loopback.Memory}); 13 | var testModel = testApp.model('xxx-test-model', {dataSource: 'db'}); 14 | 15 | testApp.use(loopback.rest()); 16 | helpers.beforeEach.withApp(testApp); 17 | 18 | describe('helpers.it', function() { 19 | ['shouldBeAllowed', 20 | 'shouldBeDenied', 21 | 'shouldNotBeFound', 22 | 'shouldBeAllowedWhenCalledAnonymously', 23 | 'shouldBeDeniedWhenCalledAnonymously', 24 | 'shouldBeAllowedWhenCalledUnauthenticated', 25 | 'shouldBeDeniedWhenCalledUnauthenticated', 26 | 'shouldBeAllowedWhenCalledByUser', 27 | 'shouldBeDeniedWhenCalledByUser'] 28 | .forEach(function(func) { 29 | it('should have a method named ' + func, function () { 30 | assert.equal(typeof helpers.it[func], 'function'); 31 | }); 32 | }); 33 | }); 34 | 35 | describe('helpers.describe', function() { 36 | ['staticMethod', 37 | 'instanceMethod', 38 | 'whenLoggedInAsUser', 39 | 'whenCalledByUser', 40 | 'whenCalledAnonymously', 41 | 'whenCalledUnauthenticated'] 42 | .forEach(function(func) { 43 | it('should have a method named ' + func, function () { 44 | assert.equal(typeof helpers.describe[func], 'function'); 45 | }); 46 | }); 47 | }); 48 | 49 | describe('helpers.beforeEach', function() { 50 | ['withArgs', 51 | 'givenModel', 52 | 'givenUser', 53 | 'givenLoggedInUser', 54 | 'givenAnUnauthenticatedToken', 55 | 'givenAnAnonymousToken'] 56 | .forEach(function(func) { 57 | it('should have a helper method named ' + func, function () { 58 | assert.equal(typeof helpers.beforeEach[func], 'function'); 59 | }); 60 | }); 61 | }); 62 | 63 | describe('helpers.beforeEach.givenModel', function() { 64 | helpers.beforeEach.givenModel('xxx-test-model'); 65 | it('should have an xxx-test-model property', function () { 66 | assert(this['xxx-test-model']); 67 | assert(this['xxx-test-model'].id); 68 | }); 69 | }); 70 | 71 | describe('whenCalledRemotely', function() { 72 | helpers.describe.staticMethod('create', function() { 73 | helpers.beforeEach.withArgs({foo: 'bar'}); 74 | helpers.describe.whenCalledRemotely('POST', '/xxx-test-models', function() { 75 | it('should call the method over rest', function () { 76 | assert.equal(this.res.statusCode, 200); 77 | }); 78 | }); 79 | }); 80 | helpers.describe.staticMethod('findById', function() { 81 | helpers.beforeEach.givenModel('xxx-test-model', {foo: 'bar'}); 82 | helpers.describe.whenCalledRemotely('GET', function () { 83 | return '/xxx-test-models/' + this['xxx-test-model'].id; 84 | }, function() { 85 | it('should retrieve the expected model in the first test', function () { 86 | assert.equal(this.res.body.id, this['xxx-test-model'].id); 87 | }); 88 | it('should retrieve the expected model in subsequent tests', function () { 89 | assert.equal(this.res.body.id, this['xxx-test-model'].id); 90 | }); 91 | }); 92 | }); 93 | }); 94 | 95 | describe('cleanDatasource', function() { 96 | helpers.describe.staticMethod('create', function() { 97 | helpers.beforeEach.withArgs({foo: 'bar'}); 98 | helpers.describe.whenCalledRemotely('POST', '/xxx-test-models', function() { 99 | it('should call the method over rest', function () { 100 | assert.equal(this.res.statusCode, 200); 101 | }); 102 | }); 103 | }); 104 | 105 | helpers.describe.staticMethod('findById', function() { 106 | helpers.beforeEach.givenModel('xxx-test-model', {foo: 'bar'}); 107 | helpers.beforeEach.cleanDatasource(); 108 | helpers.describe.whenCalledRemotely('GET', function () { 109 | return '/xxx-test-models/' + this['xxx-test-model'].id; 110 | }, function() { 111 | it('should not find the given model', function () { 112 | assert.equal(this.res.statusCode, 404); 113 | }); 114 | }); 115 | }); 116 | }); 117 | }); 118 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | 2016-09-12, Version 1.4.0 2 | ========================= 3 | 4 | * Update dependencies to the latest version (Miroslav Bajtoš) 5 | 6 | 7 | 2016-08-10, Version 1.3.0 8 | ========================= 9 | 10 | * Update deprecation notice (Miroslav Bajtoš) 11 | 12 | * Update URLs in CONTRIBUTING.md (#73) (Ryan Graham) 13 | 14 | * update copyright notices and license (Ryan Graham) 15 | 16 | * Updates (Simon Ho) 17 | 18 | * Update README.md (Simon Ho) 19 | 20 | * Refer to licenses with a link (Sam Roberts) 21 | 22 | * Use strongloop conventions for licensing (Sam Roberts) 23 | 24 | * Move mocha to dev deps (Ritchie Martori) 25 | 26 | * Wait for automigrate to finish (Vlad Miller) 27 | 28 | * Replace call of login method on constructor (Aris Kemper) 29 | 30 | 31 | 2015-06-24, Version 1.2.0 32 | ========================= 33 | 34 | * Respect model property length for TDB strings (zero5100) 35 | 36 | * use object to support placeholder pattern for when called remotely (ningsuhen) 37 | 38 | * use findorCreate to create roles (ningsuhen) 39 | 40 | * Initial withUserModel feature (Ryan Schumacher) 41 | 42 | * Update helpers.js (Vladyslav Millier) 43 | 44 | 45 | 2015-02-23, Version 1.1.0 46 | ========================= 47 | 48 | * Add done callback (Ryan Schumacher) 49 | 50 | * Add clean datasource feature (Ryan Schumacher) 51 | 52 | 53 | 2015-01-13, Version 1.0.5 54 | ========================= 55 | 56 | * Throw err when beforeEach.withApp is not called (Farid Neshat) 57 | 58 | * Fix bad CLA URL in CONTRIBUTING.md (Ryan Graham) 59 | 60 | 61 | 2014-12-05, Version 1.0.4 62 | ========================= 63 | 64 | * README: add basic mocha setup instructions (pulkitsinghal) 65 | 66 | 67 | 2014-10-02, Version 1.0.3 68 | ========================= 69 | 70 | * Update version to 1.0.3 (Simon Ho) 71 | 72 | 73 | 2014-10-02, Version 1.0.2 74 | ========================= 75 | 76 | 77 | 78 | 2014-10-02, Version 1.0.1 79 | ========================= 80 | 81 | * package: move chai from devDependencies to dependencies (Simon Ho) 82 | 83 | * Add contribution guidelines (Ryan Graham) 84 | 85 | 86 | 2014-09-26, Version 1.0.0 87 | ========================= 88 | 89 | * Upgrade dependencies (Miroslav Bajtoš) 90 | 91 | * add support for testing with users with roles (Jaka Hudoklin) 92 | 93 | * fix imports (Jaka Hudoklin) 94 | 95 | 96 | 2014-09-25, Version 0.2.1 97 | ========================= 98 | 99 | * Fix handling of dynamic URLs in the test builder (Clark Wang) 100 | 101 | * Extending tests to cover other methods (ariskemper) 102 | 103 | * package: add repository URL (Miroslav Bajtoš) 104 | 105 | 106 | 2014-06-12, Version 0.2.0 107 | ========================= 108 | 109 | * helpers: add parameter for request body (Miroslav Bajtoš) 110 | 111 | * helpers: Improve ACL asserts (Miroslav Bajtoš) 112 | 113 | * helpers: fix usage of mocha contexts (Miroslav Bajtoš) 114 | 115 | * Drop peer dependency on loopback (Miroslav Bajtoš) 116 | 117 | * un-used variable (Karl Mikkelsen) 118 | 119 | * tighter status constraints (Karl Mikkelsen) 120 | 121 | * extend range of status allowed/denied (Karl Mikkelsen) 122 | 123 | 124 | 2014-06-06, Version 0.1.5 125 | ========================= 126 | 127 | * Support loopback 2.x (Miroslav Bajtoš) 128 | 129 | * Bump loopback dependency (Ritchie Martori) 130 | 131 | 132 | 2014-05-25, Version 0.1.3 133 | ========================= 134 | 135 | * Remove juggler from peerDependencies. (Miroslav Bajtoš) 136 | 137 | * Update to dual MIT/StrongLoop license (Raymond Feng) 138 | 139 | 140 | 2014-02-11, Version 0.1.2 141 | ========================= 142 | 143 | * Bump version and update deps (Raymond Feng) 144 | 145 | 146 | 2014-01-13, Version 0.1.1 147 | ========================= 148 | 149 | * Speed up login action (Miroslav Bajtoš) 150 | 151 | * Fix function typo (Ritchie Martori) 152 | 153 | 154 | 2013-12-20, Version 0.1.0 155 | ========================= 156 | 157 | * Relax loopback dependency (Ritchie Martori) 158 | 159 | * Factor out LoopBack dep to removing coupling on the exact loopback instance. (Ritchie Martori) 160 | 161 | * Remove describe.model (Ritchie Martori) 162 | 163 | * Remove beforeEach.withDefaultDataSource (Ritchie Martori) 164 | 165 | * Move helpers to lib (Ritchie Martori) 166 | 167 | 168 | 2013-12-13, Version 0.0.4 169 | ========================= 170 | 171 | * Bump version (Ritchie Martori) 172 | 173 | * Move loopback to peerDependencies to avoid using the wrong loopback object (Ritchie Martori) 174 | 175 | * Describe TestDataBuilder in README. (Miroslav Bajtos) 176 | 177 | 178 | 2013-12-11, Version 0.0.3 179 | ========================= 180 | 181 | * First release! 182 | -------------------------------------------------------------------------------- /test/test-data-builder.test.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2013,2015. All Rights Reserved. 2 | // Node module: loopback-testing 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | var loopback = require('loopback'); 7 | var TestDataBuilder = require('../lib/test-data-builder'); 8 | var expect = require('chai').expect; 9 | 10 | describe('TestDataBuilder', function() { 11 | var db; 12 | var TestModel; 13 | 14 | beforeEach(function() { 15 | db = loopback.createDataSource({ connector: loopback.Memory }); 16 | }); 17 | 18 | it('builds a model', function(done) { 19 | givenTestModel({ value: String }); 20 | 21 | new TestDataBuilder() 22 | .define('model', TestModel, { value: 'a-string-value' }) 23 | .buildTo(this, function(err) { 24 | if (err) return done(err); 25 | expect(this.model).to.have.property('value', 'a-string-value'); 26 | done(); 27 | }.bind(this)); 28 | }); 29 | 30 | // Parameterized test 31 | function itAutoFillsRequiredPropertiesWithUniqueValuesFor(type) { 32 | it( 33 | 'auto-fills required ' + type + ' properties with unique values', 34 | function(done) { 35 | givenTestModel({ 36 | required1: { type: type, required: true }, 37 | required2: { type: type, required: true } 38 | }); 39 | 40 | new TestDataBuilder() 41 | .define('model', TestModel, {}) 42 | .buildTo(this, function(err) { 43 | if (err) return done(err); 44 | expect(this.model.required1).to.not.equal(this.model.required2); 45 | expect(this.model.optional).to.satisfy(notSet); 46 | done(); 47 | }.bind(this)); 48 | } 49 | ); 50 | } 51 | 52 | itAutoFillsRequiredPropertiesWithUniqueValuesFor(String); 53 | itAutoFillsRequiredPropertiesWithUniqueValuesFor(Number); 54 | itAutoFillsRequiredPropertiesWithUniqueValuesFor(Date); 55 | 56 | it('limits the length of the generated string value to the property length', function(done) { 57 | var testMaxStringLength = 10; 58 | givenTestModel({ 59 | required1: { type: String, required: true }, 60 | required2: { type: String, required: true, length: testMaxStringLength } 61 | }); 62 | 63 | new TestDataBuilder() 64 | .define('model', TestModel, {}) 65 | .buildTo(this, function(err) { 66 | if (err) return done(err); 67 | expect(this.model.required1).to.not.equal(this.model.required2); 68 | expect(this.model.required2).to.have.length.of.at.most(testMaxStringLength); 69 | done(); 70 | }.bind(this)); 71 | }); 72 | 73 | it('auto-fills required Boolean properties with false', function(done) { 74 | givenTestModel({ 75 | required: { type: Boolean, required: true } 76 | }); 77 | 78 | new TestDataBuilder() 79 | .define('model', TestModel, {}) 80 | .buildTo(this, function(err) { 81 | if (err) return done(err); 82 | expect(this.model.required).to.equal(false); 83 | done(); 84 | }.bind(this)); 85 | }); 86 | 87 | it('does not fill optional properties', function(done) { 88 | givenTestModel({ 89 | optional: { type: String, required: false } 90 | }); 91 | 92 | new TestDataBuilder() 93 | .define('model', TestModel, {}) 94 | .buildTo(this, function(err) { 95 | if (err) return done(err); 96 | expect(this.model.optional).to.satisfy(notSet); 97 | done(); 98 | }.bind(this)); 99 | }); 100 | 101 | it('resolves references', function(done) { 102 | var Parent = givenModel('Parent', { name: { type: String, required: true } }); 103 | var Child = givenModel('Child', { parentName: String }); 104 | 105 | new TestDataBuilder() 106 | .define('parent', Parent) 107 | .define('child', Child, { 108 | parentName: TestDataBuilder.ref('parent.name') 109 | }) 110 | .buildTo(this, function(err) { 111 | if(err) return done(err); 112 | expect(this.child.parentName).to.equal(this.parent.name); 113 | done(); 114 | }.bind(this)); 115 | }); 116 | 117 | function givenTestModel(properties) { 118 | TestModel = givenModel('TestModel', properties); 119 | } 120 | 121 | function givenModel(name, properties) { 122 | var ModelCtor = loopback.createModel(name, properties); 123 | ModelCtor.attachTo(db); 124 | return ModelCtor; 125 | } 126 | 127 | function notSet(value) { 128 | // workaround for `expect().to.exist` that triggers a JSHint error 129 | // (a no-op statement discarding the property value) 130 | return value === undefined || value === null; 131 | } 132 | }); 133 | -------------------------------------------------------------------------------- /lib/test-data-builder.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2013,2015. All Rights Reserved. 2 | // Node module: loopback-testing 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | var extend = require('util')._extend; 7 | var async = require('async'); 8 | 9 | module.exports = exports = TestDataBuilder; 10 | 11 | /** 12 | * Build many Model instances in one async call. 13 | * 14 | * Usage: 15 | * ```js 16 | * // The context object to hold the created models. 17 | * // You can use `this` in mocha test instead. 18 | * var context = {}; 19 | * 20 | * var ref = TestDataBuilder.ref; 21 | * new TestDataBuilder() 22 | * .define('application', Application, { 23 | * pushSettings: { stub: { } } 24 | * }) 25 | * .define('device', Device, { 26 | * appId: ref('application.id'), 27 | * deviceType: 'android' 28 | * }) 29 | * .define('notification', Notification) 30 | * .buildTo(context, function(err) { 31 | * // test models are available as 32 | * // context.application 33 | * // context.device 34 | * // context.notification 35 | * }); 36 | * ``` 37 | * @constructor 38 | */ 39 | function TestDataBuilder() { 40 | this._definitions = []; 41 | } 42 | 43 | /** 44 | * Define a new model instance. 45 | * @param {string} name Name of the instance. 46 | * `buildTo()` will save the instance created as context[name]. 47 | * @param {constructor} Model Model class/constructor. 48 | * @param {Object.=} properties 49 | * Properties to set in the object. 50 | * Intelligent default values are supplied by the builder 51 | * for required properties not listed. 52 | * @return TestDataBuilder (fluent interface) 53 | */ 54 | TestDataBuilder.prototype.define = function(name, Model, properties) { 55 | this._definitions.push({ 56 | name: name, 57 | model: Model, 58 | properties: properties 59 | }); 60 | return this; 61 | }; 62 | 63 | /** 64 | * Reference the value of a property from a model instance defined before. 65 | * @param {string} path Generally in the form '{name}.{property}', where {name} 66 | * is the name passed to `define()` and {property} is the name of 67 | * the property to use. 68 | */ 69 | TestDataBuilder.ref = function(path) { 70 | return new Reference(path); 71 | }; 72 | 73 | /** 74 | * Asynchronously build all models defined via `define()` and save them in 75 | * the supplied context object. 76 | * @param {Object.} context The context to object to populate. 77 | * @param {function(Error)} callback Callback. 78 | */ 79 | TestDataBuilder.prototype.buildTo = function(context, callback) { 80 | this._context = context; 81 | async.eachSeries( 82 | this._definitions, 83 | this._buildObject.bind(this), 84 | callback); 85 | }; 86 | 87 | TestDataBuilder.prototype._buildObject = function(definition, callback) { 88 | var defaultValues = this._gatherDefaultPropertyValues(definition.model); 89 | var values = extend(defaultValues, definition.properties || {}); 90 | var resolvedValues = this._resolveValues(values); 91 | 92 | definition.model.create(resolvedValues, function(err, result) { 93 | if (err) { 94 | console.error( 95 | 'Cannot build object %j - %s\nDetails: %j', 96 | definition, 97 | err.message, 98 | err.details); 99 | } else { 100 | this._context[definition.name] = result; 101 | } 102 | 103 | callback(err); 104 | }.bind(this)); 105 | }; 106 | 107 | TestDataBuilder.prototype._resolveValues = function(values) { 108 | var result = {}; 109 | for (var key in values) { 110 | var val = values[key]; 111 | if (val instanceof Reference) { 112 | val = values[key].resolveFromContext(this._context); 113 | } 114 | result[key] = val; 115 | } 116 | return result; 117 | }; 118 | 119 | var valueCounter = 0; 120 | TestDataBuilder.prototype._gatherDefaultPropertyValues = function(Model) { 121 | var result = {}; 122 | Model.forEachProperty(function createDefaultPropertyValue(name) { 123 | var prop = Model.definition.properties[name]; 124 | if (!prop.required) return; 125 | 126 | switch (prop.type) { 127 | case String: 128 | var generatedString = 'a test ' + name + ' #' + (++valueCounter); 129 | 130 | // If this property has a maximum length, ensure that the generated 131 | // string is not longer than the property's max length 132 | if (prop.length) { 133 | // Chop off the front part of the string so it is equal to the length 134 | generatedString = generatedString.substring( 135 | generatedString.length - prop.length); 136 | } 137 | result[name] = generatedString; 138 | break; 139 | case Number: 140 | result[name] = 1230000 + (++valueCounter); 141 | break; 142 | case Date: 143 | result[name] = new Date( 144 | 2222, 12, 12, // yyyy, mm, dd 145 | 12, 12, 12, // hh, MM, ss 146 | ++valueCounter // milliseconds 147 | ); 148 | break; 149 | case Boolean: 150 | // There isn't much choice here, is it? 151 | // Let's use "false" to encourage users to be explicit when they 152 | // require "true" to turn some flag/behaviour on 153 | result[name] = false; 154 | break; 155 | // TODO: support nested structures - array, object 156 | } 157 | }); 158 | return result; 159 | }; 160 | 161 | /** 162 | * Placeholder for values that will be resolved during build. 163 | * @param path 164 | * @constructor 165 | * @private 166 | */ 167 | function Reference(path) { 168 | this._path = path; 169 | } 170 | 171 | Reference.prototype.resolveFromContext = function(context) { 172 | var elements = this._path.split('.'); 173 | 174 | var result = elements.reduce( 175 | function(obj, prop) { 176 | return obj[prop]; 177 | }, 178 | context 179 | ); 180 | 181 | return result; 182 | }; 183 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ### Contributing ### 2 | 3 | Thank you for your interest in `loopback-testing`, an open source project 4 | administered by StrongLoop. 5 | 6 | Contributing to `loopback-testing` is easy. In a few simple steps: 7 | 8 | * Ensure that your effort is aligned with the project's roadmap by 9 | talking to the maintainers, especially if you are going to spend a 10 | lot of time on it. 11 | 12 | * Make something better or fix a bug. 13 | 14 | * Adhere to code style outlined in the [Google C++ Style Guide][] and 15 | [Google Javascript Style Guide][]. 16 | 17 | * Sign the [Contributor License Agreement](https://cla.strongloop.com/agreements/strongloop/loopback-testing) 18 | 19 | * Submit a pull request through Github. 20 | 21 | 22 | ### Contributor License Agreement ### 23 | 24 | ``` 25 | Individual Contributor License Agreement 26 | 27 | By signing this Individual Contributor License Agreement 28 | ("Agreement"), and making a Contribution (as defined below) to 29 | StrongLoop, Inc. ("StrongLoop"), You (as defined below) accept and 30 | agree to the following terms and conditions for Your present and 31 | future Contributions submitted to StrongLoop. Except for the license 32 | granted in this Agreement to StrongLoop and recipients of software 33 | distributed by StrongLoop, You reserve all right, title, and interest 34 | in and to Your Contributions. 35 | 36 | 1. Definitions 37 | 38 | "You" or "Your" shall mean the copyright owner or the individual 39 | authorized by the copyright owner that is entering into this 40 | Agreement with StrongLoop. 41 | 42 | "Contribution" shall mean any original work of authorship, 43 | including any modifications or additions to an existing work, that 44 | is intentionally submitted by You to StrongLoop for inclusion in, 45 | or documentation of, any of the products owned or managed by 46 | StrongLoop ("Work"). For purposes of this definition, "submitted" 47 | means any form of electronic, verbal, or written communication 48 | sent to StrongLoop or its representatives, including but not 49 | limited to communication or electronic mailing lists, source code 50 | control systems, and issue tracking systems that are managed by, 51 | or on behalf of, StrongLoop for the purpose of discussing and 52 | improving the Work, but excluding communication that is 53 | conspicuously marked or otherwise designated in writing by You as 54 | "Not a Contribution." 55 | 56 | 2. You Grant a Copyright License to StrongLoop 57 | 58 | Subject to the terms and conditions of this Agreement, You hereby 59 | grant to StrongLoop and recipients of software distributed by 60 | StrongLoop, a perpetual, worldwide, non-exclusive, no-charge, 61 | royalty-free, irrevocable copyright license to reproduce, prepare 62 | derivative works of, publicly display, publicly perform, 63 | sublicense, and distribute Your Contributions and such derivative 64 | works under any license and without any restrictions. 65 | 66 | 3. You Grant a Patent License to StrongLoop 67 | 68 | Subject to the terms and conditions of this Agreement, You hereby 69 | grant to StrongLoop and to recipients of software distributed by 70 | StrongLoop a perpetual, worldwide, non-exclusive, no-charge, 71 | royalty-free, irrevocable (except as stated in this Section) 72 | patent license to make, have made, use, offer to sell, sell, 73 | import, and otherwise transfer the Work under any license and 74 | without any restrictions. The patent license You grant to 75 | StrongLoop under this Section applies only to those patent claims 76 | licensable by You that are necessarily infringed by Your 77 | Contributions(s) alone or by combination of Your Contributions(s) 78 | with the Work to which such Contribution(s) was submitted. If any 79 | entity institutes a patent litigation against You or any other 80 | entity (including a cross-claim or counterclaim in a lawsuit) 81 | alleging that Your Contribution, or the Work to which You have 82 | contributed, constitutes direct or contributory patent 83 | infringement, any patent licenses granted to that entity under 84 | this Agreement for that Contribution or Work shall terminate as 85 | of the date such litigation is filed. 86 | 87 | 4. You Have the Right to Grant Licenses to StrongLoop 88 | 89 | You represent that You are legally entitled to grant the licenses 90 | in this Agreement. 91 | 92 | If Your employer(s) has rights to intellectual property that You 93 | create, You represent that You have received permission to make 94 | the Contributions on behalf of that employer, that Your employer 95 | has waived such rights for Your Contributions, or that Your 96 | employer has executed a separate Corporate Contributor License 97 | Agreement with StrongLoop. 98 | 99 | 5. The Contributions Are Your Original Work 100 | 101 | You represent that each of Your Contributions are Your original 102 | works of authorship (see Section 8 (Submissions on Behalf of 103 | Others) for submission on behalf of others). You represent that to 104 | Your knowledge, no other person claims, or has the right to claim, 105 | any right in any intellectual property right related to Your 106 | Contributions. 107 | 108 | You also represent that You are not legally obligated, whether by 109 | entering into an agreement or otherwise, in any way that conflicts 110 | with the terms of this Agreement. 111 | 112 | You represent that Your Contribution submissions include complete 113 | details of any third-party license or other restriction (including, 114 | but not limited to, related patents and trademarks) of which You 115 | are personally aware and which are associated with any part of 116 | Your Contributions. 117 | 118 | 6. You Don't Have an Obligation to Provide Support for Your Contributions 119 | 120 | You are not expected to provide support for Your Contributions, 121 | except to the extent You desire to provide support. You may provide 122 | support for free, for a fee, or not at all. 123 | 124 | 6. No Warranties or Conditions 125 | 126 | StrongLoop acknowledges that unless required by applicable law or 127 | agreed to in writing, You provide Your Contributions on an "AS IS" 128 | BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER 129 | EXPRESS OR IMPLIED, INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES 130 | OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY, OR 131 | FITNESS FOR A PARTICULAR PURPOSE. 132 | 133 | 7. Submission on Behalf of Others 134 | 135 | If You wish to submit work that is not Your original creation, You 136 | may submit it to StrongLoop separately from any Contribution, 137 | identifying the complete details of its source and of any license 138 | or other restriction (including, but not limited to, related 139 | patents, trademarks, and license agreements) of which You are 140 | personally aware, and conspicuously marking the work as 141 | "Submitted on Behalf of a Third-Party: [named here]". 142 | 143 | 8. Agree to Notify of Change of Circumstances 144 | 145 | You agree to notify StrongLoop of any facts or circumstances of 146 | which You become aware that would make these representations 147 | inaccurate in any respect. Email us at callback@strongloop.com. 148 | ``` 149 | 150 | [Google C++ Style Guide]: https://google.github.io/styleguide/cppguide.html 151 | [Google Javascript Style Guide]: https://google.github.io/styleguide/javascriptguide.xml 152 | -------------------------------------------------------------------------------- /lib/helpers.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2013,2015. All Rights Reserved. 2 | // Node module: loopback-testing 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | 6 | var _describe = {}; 7 | var _it = {}; 8 | var _beforeEach = {}; 9 | var helpers = exports = module.exports = { 10 | describe: _describe, 11 | it: _it, 12 | beforeEach: _beforeEach 13 | }; 14 | var assert = require('assert'); 15 | var request = require('supertest'); 16 | var expect = require('chai').expect; 17 | 18 | _beforeEach.withApp = function(app) { 19 | if (app.models.User) { 20 | // Speed up the password hashing algorithm 21 | app.models.User.settings.saltWorkFactor = 4; 22 | } 23 | 24 | beforeEach(function() { 25 | this.app = app; 26 | var _request = this.request = request(app); 27 | this.post = _request.post; 28 | this.get = _request.get; 29 | this.put = _request.put; 30 | this.del = _request.del; 31 | }); 32 | } 33 | 34 | _beforeEach.cleanDatasource = function(dsName) { 35 | beforeEach(function(done) { 36 | if(!dsName) dsName = 'db'; 37 | 38 | if (typeof this.app === 'function' 39 | && typeof this.app.datasources === 'object' 40 | && typeof this.app.datasources[dsName] === 'object') { 41 | this.app.datasources[dsName].automigrate(done); 42 | this.app.datasources[dsName].connector.ids = {}; 43 | } else { 44 | done(); 45 | } 46 | }); 47 | } 48 | 49 | function mixin(obj, into) { 50 | Object.keys(obj).forEach(function(key) { 51 | if(typeof obj[key] === 'function') { 52 | into[key] = obj[key]; 53 | } 54 | }); 55 | } 56 | 57 | _describe.staticMethod = function(methodName, cb) { 58 | describe('.' + methodName, function() { 59 | beforeEach(function() { 60 | this.method = methodName; 61 | this.isStaticMethod = true; 62 | }); 63 | cb(); 64 | }); 65 | } 66 | 67 | _describe.instanceMethod = function(methodName, cb) { 68 | describe('.prototype.' + methodName, function() { 69 | beforeEach(function() { 70 | this.method = methodName; 71 | this.isInstanceMethod = true; 72 | }); 73 | cb(); 74 | }); 75 | } 76 | 77 | _beforeEach.withArgs = function() { 78 | var args = Array.prototype.slice.call(arguments, 0); 79 | beforeEach(function() { 80 | this.args = args; 81 | }); 82 | } 83 | 84 | _beforeEach.givenModel = function(modelName, attrs, optionalHandler) { 85 | var modelKey = modelName; 86 | 87 | if(typeof attrs === 'function') { 88 | optionalHandler = attrs; 89 | attrs = undefined; 90 | } 91 | 92 | if(typeof optionalHandler === 'string') { 93 | modelKey = optionalHandler; 94 | } 95 | 96 | attrs = attrs || {}; 97 | 98 | beforeEach(function(done) { 99 | if(modelName === '__USERMODEL__') { 100 | modelName = this.userModel ? this.userModel : 'user'; 101 | } 102 | 103 | var test = this; 104 | var app = this.app; 105 | var model = app.models[modelName]; 106 | assert(model, 'cannot get model of name ' + modelName + ' from app.models'); 107 | assert(model.dataSource, 'cannot test model '+ modelName 108 | + ' without attached dataSource'); 109 | assert( 110 | typeof model.create === 'function', 111 | modelName + ' does not have a create method' 112 | ); 113 | 114 | model.create(attrs, function(err, result) { 115 | if(err) { 116 | console.error(err.message); 117 | if(err.details) console.error(err.details); 118 | done(err); 119 | } else { 120 | test[modelKey] = result; 121 | done(); 122 | } 123 | }); 124 | }); 125 | 126 | if(typeof optionalHandler === 'function') { 127 | beforeEach(optionalHandler); 128 | } 129 | 130 | afterEach(function(done) { 131 | this[modelKey].destroy(done); 132 | }); 133 | } 134 | 135 | _beforeEach.withUserModel = function(model) { 136 | beforeEach(function(done) { 137 | this.userModel = model; 138 | done(); 139 | }); 140 | }; 141 | 142 | _beforeEach.givenUser = function(attrs, optionalHandler) { 143 | _beforeEach.givenModel('__USERMODEL__', attrs, optionalHandler); 144 | } 145 | 146 | _beforeEach.givenUserWithRole = function (attrs, role, optionalHandler) { 147 | if (typeof role === 'string') { 148 | role = { 149 | name: role 150 | } 151 | } 152 | _beforeEach.givenUser(attrs, function (done) { 153 | var test = this; 154 | test.app.models.Role.findOrCreate({name: role}, function (err, result) { 155 | if(err) { 156 | console.error(err.message); 157 | if(err.details) console.error(err.details); 158 | return done(err); 159 | } 160 | 161 | test.userRole = result; 162 | test.app.models.roleMapping.create( 163 | {principalId: test.user.id, 164 | principalType: test.app.models.roleMapping.USER, 165 | roleId: result.id}, 166 | function (err, result) { 167 | if(err) { 168 | console.error(err.message); 169 | if(err.details) console.error(err.details); 170 | return done(err); 171 | } 172 | 173 | test.userRoleMapping = result; 174 | done(); 175 | } 176 | ); 177 | }); 178 | }); 179 | 180 | if(typeof optionalHandler === 'function') { 181 | beforeEach(optionalHandler); 182 | } 183 | 184 | afterEach(function(done) { 185 | var test = this; 186 | this.userRole.destroy(function(err) { 187 | if(err) return done(err); 188 | test.userRole = undefined; 189 | 190 | test.userRoleMapping.destroy(function(err) { 191 | if(err) return done(err); 192 | test.userRoleMapping = undefined; 193 | done(); 194 | }); 195 | }); 196 | }); 197 | } 198 | 199 | _beforeEach.givenLoggedInUser = function(credentials, optionalHandler) { 200 | _beforeEach.givenUser(credentials, function(done) { 201 | var test = this; 202 | this.app.models[this.userModel].login(credentials, function(err, token) { 203 | if(err) { 204 | done(err); 205 | } else { 206 | test.loggedInAccessToken = token; 207 | done(); 208 | } 209 | }); 210 | }); 211 | 212 | afterEach(function(done) { 213 | var test = this; 214 | this.loggedInAccessToken.destroy(function(err) { 215 | if(err) return done(err); 216 | test.loggedInAccessToken = undefined; 217 | done(); 218 | }); 219 | }); 220 | } 221 | 222 | _beforeEach.givenLoggedInUserWithRole = function(credentials, role, optionalHandler){ 223 | _beforeEach.givenUserWithRole(credentials, role, function(done) { 224 | var test = this; 225 | this.app.models[this.userModel].login(credentials, function(err, token) { 226 | if(err) { 227 | done(err); 228 | } else { 229 | test.loggedInAccessToken = token; 230 | done(); 231 | } 232 | }); 233 | }); 234 | 235 | afterEach(function(done) { 236 | var test = this; 237 | this.loggedInAccessToken.destroy(function(err) { 238 | if(err) return done(err); 239 | test.loggedInAccessToken = undefined; 240 | done(); 241 | }); 242 | }); 243 | } 244 | 245 | _beforeEach.givenAnUnauthenticatedToken = function(attrs, optionalHandler) { 246 | _beforeEach.givenModel('AccessToken', attrs, optionalHandler); 247 | } 248 | 249 | _beforeEach.givenAnAnonymousToken = function(attrs, optionalHandler) { 250 | _beforeEach.givenModel('AccessToken', {id: '$anonymous'}, optionalHandler); 251 | } 252 | 253 | _describe.whenCalledRemotely = function(verb, url, data, cb) { 254 | if (cb == undefined) { 255 | cb = data; 256 | data = null; 257 | } 258 | 259 | var urlStr = url; 260 | if(typeof url === 'function') { 261 | urlStr = '/'; 262 | } 263 | else if(typeof url === 'object' && url.hasOwnProperty('placeHolder')) { 264 | urlStr = url.placeHolder; 265 | } 266 | 267 | describe(verb.toUpperCase() + ' ' + urlStr, function() { 268 | beforeEach(function(cb) { 269 | if(typeof url === 'function') { 270 | this.url = url.call(this); 271 | } 272 | else if(typeof url === 'object' && url.hasOwnProperty('callback')){ 273 | this.url = url.callback.call(this); 274 | } 275 | this.remotely = true; 276 | this.verb = verb.toUpperCase(); 277 | this.url = this.url || url; 278 | var methodForVerb = verb.toLowerCase(); 279 | if(methodForVerb === 'delete') methodForVerb = 'del'; 280 | 281 | if (this.request === undefined) { 282 | throw new Error('App is not specified. Please use lt.beforeEach.withApp to specify the app.'); 283 | } 284 | 285 | this.http = this.request[methodForVerb](this.url); 286 | delete this.url; 287 | this.http.set('Accept', 'application/json'); 288 | if(this.loggedInAccessToken) { 289 | this.http.set('authorization', this.loggedInAccessToken.id); 290 | } 291 | if (data) { 292 | var payload = data; 293 | if (typeof data === 'function') 294 | payload = data.call(this); 295 | this.http.send(payload); 296 | } 297 | this.req = this.http.req; 298 | var test = this; 299 | this.http.end(function(err) { 300 | test.req = test.http.req; 301 | test.res = test.http.res; 302 | delete test.url; 303 | cb(); 304 | }); 305 | }); 306 | 307 | cb(); 308 | }); 309 | } 310 | 311 | _describe.whenLoggedInAsUser = function(credentials, cb) { 312 | describe('when logged in as user', function () { 313 | _beforeEach.givenLoggedInUser(credentials); 314 | cb(); 315 | }); 316 | } 317 | 318 | _describe.whenLoggedInAsUserWithRole = function(credentials, role, cb) { 319 | describe('when logged in as user', function () { 320 | _beforeEach.givenLoggedInUser(credentials, role); 321 | cb(); 322 | }); 323 | } 324 | 325 | _describe.whenCalledByUser = function(credentials, verb, url, data, cb) { 326 | describe('when called by logged in user', function () { 327 | _beforeEach.givenLoggedInUser(credentials); 328 | _describe.whenCalledRemotely(verb, url, data, cb); 329 | }); 330 | } 331 | 332 | _describe.whenCalledByUserWithRole = function (credentials, role, verb, url, data, cb) { 333 | describe('when called by logged in user with role ' + role, function () { 334 | _beforeEach.givenLoggedInUserWithRole(credentials, role); 335 | _describe.whenCalledRemotely(verb, url, data, cb); 336 | }); 337 | } 338 | 339 | _describe.whenCalledAnonymously = function(verb, url, data, cb) { 340 | describe('when called anonymously', function () { 341 | _beforeEach.givenAnAnonymousToken(); 342 | _describe.whenCalledRemotely(verb, url, data, cb); 343 | }); 344 | } 345 | 346 | _describe.whenCalledUnauthenticated = function(verb, url, data, cb) { 347 | describe('when called with unauthenticated token', function () { 348 | _beforeEach.givenAnAnonymousToken(); 349 | _describe.whenCalledRemotely(verb, url, data, cb); 350 | }); 351 | } 352 | 353 | _it.shouldBeAllowed = function() { 354 | it('should be allowed', function() { 355 | assert(this.req); 356 | assert(this.res); 357 | // expect success - status 2xx or 3xx 358 | expect(this.res.statusCode).to.be.within(100, 399); 359 | }); 360 | } 361 | 362 | _it.shouldBeDenied = function() { 363 | it('should not be allowed', function() { 364 | assert(this.res); 365 | var expectedStatus = this.aclErrorStatus || 366 | this.app && this.app.get('aclErrorStatus') || 367 | 401; 368 | expect(this.res.statusCode).to.equal(expectedStatus); 369 | }); 370 | } 371 | 372 | _it.shouldNotBeFound = function() { 373 | it('should not be found', function() { 374 | assert(this.res); 375 | assert.equal(this.res.statusCode, 404); 376 | }); 377 | } 378 | 379 | _it.shouldBeAllowedWhenCalledAnonymously = 380 | function(verb, url, data) { 381 | _describe.whenCalledAnonymously(verb, url, data, function() { 382 | _it.shouldBeAllowed(); 383 | }); 384 | } 385 | 386 | _it.shouldBeDeniedWhenCalledAnonymously = 387 | function(verb, url) { 388 | _describe.whenCalledAnonymously(verb, url, function() { 389 | _it.shouldBeDenied(); 390 | }); 391 | } 392 | 393 | _it.shouldBeAllowedWhenCalledUnauthenticated = 394 | function(verb, url, data) { 395 | _describe.whenCalledUnauthenticated(verb, url, data, function() { 396 | _it.shouldBeAllowed(); 397 | }); 398 | } 399 | 400 | _it.shouldBeDeniedWhenCalledUnauthenticated = 401 | function(verb, url) { 402 | _describe.whenCalledUnauthenticated(verb, url, function() { 403 | _it.shouldBeDenied(); 404 | }); 405 | } 406 | 407 | _it.shouldBeAllowedWhenCalledByUser = 408 | function(credentials, verb, url, data) { 409 | _describe.whenCalledByUser(credentials, verb, url, data, function() { 410 | _it.shouldBeAllowed(); 411 | }); 412 | } 413 | 414 | _it.shouldBeDeniedWhenCalledByUser = 415 | function(credentials, verb, url) { 416 | _describe.whenCalledByUser(credentials, verb, url, function() { 417 | _it.shouldBeDenied(); 418 | }); 419 | } 420 | 421 | _it.shouldBeAllowedWhenCalledByUserWithRole = 422 | function(credentials, role, verb, url, data) { 423 | _describe.whenCalledByUserWithRole(credentials, role, verb, url, data, function() { 424 | _it.shouldBeAllowed(); 425 | }); 426 | } 427 | 428 | _it.shouldBeDeniedWhenCalledByUserWithRole = 429 | function(credentials, role, verb, url) { 430 | _describe.whenCalledByUserWithRole(credentials, role, verb, url, function() { 431 | _it.shouldBeDenied(); 432 | }); 433 | } 434 | --------------------------------------------------------------------------------