Repository: rickbergfalk/sqlpad
Pull request #553 (2020-02-25 19:19:45)

Test infrastructure improvements
A bunch of testing infrastructure improvements to take advantage of the recent config, app, and models refactoring. 

* Restructures test utils to be a class, containing references to its own database and application objects. Each testUtils instance should technically be its own isolated environment!!!
* Adds a `dbInMemory` config item to create nedb databases in memory. `dbPath` is still required for cache and session support at this time.
* Adds a `wait` setting to mock driver to reduce faked wait time when running a mock query
* `fromEnv` and `fromFile` no longer have a default value provided. It is expected something is always provided like `fromCli`

With these changes each test case should be able to stand on its own, and not rely on a "global" test config. The general test cases will go something like this:

```js
const assert = require('assert');
const TestUtils = require('../utils');

describe('api/tags', function() {
  // This collection has its own private nedb automatically created
  const utils = new TestUtils();

  // before anything is done test utils must be initialized
  // the parameter adds in users (effectively `resetWithUsers` but with a fresh db)
  before(function() {
    return utils.init(true);
  });

  // assert, make API calls, whatever
  it('Performs some test', async function() {
    const res = await utils.post();
  });
});

```


Patch:
diff --git a/server/drivers/mock/index.js b/server/drivers/mock/index.js
index 15d13028..ab735b41 100644
--- a/server/drivers/mock/index.js
+++ b/server/drivers/mock/index.js
@@ -89,7 +89,8 @@ function cartesianify(rows, field) {
  * @param {object} connection
  */
 async function runQuery(query, connection) {
-  await wait(2000);
+  const ms = connection.hasOwnProperty('wait') ? connection.wait : 2000;
+  await wait(ms);
 
   // Connection here doesn't actually matter.
   // Someday this mock could get fancy and change output based on some connection value
@@ -315,6 +316,11 @@ const fields = [
     key: 'socksPassword',
     formType: 'TEXT',
     label: 'Password for socks proxy'
+  },
+  {
+    key: 'wait',
+    formType: 'TEXT',
+    label: 'Milliseconds to wait running query'
   }
 ];
 
diff --git a/server/lib/config/configItems.js b/server/lib/config/configItems.js
index 39299537..8a6787a0 100644
--- a/server/lib/config/configItems.js
+++ b/server/lib/config/configItems.js
@@ -218,6 +218,11 @@ const configItems = [
     key: 'webLogLevel',
     envVar: 'SQLPAD_WEB_LOG_LEVEL',
     default: 'info'
+  },
+  {
+    key: 'dbInMemory',
+    envVar: 'SQLPAD_DB_IN_MEMORY',
+    default: false
   }
 ];
 
diff --git a/server/lib/config/fromEnv.js b/server/lib/config/fromEnv.js
index f2f631f1..4454e6c4 100644
--- a/server/lib/config/fromEnv.js
+++ b/server/lib/config/fromEnv.js
@@ -5,7 +5,7 @@ const definitions = require('./configItems');
  * @param {object} env optional
  * @returns {object} configMap
  */
