diff --git a/.gitignore b/.gitignore index 68a7dd4..3df26ac 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ build node_modules test.db +tempdb diff --git a/package.json b/package.json index c48d172..1fe778a 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "description": "", "scripts": { "start": "node ./build/index.js", - "test": "echo \"Error: no test specified\" && exit 1", + "test": "node scripts/test-launch-with-different-databases.js", "lint": "tslint --project .", "lint:fix": "tslint --project . --fix", "build": "npm run build:clean && npm run build:json && npm run build:ts && npm run lint && npm run build:doc", diff --git a/scripts/test-launch-with-different-databases.js b/scripts/test-launch-with-different-databases.js new file mode 100644 index 0000000..fefcf41 --- /dev/null +++ b/scripts/test-launch-with-different-databases.js @@ -0,0 +1,56 @@ +/* + * server component for the TimeLimit App + * Copyright (C) 2019 - 2021 Jonas Lochmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +const { databaseLaunchers } = require('./util/database') +const { startMainApp } = require('./util/mainapp.js') + +// use export PATH="$PATH:/usr/lib/postgresql/11/bin" +// if the postgres binaries are not in the path + +async function main() { + let log = '' + + for (const launcher of databaseLaunchers) { + const database = await launcher() + + console.log('Test with ' + database.type) + + try { + const task = await startMainApp({ + DATABASE_URL: database.connectionUrl + }) + + console.log('test successfull') + log += 'Worked with ' + database.type + '\n' + + task.shutdown() + } catch (ex) { + log += 'Failure with ' + database.type + '\n' + console.warn('test failed', ex) + } + + database.shutdown() + } + + console.log('\nRESULTS\n\n' + log) + process.exit(0) +} + +main().catch((ex) => { + console.warn(ex) + process.exit(1) +}) diff --git a/scripts/util/database/helper.js b/scripts/util/database/helper.js new file mode 100644 index 0000000..f58d31e --- /dev/null +++ b/scripts/util/database/helper.js @@ -0,0 +1,22 @@ +/* + * server component for the TimeLimit App + * Copyright (C) 2019 - 2021 Jonas Lochmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +const { resolve } = require('path') + +const tempDir = resolve(__dirname, '../../../tempdb') + +module.exports = { tempDir } diff --git a/scripts/util/database/index.js b/scripts/util/database/index.js new file mode 100644 index 0000000..1a27cdd --- /dev/null +++ b/scripts/util/database/index.js @@ -0,0 +1,25 @@ +/* + * server component for the TimeLimit App + * Copyright (C) 2019 - 2021 Jonas Lochmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +const { startPostgres } = require('./postgres.js') +const { startMariadb } = require('./mariadb.js') +const { startSqlite } = require('./sqlite.js') + +module.exports = { + startPostgres, + databaseLaunchers: [ startMariadb, startPostgres, startSqlite ] +} diff --git a/scripts/util/database/mariadb.js b/scripts/util/database/mariadb.js new file mode 100644 index 0000000..a7a91e2 --- /dev/null +++ b/scripts/util/database/mariadb.js @@ -0,0 +1,78 @@ +/* + * server component for the TimeLimit App + * Copyright (C) 2019 - 2021 Jonas Lochmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +const { resolve } = require('path') +const { spawn } = require('child_process') +const { tempDir } = require('./helper.js') +const { generateShortToken, generateToken } = require('../token.js') +const { rimrafAsync, mkdirAsync, readFileAsync, writeFileAsync } = require('../filesystem.js') +const { spawnAsync } = require('../process.js') +const { sleep } = require('../sleep.js') + +async function startMariadb() { + try { await mkdirAsync(tempDir) } catch (ex) {/* ignore */} + + const instanceDir = resolve(tempDir, generateShortToken()) + const dataDir = resolve(instanceDir, 'data') + const socketPath = resolve(instanceDir, 'socket') + + await rimrafAsync(instanceDir) + await mkdirAsync(instanceDir); await mkdirAsync(dataDir) + + await spawnAsync('mysql_install_db', ['--datadir=' + dataDir, '--user=' + process.env.USER], { stdio: 'inherit' }) + + const task = await spawn('mysqld_safe', ['--no-defaults', '--datadir=' + dataDir, '--socket=' + socketPath, '--skip-networking'], { stdio: 'inherit' }) + task.on('exit', () => rimrafAsync(instanceDir)) + + for (let i = 0; i < 100; i++) { + const { status } = await spawnAsync('mysqladmin', ['ping', '-S', socketPath]) + + if (status === 0) break + + await sleep(100) + } + + const database = generateShortToken() + const username = generateShortToken() + const password = generateToken() + + const commands = [ + 'CREATE DATABASE `' + database + '` DEFAULT CHARACTER SET `utf8mb4` COLLATE `utf8mb4_bin`', + // all users of the system can see this password because it is passed as command + // line parameter - don't do this for anything important + 'CREATE USER `' + username + '`@localhost IDENTIFIED BY \'' + password + '\'', + 'GRANT ALL PRIVILEGES ON `' + database + '`.* TO `' + username + '`@localhost' + ] + + for (command of commands) { + console.log(command) + await spawnAsync('mysql', ['-S', socketPath, '-u', 'root', '-e', command], { stdio: 'inherit' }) + } + + return { + shutdown: () => task.kill('SIGINT'), + socketPath, + dataDir, + database, + username, + password, + connectionUrl: 'mariadb://' + username + ':' + password + '@localhost/' + database + '?socketPath=' + encodeURIComponent(socketPath), + type: 'mariadb' + } +} + +module.exports = { startMariadb } diff --git a/scripts/util/database/postgres.js b/scripts/util/database/postgres.js new file mode 100644 index 0000000..0a5c20d --- /dev/null +++ b/scripts/util/database/postgres.js @@ -0,0 +1,75 @@ +/* + * server component for the TimeLimit App + * Copyright (C) 2019 - 2021 Jonas Lochmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +const { resolve } = require('path') +const { spawn } = require('child_process') +const { tempDir } = require('./helper.js') +const { generateShortToken, generateToken } = require('../token.js') +const { rimrafAsync, mkdirAsync, readFileAsync, writeFileAsync } = require('../filesystem.js') +const { spawnAsync } = require('../process.js') +const { sleep } = require('../sleep.js') + +async function startPostgres() { + try { await mkdirAsync(tempDir) } catch (ex) {/* ignore */} + + const instanceDir = resolve(tempDir, generateShortToken()) + const dataDir = resolve(instanceDir, 'data') + const socketDir = resolve(instanceDir, 'socket') + + await rimrafAsync(instanceDir) + await mkdirAsync(instanceDir); await mkdirAsync(dataDir); await mkdirAsync(socketDir) + + await spawnAsync('initdb', ['--locale=en_US.UTF-8', '-E', ' UTF8', '-D', dataDir], { stdio: 'inherit' }) + + const configFilePath = resolve(dataDir, 'postgresql.conf') + const configFileContent = (await readFileAsync(configFilePath)) + '\n' + + 'unix_socket_directories = \'' + socketDir + '\'' + '\n' + + 'listen_addresses = \'\' # do not listen using TCP' + + await writeFileAsync(configFilePath, configFileContent) + + const task = spawn('postgres', ['-D', dataDir], { stdio: 'inherit' }) + task.on('exit', () => rimrafAsync(instanceDir)) + + for (let i = 0; i < 100; i++) { + const { status } = await spawnAsync('pg_isready', ['-h', socketDir]) + + if (status === 0) break + + await sleep(100) + } + + const database = generateShortToken() + const username = generateShortToken() + const password = generateToken() // this database accepts anything + + await spawnAsync('createuser', ['-h', socketDir, username], { stdio: 'inherit' }) + await spawnAsync('createdb', ['-h', socketDir, database], { stdio: 'inherit' }) + + return { + shutdown: () => task.kill('SIGINT'), + socketDir, + dataDir, + database, + username, + password, + connectionUrl: 'postgres://' + username + ':' + password + '@localhost/' + database + '?host=' + encodeURIComponent(socketDir), + type: 'postgres' + } +} + +module.exports = { startPostgres } diff --git a/scripts/util/database/sqlite.js b/scripts/util/database/sqlite.js new file mode 100644 index 0000000..081f98d --- /dev/null +++ b/scripts/util/database/sqlite.js @@ -0,0 +1,42 @@ +/* + * server component for the TimeLimit App + * Copyright (C) 2019 - 2021 Jonas Lochmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +const { resolve } = require('path') +const { spawn } = require('child_process') +const { tempDir } = require('./helper.js') +const { generateShortToken, generateToken } = require('../token.js') +const { rimrafAsync, mkdirAsync, readFileAsync, writeFileAsync } = require('../filesystem.js') +const { spawnAsync } = require('../process.js') +const { sleep } = require('../sleep.js') + +async function startSqlite() { + try { await mkdirAsync(tempDir) } catch (ex) {/* ignore */} + + const instanceDir = resolve(tempDir, generateShortToken()) + + await rimrafAsync(instanceDir) + await mkdirAsync(instanceDir) + + return { + shutdown: () => rimrafAsync(instanceDir), + instanceDir, + connectionUrl: 'sqlite:///' + instanceDir + '/test.db', + type: 'sqlite' + } +} + +module.exports = { startSqlite } diff --git a/scripts/util/filesystem.js b/scripts/util/filesystem.js new file mode 100644 index 0000000..94efbe6 --- /dev/null +++ b/scripts/util/filesystem.js @@ -0,0 +1,57 @@ +/* + * server component for the TimeLimit App + * Copyright (C) 2019 - 2021 Jonas Lochmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +const rimraf = require('rimraf') +const { mkdir, readFile, writeFile } = require('fs') + +function mkdirAsync(path) { + return new Promise((resolve, reject) => { + mkdir(path, (err) => { + if (err) reject(err) + else resolve() + }) + }) +} + +function rimrafAsync(path) { + return new Promise((resolve, reject) => { + rimraf(path, (err) => { + if (err) reject(err) + else resolve() + }) + }) +} + +function readFileAsync(path) { + return new Promise((resolve, reject) => { + readFile(path, (err, res) => { + if (err) reject(err) + else resolve(res) + }) + }) +} + +function writeFileAsync(path, content) { + return new Promise((resolve, reject) => { + writeFile(path, content, (err) => { + if (err) reject(err) + else resolve() + }) + }) +} + +module.exports = { mkdirAsync, rimrafAsync, readFileAsync, writeFileAsync } diff --git a/scripts/util/mainapp.js b/scripts/util/mainapp.js new file mode 100644 index 0000000..141b154 --- /dev/null +++ b/scripts/util/mainapp.js @@ -0,0 +1,48 @@ +/* + * server component for the TimeLimit App + * Copyright (C) 2019 - 2021 Jonas Lochmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +const { spawn } = require('child_process') + +function startMainApp(env) { + return new Promise((resolve, reject) => { + const task = spawn('npm', ['start'], { + stdio: ['inherit', 'pipe', 'inherit'], + env: { ...process.env, PORT: 0 /* random port */, ...env } + }) + + task.on('exit', () => reject(new Error('task terminated too early'))) + task.on('error', (ex) => reject(ex)) + + task.stdout.on('data', (data) => { + if (data.toString('utf8').split('\n').indexOf('ready') !== -1) resolve(task) + + process.stdout.write(data) + }) + + setTimeout(() => { + reject(new Error('timeout')) + + task.kill('SIGINT') + }, 1000 * 30) + }).then((task) => { + return { + shutdown: () => task.kill('SIGINT') + } + }) +} + +module.exports = { startMainApp } diff --git a/scripts/util/process.js b/scripts/util/process.js new file mode 100644 index 0000000..6c2d5ea --- /dev/null +++ b/scripts/util/process.js @@ -0,0 +1,29 @@ +/* + * server component for the TimeLimit App + * Copyright (C) 2019 - 2021 Jonas Lochmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +const { spawn } = require('child_process') + +function spawnAsync(command, args, options) { + return new Promise((resolve, reject) => { + const task = spawn(command, args, options) + + task.on('error', (ex) => reject(ex)) + task.on('exit', (status) => resolve({ status })) + }) +} + +module.exports = { spawnAsync } diff --git a/scripts/util/sleep.js b/scripts/util/sleep.js new file mode 100644 index 0000000..5e7632b --- /dev/null +++ b/scripts/util/sleep.js @@ -0,0 +1,24 @@ +/* + * server component for the TimeLimit App + * Copyright (C) 2019 - 2021 Jonas Lochmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +function sleep(delay) { + return new Promise((resolve, reject) => { + setTimeout(() => resolve(), delay) + }) +} + +module.exports = { sleep } diff --git a/scripts/util/token.js b/scripts/util/token.js new file mode 100644 index 0000000..a74e172 --- /dev/null +++ b/scripts/util/token.js @@ -0,0 +1,33 @@ +/* + * server component for the TimeLimit App + * Copyright (C) 2019 - 2021 Jonas Lochmann + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +const TokenGenerator = require('tokgen') + +const tokenGenerator = new TokenGenerator({ + length: 32, + chars: 'a-zA-Z0-9' +}) + +const shortTokenGenerator = new TokenGenerator({ + length: 8, + chars: 'a-zA-Z0-9' +}) + +function generateToken() { return tokenGenerator.generate() } +function generateShortToken() { return shortTokenGenerator.generate() } + +module.exports = { generateToken, generateShortToken }