mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-12-24 11:22:19 +03:00
70ed998838
refs dd715b33dc
- this is the last event that is used to trigger part of the standard boot process
- events make the code harder to read/reason about
- the urlservice is one of the most core and critical components in Ghost, possibly the biggest consumer of time and memory
- we want to have the work it is doing front and center so that we can improve it
380 lines
13 KiB
JavaScript
380 lines
13 KiB
JavaScript
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 boot = require('../../core/boot');
|
|
const {events} = require('../../core/server/lib/common');
|
|
const db = require('../../core/server/data/db');
|
|
const models = require('../../core/server/models');
|
|
const notify = require('../../core/server/notify');
|
|
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;
|
|
|
|
/**
|
|
* Because we use ObjectID we don't know the ID of fixtures ahead of time
|
|
* This function fetches all of our fixtures and exposes them so that tests can use them
|
|
* @TODO: Optimise this by making it optional / selective
|
|
*/
|
|
const exposeFixtures = async () => {
|
|
const fixturePromises = {
|
|
roles: models.Role.findAll({columns: ['id']}),
|
|
users: models.User.findAll({columns: ['id', 'email']}),
|
|
tags: models.Tag.findAll({columns: ['id']}),
|
|
apiKeys: models.ApiKey.findAll({withRelated: 'integration'})
|
|
};
|
|
const keys = Object.keys(fixturePromises);
|
|
module.exports.existingData = {};
|
|
|
|
return Promise
|
|
.all(Object.values(fixturePromises))
|
|
.then((results) => {
|
|
for (let i = 0; i < keys.length; i += 1) {
|
|
module.exports.existingData[keys[i]] = results[i].toJSON(context.internal);
|
|
}
|
|
})
|
|
.catch((err) => {
|
|
console.error('Unable to expose fixtures', err); // eslint-disable-line no-console
|
|
process.exit(1);
|
|
});
|
|
};
|
|
|
|
const prepareContentFolder = (options) => {
|
|
const contentFolderForTests = options.contentFolder;
|
|
|
|
/**
|
|
* 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'));
|
|
}
|
|
};
|
|
|
|
// CASE: Ghost Server is Running
|
|
// In this case we need to reset things so it's as though Ghost just booted:
|
|
// - truncate database
|
|
// - re-run default fixtures
|
|
// - reload affected services
|
|
const restartModeGhostStart = async () => {
|
|
console.log('Restart Mode'); // eslint-disable-line no-console
|
|
|
|
// Teardown truncates all tables and also calls urlServiceUtils.reset();
|
|
await dbUtils.teardown();
|
|
|
|
// The tables have been truncated, this runs the fixture init task (init file 2) to re-add our default fixtures
|
|
await knexMigrator.init({only: 2});
|
|
|
|
// Reset the settings cache
|
|
// @TODO: Prob A: why/how is this different to using settingsCache.reset()
|
|
settingsCache.shutdown();
|
|
await settingsService.init();
|
|
|
|
// Reload the frontend
|
|
await frontendSettingsService.init();
|
|
await themes.init();
|
|
|
|
// Reload the URL service & wait for it to be ready again
|
|
// @TODO: Prob B: why/how is this different to urlService.resetGenerators?
|
|
urlServiceUtils.reset();
|
|
urlServiceUtils.init();
|
|
await urlServiceUtils.isFinished();
|
|
// @TODO: why does this happen _after_ URL service
|
|
web.shared.middlewares.customRedirects.reload();
|
|
|
|
// Trigger themes to load again
|
|
themes.loadInactiveThemes();
|
|
};
|
|
|
|
const bootGhost = async () => {
|
|
ghostServer = await boot();
|
|
};
|
|
|
|
// CASE: Ghost Server needs Starting
|
|
// In this case we need to ensure that Ghost is started cleanly:
|
|
// - ensure the DB is reset
|
|
// - CASE: If we are in force start mode the server is already running so we
|
|
// - stop the server (if we are in force start mode it will be running)
|
|
// - reload affected services - just settings and not the frontend!?
|
|
// - Start Ghost: Uses OLD Boot process
|
|
const freshModeGhostStart = async (options) => {
|
|
if (options.forceStart) {
|
|
console.log('Force Start Mode'); // eslint-disable-line no-console
|
|
} else {
|
|
console.log('Fresh Start Mode'); // eslint-disable-line no-console
|
|
}
|
|
|
|
// Reset the DB
|
|
await knexMigrator.reset({force: true});
|
|
|
|
// Stop the serve (forceStart Mode)
|
|
if (ghostServer && ghostServer.httpServer) {
|
|
await ghostServer.stop();
|
|
}
|
|
|
|
// Reset the settings cache
|
|
// @TODO: Prob A: why/how is this different to using settingsService.init() and why to do we need this?
|
|
settingsCache.shutdown();
|
|
settingsCache.reset();
|
|
|
|
// Do a full database initialisation
|
|
await knexMigrator.init();
|
|
|
|
if (config.get('database:client') === 'sqlite3') {
|
|
await db.knex.raw('PRAGMA journal_mode = TRUNCATE;');
|
|
}
|
|
|
|
// Reset the URL service generators
|
|
// @TODO: Prob B: why/how is this different to urlService.reset?
|
|
// @TODO: why would we do this on a fresh boot?!
|
|
urlService.resetGenerators();
|
|
|
|
// Actually boot Ghost
|
|
await bootGhost(options);
|
|
|
|
// Wait for the URL service to be ready, which happens after boot
|
|
await urlServiceUtils.isFinished();
|
|
};
|
|
|
|
const startGhost = async (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);
|
|
|
|
// Ensure we have tmp content folders populated ready for testing
|
|
// @TODO: tidy up the tmp folders after tests
|
|
prepareContentFolder(options);
|
|
|
|
if (ghostServer && ghostServer.httpServer && !options.forceStart) {
|
|
await restartModeGhostStart(options);
|
|
} else {
|
|
await freshModeGhostStart(options);
|
|
}
|
|
|
|
// Expose fixture data, wrap-up and return
|
|
await exposeFixtures();
|
|
console.timeEnd('Start Ghost'); // eslint-disable-line no-console
|
|
return ghostServer;
|
|
};
|
|
|
|
const stopGhost = async () => {
|
|
if (ghostServer && ghostServer.httpServer) {
|
|
await ghostServer.stop();
|
|
urlService.resetGenerators();
|
|
}
|
|
};
|
|
|
|
module.exports = {
|
|
startGhost: startGhost,
|
|
stopGhost: stopGhost,
|
|
|
|
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
|
|
};
|