require('../../core/server/overrides'); // Utility Packages const Promise = require('bluebird'); const {sequence} = require('@tryghost/promise'); const _ = require('lodash'); const fs = require('fs-extra'); const path = require('path'); const os = require('os'); const uuid = require('uuid'); const KnexMigrator = require('knex-migrator'); const knexMigrator = new KnexMigrator(); // Ghost Internals const config = require('../../core/shared/config'); const express = require('../../core/shared/express'); const ghost = require('../../core/server'); const GhostServer = require('../../core/server/ghost-server'); const {events} = require('../../core/server/lib/common'); const db = require('../../core/server/data/db'); const models = require('../../core/server/models'); const urlUtils = require('../../core/shared/url-utils'); const urlService = require('../../core/frontend/services/url'); const settingsService = require('../../core/server/services/settings'); const frontendSettingsService = require('../../core/frontend/services/settings'); const settingsCache = require('../../core/server/services/settings/cache'); const web = require('../../core/server/web'); const themes = require('../../core/frontend/services/themes'); // Other Test Utilities const APIUtils = require('./api'); const configUtils = require('./configUtils'); const dbUtils = require('./db-utils'); const fixtureUtils = require('./fixture-utils'); const urlServiceUtils = require('./url-service-utils'); const oldIntegrationUtils = require('./old-integration-utils'); const redirects = require('./redirects'); const cacheRules = require('./fixtures/cache-rules'); const context = require('./fixtures/context'); const DataGenerator = require('./fixtures/data-generator'); const filterData = require('./fixtures/filter-param'); // Require additional assertions which help us keep our tests small and clear require('./assertions'); // ## Test Setup and Teardown const initFixtures = function initFixtures() { const options = _.merge({init: true}, _.transform(arguments, function (result, val) { result[val] = true; })); const fixtureOps = fixtureUtils.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} */ const setup = function setup() { /*eslint no-invalid-this: "off"*/ const self = this; const args = arguments; return function innerSetup() { models.init(); return initFixtures.apply(self, args); }; }; const createUser = function createUser(options) { const user = options.user; const role = options.role; return models.Role.fetchAll(context.internal) .then(function (roles) { roles = roles.toJSON(); user.roles = [_.find(roles, {name: role})]; return models.User.add(user, context.internal) .then(function () { return user; }); }); }; const createPost = function createPost(options) { const 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, context.internal); }; const createEmail = function createEmail(options) { const email = DataGenerator.forKnex.createEmail(options.email); return models.Email.add(email, context.internal); }; const createEmailedPost = async function createEmailedPost({postOptions, emailOptions}) { const post = await createPost(postOptions); emailOptions.email.post_id = post.id; const email = await createEmail(emailOptions); return {post, email}; }; let ghostServer; const dirtyDataFunction = () => { /** * @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.User.findAll({columns: ['id', 'email']}); }) .then((users) => { module.exports.existingData.users = users.toJSON(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(context.internal); }); }; /** * 1. reset & init db * 2. start the server once * * @TODO: tidy up the tmp folders */ const startGhost = async function startGhost(options) { console.time('Start Ghost'); // eslint-disable-line no-console options = _.merge({ redirectsFile: true, redirectsFileExt: '.json', forceStart: false, copyThemes: true, copySettings: true, contentFolder: path.join(os.tmpdir(), uuid.v4(), 'ghost-test'), subdir: false }, options); const contentFolderForTests = options.contentFolder; let 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) { redirects.setupFile(contentFolderForTests, options.redirectsFileExt); } 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) { await dbUtils.teardown(); await knexMigrator.init({only: 2}); settingsCache.shutdown(); await settingsService.init(); await frontendSettingsService.init(); await themes.init(); urlServiceUtils.reset(); await urlServiceUtils.isFinished(); web.shared.middlewares.customRedirects.reload(); events.emit('server.start'); await dirtyDataFunction(); console.log('Restart Mode'); // eslint-disable-line no-console console.timeEnd('Start Ghost'); // eslint-disable-line no-console return ghostServer; } await knexMigrator.reset({force: true}); if (ghostServer && ghostServer.httpServer) { await ghostServer.stop(); } settingsCache.shutdown(); settingsCache.reset(); await knexMigrator.init(); if (config.get('database:client') === 'sqlite3') { await db.knex.raw('PRAGMA journal_mode = TRUNCATE;'); } urlService.resetGenerators(); ghostServer = await ghost(); if (options.subdir) { parentApp = express('test parent'); parentApp.use(urlUtils.getSubdir(), ghostServer.rootApp); await ghostServer.start(parentApp); } else { await ghostServer.start(); } GhostServer.announceServerReadiness(); await urlServiceUtils.isFinished({disableDbReadyEvent: true}); await dirtyDataFunction(); console.log('Fresh Start Mode'); // eslint-disable-line no-console console.timeEnd('Start Ghost'); // eslint-disable-line no-console return ghostServer; }; module.exports = { startGhost: startGhost, stopGhost: async () => { if (ghostServer && ghostServer.httpServer) { await ghostServer.stop(); urlService.resetGenerators(); } }, teardownDb: dbUtils.teardown, truncate: dbUtils.truncate, setup: setup, createUser: createUser, createPost: createPost, createEmailedPost, integrationTesting: oldIntegrationUtils, /** * renderObject: res.render(view, dbResponse) * templateOptions: hbs.updateTemplateOptions(...) */ createHbsResponse: function createHbsResponse(options) { const renderObject = options.renderObject || {}; const templateOptions = options.templateOptions; const locals = options.locals || {}; const 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: dbUtils.initData, clearData: dbUtils.clearData, setupRedirectsFile: redirects.setupFile, fixtures: fixtureUtils.fixtures, DataGenerator: DataGenerator, filterData: filterData, API: APIUtils({getFixtureOps: fixtureUtils.getFixtureOps}), // Helpers to make it easier to write tests which are easy to read context: context, 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: cacheRules };