diff --git a/core/application.mjs b/core/application.mjs index a1acc36..a7b5d5c 100644 --- a/core/application.mjs +++ b/core/application.mjs @@ -1,5 +1,69 @@ +import { EventEmitter } from 'events' +import fs from 'fs/promises' + export default class Application extends EventEmitter { - constructor() { - + constructor(util, db, provider, name, opts = {}) { + super() + this.util = util + this.db = db + this.config = db.config[name] + this.provider = provider + this.name = name + this.updating = false + + Object.assign(this, { + setInterval: opts.setInterval || setInterval, + }) + + this.db.addApplication(name) + } + + startAutoupdater() { + let timer = this.setInterval(() => { + this.update().then( + () => { + this.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.config.updateEvery || 180) * 60 * 1000) + timer.unref() + } + + updateLog(message) { + this.db.data.core[this.name].updater += message + this.db.log.info(message) + } + + async update() { + if (this.updating) return + + this.updating = true + this.db.data.core[this.name].updater = '' + + try { + this.updateLog(`Checking for latest version at ${new Date().toISOString().replace('T', ' ').split('.')[0]}. `) + + let latest = await this.provider.getLatestVersion() + + this.updateLog(`Found ${latest.version}. `) + + if (this.db.data.core[this.name].latestInstalled === latest.version) { + this.updateLog('Already up to date, nothing to do. ') + return + } + + let target = this.util.getPathFromRoot(`./${this.name}/${latest.version}/file${this.util.getExtension(latest.filename)}`) + + await fs.mkdir(this.util.getPathFromRoot(`./${this.name}/${latest.version}`), { recursive: true }) + + this.updateLog(`Downloading ${latest.link} to ${target}. `) + await this.provider.downloadVersion(latest, target) + } catch (err) { + this.updating = false + return Promise.reject(err) + } } } \ No newline at end of file diff --git a/core/db.mjs b/core/db.mjs index 7cbb47e..40506f6 100644 --- a/core/db.mjs +++ b/core/db.mjs @@ -1,10 +1,14 @@ -import { Low, JSONFile } from 'lowdb' +import { Low, JSONFile, Memory } from 'lowdb' import { type } from 'os' import { defaults, isObject } from './defaults.mjs' -export default function GetDB(config, util, log, filename = 'db.json') { - let fullpath = util.getPathFromRoot('./' + filename) - const adapter = new JSONFile(fullpath) +export default function GetDB(config, log, orgFilename = 'db.json') { + let adapter = new Memory() + let fullpath = 'in-memory' + if (orgFilename) { + fullpath = orgFilename + adapter = new JSONFile(fullpath) + } const db = new Low(adapter) db.id = 'id' @@ -70,14 +74,17 @@ export default function GetDB(config, util, log, filename = 'db.json') { active: null, latestInstalled: null, latestVersion: null, + updater: '', versions: [], }) } + db.log = log + return db.read() .then(function() { if (!isObject(db.data)) { - log.warn(`File ${fullpath} was empty or not a json object, clearing it.`) + db.log.warn(`File ${fullpath} was empty or not a json object, clearing it.`) db.data = {} } defaults(db.data, { core: { version: 1 } }) @@ -92,33 +99,4 @@ export default function GetDB(config, util, log, filename = 'db.json') { throw wrapped } ) - - /* - const adapter = new FileAsync(util.getPathFromRoot('./' + filename)) - - return lowdb(adapter) - .then(function(db) { - db._.mixin(lodashId) - db.adapterFilePath = util.getPathFromRoot('./' + filename) - - return db.defaults({ - core: { - "appActive": null, // Current active running - "appLatestInstalled": null, // Latest installed version - "appLatestVersion": null, // Newest version available - "manageActive": null, - "manageLatestInstalled": null, - "manageLatestVersion": null - }, - core_appHistory: [], - core_manageHistory: [], - core_version: 1, - }) - .write() - .then( - function() { return db }, - function() { throw new Error('Error writing defaults to lowdb'); } - ) - }) - */ } diff --git a/core/updater.mjs b/core/updater.mjs deleted file mode 100644 index e69de29..0000000 diff --git a/package.json b/package.json index 2b45406..24ff247 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "main": "lib.mjs", "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": "eltro \"test/**/*.test.mjs\" -r dot", "test:watch": "npm-watch test" }, "watch": { diff --git a/test/application.test.mjs b/test/application.test.mjs new file mode 100644 index 0000000..ed5cd39 --- /dev/null +++ b/test/application.test.mjs @@ -0,0 +1,269 @@ +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' + +const util = new Util(import.meta.url) + +var logger = { + info: stub(), + warn: stub(), + error: stub(), +} +function createProvider() { + return { + getLatestVersion: stub(), + downloadVersion: stub(), + } +} + +t.describe('constructor()', function() { + let db + + t.beforeEach(function() { + return lowdb({ test: { } }, logger, null).then(function(res) { + db = res + }) + }) + + t.test('should auto-create application', function() { + assert.notOk(db.data.core.test) + + new Application(util, db, {}, '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) + }) + + t.test('should keep config and other of itself', function() { + const assertTest = { a: 1 } + const assertName = 'test' + db.config = { + test: assertTest, + app: { b: 2}, + manage: { c: 3 }, + } + + let app = new Application(util, db, {}, assertName) + assert.strictEqual(app.config, assertTest) + assert.strictEqual(app.db, db) + assert.strictEqual(app.util, util) + assert.strictEqual(app.name, assertName) + }) + + t.test('should keep provider', function() { + const assertProvider = { a: 1 } + let app = new Application(util, db, assertProvider, 'test') + assert.strictEqual(app.provider, assertProvider) + }) +}) + +t.timeout(250).describe('#startAutoupdater()', function() { + let db + + t.beforeEach(function() { + return lowdb({ test: { }, testapp: { } }, logger, null).then(function(res) { + db = res + }) + }) + + t.test('should call setInterval correctly', function() { + const assertTimeMinutes = 1440 + const stubInterval = stub() + const stubUnref = stub() + stubInterval.returns({ unref: stubUnref }) + + db.config.test.updateEvery = assertTimeMinutes + + let app = new Application(util, db, {}, 'test', { setInterval: stubInterval }) + + assert.strictEqual(stubInterval.called, false) + assert.strictEqual(stubUnref.called, false) + + app.startAutoupdater() + + assert.strictEqual(stubInterval.called, true) + assert.strictEqual(stubUnref.called, true) + assert.strictEqual(typeof(stubInterval.firstCall[0]), 'function') + assert.strictEqual(stubInterval.firstCall[1], assertTimeMinutes * 60 * 1000) + }) + + t.test('should support default value if updateEvery is not defined', function() { + const stubInterval = stub() + stubInterval.returns({ unref: function() {} }) + + let app = new Application(util, db, {}, 'test', { setInterval: stubInterval }) + + assert.strictEqual(stubInterval.called, false) + + app.startAutoupdater() + + assert.strictEqual(stubInterval.called, true) + assert.strictEqual(typeof(stubInterval.firstCall[0]), 'function') + assert.strictEqual(stubInterval.firstCall[1], 3 * 60 * 60 * 1000) + }) + + t.test('should call update as promise correctly', async function() { + const stubUpdate = stub() + const stubInterval = stub() + stubInterval.returns({ unref: function() {} }) + + stubUpdate.returnWith(function() { + return Promise.resolve() + }) + + let app = new Application(util, db, {}, 'test', { setInterval: stubInterval }) + app.update = stubUpdate + app.startAutoupdater() + + assert.strictEqual(typeof(stubInterval.firstCall[0]), 'function') + assert.notStrictEqual(stubInterval.firstCall, stubUpdate) + + stubInterval.firstCall[0]() + + while (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) + }) + + t.test('should add any errors to last in db update check on errors when updating', async function() { + const stubInterval = stub() + const assertErrorMessage = 'Ai Do' + stubInterval.returns({ unref: function() {} }) + + let app = new Application(util, db, {}, 'test', { setInterval: stubInterval }) + app.update = function() { + return Promise.reject(new Error(assertErrorMessage)) + } + app.startAutoupdater() + + assert.strictEqual(db.data.core.test.updater, '') + + stubInterval.firstCall[0]() + + while (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)) + }) +}) + +t.timeout(250).describe('#update()', function() { + let db + let app + let provider + + t.beforeEach(function() { + return lowdb({ test: { } }, logger, null).then(function(res) { + db = res + provider = createProvider() + app = new Application(util, db, provider, 'testapp') + }) + }) + + t.afterEach(function() { + return fs.rm('./test/testapp/123456789', { force: true, recursive: true }) + }) + + t.test('multiple calls should be safe', async function() { + db.data.core.testapp.updater = '' + + provider.getLatestVersion.returnWith(function() { + return new Promise(function() {}) + }) + + assert.strictEqual(app.updating, false) + + app.update() + await setImmediate() + + assert.strictEqual(app.updating, true) + assert.strictEqual(provider.getLatestVersion.callCount, 1) + + app.update() + await setImmediate() + + assert.strictEqual(provider.getLatestVersion.callCount, 1) + }) + + 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 = '' + + let err = await assert.isRejected(app.update()) + assert.strictEqual(err, assertError) + assert.strictEqual(app.updating, false) + + 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])) + }) + + t.test('should call provider download latest correctly if new version', async function() { + const assertError = new Error('Without a fight') + const assertLink = 'All of you' + const assertVersion = { version: '123456789', link: assertLink, filename: 'test.7z' } + const assertTarget = util.getPathFromRoot('./testapp/123456789/file.7z') + + await assert.isFulfilled(fs.stat('./test/testapp')) + await assert.isRejected(fs.stat('./test/testapp/123456789')) + + provider.getLatestVersion.resolves(assertVersion) + provider.downloadVersion.rejects(assertError) + db.data.core.testapp.updater = '' + + let err = await assert.isRejected(app.update()) + assert.strictEqual(err, assertError) + assert.strictEqual(app.updating, false) + + 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.strictEqual(provider.downloadVersion.firstCall[0], assertVersion) + assert.strictEqual(provider.downloadVersion.firstCall[1], assertTarget) + + await fs.stat('./test/testapp/123456789') + }) + + t.test('should do nothing if latestInstalled matches version', async function() { + const assertError = new Error('should not be seen') + 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 + + await app.update() + assert.notOk(provider.downloadVersion.called) + assert.match(db.data.core.testapp.updater, /already/i) + assert.match(db.data.core.testapp.updater, /nothing/i) + }) + + t.test('should do nothing if version is found in versions', async function() { + const assertError = new Error('should not be seen') + 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 + + await app.update() + assert.notOk(provider.downloadVersion.called) + assert.match(db.data.core.testapp.updater, /already/i) + assert.match(db.data.core.testapp.updater, /nothing/i) + }) +}) diff --git a/test/db.test.mjs b/test/db.test.mjs index f7f85ee..2e5e721 100644 --- a/test/db.test.mjs +++ b/test/db.test.mjs @@ -11,19 +11,23 @@ var logger = { } t.before(function() { - return fs.rm('./test/db_test.json') - .catch(function() {}) + return Promise.all([ + fs.rm('./test/db_test.json', { force: true }), + fs.rm('./test/null', { force: true }), + ]) }) t.afterEach(function() { - return fs.rm('./test/db_test.json') - .catch(function() { }) + return Promise.all([ + fs.rm('./test/db_test.json', { force: true }), + fs.rm('./test/null', { force: true }), + ]) }) t.test('Should auto create file with some defaults', async function() { await assert.isRejected(fs.stat('./test/db_test.json')) - let db = await lowdb({}, util, logger, 'db_test.json') + let db = await lowdb({}, logger, util.getPathFromRoot('./db_test.json')) let stat = await fs.stat('./test/db_test.json') assert.ok(stat.size > 0) @@ -34,10 +38,25 @@ t.test('Should auto create file with some defaults', async function() { assert.notOk(db.data.core.manager) }) +t.test('Should support in-memory db', async function() { + await assert.isRejected(fs.stat('./test/db_test.json')) + await assert.isRejected(fs.stat('./test/null')) + + let db = await lowdb({}, logger, null) + + await assert.isRejected(fs.stat('./test/db_test.json')) + await assert.isRejected(fs.stat('./test/null')) + + assert.ok(db.data.core) + assert.ok(db.data.core.version) + assert.notOk(db.data.core.app) + assert.notOk(db.data.core.manager) +}) + t.test('Should map config to config', async function() { const assertConfig = { a: 1 } - let db = await lowdb(assertConfig, util, logger, 'db_test.json') + let db = await lowdb(assertConfig, logger, util.getPathFromRoot('./db_test.json')) assert.strictEqual(db.config, assertConfig) }) @@ -46,7 +65,7 @@ t.test('Should apply defaults to existing file', async function() { await assert.isRejected(fs.stat('./test/db_test.json')) await fs.writeFile('./test/db_test.json', '{"test":true}') - let db = await lowdb({}, util, logger, 'db_test.json') + let db = await lowdb({}, logger, util.getPathFromRoot('./db_test.json')) let stat = await fs.stat('./test/db_test.json') assert.ok(stat.size > 0) @@ -63,7 +82,7 @@ t.test('Should not fail if db is invalid', async function() { await assert.isRejected(fs.stat('./test/db_test.json')) await fs.writeFile('./test/db_test.json', '[]') - let db = await lowdb({}, util, logger, 'db_test.json') + let db = await lowdb({}, logger, util.getPathFromRoot('./db_test.json')) let stat = await fs.stat('./test/db_test.json') assert.ok(stat.size > 0) @@ -77,10 +96,15 @@ t.test('Should not fail if db is invalid', async function() { assert.match(logger.warn.firstCall[0], /clear/i) }) +t.test('Should attach logger to log', async function() { + let db = await lowdb({}, logger, util.getPathFromRoot('./db_test.json')) + assert.strictEqual(db.log, logger) +}) + t.test('Should support adding an application with defaults', async function() { await assert.isRejected(fs.stat('./test/db_test.json')) - let db = await lowdb({}, util, logger, 'db_test.json') + let db = await lowdb({}, logger, util.getPathFromRoot('./db_test.json')) let stat = await fs.stat('./test/db_test.json') assert.ok(stat.size > 0) @@ -113,7 +137,7 @@ t.test('Should support reading from db', async function() { const assertValue = { a: 1 } await assert.isRejected(fs.stat('./test/db_test.json')) - let db = await lowdb({}, util, logger, 'db_test.json') + let db = await lowdb({}, logger, util.getPathFromRoot('./db_test.json')) db.data.test = assertValue @@ -121,20 +145,20 @@ t.test('Should support reading from db', async function() { assert.strictEqual(db.data.test, assertValue) - let dbSecondary = await lowdb({}, util, logger, 'db_test.json') + let dbSecondary = await lowdb({}, logger, util.getPathFromRoot('./db_test.json')) assert.notStrictEqual(dbSecondary.data.test, assertValue) assert.deepStrictEqual(dbSecondary.data.test, assertValue) }) t.test('should throw if unable to write to file', function() { - return assert.isRejected(lowdb({}, util, logger, '../test')) + return assert.isRejected(lowdb({}, logger, '../test')) }) t.test('should have basic database-like functions defined', async function() { const assertItem1 = { a: 1 } const assertItem2 = { a: 2 } const assertItem3 = { a: 3 } - let db = await lowdb({}, util, logger, 'db_test.json') + let db = await lowdb({}, logger, util.getPathFromRoot('./db_test.json')) assert.strictEqual(db.id, 'id') db.data.myarr = [] @@ -152,7 +176,7 @@ t.test('should have basic database-like functions defined', async function() { await db.write() - let dbSec = await lowdb({}, util, logger, 'db_test.json') + let dbSec = await lowdb({}, logger, util.getPathFromRoot('./db_test.json')) assert.strictEqual(dbSec.data.myarr.length, 2) assert.notStrictEqual(dbSec.get(dbSec.data.myarr, assertItem1.id), assertItem1) @@ -193,7 +217,7 @@ t.test('should have basic database-like functions with string-like name of colle const assertItem1 = { a: 1 } const assertItem2 = { a: 2 } const assertItem3 = { a: 3 } - let db = await lowdb({}, util, logger, 'db_test.json') + let db = await lowdb({}, logger, util.getPathFromRoot('./db_test.json')) assert.strictEqual(db.id, 'id') db.data.myarr = [] @@ -211,7 +235,7 @@ t.test('should have basic database-like functions with string-like name of colle await db.write() - let dbSec = await lowdb({}, util, logger, 'db_test.json') + let dbSec = await lowdb({}, logger, util.getPathFromRoot('./db_test.json')) assert.strictEqual(dbSec.data.myarr.length, 2) assert.notStrictEqual(dbSec.get('myarr', assertItem1.id), assertItem1)