diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..78b2aca --- /dev/null +++ b/.editorconfig @@ -0,0 +1,12 @@ +root = true + +[*] +indent_style = tab +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = false +insert_final_newline = true + +[{package.json,*.yml}] +indent_style = space +indent_size = 2 diff --git a/changelog.md b/changelog.md index 92f7f7d..b917dbd 100644 --- a/changelog.md +++ b/changelog.md @@ -1,5 +1,21 @@ # Changelog +v0.10.2 - 8ed75d2 - Dec 4th 2017 + * Fix for DataTypes not being exposed during `sequelize.import` calls + * *DEV* Added .editorconfig file to normalize editors and minimize whitespace changes + +v0.10.1 - 43cc668 - Nov 20th 2017 + * Fix for relative file paths in the `sequelize.import` function + +v0.10.0 - 0a7f270f - Oct 24th 2017 + * Add `sequelize.import` support + * Add `sequelize.$overrideImport` test functionality to allow overriding imported module paths + * Add support for `Model.findAndCount()` and it's alias `Model.findAndCountAll()` (thanks to @TerryMooreII) + +v0.9.1 - 3aeaa05 - Sep 21st 2017 + * Add `authenticate()` to `sequelize` which always resolves (thanks to @vFederer) + * Fix a few documentation issues + v0.9.0 - c75d75e - Jul 28th 2017 * Add DataType mock objects for use with any DataType funcitonality * Add support for conditional query result handling (thanks to @scinos) diff --git a/docs/api/model.md b/docs/api/model.md index 7eb8271..421aaba 100644 --- a/docs/api/model.md +++ b/docs/api/model.md @@ -266,6 +266,46 @@ Name | Type | Description + +## findAndCount([options]) -> Promise.<Object> + +Executes a mock query to find all of the instances with any provided options and also return +the count. Without any other configuration, the default behavior when no queueud query result +is present is to create an array of a single result based on the where query in the options and +wraps it in a promise. + +To turn off this behavior, the `$autoQueryFallback` option on the model should be set +to `false`.
**Alias** findAndCountAll + +**Example** + +```javascript +// This is an example of the default behavior with no queued results +// If there is a queued result or failure, that will be returned instead +User.findAndCountAll({ + where: { + email: 'myEmail@example.com', + }, +}).then(function (results) { + // results is an array of 1 + results.count = 1 + results.rows[0].get('email') === 'myEmail@example.com'; // true +}); +``` + +### Parameters + +Name | Type | Description +--- | --- | --- +[options] | Object | Options for the findAll query +[options.where] | Object | Values that any automatically created Instances should have + + +### Return +`Promise.`: result returned by the mock query + + + ## findById(id) -> Promise.<Instance> diff --git a/docs/api/sequelize.md b/docs/api/sequelize.md index 684f495..9f934c4 100644 --- a/docs/api/sequelize.md +++ b/docs/api/sequelize.md @@ -33,6 +33,13 @@ Options passed into the Sequelize initialization + +### .importCache + +Used to cache and override model imports for easy mock model importing + + + ### .models @@ -151,6 +158,25 @@ Clears any queued results from `$queueResult` or `$queueFailure`
**Alias** $ + +## $overrideImport(importPath, overridePath) + +Overrides a path used for import + +**See** + + - [import](#import) + +### Parameters + +Name | Type | Description +--- | --- | --- +importPath | String | The original path that import will be called with +overridePath | String | The path that should actually be used for resolving. If this path is relative, it will be relative to the file calling the import function + + + + ## getDialect() -> String @@ -240,6 +266,30 @@ name | String | Name of the model + +## import(path) -> Any + +Imports a given model from the provided file path. Files that are imported should +export a function that accepts two parameters, this sequelize instance, and an object +with all of the available datatypes + +Before importing any modules, it will remap any paths that were overridden using the +`$overrideImport` test function. This method is most helpful when used to make the +SequelizeMock framework import your mock models instead of the real ones in your test +code. + +### Parameters + +Name | Type | Description +--- | --- | --- +path | String | Path of the model to import. Can be relative or absolute + + +### Return +`Any`: The result of evaluating the imported file's function + + + ## model(name) -> Model @@ -303,3 +353,13 @@ arg | Any | Value to return ### Return `Any`: value passed in + + + +## authenticate() -> Promise + +Always returns a resolved promise + +### Return +`Promise`: will always resolve as a successful authentication + diff --git a/docs/api/utils.md b/docs/api/utils.md index e261fc9..b613553 100644 --- a/docs/api/utils.md +++ b/docs/api/utils.md @@ -74,6 +74,13 @@ str | String | Word to convert to its plural form + +## stack() + +Gets the current stack frame + + + ## lodash diff --git a/docs/docs/getting-started.md b/docs/docs/getting-started.md index d2eade6..bedfe53 100644 --- a/docs/docs/getting-started.md +++ b/docs/docs/getting-started.md @@ -38,6 +38,27 @@ var UserMock = dbMock.define('user', { ## Swapping Model for Mocks +### Using `Sequelize.import()` + +In SequelizeMock, we provide the same `import()` functionality that Sequelize provides, with the added ability to override any given imported path with your mock paths. + +Using the `$overrideImport` method, you can simply define a mapping between your model and your mock object. + +```javascript +// If your Sequelize code looks like this +sequelize.import('./users/model.js'); + +// Your test code can simply override the import so your code will function as expected +sequelize.$overrideImport('./users/model.js', './users/mock.js'); + +// Now an import for your users model will actually import your user mock file instead +sequelize.import('./users/model.js'); // Will load './users/mock.js' instead +``` + +**Note that relative paths are relative to the file calling the `import` function, and not to your test code.** + +### Using `require()` + There are a number of libraries out there that can be used to replace `require()` dependencies with mock objects. You can simply use one of these libraries to replace the Sequelize Mock object into your code and it should run exactly as you would expect. Here is an example of doing so with [proxyquire](https://www.npmjs.com/package/proxyquire) @@ -54,7 +75,7 @@ var myModule = proxyquire('user.controller', { }); ``` -### Some Mock Injection Libraries +#### Some Mock Injection Libraries * [proxyquire](https://www.npmjs.com/package/proxyquire) * [mockery](https://www.npmjs.com/package/mockery) * [mock-require](https://www.npmjs.com/package/mock-require) diff --git a/docs/docs/mock-queries.md b/docs/docs/mock-queries.md index 4ad65b6..620b1eb 100644 --- a/docs/docs/mock-queries.md +++ b/docs/docs/mock-queries.md @@ -4,7 +4,7 @@ Sequelize Mock utilizes a special `QueryInterface` to be utilized for testing. T When you call into a function that would run a DB query in standard Sequelize, this function instead calls into our special test `QueryInterface` object to get the results for the query. Query results can be either manually queued up based on what your code may expect next, automatically filled in from the `Model`'s defined default values, or dynamically generated. -Each query will return the first availble result from the list. +Each query will return the first available result from the list. 1. The value generated by a query handler 2. If not available, the next result queued for the object the query is being run on @@ -39,7 +39,7 @@ Option | Type | Description ## Query handlers -Query results can be generated using query handlers. When multiple handlers are added to the QueryInterface, they will be called in order until one of them returns a valud result. If no handler returns a result, then QueryInterface will get the value from the list of queued results. +Query results can be generated using query handlers. When multiple handlers are added to the QueryInterface, they will be called in order until one of them returns a valid result. If no handler returns a result, then QueryInterface will get the value from the list of queued results. The handler will receive two arguments @@ -49,9 +49,12 @@ The handler will receive two arguments Those arguments can be used to filter the results. For example: ```javascript -User.$useHandler(function(query, queryOptions, done) { +User.$queryInterface.$useHandler(function(query, queryOptions, done) { + //this will return + //query: 'findOne', + //queryOptions: [Arguments] { '0': { [field_your_are_querying]: 'value' } if (query === 'findOne') { - if (queryOptions[0].where.id === 42) { + if (queryOptions[0].field === value_you_desire_to_find) { // Result found, return it return User.build({ id: 42, name: 'foo' }); } else { diff --git a/package.json b/package.json index ead4daa..a2974c8 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "sequelize-mock", - "version": "0.9.0", + "version": "0.10.2", "description": "A simple mock interface specifically for testing code relying on Sequelize models", "main": "src/index.js", "scripts": { diff --git a/src/data-types.js b/src/data-types.js index 0b62155..818ac5f 100644 --- a/src/data-types.js +++ b/src/data-types.js @@ -546,4 +546,6 @@ module.exports = function (Sequelize) { Sequelize.ARRAY = ARRAY; Sequelize.GEOMETRY = GEOMETRY; Sequelize.GEOGRAPHY = GEOGRAPHY; + + return Sequelize; }; diff --git a/src/instance.js b/src/instance.js index b35df2d..a2cb256 100644 --- a/src/instance.js +++ b/src/instance.js @@ -205,6 +205,24 @@ fakeModelInstance.prototype.get = function (key) { } }; +/** + * Get plain value + * @param {String} key Key yo get the value for + * @return {Any} + */ +fakeModelInstance.prototype.getDataValue = function (key) { + return this._values[key]; +}; + +/** + * Set plain value + * @param {String} key Key yo get the value for + * @param {Any} value + */ +fakeModelInstance.prototype.setDataValue = function (key, value) { + this._values[key] = value; +}; + /** * Triggers validation. If there are errors added through `$addValidationError` they will * be returned and the queue of validation errors will be cleared. diff --git a/src/model.js b/src/model.js index 3e2f3c5..313f447 100644 --- a/src/model.js +++ b/src/model.js @@ -239,6 +239,14 @@ fakeModel.prototype.scope = function () { return this; }; +/** + * No-op that returns a void. + * + * @instance + * @return {undefined} + **/ +fakeModel.prototype.addScope = function () {}; + /** * Executes a mock query to find all of the instances with any provided options. Without * any other configuration, the default behavior when no queueud query result is present @@ -277,6 +285,55 @@ fakeModel.prototype.findAll = function (options) { }); }; + +/** + * Executes a mock query to find all of the instances with any provided options and also return + * the count. Without any other configuration, the default behavior when no queueud query result + * is present is to create an array of a single result based on the where query in the options and + * wraps it in a promise. + * + * To turn off this behavior, the `$autoQueryFallback` option on the model should be set + * to `false`. + * + * @example + * // This is an example of the default behavior with no queued results + * // If there is a queued result or failure, that will be returned instead + * User.findAndCountAll({ + * where: { + * email: 'myEmail@example.com', + * }, + * }).then(function (results) { + * // results is an array of 1 + * results.count = 1 + * results.rows[0].get('email') === 'myEmail@example.com'; // true + * }); + * + * @instance + * @method findAndCount + * @alias findAndCountAll + * @param {Object} [options] Options for the findAll query + * @param {Object} [options.where] Values that any automatically created Instances should have + * @return {Promise} result returned by the mock query + **/ +fakeModel.prototype.findAndCount = +fakeModel.prototype.findAndCountAll = function (options) { + var self = this; + + return this.$query({ + query: "findAndCountAll", + queryOptions: arguments, + fallbackFn: !this.options.autoQueryFallback ? null : function () { + return Promise.resolve([ self.build(options ? options.where : {}) ]) + .then(function(result){ + return Promise.resolve({ + count: result.length, + rows: result + }); + }); + }, + }); +}; + /** * Executes a mock query to find an instance with the given ID value. Without any other * configuration, the default behavior when no queueud query result is present is to diff --git a/src/sequelize.js b/src/sequelize.js index 36be0d2..485b4d2 100644 --- a/src/sequelize.js +++ b/src/sequelize.js @@ -7,12 +7,14 @@ * @fileOverview Mock class for the base Sequelize class */ -var _ = require('lodash'), +var path = require('path'), + _ = require('lodash'), bluebird = require('bluebird'), Model = require('./model'), Instance = require('./instance'), Utils = require('./utils'), Errors = require('./errors'), + DataTypes = require('./data-types')({}), QueryInterface = require('./queryinterface'); /** @@ -51,6 +53,14 @@ function Sequelize(database, username, password, options) { dialect: 'mock', }, options || {}); + /** + * Used to cache and override model imports for easy mock model importing + * + * @member Sequelize + * @property + **/ + this.importCache = {}; + /** * Models that have been defined in this Sequelize Mock instances * @@ -213,6 +223,17 @@ Sequelize.prototype.$queueQueryClear = Sequelize.prototype.$cqq = Sequelize.prototype.$qqc = Sequelize.prototype.$clearQueue; +/** + * Overrides a path used for import + * + * @see {@link import} + * @param {String} importPath The original path that import will be called with + * @param {String} overridePath The path that should actually be used for resolving. If this path is relative, it will be relative to the file calling the import function + **/ +Sequelize.prototype.$overrideImport = function (realPath, mockPath) { + this.importCache[realPath] = mockPath; +}; + /* Mock Functionality * */ @@ -288,6 +309,43 @@ Sequelize.prototype.isDefined = function (name) { return name in this.models && typeof this.models[name] !== 'undefined'; }; +/** + * Imports a given model from the provided file path. Files that are imported should + * export a function that accepts two parameters, this sequelize instance, and an object + * with all of the available datatypes + * + * Before importing any modules, it will remap any paths that were overridden using the + * `$overrideImport` test function. This method is most helpful when used to make the + * SequelizeMock framework import your mock models instead of the real ones in your test + * code. + * + * @param {String} path Path of the model to import. Can be relative or absolute + * @return {Any} The result of evaluating the imported file's function + **/ +Sequelize.prototype.import = function (importPath) { + if(typeof this.importCache[importPath] === 'string') { + importPath = this.importCache[importPath]; + } + + if(path.normalize(importPath) !== path.resolve(importPath)) { + // We're relative, and need the calling files location + var callLoc = path.dirname(Utils.stack()[1].getFileName()); + + importPath = path.resolve(callLoc, importPath); + } + + if(this.importCache[importPath] === 'string' || !this.importCache[importPath]) { + var defineCall = arguments.length > 1 ? arguments[1] : require(importPath); + if(typeof defineCall === 'object') { + // ES6 module compatibility + defineCall = defineCall.default; + } + this.importCache[importPath] = defineCall(this, DataTypes); + } + + return this.importCache[importPath]; +}; + /** * Fetch a Model which is already defined. * @@ -346,4 +404,15 @@ Sequelize.prototype.literal = function (arg) { return arg; }; +/** + * Always returns a resolved promise + * + * @return {Promise} will always resolve as a successful authentication + */ +Sequelize.prototype.authenticate = function() { + return new bluebird(function (resolve) { + return resolve(); + }); +}; + module.exports = Sequelize; diff --git a/src/utils.js b/src/utils.js index 8eccf59..1c9d514 100644 --- a/src/utils.js +++ b/src/utils.js @@ -51,6 +51,21 @@ exports.pluralize = function(str) { return inflection.pluralize(str); }; +/** + * Gets the current stack frame + * + **/ +exports.stack = function () { + // Stash original stack prep + var prepareStackTrace = Error.prepareStackTrace; + Error.prepareStackTrace = function (_, s) { return s; }; + var curr = {}; + Error.captureStackTrace(curr, exports.stack); + var stack = curr.stack; + Error.prepareStackTrace = prepareStackTrace; + return stack; +}; + /** * Exposed version of the lodash library * diff --git a/test/instance.spec.js b/test/instance.spec.js index 916c71b..e45c145 100644 --- a/test/instance.spec.js +++ b/test/instance.spec.js @@ -161,7 +161,25 @@ describe('Instance', function () { inst.get().should.be.eql({foo: 'bar'}); }); }); - + + describe('#getDataValue', function () { + it('should get the value of a property on the object', function () { + var inst = new Instance(); + inst._values.foo = 'bar'; + + inst.getDataValue('foo').should.be.exactly('bar'); + }); + }); + + describe('#setDataValue', function () { + it('should set the value of a property on the object', function () { + var inst = new Instance(); + inst._values.foo = 'bar'; + inst.setDataValue('foo', 'baz'); + inst._values.foo.should.be.exactly('baz'); + }); + }); + describe('#validate', function () { it('should return no validation errors if no errors are set on the instances', function (done) { var inst = new Instance(); diff --git a/test/model.spec.js b/test/model.spec.js index e91a8ad..02067cf 100644 --- a/test/model.spec.js +++ b/test/model.spec.js @@ -162,10 +162,12 @@ describe('Model', function () { // mdl.should.have.property('schema').which.is.a.Function(); mdl.should.have.property('getTableName').which.is.a.Function(); mdl.should.have.property('unscoped').which.is.a.Function(); - // mdl.should.have.property('addScope').which.is.a.Function(); + mdl.should.have.property('addScope').which.is.a.Function(); mdl.should.have.property('scope').which.is.a.Function(); mdl.should.have.property('find').which.is.a.Function(); mdl.should.have.property('findAll').which.is.a.Function(); + mdl.should.have.property('findAndCount').which.is.a.Function(); + mdl.should.have.property('findAndCountAll').which.is.a.Function(); mdl.should.have.property('findById').which.is.a.Function(); mdl.should.have.property('findOne').which.is.a.Function(); // mdl.should.have.property('aggregate').which.is.a.Function(); @@ -605,6 +607,55 @@ describe('Model', function () { }); }); + describe('#findAndCountAll', function () { + var mdl; + beforeEach(function () { + mdl = new Model('foo'); + }); + + it('should pass along where value to Instance creation', function (done) { + var options = { + where: { + 'foo': 'bar', + }, + }; + + mdl.findAndCountAll(options) + .fallbackFn().then(function (result) { + result.rows.length.should.equal(1); + result.count.should.equal(1); + result.rows[0]._args[0].should.have.property('foo').which.is.exactly('bar'); + done(); + }).catch(done); + }); + + it('should still find results if there is not options', function (done) { + mdl.findAndCountAll() + .fallbackFn().then(function (result) { + result.count.should.equal(1); + result.rows.length.should.equal(1); + done(); + }).catch(done); + }); + + it('should not pass along a fallback function if auto fallback is turned off', function () { + mdl.options.autoQueryFallback = false; + should.not.exist(mdl.findAndCountAll().fallbackFn); + }); + + it('should pass query info to the QueryInterface instance', function(done) { + var queryOptions = {}; + + mdl.$query = function(options) { + options.query.should.equal('findAndCountAll'); + options.queryOptions.length.should.equal(1); + options.queryOptions[0].should.equal(queryOptions); + done(); + } + mdl.findAndCountAll(queryOptions) + }); + }); + describe('#destroy', function () { var mdl; beforeEach(function () { diff --git a/test/sequelize.spec.js b/test/sequelize.spec.js index 2b0acce..5072d48 100644 --- a/test/sequelize.spec.js +++ b/test/sequelize.spec.js @@ -5,7 +5,16 @@ var bluebird = require('bluebird'); var proxyquire = require('proxyquire').noCallThru(); var ModelMock = function () {}; -var UtilsMock = {}; +var PathMock = { + normalize: function (p) { return p }, + resolve: function (p) { return p }, + dirname: function (p) { return p }, +}; + +var UtilsMock = { + stack: function () { return {}; }, +}; + var PackageMock = { version: 'test', }; @@ -15,13 +24,22 @@ var ErrorMock = { }; var QueryInterfaceMock = function () {}; +var lastImportTestCall; +function importTestFunc() { + lastImportTestCall = arguments; +} + var Sequelize = proxyquire('../src/sequelize', { + 'path' : PathMock, './model' : ModelMock, './utils' : UtilsMock, './errors' : ErrorMock, './queryinterface' : QueryInterfaceMock, '../package.json' : PackageMock, - './data-types' : function () {} + './data-types' : function () {}, + + 'import-test' : importTestFunc, + 'import-test-es6' : { default: importTestFunc }, }); describe('Sequelize', function () { @@ -141,7 +159,7 @@ describe('Sequelize', function () { seq = new Sequelize(); }); - it('should queue a result against the QueryInterface', function () { + it('should clear queue of results in the QueryInterface', function () { var run = 0; seq.queryInterface = { $clearQueue: function (res) { @@ -153,6 +171,19 @@ describe('Sequelize', function () { }); }); + describe('#$overrideImport', function () { + var seq; + beforeEach(function () { + seq = new Sequelize(); + }); + + it('should override an import path in the importCache', function () { + seq.importCache = {}; + seq.$overrideImport('foo', 'bar'); + seq.importCache.should.have.property('foo').which.is.exactly('bar'); + }); + }); + describe('#getDialect', function () { it('should return the dialect set during initialization', function () { var seq = new Sequelize({ @@ -190,7 +221,72 @@ describe('Sequelize', function () { seq.isDefined('test').should.be.false() }); }); + + describe('#import', function () { + var seq, resolve, stack; + beforeEach(function () { + seq = new Sequelize(); + lastImportTestCall = null; + resolve = PathMock.resolve; + stack = UtilsMock.stack; + }); + + afterEach(function () { + PathMock.resolve = resolve; + UtilsMock.stack = stack; + }); + + it('should return an already imported model', function () { + var findItem = {}; + seq.importCache = { + 'foo': findItem, + }; + seq.import('foo').should.be.exactly(findItem); + }); + + it('should import a model from the given path', function () { + seq.import('import-test'); + should(lastImportTestCall).not.be.Null(); + lastImportTestCall[0].should.be.exactly(seq); + }); + + it('should turn a relative path into an absolute path', function () { + var pathRun = 0; + var stackRun = 0; + PathMock.resolve = function () { pathRun++; return './bar'; }; + UtilsMock.stack = function () { stackRun++; return [0, { getFileName: function () { return 'baz'; } } ] }; + var findItem = {}; + + seq.importCache = { + './bar': findItem, + }; + seq.import('./foo').should.be.exactly(findItem); + pathRun.should.be.exactly(2); + stackRun.should.be.exactly(1); + }); + + it('should import a replaced model from an overridden import', function () { + var findItem = {}; + seq.importCache = { + 'foo': 'bar', + 'bar': findItem, + }; + seq.import('foo').should.be.exactly(findItem); + }); + + it('should import an es6 model from the given path', function () { + seq.import('import-test-es6'); + should(lastImportTestCall).not.be.Null(); + lastImportTestCall[0].should.be.exactly(seq); + }); + it('should import a model function as the second argument (for meteor compatibility)', function () { + seq.import('import-test', importTestFunc); + should(lastImportTestCall).not.be.Null(); + lastImportTestCall[0].should.be.exactly(seq); + }); + }); + describe('#model', function() { it('should return a previously defined Mock Model referenced its name', function() { var seq = new Sequelize(); @@ -257,4 +353,10 @@ describe('Sequelize', function () { }); }); + describe('#authenticate', function () { + it('should simply return a resolving promise', (done) => { + var seq = new Sequelize(); + seq.authenticate().then(done).catch(done); + }) + }); }); diff --git a/test/utils.spec.js b/test/utils.spec.js index cfdb629..96b9124 100644 --- a/test/utils.spec.js +++ b/test/utils.spec.js @@ -80,4 +80,44 @@ describe('Utils', function () { }); }); + describe('#stack', function () { + var captureStack; + beforeEach(function () { + captureStack = Error.captureStackTrace; + }); + + afterEach(function () { + Error.captureStackTrace = captureStack; + }); + + // TODO: This test is more integration than unit. Need to move this + // code over to a different test suite once one exists. + it('should capture and return the stack trace', function () { + /* + var arg1, arg2; + Error.captureStackTrace = function (obj, fn) { + arg1 = obj; + arg2 = fn; + obj.stack = 'bar'; + }; + */ + + var ret = Utils.stack(); + + /* + // We need to restore nomality here so that the asserts and things can work properly + Error.captureStackTrace = captureStack; + + should(arg1).be.Object(); + should(arg2).be.Function(); + + ret.should.equal('bar'); + */ + + ret.should.be.an.Array(); + should(ret[0].getFileName).be.a.Function(); + }); + + }); + });