diff --git a/config/env/development.js b/config/env/development.js index 42cbecf3..a125670a 100644 --- a/config/env/development.js +++ b/config/env/development.js @@ -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' + } + }] + }] } }; diff --git a/config/env/production.js b/config/env/production.js index c43281fa..268c1f23 100644 --- a/config/env/production.js +++ b/config/env/production.js @@ -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'] + } + }] + }] } }; diff --git a/config/env/test.js b/config/env/test.js index 0cbfb6bc..e2c74f01 100644 --- a/config/env/test.js +++ b/config/env/test.js @@ -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!' + } + }] + }] } }; diff --git a/config/lib/app.js b/config/lib/app.js index 3321e78e..e9f38b08 100644 --- a/config/lib/app.js +++ b/config/lib/app.js @@ -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) { diff --git a/config/lib/mongo-seed.js b/config/lib/mongo-seed.js new file mode 100644 index 00000000..8762d1e8 --- /dev/null +++ b/config/lib/mongo-seed.js @@ -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); + } + }); + } + }); +} diff --git a/config/lib/seed.js b/config/lib/seed.js deleted file mode 100644 index eca41888..00000000 --- a/config/lib/seed.js +++ /dev/null @@ -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)); - } - }); -}; diff --git a/gulpfile.js b/gulpfile.js index 4a234cd1..1d4784f5 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -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); +}); diff --git a/modules/articles/server/models/article.server.model.js b/modules/articles/server/models/article.server.model.js index 8a557394..cc8f689d 100644 --- a/modules/articles/server/models/article.server.model.js +++ b/modules/articles/server/models/article.server.model.js @@ -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' + }); + }); + }); + } + }); +} diff --git a/modules/core/tests/server/core.server.config.tests.js b/modules/core/tests/server/core.server.config.tests.js index c4f3159e..f5e83043 100644 --- a/modules/core/tests/server/core.server.config.tests.js +++ b/modules/core/tests/server/core.server.config.tests.js @@ -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(); }); }); }); diff --git a/modules/users/server/models/user.server.model.js b/modules/users/server/models/user.server.model.js index 1dc4fc24..940085c4 100644 --- a/modules/users/server/models/user.server.model.js +++ b/modules/users/server/models/user.server.model.js @@ -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); + }); + }); + } + + }); +} diff --git a/package.json b/package.json index cd8d3c23..5e04ed1a 100644 --- a/package.json +++ b/package.json @@ -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",