From 5fcd007fabc77fd3f0a76fa10cc4e0a42b87cde8 Mon Sep 17 00:00:00 2001 From: Artur Dudnik Date: Thu, 28 Nov 2019 02:26:00 +0300 Subject: [PATCH] add first api tests and new environment --- bin/dev.js | 13 +++ bin/server.js | 17 ++++ dev.server.js | 91 ++++++++++++++++++++ jest.config.js | 7 +- jest.init.js | 2 + jest.transform.js | 3 + package.json | 35 ++++++-- src/client/constants.js | 8 +- src/client/main.js | 1 + server.js => src/server/index.js | 72 ++++------------ src/server/libs/utils.js | 4 +- src/server/models/CompanyToken.js | 10 +-- src/server/routes/api-v2.js | 17 ++-- tests/api.test.js | 52 ++++++----- webpack.config.babel.js => webpack.config.js | 9 +- 15 files changed, 242 insertions(+), 99 deletions(-) create mode 100644 bin/dev.js create mode 100644 bin/server.js create mode 100644 dev.server.js create mode 100644 jest.init.js create mode 100644 jest.transform.js rename server.js => src/server/index.js (52%) rename webpack.config.babel.js => webpack.config.js (93%) diff --git a/bin/dev.js b/bin/dev.js new file mode 100644 index 0000000..184c5a0 --- /dev/null +++ b/bin/dev.js @@ -0,0 +1,13 @@ +#!/usr/bin/env node +'use strict'; + +if (!require('piping')({ + hook: true, + ignore: /(\/\.|~$|\.json$)/i, +})) { + return; +} + +require('@babel/polyfill/noConflict'); +require('@babel/register')(); +require('../dev.server'); diff --git a/bin/server.js b/bin/server.js new file mode 100644 index 0000000..70aee12 --- /dev/null +++ b/bin/server.js @@ -0,0 +1,17 @@ +#!/usr/bin/env node +'use strict'; + +const isProduction = process.env.NODE_ENV === 'production'; + +if (!isProduction) { + if (!require('piping')({ + hook: true, + ignore: /(\/\.|~$|\.json$)/i, + })) { + return; + } +} + +require('@babel/polyfill/noConflict'); +require('@babel/register')(); +require('../src/server'); diff --git a/dev.server.js b/dev.server.js new file mode 100644 index 0000000..0e98fe2 --- /dev/null +++ b/dev.server.js @@ -0,0 +1,91 @@ +import path from 'path'; +import express from 'express'; +import webpack from 'webpack'; +import webpackDevMiddleware from 'webpack-dev-middleware'; +import webpackHotMiddleware from 'webpack-hot-middleware'; +import httpProxy from 'http-proxy'; + +import webpackConfig from './webpack.config'; + +const devPort = process.env.DEV_PORT || 8080; +const port = process.env.DEV_PORT || 9000; +const app = express(); +const compiler = webpack(webpackConfig); +const make = (apiAddress) => { + const proxy = apiAddress + ? httpProxy.createProxyServer({ + target: apiAddress, + }) : httpProxy.createProxyServer(); + + proxy.on('error', (error, req, res) => { + if (error.code !== 'ECONNRESET') { + console.error('proxy error', error); + } + if (!res.headersSent) { + res.writeHead(500, { 'content-type': 'application/json' }); + } + + res.status(500).json({ error: 'proxy_error', reason: error.message }); + }); + + return proxy; +}; +const register = (app, proxy, path, apiAddress) => { + console.log(`Server ${app.name} will proxy ${path} to ${apiAddress}`); + + app.use(path, (req, res) => { + proxy.web(req, res, { + target: apiAddress, + }); + }); +}; +const middleware = [ + webpackDevMiddleware(compiler, { + port: devPort, + contentBase: path.join(__dirname, 'src', 'client'), + hot: true, + stats: { + colors: true, + }, + compress: true, + }), + webpackHotMiddleware(compiler, { + // eslint-disable-next-line no-console + log: console.log, + heartbeat: 2000, + path: '/__webpack_hmr', + }), +]; + +app.use(middleware); + +[ + { address: `http://localhost:${port}/v1`, path: '/v1' }, + { address: `http://localhost:${port}/v2`, path: '/v2' }, +].forEach(cfg => { + const proxy = make(cfg.address); + app.on('stop', () => proxy.close()); + register(app, proxy, cfg.path, cfg.address); +}); + +app.get('*', (req, res, next) => { + const filename = path.join(compiler.outputPath, 'index.html'); + compiler.outputFileSystem.readFile(filename, (err, result) => { + if (err) { + return next(err); + } + res.set('content-type', 'text/html'); + res.send(result); + res.end(); + }); +}); + +app.listen(devPort, () => { + console.log('Developer Server | port: %s', devPort); +}); + +process + .on('dev server Uncaught Exception', (err) => { + // eslint-disable-next-line no-console + console.error(' Exception %s: ', err.message, err.stack); + }); diff --git a/jest.config.js b/jest.config.js index 9f73032..7f366d2 100644 --- a/jest.config.js +++ b/jest.config.js @@ -1,10 +1,10 @@ const { defaults } = require('jest-config'); module.exports = { - // rootDir: 'src/server', + // rootDir: './', transform: { ...defaults.transform, - '^.+\\.[t|j]sx?$': 'babel-jest', + '^.+\\.[t|j]sx?$': '/jest.transform.js', }, moduleFileExtensions: [ 'js', @@ -15,5 +15,8 @@ module.exports = { defaults.coveragePathIgnorePatterns, [] ), + setupFiles: [ + '/jest.init.js', + ], verbose: true, }; diff --git a/jest.init.js b/jest.init.js new file mode 100644 index 0000000..dc8d976 --- /dev/null +++ b/jest.init.js @@ -0,0 +1,2 @@ +require('@babel/polyfill/noConflict'); +require('@babel/register')(); diff --git a/jest.transform.js b/jest.transform.js new file mode 100644 index 0000000..53f4471 --- /dev/null +++ b/jest.transform.js @@ -0,0 +1,3 @@ +const babelConfig = require('./babel.config'); + +module.exports = require('babel-jest').createTransformer(babelConfig); diff --git a/package.json b/package.json index 3ecf433..b60cd32 100644 --- a/package.json +++ b/package.json @@ -5,9 +5,11 @@ "main": "index.js", "scripts": { "lint": "eslint src/ && flow", - "build": "webpack", - "start": "babel-node server.js", - "server": "babel-node server.js", + "build": "NODE_ENV=production webpack", + "start": "better-npm-run start", + "dev": "better-npm-run dev", + "test": "jest", + "server": "node server.js", "heroku-postbuild": "rm -rf build/ && webpack -p --progress", "migrate": "babel-node ./src/server/database/migrate.js" }, @@ -17,6 +19,25 @@ "author": "Chris Scott, Transistor Software", "license": "MIT", "repository": "https://github.com/transistorsoft/background-geolocation-console", + "betterScripts": { + "dev": { + "command": "concurrently --kill-others \"BABEL_ENV=node node --inspect=9229 ./bin/server.js\" \"BABEL_ENV=webpack node ./bin/dev.js\"", + "env": { + "TZ": "UTC", + "NODE_PATH": "./src/server/", + "DYNO": "1" + } + }, + "start": { + "command": "node ./bin/server.js", + "env": { + "TZ": "UTC", + "NODE_PATH": "./src/server/", + "DYNO": "1", + "NODE_ENV": "production" + } + } + }, "dependencies": { "@babel/cli": "^7.6.0", "@babel/node": "^7.6.1", @@ -41,6 +62,7 @@ "babel-plugin-transform-require-ignore": "0.0.2", "bluebird": "^3.7.1", "body-parser": "^1.17.1", + "better-npm-run": "^0.1.1", "classnames": "^2.2.5", "colors": "^1.1.2", "compression": "^1.6.2", @@ -74,7 +96,9 @@ "request": "^2.81.0", "reselect": "^3.0.1", "rsa-key-gen": "^0.6.1", - "sequelize": "^5.2.7" + "sequelize": "^5.2.7", + "webpack": "^4.39.3", + "webpack-cli": "^3.3.8" }, "devDependencies": { "@babel/core": "^7.7.2", @@ -83,6 +107,7 @@ "babel-jest": "^24.9.0", "chai": "^4.2.0", "chai-http": "^4.3.0", + "concurrently": "^5.0.0", "copy-webpack-plugin": "^5.0.4", "css-loader": "^0.26.1", "ejs-loader": "^0.3.0", @@ -118,8 +143,6 @@ "style-loader": "^0.13.1", "uglifyjs-webpack-plugin": "^2.2.0", "url-loader": "^2.1.0", - "webpack": "^4.39.3", - "webpack-cli": "^3.3.8", "webpack-dev-middleware": "^3.7.1", "webpack-dev-server": "^3.8.0", "webpack-hot-middleware": "^2.25.0" diff --git a/src/client/constants.js b/src/client/constants.js index 65c5962..c291756 100644 --- a/src/client/constants.js +++ b/src/client/constants.js @@ -1,4 +1,10 @@ -export const API_URL = window.location.origin; +const isProduction = process.env.NODE_ENV === 'production'; + +export const API_URI = isProduction + ? '' + : '/v1'; + +export const API_URL = window.location.origin + API_URI; // Colors export const COLORS = { diff --git a/src/client/main.js b/src/client/main.js index 2964525..175c67c 100644 --- a/src/client/main.js +++ b/src/client/main.js @@ -13,6 +13,7 @@ import WrappedViewport from './components/WrappedViewport'; // view in browser at same url. This is incorrect. const path = window.location.pathname; const pathQuery = path.match(/^\/locations\/(.*)$/); + if (pathQuery) { // Redirect /locations/username -> /username window.location.pathname = pathQuery[1]; diff --git a/server.js b/src/server/index.js similarity index 52% rename from server.js rename to src/server/index.js index d8feb96..28e7f20 100644 --- a/server.js +++ b/src/server/index.js @@ -1,30 +1,21 @@ -import initializeDatabase from './src/server/database/initializeDatabase'; +import initializeDatabase from './database/initializeDatabase'; import express from 'express'; import morgan from 'morgan'; import bodyParser from 'body-parser'; -import path from 'path'; +import { resolve, extname } from 'path'; import compress from 'compression'; import 'colors'; import opn from 'opn'; -import request from 'request'; -import obsoleteApi from './src/server/routes/obsolete-api'; -import { makeKeys } from './src/server/libs/jwt'; -import api from './src/server/routes/api-v2'; +import obsoleteApi from './routes/obsolete-api'; +import { makeKeys } from './libs/jwt'; +import api from './routes/api-v2'; const isProduction = process.env.NODE_ENV === 'production'; - -// if (!isProduction) { -// if (!require('piping')({ -// hook: true, -// ignore: /(\/\.|~$|\.json$)/i, -// })) { -// return; -// } -// } - -const app = express(); const port = process.env.PORT || 9000; +const dyno = process.env.DYNO; +const app = express(); +const buildPath = resolve(__dirname, '..', '..', 'build'); process .on('uncaughtException', (err) => { @@ -35,7 +26,7 @@ process process .on('message', (msg) => { // eslint-disable-next-line no-console - console.log('Server %s process.on( message = %s )', this.name, msg); + console.log('Server %s process.on( message = %s )', msg); }); (async function () { @@ -50,46 +41,19 @@ process // default old api app.use(obsoleteApi); + app.use('/v1', obsoleteApi); // v2 with jwt auth support app.use('/v2', api); if (isProduction) { - app.use(express.static('./build')); - } else { - console.info('adding webpack'); - const webpack = require('webpack'); - const webpackDevMiddleware = require('webpack-dev-middleware'); - const webpackHotMiddleware = require('webpack-hot-middleware'); - const webpackConfig = require('./webpack.config.babel'); - const compiler = webpack(webpackConfig); - - const middleware = [ - webpackDevMiddleware(compiler, { - publicPath: '/', // Same as `output.publicPath` in most cases. - contentBase: path.join(__dirname, 'src', 'client'), - hot: true, - stats: { - colors: true, - }, - }), - webpackHotMiddleware(compiler, { - log: console.log, // eslint-disable-line no-console - heartbeat: 2000, - path: '/__webpack_hmr', - }), - ]; - - app.use(middleware); + app.use(express.static(buildPath)); } app.use((req, res, next) => { - var ext = path.extname(req.url); - console.info(ext, req.url); - if ((ext === '' || ext === '.html') && req.url !== '/') { - console.info('returning the index.html here'); - console.info(req.url); - console.info(`http://localhost:${port}/`); - req.pipe(request(`http://localhost:${port}/`)).pipe(res); + var ext = extname(req.url); + console.info(ext, isProduction, resolve(buildPath, 'index.html'), (ext === '' || ext === '.html') && req.url !== '/', req.url); + if ((!ext || ext === '.html') && req.url !== '/') { + res.sendFile(resolve(buildPath, 'index.html')); } else { next(); } @@ -99,15 +63,17 @@ process }); }); - app.listen(port, function () { + app.listen(port, () => { console.log('╔═══════════════════════════════════════════════════════════'.green.bold); console.log('║ Background Geolocation Server | port: %s'.green.bold, port); console.log('╚═══════════════════════════════════════════════════════════'.green.bold); // Spawning dedicated process on opened port.. only if not deployed on heroku - if (!process.env.DYNO) { + if (!dyno) { opn(`http://localhost:${port}`) .catch(error => console.log('Optional site open failed:', error)); } }); })(); + +module.exports = app; diff --git a/src/server/libs/utils.js b/src/server/libs/utils.js index bb63561..baec140 100644 --- a/src/server/libs/utils.js +++ b/src/server/libs/utils.js @@ -91,8 +91,8 @@ export const checkAuth = (req, res, next) => { } }; -export const checkCompany = ({ companyToken, model }) => { - if (isDeniedCompany(companyToken)) { +export const checkCompany = ({ org, model }) => { + if (isDeniedCompany(org)) { throw new AccessDeniedError( 'This is a question from the CEO of Transistor Software.\n' + 'Why are you spamming my demo server1/v2?\n' + diff --git a/src/server/models/CompanyToken.js b/src/server/models/CompanyToken.js index 0897fdd..b911b2b 100644 --- a/src/server/models/CompanyToken.js +++ b/src/server/models/CompanyToken.js @@ -4,7 +4,7 @@ import { } from '../libs/utils'; import CompanyModel from '../database/CompanyModel'; -export async function getCompanyTokens ({ company_token: companyToken }) { +export async function getCompanyTokens ({ company_token: org }) { if (!filterByCompany) { return [ { @@ -13,7 +13,7 @@ export async function getCompanyTokens ({ company_token: companyToken }) { }, ]; } - const whereConditions = isAdmin(companyToken) ? {} : { company_token: companyToken }; + const whereConditions = isAdmin(org) ? {} : { company_token: org }; const result = await CompanyModel.findAll({ where: whereConditions, attributes: ['id', 'company_token'], @@ -24,11 +24,11 @@ export async function getCompanyTokens ({ company_token: companyToken }) { return result; } -export async function findOrCreate ({ company_token: companyToken }) { +export async function findOrCreate ({ company_token: org }) { const now = new Date(); const [company] = await CompanyModel.findOrCreate({ - where: { company_token: companyToken }, - defaults: { created_at: now, company_token: companyToken, updated_at: now }, + where: { company_token: org }, + defaults: { created_at: now, company_token: org, updated_at: now }, raw: true, }); return company; diff --git a/src/server/routes/api-v2.js b/src/server/routes/api-v2.js index 8c1f18b..b62925f 100644 --- a/src/server/routes/api-v2.js +++ b/src/server/routes/api-v2.js @@ -25,15 +25,15 @@ const router = new Router(); // -H 'Content-Type: application/json' router.post('/register', async function (req, res) { const { - org: org, - uuid: uuid, - model: model, + org, + uuid, + model, framework = null, version = null, } = req.body; const jwtInfo = { - org: org, + org, deviceUuid: uuid, model: model, }; @@ -62,8 +62,9 @@ router.post('/register', async function (req, res) { return res.send({ accessToken: jwt, - renewalToken: null, // TODO - expires: null // TODO + // TODO + renewalToken: null, + expires: null, }); } catch (err) { @@ -78,9 +79,9 @@ router.post('/register', async function (req, res) { // -H 'Authorization: Bearer ey...Pg' // router.get('/company_tokens', checkAuth, async function (req, res) { - const { company: companyToken } = req.jwt; + const { org } = req.jwt; try { - const companyTokens = await getCompanyTokens({ company_token: companyToken }); + const companyTokens = await getCompanyTokens({ company_token: org }); res.send(companyTokens); } catch (err) { console.error('/company_tokens', err); diff --git a/tests/api.test.js b/tests/api.test.js index d953f74..ecb9b67 100644 --- a/tests/api.test.js +++ b/tests/api.test.js @@ -1,43 +1,55 @@ +/* eslint-disable no-unused-expressions */ import chai from 'chai'; import chaiHttp from 'chai-http'; -import app from '../server'; chai.use(chaiHttp); chai.should(); +const { expect, assert } = chai; +const server = 'http://localhost:9000'; +let token; + describe('api v2', () => { - describe('/register', (done) => { - chai.request(app) - .post('/register') + test('/register', (done) => { + chai.request(server) + .post('/v2/register') .send({ - company_token: 'test', - device_id: 'test', - device_model: 'test', + org: 'test', + uuid: 'test', + model: 'test', framework: 'test', version: '10', }) .end((err, res) => { - console.log('register', err, res); + ({ accessToken: token } = res.body); expect(res).have.status(200); - expect(res).to.be.json(); - expect(res.body).toMatchObject({ - device: expect.objectContaining({ id: expect.any(Number) }), - jwt: expect.any(String), - }); - expect(err).toBeNull(); + expect(res).to.be.json; + expect(res.body).to.have.property('accessToken').to.be.a('string'); + expect(err).to.be.null; done(); }); }); - describe('/company_tokens', (done) => { - chai.request(app) - .get('/company_tokens') + test('/company_tokens', (done) => { + chai.request(server) + .get('/v2/company_tokens') .set('Authorization', 'Bearer !!!') .end((err, res) => { - console.log('company_tokens', err, res); expect(res).have.status(403); - expect(res).to.be.json(); - expect(err).toBeNull(); + expect(res).to.be.json; + expect(err).to.be.null; + done(); + }); + }); + + test('/company_tokens', (done) => { + chai.request(server) + .get('/v2/company_tokens') + .set('Authorization', 'Bearer ' + token) + .end((err, res) => { + expect(res).have.status(200); + expect(res).to.be.json; + expect(err).to.be.null; done(); }); }); diff --git a/webpack.config.babel.js b/webpack.config.js similarity index 93% rename from webpack.config.babel.js rename to webpack.config.js index 985def2..1de9e79 100644 --- a/webpack.config.babel.js +++ b/webpack.config.js @@ -9,6 +9,7 @@ const copyAssets = new CopyPlugin([ { from: 'assets/images', to: 'images' }, ]); const isProduction = process.env.NODE_ENV === 'production'; + const htmlWebpackPlugin = new HtmlWebpackPlugin({ template: 'index.ejs', inject: true, @@ -30,9 +31,9 @@ const htmlWebpackPlugin = new HtmlWebpackPlugin({ GOOGLE_MAPS_API_KEY: process.env.GOOGLE_MAPS_API_KEY, }); -module.exports = { +const config = { context: path.resolve(__dirname, 'src', 'client'), - devtool: 'source-map', + devtool: !isProduction ? 'cheap-module-eval-source-map' : 'source-map', target: 'web', entry: isProduction ? [ @@ -126,6 +127,7 @@ module.exports = { ? [ new webpack.DefinePlugin({ 'process.env.SHARED_DASHBOARD': !!process.env.SHARED_DASHBOARD || '', + 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV), }), new webpack.LoaderOptionsPlugin({ minimize: true, @@ -138,9 +140,12 @@ module.exports = { new webpack.NamedModulesPlugin(), new webpack.DefinePlugin({ 'process.env.SHARED_DASHBOARD': !!process.env.SHARED_DASHBOARD || '', + 'process.env.NODE_ENV': JSON.stringify(process.env.NODE_ENV || 'development'), }), copyAssets, new webpack.HotModuleReplacementPlugin(), htmlWebpackPlugin, ], }; + +module.exports = config;