iotagent-node-lib
Version:
IoT Agent library to interface with NGSI Context Broker
300 lines (282 loc) • 11.8 kB
JavaScript
/*
* Copyright 2023 Telefonica Investigación y Desarrollo, S.A.U
*
* This file is part of iotagent-json
*
* iotagent-json is free software: you can redistribute it and/or
* modify it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the License,
* or (at your option) any later version.
*
* iotagent-json is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public
* License along with iotagent-json.
* If not, seehttp://www.gnu.org/licenses/.
*
* For those usages not covered by the GNU Affero General Public License
* please contact with::[contacto@tid.es]
*
* Modified by: Miguel Angel Pedraza
*/
/* eslint-disable no-unused-vars*/
/* eslint-disable no-unused-expressions*/
const nock = require('nock');
const utils = require('../tools/utils');
const request = utils.request;
const async = require('async');
const chai = require('chai');
const MQTT = require('async-mqtt');
const iotAgentLib = require('../../lib/fiware-iotagent-lib');
var chaiMatchPattern = require('chai-match-pattern');
chai.use(chaiMatchPattern);
var _ = chaiMatchPattern.getLodashModule();
var expect = chai.expect;
chai.config.truncateThreshold = 0;
// Error messages
const ERR_CB_EXPECTATION_DIFFER = 'Assertion Error - Context Broker received payload differs from expectation';
const ERR_MEAS_BODY = 'Assertion Error - Measure response is not empty';
const ERR_MEAS_CODE = 'Assertion Error - Measure response status code differs from 200';
const ERR_MQTT = 'Error with MQTT: ';
const ERR_CB_NOT_EMPTY = 'Assertion Error - unexpected Context Broker request received (no request expected)';
const DEF_TYPE = 'TestType';
/**
* @brief Sends a measure to the IoT Agent and returns a promise with the response
*
* @param {Object} measure Measure to be sent to the IoT Agent
*/
function sendMeasureHttp(measure) {
return new Promise((resolve, reject) => {
request(measure, function (error, result, body) {
error ? reject(error) : resolve(result);
});
});
}
/**
* @brief Sends a measure to the IoT Agent and returns a promise with the response
*
* @param {Object} measure Measure to be sent to the IoT Agent
*/
function sendMeasureIotaLib(measure, provision) {
return new Promise((resolve, reject) => {
/**
* WARNING: This is kind of a hack, only required for the tests using Lib, since the method iotAgentLib.update
* requires a type and does not check the type of the group. For this purpose, this function uses the
* provision type, setting the measure type to the same type as the provision.
* This is not a problem for the tests using other transports than Lib, in that case, the type will be retrieved
* from the real provision.
*/
let typeInformation = {
service: provision.headers['fiware-service'],
subservice: provision.headers['fiware-servicepath']
};
let type;
let staticAttrs;
if (Array.isArray(provision.json.services) && provision.json.services.length > 0) {
type = provision.json.services[0].entity_type;
staticAttrs = provision.json.services[0].static_attributes;
} else {
type = DEF_TYPE;
}
typeInformation.type = type;
if (staticAttrs) {
typeInformation.staticAttributes = staticAttrs;
}
typeInformation.id = measure.qs.i;
iotAgentLib.update(
type + ':' + measure.qs.i,
type,
typeInformation,
jsonToIotaMeasures(measure.json),
function (error, result, body) {
error ? reject(error) : resolve(result);
}
);
});
}
/**
* @brief Converts a IOTA JSON object to an array of measures as expected by the IOTA Lib
*
* @param {Object} json
* @returns {Array} measures
*/
function jsonToIotaMeasures(originJson) {
// FIXME: maybe this could be refactored to use less code
if (originJson && originJson[0]) {
// multimeasure case
let finalMeasures = [];
for (let json of originJson) {
let measures = [];
for (let key in json) {
/* eslint-disable-next-line no-prototype-builtins */
if (json.hasOwnProperty(key)) {
let measure = {
name: key,
value: json[key]
};
// A bit of Magic. If the key is TimeInstant, we set the type to DateTime.
// When sending the data through iot
if (key === 'TimeInstant') {
measure.type = 'DateTime';
} else {
// Although the type is not meaningfull and we could have picked any string for this,
// we have aligned with DEFAULT_ATTRIBUTE_TYPE constant in IOTA-JSON and IOTA-UL repositories
measure.type = 'Text';
}
measures.push(measure);
}
}
finalMeasures.push(measures);
}
return finalMeasures;
} else {
let json = originJson;
let measures = [];
for (let key in json) {
/* eslint-disable-next-line no-prototype-builtins */
if (json.hasOwnProperty(key)) {
let measure = {
name: key,
value: json[key]
};
// A bit of Magic. If the key is TimeInstant, we set the type to DateTime.
// When sending the data through iot
if (key === 'TimeInstant') {
measure.type = 'DateTime';
} else {
// Although the type is not meaningfull and we could have picked any string for this,
// we have aligned with DEFAULT_ATTRIBUTE_TYPE constant in IOTA-JSON and IOTA-UL repositories
measure.type = 'Text';
}
measures.push(measure);
}
}
return measures;
}
}
/**
* @brief Delays the execution of the code for the specified time in ms
*
* @param {Number} time in ms
* @returns
*/
const delay = (time) => new Promise((res) => setTimeout(res, time));
function groupToIoTAConfigType(group, service, subservice) {
let type = {};
for (var key in group) {
/* eslint-disable-next-line no-prototype-builtins */
if (group.hasOwnProperty(key)) {
if (key === 'attributes') {
type.active = group.attributes;
} else if (key === 'entity_type') {
type.type = group.entity_type;
} else if (key === 'static_attributes') {
type.staticAttributes = group.static_attributes;
} else if (key === 'commands') {
type.commands = group.commands;
} else if (key !== 'resource') {
type[key] = group[key];
}
}
}
type.service = service;
type.subservice = subservice;
return { name: group.entity_type, type: type };
}
/**
* Test Case function
* @brief Sends a measure to the IoT Agent and validates the response
* and validates the Context Broker expectation
*
* @param {Object} measure Measure to be sent to the IoT Agent
* @param {Object} expectation Expectation for the Context Broker
* @param {Object} env Environment variables
* @param {Object} config IoTA Configuration object
* @param {String} type Type of test (multientity or multimeasure)
* @param {String} transport Transport to be used (Lib, HTTP or MQTT). If not specified, Lib is used.
* @param {Boolean} regex If true, the expectation is treated as a regex
*/
async function testCase(measure, expectation, provision, env, config, type, transport, regex) {
let receivedContext = [];
let cbMockRoute = '';
// Set the correct route depending if the test is multientity or not
if (type === 'multientity' || type === 'multimeasure') {
cbMockRoute = '/v2/op/update?options=flowControl';
} else {
cbMockRoute = '/v2/entities?options=upsert,flowControl';
}
// Set the correct mock times depending if the test is multimeasure or not
// based on the length of the expectation array
let mockTimes = 1;
if (expectation.length > 1) {
mockTimes = expectation.length;
}
let contextBrokerMock = nock('http://192.168.1.1:1026')
.matchHeader('fiware-service', env.service)
.matchHeader('fiware-servicepath', env.servicePath)
.post(cbMockRoute, function (body) {
mockTimes === 1 ? (receivedContext = body) : receivedContext.push(body); // Save the received body for later comparison
return true;
})
.times(mockTimes)
.reply(204);
// Send a measure to the IoT Agent and wait for the response
if (transport === 'MQTT') {
try {
let client = await MQTT.connectAsync('mqtt://' + config.mqtt.host);
await client.publish('/json/' + measure.qs.k + '/' + measure.qs.i + '/attrs', JSON.stringify(measure.json));
await client.end();
} catch (error) {
expect.fail(ERR_MQTT + error);
}
} else if (transport === 'HTTP') {
// HTTP
const response = await sendMeasureHttp(measure);
// Validate the response status code and the response body
expect(response.statusCode, ERR_MEAS_CODE).to.equal(200);
expect(response.body, ERR_MEAS_BODY).to.be.empty;
} else {
const response = await sendMeasureIotaLib(measure, provision);
}
// Validate Context Broker Expectation
if ((Array.isArray(expectation) && expectation.length > 0) || !Array.isArray(expectation)) {
// Filter empty expectations
regex && regex === true
? expect(receivedContext, ERR_CB_EXPECTATION_DIFFER).to.matchPattern(expectation)
: expect(receivedContext, ERR_CB_EXPECTATION_DIFFER).to.deep.equal(expectation);
contextBrokerMock.done(); // Ensure the request was made, no matter the body content
} else {
// If empty expectation, ensure no request was made
expect(contextBrokerMock.isDone(), ERR_CB_NOT_EMPTY).to.be.false;
expect(receivedContext, ERR_CB_NOT_EMPTY).to.be.empty;
}
}
/**
*
* @param {*} skip skip string from test case. I.E: "lib, !json"
* @param {*} matchPattern skip pattern to check. I.E: "lib"
* @returns true if the test should be skipped. False otherwise
*/
function checkSkip(skip, matchPattern) {
var isMatch = false;
// Separate tokens by comma or space, and remove empty tokens
var tokens = skip.split(/[ , ]+/).filter(function (value, index, arr) {
return value !== '' && !value.match(/[* ]+/);
});
// Check if the skip pattern is in the tokens array, or there is a token starting with ! without the pattern (negative match -!b)
tokens.forEach((element) => {
if (element === matchPattern || (element[0] === '!' && element.substr(1) !== matchPattern)) {
isMatch = true;
}
});
return isMatch;
}
exports.checkSkip = checkSkip;
exports.sendMeasureHttp = sendMeasureHttp;
exports.sendMeasureIotaLib = sendMeasureIotaLib;
exports.delayMs = delay;
exports.testCase = testCase;
exports.groupToIoTAConfigType = groupToIoTAConfigType;