From 758e61b8b11c861574fb72e88f9d438dcc3560c1 Mon Sep 17 00:00:00 2001 From: Jonatan Nilsson Date: Fri, 4 Feb 2022 09:33:03 +0000 Subject: [PATCH] Massive development. Application finished. Core started --- core/application.mjs | 112 ++++-- core/client.mjs | 4 +- core/core.mjs | 539 +++----------------------- core/core_new.mjs | 0 core/core_old.mjs | 499 ++++++++++++++++++++++++ core/db.mjs | 7 +- core/http.mjs | 17 +- core/log.mjs | 77 +++- core/util.mjs | 55 ++- package.json | 6 +- test/application.integration.test.mjs | 50 +-- test/application.run.test.mjs | 263 +++++++++++-- test/application.test.mjs | 367 ++++++++++-------- test/core.test.mjs | 365 +++++++++++++++++ test/db.test.mjs | 12 +- test/exampleindex.mjs | 6 +- test/helpers.mjs | 29 ++ test/http.test.mjs | 64 ++- test/log.test.mjs | 286 ++++++++++++++ test/updater.test.mjs | 0 test/util.test.mjs | 190 +++++++-- 21 files changed, 2117 insertions(+), 831 deletions(-) delete mode 100644 core/core_new.mjs create mode 100644 core/core_old.mjs create mode 100644 test/core.test.mjs create mode 100644 test/helpers.mjs create mode 100644 test/log.test.mjs delete mode 100644 test/updater.test.mjs diff --git a/core/application.mjs b/core/application.mjs index 5eb9ec7..e1c85ec 100644 --- a/core/application.mjs +++ b/core/application.mjs @@ -1,13 +1,20 @@ import { EventEmitter } from 'events' import fs from 'fs/promises' +import { request } from './client.mjs' import HttpServer from './http.mjs' +import { defaults } from './defaults.mjs' export default class Application extends EventEmitter { - constructor(util, db, provider, name, opts = {}) { + constructor(ctx, provider, name, opts = {}) { super() - this.util = util - this.db = db - this.config = db.config[name] || { } + this.ctx = { + db: ctx.db, + util: ctx.util, + log: ctx.log, + core: ctx.core, + app: this, + } + this.config = defaults({}, this.ctx.db.config[name]) this.provider = provider this.name = name this.updating = false @@ -26,14 +33,18 @@ export default class Application extends EventEmitter { // Apply defaults to config this.config.updateEvery = this.config.updateEvery || 180 - this.config.waitUntilFail = this.config.waitUntilFail || (60 * 1000) + this.config.startWaitUntilFail = this.config.startWaitUntilFail || (60 * 1000) + this.config.heartbeatTimeout = this.config.heartbeatTimeout || (3 * 1000) + this.config.heartbeatAttempts = this.config.heartbeatAttempts || 5 + this.config.heartbeatAttemptsWait = this.config.heartbeatAttemptsWait || (2 * 1000) + this.config.heartbeatPath = this.config.heartbeatPath || '/' Object.assign(this, { setInterval: opts.setInterval || setInterval, fs: opts.fs || fs, }) - this.db.addApplication(name) + this.ctx.db.addApplication(name) } startAutoupdater() { @@ -42,10 +53,10 @@ export default class Application extends EventEmitter { let timer = this.setInterval(() => { this.update().then( () => { - this.db.data.core[this.name].updater += 'Automatic update finished successfully. ' + this.ctx.db.data.core[this.name].updater += 'Automatic update finished successfully. ' }, (err) => { - this.db.data.core[this.name].updater += 'Error while running automatic update: ' + err.message + '. ' + this.ctx.db.data.core[this.name].updater += 'Error while running automatic update: ' + err.message + '. ' } ) }, this.config.updateEvery * 60 * 1000) @@ -53,8 +64,8 @@ export default class Application extends EventEmitter { } updateLog(message) { - this.db.data.core[this.name].updater += message - this.db.log.info(message) + this.ctx.db.data.core[this.name].updater += message + this.ctx.db.log.info(message) return message } @@ -62,10 +73,10 @@ export default class Application extends EventEmitter { update() { if (this.provider.static) { - if (this.db.data.core[this.name].updater !== this.msgStatic) { - this.db.data.core[this.name].updater = '' + if (this.ctx.db.data.core[this.name].updater !== this.msgStatic) { + this.ctx.db.data.core[this.name].updater = '' this.updateLog(this.msgStatic) - return this.db.write() + return this.ctx.db.write() } return Promise.resolve() } @@ -76,11 +87,11 @@ export default class Application extends EventEmitter { return this._update() .then(() => { this.updating = false - return this.db.write() + return this.ctx.db.write() }) .catch((err) => { this.updating = false - return this.db.write() + return this.ctx.db.write() .then(function() { return Promise.reject(err) }) }) } @@ -96,7 +107,7 @@ export default class Application extends EventEmitter { } async _update() { - this.db.data.core[this.name].updater = '' + this.ctx.db.data.core[this.name].updater = '' let cleanup = true let folder = '' let log = '' @@ -111,7 +122,7 @@ export default class Application extends EventEmitter { log += this.updateLog(`Found ${latest.version}. `) + '\n' // If the versino matches the latest installed, then there's nothing to do - if (this.db.data.core[this.name].latestInstalled === latest.version) { + if (this.ctx.db.data.core[this.name].latestInstalled === latest.version) { this.updateLog('Already up to date, nothing to do. ') return } @@ -121,7 +132,7 @@ export default class Application extends EventEmitter { latest.id = latest.version // check to see if we already have this version in our database. - var found = this.db.get(this.db.data.core[this.name].versions, latest.id) + var found = this.ctx.db.get(this.ctx.db.data.core[this.name].versions, latest.id) if (found) { // Check if the existing version found was already installed. if (found.installed) { @@ -155,18 +166,18 @@ export default class Application extends EventEmitter { } else { // This is a new version, mark it with stable tag of zero. latest.stable = 0 - this.db.upsertFirst(this.db.data.core[this.name].versions, latest) + this.ctx.db.upsertFirst(this.ctx.db.data.core[this.name].versions, latest) } // The target file for the archive and the target folder for new our version - let target = this.util.getPathFromRoot(`./${this.name}/${latest.version}/file${this.util.getExtension(latest.filename)}`) - folder = this.util.getPathFromRoot(`./${this.name}/${latest.version}`) + let target = this.ctx.util.getPathFromRoot(`./${this.name}/${latest.version}/file${this.ctx.util.getExtension(latest.filename)}`) + folder = this.ctx.util.getPathFromRoot(`./${this.name}/${latest.version}`) // Create it in case it does not exist. await this.fs.mkdir(folder, { recursive: true }) log += this.updateLog(`Downloading ${latest.link} to ${target}. `) + '\n' - await this.db.write() + await this.ctx.db.write() // Download the latest version using the provider in question. await this.provider.downloadVersion(latest, target) @@ -176,10 +187,10 @@ export default class Application extends EventEmitter { }) log += '\n' + this.updateLog(`Extracting ${target}. `) + '\n' - await this.db.write() + await this.ctx.db.write() // Download was successful, extract the archived file that we downloaded - await this.util.extractFile(target, function(msg) { + await this.ctx.util.extractFile(target, function(msg) { log += msg }).catch(function(err) { latest.failtodownload = (latest.failtodownload || 0) + 1 @@ -196,7 +207,7 @@ export default class Application extends EventEmitter { // check if the version we downloaded had index.mjs. If this is // missing then either the extracting or download failed without erroring // or the archived is borked. - await this.fs.stat(this.util.getPathFromRoot(`./${this.name}/${latest.version}/index.mjs`)) + await this.fs.stat(this.ctx.util.getPathFromRoot(`./${this.name}/${latest.version}/index.mjs`)) .catch((err) => { latest.failtodownload = (latest.failtodownload || 0) + 1 log += this.updateLog('Version did not include or was missing index.mjs. ') + '\n' @@ -212,16 +223,16 @@ export default class Application extends EventEmitter { // Check if we have a package.json file. If we do, we need to run // npm install. If we don't then this application either has all the // required packages or it doesn't need them to run - let packageStat = await this.fs.stat(this.util.getPathFromRoot(`./${this.name}/${latest.version}/package.json`)) + let packageStat = await this.fs.stat(this.ctx.util.getPathFromRoot(`./${this.name}/${latest.version}/package.json`)) .catch(function() { return null }) if (packageStat) { log += this.updateLog(`running npm install --production. `) + '\n' - await this.db.write() + await this.ctx.db.write() // For some weird reason, --loglevel=notice is required otherwise // we get practically zero log output. - await this.util.runCommand( + await this.ctx.util.runCommand( 'npm.cmd', ['install', '--production', '--no-optional', '--no-package-lock', '--no-audit', '--loglevel=notice'], folder, @@ -255,33 +266,45 @@ export default class Application extends EventEmitter { // If we reached here then everything went swimmingly. Mark the version // as being installed and attach the install log to it. log += this.updateLog(`Finished updating ${this.name} to version ${latest.version}.`) + '\n' - this.db.data.core[this.name].latestInstalled = latest.version + this.ctx.db.data.core[this.name].latestInstalled = latest.version latest.installed = true latest.log = log } - registerModule(module) { + registerModule(module, version = '') { if (module && typeof(module) === 'function') { return this.registerModule({ start: module }) } if (!module || typeof(module) !== 'object' || typeof(module.start) !== 'function') { - throw new Error(`Application ${this.name} registerModule was called with a non module missing start function`) + throw new Error(`Application ${this.name}${version ? ' version ' + version : '' } registerModule was called with a non module missing start function`) } this.module = module } async runVersion(version) { - let module = this.module + this.ctx.db.data.core[this.name].active = version + await this.ctx.db.write() - let errTimeout = new Error(`Application ${this.name} version ${version} timed out (took over ${this.config.waitUntilFail}ms) while running start()`) + if (version !== 'static') { + let indexPath = this.ctx.util.getPathFromRoot(`./${this.name}/${version}/index.mjs`) + await this.fs.stat(indexPath).catch((err) => { + return Promise.reject(new Error(`Application ${this.name} version ${version} was missing index.mjs: ${err.message}`)) + }) + let module = await import(this.ctx.util.getUrlFromRoot(`./${this.name}/${version}/index.mjs`)).catch((err) => { + return Promise.reject(new Error(`Application ${this.name} version ${version} failed to load index.mjs: ${err.message}`)) + }) + this.registerModule(module, version) + } + + let errTimeout = new Error(`Application ${this.name} version ${version} timed out (took over ${this.config.startWaitUntilFail}ms) while running start()`) await new Promise((res, rej) => { setTimeout(() => { rej(errTimeout) - }, this.config.waitUntilFail) + }, this.config.startWaitUntilFail) - let startRes = module.start(this.db, this.db.log, this.http, this.config.port) + let startRes = this.module.start(this.http, this.config.port, this.ctx) if (startRes && startRes.then) { return startRes.then(res, rej) } @@ -291,5 +314,22 @@ export default class Application extends EventEmitter { if (!this.http.active) { return Promise.reject(new Error(`Application ${this.name} version ${version} did not call http.createServer()`)) } + + let lastErr = null + + for (let i = 0; i < this.config.heartbeatAttempts; i++) { + try { + await request({ timeout: this.config.heartbeatAttemptsWait }, `http://localhost:${this.config.port}` + this.config.heartbeatPath, null, 0, true) + return + } catch (err) { + lastErr = err + } + } + + return Promise.reject(new Error(`Application ${this.name} version ${version} failed to start properly: ${lastErr.message}`)) } -} \ No newline at end of file + + closeServer() { + return this.http.closeServer() + } +} diff --git a/core/client.mjs b/core/client.mjs index 44deb41..65384d8 100644 --- a/core/client.mjs +++ b/core/client.mjs @@ -46,13 +46,13 @@ export function request(config, path, filePath = null, redirects, fastRaw = fals if (config.token) { headers['Authorization'] = `token ${config.token}` } - let timeout = fastRaw ? 5000 : config.timeout || 10000 + let timeout = config.timeout || 10000 let timedout = false let timer = setTimeout(function() { timedout = true if (req) { req.destroy() } - reject(new Error(`Request ${path} timed out out after ${timeout}`)) + reject(new Error(`Request ${path} timed out after ${timeout}ms`)) }, timeout) req = h.request({ diff --git a/core/core.mjs b/core/core.mjs index ba9961d..3646915 100644 --- a/core/core.mjs +++ b/core/core.mjs @@ -1,499 +1,68 @@ -import fs from 'fs' -import { EventEmitter } from 'events' -import { request } from './client.mjs' -import HttpServer from './http.mjs' +import Application from './application.mjs' +import Util from './util.mjs' +import { Low } from 'lowdb' -const fsp = fs.promises +export default class Core { + static providers = new Map() + static addProvider(name, provider) { + if (!name || typeof(name) !== 'string') + throw new Error('addProvider name must be a string') + if (typeof(provider) !== 'function') + throw new Error(`addProvider ${name} provider must be a class`) + + let test = new provider({}) + if (typeof(test.checkConfig) !== 'function') + throw new Error(`addProvider ${name} provider class missing checkConfig`) + if (typeof(test.getLatestVersion) !== 'function') + throw new Error(`addProvider ${name} provider class missing getLatestVersion`) + + Core.providers.set(name, provider) + } + + constructor(db, util, log, restart) { + // some sanity checks + if (typeof(restart) !== 'function') throw new Error('restart parameter was not a function') + if (!log || typeof(log) !== 'object') throw new Error('log parameter was invalid') + if (typeof(log.event) !== 'object') throw new Error('log parameter was invalid') + if (typeof(log.info) !== 'function' + || typeof(log.warn) !== 'function' + || typeof(log.error) !== 'function' + || typeof(log.event.info) !== 'function' + || typeof(log.event.warn) !== 'function' + || typeof(log.event.error) !== 'function') throw new Error('log parameter was invalid') + if (!util || !(util instanceof Util)) throw new Error('util not instance of Util') + if (!db || !(db instanceof Low)) throw new Error('db not instance of Low') -export default class Core extends EventEmitter{ - constructor(util, config, db, log, closeCb) { - super() - process.stdin.resume() - this.http = new HttpServer() - this.util = util - this.config = config this.db = db + this.util = util this.log = log - this._close = closeCb - this._activeCrashHandler = null - this.appRunning = false - this.manageRunning = false - this.monitoring = false - this._appUpdating = { - fresh: true, - updating: false, - starting: false, - logs: '', - } - this._manageUpdating = { - fresh: true, - updating: false, - starting: false, - logs: '', - } - - this.db.set('core.manageActive', null) - .set('core.appActive', null) - .write().then() + this.restart = restart + this.applications = [] + this.applicationMap = new Map() } - startMonitor() { - if (this.monitoring) return - this.log.info('[Scheduler] Automatic updater has been turned on. Will check for updates every 3 hours') - let updating = false - - this.monitoring = setInterval(async () => { - if (updating) return - updating = true - this.log.info('[Scheduler] Starting automatic check for latest version of app and manage') - - try { - await this.installLatestVersion('app') - await this.installLatestVersion('manage') - } catch(err) { - this.log.error(err, 'Error checking for latest versions') - this.log.event.error('Error checking for latest versions: ' + err.message) - updating = false - return - } - - try { - if (this.hasNewVersionAvailable('app') || !this.appRunning) { - await this.tryStartProgram('app') - } - } catch(err) { - this.log.error(err, 'Unknown error occured attempting to app') - this.log.event.error('Unknown error starting app: ' + err.message) - } - try { - if (this.hasNewVersionAvailable('manage') || !this.manageRunning) { - await this.tryStartProgram('manage') - } - } catch(err) { - this.log.error(err, 'Unknown error occured attempting to start manage') - this.log.event.error('Unknown error starting manage: ' + err.message) - } - updating = false - }, 1000 * 60 * 60 * 3) // every 3 hours + getApplication(name) { + return this.applicationMap.get(name) } - restart() { - this._close() - } + async init() { + this.util.verifyConfig(this.db.config) - status() { - return { - app: this.appRunning, - manage: this.manageRunning, - appUpdating: this._appUpdating.updating, - manageUpdating: this._manageUpdating.updating, - appStarting: this._appUpdating.starting, - manageStarting: this._manageUpdating.starting, - } - } + let names = this.util.getAppNames(this.db.config) - async getLatestVersion(active, name) { - // Example: 'https://api.github.com/repos/thething/sc-helloworld/releases' - this.logActive(name, active, `Updater: Fetching release info from: https://api.github.com/repos/${this.config[name + 'Repository']}/releases\n`) + for (let name of names) { + let provConstructor = Core.providers.get(this.db.config[name].provider) + let provider = new provConstructor(this.db.config[name]) + await provider.checkConfig(this.db.config[name]) - let result = await request(this.config, `https://api.github.com/repos/${this.config[name + 'Repository']}/releases`) - - let items = result.body.filter(function(item) { - if (!item.assets.length) return false - for (let i = 0; i < item.assets.length; i++) { - if (item.assets[i].name.endsWith('-sc.zip')) return true - } - }) - - if (items && items.length) { - for (let x = 0; x < items.length; x++) { - let item = items[x] - for (let i = 0; i < item.assets.length; i++) { - if (item.assets[i].name.endsWith('-sc.zip')) { - if (this.db.get('core.' + name + 'LatestInstalled').value() === item.name) { - this.logActive(name, active, `Updater: Latest version already installed, exiting early\n`) - return null - } - this.logActive(name, active, `Updater: Found version ${item.name} with file ${item.assets[i].name}\n`) - - await this.db.set(`core.${name}LatestVersion`, item.name) - .write() - this.emit('dbupdated', {}) - - return { - name: item.name, - filename: item.assets[i].name, - url: item.assets[i].browser_download_url, - description: item.body, - } - } - } - } - } else { - return null - } - } - - logActive(name, active, logline, doNotPrint = false) { - if (!doNotPrint) { - this.log.info(`[${name}] ` + logline.replace(/\n/g, '')) - } - active.logs += logline - this.emit(name + 'log', active) - } - - getProgramLogs(name) { - if (name === 'app' && this._appUpdating.logs) { - return this._appUpdating.logs - } else if (name === 'manage' && this._manageUpdating.logs) { - return this._manageUpdating.logs - } - - let latestInstalled = this.db.get('core.' + name + 'LatestInstalled').value() - let latestVersion = this.db.get('core.' + name + 'LatestVersion').value() - if (latestVersion) { - let value = this.db.get(`core_${name}History`).getById(latestVersion).value() - if (value) return value.logs - } - if (latestInstalled) { - let value = this.db.get(`core_${name}History`).getById(latestInstalled).value() - if (value) return value.logs - } - return '< no logs found >' - } - - async installVersion(name, active, version) { - if (fs.existsSync(this.util.getPathFromRoot(`./${name}/` + version.name))) { - await this.util.runCommand('rmdir', ['/S', '/Q', `"${this.util.getPathFromRoot(`./${name}/` + version.name)}"`]) - } - if (!fs.existsSync(this.util.getPathFromRoot(`./${name}/`))) { - await fsp.mkdir(this.util.getPathFromRoot(`./${name}/`)) - } - try { - await fsp.mkdir(this.util.getPathFromRoot(`./${name}/` + version.name)) - } catch(err) { - if (err.code !== 'EEXIST') { - throw err - } - } - // await fsp.mkdir(this.util.getPathFromRoot(`./${name}/` + version.name + '/node_modules')) - this.logActive(name, active, `Installer: Downloading ${version.name} (${version.url}) to ${version.name + '/' + version.name + '.zip'}\n`) - let filePath = this.util.getPathFromRoot(`./${name}/` + version.name + '/' + version.name + '.zip') - await request(this.config, version.url, filePath) - this.logActive(name, active, `Installer: Downloading finished, starting extraction\n`) - await this.util.runCommand( - '"C:\\Program Files\\7-Zip\\7z.exe"', - ['x', `"${filePath}"`], - this.util.getPathFromRoot(`./${name}/` + version.name + '/'), - this.logActive.bind(this, name, active) - ) - - if (!fs.existsSync(this.util.getPathFromRoot(`./${name}/` + version.name + '/index.mjs'))) { - this.logActive(name, active, `\nInstaller: ERROR: Missing index.mjs in the folder, exiting\n`) - throw new Error(`Missing index.mjs in ${this.util.getPathFromRoot(`./${name}/` + version.name + '/index.mjs')}`) - } - - this.logActive(name, active, `\nInstaller: Starting npm install\n`) - - await this.util.runCommand( - 'npm.cmd', - ['install', '--production', '--no-optional', '--no-package-lock', '--no-audit'], - this.util.getPathFromRoot(`./${name}/` + version.name + '/'), - this.logActive.bind(this, name, active) - ) - - await this.db.set(`core.${name}LatestInstalled`, version.name) - .write() - this.emit('dbupdated', {}) - - this.logActive(name, active, `\nInstaller: Successfully installed ${version.name}\n`) - } - - getActive(name) { - if (name === 'app') { - return this._appUpdating - } else if (name === 'manage') { - return this._manageUpdating - } else { - throw new Error('Invalid name: ' + name) - } - } - - async startModule(module, port) { - let out = await module.start(this.config, this.db, this.log, this, this.http, port) - if (out && out.then) { - await out - } - if (!this.http.getCurrentServer()) { - this.log.warn('Module did not call http.createServer') - } - } - - hasNewVersionAvailable(name) { - let newestVersion = this.db.get(`core.${name}LatestInstalled`).value() - if (!newestVersion) return false - - let history = this.db.get(`core_${name}History`).getById(newestVersion).value() - if (history.installed && history.stable === 0) { - return true - } - return false - } - - async tryStartProgram(name) { - let active = this.getActive(name) - - if (this[name + 'Running'] && !this.hasNewVersionAvailable(name)) { - this.log.event.warn('Attempting to start ' + name + ' which is already running') - this.log.warn('Attempting to start ' + name + ' which is already running') - this.logActive(name, active, `Runner: Attempting to start it but it is already running\n`, true) - return - } - active.starting = true - - if (this[name + 'Running']) { - let success = await this.http.closeServer(name) - if (!success) { - if (process.env.NODE_ENV === 'production') { - this.logActive(name, active, `Runner: Found new version but server could not be shut down, restarting service core\n`) - await new Promise(() => { - this.log.event.warn('Found new version of ' + name + ' but server could not be shut down gracefully, restarting...', null, () => { - process.exit(100) - }) - }) - } else { - this.logActive(name, active, `Runner: Found new version but server could not be shut down\n`) - return - } - } - this[name + 'Running'] = false - this.emit('statusupdated', {}) - } - - let history = this.db.get(`core_${name}History`) - .filter('installed') - .orderBy('installed', 'desc') - .value() - this.logActive(name, active, `Runner: Finding available version\n`) - - for (let i = 0; i < history.length; i++) { - if ((history[i].stable === -1 && !active.fresh) - || (history[i].stable < -1)) { - this.logActive(name, active, `Runner: Skipping version ${history[i].name} due to marked as unstable\n`) - continue - } - - await this.db.set(`core.${name}Active`, history[i].name) - .write() - this.emit('dbupdated', {}) - - let running = await this.tryStartProgramVersion(name, active, history[i].name) - if (running) { - history[i].stable = 1 - } else { - if (active.fresh || history[i].stable === -1) { - history[i].stable = -2 - } else { - history[i].stable = -1 - } - await this.db.set(`core.${name}Active`, null) - .write() - this.emit('dbupdated', {}) - } - active.fresh = false - - await this.db.get(`core_${name}History`).updateById(history[i].id, history[i].stable).write() - if (history[i].stable > 0) break - } - - if (!this.db.get(`core.${name}Active`).value()) { - this.logActive(name, active, `Runner: Could not find any available stable version of ${name}\n`) - this.log.error('Unable to start ' + name) - this.log.event.error('Unable to start ' + name) - } - - active.starting = false - } - - programCrashed(name, version, active, oldStable) { - let newStable = -2 - console.log('EXITING:', oldStable, active) - if (oldStable === 0 && !active.fresh) { - newStable = -1 - } - let temp = this.db.get(`core_${name}History`).getById(version).set('stable', newStable ) - temp.value() // Trigger update on __wrapped__ - fs.writeFileSync(this.db.adapterFilePath, JSON.stringify(temp.__wrapped__, null, 2)) - } - - async tryStartProgramVersion(name, active, version) { - if (!version) return false - this.logActive(name, active, `Runner: Attempting to start ${version}\n`) - let indexPath = this.util.getUrlFromRoot(`./${name}/` + version + '/index.mjs') - let module - - try { - this.logActive(name, active, `Runner: Loading ${indexPath}\n`) - module = await import(indexPath) - } catch (err) { - this.logActive(name, active, `Runner: Error importing module\n`, true) - this.logActive(name, active, `${err.stack}\n`, true) - this.log.error(err, `Failed to load ${indexPath}`) - return false - } - - let checkTimeout = null - let oldStable = this.db.get(`core_${name}History`).getById(version).value().stable - this._activeCrashHandler = this.programCrashed.bind(this, name, version, active, oldStable) - process.once('exit', this._activeCrashHandler) - try { - let port = name === 'app' ? this.config.port : this.config.managePort - await new Promise((res, rej) => { - checkTimeout = setTimeout(function() { - rej(new Error('Program took longer than 60 seconds to resolve promise')) - }, 60 * 1000) - - this.logActive(name, active, `Runner: Starting module\n`) - - try { - this.http.setContext(name) - this.startModule(module, port) - .then(res, rej) - } catch (err) { - rej(err) - } - }) - clearTimeout(checkTimeout) - - await this.checkProgramRunning(name, active, port) - process.off('exit', this._activeCrashHandler) - } catch (err) { - clearTimeout(checkTimeout) - process.off('exit', this._activeCrashHandler) - await this.http.closeServer(name) - - this.logActive(name, active, `Runner: Error starting\n`, true) - this.logActive(name, active, `${err.stack}\n`, true) - this.log.error(err, `Failed to start ${name}`) - return false - } - this._activeCrashHandler = null - - this.logActive(name, active, `Runner: Successfully started version ${version}\n`) - await this.db.set(`core.${name}Active`, version) - .write() - - if (name === 'app') { - this.appRunning = true - } else { - this.manageRunning = true - } - this.emit('statusupdated', {}) - - this.logActive(name, active, `Runner: Module is running successfully\n`) - - return true - } - - async checkProgramRunning(name, active, port) { - this.logActive(name, active, `Checker: Testing out module port ${port}\n`) - let start = new Date() - let error = null - let success = false - - while (new Date() - start < 10 * 1000) { - try { - let check = await request(this.config, `http://localhost:${port}`, null, 0, true) - success = true - break - } catch(err) { - this.logActive(name, active, `Checker: ${err.message}, retrying in 3 seconds\n`) - error = err - await new Promise(function(res) { setTimeout(res, 3000)}) - } - } - if (success) return true - throw error || new Error('Checking server failed') - } - - async installLatestVersion(name) { - if (!this.config[name + 'Repository']) { - if (name === 'app') { - this.log.error(name + ' Repository was missing from config') - this.log.event.error(name + ' Repository was missing from config') - } else { - this.log.warn(name + ' Repository was missing from config') - this.log.event.warn(name + ' Repository was missing from config') - } - return - } - - let active = this.getActive(name) - let oldLogs = active.logs || '' - if (oldLogs) { - oldLogs += '\n' - } - active.logs = '' - active.updating = true - - this.emit('statusupdated', {}) - this.logActive(name, active, `Installer: Checking for updates at time: ${new Date().toISOString().replace('T', ' ').split('.')[0]}\n`) - - let version = null - let installed = false - let found = false - try { - version = await this.getLatestVersion(active, name) - if (version) { - let core = this.db.get('core').value() - let fromDb = this.db.get(`core_${name}History`).getById(version.name).value() - if (!fromDb || !fromDb.installed) { - let oldVersion = core[name + 'Current'] || '' - this.logActive(name, active, `Installer: Updating from ${oldVersion} to ${version.name}\n`) - await this.installVersion(name, active, version) - this.logActive(name, active, `Installer: Finished: ${new Date().toISOString().replace('T', ' ').split('.')[0]}\n`) - installed = new Date() - } else { - found = true - this.logActive(name, active, `Installer: Version ${version.name} already installed\n`) - } - } - } catch(err) { - this.logActive(name, active, '\n', true) - this.logActive(name, active, `Installer: Exception occured while updating ${name}\n`, true) - this.logActive(name, active, err.stack, true) - this.log.error('Error while updating ' + name, err) - } - active.updating = false - if (version && !found) { - await this.db.get(`core_${name}History`).upsert({ - id: version.name, - name: version.name, - filename: version.filename, - url: version.url, - description: version.description, - logs: active.logs, - stable: 0, - installed: installed && installed.toISOString(), - }).write() - } - active.logs = oldLogs + active.logs - this.emit(name + 'log', active) - this.emit('statusupdated', {}) - } - - async start(name) { - var version = this.db.get('core.' + name + 'LatestInstalled').value() - if (version) { - await this.tryStartProgram(name) - } - - await this.installLatestVersion(name) - - if (version !== this.db.get('core.' + name + 'LatestInstalled').value()) { - if (!this[name + 'Running'] || this.hasNewVersionAvailable(name)) { - await this.tryStartProgram(name) - } + let application = new Application({ + db: this.db, + util: this.util, + log: this.log, + core: this, + }, provider, name) + this.applications.push(application) + this.applicationMap.set(name, application) } } } diff --git a/core/core_new.mjs b/core/core_new.mjs deleted file mode 100644 index e69de29..0000000 diff --git a/core/core_old.mjs b/core/core_old.mjs new file mode 100644 index 0000000..ba9961d --- /dev/null +++ b/core/core_old.mjs @@ -0,0 +1,499 @@ +import fs from 'fs' +import { EventEmitter } from 'events' +import { request } from './client.mjs' +import HttpServer from './http.mjs' + +const fsp = fs.promises + +export default class Core extends EventEmitter{ + constructor(util, config, db, log, closeCb) { + super() + process.stdin.resume() + this.http = new HttpServer() + this.util = util + this.config = config + this.db = db + this.log = log + this._close = closeCb + this._activeCrashHandler = null + this.appRunning = false + this.manageRunning = false + this.monitoring = false + this._appUpdating = { + fresh: true, + updating: false, + starting: false, + logs: '', + } + this._manageUpdating = { + fresh: true, + updating: false, + starting: false, + logs: '', + } + + this.db.set('core.manageActive', null) + .set('core.appActive', null) + .write().then() + } + + startMonitor() { + if (this.monitoring) return + this.log.info('[Scheduler] Automatic updater has been turned on. Will check for updates every 3 hours') + let updating = false + + this.monitoring = setInterval(async () => { + if (updating) return + updating = true + this.log.info('[Scheduler] Starting automatic check for latest version of app and manage') + + try { + await this.installLatestVersion('app') + await this.installLatestVersion('manage') + } catch(err) { + this.log.error(err, 'Error checking for latest versions') + this.log.event.error('Error checking for latest versions: ' + err.message) + updating = false + return + } + + try { + if (this.hasNewVersionAvailable('app') || !this.appRunning) { + await this.tryStartProgram('app') + } + } catch(err) { + this.log.error(err, 'Unknown error occured attempting to app') + this.log.event.error('Unknown error starting app: ' + err.message) + } + try { + if (this.hasNewVersionAvailable('manage') || !this.manageRunning) { + await this.tryStartProgram('manage') + } + } catch(err) { + this.log.error(err, 'Unknown error occured attempting to start manage') + this.log.event.error('Unknown error starting manage: ' + err.message) + } + updating = false + }, 1000 * 60 * 60 * 3) // every 3 hours + } + + restart() { + this._close() + } + + status() { + return { + app: this.appRunning, + manage: this.manageRunning, + appUpdating: this._appUpdating.updating, + manageUpdating: this._manageUpdating.updating, + appStarting: this._appUpdating.starting, + manageStarting: this._manageUpdating.starting, + } + } + + async getLatestVersion(active, name) { + // Example: 'https://api.github.com/repos/thething/sc-helloworld/releases' + this.logActive(name, active, `Updater: Fetching release info from: https://api.github.com/repos/${this.config[name + 'Repository']}/releases\n`) + + let result = await request(this.config, `https://api.github.com/repos/${this.config[name + 'Repository']}/releases`) + + let items = result.body.filter(function(item) { + if (!item.assets.length) return false + for (let i = 0; i < item.assets.length; i++) { + if (item.assets[i].name.endsWith('-sc.zip')) return true + } + }) + + if (items && items.length) { + for (let x = 0; x < items.length; x++) { + let item = items[x] + for (let i = 0; i < item.assets.length; i++) { + if (item.assets[i].name.endsWith('-sc.zip')) { + if (this.db.get('core.' + name + 'LatestInstalled').value() === item.name) { + this.logActive(name, active, `Updater: Latest version already installed, exiting early\n`) + return null + } + this.logActive(name, active, `Updater: Found version ${item.name} with file ${item.assets[i].name}\n`) + + await this.db.set(`core.${name}LatestVersion`, item.name) + .write() + this.emit('dbupdated', {}) + + return { + name: item.name, + filename: item.assets[i].name, + url: item.assets[i].browser_download_url, + description: item.body, + } + } + } + } + } else { + return null + } + } + + logActive(name, active, logline, doNotPrint = false) { + if (!doNotPrint) { + this.log.info(`[${name}] ` + logline.replace(/\n/g, '')) + } + active.logs += logline + this.emit(name + 'log', active) + } + + getProgramLogs(name) { + if (name === 'app' && this._appUpdating.logs) { + return this._appUpdating.logs + } else if (name === 'manage' && this._manageUpdating.logs) { + return this._manageUpdating.logs + } + + let latestInstalled = this.db.get('core.' + name + 'LatestInstalled').value() + let latestVersion = this.db.get('core.' + name + 'LatestVersion').value() + if (latestVersion) { + let value = this.db.get(`core_${name}History`).getById(latestVersion).value() + if (value) return value.logs + } + if (latestInstalled) { + let value = this.db.get(`core_${name}History`).getById(latestInstalled).value() + if (value) return value.logs + } + return '< no logs found >' + } + + async installVersion(name, active, version) { + if (fs.existsSync(this.util.getPathFromRoot(`./${name}/` + version.name))) { + await this.util.runCommand('rmdir', ['/S', '/Q', `"${this.util.getPathFromRoot(`./${name}/` + version.name)}"`]) + } + if (!fs.existsSync(this.util.getPathFromRoot(`./${name}/`))) { + await fsp.mkdir(this.util.getPathFromRoot(`./${name}/`)) + } + try { + await fsp.mkdir(this.util.getPathFromRoot(`./${name}/` + version.name)) + } catch(err) { + if (err.code !== 'EEXIST') { + throw err + } + } + // await fsp.mkdir(this.util.getPathFromRoot(`./${name}/` + version.name + '/node_modules')) + this.logActive(name, active, `Installer: Downloading ${version.name} (${version.url}) to ${version.name + '/' + version.name + '.zip'}\n`) + let filePath = this.util.getPathFromRoot(`./${name}/` + version.name + '/' + version.name + '.zip') + await request(this.config, version.url, filePath) + this.logActive(name, active, `Installer: Downloading finished, starting extraction\n`) + await this.util.runCommand( + '"C:\\Program Files\\7-Zip\\7z.exe"', + ['x', `"${filePath}"`], + this.util.getPathFromRoot(`./${name}/` + version.name + '/'), + this.logActive.bind(this, name, active) + ) + + if (!fs.existsSync(this.util.getPathFromRoot(`./${name}/` + version.name + '/index.mjs'))) { + this.logActive(name, active, `\nInstaller: ERROR: Missing index.mjs in the folder, exiting\n`) + throw new Error(`Missing index.mjs in ${this.util.getPathFromRoot(`./${name}/` + version.name + '/index.mjs')}`) + } + + this.logActive(name, active, `\nInstaller: Starting npm install\n`) + + await this.util.runCommand( + 'npm.cmd', + ['install', '--production', '--no-optional', '--no-package-lock', '--no-audit'], + this.util.getPathFromRoot(`./${name}/` + version.name + '/'), + this.logActive.bind(this, name, active) + ) + + await this.db.set(`core.${name}LatestInstalled`, version.name) + .write() + this.emit('dbupdated', {}) + + this.logActive(name, active, `\nInstaller: Successfully installed ${version.name}\n`) + } + + getActive(name) { + if (name === 'app') { + return this._appUpdating + } else if (name === 'manage') { + return this._manageUpdating + } else { + throw new Error('Invalid name: ' + name) + } + } + + async startModule(module, port) { + let out = await module.start(this.config, this.db, this.log, this, this.http, port) + if (out && out.then) { + await out + } + if (!this.http.getCurrentServer()) { + this.log.warn('Module did not call http.createServer') + } + } + + hasNewVersionAvailable(name) { + let newestVersion = this.db.get(`core.${name}LatestInstalled`).value() + if (!newestVersion) return false + + let history = this.db.get(`core_${name}History`).getById(newestVersion).value() + if (history.installed && history.stable === 0) { + return true + } + return false + } + + async tryStartProgram(name) { + let active = this.getActive(name) + + if (this[name + 'Running'] && !this.hasNewVersionAvailable(name)) { + this.log.event.warn('Attempting to start ' + name + ' which is already running') + this.log.warn('Attempting to start ' + name + ' which is already running') + this.logActive(name, active, `Runner: Attempting to start it but it is already running\n`, true) + return + } + active.starting = true + + if (this[name + 'Running']) { + let success = await this.http.closeServer(name) + if (!success) { + if (process.env.NODE_ENV === 'production') { + this.logActive(name, active, `Runner: Found new version but server could not be shut down, restarting service core\n`) + await new Promise(() => { + this.log.event.warn('Found new version of ' + name + ' but server could not be shut down gracefully, restarting...', null, () => { + process.exit(100) + }) + }) + } else { + this.logActive(name, active, `Runner: Found new version but server could not be shut down\n`) + return + } + } + this[name + 'Running'] = false + this.emit('statusupdated', {}) + } + + let history = this.db.get(`core_${name}History`) + .filter('installed') + .orderBy('installed', 'desc') + .value() + this.logActive(name, active, `Runner: Finding available version\n`) + + for (let i = 0; i < history.length; i++) { + if ((history[i].stable === -1 && !active.fresh) + || (history[i].stable < -1)) { + this.logActive(name, active, `Runner: Skipping version ${history[i].name} due to marked as unstable\n`) + continue + } + + await this.db.set(`core.${name}Active`, history[i].name) + .write() + this.emit('dbupdated', {}) + + let running = await this.tryStartProgramVersion(name, active, history[i].name) + if (running) { + history[i].stable = 1 + } else { + if (active.fresh || history[i].stable === -1) { + history[i].stable = -2 + } else { + history[i].stable = -1 + } + await this.db.set(`core.${name}Active`, null) + .write() + this.emit('dbupdated', {}) + } + active.fresh = false + + await this.db.get(`core_${name}History`).updateById(history[i].id, history[i].stable).write() + if (history[i].stable > 0) break + } + + if (!this.db.get(`core.${name}Active`).value()) { + this.logActive(name, active, `Runner: Could not find any available stable version of ${name}\n`) + this.log.error('Unable to start ' + name) + this.log.event.error('Unable to start ' + name) + } + + active.starting = false + } + + programCrashed(name, version, active, oldStable) { + let newStable = -2 + console.log('EXITING:', oldStable, active) + if (oldStable === 0 && !active.fresh) { + newStable = -1 + } + let temp = this.db.get(`core_${name}History`).getById(version).set('stable', newStable ) + temp.value() // Trigger update on __wrapped__ + fs.writeFileSync(this.db.adapterFilePath, JSON.stringify(temp.__wrapped__, null, 2)) + } + + async tryStartProgramVersion(name, active, version) { + if (!version) return false + this.logActive(name, active, `Runner: Attempting to start ${version}\n`) + let indexPath = this.util.getUrlFromRoot(`./${name}/` + version + '/index.mjs') + let module + + try { + this.logActive(name, active, `Runner: Loading ${indexPath}\n`) + module = await import(indexPath) + } catch (err) { + this.logActive(name, active, `Runner: Error importing module\n`, true) + this.logActive(name, active, `${err.stack}\n`, true) + this.log.error(err, `Failed to load ${indexPath}`) + return false + } + + let checkTimeout = null + let oldStable = this.db.get(`core_${name}History`).getById(version).value().stable + this._activeCrashHandler = this.programCrashed.bind(this, name, version, active, oldStable) + process.once('exit', this._activeCrashHandler) + try { + let port = name === 'app' ? this.config.port : this.config.managePort + await new Promise((res, rej) => { + checkTimeout = setTimeout(function() { + rej(new Error('Program took longer than 60 seconds to resolve promise')) + }, 60 * 1000) + + this.logActive(name, active, `Runner: Starting module\n`) + + try { + this.http.setContext(name) + this.startModule(module, port) + .then(res, rej) + } catch (err) { + rej(err) + } + }) + clearTimeout(checkTimeout) + + await this.checkProgramRunning(name, active, port) + process.off('exit', this._activeCrashHandler) + } catch (err) { + clearTimeout(checkTimeout) + process.off('exit', this._activeCrashHandler) + await this.http.closeServer(name) + + this.logActive(name, active, `Runner: Error starting\n`, true) + this.logActive(name, active, `${err.stack}\n`, true) + this.log.error(err, `Failed to start ${name}`) + return false + } + this._activeCrashHandler = null + + this.logActive(name, active, `Runner: Successfully started version ${version}\n`) + await this.db.set(`core.${name}Active`, version) + .write() + + if (name === 'app') { + this.appRunning = true + } else { + this.manageRunning = true + } + this.emit('statusupdated', {}) + + this.logActive(name, active, `Runner: Module is running successfully\n`) + + return true + } + + async checkProgramRunning(name, active, port) { + this.logActive(name, active, `Checker: Testing out module port ${port}\n`) + let start = new Date() + let error = null + let success = false + + while (new Date() - start < 10 * 1000) { + try { + let check = await request(this.config, `http://localhost:${port}`, null, 0, true) + success = true + break + } catch(err) { + this.logActive(name, active, `Checker: ${err.message}, retrying in 3 seconds\n`) + error = err + await new Promise(function(res) { setTimeout(res, 3000)}) + } + } + if (success) return true + throw error || new Error('Checking server failed') + } + + async installLatestVersion(name) { + if (!this.config[name + 'Repository']) { + if (name === 'app') { + this.log.error(name + ' Repository was missing from config') + this.log.event.error(name + ' Repository was missing from config') + } else { + this.log.warn(name + ' Repository was missing from config') + this.log.event.warn(name + ' Repository was missing from config') + } + return + } + + let active = this.getActive(name) + let oldLogs = active.logs || '' + if (oldLogs) { + oldLogs += '\n' + } + active.logs = '' + active.updating = true + + this.emit('statusupdated', {}) + this.logActive(name, active, `Installer: Checking for updates at time: ${new Date().toISOString().replace('T', ' ').split('.')[0]}\n`) + + let version = null + let installed = false + let found = false + try { + version = await this.getLatestVersion(active, name) + if (version) { + let core = this.db.get('core').value() + let fromDb = this.db.get(`core_${name}History`).getById(version.name).value() + if (!fromDb || !fromDb.installed) { + let oldVersion = core[name + 'Current'] || '' + this.logActive(name, active, `Installer: Updating from ${oldVersion} to ${version.name}\n`) + await this.installVersion(name, active, version) + this.logActive(name, active, `Installer: Finished: ${new Date().toISOString().replace('T', ' ').split('.')[0]}\n`) + installed = new Date() + } else { + found = true + this.logActive(name, active, `Installer: Version ${version.name} already installed\n`) + } + } + } catch(err) { + this.logActive(name, active, '\n', true) + this.logActive(name, active, `Installer: Exception occured while updating ${name}\n`, true) + this.logActive(name, active, err.stack, true) + this.log.error('Error while updating ' + name, err) + } + active.updating = false + if (version && !found) { + await this.db.get(`core_${name}History`).upsert({ + id: version.name, + name: version.name, + filename: version.filename, + url: version.url, + description: version.description, + logs: active.logs, + stable: 0, + installed: installed && installed.toISOString(), + }).write() + } + active.logs = oldLogs + active.logs + this.emit(name + 'log', active) + this.emit('statusupdated', {}) + } + + async start(name) { + var version = this.db.get('core.' + name + 'LatestInstalled').value() + if (version) { + await this.tryStartProgram(name) + } + + await this.installLatestVersion(name) + + if (version !== this.db.get('core.' + name + 'LatestInstalled').value()) { + if (!this[name + 'Running'] || this.hasNewVersionAvailable(name)) { + await this.tryStartProgram(name) + } + } + } +} diff --git a/core/db.mjs b/core/db.mjs index 52670fc..71059c7 100644 --- a/core/db.mjs +++ b/core/db.mjs @@ -1,6 +1,5 @@ import { setTimeout } from 'timers/promises' import { Low, JSONFile, Memory } from 'lowdb' -import { type } from 'os' import { defaults, isObject } from './defaults.mjs' export default function GetDB(config, log, orgFilename = 'db.json') { @@ -87,9 +86,9 @@ export default function GetDB(config, log, orgFilename = 'db.json') { db.addApplication = function(name) { db.data.core[name] ||= {} defaults(db.data.core[name], { - active: null, - latestInstalled: null, - latestVersion: null, + active: '', + latestInstalled: '', + latestVersion: '', updater: '', versions: [], }) diff --git a/core/http.mjs b/core/http.mjs index dda2d77..4602250 100644 --- a/core/http.mjs +++ b/core/http.mjs @@ -24,6 +24,17 @@ export default class HttpServer { }) }) + server.listenAsync = (port, host) => { + return new Promise((res, rej) => { + server.once('error', rej) + + server.listen(port, host || '0.0.0.0', () => { + server.off('error', rej) + res() + }) + }) + } + this.active = server return server } @@ -38,11 +49,13 @@ export default class HttpServer { this.sockets.clear() this.active.close(err => { - if (err) return rej(err) + if (err) { + if (err.code !== 'ERR_SERVER_NOT_RUNNING') return rej(err) + } this.active = null // Waiting 1 second for it to close down - setTimeout(function() {res() }, 1000) + setTimeout(function() {res() }, 100) }) }) } diff --git a/core/log.mjs b/core/log.mjs index 6d340bb..7c9eaa1 100644 --- a/core/log.mjs +++ b/core/log.mjs @@ -1,15 +1,24 @@ -import nodewindows from 'node-windows' +// import nodewindows from 'node-windows' import bunyan from 'bunyan-lite' +import { setTimeout } from 'timers/promises' -export default function getLog(name) { +export default function getLog(name, streams = null, opts = {}) { let settings let ringbuffer = new bunyan.RingBuffer({ limit: 100 }) let ringbufferwarn = new bunyan.RingBuffer({ limit: 100 }) + if (streams) { + streams.forEach(function(stream) { + if (stream.stream === 'process.stdout') { + stream.stream = process.stdout + } + }) + } + if (process.env.NODE_ENV === 'production') { settings = { - "name": "service-core", - "streams": [{ + "name": name, + "streams": streams || [{ path: 'log.log', level: 'info', } @@ -17,8 +26,8 @@ export default function getLog(name) { } } else { settings = { - "name": "service-core", - "streams": [{ + "name": name, + "streams": streams || [{ "stream": process.stdout, "level": "debug" } @@ -53,16 +62,64 @@ export default function getLog(name) { level: 'info', }) + let eventManager = null + let eventManagerLoading = false + + async function safeLoadEvent(level, message, code) { + if (eventManager === false) { + return Promise.resolve() + } + + if (!eventManager) { + if (eventManagerLoading) { + for (let i = 0; i < 10 && eventManagerLoading; i++) { + await setTimeout(50) + } + if (eventManagerLoading) { + eventManager = false + } + return safeLoadEvent(level, message, code) + } + + eventManagerLoading = true + + let prom + if (opts.import) { + prom = opts.import('node-windows') + } else { + prom = import('node-windows') + } + await prom.then( + function(res) { eventManager = new res.default.EventLogger(name) }, + function() { eventManager = false }, + ) + eventManagerLoading = false + return safeLoadEvent(level, message, code) + } + + return new Promise(function(res) { + try { + eventManager[level](message, code, function() { res() }) + } catch { + res() + } + }) + } + // Create our logger. logger = bunyan.createLogger(settings) if (process.env.NODE_ENV === 'production') { - logger.event = new nodewindows.EventLogger(name) + logger.event = { + info: safeLoadEvent.bind(this, 'info'), + warn: safeLoadEvent.bind(this, 'warn'), + error: safeLoadEvent.bind(this, 'error'), + } } else { logger.event = { - info: function() {}, - warn: function() {}, - error: function() {}, + info: function() { return Promise.resolve() }, + warn: function() { return Promise.resolve() }, + error: function() { return Promise.resolve() }, } } logger.ringbuffer = ringbuffer diff --git a/core/util.mjs b/core/util.mjs index c427286..8820a0c 100644 --- a/core/util.mjs +++ b/core/util.mjs @@ -26,15 +26,58 @@ export default class Util { } getAppNames(config) { + const validLevels = [ + 'fatal', + 'error', + 'warn', + 'info', + 'debug', + 'trace', + ] let out = [] let keys = Object.keys(config) for (let key of keys) { - if (typeof(config[key]) !== 'object' || config[key] == null) continue - if (typeof(config[key].port) !== 'number' || !config[key].port) continue - if (typeof(config[key].provider) !== 'string' || !config[key].provider) continue - if (config[key].https != null && typeof(config[key].https) !== 'boolean') continue - if (config[key].updateEvery != null && (typeof(config[key].updateEvery) !== 'number' || config[key].updateEvery < 1)) continue - if (config[key].waitUntilFail != null && (typeof(config[key].waitUntilFail) !== 'number' || config[key].waitUntilFail < 10)) continue + if (typeof(config[key]) !== 'object' || config[key] == null) + continue + if (typeof(config[key].port) !== 'number' || !config[key].port) + continue + if (typeof(config[key].provider) !== 'string' || !config[key].provider) + continue + if (config[key].https != null && typeof(config[key].https) !== 'boolean') + continue + if (config[key].updateEvery != null && (typeof(config[key].updateEvery) !== 'number' || config[key].updateEvery < 0)) + continue + if (config[key].startWaitUntilFail != null && (typeof(config[key].startWaitUntilFail) !== 'number' || config[key].startWaitUntilFail < 10)) + continue + if (config[key].heartbeatTimeout != null && (typeof(config[key].heartbeatTimeout) !== 'number' || config[key].heartbeatTimeout < 10)) + continue + if (config[key].heartbeatAttempts != null && (typeof(config[key].heartbeatAttempts) !== 'number' || config[key].heartbeatAttempts < 1)) + continue + if (config[key].heartbeatAttemptsWait != null && (typeof(config[key].heartbeatAttemptsWait) !== 'number' || config[key].heartbeatAttemptsWait < 10)) + continue + if (config[key].heartbeatPath != null && (typeof(config[key].heartbeatPath) !== 'string' || config[key].heartbeatPath[0] !== '/')) + continue + if (config[key].log != null) { + if (!Array.isArray(config[key].log)) + continue + let valid = true + for (let stream of config[key].log) { + if (!stream || typeof(stream) !== 'object' || Array.isArray(stream)) { + valid = false + break + } + if (typeof(stream.level) !== 'string' || !stream.level || !validLevels.includes(stream.level)) { + valid = false + break + } + if ((typeof(stream.path) !== 'string' || !stream.path) && stream.stream !== 'process.stdout') { + valid = false + break + } + } + if (!valid) + continue + } out.push(key) } diff --git a/package.json b/package.json index f7e84ca..912e3d5 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "scripts": { "dev": "nodemon --watch dev/api --watch core --watch runner.mjs --watch db.mjs --watch log.mjs runner.mjs | bunyan", "test": "eltro \"test/**/*.test.mjs\" -r dot", + "test:spec": "eltro \"test/**/*.test.mjs\" -r list", "test:watch": "npm-watch test" }, "watch": { @@ -13,7 +14,10 @@ "patterns": [ "{core,test}/*" ], - "ignore": "test/testapp", + "ignore": [ + "test/testapp", + "test/testnoexisting" + ], "extensions": "js,mjs", "quiet": true, "inherit": true diff --git a/test/application.integration.test.mjs b/test/application.integration.test.mjs index 3000798..f9b7c44 100644 --- a/test/application.integration.test.mjs +++ b/test/application.integration.test.mjs @@ -1,38 +1,38 @@ import { Eltro as t, assert, stub } from 'eltro' import fs from 'fs/promises' -import lowdb from '../core/db.mjs' import Application from '../core/application.mjs' import GitProvider from '../core/providers/git.mjs' import Util from '../core/util.mjs' +import { createFakeContext } from './helpers.mjs' const util = new Util(import.meta.url) -const logger = { - info: stub(), - warn: stub(), - error: stub(), -} + t.skip().timeout(10000).describe('Application update integration test', function() { - let db + let ctx let app let provider t.before(function() { - return lowdb({ test: { } }, logger, util.getPathFromRoot('./db_test.json')).then(function(res) { - db = res - provider = new GitProvider({ url: 'https://git.nfp.is/api/v1/repos/thething/sc-helloworld/releases' }) - app = new Application(util, db, provider, 'testapp') - - return provider.getLatestVersion() - }).then(function(version) { - return fs.rm(`./test/testapp/${version.version}`, { force: true, recursive: true }) - }) + return createFakeContext({ }, util, util.getPathFromRoot('./db_test.json')) + .then(function(res) { + ctx = res + provider = new GitProvider({ url: 'https://git.nfp.is/api/v1/repos/thething/sc-helloworld/releases' }) + app = new Application(ctx, provider, 'testapp') + + return provider.getLatestVersion() + }).then(function(version) { + return fs.rm(`./test/testapp/${version.version}`, { force: true, recursive: true }) + }) }) t.after(function() { - if (db.data.core.testapp.versions.length) { - return fs.rm(`./test/testapp/${db.data.core.testapp.versions[0].id}`, { force: true, recursive: true }) - } + return fs.rm(util.getPathFromRoot('./db_test.json')) + .then(function() { + if (ctx.db.data.core.testapp.versions.length) { + return fs.rm(`./test/testapp/${ctx.db.data.core.testapp.versions[0].id}`, { force: true, recursive: true }) + } + }) }) t.test('should run update and install correctly', async function(){ @@ -40,15 +40,15 @@ t.skip().timeout(10000).describe('Application update integration test', function await app.update() } catch (err) { console.log(err) - if (db.data.core.testapp.versions.length) { - console.log(db.data.core.testapp.versions[0].log) + if (ctx.db.data.core.testapp.versions.length) { + console.log(ctx.db.data.core.testapp.versions[0].log) } throw err } - assert.ok(db.data.core.testapp.latestInstalled) - await fs.stat(util.getPathFromRoot(`./testapp/${db.data.core.testapp.latestInstalled}/index.mjs`)) - await fs.stat(util.getPathFromRoot(`./testapp/${db.data.core.testapp.latestInstalled}/package.json`)) - await fs.stat(util.getPathFromRoot(`./testapp/${db.data.core.testapp.latestInstalled}/node_modules`)) + assert.ok(ctx.db.data.core.testapp.latestInstalled) + await fs.stat(util.getPathFromRoot(`./testapp/${ctx.db.data.core.testapp.latestInstalled}/index.mjs`)) + await fs.stat(util.getPathFromRoot(`./testapp/${ctx.db.data.core.testapp.latestInstalled}/package.json`)) + await fs.stat(util.getPathFromRoot(`./testapp/${ctx.db.data.core.testapp.latestInstalled}/node_modules`)) }) }) \ No newline at end of file diff --git a/test/application.run.test.mjs b/test/application.run.test.mjs index 5c517cb..96e4233 100644 --- a/test/application.run.test.mjs +++ b/test/application.run.test.mjs @@ -1,58 +1,59 @@ import { Eltro as t, assert, stub } from 'eltro' import fs from 'fs/promises' -import lowdb from '../core/db.mjs' import Application from '../core/application.mjs' import Util from '../core/util.mjs' +import lowdb from '../core/db.mjs' import StaticProvider from '../core/providers/static.mjs' +import { createFakeContext } from './helpers.mjs' const util = new Util(import.meta.url) -const logger = { - info: stub(), - warn: stub(), - error: stub(), -} -function createProvider() { - return { - getLatestVersion: stub(), - downloadVersion: stub(), - } -} - -t.timeout(250).describe('#runVersion()', function() { +t.describe('#runVersion("static")', function() { const assertPort = 22345 - let db + let ctx let app - const defaultHandler = function(db, log, http, port) { - const server = http.createServer(function (req, res) { + const defaultHandler = function(orgHandler) { + let handler = orgHandler || function (req, res) { res.writeHead(204); res.end(JSON.stringify({ a: 1 })) - }) + } + return function(http, port, ctx) { + const server = http.createServer(handler) - return new Promise(function(res, rej) { - server.listen(port, '0.0.0.0', function(err) { - if (err) return rej(err) - res() - }) - }) + return server.listenAsync(port) + } } + + t.before(function() { + return fs.mkdir(util.getPathFromRoot('./testnoexisting'), { recursive: true }) + }) + + t.after(function() { + return fs.rm(util.getPathFromRoot('./testnoexisting'), { force: true, recursive: true }) + }) t.beforeEach(function() { - return lowdb({ test: { } }, logger, null).then(function(res) { - db = res - let provider = new StaticProvider() - app = new Application(util, db, provider, 'testapp') - app.config.port = assertPort - app.registerModule(defaultHandler) - }) + return createFakeContext() + .then(function(res) { + ctx = res + let provider = new StaticProvider() + app = new Application(ctx, provider, 'testapp') + app.config.port = assertPort + app.registerModule(defaultHandler()) + }) + }) + + t.afterEach(function() { + return app.http.closeServer() }) t.test('should throw if http is not called', async function() { - app.registerModule(function(checkDb, checkLog, checkHttp, checkPort) { - assert.strictEqual(checkDb, db) - assert.strictEqual(checkLog, db.log) + app.registerModule(function(checkHttp, checkPort, checkCtx) { assert.strictEqual(checkHttp, app.http) assert.strictEqual(checkPort, assertPort) + assert.strictEqual(checkCtx.db, ctx.db) + assert.strictEqual(checkCtx.log, ctx.log) + assert.strictEqual(checkCtx.app, app) }) let err = await assert.isRejected(app.runVersion('static')) @@ -62,10 +63,11 @@ t.timeout(250).describe('#runVersion()', function() { assert.match(err.message, /static/) assert.match(err.message, new RegExp(app.name)) assert.match(err.message, /call/i) + assert.strictEqual(ctx.db.data.core.testapp.active, 'static') }) t.test('should throw if it timeouts waiting for promise to succeed', async function() { - app.config.waitUntilFail = 50 + app.config.startWaitUntilFail = 50 app.registerModule(function() { return new Promise(function() {}) }) let err = await assert.isRejected(app.runVersion('static')) @@ -75,18 +77,203 @@ t.timeout(250).describe('#runVersion()', function() { assert.match(err.message, /static/) assert.match(err.message, /50ms/) assert.match(err.message, new RegExp(app.name)) + assert.strictEqual(ctx.db.data.core.testapp.active, 'static') }) t.test('should otherwise succeed if it finished within the time limit', async function() { - app.config.waitUntilFail = 250 - app.registerModule(function(db, log, http, port) { + const handler = defaultHandler() + app.config.startWaitUntilFail = 250 + app.registerModule(function(http, port, ctx) { return new Promise(function(res) { setTimeout(res, 25) }).then(function() { - return defaultHandler(db, log, http, port) + return handler(http, port, ctx) }) }) await app.runVersion('static') + + assert.strictEqual(ctx.db.data.core.testapp.active, 'static') + }) + + t.test('should fail if run succeeds but heartbeat errors', async function() { + let called = 0 + const handler = function(req, res) { + called++ + res.statusCode = 400 + res.end(JSON.stringify({ a: 1 })) + } + app.config.heartbeatAttempts = 3 + app.registerModule(defaultHandler(handler)) + + let err = await assert.isRejected(app.runVersion('static')) + assert.match(err.message, /app/i) + assert.match(err.message, /failed/i) + assert.match(err.message, /static/i) + assert.match(err.message, /testapp/i) + assert.match(err.message, /400/i) + assert.strictEqual(called, 3) + assert.strictEqual(ctx.db.data.core.testapp.active, 'static') + }) + + t.test('should fail if run succeeds but heartbeat times out', async function() { + let called = 0 + const handler = function(req, res) { + called++ + } + app.config.heartbeatAttempts = 2 + app.config.heartbeatAttemptsWait = 30 + app.registerModule(defaultHandler(handler)) + + let start = performance.now() + let err = await assert.isRejected(app.runVersion('static')) + let end = performance.now() + assert.match(err.message, /app/i) + assert.match(err.message, /failed/i) + assert.match(err.message, /static/i) + assert.match(err.message, /testapp/i) + assert.match(err.message, /time/i) + assert.match(err.message, /out/i) + assert.match(err.message, /30ms/i) + assert.ok(end - start > app.config.heartbeatAttempts * app.config.heartbeatAttemptsWait) + assert.strictEqual(called, 2) + assert.strictEqual(ctx.db.data.core.testapp.active, 'static') + }) + + t.test('should call with correct path', async function() { + const assertPath = '/test/something' + const handler = function(req, res) { + if (req.url === assertPath) { + res.writeHead(204); res.end(JSON.stringify({ a: 1 })) + } else { + res.statusCode = 400 + res.end(JSON.stringify({ a: 1 })) + } + } + app.config.heartbeatAttempts = 3 + app.registerModule(defaultHandler(handler)) + + let err = await assert.isRejected(app.runVersion('static')) + assert.match(err.message, /app/i) + assert.match(err.message, /failed/i) + assert.match(err.message, /static/i) + assert.match(err.message, /testapp/i) + assert.match(err.message, /400/i) + + await app.http.closeServer() + app.registerModule(defaultHandler(handler)) + + app.config.heartbeatPath = assertPath + await app.runVersion('static') + + assert.strictEqual(ctx.db.data.core.testapp.active, 'static') + }) +}) + +t.skip().describe('#runVersion("version")', function() { + const assertConfig = util.getPathFromRoot('./db_test_applicationrun.json') + const assertPort = 22345 + let ctx + let app + + t.before(function() { + return fs.rm(util.getPathFromRoot('./testnoexisting'), { force: true, recursive: true }) + .then(function() { + return fs.mkdir(util.getPathFromRoot('./testnoexisting'), { recursive: true }) + }) + }) + + t.after(function() { + return fs.rm(util.getPathFromRoot('./testnoexisting'), { force: true, recursive: true }) + }) + + t.beforeEach(function() { + return createFakeContext({ }, util, assertConfig) + .then(function(res) { + ctx = res + let provider = new StaticProvider() + app = new Application(ctx, provider, 'testnoexisting') + app.config.port = assertPort + return app.ctx.db.write() + }) + }) + + t.afterEach(function() { + return Promise.all([ + fs.rm(assertConfig), + app.http.closeServer(), + ]) + }) + + t.test('when version is specified, should check if index.mjs exists', async function() { + const assertNotError = new Error('AI DO') + const assertTarget = util.getPathFromRoot('./testnoexisting/v100/index.mjs') + let stubFsStat = stub() + let provider = new StaticProvider() + app = new Application(ctx, provider, 'testnoexisting', { + fs: { stat: stubFsStat } + }) + app.config.port = assertPort + stubFsStat.rejects(assertNotError) + + let err = await assert.isRejected(app.runVersion('v100')) + assert.notStrictEqual(err, assertNotError) + assert.match(err.message, new RegExp(assertNotError.message)) + assert.match(err.message, /index\.mjs/i) + assert.match(err.message, /testnoexisting/i) + assert.match(err.message, /v100/i) + assert.match(err.message, /missing/i) + assert.strictEqual(stubFsStat.firstCall[0], assertTarget) + + assert.strictEqual(app.ctx.db.data.core.testnoexisting.active, 'v100') + let checkDb = await lowdb({}, ctx.log, assertConfig) + assert.strictEqual(checkDb.data.core.testnoexisting.active, 'v100') + }) + + t.test('when version is specified and file exists, should attempt to load module', async function() { + const assertError = new Error('Parallel Days') + await fs.mkdir(util.getPathFromRoot('./testnoexisting/v99'), { recursive: true }) + await fs.writeFile(util.getPathFromRoot('./testnoexisting/v99/index.mjs'), `throw new Error('${assertError.message}')`) + + let err = await assert.isRejected(app.runVersion('v99')) + assert.notStrictEqual(err, assertError) + assert.match(err.message, new RegExp(assertError.message)) + assert.match(err.message, /testnoexisting/i) + assert.match(err.message, /v99/i) + assert.match(err.message, /index\.mjs/i) + + assert.strictEqual(app.ctx.db.data.core.testnoexisting.active, 'v99') + let checkDb = await lowdb({}, ctx.log, assertConfig) + assert.strictEqual(checkDb.data.core.testnoexisting.active, 'v99') + }) + + t.test('when version is specified and file exists, should check if it has start', async function() { + await fs.mkdir(util.getPathFromRoot('./testnoexisting/v98'), { recursive: true }) + await fs.writeFile(util.getPathFromRoot('./testnoexisting/v98/index.mjs'), ``) + + let err = await assert.isRejected(app.runVersion('v98')) + assert.match(err.message, /testnoexisting/i) + assert.match(err.message, /v98/i) + assert.match(err.message, /start/i) + + assert.strictEqual(app.ctx.db.data.core.testnoexisting.active, 'v98') + let checkDb = await lowdb({}, ctx.log, assertConfig) + assert.strictEqual(checkDb.data.core.testnoexisting.active, 'v98') + }) + + t.test('when version is specified and file exists and everything is okay, should work normally', async function() { + await fs.mkdir(util.getPathFromRoot('./testnoexisting/v97'), { recursive: true }) + await fs.copyFile( + util.getPathFromRoot('./exampleindex.mjs'), + util.getPathFromRoot('./testnoexisting/v97/index.mjs') + ) + + app.ctx.log.info.reset() + app.ctx.log.event.info.reset() + + await app.runVersion('v97') + + assert.ok(app.ctx.log.info.called) + assert.ok(app.ctx.log.event.info.called) }) }) diff --git a/test/application.test.mjs b/test/application.test.mjs index 29bb2d1..c837f70 100644 --- a/test/application.test.mjs +++ b/test/application.test.mjs @@ -1,10 +1,10 @@ import { setTimeout, setImmediate } from 'timers/promises' import { Eltro as t, assert, stub } from 'eltro' import fs from 'fs/promises' -import lowdb from '../core/db.mjs' import Application from '../core/application.mjs' import Util from '../core/util.mjs' import StaticProvider from '../core/providers/static.mjs' +import { createFakeContext } from './helpers.mjs' const util = new Util(import.meta.url) @@ -21,41 +21,46 @@ function createProvider() { } t.describe('constructor()', function() { - let db + let ctx t.beforeEach(function() { - return lowdb({ test: { } }, logger, null).then(function(res) { - db = res - }) + return createFakeContext() + .then(function(res) { ctx = res }) }) t.test('should auto-create application', function() { - assert.notOk(db.data.core.test) + assert.notOk(ctx.db.data.core.test) - new Application(util, db, {}, 'test') + new Application(ctx, {}, 'test') - assert.ok(db.data.core.test) - assert.ok(db.data.core.test.versions) - assert.strictEqual(db.data.core.test.active, null) - assert.strictEqual(db.data.core.test.latestInstalled, null) - assert.strictEqual(db.data.core.test.latestVersion, null) + assert.ok(ctx.db.data.core.test) + assert.ok(ctx.db.data.core.test.versions) + assert.strictEqual(ctx.db.data.core.test.active, '') + assert.strictEqual(ctx.db.data.core.test.latestInstalled, '') + assert.strictEqual(ctx.db.data.core.test.latestVersion, '') }) t.test('should keep config and other of itself', function() { const assertTest = { a: 1 } const assertName = 'test' - db.config = { + ctx.db.config = { test: assertTest, app: { b: 2}, manage: { c: 3 }, } - let app = new Application(util, db, {}, assertName) - assert.strictEqual(app.config, assertTest) + let app = new Application(ctx, {}, assertName) + assert.notStrictEqual(app.config, assertTest) + assert.strictEqual(app.config.a, assertTest.a) assert.strictEqual(app.config.updateEvery, 180) - assert.strictEqual(app.config.waitUntilFail, 60 * 1000) - assert.strictEqual(app.db, db) - assert.strictEqual(app.util, util) + assert.strictEqual(app.config.startWaitUntilFail, 60 * 1000) + assert.strictEqual(app.config.heartbeatTimeout, 3 * 1000) + assert.strictEqual(app.config.heartbeatAttempts, 5) + assert.strictEqual(app.config.heartbeatAttemptsWait, 2 * 1000) + assert.strictEqual(app.config.heartbeatPath, '/') + assert.strictEqual(app.ctx.db, ctx.db) + assert.strictEqual(app.ctx.app, app) + assert.strictEqual(app.ctx.util, ctx.util) assert.strictEqual(app.name, assertName) assert.strictEqual(app.fresh, true) assert.strictEqual(app.running, false) @@ -66,38 +71,37 @@ t.describe('constructor()', function() { }) t.test('should create http instance correctly', function() { - db.config = { + ctx.db.config = { testapp: { a: 1, https: true }, app: { b: 2}, manage: { c: 3 }, } - let app = new Application(util, db, {}, 'testapp') + let app = new Application(ctx, {}, 'testapp') assert.ok(app.http) assert.ok(app.http.ishttps) }) t.test('should keep provider', function() { const assertProvider = { a: 1 } - let app = new Application(util, db, assertProvider, 'test') + let app = new Application(ctx, assertProvider, 'test') assert.strictEqual(app.provider, assertProvider) }) }) t.timeout(250).describe('#startAutoupdater()', function() { - let db + let ctx t.beforeEach(function() { - return lowdb({ test: { }, testapp: { } }, logger, null).then(function(res) { - db = res - }) + return createFakeContext() + .then(function(res) { ctx = res }) }) t.test('should do nothing if provider is static', async function() { const stubInterval = stub() stubInterval.throws(new Error('should not be seen')) let provider = new StaticProvider() - let app = new Application(util, db, provider, 'teststatic', { setInterval: stubInterval }) + let app = new Application(ctx, provider, 'teststatic', { setInterval: stubInterval }) app.startAutoupdater() }) @@ -108,9 +112,11 @@ t.timeout(250).describe('#startAutoupdater()', function() { const stubUnref = stub() stubInterval.returns({ unref: stubUnref }) - db.config.test.updateEvery = assertTimeMinutes + ctx.db.config.test = { + updateEvery: assertTimeMinutes, + } - let app = new Application(util, db, {}, 'test', { setInterval: stubInterval }) + let app = new Application(ctx, {}, 'test', { setInterval: stubInterval }) assert.strictEqual(stubInterval.called, false) assert.strictEqual(stubUnref.called, false) @@ -127,7 +133,7 @@ t.timeout(250).describe('#startAutoupdater()', function() { const stubInterval = stub() stubInterval.returns({ unref: function() {} }) - let app = new Application(util, db, {}, 'test', { setInterval: stubInterval }) + let app = new Application(ctx, {}, 'test', { setInterval: stubInterval }) assert.strictEqual(stubInterval.called, false) @@ -147,7 +153,7 @@ t.timeout(250).describe('#startAutoupdater()', function() { return Promise.resolve() }) - let app = new Application(util, db, {}, 'test', { setInterval: stubInterval }) + let app = new Application(ctx, {}, 'test', { setInterval: stubInterval }) app.update = stubUpdate app.startAutoupdater() @@ -156,12 +162,12 @@ t.timeout(250).describe('#startAutoupdater()', function() { stubInterval.firstCall[0]() - while (db.data.core.test.updater === '') { + while (ctx.db.data.core.test.updater === '') { await setTimeout(10) } - assert.match(db.data.core.test.updater, /auto/i) - assert.match(db.data.core.test.updater, /update/i) + assert.match(ctx.db.data.core.test.updater, /auto/i) + assert.match(ctx.db.data.core.test.updater, /update/i) }) t.test('should add any errors to last in db update check on errors when updating', async function() { @@ -169,28 +175,55 @@ t.timeout(250).describe('#startAutoupdater()', function() { const assertErrorMessage = 'Ai Do' stubInterval.returns({ unref: function() {} }) - let app = new Application(util, db, {}, 'test', { setInterval: stubInterval }) + let app = new Application(ctx, {}, 'test', { setInterval: stubInterval }) app.update = function() { return Promise.reject(new Error(assertErrorMessage)) } app.startAutoupdater() - assert.strictEqual(db.data.core.test.updater, '') + assert.strictEqual(ctx.db.data.core.test.updater, '') stubInterval.firstCall[0]() - while (db.data.core.test.updater === '') { + while (ctx.db.data.core.test.updater === '') { await setTimeout(10) } - assert.match(db.data.core.test.updater, /auto/i) - assert.match(db.data.core.test.updater, /update/i) - assert.match(db.data.core.test.updater, new RegExp(assertErrorMessage)) + assert.match(ctx.db.data.core.test.updater, /auto/i) + assert.match(ctx.db.data.core.test.updater, /update/i) + assert.match(ctx.db.data.core.test.updater, new RegExp(assertErrorMessage)) + }) +}) + +t.timeout(250).describe('#closeServer()', function() { + let app + let stubCloseServer + + t.beforeEach(function() { + return createFakeContext() + .then(function(res) { + let provider = createProvider() + app = new Application(res, provider, 'testapp') + app.http.closeServer = stubCloseServer = stub().resolves() + }) + }) + + t.test('should call closeServer correctly', async function() { + const assertError = new Error('Moonlight Fiesta') + stubCloseServer.rejects(assertError) + + let err = await assert.isRejected(app.closeServer()) + + assert.strictEqual(err, assertError) + }) + + t.test('otherwise should work fine', async function() { + await app.closeServer() }) }) t.timeout(250).describe('#update()', function() { - let db + let ctx let app let provider let stubExtract @@ -201,30 +234,27 @@ t.timeout(250).describe('#update()', function() { let stubFsStat t.beforeEach(function() { - util.extractFile = stubExtract = stub() - util.runCommand = stubRunCommand = stub() + return createFakeContext() + .then(function(res) { + ctx = res + ctx.util.extractFile = stubExtract = stub().resolves() + ctx.util.runCommand = stubRunCommand = stub().resolves() + ctx.db.write = stubWrite = stub().resolves() - stubExtract.resolves() - stubRunCommand.resolves() - - return lowdb({ test: { } }, logger, null).then(function(res) { - db = res - db.write = stubWrite = stub() - stubWrite.resolves() - provider = createProvider() - app = new Application(util, db, provider, 'testapp', { - fs: { - mkdir: stubFsMkdir = stub(), - rm: stubFsRm = stub(), - stat: stubFsStat = stub(), - }, + provider = createProvider() + app = new Application(ctx, provider, 'testapp', { + fs: { + mkdir: stubFsMkdir = stub(), + rm: stubFsRm = stub(), + stat: stubFsStat = stub(), + }, + }) + stubFsMkdir.resolves() + stubFsRm.resolves() + stubFsStat.resolves({}) + provider.downloadVersion.resolves() + provider.getLatestVersion.resolves({ version: '123456789', link: 'httplinkhere', filename: 'test.7z' }) }) - stubFsMkdir.resolves() - stubFsRm.resolves() - stubFsStat.resolves({}) - provider.downloadVersion.resolves() - provider.getLatestVersion.resolves({ version: '123456789', link: 'httplinkhere', filename: 'test.7z' }) - }) }) t.afterEach(function() { @@ -233,33 +263,35 @@ t.timeout(250).describe('#update()', function() { t.test('should do nothing if provider is static', async function() { provider = new StaticProvider() - app = new Application(util, db, provider, 'teststatic') + app = new Application(ctx, provider, 'teststatic') + + stubWrite.reset() await app.update() - assert.match(db.data.core.teststatic.updater, /static/i) - assert.match(db.data.core.teststatic.updater, /nothing/i) - let old = db.data.core.teststatic.updater + assert.match(ctx.db.data.core.teststatic.updater, /static/i) + assert.match(ctx.db.data.core.teststatic.updater, /nothing/i) + let old = ctx.db.data.core.teststatic.updater assert.ok(stubWrite.called) assert.strictEqual(stubWrite.callCount, 1) await app.update() - assert.strictEqual(db.data.core.teststatic.updater, old) + assert.strictEqual(ctx.db.data.core.teststatic.updater, old) assert.strictEqual(stubWrite.callCount, 1) - db.data.core.teststatic.updater = 'asdf' + ctx.db.data.core.teststatic.updater = 'asdf' await app.update() - assert.strictEqual(db.data.core.teststatic.updater, old) + assert.strictEqual(ctx.db.data.core.teststatic.updater, old) assert.strictEqual(stubWrite.callCount, 2) await app.update() - assert.strictEqual(db.data.core.teststatic.updater, old) + assert.strictEqual(ctx.db.data.core.teststatic.updater, old) assert.strictEqual(stubWrite.callCount, 2) }) t.test('multiple calls should be safe', async function() { - db.data.core.testapp.updater = '' + ctx.db.data.core.testapp.updater = '' provider.getLatestVersion.returnWith(function() { return new Promise(function() {}) @@ -282,7 +314,7 @@ t.timeout(250).describe('#update()', function() { t.test('should check for latest version', async function() { const assertError = new Error('Ore wa Subete wo Shihaisuru') provider.getLatestVersion.rejects(assertError) - db.data.core.testapp.updater = '' + ctx.db.data.core.testapp.updater = '' let err = await assert.isRejected(app.update()) @@ -291,10 +323,10 @@ t.timeout(250).describe('#update()', function() { assert.ok(stubWrite.called) assert.ok(stubWrite.callCount >= 1) - assert.match(db.data.core.testapp.updater, /check/i) - assert.match(db.data.core.testapp.updater, /version/i) - assert.match(db.data.core.testapp.updater, new RegExp(new Date().toISOString().split('T')[0])) - assert.match(db.data.core.testapp.updater, new RegExp(assertError.message)) + assert.match(ctx.db.data.core.testapp.updater, /check/i) + assert.match(ctx.db.data.core.testapp.updater, /version/i) + assert.match(ctx.db.data.core.testapp.updater, new RegExp(new Date().toISOString().split('T')[0])) + assert.match(ctx.db.data.core.testapp.updater, new RegExp(assertError.message)) }) t.test('should call provider download latest correctly if new 7zip version', async function() { @@ -302,27 +334,27 @@ t.timeout(250).describe('#update()', function() { const assertLink = 'All of you' const assertVersion = { version: '123456789', link: assertLink, filename: 'test.7z' } const assertTarget = util.getPathFromRoot('./testapp/123456789/file.7z') - assert.strictEqual(db.data.core.testapp.versions.length, 0) + assert.strictEqual(ctx.db.data.core.testapp.versions.length, 0) provider.getLatestVersion.resolves(assertVersion) provider.downloadVersion.rejects(assertError) - db.data.core.testapp.updater = '' + ctx.db.data.core.testapp.updater = '' let err = await assert.isRejected(app.update()) assert.strictEqual(app.updating, false) assert.strictEqual(err, assertError) - assert.match(db.data.core.testapp.updater, /found/i) - assert.match(db.data.core.testapp.updater, new RegExp(assertVersion.version)) - assert.match(db.data.core.testapp.updater, /downloading/i) - assert.match(db.data.core.testapp.updater, new RegExp(assertLink)) - assert.match(db.data.core.testapp.updater, new RegExp(assertTarget.replace(/\\/g, '\\\\'))) + assert.match(ctx.db.data.core.testapp.updater, /found/i) + assert.match(ctx.db.data.core.testapp.updater, new RegExp(assertVersion.version)) + assert.match(ctx.db.data.core.testapp.updater, /downloading/i) + assert.match(ctx.db.data.core.testapp.updater, new RegExp(assertLink)) + assert.match(ctx.db.data.core.testapp.updater, new RegExp(assertTarget.replace(/\\/g, '\\\\'))) assert.strictEqual(provider.downloadVersion.firstCall[0], assertVersion) assert.strictEqual(provider.downloadVersion.firstCall[1], assertTarget) - assert.match(db.data.core.testapp.updater, new RegExp(assertError.message)) + assert.match(ctx.db.data.core.testapp.updater, new RegExp(assertError.message)) - assert.strictEqual(db.data.core.testapp.versions.length, 1) - assert.strictEqual(db.data.core.testapp.versions[0], assertVersion) + assert.strictEqual(ctx.db.data.core.testapp.versions.length, 1) + assert.strictEqual(ctx.db.data.core.testapp.versions[0], assertVersion) assert.ok(assertVersion.log) assert.match(assertVersion.log, /\. \n/) assert.match(assertVersion.log, new RegExp(assertError.message)) @@ -341,8 +373,8 @@ t.timeout(250).describe('#update()', function() { assert.strictEqual(app.updating, false) assert.strictEqual(err, assertError) - assert.strictEqual(db.data.core.testapp.versions.length, 1) - assert.strictEqual(db.data.core.testapp.versions[0], assertVersion) + assert.strictEqual(ctx.db.data.core.testapp.versions.length, 1) + assert.strictEqual(ctx.db.data.core.testapp.versions[0], assertVersion) assert.strictEqual(assertVersion.failtodownload, 2) }) @@ -351,27 +383,27 @@ t.timeout(250).describe('#update()', function() { const assertLink = 'All of you' const assertVersion = { version: '123456789', link: assertLink, filename: 'test.7z' } const assertTarget = util.getPathFromRoot('./testapp/123456789/file.7z') - assert.strictEqual(db.data.core.testapp.versions.length, 0) + assert.strictEqual(ctx.db.data.core.testapp.versions.length, 0) provider.getLatestVersion.resolves(assertVersion) provider.downloadVersion.rejects(assertError) - db.data.core.testapp.updater = '' + ctx.db.data.core.testapp.updater = '' let err = await assert.isRejected(app.update()) assert.strictEqual(app.updating, false) assert.strictEqual(err, assertError) - assert.match(db.data.core.testapp.updater, /found/i) - assert.match(db.data.core.testapp.updater, new RegExp(assertVersion.version)) - assert.match(db.data.core.testapp.updater, /downloading/i) - assert.match(db.data.core.testapp.updater, new RegExp(assertLink)) - assert.match(db.data.core.testapp.updater, new RegExp(assertTarget.replace(/\\/g, '\\\\'))) + assert.match(ctx.db.data.core.testapp.updater, /found/i) + assert.match(ctx.db.data.core.testapp.updater, new RegExp(assertVersion.version)) + assert.match(ctx.db.data.core.testapp.updater, /downloading/i) + assert.match(ctx.db.data.core.testapp.updater, new RegExp(assertLink)) + assert.match(ctx.db.data.core.testapp.updater, new RegExp(assertTarget.replace(/\\/g, '\\\\'))) assert.strictEqual(provider.downloadVersion.firstCall[0], assertVersion) assert.strictEqual(provider.downloadVersion.firstCall[1], assertTarget) - assert.match(db.data.core.testapp.updater, new RegExp(assertError.message)) + assert.match(ctx.db.data.core.testapp.updater, new RegExp(assertError.message)) - assert.strictEqual(db.data.core.testapp.versions.length, 1) - assert.strictEqual(db.data.core.testapp.versions[0], assertVersion) + assert.strictEqual(ctx.db.data.core.testapp.versions.length, 1) + assert.strictEqual(ctx.db.data.core.testapp.versions[0], assertVersion) assert.ok(assertVersion.log) assert.match(assertVersion.log, /\. \n/) assert.match(assertVersion.log, new RegExp(assertError.message)) @@ -389,8 +421,8 @@ t.timeout(250).describe('#update()', function() { assert.strictEqual(app.updating, false) assert.strictEqual(err, assertError) - assert.strictEqual(db.data.core.testapp.versions.length, 1) - assert.strictEqual(db.data.core.testapp.versions[0], assertVersion) + assert.strictEqual(ctx.db.data.core.testapp.versions.length, 1) + assert.strictEqual(ctx.db.data.core.testapp.versions[0], assertVersion) assert.strictEqual(assertVersion.failtodownload, 2) }) @@ -404,7 +436,7 @@ t.timeout(250).describe('#update()', function() { stream(assertExtractText) return Promise.reject(assertError) }) - assert.strictEqual(db.data.core.testapp.versions.length, 0) + assert.strictEqual(ctx.db.data.core.testapp.versions.length, 0) let err = await assert.isRejected(app.update()) @@ -413,8 +445,8 @@ t.timeout(250).describe('#update()', function() { assert.strictEqual(app.updating, false) assert.strictEqual(stubExtract.firstCall[0], assertTarget) - assert.match(db.data.core.testapp.updater, new RegExp('extracting[^.]+file\.7z', 'i')) - assert.match(db.data.core.testapp.updater, new RegExp(assertError.message)) + assert.match(ctx.db.data.core.testapp.updater, new RegExp('extracting[^.]+file\.7z', 'i')) + assert.match(ctx.db.data.core.testapp.updater, new RegExp(assertError.message)) assert.ok(stubFsRm.called) assert.strictEqual(stubFsRm.firstCall[0], assertFolder) @@ -423,8 +455,8 @@ t.timeout(250).describe('#update()', function() { assert.ok(stubWrite.called) assert.ok(stubWrite.callCount >= 3) - assert.strictEqual(db.data.core.testapp.versions.length, 1) - let version = db.data.core.testapp.versions[0] + assert.strictEqual(ctx.db.data.core.testapp.versions.length, 1) + let version = ctx.db.data.core.testapp.versions[0] assert.ok(version.log) assert.match(version.log, /\. \n/) assert.match(version.log, new RegExp('extracting[^.]+file\.7z', 'i')) @@ -439,15 +471,15 @@ t.timeout(250).describe('#update()', function() { assert.strictEqual(app.updating, false) assert.strictEqual(err, assertError) - assert.strictEqual(db.data.core.testapp.versions.length, 1) - assert.strictEqual(db.data.core.testapp.versions[0], version) + assert.strictEqual(ctx.db.data.core.testapp.versions.length, 1) + assert.strictEqual(ctx.db.data.core.testapp.versions[0], version) assert.strictEqual(version.failtodownload, 2) }) t.test('should call fs remove the archieve file', async function() { const assertError = new Error('Tiny Kizuna') const assertTarget = util.getPathFromRoot('./testapp/123456789/file.7z') - assert.strictEqual(db.data.core.testapp.versions.length, 0) + assert.strictEqual(ctx.db.data.core.testapp.versions.length, 0) stubFsRm.rejects(assertError) await app.update() @@ -464,7 +496,7 @@ t.timeout(250).describe('#update()', function() { const assertTarget = util.getPathFromRoot('./testapp/123456789/index.mjs') stubRunCommand.rejects(new Error('should not be seen')) stubFsStat.rejects(assertError) - assert.strictEqual(db.data.core.testapp.versions.length, 0) + assert.strictEqual(ctx.db.data.core.testapp.versions.length, 0) let err = await assert.isRejected(app.update()) @@ -472,11 +504,11 @@ t.timeout(250).describe('#update()', function() { assert.ok(stubExtract.called) assert.strictEqual(err, assertError) assert.strictEqual(stubFsStat.firstCall[0], assertTarget) - assert.match(db.data.core.testapp.updater, /index\.mjs/i) - assert.match(db.data.core.testapp.updater, /missing/i) + assert.match(ctx.db.data.core.testapp.updater, /index\.mjs/i) + assert.match(ctx.db.data.core.testapp.updater, /missing/i) - assert.strictEqual(db.data.core.testapp.versions.length, 1) - let version = db.data.core.testapp.versions[0] + assert.strictEqual(ctx.db.data.core.testapp.versions.length, 1) + let version = ctx.db.data.core.testapp.versions[0] assert.ok(version.log) assert.match(version.log, /index\.mjs/i) assert.match(version.log, /missing/i) @@ -489,8 +521,8 @@ t.timeout(250).describe('#update()', function() { assert.strictEqual(app.updating, false) assert.strictEqual(err, assertError) - assert.strictEqual(db.data.core.testapp.versions.length, 1) - assert.strictEqual(db.data.core.testapp.versions[0], version) + assert.strictEqual(ctx.db.data.core.testapp.versions.length, 1) + assert.strictEqual(ctx.db.data.core.testapp.versions[0], version) assert.strictEqual(version.failtodownload, 2) }) @@ -504,7 +536,7 @@ t.timeout(250).describe('#update()', function() { } return Promise.resolve({}) }) - assert.strictEqual(db.data.core.testapp.versions.length, 0) + assert.strictEqual(ctx.db.data.core.testapp.versions.length, 0) await app.update() @@ -514,16 +546,16 @@ t.timeout(250).describe('#update()', function() { assert.strictEqual(stubFsStat.secondCall[0], assertTarget) assert.ok(stubExtract.called) assert.notOk(stubRunCommand.called) - assert.match(db.data.core.testapp.updater, /package\.json/i) - assert.match(db.data.core.testapp.updater, /contain/i) + assert.match(ctx.db.data.core.testapp.updater, /package\.json/i) + assert.match(ctx.db.data.core.testapp.updater, /contain/i) - assert.strictEqual(db.data.core.testapp.versions.length, 1) - let version = db.data.core.testapp.versions[0] + assert.strictEqual(ctx.db.data.core.testapp.versions.length, 1) + let version = ctx.db.data.core.testapp.versions[0] assert.ok(version.log) assert.match(version.log, /package\.json/i) assert.match(version.log, /contain/i) assert.ok(version.log.endsWith('\n')) - assert.ok(db.data.core.testapp.latestInstalled) + assert.ok(ctx.db.data.core.testapp.latestInstalled) assert.match(version.log, /finished/i) assert.match(version.log, /updating/i) @@ -533,10 +565,10 @@ t.timeout(250).describe('#update()', function() { await app.update() - assert.strictEqual(db.data.core.testapp.versions.length, 1) + assert.strictEqual(ctx.db.data.core.testapp.versions.length, 1) assert.notOk(provider.downloadVersion.called) - assert.match(db.data.core.testapp.updater, /already/i) - assert.match(db.data.core.testapp.updater, /nothing/i) + assert.match(ctx.db.data.core.testapp.updater, /already/i) + assert.match(ctx.db.data.core.testapp.updater, /nothing/i) }) t.test('should otherwise call npm install correctly', async function() { @@ -547,7 +579,7 @@ t.timeout(250).describe('#update()', function() { const assertTarget = util.getPathFromRoot('./testapp/123456789') const assertTargetCheck = util.getPathFromRoot('./testapp/123456789/') provider.getLatestVersion.resolves(assertVersion) - assert.strictEqual(db.data.core.testapp.versions.length, 0) + assert.strictEqual(ctx.db.data.core.testapp.versions.length, 0) stubExtract.returnWith(function(target, stream) { stream(assertExtractText) @@ -578,10 +610,10 @@ t.timeout(250).describe('#update()', function() { assert.ok(stubWrite.called) assert.ok(stubWrite.callCount >= 4) - assert.match(db.data.core.testapp.updater, /npm/i) - assert.match(db.data.core.testapp.updater, /install/i) - assert.strictEqual(db.data.core.testapp.versions.length, 1) - assert.strictEqual(db.data.core.testapp.versions[0], assertVersion) + assert.match(ctx.db.data.core.testapp.updater, /npm/i) + assert.match(ctx.db.data.core.testapp.updater, /install/i) + assert.strictEqual(ctx.db.data.core.testapp.versions.length, 1) + assert.strictEqual(ctx.db.data.core.testapp.versions[0], assertVersion) assert.ok(assertVersion.log) assert.match(assertVersion.log, /\. \n/) assert.match(assertVersion.log, new RegExp(assertExtractText)) @@ -598,23 +630,23 @@ t.timeout(250).describe('#update()', function() { assert.strictEqual(app.updating, false) assert.strictEqual(err, assertError) - assert.strictEqual(db.data.core.testapp.versions.length, 1) - assert.strictEqual(db.data.core.testapp.versions[0], assertVersion) + assert.strictEqual(ctx.db.data.core.testapp.versions.length, 1) + assert.strictEqual(ctx.db.data.core.testapp.versions[0], assertVersion) assert.strictEqual(assertVersion.failtoinstall, 2) }) t.test('should update latest installed correctly', async function() { const assertVersion = { version: '123456789', link: 'httplinkhere', filename: 'test.7z' } provider.getLatestVersion.resolves(assertVersion) - assert.notStrictEqual(db.data.core.testapp.latestInstalled, assertVersion.version) - assert.strictEqual(db.data.core.testapp.versions.length, 0) + assert.notStrictEqual(ctx.db.data.core.testapp.latestInstalled, assertVersion.version) + assert.strictEqual(ctx.db.data.core.testapp.versions.length, 0) await app.update() assert.strictEqual(app.updating, false) - assert.strictEqual(db.data.core.testapp.latestInstalled, assertVersion.version) - assert.strictEqual(db.data.core.testapp.versions.length, 1) - assert.strictEqual(db.data.core.testapp.versions[0], assertVersion) + assert.strictEqual(ctx.db.data.core.testapp.latestInstalled, assertVersion.version) + assert.strictEqual(ctx.db.data.core.testapp.versions.length, 1) + assert.strictEqual(ctx.db.data.core.testapp.versions[0], assertVersion) assert.ok(assertVersion.log) assert.ok(stubWrite.callCount >= 4) assert.strictEqual(assertVersion.installed, true) @@ -635,18 +667,18 @@ t.timeout(250).describe('#update()', function() { const assertVersion = { version: '123456789', link: 'httplinkhere', filename: 'test.7z', log: oldLog, stable: assertStable } assertVersion.id = assertVersion.version - db.upsert(db.data.core.testapp.versions, assertVersion) + ctx.db.upsert(ctx.db.data.core.testapp.versions, assertVersion) provider.getLatestVersion.resolves({ version: assertVersion.version, link: assertNewLink, filename: assertNewFilename }) - assert.strictEqual(db.data.core.testapp.versions.length, 1) + assert.strictEqual(ctx.db.data.core.testapp.versions.length, 1) await app.update() assert.ok(assertVersion.log) assert.ok(stubWrite.callCount >= 4) assert.strictEqual(app.updating, false) - assert.strictEqual(db.data.core.testapp.versions.length, 1) - assert.strictEqual(db.data.core.testapp.versions[0], assertVersion) + assert.strictEqual(ctx.db.data.core.testapp.versions.length, 1) + assert.strictEqual(ctx.db.data.core.testapp.versions[0], assertVersion) assert.strictEqual(assertVersion.stable, assertStable) assert.ok(assertVersion.log) assert.ok(assertVersion.log.startsWith(oldLog)) @@ -664,16 +696,16 @@ t.timeout(250).describe('#update()', function() { const assertVersion = { version: '999.888.777.666', filename: 'test.7z' } provider.getLatestVersion.resolves(assertVersion) provider.downloadVersion.rejects(assertError) - db.data.core.testapp.updater = '' - db.data.core.testapp.latestInstalled = assertVersion.version + ctx.db.data.core.testapp.updater = '' + ctx.db.data.core.testapp.latestInstalled = assertVersion.version await app.update() assert.ok(stubWrite.callCount >= 1) assert.strictEqual(app.updating, false) assert.notOk(provider.downloadVersion.called) - assert.match(db.data.core.testapp.updater, /already/i) - assert.match(db.data.core.testapp.updater, /nothing/i) + assert.match(ctx.db.data.core.testapp.updater, /already/i) + assert.match(ctx.db.data.core.testapp.updater, /nothing/i) }) t.test('should do nothing if installed version is found', async function() { @@ -681,20 +713,20 @@ t.timeout(250).describe('#update()', function() { const assertVersion = { version: '999.888.777.666', filename: 'test.7z' } provider.getLatestVersion.resolves(assertVersion) provider.downloadVersion.rejects(assertError) - db.data.core.testapp.updater = '' + ctx.db.data.core.testapp.updater = '' - db.upsert(db.data.core.testapp.versions, { id: '111.111.111.111', installed: true }) - db.upsert(db.data.core.testapp.versions, { id: '222.222.222.222', installed: true }) - db.upsert(db.data.core.testapp.versions, { id: '999.888.777.666', installed: true }) - db.upsert(db.data.core.testapp.versions, { id: '333.333.333.333', installed: true }) + ctx.db.upsert(ctx.db.data.core.testapp.versions, { id: '111.111.111.111', installed: true }) + ctx.db.upsert(ctx.db.data.core.testapp.versions, { id: '222.222.222.222', installed: true }) + ctx.db.upsert(ctx.db.data.core.testapp.versions, { id: '999.888.777.666', installed: true }) + ctx.db.upsert(ctx.db.data.core.testapp.versions, { id: '333.333.333.333', installed: true }) await app.update() assert.ok(stubWrite.callCount >= 1) assert.strictEqual(app.updating, false) assert.notOk(provider.downloadVersion.called) - assert.match(db.data.core.testapp.updater, /already/i) - assert.match(db.data.core.testapp.updater, /nothing/i) + assert.match(ctx.db.data.core.testapp.updater, /already/i) + assert.match(ctx.db.data.core.testapp.updater, /nothing/i) }) t.test('should do nothing it exists and failtodownload is higher than 3', async function() { @@ -702,17 +734,17 @@ t.timeout(250).describe('#update()', function() { const assertVersion = { version: '999.888.777.666', filename: 'test.7z' } provider.getLatestVersion.resolves(assertVersion) provider.downloadVersion.rejects(assertError) - db.data.core.testapp.updater = '' - db.upsert(db.data.core.testapp.versions, { id: '999.888.777.666', version: '999.888.777.666', link: 'httplinkhere', filename: 'test.7z', failtodownload: 4 }) + ctx.db.data.core.testapp.updater = '' + ctx.db.upsert(ctx.db.data.core.testapp.versions, { id: '999.888.777.666', version: '999.888.777.666', link: 'httplinkhere', filename: 'test.7z', failtodownload: 4 }) await app.update() assert.ok(stubWrite.callCount >= 1) assert.strictEqual(app.updating, false) assert.notOk(provider.downloadVersion.called) - assert.match(db.data.core.testapp.updater, /many/i) - assert.match(db.data.core.testapp.updater, /fail/i) - assert.match(db.data.core.testapp.updater, /skip/i) + assert.match(ctx.db.data.core.testapp.updater, /many/i) + assert.match(ctx.db.data.core.testapp.updater, /fail/i) + assert.match(ctx.db.data.core.testapp.updater, /skip/i) }) t.test('should do nothing it exists and failtoinstall is higher than 3', async function() { @@ -720,31 +752,32 @@ t.timeout(250).describe('#update()', function() { const assertVersion = { version: '999.888.777.666', filename: 'test.7z' } provider.getLatestVersion.resolves(assertVersion) provider.downloadVersion.rejects(assertError) - db.data.core.testapp.updater = '' - db.upsert(db.data.core.testapp.versions, { id: '999.888.777.666', version: '999.888.777.666', link: 'httplinkhere', filename: 'test.7z', failtoinstall: 4 }) + ctx.db.data.core.testapp.updater = '' + ctx.db.upsert(ctx.db.data.core.testapp.versions, { id: '999.888.777.666', version: '999.888.777.666', link: 'httplinkhere', filename: 'test.7z', failtoinstall: 4 }) await app.update() assert.ok(stubWrite.callCount >= 1) assert.strictEqual(app.updating, false) assert.notOk(provider.downloadVersion.called) - assert.match(db.data.core.testapp.updater, /many/i) - assert.match(db.data.core.testapp.updater, /fail/i) - assert.match(db.data.core.testapp.updater, /skip/i) + assert.match(ctx.db.data.core.testapp.updater, /many/i) + assert.match(ctx.db.data.core.testapp.updater, /fail/i) + assert.match(ctx.db.data.core.testapp.updater, /skip/i) }) }) t.timeout(250).describe('#registerModule()', function() { const assertAppName = 'testappregister' - let db + let ctx let app t.beforeEach(function() { - return lowdb({ test: { } }, logger, null).then(function(res) { - db = res - let provider = new StaticProvider() - app = new Application(util, db, provider, assertAppName) - }) + return createFakeContext() + .then(function(res) { + ctx = res + let provider = new StaticProvider() + app = new Application(ctx, provider, assertAppName) + }) }) t.test('should fail if not an object with start', function() { diff --git a/test/core.test.mjs b/test/core.test.mjs new file mode 100644 index 0000000..57a548b --- /dev/null +++ b/test/core.test.mjs @@ -0,0 +1,365 @@ +import { Eltro as t, assert, stub } from 'eltro' +import fs from 'fs/promises' +import Core from '../core/core.mjs' +import Util from '../core/util.mjs' +import { createFakeLog } from './helpers.mjs' +import StaticProvider from '../core/providers/static.mjs' +import lowdb from '../core/db.mjs' + +const util = new Util(import.meta.url) +const log = createFakeLog() +let db + +t.before(function() { + return lowdb({}, log, null).then(function(res) { + db = res + }) +}) + +t.describe('Core.addProvider()', function() { + t.beforeEach(function() { + Core.providers.clear() + }) + + t.after(function() { + Core.providers.clear() + }) + + t.test('should fail if name is not a string', function() { + let tests = [ + [1, 'number'], + [0, 'false number'], + ['', 'false string'], + [[], 'array'], + [{}, 'object'], + ] + + tests.forEach(function(check) { + assert.throws(function() { + Core.addProvider(check[0], StaticProvider) + }, function(err) { + assert.match(err.message, /name/i) + assert.match(err.message, /string/i) + return true + }, `throw if name is ${check[1]}`) + }) + }) + + t.test('should fail if provider not a function', function() { + let tests = [ + [1, 'number'], + [0, 'false number'], + ['asdf', 'string'], + ['', 'false string'], + [[], 'array'], + [{}, 'object'], + ] + + tests.forEach(function(check) { + assert.throws(function() { + Core.addProvider('insertname', check[0]) + }, function(err) { + assert.match(err.message, /provider/i) + assert.match(err.message, /class/i) + assert.match(err.message, /insertname/i) + return true + }, `throw if provider is ${check[1]}`) + }) + }) + + t.test('should fail if provider instance is missing checkConfig', function() { + let tests = [ + [1, 'number'], + [0, 'false number'], + ['asdf', 'string'], + ['', 'false string'], + [[], 'array'], + [{}, 'object'], + ] + + tests.forEach(function(check) { + assert.throws(function() { + let provider = function() { this.getLatestVersion = function() {}; this.checkConfig = check[0] } + Core.addProvider('somename', provider) + }, function(err) { + assert.match(err.message, /provider/i) + assert.match(err.message, /class/i) + assert.match(err.message, /missing/i) + assert.match(err.message, /checkConfig/i) + assert.match(err.message, /somename/i) + return true + }, `throw if provider checkConfig is ${check[1]}`) + }) + }) + + t.test('should fail if provider instance is missing getLatestVersion', function() { + let tests = [ + [1, 'number'], + [0, 'false number'], + ['asdf', 'string'], + ['', 'false string'], + [[], 'array'], + [{}, 'object'], + ] + + tests.forEach(function(check) { + assert.throws(function() { + let provider = function() { this.checkConfig = function() {}; this.getLatestVersion = check[0] } + Core.addProvider('somename', provider) + }, function(err) { + assert.match(err.message, /provider/i) + assert.match(err.message, /class/i) + assert.match(err.message, /missing/i) + assert.match(err.message, /getLatestVersion/i) + assert.match(err.message, /somename/i) + return true + }, `throw if provider getLatestVersion is ${check[1]}`) + }) + }) + + t.test('should otherwise add provider to map', function() { + assert.strictEqual(Core.providers.size, 0) + Core.addProvider('testnamehere', StaticProvider) + assert.strictEqual(Core.providers.size, 1) + assert.strictEqual(Core.providers.get('testnamehere'), StaticProvider) + }) +}) + +t.describe('#constructor()', function() { + t.test('should throw if close is not a function', function() { + let tests = [ + [1, 'number'], + [0, 'false number'], + ['asdf', 'string'], + ['', 'false string'], + [[], 'array'], + [{}, 'object'], + ] + + tests.forEach(function(check) { + assert.throws(function() { + new Core(db, util, log, check[0]) + }, function(err) { + assert.match(err.message, /restart/i) + assert.match(err.message, /function/i) + return true + }, `throw if restart is ${check[1]}`) + }) + }) + + t.test('should throw if util is not util', function() { + let tests = [ + [1, 'number'], + [0, 'false number'], + ['asdf', 'string'], + ['', 'false string'], + [[], 'array'], + [{}, 'object'], + [Util, 'not instance'], + ] + + tests.forEach(function(check) { + assert.throws(function() { + new Core(db, check[0], log, function() {}) + }, function(err) { + assert.match(err.message, /util/i) + assert.match(err.message, /instance/i) + return true + }, `throw if util is ${check[1]}`) + }) + }) + + t.test('should throw if db is not lowdb', function() { + let tests = [ + [1, 'number'], + [0, 'false number'], + ['asdf', 'string'], + ['', 'false string'], + [[], 'array'], + [{}, 'object'], + [lowdb.Low, 'not instance'], + ] + + tests.forEach(function(check) { + assert.throws(function() { + new Core(check[0], util, log, function() {}) + }, function(err) { + assert.match(err.message, /db/i) + assert.match(err.message, /instance/i) + return true + }, `throw if db is ${check[1]}`) + }) + }) + + t.test('should throw if log is not an object with event', function() { + let func = function() {} + let validEvent = { info: func, warn: func, error: func } + let tests = [ + [1, 'number'], + [0, 'false number'], + [null, 'null'], + [undefined, 'undefined'], + ['asdf', 'string'], + ['', 'false string'], + [[], 'array'], + [{}, 'object'], + [{warn: func, event: validEvent }, 'log only warn'], + [{error: func, event: validEvent }, 'log only error'], + [{info: func, event: validEvent }, 'log only info'], + [{warn: func, error: func, event: validEvent }, 'log only warn and error'], + [{warn: func, info: func, event: validEvent }, 'log only warn and info'], + [{error: func, info: func, event: validEvent }, 'log only error and info'], + [{ warn: func, error: func, info: func, event: { warn: func } }, 'event only warn'], + [{ warn: func, error: func, info: func, event: { error: func } }, 'event only error'], + [{ warn: func, error: func, info: func, event: { info: func } }, 'event only info'], + [{ warn: func, error: func, info: func, event: { warn: func, error: func } }, 'event only warn and error'], + [{ warn: func, error: func, info: func, event: { warn: func, info: func } }, 'event only warn and info'], + [{ warn: func, error: func, info: func, event: { error: func, info: func } }, 'event only error and info'], + ] + + tests.forEach(function(check) { + assert.throws(function() { + new Core(db, util, check[0], func) + }, function(err) { + assert.match(err.message, /log/i) + assert.match(err.message, /valid/i) + return true + }, `throw if log is ${check[1]}`) + }) + }) + + t.test('should accept log, util and close function', function() { + const assertLog = log + const assertClose = function() {} + + let core = new Core(db, util, assertLog, assertClose) + assert.strictEqual(core.db, db) + assert.strictEqual(core.util, util) + assert.strictEqual(core.log, assertLog) + assert.strictEqual(core.restart, assertClose) + assert.deepStrictEqual(core.applications, []) + assert.ok(core.applicationMap) + }) +}) + +t.describe('#getApplication()', function() { + + t.test('should return application based on the name', function() { + const assertName = 'Yami no Naka' + const assertApplication = { a: 1 } + let core = new Core(db, util, log, function() {}) + core.applicationMap.set(assertName, assertApplication) + assert.strictEqual(core.getApplication(assertName), assertApplication) + }) +}) + +t.describe('#init()', function() { + const assertProviderName = 'Kyousuu Gakku Gogyou Kikan' + let core + let fakeUtil + let fakeProvider + let fakeProviderConfig + + function FakeProvider(config) { + fakeProvider(config) + this.static = true + this.checkConfig = fakeProviderConfig + } + + t.beforeEach(function() { + core = new Core(db, util, createFakeLog(), function() {}) + core.util = fakeUtil = { + verifyConfig: stub(), + getAppNames: stub().returns([]), + } + fakeProvider = stub() + fakeProviderConfig = stub() + Core.providers.set(assertProviderName, FakeProvider) + }) + + t.test('it should call util.verifyConfig correctly', async function() { + const assertError = new Error('Red faction IO drive mix') + const assertConfig = { a: 1 } + db.config = assertConfig + fakeUtil.verifyConfig.throws(assertError) + + let err = await assert.isRejected(core.init()) + assert.strictEqual(err, assertError) + assert.strictEqual(fakeUtil.verifyConfig.firstCall[0], assertConfig) + }) + + t.test('should call util.getNames correctly', async function() { + const assertError = new Error('Hero within') + const assertConfig = { a: 1 } + db.config = assertConfig + fakeUtil.getAppNames.throws(assertError) + + let err = await assert.isRejected(core.init()) + assert.strictEqual(err, assertError) + assert.strictEqual(fakeUtil.getAppNames.firstCall[0], assertConfig) + }) + + t.test('should call provider constructor correctly', async function() { + const assertError = new Error('Funny days') + const assertAppName = 'Tsuugakuro' + const assertConfig = { + [assertAppName]: { + provider: assertProviderName, + } + } + db.config = assertConfig + fakeProvider.throws(assertError) + fakeUtil.getAppNames.returns([assertAppName]) + + let err = await assert.isRejected(core.init()) + assert.strictEqual(err, assertError) + assert.strictEqual(fakeProvider.firstCall[0], assertConfig[assertAppName]) + }) + + t.test('should call provider checkConfig correctly', async function() { + const assertAppName = 'Zetsubou' + const assertConfig = { + [assertAppName]: { + provider: assertProviderName, + } + } + db.config = assertConfig + + const assertError = new Error('Shousou') + fakeProviderConfig.rejects(assertError) + fakeUtil.getAppNames.returns([assertAppName]) + + let err = await assert.isRejected(core.init()) + assert.strictEqual(err, assertError) + assert.strictEqual(fakeProviderConfig.firstCall[0], assertConfig[assertAppName]) + }) + + t.test('should create an application with the provider and name and config', async function() { + const assertAppName = 'Yasashii Ketsumatsu' + const assertTestString = 'Serozore no Omoi' + const assertConfig = { + [assertAppName]: { + provider: assertProviderName, + teststring: assertTestString, + } + } + db.config = assertConfig + fakeUtil.getAppNames.returns([assertAppName]) + assert.strictEqual(core.applications.length, 0) + + await core.init() + let application = core.getApplication(assertAppName) + + assert.ok(application) + assert.strictEqual(core.applications.length, 1) + assert.strictEqual(core.applications[0], application) + assert.strictEqual(application.name, assertAppName) + assert.strictEqual(application.ctx.db, core.db) + assert.strictEqual(application.ctx.util, core.util) + assert.strictEqual(application.ctx.log, core.log) + assert.strictEqual(application.ctx.core, core) + assert.strictEqual(application.config.teststring, assertTestString) + assert.ok(application.fresh) + assert.ok(application.provider instanceof FakeProvider) + }) +}) diff --git a/test/db.test.mjs b/test/db.test.mjs index 52de7dd..8db534f 100644 --- a/test/db.test.mjs +++ b/test/db.test.mjs @@ -118,9 +118,9 @@ t.test('Should support adding an application with defaults', async function() { assert.ok(db.data.core.app) assert.ok(db.data.core.app.versions) - assert.strictEqual(db.data.core.app.active, null) - assert.strictEqual(db.data.core.app.latestInstalled, null) - assert.strictEqual(db.data.core.app.latestVersion, null) + assert.strictEqual(db.data.core.app.active, '') + assert.strictEqual(db.data.core.app.latestInstalled, '') + assert.strictEqual(db.data.core.app.latestVersion, '') assert.notOk(db.data.core.herpderp) @@ -128,9 +128,9 @@ t.test('Should support adding an application with defaults', async function() { assert.ok(db.data.core.herpderp) assert.ok(db.data.core.herpderp.versions) - assert.strictEqual(db.data.core.herpderp.active, null) - assert.strictEqual(db.data.core.herpderp.latestInstalled, null) - assert.strictEqual(db.data.core.herpderp.latestVersion, null) + assert.strictEqual(db.data.core.herpderp.active, '') + assert.strictEqual(db.data.core.herpderp.latestInstalled, '') + assert.strictEqual(db.data.core.herpderp.latestVersion, '') }) t.test('Should support reading from db', async function() { diff --git a/test/exampleindex.mjs b/test/exampleindex.mjs index 45fc686..e58e244 100644 --- a/test/exampleindex.mjs +++ b/test/exampleindex.mjs @@ -1,4 +1,4 @@ -export function start(db, log, core, http, port) { +export function start(http, port, ctx) { const server = http.createServer(function (req, res) { res.writeHead(200); res.end(JSON.stringify({ version: 'exampleindex' })) @@ -9,8 +9,8 @@ export function start(db, log, core, http, port) { if (err) { return rej(err) } - log.event.info(`Server is listening on ${port} serving package ${staticPackage}`) - log.info(`Server is listening on ${port} serving package ${staticPackage}`) + ctx.log.event.info(`Server is listening on ${port} serving exampleindex`) + ctx.log.info(`Server is listening on ${port} serving exampleindex`) res() }) }) diff --git a/test/helpers.mjs b/test/helpers.mjs new file mode 100644 index 0000000..774f194 --- /dev/null +++ b/test/helpers.mjs @@ -0,0 +1,29 @@ +import { stub } from 'eltro' +import lowdb from '../core/db.mjs' +import Util from '../core/util.mjs' + +export function createFakeLog() { + return { + info: stub(), + warn: stub(), + error: stub(), + event: { + info: stub(), + warn: stub(), + error: stub(), + } + } +} + +export function createFakeContext(config = { }, util = new Util(import.meta.url), filename = null) { + const log = createFakeLog() + + return lowdb(config, log, filename).then(function(res) { + return { + db: res, + util: util, + log: log, + core: { }, + } + }) +} \ No newline at end of file diff --git a/test/http.test.mjs b/test/http.test.mjs index c713d5e..1ce1a37 100644 --- a/test/http.test.mjs +++ b/test/http.test.mjs @@ -1,6 +1,7 @@ import { Eltro as t, assert, stub } from 'eltro' import http from 'http' import https from 'https' +import { setTimeout } from 'timers/promises' import { request } from '../core/client.mjs' import HttpServer from '../core/http.mjs' @@ -22,9 +23,7 @@ t.describe('Sockets', function() { let http = new HttpServer() t.after(function() { - http.closeServer().then(function() { }, function(err) { - console.error(err) - }) + return http.closeServer() }) t.test('should keep track of sockets through its lifetime', function(cb) { @@ -41,7 +40,7 @@ t.describe('Sockets', function() { Promise.resolve() .then(async function() { await new Promise(function(res, rej) { - server.listen(port, function(err) { if (err) rej(err); res()}) + server.listen(port, function() { res()}) }) assert.strictEqual(actives.length, 0) @@ -50,16 +49,18 @@ t.describe('Sockets', function() { request({}, prefix).then(function() {}, cb) request({}, prefix).then(async function() { while (http.sockets.size > 0) { - await new Promise(function(res) { setTimeout(res, 10) }) + await setTimeout(10) } assert.strictEqual(http.sockets.size, 0) cb() }, cb) while (actives.length < 2) { - await new Promise(function(res) { setTimeout(res, 10) }) + await setTimeout(10) } assert.strictEqual(http.sockets.size, 2) + + assert.ok(http.active) actives[0].statusCode = 200 actives[0].end('{}') actives[1].statusCode = 200 @@ -68,13 +69,17 @@ t.describe('Sockets', function() { }) }) -t.describe('Close', function() { +t.describe('closeServer()', function() { let http = new HttpServer() t.after(function() { - http.closeServer().then(function() { }, function(err) { - console.error(err) - }) + return http.closeServer() + }) + + t.test('should not fail if server is not listening', function() { + http.createServer(function() { }) + + return http.closeServer() }) t.test('should support forcefully closing them on server close', function(cb) { @@ -90,7 +95,7 @@ t.describe('Close', function() { Promise.resolve() .then(async function() { await new Promise(function(res, rej) { - server.listen(port, function(err) { if (err) rej(err); res()}) + server.listen(port, function() { res()}) }) assert.strictEqual(http.sockets.size, 0) @@ -105,13 +110,15 @@ t.describe('Close', function() { ) while (http.sockets.size < 2) { - await new Promise(function(res) { setTimeout(res, 10) }) + await setTimeout(10) } + + assert.ok(http.active) http.closeServer().then(function() { }, cb) while (requestErrors.length < 2) { - await new Promise(function(res) { setTimeout(res, 10) }) + await setTimeout(10) } assert.strictEqual(http.sockets.size, 0) assert.strictEqual(requestErrors.length, 2) @@ -122,12 +129,39 @@ t.describe('Close', function() { assert.strictEqual(requestErrors[1].code, 'ECONNRESET') while (requestErrors.length < 2) { - await new Promise(function(res) { setTimeout(res, 10) }) + await setTimeout(10) } while (http.active) { - await new Promise(function(res) { setTimeout(res, 10) }) + await setTimeout(10) } }) .then(function() { cb()}, cb) }) }) + +t.describe('listenAsync()', function() { + let httpFirst = new HttpServer() + let httpSecond = new HttpServer() + + t.after(function() { + return Promise.all([ + httpFirst.closeServer(), + httpSecond.closeServer(), + ]) + }) + + t.test('should reject successfully if port is busy', async function() { + let serverFirst = httpFirst.createServer(function() { }) + let serverSecond = httpSecond.createServer(function() { }) + + await serverFirst.listenAsync(port) + + await setTimeout(10) + + let err = await assert.isRejected(serverSecond.listenAsync(port)) + assert.strictEqual(err.code, 'EADDRINUSE') + + assert.ok(serverFirst.listening) + assert.notOk(serverSecond.listening) + }) +}) diff --git a/test/log.test.mjs b/test/log.test.mjs new file mode 100644 index 0000000..a913815 --- /dev/null +++ b/test/log.test.mjs @@ -0,0 +1,286 @@ +import { Eltro as t, assert, stub } from 'eltro' +import getLog from '../core/log.mjs' + +t.describe('#constructor', function() { + t.afterEach(function() { + process.env.NODE_ENV = null + }) + + t.test('should add name', function() { + const assertName = 'Stray Cat' + let logger = getLog(assertName) + assert.strictEqual(logger.fields.name, assertName) + + process.env.NODE_ENV = 'production' + logger = getLog(assertName) + assert.strictEqual(logger.fields.name, assertName) + }) + + t.test('should add default stdout streams in normal environment', function() { + let logger = getLog('app', null) + assert.strictEqual(logger.streams.length, 4) + assert.strictEqual(logger.streams[0].stream, process.stdout) + assert.strictEqual(logger.streams[0].level, 20) + }) + + t.test('should add default file log stream in production environment', function() { + process.env.NODE_ENV = 'production' + + let logger = getLog('app', null) + assert.strictEqual(logger.streams.length, 4) + assert.strictEqual(logger.streams[0].path, 'log.log') + assert.strictEqual(logger.streams[0].level, 30) + }) + + t.test('should not add default stream if empty array', function() { + let logger = getLog('app', []) + assert.strictEqual(logger.streams.length, 3) + + process.env.NODE_ENV = 'production' + logger = getLog('app', []) + assert.strictEqual(logger.streams.length, 3) + }) + + t.test('should replace process.stdout with actual process', function() { + let logger = getLog('app', [{ stream: 'process.stdout', level: 'info' }]) + assert.strictEqual(logger.streams.length, 4) + assert.strictEqual(logger.streams[0].stream, process.stdout) + + process.env.NODE_ENV = 'production' + logger = getLog('app', [{ stream: 'process.stdout', level: 'info' }]) + assert.strictEqual(logger.streams.length, 4) + assert.strictEqual(logger.streams[0].stream, process.stdout) + }) +}) + +t.describe('ringbuffer', function() { + let logger + + t.beforeEach(function() { + logger = getLog('app', []) + }) + + t.test('should have ringbuffer for info', function() { + const assertMessage = 'Oitachi' + + assert.strictEqual(logger.ringbuffer.records.length, 0) + logger.info(assertMessage) + assert.strictEqual(logger.ringbuffer.records.length, 1) + assert.strictEqual(logger.ringbuffer.records[0].level, 30) + assert.strictEqual(logger.ringbuffer.records[0].msg, assertMessage) + logger.debug(assertMessage) + assert.strictEqual(logger.ringbuffer.records.length, 1) + logger.warn(assertMessage) + assert.strictEqual(logger.ringbuffer.records.length, 2) + assert.strictEqual(logger.ringbuffer.records[1].level, 40) + assert.strictEqual(logger.ringbuffer.records[1].msg, assertMessage) + }) + + t.test('should keep it limited to max 100 records', function() { + const assertPrefix = 'In memory of Keiten' + + for (let i = 1; i <= 101; i++) { + logger.info(assertPrefix + i) + } + assert.strictEqual(logger.ringbuffer.records.length, 100) + assert.strictEqual(logger.ringbuffer.records[0].msg, assertPrefix + '2') + + logger.info(assertPrefix) + + assert.strictEqual(logger.ringbuffer.records.length, 100) + assert.strictEqual(logger.ringbuffer.records[0].msg, assertPrefix + '3') + }) +}) + +t.describe('ringbufferwarn', function() { + let logger + + t.beforeEach(function() { + logger = getLog('app', []) + }) + + t.test('should have ringbufferwarn for info', function() { + const assertMessage = 'Oitachi' + + assert.strictEqual(logger.ringbufferwarn.records.length, 0) + logger.warn(assertMessage) + assert.strictEqual(logger.ringbufferwarn.records.length, 1) + assert.strictEqual(logger.ringbufferwarn.records[0].level, 40) + assert.strictEqual(logger.ringbufferwarn.records[0].msg, assertMessage) + logger.info(assertMessage) + assert.strictEqual(logger.ringbufferwarn.records.length, 1) + logger.error(assertMessage) + assert.strictEqual(logger.ringbufferwarn.records.length, 2) + assert.strictEqual(logger.ringbufferwarn.records[1].level, 50) + assert.strictEqual(logger.ringbufferwarn.records[1].msg, assertMessage) + }) + + t.test('should keep it limited to max 100 records', function() { + const assertPrefix = 'In memory of Keiten' + + for (let i = 1; i <= 101; i++) { + logger.warn(assertPrefix + i) + } + assert.strictEqual(logger.ringbufferwarn.records.length, 100) + assert.strictEqual(logger.ringbufferwarn.records[0].msg, assertPrefix + '2') + + logger.warn(assertPrefix) + + assert.strictEqual(logger.ringbufferwarn.records.length, 100) + assert.strictEqual(logger.ringbufferwarn.records[0].msg, assertPrefix + '3') + }) +}) + +t.describe('event', function() { + t.test('should call import if not in production', async function() { + let stubImport = stub() + stubImport.rejects(new Error('should not be seen')) + + let logger = getLog('app', [], { import: stubImport }) + + let first = new Promise(function(res, rej) { + setImmediate(function() { logger.event.warn('text message here').then(res, rej) }) + }) + let second = new Promise(function(res, rej) { + setImmediate(function() { logger.event.warn('new message here').then(res, rej) }) + }) + + await Promise.all([ + first, second, + ]) + + assert.notOk(stubImport.called) + }) + + t.test('should call import correctly if in production and fail only once', async function() { + process.env.NODE_ENV = 'production' + + let stubImport = stub() + stubImport.rejects(new Error('should not be seen')) + + let logger = getLog('app', [], { import: stubImport }) + + let first = new Promise(function(res, rej) { + setImmediate(function() { try { logger.event.warn('first').then(res, rej) } catch (err) { rej(err) } }) + }) + let second = new Promise(function(res, rej) { + setImmediate(function() { try { logger.event.warn('second').then(res, rej) } catch (err) { rej(err) } }) + }) + + await Promise.all([ + first, second, + ]) + + assert.ok(stubImport.called) + assert.strictEqual(stubImport.callCount, 1) + assert.strictEqual(stubImport.firstCall[0], 'node-windows') + + await new Promise(function(res, rej) { + setImmediate(function() { try { logger.event.warn('third').then(res, rej) } catch (err) { rej(err) } }) + }) + }) + + t.test('should call event on imported object correctly', async function() { + const assertName = 'It is going to be The Special' + let checkName = '' + let stubInfo = stub().returnWith(function(msg, code, cb) { setTimeout(cb, 20) }) + let stubWarn = stub().returnWith(function(msg, code, cb) { setTimeout(cb, 20) }) + let stubError = stub().returnWith(function(msg, code, cb) { setTimeout(cb, 20) }) + process.env.NODE_ENV = 'production' + + let stubImport = stub().resolves({ + default: { + EventLogger: function(name) { + checkName = name + this.info = stubInfo + this.warn = stubWarn + this.error = stubError + }, + }, + }) + + let logger = getLog(assertName, [], { import: stubImport }) + + let first = new Promise(function(res, rej) { + setImmediate(function() { try { logger.event.info('first', 1010).then(res, rej) } catch (err) { rej(err) } }) + }) + let second = new Promise(function(res, rej) { + setImmediate(function() { try { logger.event.warn('second', 1020).then(res, rej) } catch (err) { rej(err) } }) + }) + + await Promise.all([ + first, second, + ]) + + assert.strictEqual(checkName, assertName) + assert.ok(stubInfo.called) + assert.strictEqual(stubInfo.firstCall[0], 'first') + assert.strictEqual(stubInfo.firstCall[1], 1010) + assert.ok(stubWarn.called) + assert.strictEqual(stubWarn.firstCall[0], 'second') + assert.strictEqual(stubWarn.firstCall[1], 1020) + assert.notOk(stubError.called) + + await new Promise(function(res, rej) { + setImmediate(function() { try { logger.event.error('third', 1030).then(res, rej) } catch (err) { rej(err) } }) + }) + + assert.ok(stubError.called) + assert.strictEqual(stubError.firstCall[0], 'third') + assert.strictEqual(stubError.firstCall[1], 1030) + }) + + t.test('should work even if it were to throw', async function() { + const assertName = 'It is going to be The Special' + let checkName = '' + let stubInfo = stub().returnWith(function() { throw new Error('not to be seen') }) + let stubWarn = stub().returnWith(function() { throw new Error('not to be seen') }) + let stubError = stub().returnWith(function() { throw new Error('not to be seen') }) + process.env.NODE_ENV = 'production' + + let stubImport = stub().resolves({ + default: { + EventLogger: function(name) { + checkName = name + this.info = stubInfo + this.warn = stubWarn + this.error = stubError + }, + }, + }) + + let logger = getLog(assertName, [], { import: stubImport }) + + let first = new Promise(function(res, rej) { + setImmediate(function() { try { logger.event.info().then(res, rej) } catch (err) { rej(err) } }) + }) + let second = new Promise(function(res, rej) { + setImmediate(function() { try { logger.event.warn().then(res, rej) } catch (err) { rej(err) } }) + }) + + await Promise.all([ + first, second, + ]) + + assert.strictEqual(checkName, assertName) + assert.ok(stubInfo.called) + assert.ok(stubWarn.called) + assert.notOk(stubError.called) + + await new Promise(function(res, rej) { + setImmediate(function() { try { logger.event.error().then(res, rej) } catch (err) { rej(err) } }) + }) + + assert.ok(stubError.called) + }) + + t.test('should work without stub', async function() { + let res = await import('node-windows').catch(function() {}) + if (!res) { return } + + process.env.NODE_ENV = 'production' + let logger = getLog('service-core-unit-test', []) + + await logger.event.info('Hello from service-core log.event unit test') + }) +}) diff --git a/test/updater.test.mjs b/test/updater.test.mjs deleted file mode 100644 index e69de29..0000000 diff --git a/test/util.test.mjs b/test/util.test.mjs index b43de75..d5d63e2 100644 --- a/test/util.test.mjs +++ b/test/util.test.mjs @@ -1,6 +1,7 @@ import { Eltro as t, assert} from 'eltro' import fs from 'fs/promises' import Util from '../core/util.mjs' +import { defaults } from '../core/defaults.mjs' const isWindows = process.platform === 'win32' @@ -108,43 +109,162 @@ t.describe('#getApplications()', function() { assert.deepStrictEqual(util.getAppNames({ app: { provider: 1234, port: 1234 } }), []) }) + function getBase(extra = {}) { + return defaults({ app: extra }, { app: { provider: 'asdf', port: 1234, } }) + } + t.test('should fail to find if https is defined but not a boolean', function() { - assert.deepStrictEqual(util.getAppNames({ app: { provider: 'asdf', port: 1234, } }), ['app']) - assert.deepStrictEqual(util.getAppNames({ app: { provider: 'asdf', port: 1234, https: null } }), ['app']) - assert.deepStrictEqual(util.getAppNames({ app: { provider: 'asdf', port: 1234, https: false } }), ['app']) - assert.deepStrictEqual(util.getAppNames({ app: { provider: 'asdf', port: 1234, https: true } }), ['app']) - assert.deepStrictEqual(util.getAppNames({ app: { provider: 'asdf', port: 1234, https: 'asdf' } }), []) - assert.deepStrictEqual(util.getAppNames({ app: { provider: 'asdf', port: 1234, https: '1234' } }), []) - assert.deepStrictEqual(util.getAppNames({ app: { provider: 'asdf', port: 1234, https: 0 } }), []) - assert.deepStrictEqual(util.getAppNames({ app: { provider: 'asdf', port: 1234, https: [] } }), []) - assert.deepStrictEqual(util.getAppNames({ app: { provider: 'asdf', port: 1234, https: {} } }), []) + assert.deepStrictEqual(util.getAppNames(getBase()), ['app']) + assert.deepStrictEqual(util.getAppNames(getBase({ https: null })), ['app']) + assert.deepStrictEqual(util.getAppNames(getBase({ https: false })), ['app']) + assert.deepStrictEqual(util.getAppNames(getBase({ https: true })), ['app']) + assert.deepStrictEqual(util.getAppNames(getBase({ https: 'asdf' })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ https: '1234' })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ https: 0 })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ https: [] })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ https: {} })), []) }) t.test('should fail to find if updateEvery is defined but not a valid number', function() { - assert.deepStrictEqual(util.getAppNames({ app: { provider: 'asdf', port: 1234, } }), ['app']) - assert.deepStrictEqual(util.getAppNames({ app: { provider: 'asdf', port: 1234, updateEvery: null } }), ['app']) - assert.deepStrictEqual(util.getAppNames({ app: { provider: 'asdf', port: 1234, updateEvery: 5 } }), ['app']) - assert.deepStrictEqual(util.getAppNames({ app: { provider: 'asdf', port: 1234, updateEvery: 1000 } }), ['app']) - assert.deepStrictEqual(util.getAppNames({ app: { provider: 'asdf', port: 1234, updateEvery: 'asdf' } }), []) - assert.deepStrictEqual(util.getAppNames({ app: { provider: 'asdf', port: 1234, updateEvery: '1234' } }), []) - assert.deepStrictEqual(util.getAppNames({ app: { provider: 'asdf', port: 1234, updateEvery: 0 } }), []) - assert.deepStrictEqual(util.getAppNames({ app: { provider: 'asdf', port: 1234, updateEvery: -5 } }), []) - assert.deepStrictEqual(util.getAppNames({ app: { provider: 'asdf', port: 1234, updateEvery: [] } }), []) - assert.deepStrictEqual(util.getAppNames({ app: { provider: 'asdf', port: 1234, updateEvery: {} } }), []) + assert.deepStrictEqual(util.getAppNames(getBase()), ['app']) + assert.deepStrictEqual(util.getAppNames(getBase({ updateEvery: null })), ['app']) + assert.deepStrictEqual(util.getAppNames(getBase({ updateEvery: 5 })), ['app']) + assert.deepStrictEqual(util.getAppNames(getBase({ updateEvery: 1000 })), ['app']) + assert.deepStrictEqual(util.getAppNames(getBase({ updateEvery: 'asdf' })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ updateEvery: '1234' })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ updateEvery: 0 })), ['app']) + assert.deepStrictEqual(util.getAppNames(getBase({ updateEvery: -1 })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ updateEvery: -5 })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ updateEvery: [] })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ updateEvery: {} })), []) }) - t.test('should fail to find if waitUntilFail is defined but not a valid number', function() { - assert.deepStrictEqual(util.getAppNames({ app: { provider: 'asdf', port: 1234, } }), ['app']) - assert.deepStrictEqual(util.getAppNames({ app: { provider: 'asdf', port: 1234, waitUntilFail: null } }), ['app']) - assert.deepStrictEqual(util.getAppNames({ app: { provider: 'asdf', port: 1234, waitUntilFail: 5 } }), []) - assert.deepStrictEqual(util.getAppNames({ app: { provider: 'asdf', port: 1234, waitUntilFail: 15 } }), ['app']) - assert.deepStrictEqual(util.getAppNames({ app: { provider: 'asdf', port: 1234, waitUntilFail: 1000 } }), ['app']) - assert.deepStrictEqual(util.getAppNames({ app: { provider: 'asdf', port: 1234, waitUntilFail: 'asdf' } }), []) - assert.deepStrictEqual(util.getAppNames({ app: { provider: 'asdf', port: 1234, waitUntilFail: '1234' } }), []) - assert.deepStrictEqual(util.getAppNames({ app: { provider: 'asdf', port: 1234, waitUntilFail: 0 } }), []) - assert.deepStrictEqual(util.getAppNames({ app: { provider: 'asdf', port: 1234, waitUntilFail: -5 } }), []) - assert.deepStrictEqual(util.getAppNames({ app: { provider: 'asdf', port: 1234, waitUntilFail: [] } }), []) - assert.deepStrictEqual(util.getAppNames({ app: { provider: 'asdf', port: 1234, waitUntilFail: {} } }), []) + t.test('should fail to find if startWaitUntilFail is defined but not a valid number', function() { + assert.deepStrictEqual(util.getAppNames(getBase()), ['app']) + assert.deepStrictEqual(util.getAppNames(getBase({ startWaitUntilFail: null })), ['app']) + assert.deepStrictEqual(util.getAppNames(getBase({ startWaitUntilFail: 5 })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ startWaitUntilFail: 15 })), ['app']) + assert.deepStrictEqual(util.getAppNames(getBase({ startWaitUntilFail: 1000 })), ['app']) + assert.deepStrictEqual(util.getAppNames(getBase({ startWaitUntilFail: 'asdf' })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ startWaitUntilFail: '1234' })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ startWaitUntilFail: 0 })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ startWaitUntilFail: -5 })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ startWaitUntilFail: [] })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ startWaitUntilFail: {} })), []) + }) + + t.test('should fail to find if heartbeatTimeout is defined but not a valid number', function() { + assert.deepStrictEqual(util.getAppNames(getBase()), ['app']) + assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatTimeout: null })), ['app']) + assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatTimeout: 5 })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatTimeout: 15 })), ['app']) + assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatTimeout: 1000 })), ['app']) + assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatTimeout: 'asdf' })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatTimeout: '1234' })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatTimeout: 0 })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatTimeout: -5 })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatTimeout: [] })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatTimeout: {} })), []) + }) + + t.test('should fail to find if heartbeatAttempts is defined but not a valid number', function() { + assert.deepStrictEqual(util.getAppNames(getBase()), ['app']) + assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatAttempts: null })), ['app']) + assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatAttempts: 1 })), ['app']) + assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatAttempts: 15 })), ['app']) + assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatAttempts: 1000 })), ['app']) + assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatAttempts: 'asdf' })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatAttempts: '1234' })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatAttempts: 0 })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatAttempts: -5 })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatAttempts: [] })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatAttempts: {} })), []) + }) + + t.test('should fail to find if heartbeatAttemptsWait is defined but not a valid number', function() { + assert.deepStrictEqual(util.getAppNames(getBase()), ['app']) + assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatAttemptsWait: null })), ['app']) + assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatAttemptsWait: 5 })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatAttemptsWait: 15 })), ['app']) + assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatAttemptsWait: 1000 })), ['app']) + assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatAttemptsWait: 'asdf' })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatAttemptsWait: '1234' })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatAttemptsWait: 0 })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatAttemptsWait: -5 })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatAttemptsWait: [] })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatAttemptsWait: {} })), []) + }) + + t.test('should fail to find if heartbeatPath is defined but not a valid string', function() { + assert.deepStrictEqual(util.getAppNames(getBase()), ['app']) + assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatPath: null })), ['app']) + assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatPath: 5 })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatPath: 15 })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatPath: 1000 })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatPath: 'asdf' })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatPath: '1234' })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatPath: '/asdf' })), ['app']) + assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatPath: '/1234' })), ['app']) + assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatPath: 0 })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatPath: -5 })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatPath: [] })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ heartbeatPath: {} })), []) + }) + + t.test('should fail to find if log is defined but not an array', function() { + assert.deepStrictEqual(util.getAppNames(getBase()), ['app']) + assert.deepStrictEqual(util.getAppNames(getBase({ log: null })), ['app']) + assert.deepStrictEqual(util.getAppNames(getBase({ log: 5 })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ log: 'asdf' })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ log: '1234' })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ log: 0 })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ log: [] })), ['app']) + assert.deepStrictEqual(util.getAppNames(getBase({ log: {} })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ log: { length:1 } })), []) + }) + + t.test('should fail to find if log has an item but level and either stream or path ', function() { + assert.deepStrictEqual(util.getAppNames(getBase()), ['app']) + assert.deepStrictEqual(util.getAppNames(getBase({ log: null })), ['app']) + assert.deepStrictEqual(util.getAppNames(getBase({ log: [null] })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ log: [5] })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ log: [15] })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ log: [1000] })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ log: ['asdf'] })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ log: ['1234'] })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ log: ['/asdf'] })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ log: ['/1234'] })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ log: [0] })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ log: [-5] })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ log: [[]] })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ log: [{}] })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ log: [{ level: null, path: 'log' }] })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ log: [{ level: 5, path: 'log' }] })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ log: [{ level: 0, path: 'log' }] })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ log: [{ level: [], path: 'log' }] })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ log: [{ level: {}, path: 'log' }] })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ log: [{ level: '', path: 'log' }] })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ log: [{ level: 'asdf', path: 'log' }] })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ log: [{ level: 'fatal', path: 'log' }] })), ['app']) + assert.deepStrictEqual(util.getAppNames(getBase({ log: [{ level: 'error', path: 'log' }] })), ['app']) + assert.deepStrictEqual(util.getAppNames(getBase({ log: [{ level: 'warn', path: 'log' }] })), ['app']) + assert.deepStrictEqual(util.getAppNames(getBase({ log: [{ level: 'info', path: 'log' }] })), ['app']) + assert.deepStrictEqual(util.getAppNames(getBase({ log: [{ level: 'debug', path: 'log' }] })), ['app']) + assert.deepStrictEqual(util.getAppNames(getBase({ log: [{ level: 'trace', path: 'log' }] })), ['app']) + assert.deepStrictEqual(util.getAppNames(getBase({ log: [{ level: 'info', path: '' }] })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ log: [{ level: 'info', path: null }] })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ log: [{ level: 'info', path: 5 }] })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ log: [{ level: 'info', path: 0 }] })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ log: [{ level: 'info', path: [] }] })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ log: [{ level: 'info', path: {} }] })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ log: [{ level: 'info', stream: '' }] })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ log: [{ level: 'info', stream: null }] })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ log: [{ level: 'info', stream: 5 }] })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ log: [{ level: 'info', stream: 0 }] })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ log: [{ level: 'info', stream: [] }] })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ log: [{ level: 'info', stream: {} }] })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ log: [{ level: 'info', stream: 'asdf' }] })), []) + assert.deepStrictEqual(util.getAppNames(getBase({ log: [{ level: 'info', stream: 'process.stdout' }] })), ['app']) }) }) @@ -227,6 +347,14 @@ t.describe('#verifyConfig()', function() { t.describe('#extractFile()', function() { var util = new Util(import.meta.url) + t.beforeEach(function() { + return Promise.all([ + fs.rm('./test/testapp/example.tar', { force: true }), + fs.rm('./test/testapp/file1.txt', { force: true }), + fs.rm('./test/testapp/file2.txt', { force: true }), + ]) + }) + t.afterEach(function() { return Promise.all([ fs.rm('./test/testapp/example.tar', { force: true }),