From 0967e733ad6e931f66c435a7248b92586b13de50 Mon Sep 17 00:00:00 2001 From: YingXue Date: Fri, 30 Aug 2019 14:10:37 -0700 Subject: [PATCH] test dataplane related server.ts (#101) * initial commit * Moved server build output to dist folder to avoid naming conflicts for tests --- .gitignore | 2 +- package.json | 4 +- public/electron.js | 2 +- src/server/dataPlaneHelper.spec.ts | 132 +++++++++++++++++++++++++++++ src/server/dataPlaneHelper.ts | 67 +++++++++++++++ src/server/server.ts | 47 +--------- 6 files changed, 207 insertions(+), 47 deletions(-) create mode 100644 src/server/dataPlaneHelper.spec.ts create mode 100644 src/server/dataPlaneHelper.ts diff --git a/.gitignore b/.gitignore index fb5ae4af5..ffd18b82d 100644 --- a/.gitignore +++ b/.gitignore @@ -344,7 +344,7 @@ scripts/composeLocalizationKeys.js jest-test-results.trx # server.js ( auto-generated from server.ts ) -src/server/server.js +src/server/*.js # js files compiles in the app directore used by server.ts src/app/**/*.js \ No newline at end of file diff --git a/package.json b/package.json index 69ebd3378..37dd623ae 100644 --- a/package.json +++ b/package.json @@ -48,9 +48,9 @@ "package:win": "npm run clean && npm install && npm rebuild node-sass && npm run build && electron-builder -w", "package:linux": "npm run clean:linux && npm install && npm rebuild node-sass && npm run build && electron-builder -l", "package:mac": "npm install && npm rebuild node-sass && npm run build && electron-builder -m", - "server:compile": "tsc ./src/server/server.ts --skipLibCheck --lib es2015 --inlineSourceMap", + "server:compile": "tsc ./src/server/server.ts --skipLibCheck --lib es2015 --inlineSourceMap --outDir ./dist/server/", "start": "concurrently \"npm run start:web\" \"npm run start:server\"", - "start:server": "npm run server:compile && nodemon --inspect ./src/server/server.js", + "start:server": "npm run server:compile && nodemon --inspect ./dist/server/server.js", "start:web": "npm run localization && webpack-dev-server --mode development --hot --open --port 3000 --host 127.0.0.1", "test": "npm run localization && jest --coverage", "test:debug": "node --inspect-brk ./node_modules/jest/bin/jest.js --runInBand -i --watch", diff --git a/public/electron.js b/public/electron.js index 07f4fd3ee..d3303d4b8 100644 --- a/public/electron.js +++ b/public/electron.js @@ -3,7 +3,7 @@ const electron = require('electron'); const app = electron.app; const Menu = electron.Menu; const BrowserWindow = electron.BrowserWindow; -const server = require('../src/server/server.js'); +const server = require('../dist/server/server.js'); const path = require('path'); const url = require('url'); diff --git a/src/server/dataPlaneHelper.spec.ts b/src/server/dataPlaneHelper.spec.ts new file mode 100644 index 000000000..f7add7dd7 --- /dev/null +++ b/src/server/dataPlaneHelper.spec.ts @@ -0,0 +1,132 @@ +/*********************************************************** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License + **********************************************************/ +import express = require('express'); +import { generateDataPlaneRequestBody, API_VERSION, processDataPlaneResponse } from './dataPlaneHelper'; // note: remove auto-generated dataPlaneHelper.js in order to run this test + +describe('server', () => { + const hostName = 'testHub.private.azure-devices-int.net'; + it('generates data plane request with API version and query string specified in body', () => { + const queryString = 'connectTimeoutInSeconds=20&responseTimeInSeconds=20'; + const req = { + body: { + apiVersion: '2019-07-01-preview', + hostName, + httpMethod: 'POST', + path: '/digitalTwins/testDevice/interfaces/sensor/commands/turnon', + queryString, + sharedAccessSignature: `SharedAccessSignature sr=${hostName}%2Fdevices%2Fquery&sig=123&se=456&skn=iothubowner` + } + }; + const requestBody = generateDataPlaneRequestBody(req as express.Request); + expect(requestBody).toEqual( + { + body: undefined, + headers: { + 'Accept': 'application/json', + 'Authorization': req.body.sharedAccessSignature, + 'Content-Type': 'application/json' + }, + method: 'POST', + uri: `https://${hostName}/%2FdigitalTwins%2FtestDevice%2Finterfaces%2Fsensor%2Fcommands%2Fturnon?${queryString}&api-version=2019-07-01-preview`, + } + ); + }); + + it('generates data plane request without API version or query string specified in body', () => { + const req = { + body: { + body: '{"query":"\\n SELECT deviceId as DeviceId,\\n status as Status,\\n FROM devices WHERE STARTSWITH(devices.deviceId, \'test\')"}', + headers: { 'x-ms-max-item-count': 20 }, + hostName, + httpMethod: 'POST', + path: 'devices/query', + sharedAccessSignature: `SharedAccessSignature sr=${hostName}%2Fdevices%2Fquery&sig=123&se=456&skn=iothubowner` + } + }; + const requestBody = generateDataPlaneRequestBody(req as express.Request); + expect(requestBody).toEqual( + { + body: '{"query":"\\n SELECT deviceId as DeviceId,\\n status as Status,\\n FROM devices WHERE STARTSWITH(devices.deviceId, \'test\')"}', + headers: { + 'Accept': 'application/json', + 'Authorization': req.body.sharedAccessSignature, + 'Content-Type': 'application/json', + 'x-ms-max-item-count': 20 + }, + method: 'POST', + uri: `https://${hostName}/devices%2Fquery?api-version=${API_VERSION}`, + } + ); + }); + + it('generates data plane response with success', () => { + // tslint:disable + const res: any = { + headers: { + 'content-length': '10319', + 'content-type': 'application/json; charset=utf-8', + vary: 'Origin', + server: 'Microsoft-HTTPAPI/2.0', + 'iothub-errorcode': 'ServerError', + date: 'Thu, 29 Aug 2019 00:49:10 GMT', + connection: 'close' + }, + statusCode: 200 + }; + // tslint:enable + const response = processDataPlaneResponse(res, null); + // tslint:disable-next-line:no-magic-numbers + expect(response.statusCode).toEqual(200); + expect(response.body).toEqual({body: null, headers: res.headers}); + }); + + it('generates data plane response with error', () => { + // tslint:disable + const res: any = { + headers: { + 'content-length': '10319', + 'content-type': 'application/json; charset=utf-8', + vary: 'Origin', + server: 'Microsoft-HTTPAPI/2.0', + 'iothub-errorcode': 'ServerError', + date: 'Thu, 29 Aug 2019 00:49:10 GMT', + connection: 'close' + }, + statusCode: 500 + }; + // tslint:enable + const response = processDataPlaneResponse(res, null); + // tslint:disable-next-line:no-magic-numbers + expect(response.statusCode).toEqual(500); + expect(response.body).toEqual({body: null}); + }); + + it('generates data plane response with no httpResponse', () => { + const response = processDataPlaneResponse(null, null); + expect(response.body).toEqual({body: null}); + }); + + it('generates data plane response using device status code', () => { + // tslint:disable + const res: any = { + headers: { + 'content-length': '10319', + 'content-type': 'application/json; charset=utf-8', + vary: 'Origin', + server: 'Microsoft-HTTPAPI/2.0', + 'iothub-errorcode': 'ServerError', + date: 'Thu, 29 Aug 2019 00:49:10 GMT', + connection: 'close', + 'x-ms-command-statuscode': '404' + }, + statusCode: 200 + }; + // tslint:enable + const response = processDataPlaneResponse(res, null); + // tslint:disable-next-line:no-magic-numbers + expect(response.statusCode).toEqual(404); + expect(response.body).toEqual({body: null}); + }); +}); diff --git a/src/server/dataPlaneHelper.ts b/src/server/dataPlaneHelper.ts new file mode 100644 index 000000000..0747df627 --- /dev/null +++ b/src/server/dataPlaneHelper.ts @@ -0,0 +1,67 @@ +/*********************************************************** + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License + **********************************************************/ +import express = require('express'); +import request = require('request'); +export const API_VERSION = '2018-06-30'; +const DEVICE_STATUS_HEADER = 'x-ms-command-statuscode'; +const MULTIPLE_CHOICES = 300; +const SUCCESS = 200; + +export const generateDataPlaneRequestBody = (req: express.Request) => { + const headers = { + 'Accept': 'application/json', + 'Authorization': req.body.sharedAccessSignature, + 'Content-Type': 'application/json', + ...req.body.headers + }; + if (req.body.etag) { + (headers as any)['If-Match'] = `"${req.body.etag}"`; // tslint:disable-line:no-any + } + + const apiVersion = req.body.apiVersion || API_VERSION; + const queryString = req.body.queryString ? `?${req.body.queryString}&api-version=${apiVersion}` : `?api-version=${apiVersion}`; + + return { + body: req.body.body, + headers, + method: req.body.httpMethod.toUpperCase(), + uri: `https://${req.body.hostName}/${encodeURIComponent(req.body.path)}${queryString}`, + }; +}; + +export const generateDataPlaneResponse = (httpRes: request.Response, body: any, res: express.Response) => { // tslint:disable-line:no-any + const response = processDataPlaneResponse(httpRes, body); + res.status(response.statusCode).send(response.body); +}; + +// tslint:disable-next-line:cyclomatic-complexity +export const processDataPlaneResponse = (httpRes: request.Response, body: any): {body: any, statusCode?: number} => { // tslint:disable-line:no-any + if (httpRes) { + if (httpRes.headers && httpRes.headers[DEVICE_STATUS_HEADER]) { // handles happy failure cases when error code is returned as a header + return { + body: {body: JSON.parse(body)}, + statusCode: parseInt(httpRes.headers[DEVICE_STATUS_HEADER] as string) // tslint:disable-line:radix + }; + } + else { + if (httpRes.statusCode >= SUCCESS && httpRes.statusCode < MULTIPLE_CHOICES) { + return { + body: {body: JSON.parse(body), headers: httpRes.headers}, + statusCode: httpRes.statusCode + }; + } else { + return { + body: {body: JSON.parse(body)}, + statusCode: httpRes.statusCode + }; + } + } + } + else { + return { + body: {body: JSON.parse(body)} + }; + } +}; diff --git a/src/server/server.ts b/src/server/server.ts index 598b0f9a1..31ee1d4c5 100644 --- a/src/server/server.ts +++ b/src/server/server.ts @@ -8,17 +8,15 @@ import cors = require('cors'); import request = require('request'); import { EventHubClient, EventPosition, delay, EventHubRuntimeInformation, ReceiveHandler } from '@azure/event-hubs'; +import { generateDataPlaneRequestBody, generateDataPlaneResponse } from './dataPlaneHelper'; -const API_VERSION = '2018-06-30'; const BAD_REQUEST = 400; const SUCCESS = 200; -const MULTIPLE_CHOICES = 300; const SERVER_ERROR = 500; const NOT_FOUND = 400; const SERVER_PORT = 8081; const SERVER_WAIT = 3000; // how long we'll let the call for eventHub messages run in non-socket const app = express(); -const DEVICE_STATUS_HEADER = 'x-ms-command-statuscode'; let client: EventHubClient = null; const receivers: ReceiveHandler[] = []; // tslint:disable-line: no-any let connectionString: string = ''; @@ -39,54 +37,17 @@ app.use(cors({ origin: 'http://127.0.0.1:3000', })); -// tslint:disable-next-line:cyclomatic-complexity app.post('/api/DataPlane', (req: express.Request, res: express.Response) => { try { if (!req.body) { res.status(BAD_REQUEST).send(); } else { - const headers = { - 'Accept': 'application/json', - 'Authorization': req.body.sharedAccessSignature, - 'Content-Type': 'application/json', - ...req.body.headers - }; - if (req.body.etag) { - // tslint:disable-next-line:no-any - (headers as any)['If-Match'] = `"${req.body.etag}"`; - } - - const apiVersion = req.body.apiVersion || API_VERSION; - const queryString = req.body.queryString ? `?${req.body.queryString}&api-version=${apiVersion}` : `?api-version=${apiVersion}`; request( - { - body: req.body.body, - headers, - method: req.body.httpMethod.toUpperCase(), - uri: `https://${req.body.hostName}/${encodeURIComponent(req.body.path)}${queryString}`, - }, + generateDataPlaneRequestBody(req), (err, httpRes, body) => { - if (httpRes) { - if (httpRes.headers && httpRes.headers[DEVICE_STATUS_HEADER]) { // handles happy failure cases when error code is returned as a header - // tslint:disable-next-line:radix - res.status(parseInt(httpRes.headers[DEVICE_STATUS_HEADER] as string)).send({body: JSON.parse(body)}); - } - else { - if (httpRes.statusCode >= SUCCESS && httpRes.statusCode < MULTIPLE_CHOICES) { - res.status(httpRes.statusCode).send({ - body: JSON.parse(body), - headers: httpRes.headers - }); - } else { - res.status(httpRes.statusCode).send(JSON.parse(body)); - } - } - } - else { - res.send({body: JSON.parse(body)}); - } - }); // tslint:disable-line:cyclomatic-complexity + generateDataPlaneResponse(httpRes, body, res); + }); } } catch (error) {