From 726ac81eb34b28d3334265f03d9914b3424867ca Mon Sep 17 00:00:00 2001 From: Jonatan Nilsson Date: Fri, 21 Jan 2022 02:43:48 +0000 Subject: [PATCH] Lots more development, git provider finished implementing. Started on creating application --- core/application.mjs | 5 + core/client.mjs | 73 +++++--- core/db.mjs | 4 +- core/providers/git.mjs | 64 ++++++++ core/updater.mjs | 0 core/util.mjs | 35 ++++ package.json | 13 +- test/client.test.mjs | 125 +++++++++++++- test/db.test.mjs | 30 ++-- test/providers/git.integration.test.mjs | 97 +++++++++++ test/providers/git.test.mjs | 210 ++++++++++++++++++++++++ test/util.test.mjs | 152 ++++++++++++++++- 12 files changed, 771 insertions(+), 37 deletions(-) create mode 100644 core/application.mjs create mode 100644 core/providers/git.mjs create mode 100644 core/updater.mjs create mode 100644 test/providers/git.integration.test.mjs create mode 100644 test/providers/git.test.mjs diff --git a/core/application.mjs b/core/application.mjs new file mode 100644 index 0000000..a1acc36 --- /dev/null +++ b/core/application.mjs @@ -0,0 +1,5 @@ +export default class Application extends EventEmitter { + constructor() { + + } +} \ No newline at end of file diff --git a/core/client.mjs b/core/client.mjs index cc06d8d..44deb41 100644 --- a/core/client.mjs +++ b/core/client.mjs @@ -1,7 +1,7 @@ import http from 'http' import https from 'https' +import stream from 'stream/promises' import fs from 'fs' -import url from 'url' function resolveRelative(from, to) { const resolvedUrl = new URL(to, new URL(from, 'resolve://')); @@ -23,7 +23,7 @@ export function request(config, path, filePath = null, redirects, fastRaw = fals } let parsed try { - parsed = new url.URL(path) + parsed = new URL(path) } catch { return Promise.reject(new Error('URL was empty or invalid')) } @@ -43,8 +43,8 @@ export function request(config, path, filePath = null, redirects, fastRaw = fals 'User-Agent': 'TheThing/service-core', Accept: 'application/vnd.github.v3+json' } - if (config.githubAuthToken && path.indexOf('api.github.com') >= 0) { - headers['Authorization'] = `token ${config.githubAuthToken}` + if (config.token) { + headers['Authorization'] = `token ${config.token}` } let timeout = fastRaw ? 5000 : config.timeout || 10000 @@ -66,38 +66,70 @@ export function request(config, path, filePath = null, redirects, fastRaw = fals if (timedout) { return } clearTimeout(timer) + const ac = new AbortController() + let output = '' if (filePath) { - let file = fs.createWriteStream(filePath) - res.pipe(file) + stream.pipeline(res, fs.createWriteStream(filePath), { signal: ac.signal }) + .then(function() { + resolve({ + statusCode: res.statusCode, + status: res.statusCode, + statusMessage: res.statusMessage, + headers: res.headers, + body: null + }) + }, function(err) { + if (err.code === 'ABORT_ERR') return + reject(err) + }) + // let file = fs.createWriteStream(filePath) + // res.pipe(file) } else { res.on('data', function(chunk) { output += chunk }) } res.on('end', function() { + let err = null + if (res.statusCode >= 300 && res.statusCode < 400) { if (newRedirects > 5) { - return reject(new Error(`Too many redirects (last one was ${res.headers.location})`)) + err = new Error(`Too many redirects (last one was ${res.headers.location})`) } - if (!res.headers.location) { - return reject(new Error('Redirect returned no path in location header')) + else if (!res.headers.location) { + err = new Error('Redirect returned no path in location header') } - if (res.headers.location.startsWith('http')) { + else if (res.headers.location.startsWith('http')) { + ac.abort() return resolve(request(config, res.headers.location, filePath, newRedirects, fastRaw)) } else { + ac.abort() return resolve(request(config, resolveRelative(path, res.headers.location), filePath, newRedirects, fastRaw)) } } else if (res.statusCode >= 400) { - return reject(new Error(`HTTP Error ${res.statusCode}: ${output}`)) + err = new Error(`HTTP Error ${res.statusCode}: ${output}`) + } + + if (err) { + ac.abort() + if (!filePath) return reject(err) + + // Do some cleanup in case we were in the middle of downloading file + return fs.rm(filePath, function() { + reject(err) + }) + } + // Let the pipeline do the resolving so it can finish flusing before calling resolve + if (!filePath) { + resolve({ + statusCode: res.statusCode, + status: res.statusCode, + statusMessage: res.statusMessage, + headers: res.headers, + body: output + }) } - resolve({ - statusCode: res.statusCode, - status: res.statusCode, - statusMessage: res.statusMessage, - headers: res.headers, - body: output - }) }) }) @@ -122,7 +154,10 @@ export function request(config, path, filePath = null, redirects, fastRaw = fals try { res.body = JSON.parse(res.body) } catch(e) { - throw new Error(`Error parsing body ${res.body}: ${e.message}`) + if (res.body.indexOf(' 0) { + return '.tar' + extension + } + return extension + } + + getAppNames(config) { + 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 + + out.push(key) + } + return out + } + + get7zipExecutable() { + const util = new Util(import.meta.url) + if (process.platform === 'win32') { + return util.getPathFromRoot('../bin/7za.exe') + } + return util.getPathFromRoot('../bin/7zas') + } + + verifyConfig(config) { + if (!config.name) throw new Error('name is missing in config.json') + if (this.getAppNames(config).length === 0) throw new Error('no application was found in config') + if (config.debugPort != null && (typeof(config.debugPort) !== 'number' || !config.debugPort)) throw new Error('debugPort in config not a valid number') + } + runCommand(command, options = [], folder = null, stream = function() {}) { return new Promise(function(res, rej) { stream(`[Command] ${folder ? folder : ''}${command} ${options.join(' ')}\n`) diff --git a/package.json b/package.json index 5f94209..c67f4c3 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,18 @@ "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": { + "test": { + "patterns": [ + "{core,test}/*" + ], + "extensions": "js,mjs", + "quiet": true, + "inherit": true + } }, "repository": { "type": "git", diff --git a/test/client.test.mjs b/test/client.test.mjs index f5504df..916dce0 100644 --- a/test/client.test.mjs +++ b/test/client.test.mjs @@ -1,7 +1,11 @@ import { Eltro as t, assert} from 'eltro' +import fs from 'fs/promises' import http from 'http' +import Util from '../core/util.mjs' import { request } from '../core/client.mjs' +const util = new Util(import.meta.url) +const testTargetFile = util.getPathFromRoot('./filetest.html') const port = 61412 const defaultHandler = function(req, res) { res.statusCode = 200 @@ -24,6 +28,11 @@ t.before(function(cb) { server.listen(port, cb) }) +t.after(function() { + return fs.rm(testTargetFile) + .catch(function() { }) +}) + t.describe('Basics', function() { t.beforeEach(function() { handler = defaultHandler @@ -57,10 +66,6 @@ t.describe('Basics', function() { }) t.describe('Request', function() { - t.beforeEach(function() { - handler = defaultHandler - }) - t.test('should work normally', async function() { let res = await request({}, prefix) assert.deepEqual(res.body, {a:1}) @@ -86,6 +91,35 @@ t.describe('Request', function() { assert.strictEqual(counter, 3) }) + t.test('should fail gracefully if HTML is received', async function() { + handler = function(req, res) { + res.statusCode = 200 + res.end(` + + + + + + `); + } + let err = await assert.isRejected(request({}, prefix)) + assert.match(err.message, /html/i) + assert.notMatch(err.message, /Unexpected token/i) + + handler = function(req, res) { + res.statusCode = 200 + res.end(` + + + + + `); + } + err = await assert.isRejected(request({}, prefix)) + assert.match(err.message, /html/i) + assert.notMatch(err.message, /Unexpected token/i) + }) + t.test('should fail if infinite redirect', async function() { const assertRelativeLocation = 'some/text/here' const assertLocation = prefix + assertRelativeLocation @@ -159,6 +193,89 @@ t.describe('Request', function() { }) }) +t.describe('Request download', function() { + const assertBody = ` + + + + + + ` + + t.beforeEach(function() { + handler = defaultHandler + }) + + t.afterEach(function() { + return fs.rm(testTargetFile) + .catch(function() { }) + }) + + t.test('should support downloading file successfully', async function() { + handler = function(req, res) { + res.statusCode = 200 + res.end(assertBody); + } + await request({}, prefix, testTargetFile) + + let stat = await fs.stat(testTargetFile) + assert.notStrictEqual(stat.size, 0) + assert.strictEqual(stat.size, assertBody.length) + let contents = await (await fs.readFile(testTargetFile)).toString() + assert.strictEqual(contents, assertBody) + }) + + t.test('should support downloading file after redirect successfully', async function() { + let counter = 0 + handler = function(req, res) { + if (counter < 3) { + res.statusCode = 302 + res.setHeader('Location', encodeURI(prefix)) + res.end(); + counter++ + return + } + res.statusCode = 200 + res.end(assertBody); + } + await request({}, prefix, testTargetFile) + + assert.strictEqual(counter, 3) + let stat = await fs.stat(testTargetFile) + assert.notStrictEqual(stat.size, 0) + assert.strictEqual(stat.size, assertBody.length) + let contents = await (await fs.readFile(testTargetFile)).toString() + assert.strictEqual(contents, assertBody) + }) + + t.test('should not create file if download redirects too many times', async function() { + handler = function(req, res) { + res.statusCode = 302 + res.setHeader('Location', encodeURI(prefix)) + res.end(assertBody); + } + let err = await assert.isRejected(request({}, prefix, testTargetFile)) + + assert.match(err.message, /redirects/) + let errStat = await assert.isRejected(fs.stat(testTargetFile)) + assert.strictEqual(errStat.code, 'ENOENT') + }) + + t.test('should not create file if download fails', async function() { + handler = function(req, res) { + res.statusCode = 404 + res.end('{}'); + } + let err = await assert.isRejected(request({}, prefix, testTargetFile)) + + assert.match(err.message, /HTTP/) + assert.match(err.message, /404/) + + let errStat = await assert.isRejected(fs.stat(testTargetFile)) + assert.strictEqual(errStat.code, 'ENOENT') + }) +}) + t.after(function(cb) { server.close(cb) }) diff --git a/test/db.test.mjs b/test/db.test.mjs index 0e3d16e..f7f85ee 100644 --- a/test/db.test.mjs +++ b/test/db.test.mjs @@ -23,7 +23,7 @@ t.afterEach(function() { 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({}, util, logger, 'db_test.json') let stat = await fs.stat('./test/db_test.json') assert.ok(stat.size > 0) @@ -34,11 +34,19 @@ t.test('Should auto create file with some defaults', async function() { 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') + + assert.strictEqual(db.config, assertConfig) +}) + 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({}, util, logger, 'db_test.json') let stat = await fs.stat('./test/db_test.json') assert.ok(stat.size > 0) @@ -55,7 +63,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({}, util, logger, 'db_test.json') let stat = await fs.stat('./test/db_test.json') assert.ok(stat.size > 0) @@ -72,7 +80,7 @@ t.test('Should not fail if db is invalid', async function() { 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({}, util, logger, 'db_test.json') let stat = await fs.stat('./test/db_test.json') assert.ok(stat.size > 0) @@ -105,7 +113,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({}, util, logger, 'db_test.json') db.data.test = assertValue @@ -113,20 +121,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({}, util, logger, '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({}, util, 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({}, util, logger, 'db_test.json') assert.strictEqual(db.id, 'id') db.data.myarr = [] @@ -144,7 +152,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({}, util, logger, 'db_test.json') assert.strictEqual(dbSec.data.myarr.length, 2) assert.notStrictEqual(dbSec.get(dbSec.data.myarr, assertItem1.id), assertItem1) @@ -185,7 +193,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({}, util, logger, 'db_test.json') assert.strictEqual(db.id, 'id') db.data.myarr = [] @@ -203,7 +211,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({}, util, logger, 'db_test.json') assert.strictEqual(dbSec.data.myarr.length, 2) assert.notStrictEqual(dbSec.get('myarr', assertItem1.id), assertItem1) diff --git a/test/providers/git.integration.test.mjs b/test/providers/git.integration.test.mjs new file mode 100644 index 0000000..6b00b84 --- /dev/null +++ b/test/providers/git.integration.test.mjs @@ -0,0 +1,97 @@ +import { Eltro as t, assert, stub } from 'eltro' +import Util from '../../core/util.mjs' +import fs from 'fs/promises' +import GitProvider from '../../core/providers/git.mjs' + +t.timeout(5000).describe('Git integration', function() { + t.after(function() { + return fs.rm('./test/providers/file.7z') + .catch(function() { }) + }) + + t.describe('#getLatestVersion()', function() { + t.test('should return latest version in a valid repository', async function() { + let provider = new GitProvider({ url: 'https://git.nfp.is/api/v1/repos/thething/sc-helloworld/releases' }) + let version = await provider.getLatestVersion() + assert.ok(version) + assert.ok(version.version) + assert.ok(version.link) + assert.match(version.link, /\/attachments\//) + }) + }) + + t.describe('#checkConfig()', function() { + t.test('should fail if link does not return json repository object', async function() { + let err = await assert.isRejected(new GitProvider({ url: 'http://git.nfp.is/api/v1/repos/thething/ProgramQueuer' }).checkConfig()) + assert.match(err.message, /valid/i) + assert.match(err.message, /repository/i) + err = await assert.isRejected(new GitProvider({ url: 'http://git.nfp.is/api/v1/orgs/nfp/repos' }).checkConfig()) + assert.match(err.message, /service-core/i) + assert.match(err.message, /release/i) + }) + + t.test('should fail if link returns no active release repository with assets', async function() { + let err = await assert.isRejected(new GitProvider({ url: 'https://git.nfp.is/api/v1/repos/thething/eltro/releases' }).checkConfig()) + assert.match(err.message, /service-core/i) + assert.match(err.message, /release/i) + }) + + t.test('should fail on private repositories if token missing', async function() { + let err = await assert.isRejected(new GitProvider({ url: 'https://git.nfp.is/api/v1/repos/TheThing/privateexample/releases' }).checkConfig()) + assert.match(err.message, /fail/i) + assert.match(err.message, /404/i) + assert.match(err.message, /release/i) + }) + + t.test('should otherwise succeed', function() { + return new GitProvider({ url: 'https://git.nfp.is/api/v1/repos/TheThing/sc-manager/releases' }) + .checkConfig() + }) + + let test = t + if (!process.env.gittesttoken) { + test = test.skip() + } + test.test('should succeed on private repo if token is provided', function() { + return new GitProvider({ + token: process.env.gittesttoken.trim(), + url: 'https://git.nfp.is/api/v1/repos/TheThing/privateexample/releases', + }).checkConfig() + }) + }) + + t.describe('#downloadVersion()', function() { + const util = new Util(import.meta.url) + + let test = t + if (!process.env.gittesttoken) { + test = test.skip() + } + test.test('should successfully download release', async function() { + let provider = new GitProvider({ + token: process.env.gittesttoken.trim(), + url: 'https://git.nfp.is/api/v1/repos/TheThing/privateexample/releases', + }) + await provider.checkConfig() + let version = await provider.getLatestVersion() + assert.ok(version.version) + assert.ok(version.filename) + assert.ok(version.link) + + let path = util.getPathFromRoot('./file.7z') + await provider.downloadVersion(version, path) + + let stat = await fs.stat(path) + assert.ok(stat.size > 0) + stat = await fs.stat('./test/providers/file.7z') + assert.ok(stat.size > 0) + + let output = '' + await util.runCommand(util.get7zipExecutable(), ['l', 'file.7z'], util.getPathFromRoot('./'), function(chunk) { + output += chunk + '\n' + }) + assert.ok(output.indexOf('file1.txt')) + assert.ok(output.indexOf('file2.txt')) + }) + }) +}) diff --git a/test/providers/git.test.mjs b/test/providers/git.test.mjs new file mode 100644 index 0000000..ba1ca51 --- /dev/null +++ b/test/providers/git.test.mjs @@ -0,0 +1,210 @@ +import { Eltro as t, assert, stub } from 'eltro' +import GitProvider from '../../core/providers/git.mjs' + +t.describe('#getLatestVersion()', function() { + t.test('should return correct name and link in result', async function() { + const assertName = 'Karen' + const assertLink = 'Over The Future' + const assertFilename = 'test-sc.7z' + let stubber = stub() + let provider = new GitProvider({}, stubber) + + stubber.resolves({ body: [ + { name: assertName, assets: [{ name: assertFilename, browser_download_url: assertLink }] }, + ]}) + + let version = await provider.getLatestVersion() + assert.strictEqual(version.version, assertName) + assert.strictEqual(version.link, assertLink) + assert.strictEqual(version.filename, assertFilename) + assert.strictEqual(version.log, '') + }) + + t.test('should support multiple extension', async function() { + const assertName = 'Karen' + const assertLink = 'My Wings' + const assertFilename = 'test-sc.zip' + let stubber = stub() + let provider = new GitProvider({}, stubber) + + stubber.resolves({ body: [ + { name: assertName, assets: [{ name: assertFilename, browser_download_url: assertLink }] }, + ]}) + + let version = await provider.getLatestVersion() + assert.strictEqual(version.version, assertName) + assert.strictEqual(version.link, assertLink) + assert.strictEqual(version.filename, assertFilename) + assert.strictEqual(version.log, '') + }) + + t.test('should skip versions with no assets', async function() { + const assertName = 'name1' + const assertLink = 'somelink' + const assertFilename = 'something-sc.7z' + let stubber = stub() + let provider = new GitProvider({}, stubber) + + stubber.resolves({ body: [ + { name: 'test', assets: [] }, + { name: assertName, assets: [{ name: assertFilename, browser_download_url: assertLink }] }, + ]}) + + let version = await provider.getLatestVersion() + assert.strictEqual(version.version, assertName) + assert.strictEqual(version.link, assertLink) + assert.strictEqual(version.filename, assertFilename) + }) + + t.test('should skip versions with non-sc filename', async function() { + const assertName = 'name2' + const assertLink = 'somelink2' + const assertFilename = 'something-sc.7z' + let stubber = stub() + let provider = new GitProvider({}, stubber) + + stubber.resolves({ body: [ + { name: 'test', assets: [{ name: 'nope.7z', browser_download_url: 'nope' }] }, + { name: assertName, assets: [{ name: assertFilename, browser_download_url: assertLink }] }, + ]}) + + let version = await provider.getLatestVersion() + assert.strictEqual(version.version, assertName) + assert.strictEqual(version.link, assertLink) + assert.strictEqual(version.filename, assertFilename) + }) + + t.test('should skip assets with non-sc filename', async function() { + const assertName = 'name3' + const assertLink = 'somelink3' + const assertFilename = 'something-sc.7z' + let stubber = stub() + let provider = new GitProvider({}, stubber) + + stubber.resolves({ body: [ + { name: 'test', assets: [{ name: 'nope.7z', browser_download_url: 'nope' }] }, + { name: assertName, assets: [ + { name: 'nope.7z', browser_download_url: 'nope' }, + { name: assertFilename, browser_download_url: assertLink }, + ] }, + ]}) + + let version = await provider.getLatestVersion() + assert.strictEqual(version.version, assertName) + assert.strictEqual(version.link, assertLink) + assert.strictEqual(version.filename, assertFilename) + }) + + t.test('should otherwise reject', async function() { + let stubber = stub() + let provider = new GitProvider({}, stubber) + + stubber.resolves({ body: [ + { name: 'test', assets: [{ name: 'nope.7z', browser_download_url: 'nope' }] }, + { name: 'test2', assets: [{ name: 'nope2.7z', browser_download_url: 'nope' },] }, + { name: 'test3', assets: [{ name: 'nope2.7z', browser_download_url: 'nope' },] }, + ]}) + + let err = await assert.isRejected(provider.getLatestVersion()) + assert.match(err.message, /release/) + assert.match(err.message, /found/) + }) + + t.test('should reject if not valid body', async function() { + let provider = new GitProvider({}, stub()) + + provider.requester.resolves({ body: { } }) + + let err = await assert.isRejected(provider.getLatestVersion()) + assert.match(err.message, /valid/) + assert.match(err.message, /repository/) + assert.match(err.message, /release/) + }) + + t.test('should wrap rejected errors', async function() { + const assertError = new Error('Oosegoto') + let provider = new GitProvider({}, stub()) + + provider.requester.rejects(assertError) + + let err = await assert.isRejected(provider.getLatestVersion()) + assert.notStrictEqual(err, assertError) + assert.match(err.message, /failed/i) + assert.match(err.message, /release/i) + assert.match(err.message, new RegExp(assertError.message)) + }) +}) + +t.describe('#downloadVersion()', function() { + t.test('should call requester with correct url and info', async function() { + const assertError = new Error('Moeyuru Monmon Mousou Zou') + const assertConfig = { a: 1 } + const assertUrl = 'http://test' + const assertTarget = '/some/path/here' + + let provider = new GitProvider({}, stub()) + provider.requestConfig = assertConfig + provider.requester.rejects(assertError) + + let err = await assert.isRejected(provider.downloadVersion({ + link: assertUrl, + }, assertTarget)) + assert.strictEqual(err, assertError) + assert.strictEqual(provider.requester.firstCall[0], assertConfig) + assert.strictEqual(provider.requester.firstCall[1], assertUrl) + assert.strictEqual(provider.requester.firstCall[2], assertTarget) + }) +}) + +t.describe('#getRequestConfig()', function() { + t.test('should default return empty object', function() { + assert.deepStrictEqual(GitProvider.getRequestConfig({}), {}) + assert.deepStrictEqual(GitProvider.getRequestConfig({ asdf: '1234' }), {}) + assert.deepStrictEqual(GitProvider.getRequestConfig({ url: 'http://test' }), {}) + assert.deepStrictEqual(GitProvider.getRequestConfig({ a: 1 }), {}) + }) + + t.test('should return token if token is defined', function() { + const assertToken = 'asdf1234' + assert.deepStrictEqual(GitProvider.getRequestConfig({ token: assertToken }), { token: assertToken }) + assert.deepStrictEqual(GitProvider.getRequestConfig({ token: assertToken, asdf: '1234' }), { token: assertToken }) + assert.deepStrictEqual(GitProvider.getRequestConfig({ token: assertToken, url: 'http://test' }), { token: assertToken }) + assert.deepStrictEqual(GitProvider.getRequestConfig({ token: assertToken, a: 1 }), { token: assertToken }) + }) +}) + +t.describe('#checkConfig()', function() { + t.test('should fail if missing url in config', async function() { + let err = await assert.isRejected(new GitProvider({}).checkConfig()) + assert.match(err.message, /url/i) + assert.match(err.message, /missing/i) + }) + + t.test('should fail if url is something else than an url', async function() { + let tests = [[], {}, 1234, 'asdf', 'http://'] + + let errors = await Promise.all(tests.map(function(check) { + return assert.isRejected(new GitProvider({ url: check }).checkConfig(), `Promise fulfilled with url = ${JSON.stringify(check)}`) + })) + for (let err of errors) { + assert.match(err.message, /url/i) + assert.match(err.message, /valid/i) + } + }) + + t.test('should call requester with correct config and url', async function() { + const assertError = new Error('Toki wo Koeta Yoru') + const assertRequestConfig = { a: 1 } + const assertUrl = 'http://test' + + let provider = new GitProvider({ url: assertUrl }, stub()) + provider.requestConfig = assertRequestConfig + provider.requester.rejects(assertError) + + let err = await assert.isRejected(provider.checkConfig()) + assert.notStrictEqual(err, assertError) + assert.match(err.message, new RegExp(assertError.message)) + assert.strict(provider.requester.firstCall[0], assertRequestConfig) + assert.strict(provider.requester.firstCall[1], assertUrl) + }) +}) diff --git a/test/util.test.mjs b/test/util.test.mjs index 1e6487e..d1e6c54 100644 --- a/test/util.test.mjs +++ b/test/util.test.mjs @@ -31,6 +31,156 @@ t.describe('#getUrlFromRoot()', function() { var util = new Util(import.meta.url) let data = await import(util.getUrlFromRoot('template.mjs')) - assert.deepEqual(data.default, { a: 1 }) + assert.deepStrictEqual(data.default, { a: 1 }) + }) +}) + +t.describe('#get7zipExecutable()', function() { + var util = new Util(import.meta.url) + + if (process.platform === 'win32') { + console.log('Adding 7zip windows exe path test') + + t.test('should return windows executable path', function() { + assert.ok(util.get7zipExecutable().endsWith('\\service-core\\bin\\7za.exe'), `${util.get7zipExecutable()} should end with 7za.exe`) + }) + } else { + console.log('Adding 7zip linux exe path test') + + t.test('should return linux executable path', function() { + assert.ok(util.get7zipExecutable().endsWith('/service-core/bin/7zas'), `${util.get7zipExecutable()} should end with 7zas`) + }) + } +}) + +t.describe('#getExtension()', function() { + var util = new Util(import.meta.url) + + t.test('should return correct extension on basic extension types', function() { + assert.strictEqual(util.getExtension('file.7z'), '.7z') + assert.strictEqual(util.getExtension('file.zip'), '.zip') + assert.strictEqual(util.getExtension('file.tar'), '.tar') + assert.strictEqual(util.getExtension('file.doc'), '.doc') + assert.strictEqual(util.getExtension('file.rar'), '.rar') + assert.strictEqual(util.getExtension('file.test'), '.test') + }) + + t.test('should support that annoying tar extension', function() { + assert.strictEqual(util.getExtension('file.tar.test'), '.tar.test') + assert.strictEqual(util.getExtension('file.tar.gz'), '.tar.gz') + assert.strictEqual(util.getExtension('file.tar.xz'), '.tar.xz') + assert.strictEqual(util.getExtension('file.tar.bz2'), '.tar.bz2') + }) +}) + +t.describe('#getApplications()', function() { + var util = new Util(import.meta.url) + + t.test('should fail to find if not a valid object', function() { + assert.deepStrictEqual(util.getAppNames({ app: [] }), []) + assert.deepStrictEqual(util.getAppNames({ app: 1234 }), []) + assert.deepStrictEqual(util.getAppNames({ app: '124124' }), []) + assert.deepStrictEqual(util.getAppNames({ app: '' }), []) + assert.deepStrictEqual(util.getAppNames({ app: {} }), []) + assert.deepStrictEqual(util.getAppNames({ app: null }), []) + }) + + t.test('should return the name of the key if an object with port and provider is found', function() { + assert.deepStrictEqual(util.getAppNames({ app: { port: 1234, provider: 'asdf' } }), ['app']) + }) + + t.test('should fail to find if port is missing or port not a number', function() { + assert.deepStrictEqual(util.getAppNames({ app: { provider: 'asdf', } }), []) + assert.deepStrictEqual(util.getAppNames({ app: { provider: 'asdf', port: null } }), []) + assert.deepStrictEqual(util.getAppNames({ app: { provider: 'asdf', port: 'asdf' } }), []) + assert.deepStrictEqual(util.getAppNames({ app: { provider: 'asdf', port: '1234' } }), []) + assert.deepStrictEqual(util.getAppNames({ app: { provider: 'asdf', port: 0 } }), []) + assert.deepStrictEqual(util.getAppNames({ app: { provider: 'asdf', port: [] } }), []) + assert.deepStrictEqual(util.getAppNames({ app: { provider: 'asdf', port: {} } }), []) + }) + + t.test('should fail to find if provider is missing or not a string', function() { + assert.deepStrictEqual(util.getAppNames({ app: { port: 1234 } }), []) + assert.deepStrictEqual(util.getAppNames({ app: { provider: '', port: 1234 } }), []) + assert.deepStrictEqual(util.getAppNames({ app: { provider: null, port: 1234 } }), []) + assert.deepStrictEqual(util.getAppNames({ app: { provider: [], port: 1234 } }), []) + assert.deepStrictEqual(util.getAppNames({ app: { provider: {}, port: 1234 } }), []) + assert.deepStrictEqual(util.getAppNames({ app: { provider: 1234, port: 1234 } }), []) + }) +}) + +t.describe('#verifyConfig()', function() { + var util = new Util(import.meta.url) + let input + + t.beforeEach(function() { + input = { + name: 'test', + title: 'Test', + description: 'Some description', + app: { port: 1234, provider: 'asdf' }, + } + }) + + let testMissing = ['name'] + + testMissing.forEach(function(check) { + t.test(`should fail on missing ${check}`, function() { + delete input[check] + + assert.throws(function() { + util.verifyConfig(input) + }, function(err) { + assert.match(err.message, new RegExp(check)) + return true + }) + }) + }) + + let testOptional = ['title', 'description'] + testOptional.forEach(function(check) { + t.test(`should succeed even if ${check} is missing`, function() { + delete input[check] + util.verifyConfig(input) + }) + }) + + t.test('should succeed if debug port is specified or null', function() { + input.debugPort = 1234 + util.verifyConfig(input) + input.debugPort = null + util.verifyConfig(input) + }) + + t.test('should fail if debug port is invalid', function() { + let checks = [ + [], + {}, + 0, + 'asdf', + '1234' + ] + checks.forEach(function(check) { + input.debugPort = check + + assert.throws(function() { + util.verifyConfig(input) + }, function(err) { + assert.match(err.message, /debugPort/) + assert.match(err.message, /number/) + return true + }) + }) + }) + + t.test('should fail if no objects in config', function() { + delete input.app + assert.throws(function() { + util.verifyConfig(input) + }, function(err) { + assert.match(err.message, /no/) + assert.match(err.message, /app/) + return true + }) }) })