'use strict'; const assert = require('assert'); const childProcess = require('child_process'); const fs = require('fs'); const path = require('path'); const Config = require('../../tools/js/config.js'); const Database = require('../../tools/js/database.js'); const RemoteAPI = require('../../tools/js/remote.js').RemoteAPI; const BrowserPrivilegedAPI = require('../../public/v3/privileged-api.js').PrivilegedAPI; const NodePrivilegedAPI = require('../../tools/js/privileged-api').PrivilegedAPI; class TestServer { constructor() { this._pidFile = null; this._testConfigPath = Config.path('testServer.config'); this._dataDirectory = Config.path('dataDirectory'); this._backupDataPath = null; this._pidWaitStart = null; this._shouldLog = false; this._pgsqlDirectory = null; this._server = null; this._databaseName = Config.value('testDatabaseName'); this._databaseUser = Config.value('database.username'); this._databaseHost = Config.value('database.host'); this._databasePort = Config.value('database.port'); this._database = null; this._remote = null } start() { let testConfigContent = this.testConfig(); fs.writeFileSync(this._testConfigPath, JSON.stringify(testConfigContent, null, ' ')); this._ensureTestDatabase(); this._ensureDataDirectory(); return this._startApache(); } stop() { this._restoreDataDirectory(); return this._stopApache(); } remoteAPI() { assert(this._remote); return this._remote; } database() { assert(this._databaseName); if (!this._database) this._database = new Database(this._databaseName); return this._database; } testConfig() { return { 'siteTitle': 'Test Dashboard', 'debug': true, 'jsonCacheMaxAge': 600, 'dataDirectory': Config.value('dataDirectory'), 'database': { 'host': Config.value('database.host'), 'port': Config.value('database.port'), 'username': Config.value('database.username'), 'password': Config.value('database.password'), 'name': Config.value('testDatabaseName'), }, 'uploadFileLimitInMB': 2, 'uploadUserQuotaInMB': 5, 'uploadTotalQuotaInMB': 7, 'uploadDirectory': Config.value('dataDirectory') + '/uploaded', 'universalWorkerPassword': null, 'maintenanceMode': false, 'clusterStart': [2000, 1, 1, 0, 0], 'clusterSize': [0, 2, 0], 'defaultDashboard': [[]], 'dashboards': {}, 'summaryPages': [] } } _ensureDataDirectory() { let backupPath = path.resolve(this._dataDirectory, '../original-data'); if (fs.existsSync(this._dataDirectory)) { assert.ok(!fs.existsSync(backupPath), `Both ${this._dataDirectory} and ${backupPath} exist. Cannot make a backup of data`); fs.renameSync(this._dataDirectory, backupPath); this._backupDataPath = backupPath; } else if (fs.existsSync(backupPath)) // Assume this is a backup from the last failed run this._backupDataPath = backupPath; fs.mkdirSync(this._dataDirectory, 0o755); fs.mkdirSync(path.resolve(this._dataDirectory, 'uploaded'), 0o755); } _restoreDataDirectory() { childProcess.execFileSync('rm', ['-rf', this._dataDirectory]); if (this._backupDataPath) fs.renameSync(this._backupDataPath, this._dataDirectory); } cleanDataDirectory() { let fileList = fs.readdirSync(this._dataDirectory); for (let filename of fileList) { if (filename != 'uploaded') fs.unlinkSync(path.resolve(this._dataDirectory, filename)); } fileList = fs.readdirSync(path.resolve(this._dataDirectory, 'uploaded')); for (let filename of fileList) fs.unlinkSync(path.resolve(this._dataDirectory, 'uploaded', filename)); } _ensureTestDatabase() { try { this._executePgsqlCommand('dropdb'); } catch (error) { } this._executePgsqlCommand('createdb'); this._executePgsqlCommand('psql', ['--command', `grant all privileges on database "${this._databaseName}" to "${this._databaseUser}";`]); this.initDatabase(); } initDatabase() { if (this._database) this._database.disconnect(); this._database = null; const initFilePath = Config.pathFromRoot('init-database.sql'); const migrateFilePath = Config.pathFromRoot('migrate-database.sql'); for (const filePath of [initFilePath, migrateFilePath]) { this._executePgsqlCommand('psql', ['--username', this._databaseUser, '--file', filePath], {stdio: ['ignore', 'ignore', 'ignore']}); } } _executePgsqlCommand(command, args, options) { if (!this._pgsqlDirectory) this._pgsqlDirectory = this._determinePgsqlDirectory(); childProcess.execFileSync(path.resolve(this._pgsqlDirectory, command), [this._databaseName, '--host', this._databaseHost, '--port', this._databasePort].concat(args || []), options); } _determinePgsqlDirectory() { try { let initdbLocation = childProcess.execFileSync('which', ['initdb']).toString(); return path.dirname(initdbLocation); } catch (error) { let serverPgsqlLocation = '/Applications/Server.app/Contents/ServerRoot/usr/bin/'; childProcess.execFileSync(path.resolve(serverPgsqlLocation, 'initdb'), ['--version']); return serverPgsqlLocation; } } _startApache() { let pidFile = Config.path('testServer.httpdPID'); let httpdConfig = Config.path('testServer.httpdConfig'); let port = Config.value('testServer.port'); let errorLog = Config.path('testServer.httpdErrorLog'); let mutexFile = Config.path('testServer.httpdMutexDir'); let phpVersion = childProcess.execFileSync('php', ['-v'], {stdio: ['pipe', 'pipe', 'ignore']}).toString().includes('PHP 5') ? 'PHP5' : 'PHP7'; if (!fs.existsSync(mutexFile)) fs.mkdirSync(mutexFile, 0o755); let args = [ '-f', httpdConfig, '-c', `SetEnv ORG_WEBKIT_PERF_CONFIG_PATH ${this._testConfigPath}`, '-c', `Listen ${port}`, '-c', `PidFile ${pidFile}`, '-c', `ErrorLog ${errorLog}`, '-c', `Mutex file:${mutexFile}`, '-c', `DocumentRoot ${Config.serverRoot()}`, '-D', phpVersion]; if (this._shouldLog) console.log(args); childProcess.execFileSync('httpd', args); this._server = { scheme: 'http', host: 'localhost', port: port, }; this._pidWaitStart = Date.now(); this._pidFile = pidFile; this._remote = new RemoteAPI(this._server); return new Promise(this._waitForPid.bind(this, true)); } _stopApache() { if (!this._pidFile) return; let pid = fs.readFileSync(this._pidFile, 'utf-8').trim(); if (this._shouldLog) console.log('Stopping', pid); childProcess.execFileSync('kill', ['-TERM', pid]); this._pidWaitStart = Date.now(); return new Promise(this._waitForPid.bind(this, false)); } _waitForPid(shouldExist, resolve, reject) { if (fs.existsSync(this._pidFile) != shouldExist) { if (Date.now() - this._pidWaitStart > 8000) reject(); else setTimeout(this._waitForPid.bind(this, shouldExist, resolve, reject), 100); return; } resolve(); } inject(privilegedAPIType='browser') { console.assert(privilegedAPIType === 'browser' || privilegedAPIType === 'node'); const useNodePrivilegedAPI = privilegedAPIType === 'node'; const self = this; before(function () { this.timeout(10000); return self.start(); }); let originalRemote; let originalPrivilegedAPI; beforeEach(function () { this.timeout(10000); self.initDatabase(); self.cleanDataDirectory(); originalRemote = global.RemoteAPI; global.RemoteAPI = self._remote; self._remote.clearCookies(); originalPrivilegedAPI = global.PrivilegedAPI; global.PrivilegedAPI = useNodePrivilegedAPI ? NodePrivilegedAPI : BrowserPrivilegedAPI; if (!useNodePrivilegedAPI) { global.PrivilegedAPI._token = null; global.PrivilegedAPI._expiration = null; } }); after(function () { this.timeout(10000); global.RemoteAPI = originalRemote; global.PrivilegedAPI = originalPrivilegedAPI; return self.stop(); }); } } if (!global.TestServer) global.TestServer = new TestServer; if (typeof module != 'undefined') module.exports = global.TestServer;