feat(config): Mongo Seed 2.0 (#1808)

feat(config): Mongo Seed 2.0

Adds a more configurable and easily extended MongoDB Seed feature.

Adds additional options at the collection, and collection item level to
allow more control over how each environment config handles the seeding
feature.

Enforces seed order based on the order of the  environment's seeding configuration object.

Removes the previous SeedDB config file.

Adds chalk to messages logged to the console for readability.

Refactors the Mongo Seed configuration tests.

Adds Gulp tasks to perform Mongo Seed operations for default, prod, and
test environment configurations. Also, adds accommodating npm scripts.
This commit is contained in:
Michael Leanos
2017-08-13 16:29:47 -07:00
committed by GitHub
parent be88a2ca1f
commit eb9cdd784c
11 changed files with 1122 additions and 536 deletions

View File

@@ -70,25 +70,53 @@ module.exports = {
seedDB: {
seed: process.env.MONGO_SEED === 'true',
options: {
logResults: process.env.MONGO_SEED_LOG_RESULTS !== 'false',
seedUser: {
username: process.env.MONGO_SEED_USER_USERNAME || 'seeduser',
provider: 'local',
email: process.env.MONGO_SEED_USER_EMAIL || 'user@localhost.com',
firstName: 'User',
lastName: 'Local',
displayName: 'User Local',
roles: ['user']
logResults: process.env.MONGO_SEED_LOG_RESULTS !== 'false'
},
// Order of collections in configuration will determine order of seeding.
// i.e. given these settings, the User seeds will be complete before
// Article seed is performed.
collections: [{
model: 'User',
docs: [{
data: {
username: 'local-admin',
email: 'admin@localhost.com',
firstName: 'Admin',
lastName: 'Local',
roles: ['admin', 'user']
}
}, {
// Set to true to overwrite this document
// when it already exists in the collection.
// If set to false, or missing, the seed operation
// will skip this document to avoid overwriting it.
overwrite: true,
data: {
username: 'local-user',
email: 'user@localhost.com',
firstName: 'User',
lastName: 'Local',
roles: ['user']
}
}]
}, {
model: 'Article',
options: {
// Override log results setting at the
// collection level.
logResults: true
},
seedAdmin: {
username: process.env.MONGO_SEED_ADMIN_USERNAME || 'seedadmin',
provider: 'local',
email: process.env.MONGO_SEED_ADMIN_EMAIL || 'admin@localhost.com',
firstName: 'Admin',
lastName: 'Local',
displayName: 'Admin Local',
roles: ['user', 'admin']
}
}
skip: {
// Skip collection when this query returns results.
// e.g. {}: Only seeds collection when it is empty.
when: {} // Mongoose qualified query
},
docs: [{
data: {
title: 'First Article',
content: 'This is a seeded Article for the development environment'
}
}]
}]
}
};

View File

@@ -91,25 +91,19 @@ module.exports = {
seedDB: {
seed: process.env.MONGO_SEED === 'true',
options: {
logResults: process.env.MONGO_SEED_LOG_RESULTS !== 'false',
seedUser: {
username: process.env.MONGO_SEED_USER_USERNAME || 'seeduser',
provider: 'local',
email: process.env.MONGO_SEED_USER_EMAIL || 'user@localhost.com',
firstName: 'User',
lastName: 'Local',
displayName: 'User Local',
roles: ['user']
},
seedAdmin: {
username: process.env.MONGO_SEED_ADMIN_USERNAME || 'seedadmin',
provider: 'local',
email: process.env.MONGO_SEED_ADMIN_EMAIL || 'admin@localhost.com',
firstName: 'Admin',
lastName: 'Local',
displayName: 'Admin Local',
roles: ['user', 'admin']
}
}
logResults: process.env.MONGO_SEED_LOG_RESULTS !== 'false'
},
collections: [{
model: 'User',
docs: [{
data: {
username: 'local-admin',
email: 'admin@localhost.com',
firstName: 'Admin',
lastName: 'Local',
roles: ['admin', 'user']
}
}]
}]
}
};

54
config/env/test.js vendored
View File

@@ -80,25 +80,39 @@ module.exports = {
seedDB: {
seed: process.env.MONGO_SEED === 'true',
options: {
logResults: process.env.MONGO_SEED_LOG_RESULTS !== 'false',
seedUser: {
username: process.env.MONGO_SEED_USER_USERNAME || 'seeduser',
provider: 'local',
email: process.env.MONGO_SEED_USER_EMAIL || 'user@localhost.com',
firstName: 'User',
lastName: 'Local',
displayName: 'User Local',
roles: ['user']
},
seedAdmin: {
username: process.env.MONGO_SEED_ADMIN_USERNAME || 'seedadmin',
provider: 'local',
email: process.env.MONGO_SEED_ADMIN_EMAIL || 'admin@localhost.com',
firstName: 'Admin',
lastName: 'Local',
displayName: 'Admin Local',
roles: ['user', 'admin']
}
}
// Default to not log results for tests
logResults: process.env.MONGO_SEED_LOG_RESULTS === 'true'
},
collections: [{
model: 'User',
docs: [{
overwrite: true,
data: {
username: 'seedadmin',
email: 'admin@localhost.com',
firstName: 'Admin',
lastName: 'Local',
roles: ['admin', 'user']
}
}, {
overwrite: true,
data: {
username: 'seeduser',
email: 'user@localhost.com',
firstName: 'User',
lastName: 'Local',
roles: ['user']
}
}]
}, {
model: 'Article',
docs: [{
overwrite: true,
data: {
title: 'Test Article',
content: 'Code coverage test article!'
}
}]
}]
}
};

