0
Fork 0
mirror of https://github.com/TryGhost/Ghost.git synced 2025-02-10 23:36:14 -05:00

Moved acceptance test utils into own file

- starting and stopping Ghost is part of our rather unloved acceptance test framework
- moving them into their own file to make the different pieces clearer and also to start to make improvements
- first improvement had to happen as an aside - exposing the existingData property via a function and making the API clearer
- this was a weird thing set on module.exports, very hidden and hard to follow
- Note: stopGhost is only used once in the regression/modles/model_posts_spec.js file to make the test run fast enough...
- These tests are effectively "DB integration" tests e.g. non-unit tests because they do use the DB, they need their own framework
This commit is contained in:
Hannah Wolfe 2021-06-01 13:09:12 +01:00
parent 52a97957e1
commit 0f42bbeac3
No known key found for this signature in database
GPG key ID: 9F8C7532D0A6BA55
14 changed files with 297 additions and 274 deletions

View file

@ -73,7 +73,7 @@ describe('Invites API', function () {
.post(localUtils.API.getApiQuery('invites/'))
.set('Origin', config.get('url'))
.send({
invites: [{email: 'test@example.com', role_id: testUtils.existingData.roles[1].id}]
invites: [{email: 'test@example.com', role_id: testUtils.getExistingData().roles[1].id}]
})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
@ -86,7 +86,7 @@ describe('Invites API', function () {
jsonResponse.invites.should.have.length(1);
localUtils.API.checkResponse(jsonResponse.invites[0], 'invite');
jsonResponse.invites[0].role_id.should.eql(testUtils.existingData.roles[1].id);
jsonResponse.invites[0].role_id.should.eql(testUtils.getExistingData().roles[1].id);
mailService.GhostMailer.prototype.send.called.should.be.true();

View file

@ -59,7 +59,7 @@ describe('Tag API', function () {
it('Can read a tag', async function () {
const res = await request
.get(localUtils.API.getApiQuery(`tags/${testUtils.existingData.tags[0].id}/?include=count.posts`))
.get(localUtils.API.getApiQuery(`tags/${testUtils.getExistingData().tags[0].id}/?include=count.posts`))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
@ -132,10 +132,10 @@ describe('Tag API', function () {
it('Can edit a tag', async function () {
const res = await request
.put(localUtils.API.getApiQuery(`tags/${testUtils.existingData.tags[0].id}`))
.put(localUtils.API.getApiQuery(`tags/${testUtils.getExistingData().tags[0].id}`))
.set('Origin', config.get('url'))
.send({
tags: [Object.assign({}, testUtils.existingData.tags[0], {description: 'hey ho ab ins klo'})]
tags: [Object.assign({}, testUtils.getExistingData().tags[0], {description: 'hey ho ab ins klo'})]
})
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
@ -152,7 +152,7 @@ describe('Tag API', function () {
it('Can destroy a tag', async function () {
const res = await request
.del(localUtils.API.getApiQuery(`tags/${testUtils.existingData.tags[0].id}`))
.del(localUtils.API.getApiQuery(`tags/${testUtils.getExistingData().tags[0].id}`))
.set('Origin', config.get('url'))
.expect('Cache-Control', testUtils.cacheRules.private)
.expect(204);

View file

@ -100,7 +100,7 @@ describe('User API', function () {
});
it('Can retrieve a user by id', async function () {
const res = await request.get(localUtils.API.getApiQuery('users/' + testUtils.existingData.users[0].id + '/?include=roles,roles.permissions,count.posts'))
const res = await request.get(localUtils.API.getApiQuery('users/' + testUtils.getExistingData().users[0].id + '/?include=roles,roles.permissions,count.posts'))
.set('Origin', config.get('url'))
.expect('Content-Type', /json/)
.expect('Cache-Control', testUtils.cacheRules.private)
@ -186,7 +186,7 @@ describe('User API', function () {
});
it('Can destroy an active user', async function () {
const userId = testUtils.existingData.users[1].id;
const userId = testUtils.getExistingData().users[1].id;
const res = await request
.get(localUtils.API.getApiQuery(`posts/?filter=author_id:${userId}`))
@ -257,7 +257,7 @@ describe('User API', function () {
newPassword: '1234abcde!!',
ne2Password: '1234abcde!!',
oldPassword: 'Sl1m3rson99',
user_id: testUtils.existingData.users[0].id
user_id: testUtils.getExistingData().users[0].id
}]
})
.expect('Content-Type', /json/)
@ -287,7 +287,7 @@ describe('User API', function () {
});
it('Can\'t read another user\'s Personal Token', async function () {
const userNotAdmin = testUtils.existingData.users.find(user => user.email === 'ghost-author@example.com');
const userNotAdmin = testUtils.getExistingData().users.find(user => user.email === 'ghost-author@example.com');
const res = await request
.get(localUtils.API.getApiQuery('users/' + userNotAdmin.id + '/token/'))
.set('Origin', config.get('url'))

View file

@ -28,8 +28,8 @@ describe('DB API', function () {
return localUtils.doAuth(request);
})
.then(() => {
backupKey = _.find(testUtils.existingData.apiKeys, {integration: {slug: 'ghost-backup'}});
schedulerKey = _.find(testUtils.existingData.apiKeys, {integration: {slug: 'ghost-scheduler'}});
backupKey = _.find(testUtils.getExistingData().apiKeys, {integration: {slug: 'ghost-backup'}});
schedulerKey = _.find(testUtils.getExistingData().apiKeys, {integration: {slug: 'ghost-scheduler'}});
});
});

View file

@ -33,45 +33,45 @@ describe('Canary Schedules API', function () {
request = supertest.agent(config.get('url'));
resources.push(testUtils.DataGenerator.forKnex.createPost({
created_by: testUtils.existingData.users[0].id,
author_id: testUtils.existingData.users[0].id,
published_by: testUtils.existingData.users[0].id,
created_by: testUtils.getExistingData().users[0].id,
author_id: testUtils.getExistingData().users[0].id,
published_by: testUtils.getExistingData().users[0].id,
published_at: moment().add(30, 'seconds').toDate(),
status: 'scheduled',
slug: 'first'
}));
resources.push(testUtils.DataGenerator.forKnex.createPost({
created_by: testUtils.existingData.users[0].id,
author_id: testUtils.existingData.users[0].id,
published_by: testUtils.existingData.users[0].id,
created_by: testUtils.getExistingData().users[0].id,
author_id: testUtils.getExistingData().users[0].id,
published_by: testUtils.getExistingData().users[0].id,
published_at: moment().subtract(30, 'seconds').toDate(),
status: 'scheduled',
slug: 'second'
}));
resources.push(testUtils.DataGenerator.forKnex.createPost({
created_by: testUtils.existingData.users[0].id,
author_id: testUtils.existingData.users[0].id,
published_by: testUtils.existingData.users[0].id,
created_by: testUtils.getExistingData().users[0].id,
author_id: testUtils.getExistingData().users[0].id,
published_by: testUtils.getExistingData().users[0].id,
published_at: moment().add(10, 'minute').toDate(),
status: 'scheduled',
slug: 'third'
}));
resources.push(testUtils.DataGenerator.forKnex.createPost({
created_by: testUtils.existingData.users[0].id,
author_id: testUtils.existingData.users[0].id,
published_by: testUtils.existingData.users[0].id,
created_by: testUtils.getExistingData().users[0].id,
author_id: testUtils.getExistingData().users[0].id,
published_by: testUtils.getExistingData().users[0].id,
published_at: moment().subtract(10, 'minute').toDate(),
status: 'scheduled',
slug: 'fourth'
}));
resources.push(testUtils.DataGenerator.forKnex.createPost({
created_by: testUtils.existingData.users[0].id,
author_id: testUtils.existingData.users[0].id,
published_by: testUtils.existingData.users[0].id,
created_by: testUtils.getExistingData().users[0].id,
author_id: testUtils.getExistingData().users[0].id,
published_by: testUtils.getExistingData().users[0].id,
published_at: moment().add(30, 'seconds').toDate(),
status: 'scheduled',
slug: 'fifth',
@ -89,7 +89,7 @@ describe('Canary Schedules API', function () {
let token;
before(function () {
const schedulerKey = _.find(testUtils.existingData.apiKeys, {integration: {slug: 'ghost-scheduler'}});
const schedulerKey = _.find(testUtils.getExistingData().apiKeys, {integration: {slug: 'ghost-scheduler'}});
token = localUtils.getValidAdminToken('/canary/admin/', schedulerKey);
});
@ -124,7 +124,7 @@ describe('Canary Schedules API', function () {
});
it('no access', function () {
const zapierKey = _.find(testUtils.existingData.apiKeys, {integration: {slug: 'ghost-backup'}});
const zapierKey = _.find(testUtils.getExistingData().apiKeys, {integration: {slug: 'ghost-backup'}});
const zapierToken = localUtils.getValidAdminToken('/canary/admin/', zapierKey);
return request

View file

@ -217,7 +217,7 @@ describe('User API', function () {
.set('Origin', config.get('url'))
.send({
owner: [{
id: testUtils.existingData.users[1].id
id: testUtils.getExistingData().users[1].id
}]
})
.expect('Content-Type', /json/)

View file

@ -28,8 +28,8 @@ describe('DB API', function () {
return localUtils.doAuth(request);
})
.then(() => {
backupKey = _.find(testUtils.existingData.apiKeys, {integration: {slug: 'ghost-backup'}});
schedulerKey = _.find(testUtils.existingData.apiKeys, {integration: {slug: 'ghost-scheduler'}});
backupKey = _.find(testUtils.getExistingData().apiKeys, {integration: {slug: 'ghost-backup'}});
schedulerKey = _.find(testUtils.getExistingData().apiKeys, {integration: {slug: 'ghost-scheduler'}});
});
});

View file

@ -33,45 +33,45 @@ describe('v2 Schedules API', function () {
request = supertest.agent(config.get('url'));
resources.push(testUtils.DataGenerator.forKnex.createPost({
created_by: testUtils.existingData.users[0].id,
author_id: testUtils.existingData.users[0].id,
published_by: testUtils.existingData.users[0].id,
created_by: testUtils.getExistingData().users[0].id,
author_id: testUtils.getExistingData().users[0].id,
published_by: testUtils.getExistingData().users[0].id,
published_at: moment().add(30, 'seconds').toDate(),
status: 'scheduled',
slug: 'first'
}));
resources.push(testUtils.DataGenerator.forKnex.createPost({
created_by: testUtils.existingData.users[0].id,
author_id: testUtils.existingData.users[0].id,
published_by: testUtils.existingData.users[0].id,
created_by: testUtils.getExistingData().users[0].id,
author_id: testUtils.getExistingData().users[0].id,
published_by: testUtils.getExistingData().users[0].id,
published_at: moment().subtract(30, 'seconds').toDate(),
status: 'scheduled',
slug: 'second'
}));
resources.push(testUtils.DataGenerator.forKnex.createPost({
created_by: testUtils.existingData.users[0].id,
author_id: testUtils.existingData.users[0].id,
published_by: testUtils.existingData.users[0].id,
created_by: testUtils.getExistingData().users[0].id,
author_id: testUtils.getExistingData().users[0].id,
published_by: testUtils.getExistingData().users[0].id,
published_at: moment().add(10, 'minute').toDate(),
status: 'scheduled',
slug: 'third'
}));
resources.push(testUtils.DataGenerator.forKnex.createPost({
created_by: testUtils.existingData.users[0].id,
author_id: testUtils.existingData.users[0].id,
published_by: testUtils.existingData.users[0].id,
created_by: testUtils.getExistingData().users[0].id,
author_id: testUtils.getExistingData().users[0].id,
published_by: testUtils.getExistingData().users[0].id,
published_at: moment().subtract(10, 'minute').toDate(),
status: 'scheduled',
slug: 'fourth'
}));
resources.push(testUtils.DataGenerator.forKnex.createPost({
created_by: testUtils.existingData.users[0].id,
author_id: testUtils.existingData.users[0].id,
published_by: testUtils.existingData.users[0].id,
created_by: testUtils.getExistingData().users[0].id,
author_id: testUtils.getExistingData().users[0].id,
published_by: testUtils.getExistingData().users[0].id,
published_at: moment().add(30, 'seconds').toDate(),
status: 'scheduled',
slug: 'fifth',
@ -89,7 +89,7 @@ describe('v2 Schedules API', function () {
let token;
before(function () {
const schedulerKey = _.find(testUtils.existingData.apiKeys, {integration: {slug: 'ghost-scheduler'}});
const schedulerKey = _.find(testUtils.getExistingData().apiKeys, {integration: {slug: 'ghost-scheduler'}});
token = localUtils.getValidAdminToken('/v2/admin/', schedulerKey);
});
@ -124,7 +124,7 @@ describe('v2 Schedules API', function () {
});
it('no access', function () {
const zapierKey = _.find(testUtils.existingData.apiKeys, {integration: {slug: 'ghost-backup'}});
const zapierKey = _.find(testUtils.getExistingData().apiKeys, {integration: {slug: 'ghost-backup'}});
const zapierToken = localUtils.getValidAdminToken('/v2/admin/', zapierKey);
return request

View file

@ -217,7 +217,7 @@ describe('User API', function () {
.set('Origin', config.get('url'))
.send({
owner: [{
id: testUtils.existingData.users[1].id
id: testUtils.getExistingData().users[1].id
}]
})
.expect('Content-Type', /json/)

View file

@ -28,8 +28,8 @@ describe('DB API', function () {
return localUtils.doAuth(request);
})
.then(() => {
backupKey = _.find(testUtils.existingData.apiKeys, {integration: {slug: 'ghost-backup'}});
schedulerKey = _.find(testUtils.existingData.apiKeys, {integration: {slug: 'ghost-scheduler'}});
backupKey = _.find(testUtils.getExistingData().apiKeys, {integration: {slug: 'ghost-backup'}});
schedulerKey = _.find(testUtils.getExistingData().apiKeys, {integration: {slug: 'ghost-scheduler'}});
});
});

View file

@ -33,45 +33,45 @@ describe('v3 Schedules API', function () {
request = supertest.agent(config.get('url'));
resources.push(testUtils.DataGenerator.forKnex.createPost({
created_by: testUtils.existingData.users[0].id,
author_id: testUtils.existingData.users[0].id,
published_by: testUtils.existingData.users[0].id,
created_by: testUtils.getExistingData().users[0].id,
author_id: testUtils.getExistingData().users[0].id,
published_by: testUtils.getExistingData().users[0].id,
published_at: moment().add(30, 'seconds').toDate(),
status: 'scheduled',
slug: 'first'
}));
resources.push(testUtils.DataGenerator.forKnex.createPost({
created_by: testUtils.existingData.users[0].id,
author_id: testUtils.existingData.users[0].id,
published_by: testUtils.existingData.users[0].id,
created_by: testUtils.getExistingData().users[0].id,
author_id: testUtils.getExistingData().users[0].id,
published_by: testUtils.getExistingData().users[0].id,
published_at: moment().subtract(30, 'seconds').toDate(),
status: 'scheduled',
slug: 'second'
}));
resources.push(testUtils.DataGenerator.forKnex.createPost({
created_by: testUtils.existingData.users[0].id,
author_id: testUtils.existingData.users[0].id,
published_by: testUtils.existingData.users[0].id,
created_by: testUtils.getExistingData().users[0].id,
author_id: testUtils.getExistingData().users[0].id,
published_by: testUtils.getExistingData().users[0].id,
published_at: moment().add(10, 'minute').toDate(),
status: 'scheduled',
slug: 'third'
}));
resources.push(testUtils.DataGenerator.forKnex.createPost({
created_by: testUtils.existingData.users[0].id,
author_id: testUtils.existingData.users[0].id,
published_by: testUtils.existingData.users[0].id,
created_by: testUtils.getExistingData().users[0].id,
author_id: testUtils.getExistingData().users[0].id,
published_by: testUtils.getExistingData().users[0].id,
published_at: moment().subtract(10, 'minute').toDate(),
status: 'scheduled',
slug: 'fourth'
}));
resources.push(testUtils.DataGenerator.forKnex.createPost({
created_by: testUtils.existingData.users[0].id,
author_id: testUtils.existingData.users[0].id,
published_by: testUtils.existingData.users[0].id,
created_by: testUtils.getExistingData().users[0].id,
author_id: testUtils.getExistingData().users[0].id,
published_by: testUtils.getExistingData().users[0].id,
published_at: moment().add(30, 'seconds').toDate(),
status: 'scheduled',
slug: 'fifth',
@ -89,7 +89,7 @@ describe('v3 Schedules API', function () {
let token;
before(function () {
const schedulerKey = _.find(testUtils.existingData.apiKeys, {integration: {slug: 'ghost-scheduler'}});
const schedulerKey = _.find(testUtils.getExistingData().apiKeys, {integration: {slug: 'ghost-scheduler'}});
token = localUtils.getValidAdminToken('/v3/admin/', schedulerKey);
});
@ -124,7 +124,7 @@ describe('v3 Schedules API', function () {
});
it('no access', function () {
const zapierKey = _.find(testUtils.existingData.apiKeys, {integration: {slug: 'ghost-backup'}});
const zapierKey = _.find(testUtils.getExistingData().apiKeys, {integration: {slug: 'ghost-backup'}});
const zapierToken = localUtils.getValidAdminToken('/v3/admin/', zapierKey);
return request

View file

@ -217,7 +217,7 @@ describe('User API', function () {
.set('Origin', config.get('url'))
.send({
owner: [{
id: testUtils.existingData.users[1].id
id: testUtils.getExistingData().users[1].id
}]
})
.expect('Content-Type', /json/)

View file

@ -0,0 +1,223 @@
require('../../core/server/overrides');
// Utility Packages
const Promise = require('bluebird');
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 db = require('../../core/server/data/db');
const models = require('../../core/server/models');
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 themeService = require('../../core/server/services/themes');
const limits = require('../../core/server/services/limits');
// Other Test Utilities
const configUtils = require('./configUtils');
const dbUtils = require('./db-utils');
const urlServiceUtils = require('./url-service-utils');
const redirects = require('./redirects');
const context = require('./fixtures/context');
let ghostServer;
let existingData = {};
/**
* 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);
existingData = {};
return Promise
.all(Object.values(fixturePromises))
.then((results) => {
for (let i = 0; i < keys.length; i += 1) {
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 themeService.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
themeService.loadInactiveThemes();
// Reload limits service
limits.init();
};
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)
await stopGhost();
// 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();
delete require.cache[require.resolve('../../core/app')];
urlService.resetGenerators();
}
};
module.exports = {
startGhost,
stopGhost,
getExistingData: () => {
return existingData;
}
};

View file

@ -1,35 +1,17 @@
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 db = require('../../core/server/data/db');
const models = require('../../core/server/models');
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 themeService = require('../../core/server/services/themes');
const limits = require('../../core/server/services/limits');
// Other Test Utilities
const acceptanceUtils = require('./acceptance-utils');
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');
@ -110,192 +92,10 @@ const createEmailedPost = async function createEmailedPost({postOptions, emailOp
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 themeService.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
themeService.loadInactiveThemes();
// Reload limits service
limits.init();
};
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)
await stopGhost();
// 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();
delete require.cache[require.resolve('../../core/app')];
urlService.resetGenerators();
}
};
module.exports = {
startGhost: startGhost,
stopGhost: stopGhost,
startGhost: acceptanceUtils.startGhost,
stopGhost: acceptanceUtils.stopGhost,
getExistingData: acceptanceUtils.getExistingData,
teardownDb: dbUtils.teardown,
truncate: dbUtils.truncate,