Skip to content

Commit

Permalink
test dataplane related server.ts (#101)
Browse files Browse the repository at this point in the history
* initial commit

* Moved server build output to dist folder to avoid naming conflicts for tests
  • Loading branch information
YingXue authored Aug 30, 2019
1 parent 6570492 commit 0967e73
Show file tree
Hide file tree
Showing 6 changed files with 207 additions and 47 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion public/electron.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');

Expand Down
132 changes: 132 additions & 0 deletions src/server/dataPlaneHelper.spec.ts
Original file line number Diff line number Diff line change
@@ -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});
});
});
67 changes: 67 additions & 0 deletions src/server/dataPlaneHelper.ts
Original file line number Diff line number Diff line change
@@ -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)}
};
}
};
47 changes: 4 additions & 43 deletions src/server/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '';
Expand All @@ -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) {
Expand Down

0 comments on commit 0967e73

Please sign in to comment.