-module.exports = function getEnvConfig(env = process.env) {
+module.exports = function getEnvConfig(env) {
   return definitions
     .filter(definition => definition.hasOwnProperty('envVar'))
     .reduce((envMap, definition) => {
diff --git a/server/lib/config/fromFile.js b/server/lib/config/fromFile.js
index 0a32a9e0..69df6109 100644
--- a/server/lib/config/fromFile.js
+++ b/server/lib/config/fromFile.js
@@ -1,17 +1,13 @@
-const minimist = require('minimist');
 const fs = require('fs');
 const ini = require('ini');
 const configItems = require('./configItems');
 
-const argv = minimist(process.argv.slice(2));
-const configFilePathFromConfig = argv.config || process.env.SQLPAD_CONFIG;
-
 /**
  * Reads and parses config file.
  * This file may be either JSON or INI file.
  * @param {string} configFilePath
  */
-function fromFile(configFilePath = configFilePathFromConfig) {
+function fromFile(configFilePath) {
   let parsedFile = {};
   const warnings = [];
 
diff --git a/server/lib/config/index.js b/server/lib/config/index.js
index a1d18440..be060baf 100644
--- a/server/lib/config/index.js
+++ b/server/lib/config/index.js
@@ -8,12 +8,14 @@ const fromFile = require('./fromFile');
 const getOldConfigWarning = require('./getOldConfigWarning');
 
 class Config {
-  constructor(argv) {
+  constructor(argv, env) {
     this.argv = argv;
 
+    const configFilePathFromConfig = argv.config || env.SQLPAD_CONFIG;
+
     const defaultConfig = fromDefault();
-    const envConfig = fromEnv();
-    const [fileConfig, warnings] = fromFile();
+    const envConfig = fromEnv(env);
+    const [fileConfig, warnings] = fromFile(configFilePathFromConfig);
     const cliConfig = fromCli(argv);
 
     const all = { ...defaultConfig, ...envConfig, ...fileConfig, ...cliConfig };
diff --git a/server/lib/db.js b/server/lib/db.js
index d014c015..1c65255b 100644
--- a/server/lib/db.js
+++ b/server/lib/db.js
@@ -40,23 +40,26 @@ async function initNedb(config) {
   const admin = config.get('admin');
   const adminPassword = config.get('adminPassword');
   const dbPath = config.get('dbPath');
+  const dbInMemory = config.get('dbInMemory');
   const allowConnectionAccessToEveryone = config.get(
     'allowConnectionAccessToEveryone'
   );
 
   mkdirp.sync(path.join(dbPath, '/cache'));
 
+  function getDatastore(dbName) {
+    return dbInMemory
+      ? datastore()
+      : datastore({ filename: path.join(dbPath, dbName) });
+  }
+
   const nedb = {
-    users: datastore({ filename: path.join(dbPath, 'users.db') }),
-    connections: datastore({
-      filename: path.join(dbPath, 'connections.db')
-    }),
-    connectionAccesses: datastore({
-      filename: path.join(dbPath, 'connectionaccesses.db')
-    }),
-    queries: datastore({ filename: path.join(dbPath, 'queries.db') }),
-    queryHistory: datastore({ filename: path.join(dbPath, 'queryhistory.db') }),
-    cache: datastore({ filename: path.join(dbPath, 'cache.db') }),
+    users: getDatastore('users.db'),
+    connections: getDatastore('connections.db'),
+    connectionAccesses: getDatastore('connectionaccesses.db'),
+    queries: getDatastore('queries.db'),
+    queryHistory: getDatastore('queryhistory.db'),
+    cache: getDatastore('cache.db'),
     instances: [
       'users',
       'connections',
diff --git a/server/package.json b/server/package.json
index b0149202..59faf83b 100644
--- a/server/package.json
+++ b/server/package.json
@@ -29,7 +29,7 @@
   "scripts": {
     "prepublishOnly": "cd .. && ./scripts/build.sh",
     "start": "node-dev server.js --config './config.dev.ini' | pino-pretty",
-    "test": "rimraf ../dbtest && SQLPAD_DB_PATH='../dbtest' SQLPAD_DEBUG='true' SQLPAD_APP_LOG_LEVEL='silent' SQLPAD_WEB_LOG_LEVEL='silent' mocha test --timeout 10000 --recursive --exit",
+    "test": "rimraf ../dbtest && mocha test --timeout 10000 --recursive --exit",
     "fixlint": "eslint --fix '**/*.js'",
     "lint": "eslint '**/*.js'"
   },
diff --git a/server/server.js b/server/server.js
index 04246e6b..ecf3faaa 100755
--- a/server/server.js
+++ b/server/server.js
@@ -14,7 +14,7 @@ const { makeDb, getDb } = require('./lib/db');
 require('./lib/cli-flow.js');
 
 const argv = minimist(process.argv.slice(2));
-const config = new Config(argv);
+const config = new Config(argv, process.env);
 
 appLog.setLevel(config.get('appLogLevel'));
 appLog.debug(config.get(), 'Final config values');


Test patch:
diff --git a/server/drivers/mock/test.js b/server/drivers/mock/test.js
index a80f2f14..1fe6373f 100644
--- a/server/drivers/mock/test.js
+++ b/server/drivers/mock/test.js
@@ -8,7 +8,8 @@ const connection = {
   database: 'sqlpad',
   username: 'sqlpad',
   password: 'sqlpad',
-  maxRows: 100
+  maxRows: 100,
+  wait: 0
 };
 
 describe('drivers/mock', function() {
diff --git a/server/test/api/app.js b/server/test/api/app.js
index aca838fd..f8fdbb44 100644
--- a/server/test/api/app.js
+++ b/server/test/api/app.js
@@ -1,5 +1,11 @@
 const assert = require('assert');
-const utils = require('../utils');
+const TestUtils = require('../utils');
+
+function expectKeys(data, expectedKeys) {
+  Object.keys(data).forEach(key =>
+    assert(expectedKeys.includes(key), `expected key ${key}`)
+  );
+}
 
 const expectedKeys = [
   'adminRegistrationOpen',
@@ -20,10 +26,16 @@ const expectedConfigKeys = [
 ];
 
 describe('api/app', function() {
+  const utils = new TestUtils();
+
+  before(function() {
+    return utils.init(true);
+  });
+
   it('returns expected values', async function() {
     const body = await utils.get(null, '/api/app');
-    utils.expectKeys(body, expectedKeys);
-    utils.expectKeys(body.config, expectedConfigKeys);
+    expectKeys(body, expectedKeys);
+    expectKeys(body.config, expectedConfigKeys);
     assert.equal(
       Object.keys(body.config).length,
       expectedConfigKeys.length,
@@ -33,6 +45,6 @@ describe('api/app', function() {
 
   it('handles unknown baseUrl', async function() {
     const body = await utils.get(null, '/literally/any/path/api/app');
-    utils.expectKeys(body, expectedKeys);
+    expectKeys(body, expectedKeys);
   });
 });
diff --git a/server/test/api/connection-accesses.js b/server/test/api/connection-accesses.js
index e134b0aa..0882a1c6 100644
--- a/server/test/api/connection-accesses.js
+++ b/server/test/api/connection-accesses.js
@@ -1,8 +1,9 @@
 const assert = require('assert');
-const utils = require('../utils');
+const TestUtils = require('../utils');
 const consts = require('../../lib/consts');
 
 describe('api/connection-accesses', function() {
+  const utils = new TestUtils();
   let admin2;
   let user1;
   let user2;
@@ -12,7 +13,7 @@ describe('api/connection-accesses', function() {
   let connectionAccess1;
 
   before(async function() {
-    await utils.resetWithUser();
+    await utils.init(true);
 
     let connBody = await utils.post('admin', '/api/connections', {
       name: 'test connection 1',
@@ -20,7 +21,8 @@ describe('api/connection-accesses', function() {
       host: 'localhost',
       database: 'sqlpad',
       username: 'sqlpad',
-      password: 'sqlpad'
+      password: 'sqlpad',
+      wait: 0
     });
     connection1 = connBody.connection;
 
@@ -30,7 +32,8 @@ describe('api/connection-accesses', function() {
       host: 'localhost',
       database: 'sqlpad',
       username: 'sqlpad',
-      password: 'sqlpad'
+      password: 'sqlpad',
+      wait: 0
     });
     connection2 = connBody.connection;
 
diff --git a/server/test/api/connections.js b/server/test/api/connections.js
index 50ca9add..5c886bc1 100644
--- a/server/test/api/connections.js
+++ b/server/test/api/connections.js
@@ -1,11 +1,12 @@
 const assert = require('assert');
-const utils = require('../utils');
+const TestUtils = require('../utils');
 
 describe('api/connections', function() {
+  const utils = new TestUtils();
   let connection;
 
   before(function() {
-    return utils.resetWithUser();
+    return utils.init(true);
   });
 
   it('Returns empty array', async function() {
diff --git a/server/test/api/drivers.js b/server/test/api/drivers.js
index 8adef0b9..9b45a60c 100644
--- a/server/test/api/drivers.js
+++ b/server/test/api/drivers.js
@@ -1,9 +1,11 @@
 const assert = require('assert');
-const utils = require('../utils');
+const TestUtils = require('../utils');
 
 describe('api/drivers', function() {
+  const utils = new TestUtils();
+
   before(function() {
-    return utils.resetWithUser();
+    return utils.init(true);
   });
 
   it('gets drivers', async function() {
diff --git a/server/test/api/format-sql.js b/server/test/api/format-sql.js
index b86130c8..b1074b66 100644
--- a/server/test/api/format-sql.js
+++ b/server/test/api/format-sql.js
@@ -1,9 +1,11 @@
 const assert = require('assert');
-const utils = require('../utils');
+const TestUtils = require('../utils');
 
 describe('api/format-sql', function() {
+  const utils = new TestUtils();
+
   before(function() {
-    return utils.resetWithUser();
+    return utils.init(true);
   });
 
   it('format sql query', async function() {
diff --git a/server/test/api/password-reset.js b/server/test/api/password-reset.js
index f25471bf..73e4fc50 100644
--- a/server/test/api/password-reset.js
+++ b/server/test/api/password-reset.js
@@ -1,20 +1,20 @@
 const assert = require('assert');
-const utils = require('../utils');
 const uuid = require('uuid');
-const { getDb } = require('../../lib/db');
-
-async function setReset() {
-  const { models } = await getDb();
-  const user = await models.users.findOneByEmail('admin@test.com');
-  const passwordResetId = uuid.v4();
-  user.passwordResetId = passwordResetId;
-  await models.users.save(user);
-  return passwordResetId;
-}
+const TestUtils = require('../utils');
 
 describe('api/password-reset', function() {
+  const utils = new TestUtils();
+
+  async function setReset() {
+    const user = await utils.models.users.findOneByEmail('admin@test.com');
+    const passwordResetId = uuid.v4();
+    user.passwordResetId = passwordResetId;
+    await utils.models.users.save(user);
+    return passwordResetId;
+  }
+
   before(function() {
-    return utils.resetWithUser();
+    return utils.init(true);
   });
 
   it('Allows resetting password', async function() {
diff --git a/server/test/api/queries.js b/server/test/api/queries.js
index 44d12387..8c72ec48 100644
--- a/server/test/api/queries.js
+++ b/server/test/api/queries.js
@@ -1,11 +1,12 @@
 const assert = require('assert');
-const utils = require('../utils');
+const TestUtils = require('../utils');
 
 describe('api/queries', function() {
+  const utils = new TestUtils();
   let query;
 
   before(function() {
-    return utils.resetWithUser();
+    return utils.init(true);
   });
 
   it('Returns empty array', async function() {
diff --git a/server/test/api/query-history.js b/server/test/api/query-history.js
index 0da638e6..7f07b373 100644
--- a/server/test/api/query-history.js
+++ b/server/test/api/query-history.js
@@ -1,5 +1,5 @@
 const assert = require('assert');
-const utils = require('../utils');
+const TestUtils = require('../utils');
 const urlFilterToNeDbFilter = require('../../lib/urlFilterToNeDbFilter');
 
 const queryText1 = `
@@ -19,11 +19,12 @@ const queryText2 = `
 `;
 
 describe('api/query-history', function() {
+  const utils = new TestUtils();
   let connection;
   let query1;
 
   before(async function() {
-    await utils.resetWithUser();
+    await utils.init(true);
 
     const connBody = await utils.post('admin', '/api/connections', {
       name: 'test postgres',
@@ -31,7 +32,8 @@ describe('api/query-history', function() {
       host: 'localhost',
       database: 'sqlpad',
       username: 'sqlpad',
-      password: 'sqlpad'
+      password: 'sqlpad',
+      wait: 0
     });
     connection = connBody.connection;
 
diff --git a/server/test/api/query-result.js b/server/test/api/query-result.js
index 69121c84..e56f79f4 100644
--- a/server/test/api/query-result.js
+++ b/server/test/api/query-result.js
@@ -1,5 +1,5 @@
 const assert = require('assert');
-const utils = require('../utils');
+const TestUtils = require('../utils');
 
 const queryText = `
   -- dimensions = department 10, orderdate 10
@@ -25,11 +25,12 @@ function validateQueryResult(queryResult) {
 }
 
 describe('api/query-result', function() {
+  const utils = new TestUtils();
   let query;
   let connection;
 
   before(async function() {
-    await utils.resetWithUser();
+    await utils.init(true);
 
     const connBody = await utils.post('admin', '/api/connections', {
       name: 'test postgres',
@@ -37,7 +38,8 @@ describe('api/query-result', function() {
       host: 'localhost',
       database: 'sqlpad',
       username: 'sqlpad',
-      password: 'sqlpad'
+      password: 'sqlpad',
+      wait: 0
     });
     connection = connBody.connection;
 
diff --git a/server/test/api/schema-info.js b/server/test/api/schema-info.js
index 4a20065b..13cd8e4d 100644
--- a/server/test/api/schema-info.js
+++ b/server/test/api/schema-info.js
@@ -1,18 +1,20 @@
 const assert = require('assert');
-const utils = require('../utils');
+const TestUtils = require('../utils');
 
 describe('api/schema-info', function() {
+  const utils = new TestUtils();
   let connection;
 
   before(async function() {
-    await utils.resetWithUser();
+    await utils.init(true);
     const body = await utils.post('admin', '/api/connections', {
       driver: 'mock',
       name: 'sqlpad',
       host: 'localhost',
       database: 'sqlpad',
       username: 'sqlpad',
-      password: 'sqlpad'
+      password: 'sqlpad',
+      wait: 0
     });
     assert(!body.error, 'no error');
     connection = body.connection;
diff --git a/server/test/api/signup-signin.js b/server/test/api/signup-signin.js
index b0ac890a..22bfaed1 100644
--- a/server/test/api/signup-signin.js
+++ b/server/test/api/signup-signin.js
@@ -1,9 +1,11 @@
 const assert = require('assert');
-const utils = require('../utils');
+const TestUtil = require('../utils');
 
 describe('api/signup', function() {
+  const utils = new TestUtil();
+
   before(function() {
-    return utils.reset();
+    return utils.init();
   });
 
   it('allows new user signup', async function() {
@@ -50,9 +52,12 @@ describe('api/signup', function() {
 });
 
 describe('api/signin', function() {
+  const utils = new TestUtil();
+
   before(function() {
-    return utils.resetWithUser();
+    return utils.init(true);
   });
+
   it('signs in user', async function() {
     const body = await utils.post(null, '/api/signin', {
       password: 'admin',
diff --git a/server/test/api/tags.js b/server/test/api/tags.js
index 589717ea..7663127c 100644
--- a/server/test/api/tags.js
+++ b/server/test/api/tags.js
@@ -1,9 +1,11 @@
 const assert = require('assert');
-const utils = require('../utils');
+const TestUtils = require('../utils');
 
 describe('api/tags', function() {
+  const utils = new TestUtils();
+
   before(function() {
-    return utils.resetWithUser();
+    return utils.init(true);
   });
 
   it('Returns empty array', async function() {
diff --git a/server/test/api/test-connection.js b/server/test/api/test-connection.js
index dbabb80e..9fd0d534 100644
--- a/server/test/api/test-connection.js
+++ b/server/test/api/test-connection.js
@@ -1,9 +1,11 @@
 const assert = require('assert');
-const utils = require('../utils');
+const TestUtils = require('../utils');
 
 describe('api/test-connection', function() {
+  const utils = new TestUtils();
+
   before(function() {
-    return utils.resetWithUser();
+    return utils.init(true);
   });
 
   it('tests connection', async function() {
@@ -13,7 +15,8 @@ describe('api/test-connection', function() {
       host: 'localhost',
       database: 'sqlpad',
       username: 'sqlpad',
-      password: 'sqlpad'
+      password: 'sqlpad',
+      wait: 0
     });
     assert(!body.error, 'Expect no error');
   });
diff --git a/server/test/api/users.js b/server/test/api/users.js
index 650a3427..f43d894e 100644
--- a/server/test/api/users.js
+++ b/server/test/api/users.js
@@ -1,11 +1,12 @@
 const assert = require('assert');
-const utils = require('../utils');
+const TestUtils = require('../utils');
 
 describe('api/users', function() {
+  const utils = new TestUtils();
   let user;
 
   before(function() {
-    return utils.resetWithUser();
+    return utils.init(true);
   });
 
   it('Returns initial array', async function() {
diff --git a/server/test/lib/config.js b/server/test/lib/config.js
index 6d5c9532..8e244cdc 100644
--- a/server/test/lib/config.js
+++ b/server/test/lib/config.js
@@ -1,6 +1,5 @@
 const assert = require('assert');
-const { config } = require('../utils');
-
+const TestUtils = require('../utils');
 const fromDefault = require('../../lib/config/fromDefault');
 const fromEnv = require('../../lib/config/fromEnv');
 const fromCli = require('../../lib/config/fromCli');
@@ -30,10 +29,16 @@ describe('config', function() {
 });
 
 describe('lib/config', function() {
+  const utils = new TestUtils();
+
+  before(function() {
+    return utils.init(true);
+  });
+
   it('.get() should get a value provided by default', async function() {
-    assert.equal(config.get('httpsPort'), 443, 'httpsPort=443');
+    assert.equal(utils.config.get('httpsPort'), 443, 'httpsPort=443');
   });
   it('.get() should only accept key in config items', async function() {
-    assert.throws(() => config.get('non-existent-key'), Error);
+    assert.throws(() => utils.config.get('non-existent-key'), Error);
   });
 });
diff --git a/server/test/lib/email.js b/server/test/lib/email.js
index df8d16c0..b46c7bc7 100644
--- a/server/test/lib/email.js
+++ b/server/test/lib/email.js
@@ -1,9 +1,11 @@
 const makeEmail = require('../../lib/email.js');
-const { config } = require('../utils');
+const TestUtils = require('../utils');
 
 describe('lib/email.js', async function() {
-  if (config.smtpConfigured() && process.env.SQLPAD_TEST_EMAIL) {
-    const email = makeEmail(config);
+  const utils = new TestUtils();
+
+  if (utils.config.smtpConfigured() && process.env.SQLPAD_TEST_EMAIL) {
+    const email = makeEmail(utils.config);
 
     it('should send invites', function() {
       return email.sendInvite(process.env.SQLPAD_TEST_EMAIL);
diff --git a/server/test/models/connections.js b/server/test/models/connections.js
index 35cb6fb3..957143b7 100644
--- a/server/test/models/connections.js
+++ b/server/test/models/connections.js
@@ -1,22 +1,20 @@
 const assert = require('assert');
-const { config } = require('../utils');
-const { getDb } = require('../../lib/db');
+const TestUtils = require('../utils');
 
 describe('config.getConnections', function() {
-  let models;
+  const utils = new TestUtils();
 
-  before(async function() {
-    const db = await getDb();
-    models = db.models;
+  before(function() {
+    return utils.init(true);
   });
 
   it('handles empty object', function() {
-    const cs = config.getConnections({});
+    const cs = utils.config.getConnections({});
     assert(Array.isArray(cs));
   });
 
   it('skips partial connections', function() {
-    const cs = config.getConnections({
+    const cs = utils.config.getConnections({
       SQLPAD_CONNECTIONS__abc__driver: 'postgres'
     });
     assert(Array.isArray(cs));
@@ -24,7 +22,7 @@ describe('config.getConnections', function() {
   });
 
   it('parses connection properly', function() {
-    const cs = config.getConnections({
+    const cs = utils.config.getConnections({
       SQLPAD_CONNECTIONS__abc__driver: 'postgres',
       SQLPAD_CONNECTIONS__abc__name: 'env-postgres',
       SQLPAD_CONNECTIONS__abc__host: 'localhost',
@@ -50,7 +48,7 @@ describe('config.getConnections', function() {
     process.env.SQLPAD_CONNECTIONS__abc__port = '5432';
     process.env.SQLPAD_CONNECTIONS__abc__postgresSsl = 'true';
 
-    const allConnections = await models.connections.findAll();
+    const allConnections = await utils.models.connections.findAll();
     const connection = allConnections.find(c => c._id === 'abc');
     assert.strictEqual(connection.name, 'env-postgres', 'connection.name');
     assert.strictEqual(connection.editable, false, 'connection.editable');
@@ -69,7 +67,7 @@ describe('config.getConnections', function() {
     process.env.SQLPAD_CONNECTIONS__abc__port = '5432';
     process.env.SQLPAD_CONNECTIONS__abc__postgresSsl = 'true';
 
-    const connection = await models.connections.findOneById('abc');
+    const connection = await utils.models.connections.findOneById('abc');
     assert.strictEqual(connection.name, 'env-postgres', 'connection.name');
     assert.strictEqual(connection.editable, false, 'connection.editable');
 
diff --git a/server/test/utils.js b/server/test/utils.js
index 38668b1f..69b53095 100644
--- a/server/test/utils.js
+++ b/server/test/utils.js
@@ -1,21 +1,11 @@
 const assert = require('assert');
-const minimist = require('minimist');
+const uuid = require('uuid');
 const request = require('supertest');
-const consts = require('../lib/consts');
 const Config = require('../lib/config');
 const appLog = require('../lib/appLog');
 const { makeDb, getDb } = require('../lib/db');
 const makeApp = require('../app');
 
-const argv = minimist(process.argv.slice(2));
-
-// TODO - restructure test utils to allow injecting different configurations
-// config values can be supplied directly, parsing different sources can be tested separately
-const config = new Config(argv);
-
-makeDb(config);
-let app;
-
 const users = {
   admin: {
     email: 'admin@test.com',
@@ -29,40 +19,79 @@ const users = {
   }
 };
 
-function expectKeys(data, expectedKeys) {
-  Object.keys(data).forEach(key =>
-    assert(expectedKeys.includes(key), `expected key ${key}`)
-  );
-}
-
-async function reset() {
-  const { nedb } = await getDb();
-  return Promise.all([
-    nedb.users.remove({}, { multi: true }),
-    nedb.queries.remove({}, { multi: true }),
-    nedb.queryHistory.remove({}, { multi: true }),
-    nedb.connections.remove({}, { multi: true }),
-    nedb.connectionAccesses.remove(
+class TestUtils {
+  constructor(args = {}, env = {}) {
+    const config = new Config(
       {
-        $not: {
-          $and: [
-            { connectionId: consts.EVERY_CONNECTION_ID },
-            { userId: consts.EVERYONE_ID }
-          ]
-        }
+        debug: true,
+        // Despite being in-memory, still need a file path for cache and session files
+        // Eventually these will be moved to sqlite and we can be fully-in-memory
+        dbPath: '../dbtest',
+        dbInMemory: true,
+        ...args
       },
-      { multi: true }
-    )
-  ]);
-}
+      {
+        SQLPAD_APP_LOG_LEVEL: 'silent',
+        SQLPAD_WEB_LOG_LEVEL: 'silent',
+        ...env
+      }
+    );
+
+    appLog.setLevel(config.get('appLogLevel'));
+
+    this.config = config;
+    this.appLog = appLog;
+    this.instanceAlias = uuid.v1();
+
+    makeDb(config, this.instanceAlias);
+    this.app = undefined;
+  }
+
+  async init(withUsers) {
+    const { models } = await getDb(this.instanceAlias);
+
+    this.models = models;
+    this.app = makeApp(this.config, models);
+
+    assert.throws(() => {
+      makeDb(this.config, this.instanceAlias);
+    }, 'ensure nedb can be made once');
+
+    if (withUsers) {
+      const saves = Object.keys(users).map(key => {
+        return models.users.save(users[key]);
+      });
+      await Promise.all(saves);
+    }
+  }
+
+  async del(role, url, statusCode = 200) {
+    let req = request(this.app).delete(url);
+    req = addAuth(req, role);
+    const response = await req.expect(statusCode);
+    return response.body;
+  }
 
-async function resetWithUser() {
-  await reset();
-  const { models } = await getDb();
-  const saves = Object.keys(users).map(key => {
-    return models.users.save(users[key]);
-  });
-  return Promise.all(saves);
+  async get(role, url, statusCode = 200) {
+    let req = request(this.app).get(url);
+    req = addAuth(req, role);
+    const response = await req.expect(statusCode);
+    return response.body;
+  }
+
+  async post(role, url, body, statusCode = 200) {
+    let req = request(this.app).post(url);
+    req = addAuth(req, role);
+    const response = await req.send(body).expect(statusCode);
+    return response.body;
+  }
+
+  async put(role, url, body, statusCode = 200) {
+    let req = request(this.app).put(url);
+    req = addAuth(req, role);
+    const response = await req.send(body).expect(statusCode);
+    return response.body;
+  }
 }
 
 function addAuth(req, role) {
@@ -74,53 +103,4 @@ function addAuth(req, role) {
   return req;
 }
 
-async function del(role, url, statusCode = 200) {
-  let req = request(app).delete(url);
-  req = addAuth(req, role);
-  const response = await req.expect(statusCode);
-  return response.body;
-}
-
-async function get(role, url, statusCode = 200) {
-  let req = request(app).get(url);
-  req = addAuth(req, role);
-  const response = await req.expect(statusCode);
-  return response.body;
-}
-
-async function post(role, url, body, statusCode = 200) {
-  let req = request(app).post(url);
-  req = addAuth(req, role);
-  const response = await req.send(body).expect(statusCode);
-  return response.body;
-}
-
-async function put(role, url, body, statusCode = 200) {
-  let req = request(app).put(url);
-  req = addAuth(req, role);
-  const response = await req.send(body).expect(statusCode);
-  return response.body;
-}
-
-before(async function() {
-  const { models } = await getDb();
-  appLog.setLevel(config.get('appLogLevel'));
-  app = makeApp(config, models);
-
-  assert.throws(() => {
-    makeDb(config);
-  }, 'ensure nedb can be made once');
-
-  return resetWithUser();
-});
-
-module.exports = {
-  config,
-  del,
-  expectKeys,
-  get,
-  post,
-  put,
-  reset,
-  resetWithUser
-};
+module.exports = TestUtils;