Merge pull request #321 from strongloop/feature/backport-model-from-config

Backport from 2.0: create model from config, configure model
This commit is contained in:
Miroslav Bajtoš 2014-06-12 18:54:45 +02:00
commit 828aec9481
10 changed files with 550 additions and 237 deletions

View File

@ -3,6 +3,8 @@
"content": [
"lib/application.js",
"lib/loopback.js",
"lib/runtime.js",
"lib/registry.js",
{ "title": "Base model", "depth": 2 },
"lib/models/model.js",
"lib/models/data-model.js",

View File

@ -3,7 +3,7 @@
*/
var DataSource = require('loopback-datasource-juggler').DataSource
, ModelBuilder = require('loopback-datasource-juggler').ModelBuilder
, registry = require('./registry')
, compat = require('./compat')
, assert = require('assert')
, fs = require('fs')
@ -82,43 +82,70 @@ app.disuse = function (route) {
}
/**
* Define and attach a model to the app. The `Model` will be available on the
* Attach a model to the app. The `Model` will be available on the
* `app.models` object.
*
* ```js
* var Widget = app.model('Widget', {dataSource: 'db'});
* Widget.create({name: 'pencil'});
* app.models.Widget.find(function(err, widgets) {
* console.log(widgets[0]); // => {name: 'pencil'}
* // Attach an existing model
* var User = loopback.User;
* app.model(User);
*
* // Attach an existing model, alter some aspects of the model
* var User = loopback.User;
* app.model(User, { dataSource: 'db' });
*
* // The old way: create and attach a new model (deprecated)
* var Widget = app.model('Widget', {
* dataSource: 'db',
* properties: {
* name: 'string'
* }
* });
* ```
*
* @param {String} modelName The name of the model to define.
* @param {Object|String} Model The model to attach.
* @options {Object} config The model's configuration.
* @property {String|DataSource} dataSource The `DataSource` to which to attach the model.
* @property {Object} [options] an object containing `Model` options.
* @property {ACL[]} [options.acls] an array of `ACL` definitions.
* @property {String[]} [options.hidden] **experimental** an array of properties to hide when accessed remotely.
* @property {Object} [properties] object defining the `Model` properties in [LoopBack Definition Language](http://docs.strongloop.com/loopback-datasource-juggler/#loopback-definition-language).
* @property {String|DataSource} dataSource The `DataSource` to which to
* attach the model.
* @property {Boolean} [public] whether the model should be exposed via REST API
* @property {Object} [relations] relations to add/update
* @end
* @returns {ModelConstructor} the model class
*/
app.model = function (Model, config) {
var modelName = null;
var isPublic = true;
if (arguments.length > 1) {
config = config || {};
modelName = Model;
assert(typeof modelName === 'string', 'app.model(name, config) => "name" name must be a string');
Model = modelFromConfig(modelName, config, this);
if (typeof Model === 'string') {
// create & attach the model - backwards compatibility
// create config for loopback.modelFromConfig
var modelConfig = extend({}, config);
modelConfig.options = extend({}, config.options);
modelConfig.name = Model;
// modeller does not understand `dataSource` option
delete modelConfig.dataSource;
Model = registry.createModel(modelConfig);
// delete config options already applied
['relations', 'base', 'acls', 'hidden'].forEach(function(prop) {
delete config[prop];
if (config.options) delete config.options[prop];
});
delete config.properties;
}
configureModel(Model, config, this);
isPublic = config.public !== false;
} else {
assert(typeof Model === 'function', 'app.model(Model) => Model must be a function / constructor');
modelName = Model.modelName;
assert(modelName, 'Model must have a "modelName" property');
assert(Model.prototype instanceof loopback.Model,
'Model must be a descendant of loopback.Model');
}
var modelName = Model.modelName;
this.models[modelName] =
this.models[classify(modelName)] =
this.models[camelize(modelName)] = Model;
@ -504,7 +531,7 @@ app.boot = function(options) {
// try to attach models to dataSources by type
try {
require('./loopback').autoAttach();
registry.autoAttach();
} catch(e) {
if(e.name === 'AssertionError') {
console.warn(e);
@ -567,44 +594,27 @@ function dataSourcesFromConfig(config, connectorRegistry) {
}
}
return require('./loopback').createDataSource(config);
return registry.createDataSource(config);
}
function modelFromConfig(name, config, app) {
var options = buildModelOptionsFromConfig(config);
var properties = config.properties;
function configureModel(ModelCtor, config, app) {
assert(ModelCtor.prototype instanceof registry.Model,
'Model must be a descendant of loopback.Model');
var ModelCtor = require('./loopback').createModel(name, properties, options);
var dataSource = config.dataSource;
if(typeof dataSource === 'string') {
dataSource = app.dataSources[dataSource];
}
assert(isDataSource(dataSource), name + ' is referencing a dataSource that does not exist: "'+ config.dataSource +'"');
assert(dataSource instanceof DataSource,
ModelCtor.modelName + ' is referencing a dataSource that does not exist: "' +
config.dataSource +'"');
ModelCtor.attachTo(dataSource);
return ModelCtor;
}
config = extend({}, config);
config.dataSource = dataSource;
function buildModelOptionsFromConfig(config) {
var options = extend({}, config.options);
for (var key in config) {
if (['properties', 'options', 'dataSource'].indexOf(key) !== -1) {
// Skip items which have special meaning
continue;
}
if (options[key] !== undefined) {
// When both `config.key` and `config.options.key` are set,
// use the latter one to preserve backwards compatibility
// with loopback 1.x
continue;
}
options[key] = config[key];
}
return options;
registry.configureModel(ModelCtor, config);
}
function requireDir(dir, basenames) {
@ -676,14 +686,6 @@ function tryReadDir() {
}
}
function isModelCtor(obj) {
return typeof obj === 'function' && obj.modelName && obj.name === 'ModelCtor';
}
function isDataSource(obj) {
return obj instanceof DataSource;
}
function tryReadConfig(cwd, fileName) {
try {
return require(path.join(cwd, fileName + '.json'));

View File

@ -3,11 +3,11 @@
*/
var express = require('express')
, proto = require('./application')
, fs = require('fs')
, ejs = require('ejs')
, EventEmitter = require('events').EventEmitter
, path = require('path')
, proto = require('./application')
, DataSource = require('loopback-datasource-juggler').DataSource
, ModelBuilder = require('loopback-datasource-juggler').ModelBuilder
, i8n = require('inflection')
@ -30,18 +30,6 @@ var express = require('express')
var loopback = exports = module.exports = createApplication;
/**
* True if running in a browser environment; false otherwise.
*/
loopback.isBrowser = typeof window !== 'undefined';
/**
* True if running in a server environment; false otherwise.
*/
loopback.isServer = !loopback.isBrowser;
/**
* Framework version.
*/
@ -91,17 +79,23 @@ function createApplication() {
return app;
}
function mixin(source) {
for (var key in source) {
var desc = Object.getOwnPropertyDescriptor(source, key);
Object.defineProperty(loopback, key, desc);
}
}
mixin(require('./runtime'));
mixin(require('./registry'));
/*!
* Expose express.middleware as loopback.*
* for example `loopback.errorHandler` etc.
*/
for (var key in express) {
Object.defineProperty(
loopback
, key
, Object.getOwnPropertyDescriptor(express, key));
}
mixin(express);
/*!
* Expose additional loopback middleware
@ -127,58 +121,6 @@ if (loopback.isServer) {
loopback.errorHandler.title = 'Loopback';
/**
* Create a data source with passing the provided options to the connector.
*
* @param {String} name Optional name.
* @options {Object} Data Source options
* @property {Object} connector LoopBack connector.
* @property {*} Other properties See the relevant connector documentation.
*/
loopback.createDataSource = function (name, options) {
var ds = new DataSource(name, options, loopback.Model.modelBuilder);
ds.createModel = function (name, properties, settings) {
var ModelCtor = loopback.createModel(name, properties, settings);
ModelCtor.attachTo(ds);
return ModelCtor;
};
if(ds.settings && ds.settings.defaultForType) {
loopback.setDefaultDataSourceForType(ds.settings.defaultForType, ds);
}
return ds;
};
/**
* Create a named vanilla JavaScript class constructor with an attached set of properties and options.
*
* @param {String} name Unique name.
* @param {Object} properties
* @param {Object} options (optional)
*/
loopback.createModel = function (name, properties, options) {
options = options || {};
var BaseModel = options.base || options.super;
if(typeof BaseModel === 'string') {
BaseModel = loopback.getModel(BaseModel);
}
BaseModel = BaseModel || loopback.Model;
var model = BaseModel.extend(name, properties, options);
// try to attach
try {
loopback.autoAttachModel(model);
} catch(e) {}
return model;
};
/**
* Add a remote method to a model.
* @param {Function} fn
@ -211,119 +153,11 @@ loopback.template = function (file) {
return ejs.compile(str);
};
/**
* Get an in-memory data source. Use one if it already exists.
*
* @param {String} [name] The name of the data source. If not provided, the `'default'` is used.
*/
loopback.memory = function (name) {
name = name || 'default';
var memory = (
this._memoryDataSources
|| (this._memoryDataSources = {})
)[name];
if(!memory) {
memory = this._memoryDataSources[name] = loopback.createDataSource({
connector: loopback.Memory
});
}
return memory;
};
/**
* Look up a model class by name from all models created by loopback.createModel()
* @param {String} modelName The model name
* @returns {Model} The model class
*/
loopback.getModel = function(modelName) {
return loopback.Model.modelBuilder.models[modelName];
};
/**
* Look up a model class by the base model class. The method can be used by LoopBack
* to find configured models in models.json over the base model.
* @param {Model} The base model class
* @returns {Model} The subclass if found or the base class
*/
loopback.getModelByType = function(modelType) {
assert(typeof modelType === 'function', 'The model type must be a constructor');
var models = loopback.Model.modelBuilder.models;
for(var m in models) {
if(models[m].prototype instanceof modelType) {
return models[m];
}
}
return modelType;
};
/**
* Set the default `dataSource` for a given `type`.
* @param {String} type The datasource type
* @param {Object|DataSource} dataSource The data source settings or instance
* @returns {DataSource} The data source instance
*/
loopback.setDefaultDataSourceForType = function(type, dataSource) {
var defaultDataSources = this.defaultDataSources || (this.defaultDataSources = {});
if(!(dataSource instanceof DataSource)) {
dataSource = this.createDataSource(dataSource);
}
defaultDataSources[type] = dataSource;
return dataSource;
};
/**
* Get the default `dataSource` for a given `type`.
* @param {String} type The datasource type
* @returns {DataSource} The data source instance
*/
loopback.getDefaultDataSourceForType = function(type) {
return this.defaultDataSources && this.defaultDataSources[type];
};
/**
* Attach any model that does not have a dataSource to
* the default dataSource for the type the Model requests
*/
loopback.autoAttach = function() {
var models = this.Model.modelBuilder.models;
assert.equal(typeof models, 'object', 'Cannot autoAttach without a models object');
Object.keys(models).forEach(function(modelName) {
var ModelCtor = models[modelName];
// Only auto attach if the model doesn't have an explicit data source
if(ModelCtor && (!(ModelCtor.dataSource instanceof DataSource))) {
loopback.autoAttachModel(ModelCtor);
}
});
};
loopback.autoAttachModel = function(ModelCtor) {
if(ModelCtor.autoAttach) {
var ds = loopback.getDefaultDataSourceForType(ModelCtor.autoAttach);
assert(ds instanceof DataSource, 'cannot autoAttach model "'
+ ModelCtor.modelName
+ '". No dataSource found of type ' + ModelCtor.autoAttach);
ModelCtor.attachTo(ds);
}
};
/*!
* Built in models / services
*/
loopback.Model = require('./models/model');
loopback.DataModel = require('./models/data-model');
loopback.Email = require('./models/email');
loopback.User = require('./models/user');
loopback.Application = require('./models/application');

View File

@ -1,7 +1,7 @@
/*!
* Module Dependencies.
*/
var loopback = require('../loopback');
var registry = require('../registry');
var compat = require('../compat');
var ModelBuilder = require('loopback-datasource-juggler').ModelBuilder;
var modeler = new ModelBuilder();
@ -128,7 +128,7 @@ Model._ACL = function getACL(ACL) {
return _aclModel;
}
var aclModel = require('./acl').ACL;
_aclModel = loopback.getModelByType(aclModel);
_aclModel = registry.getModelByType(aclModel);
return _aclModel;
};

329
lib/registry.js Normal file
View File

@ -0,0 +1,329 @@
/*
* This file exports methods and objects for manipulating
* Models and DataSources.
*
* It is an internal file that should not be used outside of loopback.
* All exported entities can be accessed via the `loopback` object.
* @private
*/
var assert = require('assert');
var extend = require('util')._extend;
var DataSource = require('loopback-datasource-juggler').DataSource;
var registry = module.exports;
registry.defaultDataSources = {};
/**
* Create a named vanilla JavaScript class constructor with an attached
* set of properties and options.
*
* This function comes with two variants:
* * `loopback.createModel(name, properties, options)`
* * `loopback.createModel(config)`
*
* In the second variant, the parameters `name`, `properties` and `options`
* are provided in the config object. Any additional config entries are
* interpreted as `options`, i.e. the following two configs are identical:
*
* ```js
* { name: 'Customer', base: 'User' }
* { name: 'Customer', options: { base: 'User' } }
* ```
*
* **Example**
*
* Create an `Author` model using the three-parameter variant:
*
* ```js
* loopback.createModel(
* 'Author',
* {
* firstName: 'string',
* lastName: 'string
* },
* {
* relations: {
* books: {
* model: 'Book',
* type: 'hasAndBelongsToMany'
* }
* }
* }
* );
* ```
*
* Create the same model using a config object:
*
* ```js
* loopback.createModel({
* name: 'Author',
* properties: {
* firstName: 'string',
* lastName: 'string
* },
* relations: {
* books: {
* model: 'Book',
* type: 'hasAndBelongsToMany'
* }
* }
* });
* ```
*
* @param {String} name Unique name.
* @param {Object} properties
* @param {Object} options (optional)
*
* @header loopback.createModel
*/
registry.createModel = function (name, properties, options) {
if (arguments.length === 1 && typeof name === 'object') {
var config = name;
name = config.name;
properties = config.properties;
options = buildModelOptionsFromConfig(config);
assert(typeof name === 'string',
'The model-config property `name` must be a string');
}
options = options || {};
var BaseModel = options.base || options.super;
if(typeof BaseModel === 'string') {
var baseName = BaseModel;
BaseModel = this.getModel(BaseModel);
if (BaseModel === undefined) {
if (baseName === 'DataModel') {
console.warn('Model `%s` is extending deprecated `DataModel. ' +
'Use `PeristedModel` instead.', name);
BaseModel = this.PersistedModel;
} else {
console.warn('Model `%s` is extending an unknown model `%s`. ' +
'Using `PersistedModel` as the base.', name, baseName);
}
}
}
BaseModel = BaseModel || this.Model;
var model = BaseModel.extend(name, properties, options);
// try to attach
try {
this.autoAttachModel(model);
} catch(e) {}
return model;
};
function buildModelOptionsFromConfig(config) {
var options = extend({}, config.options);
for (var key in config) {
if (['name', 'properties', 'options'].indexOf(key) !== -1) {
// Skip items which have special meaning
continue;
}
if (options[key] !== undefined) {
// When both `config.key` and `config.options.key` are set,
// use the latter one
continue;
}
options[key] = config[key];
}
return options;
}
/**
* Alter an existing Model class.
* @param {Model} ModelCtor The model constructor to alter.
* @options {Object} config Additional configuration to apply
* @property {DataSource} dataSource Attach the model to a dataSource.
* @property {Object} [relations] Model relations to add/update.
*
* @header loopback.configureModel(ModelCtor, config)
*/
registry.configureModel = function(ModelCtor, config) {
var settings = ModelCtor.settings;
if (config.relations) {
var relations = settings.relations = settings.relations || {};
Object.keys(config.relations).forEach(function(key) {
relations[key] = extend(relations[key] || {}, config.relations[key]);
});
}
// It's important to attach the datasource after we have updated
// configuration, so that the datasource picks up updated relations
if (config.dataSource) {
assert(config.dataSource instanceof DataSource,
'Cannot configure ' + ModelCtor.modelName +
': config.dataSource must be an instance of DataSource');
ModelCtor.attachTo(config.dataSource);
}
};
/**
* Look up a model class by name from all models created by
* `loopback.createModel()`
* @param {String} modelName The model name
* @returns {Model} The model class
*
* @header loopback.getModel(modelName)
*/
registry.getModel = function(modelName) {
return this.Model.modelBuilder.models[modelName];
};
/**
* Look up a model class by the base model class.
* The method can be used by LoopBack
* to find configured models in models.json over the base model.
* @param {Model} modelType The base model class
* @returns {Model} The subclass if found or the base class
*
* @header loopback.getModelByType(modelType)
*/
registry.getModelByType = function(modelType) {
assert(typeof modelType === 'function',
'The model type must be a constructor');
var models = this.Model.modelBuilder.models;
for(var m in models) {
if(models[m].prototype instanceof modelType) {
return models[m];
}
}
return modelType;
};
/**
* Create a data source with passing the provided options to the connector.
*
* @param {String} name Optional name.
* @options {Object} options Data Source options
* @property {Object} connector LoopBack connector.
* @property {*} [*] Other connector properties.
* See the relevant connector documentation.
*
* @header loopback.createDataSource(name, options)
*/
registry.createDataSource = function (name, options) {
var loopback = this;
var ds = new DataSource(name, options, loopback.Model.modelBuilder);
ds.createModel = function (name, properties, settings) {
var ModelCtor = loopback.createModel(name, properties, settings);
ModelCtor.attachTo(ds);
return ModelCtor;
};
if(ds.settings && ds.settings.defaultForType) {
this.setDefaultDataSourceForType(ds.settings.defaultForType, ds);
}
return ds;
};
/**
* Get an in-memory data source. Use one if it already exists.
*
* @param {String} [name] The name of the data source.
* If not provided, the `'default'` is used.
*
* @header loopback.memory()
*/
registry.memory = function (name) {
name = name || 'default';
var memory = (
this._memoryDataSources || (this._memoryDataSources = {})
)[name];
if(!memory) {
memory = this._memoryDataSources[name] = this.createDataSource({
connector: loopback.Memory
});
}
return memory;
};
/**
* Set the default `dataSource` for a given `type`.
* @param {String} type The datasource type
* @param {Object|DataSource} dataSource The data source settings or instance
* @returns {DataSource} The data source instance
*
* @header loopback.setDefaultDataSourceForType(type, dataSource)
*/
registry.setDefaultDataSourceForType = function(type, dataSource) {
var defaultDataSources = this.defaultDataSources;
if(!(dataSource instanceof DataSource)) {
dataSource = this.createDataSource(dataSource);
}
defaultDataSources[type] = dataSource;
return dataSource;
};
/**
* Get the default `dataSource` for a given `type`.
* @param {String} type The datasource type
* @returns {DataSource} The data source instance
* @header loopback.getDefaultDataSourceForType()
*/
registry.getDefaultDataSourceForType = function(type) {
return this.defaultDataSources && this.defaultDataSources[type];
};
/**
* Attach any model that does not have a dataSource to
* the default dataSource for the type the Model requests
* @header loopback.autoAttach()
*/
registry.autoAttach = function() {
var models = this.Model.modelBuilder.models;
assert.equal(typeof models, 'object', 'Cannot autoAttach without a models object');
Object.keys(models).forEach(function(modelName) {
var ModelCtor = models[modelName];
// Only auto attach if the model doesn't have an explicit data source
if(ModelCtor && (!(ModelCtor.dataSource instanceof DataSource))) {
this.autoAttachModel(ModelCtor);
}
}, this);
};
registry.autoAttachModel = function(ModelCtor) {
if(ModelCtor.autoAttach) {
var ds = this.getDefaultDataSourceForType(ModelCtor.autoAttach);
assert(ds instanceof DataSource, 'cannot autoAttach model "'
+ ModelCtor.modelName
+ '". No dataSource found of type ' + ModelCtor.autoAttach);
ModelCtor.attachTo(ds);
}
};
registry.DataSource = DataSource;
/*
* Core models
* @private
*/
registry.Model = require('./models/model');
registry.DataModel = require('./models/data-model');

22
lib/runtime.js Normal file
View File

@ -0,0 +1,22 @@
/*
* This is an internal file that should not be used outside of loopback.
* All exported entities can be accessed via the `loopback` object.
* @private
*/
var runtime = exports;
/**
* True if running in a browser environment; false otherwise.
* @header loopback.isBrowser
*/
runtime.isBrowser = typeof window !== 'undefined';
/**
* True if running in a server environment; false otherwise.
* @header loopback.isServer
*/
runtime.isServer = !runtime.isBrowser;

View File

@ -6,6 +6,7 @@ var app = require(path.join(ACCESS_CONTROL_APP, 'app.js'));
var assert = require('assert');
var USER = {email: 'test@test.test', password: 'test'};
var CURRENT_USER = {email: 'current@test.test', password: 'test'};
var debug = require('debug')('loopback:test:access-control.integration');
describe('access control - integration', function () {
@ -99,6 +100,10 @@ describe('access control - integration', function () {
lt.describe.whenCalledRemotely('GET', '/api/users/:id', function() {
lt.it.shouldBeAllowed();
it('should not include a password', function() {
debug('GET /api/users/:id response: %s\nheaders: %j\nbody string: %s',
this.res.statusCode,
this.res.headers,
this.res.text);
var user = this.res.body;
assert.equal(user.password, undefined);
});

View File

@ -136,6 +136,20 @@ describe('app', function() {
});
describe('app.model(ModelCtor, config)', function() {
it('attaches the model to a datasource', function() {
app.dataSource('db', { connector: 'memory' });
var TestModel = loopback.Model.extend('TestModel');
// TestModel was most likely already defined in a different test,
// thus TestModel.dataSource may be already set
delete TestModel.dataSource;
app.model(TestModel, { dataSource: 'db' });
expect(app.models.TestModel.dataSource).to.equal(app.dataSources.db);
});
});
describe('app.models', function() {
it('is unique per app instance', function() {
app.dataSource('db', { connector: 'memory' });

View File

@ -1,4 +1,11 @@
describe('loopback', function() {
var nameCounter = 0;
var uniqueModelName;
beforeEach(function() {
uniqueModelName = 'TestModel-' + (++nameCounter);
});
describe('exports', function() {
it('ValidationError', function() {
expect(loopback.ValidationError).to.be.a('function')
@ -119,4 +126,95 @@ describe('loopback', function() {
});
});
});
describe('loopback.createModel(config)', function() {
it('creates the model', function() {
var model = loopback.createModel({
name: uniqueModelName
});
expect(model.prototype).to.be.instanceof(loopback.Model);
});
it('interprets extra first-level keys as options', function() {
var model = loopback.createModel({
name: uniqueModelName,
base: 'User'
});
expect(model.prototype).to.be.instanceof(loopback.User);
});
it('prefers config.options.key over config.key', function() {
var model = loopback.createModel({
name: uniqueModelName,
base: 'User',
options: {
base: 'Application'
}
});
expect(model.prototype).to.be.instanceof(loopback.Application);
});
});
describe('loopback.configureModel(ModelCtor, config)', function() {
it('adds new relations', function() {
var model = loopback.Model.extend(uniqueModelName);
loopback.configureModel(model, {
relations: {
owner: {
type: 'belongsTo',
model: 'User'
}
}
});
expect(model.settings.relations).to.have.property('owner');
});
it('updates existing relations', function() {
var model = loopback.Model.extend(uniqueModelName, {}, {
relations: {
owner: {
type: 'belongsTo',
model: 'User'
}
}
});
loopback.configureModel(model, {
relations: {
owner: {
model: 'Application'
}
}
});
expect(model.settings.relations.owner).to.eql({
type: 'belongsTo',
model: 'Application'
});
});
it('updates relations before attaching to a dataSource', function() {
var db = loopback.createDataSource({ connector: loopback.Memory });
var model = loopback.Model.extend(uniqueModelName);
loopback.configureModel(model, {
dataSource: db,
relations: {
owner: {
type: 'belongsTo',
model: 'User'
}
}
});
var owner = model.prototype.owner;
expect(owner, 'model.prototype.owner').to.be.a('function');
expect(owner._targetClass).to.equal('User');
});
});
});

View File

@ -5,6 +5,7 @@ var SIMPLE_APP = path.join(__dirname, 'fixtures', 'simple-integration-app');
var app = require(path.join(SIMPLE_APP, 'app.js'));
var assert = require('assert');
var expect = require('chai').expect;
var debug = require('debug')('loopback:test:relations.integration');
describe('relations - integration', function () {
@ -28,13 +29,19 @@ describe('relations - integration', function () {
this.url = '/api/stores/' + this.store.id + '/widgets';
});
lt.describe.whenCalledRemotely('GET', '/api/stores/:id/widgets', function() {
it('should succeed with statusCode 200', function() {
assert.equal(this.res.statusCode, 200);
});
describe('widgets (response.body)', function() {
beforeEach(function() {
debug('GET /api/stores/:id/widgets response: %s' +
'\nheaders: %j\nbody string: %s',
this.res.statusCode,
this.res.headers,
this.res.text);
this.widgets = this.res.body;
this.widget = this.res.body[0];
this.widget = this.res.body && this.res.body[0];
});
it('should be an array', function() {
assert(Array.isArray(this.widgets));