var Promise = require('bluebird'), _ = require('lodash'), fs = require('fs-extra'), path = require('path'), Module = require('module'), os = require('os'), express = require('express'), debug = require('ghost-ignition').debug('test'), ObjectId = require('bson-objectid'), uuid = require('uuid'), KnexMigrator = require('knex-migrator'), ghost = require('../../server'), GhostServer = require('../../server/ghost-server'), api = require('../../server/api'), common = require('../../server/lib/common'), fixtureUtils = require('../../server/data/schema/fixtures/utils'), db = require('../../server/data/db'), schema = require('../../server/data/schema').tables, schemaTables = Object.keys(schema), models = require('../../server/models'), urlUtils = require('../../server/lib/url-utils'), urlService = require('../../frontend/services/url'), routingService = require('../../frontend/services/routing'), settingsService = require('../../server/services/settings'), frontendSettingsService = require('../../frontend/services/settings'), settingsCache = require('../../server/services/settings/cache'), imageLib = require('../../server/lib/image'), web = require('../../server/web'), permissions = require('../../server/services/permissions'), sequence = require('../../server/lib/promise/sequence'), themes = require('../../frontend/services/themes'), DataGenerator = require('./fixtures/data-generator'), configUtils = require('./configUtils'), filterData = require('./fixtures/filter-param'), APIUtils = require('./api'), mocks = require('./mocks'), config = require('../../server/config'), knexMigrator = new KnexMigrator(), fixtures, getFixtureOps, toDoList, originalRequireFn, postsInserted = 0, mockNotExistingModule, unmockNotExistingModule, teardown, setup, truncate, createUser, createPost, startGhost, initFixtures, initData, clearData, clearBruteData; // Require additional assertions which help us keep our tests small and clear require('./assertions'); /** TEST FIXTURES **/ fixtures = { insertPosts: function insertPosts(posts) { return Promise.map(posts, function (post) { return models.Post.add(post, module.exports.context.internal); }); }, insertPostsAndTags: function insertPostsAndTags() { return Promise.map(DataGenerator.forKnex.tags, function (tag) { return models.Tag.add(tag, module.exports.context.internal); }).then(function () { return Promise.each(_.cloneDeep(DataGenerator.forKnex.posts), function (post) { let postTagRelations = _.filter(DataGenerator.forKnex.posts_tags, {post_id: post.id}); let postAuthorsRelations = _.filter(DataGenerator.forKnex.posts_authors, {post_id: post.id}); postTagRelations = _.map(postTagRelations, function (postTagRelation) { return _.find(DataGenerator.forKnex.tags, {id: postTagRelation.tag_id}); }); postAuthorsRelations = _.map(postAuthorsRelations, function (postAuthorsRelation) { return _.find(DataGenerator.forKnex.users, {id: postAuthorsRelation.author_id}); }); post.tags = postTagRelations; post.authors = postAuthorsRelations; return models.Post.add(post, module.exports.context.internal); }); }); }, insertMultiAuthorPosts: function insertMultiAuthorPosts() { let i, j, k = 0, posts = []; const count = 25; // insert users of different roles return Promise.resolve(fixtures.createUsersWithRoles()).then(function () { return Promise.map(DataGenerator.forKnex.tags, function (tag) { return models.Tag.add(tag, module.exports.context.internal); }); }).then(function () { return Promise.all([ models.User.fetchAll(_.merge({columns: ['id']}, module.exports.context.internal)), models.Tag.fetchAll(_.merge({columns: ['id']}, module.exports.context.internal)) ]); }).then(function (results) { let users = results[0], tags = results[1]; tags = tags.toJSON(); users = users.toJSON(); users = _.map(users, 'id'); // Let's insert posts with random authors for (i = 0; i < count; i += 1) { const author = users[i % users.length]; posts.push(DataGenerator.forKnex.createGenericPost(k, null, null, author)); k = k + 1; } return Promise.map(posts, function (post, index) { posts[index].authors = [{id: posts[index].author_id}]; posts[index].tags = [tags[Math.floor(Math.random() * (tags.length - 1))]]; return models.Post.add(posts[index], module.exports.context.internal); }); }); }, insertExtraPosts: function insertExtraPosts(max) { var lang, status, posts = [], i, j, k = postsInserted; max = max || 50; for (i = 0; i < 2; i += 1) { lang = i % 2 ? 'en' : 'fr'; posts.push(DataGenerator.forKnex.createGenericPost(k, null, lang)); k = k + 1; for (j = 0; j < max; j += 1) { status = j % 2 ? 'draft' : 'published'; posts.push(DataGenerator.forKnex.createGenericPost(k, status, lang)); k = k + 1; } } // Keep track so we can run this function again safely postsInserted = k; return models.User.getOwnerUser(module.exports.context.internal) .then(function (ownerUser) { return Promise.map(posts, function (post, index) { posts[index].authors = [ownerUser.toJSON()]; return models.Post.add(posts[index], module.exports.context.internal); }); }); }, insertTags: function insertTags() { return Promise.map(DataGenerator.forKnex.tags, function (tag) { return models.Tag.add(tag, module.exports.context.internal); }); }, insertExtraTags: function insertExtraTags(max) { max = max || 50; var tags = [], tagName, i; for (i = 0; i < max; i += 1) { tagName = uuid.v4().split('-')[0]; tags.push(DataGenerator.forKnex.createBasic({name: tagName, slug: tagName})); } return Promise.map(tags, function (tag, index) { return models.Tag.add(tags[index], module.exports.context.internal); }); }, insertExtraPostsTags: function insertExtraPostsTags(max) { max = max || 50; return Promise.all([ models.Post.fetchAll(_.merge({columns: ['id'], withRelated: 'tags'}, module.exports.context.internal)), models.Tag.fetchAll(_.merge({columns: ['id', 'name']}, module.exports.context.internal)) ]).then(function (results) { let posts = results[0].toJSON(); let tags = results[1].toJSON(); const injectionTagId = _.chain(tags) .filter({name: 'injection'}) .map('id') .value()[0]; if (max > posts.length) { throw new Error('Trying to add more posts_tags than the number of posts.'); } return Promise.map(posts.slice(0, max), function (post) { post.tags = post.tags ? post.tags : []; return models.Post.edit({ tags: post.tags.concat([_.find(DataGenerator.Content.tags, {id: injectionTagId})]) }, _.merge({id: post.id}, module.exports.context.internal)); }); }); }, insertRoles: function insertRoles() { return Promise.map(DataGenerator.forKnex.roles, function (role) { return models.Role.add(role, module.exports.context.internal); }); }, initOwnerUser: function initOwnerUser() { var user = DataGenerator.Content.users[0]; user = DataGenerator.forKnex.createBasic(user); user = _.extend({}, user, {status: 'inactive'}); return Promise.map(DataGenerator.forKnex.roles, function (role) { return models.Role.add(role, module.exports.context.internal); }).then(function () { const userRolesRelation = _.cloneDeep(DataGenerator.forKnex.roles_users[0]); user.roles = _.filter(DataGenerator.forKnex.roles, {id: userRolesRelation.role_id}); return models.User.add(user, module.exports.context.internal); }); }, insertOwnerUser: function insertOwnerUser() { const user = _.cloneDeep(DataGenerator.forKnex.users[0]); user.roles = [DataGenerator.forKnex.roles[3]]; return models.User.add(user, module.exports.context.internal); }, overrideOwnerUser: function overrideOwnerUser(slug) { return models.User.getOwnerUser(module.exports.context.internal) .then(function (ownerUser) { var user = DataGenerator.forKnex.createUser(DataGenerator.Content.users[0]); if (slug) { user.slug = slug; } return models.User.edit(user, _.merge({id: ownerUser.id}, module.exports.context.internal)); }); }, changeOwnerUserStatus: function changeOwnerUserStatus(options) { return models.User.getOwnerUser(module.exports.context.internal) .then(function (user) { return models.User.edit({status: options.status}, _.merge({id: user.id}, module.exports.context.internal)); }); }, createUsersWithRoles: function createUsersWithRoles() { return Promise.map(DataGenerator.forKnex.roles, function (role) { return models.Role.add(role, module.exports.context.internal); }).then(function () { return Promise.map(_.cloneDeep(DataGenerator.forKnex.users), function (user) { let userRolesRelations = _.filter(DataGenerator.forKnex.roles_users, {user_id: user.id}); userRolesRelations = _.map(userRolesRelations, function (userRolesRelation) { return _.find(DataGenerator.forKnex.roles, {id: userRolesRelation.role_id}); }); user.roles = userRolesRelations; return models.User.add(user, module.exports.context.internal); }); }); }, resetRoles: function resetRoles() { return Promise.map(_.cloneDeep(DataGenerator.forKnex.users), function (user) { let userRolesRelations = _.filter(DataGenerator.forKnex.roles_users, {user_id: user.id}); userRolesRelations = _.map(userRolesRelations, function (userRolesRelation) { return _.find(DataGenerator.forKnex.roles, {id: userRolesRelation.role_id}); }); user.roles = userRolesRelations; return models.User.edit(user, _.merge({id: user.id}, module.exports.context.internal)); }); }, createUsersWithoutOwner: function createUsersWithoutOwner() { var usersWithoutOwner = _.cloneDeep(DataGenerator.forKnex.users.slice(1)); return Promise.map(usersWithoutOwner, function (user) { let userRolesRelations = _.filter(DataGenerator.forKnex.roles_users, {user_id: user.id}); userRolesRelations = _.map(userRolesRelations, function (userRolesRelation) { return _.find(DataGenerator.forKnex.roles, {id: userRolesRelation.role_id}); }); user.roles = userRolesRelations; return models.User.add(user, module.exports.context.internal); }); }, createInactiveUser() { const user = DataGenerator.forKnex.createUser({ email: 'inactive@test.org', slug: 'inactive', status: 'inactive' }); return models.User.add(user, module.exports.context.internal); }, createExtraUsers: function createExtraUsers() { // grab 3 more users var extraUsers = _.cloneDeep(DataGenerator.Content.users.slice(2, 6)); extraUsers = _.map(extraUsers, function (user) { return DataGenerator.forKnex.createUser(_.extend({}, user, { id: ObjectId.generate(), email: 'a' + user.email, slug: 'a' + user.slug })); }); const roles = {}; roles[extraUsers[0].id] = DataGenerator.Content.roles[0]; roles[extraUsers[1].id] = DataGenerator.Content.roles[1]; roles[extraUsers[2].id] = DataGenerator.Content.roles[2]; roles[extraUsers[3].id] = DataGenerator.Content.roles[4]; // @TODO: remove when overhauling test env // tests need access to the extra created users (especially to the created id) // replacement for admin2, editor2 etc DataGenerator.Content.extraUsers = extraUsers; return Promise.map(extraUsers, function (user) { user.roles = roles[user.id]; return models.User.add(user, module.exports.context.internal); }); }, insertOneUser: function insertOneUser(options) { options = options || {}; return models.User.add({ name: options.name, email: options.email, slug: options.slug, status: options.status }, module.exports.context.internal); }, // Creates a client, and access and refresh tokens for user with index or 2 by default createTokensForUser: function createTokensForUser(index) { return Promise.map(DataGenerator.forKnex.clients, function (client) { return models.Client.add(client, module.exports.context.internal); }).then(function () { return models.Accesstoken.add(DataGenerator.forKnex.createToken({ user_id: DataGenerator.Content.users[index || 2].id }), module.exports.context.internal); }).then(function () { return models.Refreshtoken.add(DataGenerator.forKnex.createToken({ user_id: DataGenerator.Content.users[index || 2].id }), module.exports.context.internal); }); }, insertOne: function insertOne(modelName, tableName, fn, index) { const obj = DataGenerator.forKnex[fn](DataGenerator.Content[tableName][index || 0]); return models[modelName].add(obj, module.exports.context.internal); }, getImportFixturePath: function (filename) { return path.resolve(__dirname + '/fixtures/import/' + filename); }, getExportFixturePath: function (filename) { var relativePath = '/fixtures/export/'; return path.resolve(__dirname + relativePath + filename + '.json'); }, loadExportFixture: function loadExportFixture(filename) { var filePath = this.getExportFixturePath(filename); return fs.readFile(filePath).then(function (fileContents) { var data; // Parse the json data try { data = JSON.parse(fileContents); } catch (e) { return new Error('Failed to parse the file'); } return data; }); }, permissionsFor: function permissionsFor(obj) { var permsToInsert = _.cloneDeep(fixtureUtils.findModelFixtures('Permission', {object_type: obj}).entries), permsRolesToInsert = fixtureUtils.findPermissionRelationsForObject(obj).entries, actions = [], permissionsRoles = {}, roles = { Administrator: DataGenerator.Content.roles[0].id, Editor: DataGenerator.Content.roles[1].id, Author: DataGenerator.Content.roles[2].id, Owner: DataGenerator.Content.roles[3].id, Contributor: DataGenerator.Content.roles[4].id, 'Admin Integration': DataGenerator.Content.roles[5].id }; // CASE: if empty db will throw SQLITE_MISUSE, hard to debug if (_.isEmpty(permsToInsert)) { return Promise.reject(new Error('no permission found:' + obj)); } permsToInsert = _.map(permsToInsert, function (perms) { perms.id = ObjectId.generate(); actions.push({type: perms.action_type, permissionId: perms.id}); return DataGenerator.forKnex.createBasic(perms); }); _.each(permsRolesToInsert, function (perms, role) { if (perms[obj]) { if (perms[obj] === 'all') { _.each(actions, function (action) { if (!permissionsRoles[action.permissionId]) { permissionsRoles[action.permissionId] = []; } permissionsRoles[action.permissionId].push(_.find(DataGenerator.Content.roles, {id: roles[role]})); }); } else { _.each(perms[obj], function (action) { if (!permissionsRoles[_.find(actions, {type: action}).permissionId]) { permissionsRoles[_.find(actions, {type: action}).permissionId] = []; } permissionsRoles[_.find(actions, {type: action}).permissionId].push(_.find(DataGenerator.Content.roles, {id: roles[role]})); }); } } }); return Promise.map(permsToInsert, function (perm) { if (!_.isEmpty(permissionsRoles)) { perm.roles = permissionsRoles[perm.id]; } return models.Permission.add(perm, module.exports.context.internal); }); }, insertClients: function insertClients() { return Promise.map(DataGenerator.forKnex.clients, function (client) { return models.Client.add(client, module.exports.context.internal); }); }, insertClientWithTrustedDomain: function insertClientWithTrustedDomain() { const client = DataGenerator.forKnex.createClient({slug: 'ghost-test'}); return models.Client.add(client, module.exports.context.internal) .then(function () { return models.ClientTrustedDomain.add(DataGenerator.forKnex.createTrustedDomain({ client_id: client.id }), module.exports.context.internal); }); }, insertAccessToken: function insertAccessToken(override) { return models.Accesstoken.insert(DataGenerator.forKnex.createToken(override), module.exports.context.internal); }, insertInvites: function insertInvites() { return Promise.map(DataGenerator.forKnex.invites, function (invite) { return models.Invite.add(invite, module.exports.context.internal); }); }, insertWebhooks: function insertWebhooks() { return Promise.map(DataGenerator.forKnex.webhooks, function (webhook) { return models.Webhook.add(webhook, module.exports.context.internal); }); }, insertIntegrations: function insertIntegrations() { return Promise.map(DataGenerator.forKnex.integrations, function (integration) { return models.Integration.add(integration, module.exports.context.internal); }); }, insertApiKeys: function insertApiKeys() { return Promise.map(DataGenerator.forKnex.api_keys, function (api_key) { return models.ApiKey.add(api_key, module.exports.context.internal); }); } }; /** Test Utility Functions **/ initData = function initData() { return knexMigrator.init() .then(function () { common.events.emit('db.ready'); let timeout; return new Promise(function (resolve) { (function retry() { clearTimeout(timeout); if (urlService.hasFinished()) { return resolve(); } timeout = setTimeout(retry, 50); })(); }); }); }; clearBruteData = function clearBruteData() { return db.knex('brute').truncate(); }; truncate = function truncate(tableName) { if (config.get('database:client') === 'sqlite3') { return db.knex(tableName).truncate(); } return db.knex.raw('SET FOREIGN_KEY_CHECKS=0;') .then(function () { return db.knex(tableName).truncate(); }) .then(function () { return db.knex.raw('SET FOREIGN_KEY_CHECKS=1;'); }); }; // we must always try to delete all tables clearData = function clearData() { debug('Database reset'); return knexMigrator.reset({force: true}) .then(function () { urlService.softReset(); }); }; toDoList = { app: function insertApp() { return fixtures.insertOne('App', 'apps', 'createApp'); }, app_field: function insertAppField() { // TODO: use the actual app ID to create the field return fixtures.insertOne('App', 'apps', 'createApp').then(function () { return fixtures.insertOne('AppField', 'app_fields', 'createAppField'); }); }, app_setting: function insertAppSetting() { // TODO: use the actual app ID to create the field return fixtures.insertOne('App', 'apps', 'createApp').then(function () { return fixtures.insertOne('AppSetting', 'app_settings', 'createAppSetting'); }); }, permission: function insertPermission() { return fixtures.insertOne('Permission', 'permissions', 'createPermission'); }, role: function insertRole() { return fixtures.insertOne('Role', 'roles', 'createRole'); }, roles: function insertRoles() { return fixtures.insertRoles(); }, tag: function insertTag() { return fixtures.insertOne('Tag', 'tags', 'createTag'); }, subscriber: function insertSubscriber() { return fixtures.insertOne('Subscriber', 'subscribers', 'createSubscriber'); }, posts: function insertPostsAndTags() { return fixtures.insertPostsAndTags(); }, 'posts:mu': function insertMultiAuthorPosts() { return fixtures.insertMultiAuthorPosts(); }, tags: function insertTags() { return fixtures.insertTags(); }, 'tags:extra': function insertExtraTags() { return fixtures.insertExtraTags(); }, apps: function insertApps() { return fixtures.insertApps(); }, settings: function populateSettings() { settingsCache.shutdown(); return settingsService.init(); }, 'users:roles': function createUsersWithRoles() { return fixtures.createUsersWithRoles(); }, 'users:no-owner': function createUsersWithoutOwner() { return fixtures.createUsersWithoutOwner(); }, 'user:inactive': function createInactiveUser() { return fixtures.createInactiveUser(); }, 'users:extra': function createExtraUsers() { return fixtures.createExtraUsers(); }, 'user-token': function createTokensForUser(index) { return fixtures.createTokensForUser(index); }, owner: function insertOwnerUser() { return fixtures.insertOwnerUser(); }, 'owner:pre': function initOwnerUser() { return fixtures.initOwnerUser(); }, 'owner:post': function overrideOwnerUser() { return fixtures.overrideOwnerUser(); }, 'perms:init': function initPermissions() { return permissions.init(); }, perms: function permissionsFor(obj) { return fixtures.permissionsFor(obj); }, clients: function insertClients() { return fixtures.insertClients(); }, 'client:trusted-domain': function insertClients() { return fixtures.insertClientWithTrustedDomain(); }, filter: function createFilterParamFixtures() { return filterData(DataGenerator); }, invites: function insertInvites() { return fixtures.insertInvites(); }, themes: function loadThemes() { return themes.loadAll(); }, webhooks: function insertWebhooks() { return fixtures.insertWebhooks(); }, integrations: function insertIntegrations() { return fixtures.insertIntegrations(); }, api_keys: function insertApiKeys() { return fixtures.insertApiKeys(); } }; /** * ## getFixtureOps * * Takes the arguments from a setup function and turns them into an array of promises to fullfil * * This is effectively a list of instructions with regard to which fixtures should be setup for this test. * * `default` - a special option which will cause the full suite of normal fixtures to be initialised * * `perms:init` - initialise the permissions object after having added permissions * * `perms:obj` - initialise permissions for a particular object type * * `users:roles` - create a full suite of users, one per role * @param {Object} toDos */ getFixtureOps = function getFixtureOps(toDos) { // default = default fixtures, if it isn't present, init with tables only var tablesOnly = !toDos.default, fixtureOps = []; // Database initialisation if (toDos.init || toDos.default) { fixtureOps.push(function initDB() { // skip adding all fixtures! if (tablesOnly) { return knexMigrator.init({skip: 2}); } return knexMigrator.init(); }); delete toDos.default; delete toDos.init; } // Go through our list of things to do, and add them to an array _.each(toDos, function (value, toDo) { var tmp; if ((toDo !== 'perms:init' && toDo.indexOf('perms:') !== -1) || toDo.indexOf('user-token:') !== -1) { tmp = toDo.split(':'); fixtureOps.push(function addCustomFixture() { return toDoList[tmp[0]](tmp[1]); }); } else { if (!toDoList[toDo]) { throw new Error('setup todo does not exist - spell mistake?'); } fixtureOps.push(toDoList[toDo]); } }); return fixtureOps; }; // ## Test Setup and Teardown initFixtures = function initFixtures() { var options = _.merge({init: true}, _.transform(arguments, function (result, val) { result[val] = true; })), fixtureOps = getFixtureOps(options); return sequence(fixtureOps); }; /** * ## Setup Integration Tests * Setup takes a list of arguments like: 'default', 'tag', 'perms:tag', 'perms:init' * Setup does 'init' (DB) by default * @returns {Function} */ setup = function setup() { /*eslint no-invalid-this: "off"*/ const self = this, args = arguments; return function setup() { models.init(); return initFixtures.apply(self, args); }; }; createUser = function createUser(options) { var user = options.user, role = options.role; return models.Role.fetchAll(module.exports.context.internal) .then(function (roles) { roles = roles.toJSON(); user.roles = [_.find(roles, {name: role})]; return models.User.add(user, module.exports.context.internal) .then(function () { return user; }); }); }; createPost = function createPost(options) { var post = DataGenerator.forKnex.createPost(options.post); if (options.author) { post.author_id = options.author.id; } post.authors = [{id: post.author_id}]; return models.Post.add(post, module.exports.context.internal); }; /** * Has to run in a transaction for MySQL, otherwise the foreign key check does not work. * Sqlite3 has no truncate command. */ teardown = function teardown() { debug('Database teardown'); urlService.softReset(); var tables = schemaTables.concat(['migrations']); if (config.get('database:client') === 'sqlite3') { return Promise .mapSeries(tables, function createTable(table) { return db.knex.raw('DELETE FROM ' + table + ';'); }) .catch(function (err) { // CASE: table does not exist if (err.errno === 1) { return Promise.resolve(); } throw err; }); } return db.knex.transaction(function (trx) { return db.knex.raw('SET FOREIGN_KEY_CHECKS=0;').transacting(trx) .then(function () { return Promise .each(tables, function createTable(table) { return db.knex.raw('TRUNCATE ' + table + ';').transacting(trx); }); }) .then(function () { return db.knex.raw('SET FOREIGN_KEY_CHECKS=1;').transacting(trx); }) .catch(function (err) { // CASE: table does not exist if (err.errno === 1146) { return Promise.resolve(); } throw err; }); }); }; /** * offer helper functions for mocking * we start with a small function set to mock non existent modules */ originalRequireFn = Module.prototype.require; mockNotExistingModule = function mockNotExistingModule(modulePath, module, error = false) { Module.prototype.require = function (path) { if (path.match(modulePath)) { if (error) { throw module; } return module; } return originalRequireFn.apply(this, arguments); }; }; unmockNotExistingModule = function unmockNotExistingModule() { Module.prototype.require = originalRequireFn; }; var ghostServer; /** * 1. reset & init db * 2. start the server once * * @TODO: tidy up the tmp folders */ startGhost = function startGhost(options) { options = _.merge({ redirectsFile: true, forceStart: false, copyThemes: true, copySettings: true, contentFolder: path.join(os.tmpdir(), uuid.v4(), 'ghost-test'), subdir: false }, options); var contentFolderForTests = options.contentFolder, parentApp; /** * We never use the root content folder for testing! * We use a tmp folder. */ configUtils.set('paths:contentPath', contentFolderForTests); fs.ensureDirSync(contentFolderForTests); fs.ensureDirSync(path.join(contentFolderForTests, 'data')); fs.ensureDirSync(path.join(contentFolderForTests, 'themes')); fs.ensureDirSync(path.join(contentFolderForTests, 'images')); fs.ensureDirSync(path.join(contentFolderForTests, 'logs')); fs.ensureDirSync(path.join(contentFolderForTests, 'adapters')); fs.ensureDirSync(path.join(contentFolderForTests, 'settings')); if (options.copyThemes) { // Copy all themes into the new test content folder. Default active theme is always casper. If you want to use a different theme, you have to set the active theme (e.g. stub) fs.copySync(path.join(__dirname, 'fixtures', 'themes'), path.join(contentFolderForTests, 'themes')); } if (options.redirectsFile) { fs.copySync(path.join(__dirname, 'fixtures', 'data', 'redirects.json'), path.join(contentFolderForTests, 'data', 'redirects.json')); } if (options.copySettings) { fs.copySync(path.join(__dirname, 'fixtures', 'settings', 'routes.yaml'), path.join(contentFolderForTests, 'settings', 'routes.yaml')); } // truncate database and re-run fixtures // we have to ensure that some components in Ghost are reloaded if (ghostServer && ghostServer.httpServer && !options.forceStart) { return teardown() .then(function () { return knexMigrator.init({only: 2}); }) .then(function () { settingsCache.shutdown(); return settingsService.init(); }) .then(function () { return frontendSettingsService.init(); }) .then(function () { return themes.init(); }) .then(function () { urlService.softReset(); common.events.emit('db.ready'); let timeout; return new Promise(function (resolve) { (function retry() { clearTimeout(timeout); if (urlService.hasFinished()) { return resolve(); } timeout = setTimeout(retry, 50); })(); }); }) .then(function () { web.shared.middlewares.customRedirects.reload(); common.events.emit('server.start'); /** * @TODO: this is dirty, but makes routing testing a lot easier for now, because the routing test * has no easy way to access existing resource id's, which are added from the Ghost fixtures. * I can do `testUtils.existingData.roles[0].id`. */ module.exports.existingData = {}; return models.Role.findAll({columns: ['id']}) .then((roles) => { module.exports.existingData.roles = roles.toJSON(); return models.Client.findAll({columns: ['id', 'secret']}); }) .then((clients) => { module.exports.existingData.clients = clients.toJSON(); return models.User.findAll({columns: ['id', 'email']}); }) .then((users) => { module.exports.existingData.users = users.toJSON(module.exports.context.internal); return models.Tag.findAll({columns: ['id']}); }) .then((tags) => { module.exports.existingData.tags = tags.toJSON(); return models.ApiKey.findAll({withRelated: 'integration'}); }) .then((keys) => { module.exports.existingData.apiKeys = keys.toJSON(module.exports.context.internal); }) .return(ghostServer); }); } return knexMigrator.reset({force: true}) .then(function () { if (ghostServer && ghostServer.httpServer) { return ghostServer.stop(); } }) .then(function initialiseDatabase() { settingsCache.shutdown(); settingsCache.reset(); return knexMigrator.init(); }) .then(function initializeGhost() { urlService.resetGenerators(); return ghost(); }) .then(function startGhost(_ghostServer) { ghostServer = _ghostServer; if (options.subdir) { parentApp = express(); parentApp.use(urlUtils.getSubdir(), ghostServer.rootApp); return ghostServer.start(parentApp); } return ghostServer.start(); }) .then(function () { let timeout; GhostServer.announceServerStart(); return new Promise(function (resolve) { (function retry() { clearTimeout(timeout); if (urlService.hasFinished()) { return resolve(); } timeout = setTimeout(retry, 50); })(); }); }) .then(function returnGhost() { /** * @TODO: this is dirty, but makes routing testing a lot easier for now, because the routing test * has no easy way to access existing resource id's, which are added from the Ghost fixtures. * I can do `testUtils.existingData.roles[0].id`. */ module.exports.existingData = {}; return models.Role.findAll({columns: ['id']}) .then((roles) => { module.exports.existingData.roles = roles.toJSON(); return models.Client.findAll({columns: ['id', 'secret']}); }) .then((clients) => { module.exports.existingData.clients = clients.toJSON(); return models.User.findAll({columns: ['id', 'email']}); }) .then((users) => { module.exports.existingData.users = users.toJSON(module.exports.context.internal); return models.Tag.findAll({columns: ['id']}); }) .then((tags) => { module.exports.existingData.tags = tags.toJSON(); return models.ApiKey.findAll({withRelated: 'integration'}); }) .then((keys) => { module.exports.existingData.apiKeys = keys.toJSON(); }) .return(ghostServer); }); }; module.exports = { startGhost: startGhost, stopGhost: () => { if (ghostServer && ghostServer.httpServer) { return ghostServer.stop() .then(() => { urlService.resetGenerators(); }); } }, integrationTesting: { overrideGhostConfig: function overrideGhostConfig(configUtils) { configUtils.set('paths:contentPath', path.join(__dirname, 'fixtures')); configUtils.set('times:getImageSizeTimeoutInMS', 1); }, defaultMocks: function defaultMocks(sandbox, options) { options = options || {}; configUtils.set('paths:contentPath', path.join(__dirname, 'fixtures')); const cacheStub = sandbox.stub(settingsCache, 'get'); cacheStub.withArgs('active_theme').returns(options.theme || 'casper'); cacheStub.withArgs('active_timezone').returns('Etc/UTC'); cacheStub.withArgs('permalinks').returns('/:slug/'); cacheStub.withArgs('labs').returns({publicAPI: true}); if (options.amp) { cacheStub.withArgs('amp').returns(true); } if (options.apps) { cacheStub.withArgs('active_apps').returns([]); } sandbox.stub(api.clients, 'read').returns(Promise.resolve({ clients: [ {slug: 'ghost-frontend', secret: 'a1bcde23cfe5', status: 'enabled'} ] })); sandbox.stub(imageLib.imageSize, 'getImageSizeFromUrl').resolves(); }, initGhost: function () { models.init(); settingsCache.shutdown(); return settingsService.init() .then(() => { return themes.init(); }); }, routing: { reset: function () { routingService.registry.resetAll(); } }, urlService: { waitTillFinished: function (options = {dbIsReady: false}) { let timeout; if (!options.dbIsReady) { common.events.emit('db.ready'); } return new Promise(function (resolve) { (function retry() { clearTimeout(timeout); if (urlService.hasFinished()) { return resolve(); } timeout = setTimeout(retry, 50); })(); }); }, init: function () { const routes = frontendSettingsService.get('routes'); const collectionRouter = new routingService.CollectionRouter('/', routes.collections['/']); const tagRouter = new routingService.TaxonomyRouter('tag', routes.taxonomies.tag); const authorRouter = new routingService.TaxonomyRouter('author', routes.taxonomies.author); common.events.emit('db.ready'); return this.waitTillFinished(); }, reset: function () { urlService.softReset(); }, resetGenerators: function () { urlService.resetGenerators(); urlService.resources.reset({ignoreDBReady: true}); } } }, teardown: teardown, truncate: truncate, setup: setup, createUser: createUser, createPost: createPost, mockNotExistingModule: mockNotExistingModule, unmockNotExistingModule: unmockNotExistingModule, /** * renderObject: res.render(view, dbResponse) * templateOptions: hbs.updateTemplateOptions(...) */ createHbsResponse: function createHbsResponse(options) { var renderObject = options.renderObject || {}, templateOptions = options.templateOptions, locals = options.locals || {}, hbsStructure = { data: { site: {}, config: {}, labs: {}, root: { _locals: {} } } }; _.merge(hbsStructure.data, templateOptions); _.merge(hbsStructure.data.root, renderObject); _.merge(hbsStructure.data.root, locals); hbsStructure.data.root._locals = locals; return hbsStructure; }, initFixtures: initFixtures, initData: initData, clearData: clearData, clearBruteData: clearBruteData, mocks: mocks, fixtures: fixtures, DataGenerator: DataGenerator, filterData: filterData, API: APIUtils({getFixtureOps: getFixtureOps}), // Helpers to make it easier to write tests which are easy to read context: { internal: {context: {internal: true}}, external: {context: {external: true}}, owner: {context: {user: DataGenerator.Content.users[0].id}}, admin: {context: {user: DataGenerator.Content.users[1].id}}, editor: {context: {user: DataGenerator.Content.users[2].id}}, author: {context: {user: DataGenerator.Content.users[3].id}}, contributor: {context: {user: DataGenerator.Content.users[7].id}}, admin_api_key: {context: {api_key: DataGenerator.Content.api_keys[0].id}}, content_api_key: {context: {api_key: DataGenerator.Content.api_keys[1].id}} }, permissions: { owner: {user: {roles: [DataGenerator.Content.roles[3]]}}, admin: {user: {roles: [DataGenerator.Content.roles[0]]}}, editor: {user: {roles: [DataGenerator.Content.roles[1]]}}, author: {user: {roles: [DataGenerator.Content.roles[2]]}}, contributor: {user: {roles: [DataGenerator.Content.roles[4]]}} }, users: { ids: { owner: DataGenerator.Content.users[0].id, admin: DataGenerator.Content.users[1].id, editor: DataGenerator.Content.users[2].id, author: DataGenerator.Content.users[3].id, admin2: DataGenerator.Content.users[6].id, editor2: DataGenerator.Content.users[4].id, author2: DataGenerator.Content.users[5].id, contributor: DataGenerator.Content.users[7].id, contributor2: DataGenerator.Content.users[8].id } }, roles: { ids: { owner: DataGenerator.Content.roles[3].id, admin: DataGenerator.Content.roles[0].id, editor: DataGenerator.Content.roles[1].id, author: DataGenerator.Content.roles[2].id, contributor: DataGenerator.Content.roles[4].id } }, cacheRules: { public: 'public, max-age=0', hour: 'public, max-age=' + 3600, day: 'public, max-age=' + 86400, year: 'public, max-age=' + 31536000, private: 'no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0' } };