Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"version": "0.2.0",
"configurations": [
{
"name": "Launch via NPM",
"name": "Node Server",
"request": "launch",
"runtimeArgs": [
"run-script",
Expand All @@ -18,7 +18,7 @@
"type": "node"
},
{
"name": "Test",
"name": "Full Test",
"request": "launch",
"runtimeArgs": [
"run-script",
Expand All @@ -29,6 +29,6 @@
"<node_internals>/**"
],
"type": "node"
},
}
]
}
4 changes: 1 addition & 3 deletions app/appEndpoints.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ const express = require('express');
const config = require("./config");
const errors = require("./util/errors");
const nunjucks = require("./nunjucks");
const { initMail } = require("./emailer");
const knex = require("./db"); // initialize on startup
const app = express();

// shift.conf for nginx sets the x-forward header
Expand Down Expand Up @@ -70,5 +68,5 @@ app.use(function(err, req, res, next) {
console.error(err.stack);
});

// for testing
// for starting a server or running tests
module.exports = app;
67 changes: 52 additions & 15 deletions app/config.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,34 @@
// global configuration for the backend
// holds all environment, commandline options, paths, etc.
//
// const config = require('./config');
//
const path = require('path');
const fs = require("fs");
const { CommandLine } = require('./util/cmdLine.js');

// helper to read environment variables
function env_default(field, def) {
return process.env[field] ?? def;
}

// helper to turn lines of quoted text into a single string
function lines(...lines) {
return lines.join("\n");
}

const cmdLine = new CommandLine({
db: lines(
`an optional string. can be 'sqlite' or 'mysql'.`,
`the behavior depends on context. for tests, defaults to sqlite. for prod and dev defaults to mysql.`,
`- 'mysql': requires docker. for prod and dev pulls its full configuration from the environment; tests have a hardcoded configuration.`,
`- 'sqlite': can followed by the path to its file. For example: '-db=sqlite:../bin/events.db'. That example path is its default.`,
),
db_debug: lines(
`an optional flag. adds extra debugging info for queries.`,
),
});

// node server listen for its http requests
// fix? there is no such environment variable right now.
const listen = env_default('NODE_PORT', 3080);
Expand All @@ -15,23 +39,26 @@ const siteHost = siteUrl(listen);
// location of app.js ( same as config.js )
const appPath = path.resolve(__dirname);

// for max file size
// for max image size
const bytesPerMeg = 1024*1024;

const staticFiles = env_default('SHIFT_STATIC_FILES');

const isTesting = !!(process.env.npm_lifecycle_event || "").match(/test$/);
// read the command line parameter for db configuration
const dbType = env_default('npm_config_db');
const dbDebug = !!env_default('npm_config_db_debug');
// running node --test with a glob ( ex. node --test **/*_test.js )
// injects all of those files onto the command line as separate arguments.
const isTesting = process.env.npm_lifecycle_event && process.env.npm_lifecycle_event.startsWith('test');
// ^ FIX: check for --test if running node directly?

const dbConfig = getDatabaseConfig(cmdLine.options.db, cmdLine.bool('db_debug'), isTesting);

// The config object exported
const config = {
appPath,
api: {
header: 'Api-Version',
version: "3.60.0",
},
db: getDatabaseConfig(dbType, isTesting),
db: dbConfig,
// maybe bad, but some code likes to know:
isTesting,
// a nodemailer friendly config, or false if smtp is not configured.
Expand Down Expand Up @@ -195,17 +222,27 @@ function getSmtpSettings() {
}
}

