2021-06-01 15:09:12 +03:00
// Utility Packages
2021-07-06 21:36:30 +03:00
const debug = require ( '@tryghost/debug' ) ( 'test' ) ;
2021-06-01 15:09:12 +03:00
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' ) ;
2021-10-18 17:27:57 +03:00
const urlService = require ( '../../core/server/services/url' ) ;
2021-06-01 15:09:12 +03:00
const settingsService = require ( '../../core/server/services/settings' ) ;
2021-09-23 19:17:46 +03:00
const routeSettingsService = require ( '../../core/server/services/route-settings' ) ;
2021-06-01 15:09:12 +03:00
const themeService = require ( '../../core/server/services/themes' ) ;
const limits = require ( '../../core/server/services/limits' ) ;
2021-09-29 13:21:39 +03:00
const customRedirectsService = require ( '../../core/server/services/redirects' ) ;
2021-06-01 15:09:12 +03:00
// 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 = { } ;
2021-07-06 21:36:30 +03:00
let totalStartTime = 0 ;
2021-11-25 09:36:50 +03:00
let totalBoots = 0 ;
2021-06-01 15:09:12 +03:00
/ * *
* 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
2021-06-09 18:32:48 +03:00
* @ TODO : Optimize this by making it optional / selective
2021-06-01 15:09:12 +03:00
* /
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 ) ;
}
2021-11-30 09:35:33 +03:00
if ( options . routesFilePath ) {
fs . copySync ( options . routesFilePath , path . join ( contentFolderForTests , 'settings' , 'routes.yaml' ) ) ;
} else if ( options . copySettings ) {
2021-06-01 15:09:12 +03:00
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
2021-11-22 09:51:54 +03:00
const restartModeGhostStart = async ( { frontend } ) => {
2021-07-06 21:36:30 +03:00
debug ( 'Reload Mode' ) ;
2021-06-01 15:09:12 +03:00
2021-12-06 15:50:35 +03:00
// TODO: figure out why we need this if we reset again later?
urlServiceUtils . reset ( ) ;
await dbUtils . reset ( ) ;
2021-07-06 21:36:30 +03:00
debug ( 'init done' ) ;
2021-06-01 15:09:12 +03:00
// Reset the settings cache
await settingsService . init ( ) ;
2021-07-06 21:36:30 +03:00
debug ( 'settings done' ) ;
2021-06-01 15:09:12 +03:00
2021-11-22 09:51:54 +03:00
if ( frontend ) {
2021-11-19 09:58:46 +03:00
// Load the frontend-related components
await routeSettingsService . init ( ) ;
await themeService . init ( ) ;
debug ( 'frontend done' ) ;
}
2021-06-01 15:09:12 +03:00
// Reload the URL service & wait for it to be ready again
2021-06-30 12:32:04 +03:00
// @TODO: why/how is this different to urlService.resetGenerators?
2021-06-01 15:09:12 +03:00
urlServiceUtils . reset ( ) ;
2021-11-22 09:51:54 +03:00
urlServiceUtils . init ( { urlCache : ! frontend } ) ;
2021-11-17 19:14:08 +03:00
2021-11-22 09:51:54 +03:00
if ( frontend ) {
2021-11-17 19:14:08 +03:00
await urlServiceUtils . isFinished ( ) ;
}
2021-07-06 21:36:30 +03:00
debug ( 'routes done' ) ;
2021-09-29 13:21:39 +03:00
await customRedirectsService . init ( ) ;
2021-06-01 15:09:12 +03:00
// Reload limits service
limits . init ( ) ;
} ;
// 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 ) {
2021-07-06 21:36:30 +03:00
debug ( 'Forced Restart Mode' ) ;
2021-06-01 15:09:12 +03:00
} else {
2021-07-06 21:36:30 +03:00
debug ( 'Fresh Start Mode' ) ;
2021-06-01 15:09:12 +03:00
}
2021-06-30 12:32:04 +03:00
// Stop the server (forceStart Mode)
2021-06-01 15:09:12 +03:00
await stopGhost ( ) ;
2021-06-30 12:32:04 +03:00
// Reset the settings cache and disable listeners so they don't get triggered further
settingsService . shutdown ( ) ;
2021-06-01 15:09:12 +03:00
2021-12-06 15:50:35 +03:00
await dbUtils . reset ( ) ;
2021-06-01 15:09:12 +03:00
2021-06-30 12:32:04 +03:00
await settingsService . init ( ) ;
2021-06-01 15:09:12 +03:00
// Actually boot Ghost
2021-12-06 14:35:42 +03:00
ghostServer = await boot ( {
backend : options . backend ,
frontend : options . frontend ,
server : options . server
} ) ;
2021-06-01 15:09:12 +03:00
2021-11-30 10:38:53 +03:00
// Wait for the URL service to be ready, which happens after boot
2021-11-22 09:51:54 +03:00
if ( options . frontend ) {
2021-11-17 19:14:08 +03:00
await urlServiceUtils . isFinished ( ) ;
}
2021-06-01 15:09:12 +03:00
} ;
2021-11-30 09:35:33 +03:00
/ * *
*
* @ param { Object } [ options ]
* @ param { boolean } [ options . backend ]
* @ param { boolean } [ options . frontend ]
* @ param { boolean } [ options . redirectsFile ]
* @ param { String } [ options . redirectsFileExt ]
* @ param { boolean } [ options . forceStart ]
* @ param { boolean } [ options . copyThemes ]
* @ param { boolean } [ options . copySettings ]
* @ param { String } [ options . routesFilePath ] - path to a routes configuration file to start the instance with
* @ param { String } [ options . contentFolder ]
* @ param { boolean } [ options . subdir ]
* @ returns { Promise < GhostServer > }
* /
2021-06-01 15:09:12 +03:00
const startGhost = async ( options ) => {
2021-07-06 21:36:30 +03:00
const startTime = Date . now ( ) ;
debug ( 'Start Ghost' ) ;
2021-06-01 15:09:12 +03:00
options = _ . merge ( {
2021-11-22 09:51:54 +03:00
backend : true ,
frontend : true ,
2021-06-01 15:09:12 +03:00
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 ( ) ;
2021-07-06 21:36:30 +03:00
// Reporting
const totalTime = Date . now ( ) - startTime ;
totalStartTime += totalTime ;
2021-11-25 09:36:50 +03:00
totalBoots += 1 ;
const averageBootTime = Math . round ( totalStartTime / totalBoots ) ;
2021-07-06 21:36:30 +03:00
debug ( ` Started Ghost in ${ totalTime / 1000 } s ` ) ;
2021-11-25 09:36:50 +03:00
debug ( ` Accumulated start time across ${ totalBoots } boots is ${ totalStartTime / 1000 } s (average = ${ averageBootTime } ms) ` ) ;
2021-06-01 15:09:12 +03:00
return ghostServer ;
} ;
const stopGhost = async ( ) => {
if ( ghostServer && ghostServer . httpServer ) {
await ghostServer . stop ( ) ;
delete require . cache [ require . resolve ( '../../core/app' ) ] ;
2021-11-30 10:37:56 +03:00
// NOTE: similarly to urlService.reset() there doesn't seem to be a need for this call
// probable best location for this type of cleanup if it's needed is registering
// a hood during the "server cleanup" phase of the server stop
2021-06-01 15:09:12 +03:00
urlService . resetGenerators ( ) ;
}
} ;
module . exports = {
startGhost ,
stopGhost ,
getExistingData : ( ) => {
return existingData ;
}
} ;