This commit is contained in:
adashen 2018-05-10 11:13:15 +08:00
Родитель 9d8f2827c0
Коммит 362d0ee4e2
9 изменённых файлов: 3331 добавлений и 0 удалений

33
.eslintrc Normal file
Просмотреть файл

@ -0,0 +1,33 @@
{
"env": {
"es6": true,
"node": true
},
"extends": "eslint:recommended",
"parserOptions": {
"sourceType": "module",
"ecmaVersion": 2017
},
"rules": {
"indent": [
"error",
2
],
"linebreak-style": [
"off",
"unix"
],
"quotes": [
"error",
"single"
],
"semi": [
"error",
"always"
],
"no-unused-vars": [
"error",
{ "argsIgnorePattern": "next" }
]
}
}

16
Dockerfile Normal file
Просмотреть файл

@ -0,0 +1,16 @@
FROM node:8-slim
WORKDIR /app/
COPY package*.json ./
RUN npm install --production
COPY *.js ./
USER node
ENV DEBUG_OPTION " "
EXPOSE 3000
CMD ["sh", "-c", "node $DEBUG_OPTION server.js"]

45
logger.js Normal file
Просмотреть файл

@ -0,0 +1,45 @@
'use strict';
const winston = require('winston');
const fse = require('fs-extra');
winston.loggers.add('log', {
console: {
level: 'info',
handleExceptions: true,
humanReadableUnhandledException: true
}
});
const labelName = 'PrintInput';
const printer = 'printer';
const filePath = process.env.ModuleOutputFile;
if (filePath) {
const exist = fse.existsSync(filePath);
if (exist) {
winston.loggers.add(printer, {
console: {
level: 'info',
label: labelName
},
file: {
filename: filePath,
json: false,
level: 'info',
label: labelName
}
});
} else {
winston.loggers.add(printer, {
console: {
level: 'info',
label: labelName
}
});
}
}
module.exports = {
log: winston.loggers.get('log'),
printer: winston.loggers.get(printer)
};

14
module.json Normal file
Просмотреть файл

@ -0,0 +1,14 @@
{
"$schema-version": "0.0.1",
"description": "",
"image": {
"repository": "adashen/iot-edge-testing-utility",
"tag": {
"version": "0.0.1",
"platforms": {
"amd64": "./Dockerfile"
}
}
},
"language": "node"
}

2897
package-lock.json сгенерированный Normal file

Разница между файлами не показана из-за своего большого размера Загрузить разницу

35
package.json Normal file
Просмотреть файл

@ -0,0 +1,35 @@
{
"name": "iot-edge-testing-utility",
"version": "0.0.1",
"description": "Testing utility for Azure IoT Edge Modules",
"main": "server.js",
"scripts": {
"lint": "eslint **/*.js",
"test": "mocha --timeout 10000"
},
"repository": {
"type": "git",
"url": "git+https://github.com/Azure/iot-edge-testing-utility.git"
},
"author": "",
"license": "ISC",
"bugs": {
"url": "https://github.com/Azure/iot-edge-testing-utility/issues"
},
"homepage": "https://github.com/Azure/iot-edge-testing-utility#readme",
"dependencies": {
"azure-iot-device": "1.4.0-modules-preview",
"azure-iot-device-mqtt": "1.4.0-modules-preview",
"body-parser": "^1.18.2",
"express": "^4.16.3",
"fs-extra": "^5.0.0",
"winston": "^2.4.2"
},
"devDependencies": {
"chai": "^4.1.2",
"chai-http": "^4.0.0",
"eslint": "^4.19.1",
"mocha": "^5.1.1",
"sinon": "^5.0.7"
}
}

55
server.js Normal file
Просмотреть файл

@ -0,0 +1,55 @@
'use strict';
const express = require('express');
const utilityModule = require('./utilityModule');
const logger = require('./logger').log;
const http = require('http');
const bodyParser = require('body-parser');
utilityModule.initModule();
const app = express();
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));
const router = express.Router();
router.use(function log(req, res, next) {
logger.info(`Time: ${Date.now()} ${req.method} ${req.path}`);
next();
});
router.post('/api/v1/messages', async(req, res, next) => {
try {
await utilityModule.sendMessage(req.body);
res.status(202).json({'message': 'accepted'});
} catch(err) {
next(err);
}
});
app.use('/', router);
app.use('*', (req, res) => {
res.status(404).json({'error': 'I don\'t have that'});
});
app.use('*', (err, req, res, next) => {
if (err) {
res.status(500).json({'error': err.toString()});
} else {
res.status(404).json({'error': 'I don\'t have that'});
}
});
const server = http.createServer(app);
const port = process.env.PORT || 3000;
server.listen(port);
server.on('listening', () => {
if (process.send) {
process.send('online');
}
logger.info(`app listening on ${port}`);
});
module.exports = server;

110
test/testserver.js Normal file
Просмотреть файл