View File

@@ -7,7 +7,7 @@ var config = require('../config'),
mongooseService = require('./mongoose'),
express = require('./express'),
chalk = require('chalk'),
seed = require('./seed');
seed = require('./mongo-seed');
function seedDB() {
if (config.seedDB && config.seedDB.seed) {

153
config/lib/mongo-seed.js Normal file
View File

@@ -0,0 +1,153 @@
'use strict';
var _ = require('lodash'),
config = require('../config'),
mongoose = require('mongoose'),
chalk = require('chalk');
exports.start = start;
function start(seedConfig) {
return new Promise(function (resolve, reject) {
seedConfig = seedConfig || {};
var options = seedConfig.options || (config.seedDB ? _.clone(config.seedDB.options, true) : {});
var collections = seedConfig.collections || (config.seedDB ? _.clone(config.seedDB.collections, true) : []);
if (!collections.length) {
return resolve();
}
var seeds = collections
.filter(function (collection) {
return collection.model;
});
// Use the reduction pattern to ensure we process seeding in desired order.
seeds.reduce(function (p, item) {
return p.then(function () {
return seed(item, options);
});
}, Promise.resolve()) // start with resolved promise for initial previous (p) item
.then(onSuccessComplete)
.catch(onError);
// Local Promise handlers
function onSuccessComplete() {
if (options.logResults) {
console.log();
console.log(chalk.bold.green('Database Seeding: Mongo Seed complete!'));
console.log();
}
return resolve();
}
function onError(err) {
if (options.logResults) {
console.log();
console.log(chalk.bold.red('Database Seeding: Mongo Seed Failed!'));
console.log(chalk.bold.red('Database Seeding: ' + err));
console.log();
}
return reject(err);
}
});
}
function seed(collection, options) {
// Merge options with collection options
options = _.merge(options || {}, collection.options || {});
return new Promise(function (resolve, reject) {
const Model = mongoose.model(collection.model);
const docs = collection.docs;
var skipWhen = collection.skip ? collection.skip.when : null;
if (!Model.seed) {
return reject(new Error('Database Seeding: Invalid Model Configuration - ' + collection.model + '.seed() not implemented'));
}
if (!docs || !docs.length) {
return resolve();
}
// First check if we should skip this collection
// based on the collection's "skip.when" option.
// NOTE: If it exists, "skip.when" should be a qualified
// Mongoose query that will be used with Model.find().
skipCollection()
.then(seedDocuments)
.then(function () {
return resolve();
})
.catch(function (err) {
return reject(err);
});
function skipCollection() {
return new Promise(function (resolve, reject) {
if (!skipWhen) {
return resolve(false);
}
Model
.find(skipWhen)
.exec(function (err, results) {
if (err) {
return reject(err);
}
if (results && results.length) {
return resolve(true);
}
return resolve(false);
});
});
}
function seedDocuments(skipCollection) {
return new Promise(function (resolve, reject) {
if (skipCollection) {
return onComplete([{ message: chalk.yellow('Database Seeding: ' + collection.model + ' collection skipped') }]);
}
var workload = docs
.filter(function (doc) {
return doc.data;
})
.map(function (doc) {
return Model.seed(doc.data, { overwrite: doc.overwrite });
});
Promise.all(workload)
.then(onComplete)
.catch(onError);
// Local Closures
function onComplete(responses) {
if (options.logResults) {
responses.forEach(function (response) {
if (response.message) {
console.log(chalk.magenta(response.message));
}
});
}
return resolve();
}
function onError(err) {
return reject(err);
}
});
}
});
}

View File

@@ -1,158 +0,0 @@
'use strict';
var _ = require('lodash'),
config = require('../config'),
mongoose = require('mongoose'),
chalk = require('chalk'),
crypto = require('crypto');
// global seed options object
var seedOptions = {};
function removeUser (user) {
return new Promise(function (resolve, reject) {
var User = mongoose.model('User');
User.find({ username: user.username }).remove(function (err) {
if (err) {
reject(new Error('Failed to remove local ' + user.username));
}
resolve();
});
});
}
function saveUser (user) {
return function() {
return new Promise(function (resolve, reject) {
// Then save the user
user.save(function (err, theuser) {
if (err) {
reject(new Error('Failed to add local ' + user.username));
} else {
resolve(theuser);
}
});
});
};
}
function checkUserNotExists (user) {
return new Promise(function (resolve, reject) {
var User = mongoose.model('User');
User.find({ username: user.username }, function (err, users) {
if (err) {
reject(new Error('Failed to find local account ' + user.username));
}
if (users.length === 0) {
resolve();
} else {
reject(new Error('Failed due to local account already exists: ' + user.username));
}
});
});
}
function reportSuccess (password) {
return function (user) {
return new Promise(function (resolve, reject) {
if (seedOptions.logResults) {
console.log(chalk.bold.red('Database Seeding:\t\t\tLocal ' + user.username + ' added with password set to ' + password));
}
resolve();
});
};
}
// save the specified user with the password provided from the resolved promise
function seedTheUser (user) {
return function (password) {
return new Promise(function (resolve, reject) {
var User = mongoose.model('User');
// set the new password
user.password = password;
if (user.username === seedOptions.seedAdmin.username && process.env.NODE_ENV === 'production') {
checkUserNotExists(user)
.then(saveUser(user))
.then(reportSuccess(password))
.then(function () {
resolve();
})
.catch(function (err) {
reject(err);
});
} else {
removeUser(user)
.then(saveUser(user))
.then(reportSuccess(password))
.then(function () {
resolve();
})
.catch(function (err) {
reject(err);
});
}
});
};
}
// report the error
function reportError (reject) {
return function (err) {
if (seedOptions.logResults) {
console.log();
console.log('Database Seeding:\t\t\t' + err);
console.log();
}
reject(err);
};
}
module.exports.start = function start(options) {
// Initialize the default seed options
seedOptions = _.clone(config.seedDB.options, true);
// Check for provided options
if (_.has(options, 'logResults')) {
seedOptions.logResults = options.logResults;
}
if (_.has(options, 'seedUser')) {
seedOptions.seedUser = options.seedUser;
}
if (_.has(options, 'seedAdmin')) {
seedOptions.seedAdmin = options.seedAdmin;
}
var User = mongoose.model('User');
return new Promise(function (resolve, reject) {
var adminAccount = new User(seedOptions.seedAdmin);
var userAccount = new User(seedOptions.seedUser);
// If production only seed admin if it does not exist
if (process.env.NODE_ENV === 'production') {
User.generateRandomPassphrase()
.then(seedTheUser(adminAccount))
.then(function () {
resolve();
})
.catch(reportError(reject));
} else {
// Add both Admin and User account
User.generateRandomPassphrase()
.then(seedTheUser(userAccount))
.then(User.generateRandomPassphrase)
.then(seedTheUser(adminAccount))
.then(function () {
resolve();
})
.catch(reportError(reject));
}
});
};

View File

@@ -380,6 +380,39 @@ gulp.task('dropdb', function (done) {
});
});
// Seed Mongo database based on configuration
gulp.task('mongo-seed', function (done) {
var db = require('./config/lib/mongoose');
var seed = require('./config/lib/mongo-seed');
// Open mongoose database connection
db.connect(function () {
db.loadModels();
seed
.start({
options: {
logResults: true
}
})
.then(function () {
// Disconnect and finish task
db.disconnect(done);
})
.catch(function (err) {
db.disconnect(function (disconnectError) {
if (disconnectError) {
console.log('Error disconnecting from the database, but was preceded by a Mongo Seed error.');
}
// Finish task with error
done(err);
});
});
});
});
// Downloads the selenium webdriver if protractor version is compatible
gulp.task('webdriver_update', webdriver_update);
@@ -451,3 +484,17 @@ gulp.task('default', function (done) {
gulp.task('prod', function (done) {
runSequence(['copyLocalEnvConfig', 'makeUploadsDir', 'templatecache'], 'build', 'env:prod', 'lint', ['nodemon-nodebug', 'watch'], done);
});
// Run Mongo Seed with default environment config
gulp.task('seed', function (done) {
runSequence('env:dev', 'mongo-seed', done);
});
// Run Mongo Seed with production environment config
gulp.task('seed:prod', function (done) {
runSequence('env:prod', 'mongo-seed', done);
});
gulp.task('seed:test', function (done) {
runSequence('env:test', 'mongo-seed', done);
});

View File

@@ -4,7 +4,10 @@
* Module dependencies
*/
var mongoose = require('mongoose'),
Schema = mongoose.Schema;
Schema = mongoose.Schema,
path = require('path'),
config = require(path.resolve('./config/config')),
chalk = require('chalk');
/**
* Article Schema
@@ -31,4 +34,105 @@ var ArticleSchema = new Schema({
}
});
ArticleSchema.statics.seed = seed;
mongoose.model('Article', ArticleSchema);
/**
* Seeds the User collection with document (Article)
* and provided options.
*/
function seed(doc, options) {
var Article = mongoose.model('Article');
return new Promise(function (resolve, reject) {
skipDocument()
.then(findAdminUser)
.then(add)
.then(function (response) {
return resolve(response);
})
.catch(function (err) {
return reject(err);
});
function findAdminUser(skip) {
var User = mongoose.model('User');
return new Promise(function (resolve, reject) {
if (skip) {
return resolve(true);
}
User
.findOne({
roles: { $in: ['admin'] }
})
.exec(function (err, admin) {
if (err) {
return reject(err);
}
doc.user = admin;
return resolve();
});
});
}
function skipDocument() {
return new Promise(function (resolve, reject) {
Article
.findOne({
title: doc.title
})
.exec(function (err, existing) {
if (err) {
return reject(err);
}
if (!existing) {
return resolve(false);
}
if (existing && !options.overwrite) {
return resolve(true);
}
// Remove Article (overwrite)
existing.remove(function (err) {
if (err) {
return reject(err);
}
return resolve(false);
});
});
});
}
function add(skip) {
return new Promise(function (resolve, reject) {
if (skip) {
return resolve({
message: chalk.yellow('Database Seeding: Article\t' + doc.title + ' skipped')
});
}
var article = new Article(doc);
article.save(function (err) {
if (err) {
return reject(err);
}
return resolve({
message: 'Database Seeding: Article\t' + article.title + ' added'
});
});
});
}
});
}

View File

@@ -12,8 +12,9 @@ var _ = require('lodash'),
request = require('supertest'),
config = require(path.resolve('./config/config')),
logger = require(path.resolve('./config/lib/logger')),
seed = require(path.resolve('./config/lib/seed')),
express = require(path.resolve('./config/lib/express'));
seed = require(path.resolve('./config/lib/mongo-seed')),
express = require(path.resolve('./config/lib/express')),
Article = mongoose.model('Article');
/**
* Globals
@@ -28,355 +29,666 @@ var app,
describe('Configuration Tests:', function () {
describe('Testing default seedDB', function () {
before(function(done) {
User.remove(function(err) {
should.not.exist(err);
describe('Testing Mongo Seed', function () {
var _seedConfig = _.clone(config.seedDB, true);
var articleSeedConfig;
var userSeedConfig;
var _admin;
var _user;
var _article;
user1 = {
username: 'user_config_test',
provider: 'local',
email: 'user_config_test_@localhost.com',
firstName: 'User',
lastName: 'Local',
displayName: 'User Local',
roles: ['user']
};
before(function (done) {
_admin = {
username: 'test-seed-admin',
email: 'test-admin@localhost.com',
firstName: 'Admin',
lastName: 'Test',
roles: ['admin', 'user']
};
admin1 = {
username: 'admin_config_test',
provider: 'local',
email: 'admin_config_test_@localhost.com',
firstName: 'Admin',
lastName: 'Local',
displayName: 'Admin Local',
roles: ['user', 'admin']
};
_user = {
username: 'test-seed-user',
email: 'test-user@localhost.com',
firstName: 'User',
lastName: 'Test',
roles: ['user']
};
userFromSeedConfig = config.seedDB.options.seedUser;
adminFromSeedConfig = config.seedDB.options.seedAdmin;
return done();
_article = {
title: 'Testing Database Seed Article',
content: 'Testing Article Seed right now!'
};
var articleCollections = _.filter(_seedConfig.collections, function (collection) {
return collection.model === 'Article';
});
});
after(function(done) {
User.remove(function(err) {
should.not.exist(err);
return done();
// articleCollections.should.be.instanceof(Array).and.have.lengthOf(1);
articleSeedConfig = articleCollections[0];
var userCollections = _.filter(_seedConfig.collections, function (collection) {
return collection.model === 'User';
});
// userCollections.should.be.instanceof(Array).and.have.lengthOf(1);
userSeedConfig = userCollections[0];
return done();
});
it('should have seedDB configuration set for "regular" user', function() {
(typeof userFromSeedConfig).should.not.equal('undefined');
should.exist(userFromSeedConfig.username);
should.exist(userFromSeedConfig.email);
});
it('should have seedDB configuration set for admin user', function() {
(typeof adminFromSeedConfig).should.not.equal('undefined');
should.exist(adminFromSeedConfig.username);
should.exist(adminFromSeedConfig.email);
});
it('should not be an admin user to begin with', function(done) {
User.find({ username: 'seedadmin' }, function(err, users) {
should.not.exist(err);
users.should.be.instanceof(Array).and.have.lengthOf(0);
return done();
});
});
it('should not be a "regular" user to begin with', function(done) {
User.find({ username: 'seeduser' }, function(err, users) {
should.not.exist(err);
users.should.be.instanceof(Array).and.have.lengthOf(0);
return done();
});
});
it('should seed ONLY the admin user account when NODE_ENV is set to "production"', function(done) {
// Save original value
var nodeEnv = process.env.NODE_ENV;
// Set node env ro production environment
process.env.NODE_ENV = 'production';
User.find({ username: adminFromSeedConfig.username }, function(err, users) {
// There shouldn't be any errors
should.not.exist(err);
users.should.be.instanceof(Array).and.have.lengthOf(0);
seed
.start({ logResults: false })
.then(function() {
User.find({ username: adminFromSeedConfig.username }, function(err, users) {
should.not.exist(err);
users.should.be.instanceof(Array).and.have.lengthOf(1);
var _admin = users.pop();
_admin.username.should.equal(adminFromSeedConfig.username);
// Restore original NODE_ENV environment variable
process.env.NODE_ENV = nodeEnv;
User.remove(function(err) {
should.not.exist(err);
return done();
});
});
});
});
});
it('should seed admin, and "regular" user accounts when NODE_ENV is set to "test"', function(done) {
// Save original value
var nodeEnv = process.env.NODE_ENV;
// Set node env ro production environment
process.env.NODE_ENV = 'test';
User.find({ username: adminFromSeedConfig.username }, function(err, users) {
// There shouldn't be any errors
should.not.exist(err);
users.should.be.instanceof(Array).and.have.lengthOf(0);
seed
.start({ logResults: false })
.then(function() {
User.find({ username: adminFromSeedConfig.username }, function(err, users) {
should.not.exist(err);
users.should.be.instanceof(Array).and.have.lengthOf(1);
var _admin = users.pop();
_admin.username.should.equal(adminFromSeedConfig.username);
User.find({ username: userFromSeedConfig.username }, function(err, users) {
should.not.exist(err);
users.should.be.instanceof(Array).and.have.lengthOf(1);
var _user = users.pop();
_user.username.should.equal(userFromSeedConfig.username);
// Restore original NODE_ENV environment variable
process.env.NODE_ENV = nodeEnv;
User.remove(function(err) {
should.not.exist(err);
return done();
});
});
});
});
});
});
it('should seed admin, and "regular" user accounts when NODE_ENV is set to "test" when they already exist', function (done) {
// Save original value
var nodeEnv = process.env.NODE_ENV;
// Set node env ro production environment
process.env.NODE_ENV = 'test';
var _user = new User(userFromSeedConfig);
var _admin = new User(adminFromSeedConfig);
_admin.save(function (err) {
// There shouldn't be any errors
should.not.exist(err);
_user.save(function (err) {
// There shouldn't be any errors
should.not.exist(err);
User.find({ username: { $in: [adminFromSeedConfig.username, userFromSeedConfig.username] } }, function (err, users) {
// There shouldn't be any errors
should.not.exist(err);
users.should.be.instanceof(Array).and.have.lengthOf(2);
seed
.start({ logResults: false })
.then(function () {
User.find({ username: { $in: [adminFromSeedConfig.username, userFromSeedConfig.username] } }, function (err, users) {
should.not.exist(err);
users.should.be.instanceof(Array).and.have.lengthOf(2);
// Restore original NODE_ENV environment variable
process.env.NODE_ENV = nodeEnv;
User.remove(function (err) {
should.not.exist(err);
return done();
});
});
});
});
afterEach(function (done) {
Article.remove().exec()
.then(function () {
return User.remove().exec();
})
.then(function () {
return done();
})
.catch(function (err) {
return done(err);
});
});
});
it('should ONLY seed admin user account when NODE_ENV is set to "production" with custom admin', function(done) {
it('should have default seed configuration set for articles', function (done) {
articleSeedConfig.should.be.instanceof(Object);
articleSeedConfig.docs.should.be.instanceof(Array).and.have.lengthOf(1);
should.exist(articleSeedConfig.docs[0].data.title);
should.exist(articleSeedConfig.docs[0].data.content);
// Save original value
var nodeEnv = process.env.NODE_ENV;
// Set node env ro production environment
process.env.NODE_ENV = 'production';
return done();
});
User.find({ username: admin1.username }, function(err, users) {
it('should have default seed configuration set for users', function (done) {
userSeedConfig.should.be.instanceof(Object);
userSeedConfig.docs.should.be.instanceof(Array).and.have.lengthOf(2);
// There shouldn't be any errors
should.not.exist(err);
users.should.be.instanceof(Array).and.have.lengthOf(0);
should.exist(userSeedConfig.docs[0].data.username);
should.exist(userSeedConfig.docs[0].data.email);
should.exist(userSeedConfig.docs[0].data.firstName);
should.exist(userSeedConfig.docs[0].data.lastName);
should.exist(userSeedConfig.docs[0].data.roles);
seed
.start({ logResults: false, seedAdmin: admin1 })
.then(function() {
User.find({ username: admin1.username }, function(err, users) {
should.not.exist(err);
users.should.be.instanceof(Array).and.have.lengthOf(1);
should.exist(userSeedConfig.docs[1].data.username);
should.exist(userSeedConfig.docs[1].data.email);
should.exist(userSeedConfig.docs[1].data.firstName);
should.exist(userSeedConfig.docs[1].data.lastName);
should.exist(userSeedConfig.docs[1].data.roles);
var _admin = users.pop();
_admin.username.should.equal(admin1.username);
return done();
});
// Restore original NODE_ENV environment variable
process.env.NODE_ENV = nodeEnv;
it('should seed data from default config', function (done) {
User.remove(function(err) {
should.not.exist(err);
return done();
});
});
seed.start()
.then(function () {
// Check Articles Seed
return Article.find().exec();
})
.then(function (articles) {
articles.should.be.instanceof(Array).and.have.lengthOf(articleSeedConfig.docs.length);
// Check Users Seed
return User.find().exec();
})
.then(function (users) {
users.should.be.instanceof(Array).and.have.lengthOf(userSeedConfig.docs.length);
return done();
})
.catch(done);
});
it('should overwrite existing article by default', function (done) {
articleSeedConfig.docs.should.be.instanceof(Array).and.have.lengthOf(1);
var article = new Article(articleSeedConfig.docs[0].data);
article.content = '_temp_test_article_';
// save temp article
article.save()
.then(function () {
return seed.start();
})
.then(function () {
return Article.find().exec();
})
.then(function (articles) {
articles.should.be.instanceof(Array).and.have.lengthOf(1);
var newArticle = articles.pop();
articleSeedConfig.docs[0].data.title.should.equal(newArticle.title);
articleSeedConfig.docs[0].data.content.should.equal(newArticle.content);
return done();
})
.catch(done);
});
it('should overwrite existing users by default', function (done) {
userSeedConfig.docs.should.be.instanceof(Array).and.have.lengthOf(2);
var admin = new User(userSeedConfig.docs[0].data);
admin.email = 'temp-admin@localhost.com';
admin.provider = 'local';
var user = new User(userSeedConfig.docs[1].data);
user.email = 'temp-user@localhost.com';
user.provider = 'local';
admin.save()
.then(function () {
return user.save();
})
.then(function () {
return User.find().exec();
})
.then(function (users) {
users.should.be.instanceof(Array).and.have.lengthOf(2);
// Start Seed
return seed.start();
})
.then(function () {
return User.find().exec();
})
.then(function (users) {
// Should still only be two users, since we removed
// the existing users before seeding again.
users.should.be.instanceof(Array).and.have.lengthOf(2);
return User.find({ username: admin.username }).exec();
})
.then(function (users) {
users.should.be.instanceof(Array).and.have.lengthOf(1);
var newAdmin = users.pop();
userSeedConfig.docs[0].data.username.should.equal(newAdmin.username);
userSeedConfig.docs[0].data.email.should.equal(newAdmin.email);
return User.find({ username: user.username }).exec();
})
.then(function (users) {
users.should.be.instanceof(Array).and.have.lengthOf(1);
var newUser = users.pop();
userSeedConfig.docs[1].data.username.should.equal(newUser.username);
userSeedConfig.docs[1].data.email.should.equal(newUser.email);
return done();
})
.catch(done);
});
it('should seed single article with custom options', function (done) {
seed
.start({
collections: [{
model: 'Article',
docs: [{
overwrite: true,
data: _article
}]
}]
})
.then(function () {
return Article.find().exec();
})
.then(function (articles) {
articles.should.be.instanceof(Array).and.have.lengthOf(1);
var newArticle = articles.pop();
_article.title.should.equal(newArticle.title);
_article.content.should.equal(newArticle.content);
return done();
})
.catch(done);
});
it('should seed single article with user set to custom seeded admin user', function (done) {
seed
.start({
collections: [{
model: 'User',
docs: [{
data: _admin
}]
}, {
model: 'Article',
docs: [{
overwrite: true,
data: _article
}]
}]
})
.then(function () {
return User.find().exec();
})
.then(function (users) {
users.should.be.instanceof(Array).and.have.lengthOf(1);
return Article
.find()
.populate('user', 'firstName lastName username email roles')
.exec();
})
.then(function (articles) {
articles.should.be.instanceof(Array).and.have.lengthOf(1);
var newArticle = articles.pop();
_article.title.should.equal(newArticle.title);
_article.content.should.equal(newArticle.content);
should.exist(newArticle.user);
should.exist(newArticle.user._id);
_admin.username.should.equal(newArticle.user.username);
_admin.email.should.equal(newArticle.user.email);
_admin.firstName.should.equal(newArticle.user.firstName);
_admin.lastName.should.equal(newArticle.user.lastName);
should.exist(newArticle.user.roles);
newArticle.user.roles.indexOf('admin').should.equal(_admin.roles.indexOf('admin'));
return done();
})
.catch(done);
});
it('should seed single article with NO user set due to seed order', function (done) {
seed
.start({
collections: [{
model: 'Article',
docs: [{
overwrite: true,
data: _article
}]
}, {
model: 'User',
docs: [{
data: _admin
}]
}]
})
.then(function () {
return User.find().exec();
})
.then(function (users) {
users.should.be.instanceof(Array).and.have.lengthOf(1);
return Article
.find()
.populate('user', 'firstName lastName username email roles')
.exec();
})
.then(function (articles) {
articles.should.be.instanceof(Array).and.have.lengthOf(1);
var newArticle = articles.pop();
_article.title.should.equal(newArticle.title);
_article.content.should.equal(newArticle.content);
should.not.exist(newArticle.user);
return done();
})
.catch(done);
});
it('should seed admin and user accounts with custom options', function (done) {
seed
.start({
collections: [{
model: 'User',
docs: [{
data: _admin
}, {
data: _user
}]
}]
})
.then(function () {
return User.find().exec();
})
.then(function (users) {
users.should.be.instanceof(Array).and.have.lengthOf(2);
return User.find({ username: _admin.username }).exec();
})
.then(function (users) {
users.should.be.instanceof(Array).and.have.lengthOf(1);
var newAdmin = users.pop();
_admin.username.should.equal(newAdmin.username);
_admin.email.should.equal(newAdmin.email);
return User.find({ username: _user.username }).exec();
})
.then(function (users) {
users.should.be.instanceof(Array).and.have.lengthOf(1);
var newUser = users.pop();
_user.username.should.equal(newUser.username);
_user.email.should.equal(newUser.email);
return done();
})
.catch(done);
});
it('should NOT overwrite existing article with custom options', function (done) {
var article = new Article(_article);
article.content = '_temp_article_content_';
article.save()
.then(function () {
return seed.start({
collections: [{
model: 'Article',
docs: [{
overwrite: false,
data: _article
}]
}]
});
});
})
.then(function () {
return Article.find().exec();
})
.then(function (articles) {
articles.should.be.instanceof(Array).and.have.lengthOf(1);
var existingArticle = articles.pop();
article.title.should.equal(existingArticle.title);
article.content.should.equal(existingArticle.content);
return done();
})
.catch(done);
});
it('should seed admin, and "regular" user accounts when NODE_ENV is set to "test" with custom options', function(done) {
it('should NOT overwrite existing user with custom options', function (done) {
var user = new User(_user);
user.provider = 'local';
user.email = 'temp-test-user@localhost.com';
// Save original value
var nodeEnv = process.env.NODE_ENV;
// Set node env ro production environment
process.env.NODE_ENV = 'test';
User.find({ username: admin1.username }, function(err, users) {
// There shouldn't be any errors
should.not.exist(err);
users.should.be.instanceof(Array).and.have.lengthOf(0);
seed
.start({ logResults: false, seedAdmin: admin1, seedUser: user1 })
.then(function() {
User.find({ username: admin1.username }, function(err, users) {
should.not.exist(err);
users.should.be.instanceof(Array).and.have.lengthOf(1);
var _admin = users.pop();
_admin.username.should.equal(admin1.username);
User.find({ username: user1.username }, function(err, users) {
should.not.exist(err);
users.should.be.instanceof(Array).and.have.lengthOf(1);
var _user = users.pop();
_user.username.should.equal(user1.username);
// Restore original NODE_ENV environment variable
process.env.NODE_ENV = nodeEnv;
User.remove(function(err) {
should.not.exist(err);
return done();
});
});
});
user.save()
.then(function () {
return seed.start({
collections: [{
model: 'User',
docs: [{
overwrite: false,
data: _user
}]
}]
});
});
})
.then(function () {
return User.find().exec();
})
.then(function (users) {
users.should.be.instanceof(Array).and.have.lengthOf(1);
var existingUser = users.pop();
user.username.should.equal(existingUser.username);
user.email.should.equal(existingUser.email);
return done();
})
.catch(done);
});
it('should NOT seed admin user account if it already exists when NODE_ENV is set to "production"', function(done) {
// Save original value
var nodeEnv = process.env.NODE_ENV;
// Set node env ro production environment
process.env.NODE_ENV = 'production';
var _admin = new User(adminFromSeedConfig);
_admin.save(function(err, user) {
// There shouldn't be any errors
should.not.exist(err);
user.username.should.equal(adminFromSeedConfig.username);
seed
.start({ logResults: false })
.then(function () {
// we don't ever expect to make it here but we don't want to timeout
User.remove(function(err) {
should.not.exist(err);
// force this test to fail since we should never be here
should.exist(undefined);
// Restore original NODE_ENV environment variable
process.env.NODE_ENV = nodeEnv;
return done();
});
})
.catch(function (err) {
should.exist(err);
err.message.should.equal('Failed due to local account already exists: ' + adminFromSeedConfig.username);
// Restore original NODE_ENV environment variable
process.env.NODE_ENV = nodeEnv;
User.remove(function(removeErr) {
should.not.exist(removeErr);
return done();
});
});
});
});
it('should NOT seed "regular" user account if missing email when NODE_ENV set to "test"', function (done) {
// Save original value
var nodeEnv = process.env.NODE_ENV;
// Set node env ro test environment
process.env.NODE_ENV = 'test';
var _user = new User(user1);
_user.email = '';
it('should NOT seed article when missing title with custom options', function (done) {
var invalid = {
content: '_temp_article_content_'
};
seed
.start({ logResults: false, seedUser: _user })
.start({
collections: [{
model: 'Article',
docs: [{
data: invalid
}]
}]
})
.then(function () {
// we don't ever expect to make it here but we don't want to timeout
User.remove(function(err) {
// force this test to fail since we should never be here
should.exist(undefined);
// Restore original NODE_ENV environment variable
process.env.NODE_ENV = nodeEnv;
return done();
});
// We should not make it here so we
// force an assert failure to prevent hangs.
should.exist(undefined);
return done();
})
.catch(function (err) {
should.exist(err);
err.message.should.equal('Failed to add local ' + user1.username);
err.message.should.equal('Article validation failed: title: Title cannot be blank');
// Restore original NODE_ENV environment variable
process.env.NODE_ENV = nodeEnv;
return done();
});
});
User.remove(function(removeErr) {
should.not.exist(removeErr);
it('should NOT seed user when missing username with custom options', function (done) {
var invalid = _.clone(_user, true);
invalid.username = undefined;
return done();
seed
.start({
collections: [{
model: 'User',
docs: [{
data: invalid
}]
}]
})
.then(function () {
// We should not make it here so we
// force an assert failure to prevent hangs.
should.exist(undefined);
return done();
})
.catch(function (err) {
should.exist(err);
err.message.should.equal('User validation failed: username: Please fill in a username');
return done();
});
});
it('should NOT seed user when missing email with custom options', function (done) {
var invalid = _.clone(_user, true);
invalid.email = undefined;
seed
.start({
collections: [{
model: 'User',
docs: [{
data: invalid
}]
}]
})
.then(function () {
// We should not make it here so we
// force an assert failure to prevent hangs.
should.exist(undefined);
return done();
})
.catch(function (err) {
should.exist(err);
err.message.should.equal('User validation failed: email: Please fill a valid email address');
return done();
});
});
it('should NOT seed user with invalid email with custom options', function (done) {
var invalid = _.clone(_user, true);
invalid.email = '...invalid-email...';
seed
.start({
collections: [{
model: 'User',
docs: [{
data: invalid
}]
}]
})
.then(function () {
// We should not make it here so we
// force an assert failure to prevent hangs.
should.exist(undefined);
return done();
})
.catch(function (err) {
should.exist(err);
err.message.should.equal('User validation failed: email: Please fill a valid email address');
return done();
});
});
it('should NOT continue seed when empty collections config', function (done) {
seed
.start({
collections: []
})
.then(function () {
return Article.find().exec();
})
.then(function (articles) {
articles.should.be.instanceof(Array).and.have.lengthOf(0);
return User.find().exec();
})
.then(function (users) {
users.should.be.instanceof(Array).and.have.lengthOf(0);
return done();
})
.catch(done);
});
it('should NOT seed any data when empty docs config', function (done) {
seed
.start({
collections: [{
model: 'Article',
docs: []
}]
})
.then(function () {
return Article.find().exec();
})
.then(function (articles) {
articles.should.be.instanceof(Array).and.have.lengthOf(0);
return User.find().exec();
})
.then(function (users) {
users.should.be.instanceof(Array).and.have.lengthOf(0);
return done();
})
.catch(done);
});
it('should seed article with custom options & skip.when results are empty', function (done) {
seed
.start({
collections: [{
model: 'Article',
skip: {
when: { title: 'should-not-find-this-title' }
},
docs: [{
data: _article
}]
}]
})
.then(function () {
return Article.find().exec();
})
.then(function (articles) {
articles.should.be.instanceof(Array).and.have.lengthOf(1);
var newArticle = articles.pop();
_article.title.should.be.equal(newArticle.title);
_article.content.should.be.equal(newArticle.content);
return done();
})
.catch(done);
});
it('should skip seed on collection with custom options & skip.when has results', function (done) {
var article = new Article({
title: 'temp-article-title',
content: 'temp-article-content'
});
article
.save()
.then(function () {
return Article.find().exec();
})
.then(function (articles) {
articles.should.be.instanceof(Array).and.have.lengthOf(1);
var newArticle = articles.pop();
article.title.should.equal(newArticle.title);
article.content.should.equal(newArticle.content);
return seed.start({
collections: [{
model: 'Article',
skip: {
when: { title: newArticle.title }
},
docs: [{
data: _article
}]
}]
});
})
.then(function () {
return Article.find().exec();
})
.then(function (articles) {
// We should have the same article added at start of this unit test.
articles.should.be.instanceof(Array).and.have.lengthOf(1);
var existingArticle = articles.pop();
article.title.should.equal(existingArticle.title);
article.content.should.equal(existingArticle.content);
return done();
})
.catch(done);
});
it('should fail seed with custom options & invalid skip.when query', function (done) {
seed
.start({
collections: [{
model: 'Article',
skip: {
when: { created: 'not-a-valid-date' }
},
docs: [{
data: _article
}]
}]
})
.then(function () {
// We should not get here
should.exist(undefined);
return done();
})
.catch(function (err) {
should.exist(err);
// We expect the error message to include
err.message.indexOf('Cast to date failed').should.equal(0);
return done();
});
});
});

View File

@@ -10,7 +10,8 @@ var mongoose = require('mongoose'),
crypto = require('crypto'),
validator = require('validator'),
generatePassword = require('generate-password'),
owasp = require('owasp-password-strength-test');
owasp = require('owasp-password-strength-test'),
chalk = require('chalk');
owasp.config(config.shared.owasp);
@@ -230,4 +231,92 @@ UserSchema.statics.generateRandomPassphrase = function () {
});
};
UserSchema.statics.seed = seed;
mongoose.model('User', UserSchema);
/**
* Seeds the User collection with document (User)
* and provided options.
*/
function seed(doc, options) {
var User = mongoose.model('User');
return new Promise(function (resolve, reject) {
skipDocument()
.then(add)
.then(function (response) {
return resolve(response);
})
.catch(function (err) {
return reject(err);
});
function skipDocument() {
return new Promise(function (resolve, reject) {
User
.findOne({
username: doc.username
})
.exec(function (err, existing) {
if (err) {
return reject(err);
}
if (!existing) {
return resolve(false);
}
if (existing && !options.overwrite) {
return resolve(true);
}
// Remove User (overwrite)
existing.remove(function (err) {
if (err) {
return reject(err);
}
return resolve(false);
});
});
});
}
function add(skip) {
return new Promise(function (resolve, reject) {
if (skip) {
return resolve({
message: chalk.yellow('Database Seeding: User\t\t' + doc.username + ' skipped')
});
}
User.generateRandomPassphrase()
.then(function (passphrase) {
var user = new User(doc);
user.provider = 'local';
user.displayName = user.firstName + ' ' + user.lastName;
user.password = passphrase;
user.save(function (err) {
if (err) {
return reject(err);
}
return resolve({
message: 'Database Seeding: User\t\t' + user.username + ' added with password set to ' + passphrase
});
});
})
.catch(function (err) {
return reject(err);
});
});
}
});
}

View File

@@ -31,7 +31,10 @@
"test:e2e": "gulp test:e2e",
"test:coverage": "gulp test:coverage",
"postinstall": "npm run bower",
"generate-ssl-certs": "scripts/generate-ssl-certs.sh"
"generate-ssl-certs": "scripts/generate-ssl-certs.sh",
"seed": "gulp seed",
"seed:prod": "gulp seed:prod",
"seed:test": "gulp seed:test"
},
"dependencies": {
"acl": "~0.4.10",