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 }