big refactor - move into src/worker and src/server

pull/4/head
Cameron Clough 2022-03-21 10:54:50 +00:00
parent 2922b16918
commit bff3f3ca5e
No known key found for this signature in database
GPG Key ID: BFB3B74B026ED43F
56 changed files with 293 additions and 325 deletions

View File

@ -11,4 +11,4 @@ RUN npm ci
COPY . .
EXPOSE 3000
CMD ["node", "-r", "esm", "server.js"]
CMD ["node", "-r", "esm", "src/server"]

View File

@ -7,8 +7,8 @@ If you don't want to host your own instance, check out https://api.retropilot.or
The server consists of 2 node scripts.
`server.js` is using expressjs and runs the backend (file upload / communication with openpilot) and the useradmin dashboard to manage / view / download drives & logs.
`worker.js` is a background worker that is processing drives (analyzing video files & logs) to prepare drives for playback in cabana and to gather statistics. It automatically terminates itself after 60 minutes to make sure the video/log libraries do not cause memory leaks.
`src/server` is using expressjs and runs the backend (file upload / communication with openpilot) and the useradmin dashboard to manage / view / download drives & logs.
`src/worker` is a background worker that is processing drives (analyzing video files & logs) to prepare drives for playback in cabana and to gather statistics. It automatically terminates itself after 60 minutes to make sure the video/log libraries do not cause memory leaks.
Both scripts can be started with a cronjob each minute, they use locking to make sure they run exclusively.
@ -27,15 +27,15 @@ cp database.empty.sqlite database.sqlite
### [Server] Running
```
node -r esm server.js
node -r esm src/server
```
```
node -r esm worker.js
node -r esm src/worker
```
### [Server] CABANA Support
A compiled version of a custom cabana fork (https://github.com/florianbrede-ayet/retropilot-cabana) is directly bundled in the `cabana/` subdirectory and will be served by the express app. After starting `server.js`, cabana is ready to use.
A compiled version of a custom cabana fork (https://github.com/florianbrede-ayet/retropilot-cabana) is directly bundled in the `cabana/` subdirectory and will be served by the express app. After starting `index.js`, cabana is ready to use.
-----
@ -96,4 +96,4 @@ The athena websockets interface is not implemented yet, so the comma app and ath
Launch with:
```
docker-compose -f docker-compose.yml -f docker-compose.uat.yml up -d
```
```

View File

@ -12,7 +12,7 @@ services:
- "4040:4040"
worker:
build: .
command: node -r esm worker.js
command: node -r esm src/worker
restart: unless-stopped
depends_on:
- db

View File

@ -1,9 +1,9 @@
module.exports = [{
name: 'server',
script: 'server.js',
script: 'src/server',
node_args: '-r esm',
}, {
name: 'worker',
script: 'worker.js',
script: 'src/worker',
node_args: '-r esm',
}];

View File

@ -2,10 +2,10 @@
"name": "retropilot-server",
"version": "1.0.0",
"description": "replacement for comma.ai backend and useradmin dashboard. can be combined with a modified cabana instance.",
"main": "server.js",
"main": "src/server/index.js",
"scripts": {
"test": "mocha",
"start": "node --es-module-specifier-resolution=node server.js",
"start": "node -r esm src/server",
"lint": "eslint . --ext .js",
"lint:fix": "eslint . --ext .js --fix"
},

129
server.js
View File

@ -1,129 +0,0 @@
/* eslint-disable global-require */
import 'dotenv/config'
import log4js from 'log4js';
import lockfile from 'proper-lockfile';
import http from 'http';
import express from 'express';
import cors from 'cors';
import rateLimit from 'express-rate-limit';
import cookieParser from 'cookie-parser';
import storageController from './controllers/storage.js';
/* eslint-disable no-unused-vars */
import athena from './websocket/athena/index.js';
import routers from './routes/index.js';
import controllers from './controllers/index.js';
/* eslint-enable no-unused-vars */
import { fileURLToPath } from 'url';
import { dirname } from 'path';
process.on('unhandledRejection', (error, p) => {
console.log('=== UNHANDLED REJECTION ===');
console.log(error.promise, p);
console.dir(error.stack);
});
log4js.configure({
appenders: { logfile: { type: 'file', filename: 'server.log' }, out: { type: 'console' } /* {type: "file", filename: "server1.log"} */ },
categories: { default: { appenders: ['out', 'logfile'], level: 'info' } },
});
const logger = log4js.getLogger('default');
// TODO evaluate if this is the best way to determine the root of project
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
global.__basedir = __dirname;
function runAsyncWrapper(callback) {
return function wrapper(req, res, next) {
callback(req, res, next)
.catch(next);
};
}
const web = async () => {
const app = express();
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', `${process.env.BASE_URL}`);
res.header('Access-Control-Allow-Credentials', true);
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
next();
});
storageController.initializeStorage();
await storageController.updateTotalStorageUsed();
app.use(routers.api);
app.use(routers.useradmin);
app.use(routers.authenticationApi);
if (process.env.ATHENA_ENABLED) {
const athenaRateLimit = rateLimit({
windowMs: 30000,
max: process.env.ATHENA_API_RATE_LIMIT,
});
app.use((req, res, next) => {
req.athenaWebsocketTemp = athena;
return next();
});
app.use('/admin', routers.admin);
app.use('/realtime', athenaRateLimit);
app.use('/realtime', routers.realtime);
//app.use(routers.oauthAuthenticator)
} else {
logger.log('Athena disabled');
}
app.use(cors({ origin: 'http://localhost:3000' }));
app.use(cookieParser());
app.use('/favicon.ico', express.static('static/favicon.ico'));
app.use(process.env.BASE_DRIVE_DOWNLOAD_PATH_MAPPING, express.static(process.env.STORAGE_PATH));
app.use(routers.deviceApi);
app.use('/.well-known', express.static('.well-known'));
app.use('/cabana', express.static('cabana/'));
app.get('/', async (req, res) => {
res.redirect('/useradmin')
});
app.get('*', runAsyncWrapper(async (req, res) => {
logger.error(`HTTP.GET unhandled request: ${controllers.helpers.simpleStringify(req)}, ${controllers.helpers.simpleStringify(res)}`);
res.status(404);
res.send('Not Implemented');
}));
app.post('*', runAsyncWrapper(async (req, res) => {
logger.error(`HTTP.POST unhandled request: ${controllers.helpers.simpleStringify(req)}, ${controllers.helpers.simpleStringify(res)}`);
res.status(404);
res.send('Not Implemented');
}));
return app;
};
lockfile.lock('retropilot_server', { realpath: false, stale: 30000, update: 2000 })
.then(async () => {
console.log('STARTING SERVER...');
const app = await web();
const httpServer = http.createServer(app);
httpServer.listen(process.env.HTTP_PORT, () => {
logger.info(`RetroPilot Server listening at ${process.env.BASE_URL}`);
});
}).catch((e) => {
console.error(e);
process.exit();
});

84
src/server/app.js 100644
View File

@ -0,0 +1,84 @@
import cookieParser from 'cookie-parser';
import cors from 'cors';
import express from 'express';
import rateLimit from 'express-rate-limit';
import log4js from 'log4js';
import storageController from './controllers/storage';
import athena from '../websocket/athena';
import controllers from './controllers';
import routers from './routes';
const logger = log4js.getLogger('default');
function runAsyncWrapper(callback) {
return function wrapper(req, res, next) {
callback(req, res, next)
.catch(next);
};
}
const tasks = [];
const app = express();
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', `${process.env.BASE_URL}`);
res.header('Access-Control-Allow-Credentials', true);
res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
next();
});
storageController.initializeStorage();
tasks.push(storageController.updateTotalStorageUsed());
app.use(routers.api);
app.use(routers.useradmin);
app.use(routers.authenticationApi);
if (process.env.ATHENA_ENABLED) {
const athenaRateLimit = rateLimit({
windowMs: 30000,
max: process.env.ATHENA_API_RATE_LIMIT,
});
app.use((req, res, next) => {
req.athenaWebsocketTemp = athena;
return next();
});
app.use('/admin', routers.admin);
app.use('/realtime', athenaRateLimit);
app.use('/realtime', routers.realtime);
// app.use(routers.oauthAuthenticator)
} else {
logger.log('Athena disabled');
}
app.use(cors({ origin: 'http://localhost:3000' }));
app.use(cookieParser());
app.use('/favicon.ico', express.static('static/favicon.ico'));
app.use(process.env.BASE_DRIVE_DOWNLOAD_PATH_MAPPING, express.static(process.env.STORAGE_PATH));
app.use(routers.deviceApi);
app.use('/.well-known', express.static('.well-known'));
app.use('/cabana', express.static('cabana/'));
app.get('/', async (req, res) => {
res.redirect('/useradmin');
});
app.get('*', runAsyncWrapper(async (req, res) => {
logger.error(`HTTP.GET unhandled request: ${controllers.helpers.simpleStringify(req)}, ${controllers.helpers.simpleStringify(res)}`);
res.status(404);
res.send('Not Implemented');
}));
app.post('*', runAsyncWrapper(async (req, res) => {
logger.error(`HTTP.POST unhandled request: ${controllers.helpers.simpleStringify(req)}, ${controllers.helpers.simpleStringify(res)}`);
res.status(404);
res.send('Not Implemented');
}));
export default Promise.all(tasks).then(() => app);

View File

@ -1,4 +1,4 @@
import orm from '../models/index.model';
import orm from '../../models/index.model';
// TODO move everythijng away from this dumb intertwined style

View File

@ -1,7 +1,7 @@
import crypto from 'crypto';
import jsonwebtoken from 'jsonwebtoken';
import log4js from 'log4js';
import orm from '../../models/index.model';
import orm from '../../../models/index.model';
const logger = log4js.getLogger('default');

View File

@ -6,7 +6,7 @@ import {
AUTH_2FA_ENROLLED,
AUTH_2FA_BAD_TOKEN,
} from '../../consistency/terms';
import orm from '../../models/index.model';
import orm from '../../../models/index.model';
export async function twoFactorOnboard(account) {
if (!account || !account.dataValues) { return { success: false, ...AUTH_2FA_BAD_ACCOUNT }; }

View File

@ -2,7 +2,7 @@ import sanitizeFactory from 'sanitize';
import crypto from 'crypto';
import dirTree from 'directory-tree';
import log4js from 'log4js';
import orm from '../models/index.model';
import orm from '../../models/index.model';
import { readJWT, validateJWT } from './authentication';
import { getAccountFromId } from './users';

View File

@ -1,6 +1,6 @@
import crypto from 'crypto';
import log4js from 'log4js';
import orm from '../models/index.model';
import orm from '../../models/index.model';
const logger = log4js.getLogger('default');

View File

@ -0,0 +1,36 @@
import 'dotenv/config';
import http from 'http';
import log4js from 'log4js';
import { dirname } from 'path';
import { fileURLToPath } from 'url';
log4js.configure({
appenders: { logfile: { type: 'file', filename: 'server.log' }, out: { type: 'console' } /* {type: "file", filename: "server1.log"} */ },
categories: { default: { appenders: ['out', 'logfile'], level: 'info' } },
});
process.on('unhandledRejection', (error, p) => {
console.log('=== UNHANDLED REJECTION ===');
console.log(error.promise, p);
console.dir(error.stack);
});
// TODO evaluate if this is the best way to determine the root of project
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
global.__basedir = __dirname;
const main = async () => {
const logger = log4js.getLogger('default');
const httpServer = http.createServer(await require('./app').default);
httpServer.listen(process.env.HTTP_PORT, () => {
logger.info(`RetroPilot Server listening at ${process.env.BASE_URL}`);
});
};
try {
main();
} catch (e) {
console.error(e);
}

View File

@ -5,8 +5,8 @@ import crypto from 'crypto';
import log4js from 'log4js';
import storageController from '../controllers/storage';
import deviceController from '../controllers/devices';
import authenticationController from './../controllers/authentication';
import userController from './../controllers/users';
import authenticationController from '../controllers/authentication';
import userController from '../controllers/users';
const logger = log4js.getLogger('default');
const router = express.Router();
@ -314,14 +314,14 @@ async function upload(req, res) {
})
await deviceController.updateOrCreateDriveSegment(dongleId, driveName, segment, {
duration: 0,
distance_meters: 0,
upload_complete: false,
is_processed: false,
is_stalled: false,
duration: 0,
distance_meters: 0,
upload_complete: false,
is_processed: false,
is_stalled: false,
created: Date.now()
})
logger.info(`HTTP.UPLOAD_URL updated existing drive: ${JSON.stringify(drive)}`);
}

View File

@ -4,7 +4,7 @@ import authenticationController from '../../controllers/authentication';
import userController from '../../controllers/users';
import deviceController from '../../controllers/devices';
import models from '../../models/index.model';
import models from '../../../models/index.model';
/* eslint-enable no-unused-vars */
const router = express.Router();
const whitelistParams = {

View File

@ -601,11 +601,11 @@ router.get('/useradmin/drive/:dongleId/:driveIdentifier', runAsyncWrapper(async
var qlog = '--';
var rlog = '--';
for (var c in directoryTree.children[i].children) {
if (directoryTree.children[i].children[c].name == 'fcamera.hevc') fcamera = '<a target="_blank" href="' + driveUrl + segment + '/' + directoryTree.children[i].children[c].name + '">' + directoryTree.children[i].children[c].name + '</a>';
if (directoryTree.children[i].children[c].name == 'dcamera.hevc') fcamera = '<a target="_blank" href="' + driveUrl + segment + '/' + directoryTree.children[i].children[c].name + '">' + directoryTree.children[i].children[c].name + '</a>';
if (directoryTree.children[i].children[c].name == 'qcamera.ts') qcamera = '<a target="_blank" href="' + driveUrl + segment + '/' + directoryTree.children[i].children[c].name + '">' + directoryTree.children[i].children[c].name + '</a>';
if (directoryTree.children[i].children[c].name == 'qlog.bz2') qlog = '<a target="_blank" href="' + driveUrl + segment + '/' + directoryTree.children[i].children[c].name + '">' + directoryTree.children[i].children[c].name + '</a>';
if (directoryTree.children[i].children[c].name == 'rlog.bz2') rlog = '<a target="_blank" href="' + driveUrl + segment + '/' + directoryTree.children[i].children[c].name + '">' + directoryTree.children[i].children[c].name + '</a>';
if (directoryTree.children[i].children[c].name == 'fcamera.hevc') fcamera = '' + driveUrl + segment + '<a target="_blank" href="/">' + directoryTree.children[i].children[c].name + '' + directoryTree.children[i].children[c].name + '</a>';
if (directoryTree.children[i].children[c].name == 'dcamera.hevc') fcamera = '' + driveUrl + segment + '<a target="_blank" href="/">' + directoryTree.children[i].children[c].name + '' + directoryTree.children[i].children[c].name + '</a>';
if (directoryTree.children[i].children[c].name == 'qcamera.ts') qcamera = '' + driveUrl + segment + '<a target="_blank" href="/">' + directoryTree.children[i].children[c].name + '' + directoryTree.children[i].children[c].name + '</a>';
if (directoryTree.children[i].children[c].name == 'qlog.bz2') qlog = '' + driveUrl + segment + '<a target="_blank" href="/">' + directoryTree.children[i].children[c].name + '' + directoryTree.children[i].children[c].name + '</a>';
if (directoryTree.children[i].children[c].name == 'rlog.bz2') rlog = '' + driveUrl + segment + '<a target="_blank" href="/">' + directoryTree.children[i].children[c].name + '' + directoryTree.children[i].children[c].name + '</a>';
}
var isProcessed = '?';

View File

@ -7,7 +7,7 @@ import log4js from 'log4js';
import models from '../../models/index.model';
import helperFunctions from './helpers';
import deviceController from '../../controllers/devices';
import deviceController from '../../server/controllers/devices';
const logger = log4js.getLogger('default');

View File

@ -1,8 +1,8 @@
// eslint-disable-next-line no-unused-vars
import authenticationController from '../../controllers/authentication';
import authenticationController from '../../server/controllers/authentication';
import deviceController from '../../controllers/devices';
import athenaRealtime from '../athena/index';
import deviceController from '../../server/controllers/devices';
import athenaRealtime from '../athena';
// Checks if device is currently online in Athena

View File

@ -1,4 +1,4 @@
import deviceController from '../../controllers/devices';
import deviceController from '../../server/controllers/devices';
let wss;

View File

@ -2,7 +2,7 @@ import { WebSocketServer } from 'ws';
import cookie from 'cookie';
import httpServer from 'http';
import log4js from 'log4js';
import authenticationController from '../../controllers/authentication';
import authenticationController from '../../server/controllers/authentication';
import athenaRealtime from '../athena';
import controlsFunction from './controls';

View File

@ -12,7 +12,7 @@ import Reader from '@commaai/log_reader';
import ffprobe from 'ffprobe';
import ffprobeStatic from 'ffprobe-static';
import orm from './models/index.model';
import orm from '../models/index.model';
let lastCleaningTime = 0;
let startTime = Date.now();

View File

@ -1,74 +1,69 @@
import request from 'supertest';
import dummyGenerator from './../dummyGenerator';
let app;
export default (app) => {
describe('/v2/pilotauth/ - Testing device registration', function () {
it('Returns dongle ID on valid registration', function (done) {
request(app)
.post('/v2/pilotauth/')
.query({
imei: dummyGenerator.getImei(),
serial: dummyGenerator.getSerial(),
public_key: dummyGenerator.devicePubKey,
register_token: dummyGenerator.makeJWT(),
})
export default (server) => {
app = server;
describe('/v2/pilotauth/ - Testing device registration', function() {
it('Returns dongle ID on valid registration', function(done) {
request(server)
.post('/v2/pilotauth/')
.query({
imei: dummyGenerator.getImei(),
serial: dummyGenerator.getSerial(),
public_key: dummyGenerator.devicePubKey,
register_token: dummyGenerator.makeJWT()
})
.set('Accept', 'application/x-www-form-urlencoded')
.expect('Content-Type', /json/)
.expect(200)
.expect((res) => {
if (!res.body.dongle_id) throw new Error("API Failed to return dongle_id on status 200")
})
.end(done)
});
it('Returns 400 when incorrect public key given', function(done) {
request(server)
.post('/v2/pilotauth/')
.query({
imei: dummyGenerator.getImei(),
serial: dummyGenerator.getSerial(),
public_key: dummyGenerator.rougePublicKey,
register_token: dummyGenerator.makeJWT()
})
.set('Accept', 'application/x-www-form-urlencoded')
.expect('Content-Type', /text/)
.expect(400)
.end(done)
});
it('Returns 400 when missing register_token', function(done) {
request(server)
.post('/v2/pilotauth/')
.query({
imei: dummyGenerator.getImei(),
serial: dummyGenerator.getSerial(),
public_key: dummyGenerator.rougePublicKey,
register_token: ""
})
.set('Accept', 'application/x-www-form-urlencoded')
.expect('Content-Type', /text/)
.expect(400)
.end(done)
});
it('Returns 400 when missing query', function(done) {
request(server)
.post('/v2/pilotauth/')
.set('Accept', 'application/x-www-form-urlencoded')
.expect('Content-Type', /text/)
.expect(400)
.end(done)
});
.set('Accept', 'application/x-www-form-urlencoded')
.expect('Content-Type', /json/)
.expect(200)
.expect((res) => {
if (!res.body.dongle_id) {
throw new Error('API Failed to return dongle_id on status 200');
}
})
.end(done);
});
it('Returns 400 when incorrect public key given', function (done) {
request(app)
.post('/v2/pilotauth/')
.query({
imei: dummyGenerator.getImei(),
serial: dummyGenerator.getSerial(),
public_key: dummyGenerator.rougePublicKey,
register_token: dummyGenerator.makeJWT(),
})
.set('Accept', 'application/x-www-form-urlencoded')
.expect('Content-Type', /text/)
.expect(400)
.end(done);
});
it('Returns 400 when missing register_token', function (done) {
request(app)
.post('/v2/pilotauth/')
.query({
imei: dummyGenerator.getImei(),
serial: dummyGenerator.getSerial(),
public_key: dummyGenerator.rougePublicKey,
register_token: '',
})
.set('Accept', 'application/x-www-form-urlencoded')
.expect('Content-Type', /text/)
.expect(400)
.end(done);
});
it('Returns 400 when missing query', function (done) {
request(app)
.post('/v2/pilotauth/')
.set('Accept', 'application/x-www-form-urlencoded')
.expect('Content-Type', /text/)
.expect(400)
.end(done);
});
});
};

View File

@ -1,46 +1,34 @@
import request from 'supertest';
import dummyGenerator from './../dummyGenerator';
let app;
export default (server) => {
app = server;
describe('/api', function() {
it('Load general server stats', function (done) {
request(server)
.get('/retropilot/0/useradmin')
.expect('Content-Type', /json/)
.expect(200)
.expect((req) => {
const body = req.body;
try {
if (
body.hasOwnProperty('success') && body.success === true &&
body.hasOwnProperty('data') &&
body.data.hasOwnProperty('serverStats') &&
body.data.serverStats.hasOwnProperty('config') &&
typeof body.data.serverStats.config.registerAllowed === "boolean" &&
typeof body.data.serverStats.process.env.WELCOME_MESSAGE === "string" &&
typeof body.data.serverStats['accounts'] === "number" &&
typeof body.data.serverStats['devices'] === "number" &&
typeof body.data.serverStats['drives'] === "number" &&
(typeof body.data.serverStats['storageUsed'] === "number" || body.data.serverStats['storageUsed'] === "Unsupported Platform"))
{
return true;
} else {
throw new Error('Invalid returned parameters in GET /retropilot/0/useradmin')
}
} catch (exception) {
throw new Error('Invalid returned parameters in GET /retropilot/0/useradmin ')
}
})
.end(done)
});
export default (app) => {
describe('/api', function () {
it('Load general app stats', function (done) {
request(app)
.get('/retropilot/0/useradmin')
.expect('Content-Type', /json/)
.expect(200)
.expect((req) => {
const body = req.body;
try {
if (
body.hasOwnProperty('success') && body.success === true &&
body.hasOwnProperty('data') &&
body.data.hasOwnProperty('appStats') &&
body.data.appStats.hasOwnProperty('config') &&
typeof body.data.appStats.config.registerAllowed === 'boolean' &&
typeof body.data.appStats.process.env.WELCOME_MESSAGE === 'string' &&
typeof body.data.appStats['accounts'] === 'number' &&
typeof body.data.appStats['devices'] === 'number' &&
typeof body.data.appStats['drives'] === 'number' &&
(typeof body.data.appStats['storageUsed'] === 'number' || body.data.appStats['storageUsed'] === 'Unsupported Platform')) {
return true;
}
} catch (ignored) {
}
throw new Error('Invalid returned parameters in GET /retropilot/0/useradmin ');
})
.end(done);
});
});
};

View File

@ -1,52 +1,46 @@
import request from 'supertest';
import dummyGenerator from './../dummyGenerator';
let app;
export default (server) => {
app = server;
describe('/useradmin', function() {
it('Page load', function (done) {
request(server)
.get('/useradmin')
.expect('Content-Type', /html/)
.expect(200)
.end(done)
});
it('Redirect on existing session', function(done) {
request(server)
.get('/useradmin')
// pull sessions from a store
.set('Cookie', ['session=s%3Aj%3A%7B%22account%22%3A%22adam%40adamblack.us%22%2C%22expires%22%3A1653171350726%7D.cRX19pNfx6mCGZ9ZYHcUIyy5CAQVMDgKrp%2F%2Bf7NFVYA;'])
.expect('Location', '/useradmin/overview')
.expect(302)
.end(done)
});
export default (app) => {
describe('/useradmin', function () {
it('Page load', function (done) {
request(app)
.get('/useradmin')
.expect('Content-Type', /html/)
.expect(200)
.end(done);
});
describe('/useradmin/register/token', function() {
it('No duplicate emails', function (done) {
request(server)
.post('/useradmin/register/token')
// TODO add dedicated DB/user account for tests to run on
.send(`email=${dummyGenerator.alreadyRegisteredEmail}`)
.set('Accept', 'application/x-www-form-urlencoded')
.expect('Location', `/useradmin/register?status=${encodeURIComponent('Email is already registered')}`)
.end(done)
});
it('Accepts new accounts', function (done) {
request(server)
.post('/useradmin/register/token')
// TODO add dedicated DB/user account for tests to run on
.send(`email=${dummyGenerator.newUserEmail}`)
.set('Accept', 'application/x-www-form-urlencoded')
.expect(200)
.end(done)
});
it('Redirect on existing session', function (done) {
request(app)
.get('/useradmin')
// pull sessions from a store
.set('Cookie', ['session=s%3Aj%3A%7B%22account%22%3A%22adam%40adamblack.us%22%2C%22expires%22%3A1653171350726%7D.cRX19pNfx6mCGZ9ZYHcUIyy5CAQVMDgKrp%2F%2Bf7NFVYA;'])
.expect('Location', '/useradmin/overview')
.expect(302)
.end(done);
});
});
describe('/useradmin/register/token', function () {
it('No duplicate emails', function (done) {
request(app)
.post('/useradmin/register/token')
// TODO add dedicated DB/user account for tests to run on
.send(`email=${dummyGenerator.alreadyRegisteredEmail}`)
.set('Accept', 'application/x-www-form-urlencoded')
.expect('Location', `/useradmin/register?status=${encodeURIComponent('Email is already registered')}`)
.end(done);
});
it('Accepts new accounts', function (done) {
request(app)
.post('/useradmin/register/token')
// TODO add dedicated DB/user account for tests to run on
.send(`email=${dummyGenerator.newUserEmail}`)
.set('Accept', 'application/x-www-form-urlencoded')
.expect(200)
.end(done);
});
});
};

View File

@ -1,20 +1,20 @@
import request from 'supertest';
import server from '../server';
import app from '../src/server/app';
// TODO better way to only run tests once server is up
describe('loading express', () => {
it('responds to /', (done) => {
request(server)
request(app)
.get('/')
.expect(200, done);
});
it('404 everything else', (done) => {
request(server)
request(app)
.get('/foo/bar')
.expect(404, done);
});
});
require('./routes/api.test')(server);
require('./routes/useradmin.test')(server);
if (process.env.USE_USER_ADMIN_API) require('./routes/userAdminApi.test')(server);
require('./routes/api.test')(app);
require('./routes/useradmin.test')(app);
if (process.env.USE_USER_ADMIN_API) require('./routes/userAdminApi.test')(app);