@ -0,0 +1,110 @@
/* eslint-env mocha */
process.env.NODE_ENV = 'test';
const utilityModule = require('../utilityModule');
const chai = require('chai');
const chaiHttp = require('chai-http');
// eslint-disable-next-line no-unused-vars
const should = chai.should();
const expect = chai.expect;
const sinon = require('sinon');
const assert = require('assert');
chai.use(chaiHttp);
describe('server', () => {
sinon.stub(utilityModule, 'initModule').callsFake(() => {});
const server = require('../server');
describe('/POST message', () => {
it('it should invoke the sendOutputEvent', (done) => {
const stub = sinon.stub(utilityModule, 'sendOutputEvent').callsFake(() => {});
chai.request(server)
.post('/api/v1/messages')
.set('Content-Type', 'application/json')
.send({
'inputName': 'input1',
'data': 'Hello World',
'properties': {
'test': true
},
'messageId': '0',
'correlationId': '1'
})
.end((err, res) => {
assert(stub.calledOnce);
assert.equal(stub.args[0][0], 'input1');
assert.equal(stub.args[0][1]['data'], 'Hello World');
assert.equal(stub.args[0][1]['messageId'], '0');
assert.equal(stub.args[0][1]['correlationId'], '1');
res.should.have.status(202);
stub.restore();
done();
});
});
it('it should show correct error if inputName is missing', (done) => {
const stub = sinon.stub(utilityModule, 'sendOutputEvent').callsFake(() => {});
chai.request(server)
.post('/api/v1/messages')
.set('Content-Type', 'application/json')
.send({
'channel': 'input1',
'data': 'Hello World',
'properties': {
'test': true
},
'messageId': '0',
'correlationId': '1'
})
.end((err, res) => {
assert(!stub.called);
res.should.have.status(500);
expect(res).to.be.json;
assert(res.body['error'].includes('Cannot get inputName'));
stub.restore();
done();
});
});
it('it should return 500 if error happen', (done) => {
chai.request(server)
.post('/api/v1/messages')
.set('Content-Type', 'application/json')
.send({
'inputName': 'input1',
'data': 'Hello World',
'properties': {
'test': true
},
'messageId': '0',
'correlationId': '1'
})
.end((err, res) => {
res.should.have.status(500);
done();
});
});
it('it should return 404 if no resource', (done) => {
sinon.restore();
chai.request(server)
.post('/api/v1/test')
.set('Content-Type', 'application/json')
.send({
'inputName': 'input1',
'data': 'Hello World',
'properties': {
'test': true
},
'messageId': '0',
'correlationId': '1'
})
.end((err, res) => {
res.should.have.status(404);
done();
});
});
});
});

126
utilityModule.js Normal file
Просмотреть файл

@ -0,0 +1,126 @@
'use strict';
var Mqtt = require('azure-iot-device-mqtt').Mqtt;
var Client = require('azure-iot-device').Client;
var Message = require('azure-iot-device').Message;
var fse = require('fs-extra');
var logger = require('./logger').log;
var printer = require('./logger').printer;
var client = undefined;
async function setOptions(optionObj) {
return await new Promise((resolve, reject) => {
client.setOptions(optionObj, (err) => {
if(err) {
logger.error(`Could not connect: ${err.message}`);
return reject(new Error(err.toString()));
} else {
return resolve();
}
});
});
}
function printInput(inputName, msg) {
if (inputName === 'printInput') {
const message = msg.getBytes().toString('utf-8');
printer.info(message);
}
}
async function initModule() {
const connectionString = process.env.EdgeHubConnectionString;
const caFilePath = process.env.EdgeModuleCACertificateFile;
if (!connectionString || !caFilePath) {
throw new Error('cannot get connectionstring or module CA Certificate File for test module');
}
const caContent = await fse.readFile(caFilePath);
client = Client.fromConnectionString(connectionString, Mqtt);
client.on('error', (err) => {
logger.error(err.message);
});
await setOptions({
ca: caContent
});
logger.info('Client connected');
client.on('inputMessage', (inputName, msg) => {
client.complete(msg, (err) => {
if (err) {
logger.error(`Complete message fail with error: ${err.message}`);
}
});
printInput(inputName, msg);
});
}
async function sendOutputEvent(channel, message) {
if (!client) {
throw new Error('Module has not been initialized');
}
return await new Promise((resolve, reject) => {
client.sendOutputEvent(channel, message, (err) => {
if(err) {
logger.error(`Send message fail: ${err.message}`);
return reject(new Error(err.toString()));
} else {
logger.info('Send message successfullly');
return resolve();
}
});
});
}
function getValidateMessage(requestBody) {
if (!requestBody) {
throw new Error('No request body provided');
}
const channel = requestBody.inputName;
if (!channel) {
throw new Error('Cannot get inputName in request body.');
}
const data = requestBody.data;
if (!data) {
throw new Error('Cannot find message data in request body');
}
const message = new Message(data);
if(requestBody.properties && typeof requestBody.properties === 'object') {
const properties = requestBody.properties;
for(const property in properties) {
if (properties.hasOwnProperty(property)) {
message.properties.add(property, properties[property]);
}
}
}
if (requestBody.messageId) {
message.messageId = requestBody.messageId;
}
if (requestBody.correlationId) {
message.correlationId = requestBody.correlationId;
}
if (requestBody.userId) {
message.userId = requestBody.userId;
}
return {channel, message};
}
async function sendMessage(requestBody) {
const {channel, message} = getValidateMessage(requestBody);
await this.sendOutputEvent(channel, message);
}
module.exports = {
initModule: initModule,
sendMessage: sendMessage,
getValidateMessage: getValidateMessage,
sendOutputEvent: sendOutputEvent
};