// our semi-agnostic database configuration
function getDatabaseConfig(dbType, isTesting) {
// dbType comes from the command-line
// if nothing was specfied, use the MYSQL_DATABASE environment variable
const env = env_default('MYSQL_DATABASE')
if (!dbType && env) {
dbType = env.startsWith("sqlite") ? env : null;
}
// nothing specified in the env fails
// sqlite specified in the env uses.... sqlite.
// anything other than sqlite in the env is assumed to be a mysql spec.
function readDbEnv(env = 'MYSQL_DATABASE') {
const dbType = env_default(env);
if (!dbType) {
dbType = isTesting ? 'sqlite' : 'mysql'
throw new Error(`expected db type on command line or '${env}' env variable`);
}
return dbType.startsWith("sqlite") ? dbType : 'mysql';
}

// returns our semi-agnostic database configuration.
// read the command line and environment for the desired db.
//
// dbType can be 'sqlite' or 'mysql' or none.
// 'mysql' pulls its full configuration from the environment.
// 'sqlite' can optionally followed by the path to its file.
// testing defaults to sqlite; dev and prod fallback to the environment.
//
function getDatabaseConfig(dbType, dbDebug, isTesting) {
dbType = dbType || (isTesting ? 'sqlite' : readDbEnv());
const [name, parts] = dbType.split(':');
const config = {
mysql: !isTesting ? getMysqlDefault : getMysqlTesting,
Expand Down
5 changes: 4 additions & 1 deletion app/db.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ const dt = require("./util/dateTime");
const dbConfig = unpackConfig(config.db);
const useSqlite = config.db.type === 'sqlite';
const dropOnCreate = config.db.connect?.name === 'shift_test';
let prevSetup;

const db = {
config: config.db,
Expand All @@ -19,10 +20,11 @@ const db = {
// waits to open a connection.
async initialize(name) {
if (db.query) {
throw new Error("db already initialized");
throw new Error(`db being initialized by ${name} when already initialized by ${this.initialized}.`);
}
const connection = knex(dbConfig);
db.query = connection;
db.initialized = name;
await connection;
},

Expand All @@ -33,6 +35,7 @@ const db = {
throw new Error("db already destroyed");
}
db.query = false;
db.initialized = false;
return connection.destroy();
},

Expand Down
2 changes: 1 addition & 1 deletion app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
},
"scripts": {
"start": "node app.js",
"test": "node --test --trace-warnings --test-global-setup=test/db_setup.js --test-concurrency=1 **/*_test.js --"
"test": "node ./test/testRunner.js"
},
"devDependencies": {
"shelljs": "^0.10.0",
Expand Down
42 changes: 22 additions & 20 deletions app/test/db_setup.js
Original file line number Diff line number Diff line change
@@ -1,47 +1,43 @@
// Configures mysql while testing.
//
// > npm test -db=mysql -db_debug=true
//
const shell = require( 'shelljs'); // cross-platform shell interaction
const { setTimeout } = require('node:timers/promises');
const db = require('../db');
const shell = require('shelljs'); // talks to docker in a cross-platform way.
const { setTimeout } = require('node:timers/promises'); // mysql takes time to start.
const db = require('../db');

// re: Unknown cli config "--db". This will stop working in the next major version of npm.
// the alternative is ugly; tbd but would rather wait till it breaks.
// const custom = process.argv.indexOf("--");
// if (custom >= 0) {
// console.log(process.argv.slice(custom+1));
// }
const dockerImage = `mysql:8.4.7`; // fix? pull from env somewhere.

function shutdownMysql() {
console.log(`shutting down up docker mysql...`);
console.log(`shutting down up docker...`);
const code = shell.exec("docker stop test_shift", {silent: true}).code;
console.log(`docker shutdown ${code}.`);
console.log(`docker shutdown ${code === 0 ? 'successfully' : code}.`);
}

async function startupMysql(connect) {
console.log(`setting up docker mysql...`);
console.log(`setting up docker image ${dockerImage}...`);
// ex. if a test failed and the earlier run didn't exit well.
const alreadyExists = shell.exec("docker start test_shift", {silent: true}).code === 0;
if (alreadyExists) {
// maybe an earlier test failed in some way.
console.log("docker test_shift already running. reusing the container.");
}

// setup docker mysql
// setup docker mysql for testing
// https://dev.mysql.com/doc/refman/8.4/en/docker-mysql-more-topics.html
// https://docs.docker.com/reference/cli/docker/container/run/
// https://hub.docker.com/_/mysql/
if (!alreadyExists && shell.exec(lines(
if (!alreadyExists && shell.exec(join(
`docker run`,
`--name test_shift`, // container name
`--detach`, // keep running the container in the background
`--rm` , // cleanup the container after exit
`-p ${connect.port}:3306`, // expose mysql's internal 3306 port as our custom port
`-p ${connect.port}:3306`, // expose mysql's internal 3306 port as our custom port
`-e MYSQL_RANDOM_ROOT_PASSWORD=true`, // alt: MYSQL_ROOT_PASSWORD
`-e MYSQL_DATABASE=${connect.name}`,
`-e MYSQL_USER=${connect.user}`,
`-e MYSQL_PASSWORD=${connect.pass}`,
`mysql:8.4.7`, // fix? pull from env somewhere.
dockerImage,
`--disable-log-bin=true`,
`--character-set-server=utf8mb4`,
`--collation-server=utf8mb4_unicode_ci`)).code !== 0) {
Expand All @@ -51,7 +47,7 @@ async function startupMysql(connect) {
}

async function initConnection() {
// configure the connection
// configure a connection
await db.initialize("test setup");

// wait for an empty query to succeed.
Expand All @@ -70,25 +66,31 @@ async function initConnection() {
console.log(`waiting ${i} seconds....`);
await setTimeout(i * 1000);
}

await db.destroy();
}

// helpers
// helper to read environment variables
function env_default(field, def) {
return process.env[field] ?? def;
}
function lines(...lines) {

// helper to turn lines of quoted text into a single string
function join(...lines) {
return lines.join(" ");
}

// when testing, called once before any tests start
async function globalSetup() {
console.log("global setup using: ", JSON.stringify(db.config, null, " "));
if (db.config.type === 'mysql') {
await startupMysql(db.config.connect);
}
await initConnection();
}

// when testing, called once after all tests have completed
async function globalTeardown() {
await db.destroy();
if (db.config.type === 'mysql') {
shutdownMysql();
}
Expand Down
Loading