diff --git a/CHANGES_NEXT_RELEASE b/CHANGES_NEXT_RELEASE index eca0c1dc7..c2ca0620d 100644 --- a/CHANGES_NEXT_RELEASE +++ b/CHANGES_NEXT_RELEASE @@ -2,4 +2,5 @@ Refresh Documentation Add NGSIv2 metadata support to device provisioned attributes Fix: Error message when sending measures with unknown/undefined attribute Add Null check within executeWithSecurity() to avoid crash (#829) +Basic NGSI-LD active measures support (#841) Add NGSIv2 metadata support to attributeAlias plugin. diff --git a/doc/installationguide.md b/doc/installationguide.md index cc9092e14..a0b1afe8f 100644 --- a/doc/installationguide.md +++ b/doc/installationguide.md @@ -30,6 +30,20 @@ These are the parameters that can be configured in the global section: } ``` +- If you want to use NGSI-LD (experimental): + +```javascript +{ + host: '192.168.56.101', + port: '1026', + ngsiVersion: 'ld', + jsonLdContext: 'http://context.json-ld'' +} +``` + +Where `http://context.json-ld` is the location of the NGSI-LD `@context` element which provides additional information allowing the computer to +interpret the rest of the data with more clarity and depth. Read the [JSON-LD specification](https://w3c.github.io/json-ld-syntax/#the-context) for more informtaion. + - **server**: configuration used to create the Context Server (port where the IoT Agent will be listening as a Context Provider and base root to prefix all the paths). The `port` attribute is required. If no `baseRoot` attribute is used, '/' is used by default. E.g.: @@ -262,3 +276,4 @@ overrides. | IOTA_POLLING_DAEMON_FREQ | `pollingDaemonFrequency` | | IOTA_AUTOCAST | `autocast` | | IOTA_MULTI_CORE | `multiCore` | +| IOTA_JSON_LD_CONTEXT | `jsonLdContext` | diff --git a/lib/commonConfig.js b/lib/commonConfig.js index 13309ef09..7efc8ef5e 100644 --- a/lib/commonConfig.js +++ b/lib/commonConfig.js @@ -91,7 +91,8 @@ function processEnvironmentVariables() { 'IOTA_APPEND_MODE', 'IOTA_POLLING_EXPIRATION', 'IOTA_POLLING_DAEMON_FREQ', - 'IOTA_MULTI_CORE' + 'IOTA_MULTI_CORE', + 'IOTA_JSON_LD_CONTEXT' ], iotamVariables = [ 'IOTA_IOTAM_URL', @@ -146,6 +147,9 @@ function processEnvironmentVariables() { if (process.env.IOTA_CB_NGSI_VERSION) { config.contextBroker.ngsiVersion = process.env.IOTA_CB_NGSI_VERSION; } + if (process.env.IOTA_JSON_LD_CONTEXT){ + config.contextBroker.jsonLdContext = process.env.IOTA_JSON_LD_CONTEXT; + } // North Port Configuration (ensuring the configuration sub-object exists before start using it) if (config.server === undefined) { @@ -377,6 +381,21 @@ function checkNgsi2() { return false; } +/** + * It checks if the configuration file states the use of NGSI-LD + * + * @return {boolean} Result of the checking + */ +function checkNgsiLD() { + if (config.contextBroker && + config.contextBroker.ngsiVersion && + config.contextBroker.ngsiVersion === 'ld') { + return true; + } + + return false; +} + function setSecurityService(newSecurityService) { securityService = newSecurityService; } @@ -394,5 +413,6 @@ exports.getGroupRegistry = getGroupRegistry; exports.setCommandRegistry = setCommandRegistry; exports.getCommandRegistry = getCommandRegistry; exports.checkNgsi2 = checkNgsi2; +exports.checkNgsiLD = checkNgsiLD; exports.setSecurityService = setSecurityService; exports.getSecurityService = getSecurityService; diff --git a/lib/plugins/attributeAlias.js b/lib/plugins/attributeAlias.js index 728c29cb4..69ce3bd4a 100644 --- a/lib/plugins/attributeAlias.js +++ b/lib/plugins/attributeAlias.js @@ -76,7 +76,7 @@ function extractAllMappings(typeInformation) { function applyAlias(mappings) { return function aliasApplier(attribute) { if (mappings.direct[attribute.name]) { - if (config.checkNgsi2()) { + if (config.checkNgsi2() || config.checkNgsiLD()) { /*jshint camelcase: false */ attribute.object_id = attribute.name; // inverse not usefull due to collision } @@ -96,7 +96,7 @@ function applyAlias(mappings) { */ function updateAttribute(entity, typeInformation, callback) { var mappings = extractAllMappings(typeInformation); - if (config.checkNgsi2()) { + if (config.checkNgsi2() || config.checkNgsiLD()) { var attsArray = utils.extractAttributesArrayFromNgsi2Entity(entity); attsArray = attsArray.map(applyAlias(mappings)); entity = utils.createNgsi2Entity(entity.id, entity.type, attsArray, true); diff --git a/lib/plugins/bidirectionalData.js b/lib/plugins/bidirectionalData.js index 9ff44faad..484f42900 100644 --- a/lib/plugins/bidirectionalData.js +++ b/lib/plugins/bidirectionalData.js @@ -134,7 +134,7 @@ function sendSubscriptions(device, attributeList, callback) { logger.debug(context, 'Sending bidirectionality subscriptions for device [%s]', device.id); - if (config.checkNgsi2()) { + if (config.checkNgsi2() || config.checkNgsiLD()) { async.map(attributeList, sendSingleSubscriptionNgsi2, callback); } else { async.map(attributeList, sendSingleSubscriptionNgsi1, callback); diff --git a/lib/plugins/expressionParser.js b/lib/plugins/expressionParser.js index c509f154c..f06c9a743 100644 --- a/lib/plugins/expressionParser.js +++ b/lib/plugins/expressionParser.js @@ -177,7 +177,7 @@ function expressionApplier(context, typeInformation) { }; /*jshint camelcase: false */ - if (config.checkNgsi2() && attribute.object_id) { + if (config.checkNgsi2() || config.checkNgsiLD() && attribute.object_id) { newAttribute.object_id = attribute.object_id; } diff --git a/lib/plugins/expressionPlugin.js b/lib/plugins/expressionPlugin.js index 8ed729556..eb97f25e7 100644 --- a/lib/plugins/expressionPlugin.js +++ b/lib/plugins/expressionPlugin.js @@ -88,7 +88,7 @@ function update(entity, typeInformation, callback) { } try { - if (config.checkNgsi2()) { + if (config.checkNgsi2() || config.checkNgsiLD()) { var attsArray = utils.extractAttributesArrayFromNgsi2Entity(entity); attsArray = processEntityUpdateNgsi2(attsArray); entity = utils.createNgsi2Entity(entity.id, entity.type, attsArray, true); diff --git a/lib/plugins/multiEntity.js b/lib/plugins/multiEntity.js index 05c9f1791..0ce68c869 100644 --- a/lib/plugins/multiEntity.js +++ b/lib/plugins/multiEntity.js @@ -324,7 +324,7 @@ function updateAttributeNgsi2(entity, typeInformation, callback) { } function updateAttribute(entity, typeInformation, callback) { - if (config.checkNgsi2()) { + if (config.checkNgsi2() || config.checkNgsiLD()) { updateAttributeNgsi2(entity, typeInformation, callback); } else { updateAttributeNgsi1(entity, typeInformation, callback); diff --git a/lib/plugins/pluginUtils.js b/lib/plugins/pluginUtils.js index 5af245dcd..00a5e62bb 100644 --- a/lib/plugins/pluginUtils.js +++ b/lib/plugins/pluginUtils.js @@ -105,7 +105,7 @@ function createProcessAttribute(fn, attributeType) { attribute.value = fn(attribute.value); } - if (config.checkNgsi2()) { + if (config.checkNgsi2() || config.checkNgsiLD()) { // This code is backwards compatible to process metadata in the older NGSIv1-style (array) // as well as supporting the newer NGSIv2-style (object). The redundant Array Check can be // therefore be removed if/when NGSIv1 support is removed from the library. @@ -152,7 +152,7 @@ function createUpdateFilter(fn, attributeType) { return entity; } - if (config.checkNgsi2()) { + if (config.checkNgsi2() || config.checkNgsiLD()) { entity = processEntityUpdateNgsi2(entity); } else { entity.contextElements = entity.contextElements.map(processEntityUpdateNgsi1); @@ -186,7 +186,7 @@ function createQueryFilter(fn, attributeType) { return entity; } - if (config.checkNgsi2()) { + if (config.checkNgsi2() || config.checkNgsiLD()) { entity = processEntityQueryNgsi2(entity); } else { entity.contextResponses = entity.contextResponses.map(processEntityQueryNgsi1); diff --git a/lib/plugins/timestampProcessPlugin.js b/lib/plugins/timestampProcessPlugin.js index 83c21eb13..4cd8d7359 100644 --- a/lib/plugins/timestampProcessPlugin.js +++ b/lib/plugins/timestampProcessPlugin.js @@ -132,7 +132,7 @@ function updatePluginNgsi1(entity, entityType, callback) { * @param {Object} entity NGSI Entity as it would have been sent before the plugin. */ function updatePlugin(entity, entityType, callback) { - if (config.checkNgsi2()) { + if (config.checkNgsi2() || config.checkNgsiLD()) { updatePluginNgsi2(entity, entityType, callback); } else { updatePluginNgsi1(entity, entityType, callback); diff --git a/lib/services/common/genericMiddleware.js b/lib/services/common/genericMiddleware.js index 0b79198a3..4ee10cb6f 100644 --- a/lib/services/common/genericMiddleware.js +++ b/lib/services/common/genericMiddleware.js @@ -58,7 +58,7 @@ function handleError(error, req, res, next) { function traceRequest(req, res, next) { logger.debug(context, 'Request for path [%s] from [%s]', req.path, req.get('host')); - if (req.is('json')) { + if (req.is('json') || req.is('application/ld+json')) { logger.debug(context, 'Body:\n\n%s\n\n', JSON.stringify(req.body, null, 4)); } @@ -100,6 +100,8 @@ function getLogLevel(req, res, next) { function ensureType(req, res, next) { if (req.is('json')) { next(); + } else if (req.is('application/ld+json')) { + next(); } else { next(new errors.UnsupportedContentType(req.headers['content-type'])); } @@ -113,7 +115,7 @@ function ensureType(req, res, next) { */ function validateJson(template) { return function validate(req, res, next) { - if (req.is('json')) { + if (req.is('json') || req.is('application/ld+json')) { var errorList = revalidator.validate(req.body, template); if (errorList.valid) { diff --git a/lib/services/devices/deviceService.js b/lib/services/devices/deviceService.js index 35799a899..16660c2ae 100644 --- a/lib/services/devices/deviceService.js +++ b/lib/services/devices/deviceService.js @@ -143,6 +143,41 @@ function createInitialEntityHandlerNgsi2(deviceData, newDevice, callback) { }; } +/** + * Creates the response handler for the initial entity creation request using NGSI-LD. + * This handler basically deals with the errors that could have been rised during + * the communication with the Context Broker. + * + * @param {Object} deviceData Object containing all the deviceData needed to send the registration. + * @param {Object} newDevice Device object that will be stored in the database. + * @return {function} Handler to pass to the request() function. + */ +function createInitialEntityHandlerNgsiLD(deviceData, newDevice, callback) { + return function handleInitialEntityResponse(error, response, body) { + if (error) { + logger.error(context, + 'ORION-001: Connection error creating inital entity in the Context Broker: %s', error); + + alarms.raise(constants.ORION_ALARM, error); + + callback(error); + } else if (response && response.statusCode === 200) { + alarms.release(constants.ORION_ALARM); + logger.debug(context, 'Initial entity created successfully.'); + callback(null, newDevice); + } else { + var errorObj; + + logger.error(context, + 'Protocol error connecting to the Context Broker [%d]: %s', response.statusCode, body); + + errorObj = new errors.EntityGenericError(deviceData.id, deviceData.type, body); + + callback(errorObj); + } + }; +} + /** * Creates the response handler for the update entity request using NGSIv2. This handler basically deals with the errors * that could have been rised during the communication with the Context Broker. @@ -239,6 +274,8 @@ function formatAttributesNgsi2(originalVector, staticAtts) { return attributeList; } + + /** * Formats device's commands in NGSIv2 format. * @@ -264,6 +301,7 @@ function formatCommandsNgsi2(originalVector) { return attributeList; } + /** * Executes a request operation using security information if available * @@ -310,6 +348,66 @@ function executeWithSecurity(requestOptions, deviceData, callback) { }); } + +/** + * Creates the initial entity representing the device in the Context Broker using NGSI-LD. + * This is important mainly to allow the rest of the updateContext operations to be performed. + * + * @param {Object} deviceData Object containing all the deviceData needed to send the registration. + * @param {Object} newDevice Device object that will be stored in the database. + */ +function createInitialEntityNgsiLD(deviceData, newDevice, callback) { + var json = { + id: String(deviceData.name), + type: deviceData.type + }; + + jsonConcat(json, formatAttributesNgsi2(deviceData.active, false)); + jsonConcat(json, formatAttributesNgsi2(deviceData.staticAttributes, true)); + jsonConcat(json, formatCommandsNgsi2(deviceData.commands)); + + + if ( (('timestamp' in deviceData && deviceData.timestamp !== undefined) ? + deviceData.timestamp : config.getConfig().timestamp) && + ! utils.isTimestampedNgsi2(json)) { + logger.debug(context, 'config.timestamp %s %s', deviceData.timestamp, config.getConfig().timestamp); + json[constants.TIMESTAMP_ATTRIBUTE] = { + type: constants.TIMESTAMP_TYPE_NGSI2, + value: moment() + }; + } + + json = ngsiService.formatAsNGSILD(json); + delete json['@context']; + + var options = { + url: config.getConfig().contextBroker.url + '/ngsi-ld/v1/entityOperations/upsert/', + method: 'POST', + json:[json], + headers: { + 'fiware-service': deviceData.service, + 'Content-Type' : 'application/ld+json', + 'Link': '<' + config.getConfig().contextBroker.jsonLdContext + + '>; rel="http://www.w3.org/ns/json-ld#context"; type="application/ld+json"' + } + }; + + if (deviceData.cbHost && deviceData.cbHost.indexOf('://') !== -1) { + options.url = deviceData.cbHost + '/ngsi-ld/v1/entityOperations/upsert/'; + } else if (deviceData.cbHost && deviceData.cbHost.indexOf('://') === -1) { + options.url = 'http://' + deviceData.cbHost + '/ngsi-ld/v1/entityOperations/upsert/'; + } + + + + logger.debug(context, 'deviceData: %j', deviceData); + + + + logger.debug(context, 'Creating initial entity in the Context Broker:\n %s', JSON.stringify(options, null, 4)); + executeWithSecurity(options, newDevice, createInitialEntityHandlerNgsiLD(deviceData, newDevice, callback)); +} + /** * Creates the initial entity representing the device in the Context Broker using NGSIv2. * This is important mainly to allow the rest of the updateContext operations to be performed. @@ -352,7 +450,6 @@ function createInitialEntityNgsi2(deviceData, newDevice, callback) { value: moment() }; } - logger.debug(context, 'Creating initial entity in the Context Broker:\n %s', JSON.stringify(options, null, 4)); executeWithSecurity(options, newDevice, createInitialEntityHandlerNgsi2(deviceData, newDevice, callback)); } @@ -462,7 +559,9 @@ function createInitialEntityNgsi1(deviceData, newDevice, callback) { * @param {Object} newDevice Device object that will be stored in the database. */ function createInitialEntity(deviceData, newDevice, callback) { - if (config.checkNgsi2()) { + if (config.checkNgsiLD()) { + createInitialEntityNgsiLD(deviceData, newDevice, callback); + } else if (config.checkNgsi2()) { createInitialEntityNgsi2(deviceData, newDevice, callback); } else { createInitialEntityNgsi1(deviceData, newDevice, callback); @@ -522,6 +621,64 @@ function updateEntityNgsi2(deviceData, updatedDevice, callback) { } } + + +/** + * Updates the entity representing the device in the Context Broker using NGSI-LD. + * + * @param {Object} deviceData Object containing all the deviceData needed to send the registration. + * @param {Object} updatedDevice Device object that will be stored in the database. + */ +function updateEntityNgsiLD(deviceData, updatedDevice, callback) { + var options = { + url: config.getConfig().contextBroker.url + '/ngsi-ld/v1/entities/' + String(deviceData.name) + '/attrs', + method: 'POST', + json: { + }, + headers: { + 'fiware-service': deviceData.service, + 'Content-Type' : 'application/ld+json', + 'Link' :'<' + config.getConfig().contextBroker.jsonLdContext + + '>; rel="http://www.w3.org/ns/json-ld#context"; type="application/ld+json"' + } + }; + + if (deviceData.cbHost && deviceData.cbHost.indexOf('://') !== -1) { + options.url = deviceData.cbHost + '/ngsi-ld/v1/entities/' + String(deviceData.name) + '/attrs'; + } else if (deviceData.cbHost && deviceData.cbHost.indexOf('://') === -1) { + options.url = 'http://' + deviceData.cbHost + '/ngsi-ld/v1/entities/' + String(deviceData.name) + '/attrs'; + } + + if (deviceData.type) { + options.url += '?type=' + deviceData.type; + } + + jsonConcat(options.json, formatAttributesNgsi2(deviceData.active, false)); + jsonConcat(options.json, formatAttributesNgsi2(deviceData.staticAttributes, true)); + jsonConcat(options.json, formatCommandsNgsi2(deviceData.commands)); + + if ( (('timestamp' in deviceData && deviceData.timestamp !== undefined) ? + deviceData.timestamp : config.getConfig().timestamp) && + ! utils.isTimestampedNgsi2(options.json)) { + options.json[constants.TIMESTAMP_ATTRIBUTE] = { + type: constants.TIMESTAMP_TYPE_NGSI2, + value: moment() + }; + } + + options.json = ngsiService.formatAsNGSILD(options.json); + + // FIXME: maybe there is be a better way to theck options.json = {} + if (Object.keys(options.json).length === 0 && options.json.constructor === Object) { + logger.debug(context, 'Skip updating entity in the Context Broker (no actual attribute change)'); + callback(null, updatedDevice); + } + else{ + logger.debug(context, 'Updating entity in the Context Broker:\n %s', JSON.stringify(options, null, 4)); + request(options, updateEntityHandlerNgsi2(deviceData, updatedDevice, callback)); + } +} + /** * If the object_id or the name of the attribute is missing, complete it with the other piece of data. * @@ -992,20 +1149,32 @@ function updateRegisterDeviceNgsi2(deviceObj, callback) { callback(null, deviceData, oldDevice); } - async.waterfall([ - apply(config.getRegistry().get, deviceObj.id, deviceObj.service, deviceObj.subservice), - apply(extractDeviceDifference, deviceObj), - updateEntityNgsi2, - apply(combineWithNewDevice, deviceObj), - apply(registrationUtils.sendRegistrations, false), - apply(processContextRegistration, deviceObj), - config.getRegistry().update - ], callback); + if (config.checkNgsiLD()) { + async.waterfall([ + apply(config.getRegistry().get, deviceObj.id, deviceObj.service, deviceObj.subservice), + apply(extractDeviceDifference, deviceObj), + updateEntityNgsiLD, + apply(combineWithNewDevice, deviceObj), + apply(registrationUtils.sendRegistrations, false), + apply(processContextRegistration, deviceObj), + config.getRegistry().update + ], callback); + } else { + async.waterfall([ + apply(config.getRegistry().get, deviceObj.id, deviceObj.service, deviceObj.subservice), + apply(extractDeviceDifference, deviceObj), + updateEntityNgsi2, + apply(combineWithNewDevice, deviceObj), + apply(registrationUtils.sendRegistrations, false), + apply(processContextRegistration, deviceObj), + config.getRegistry().update + ], callback); + } } function updateRegisterDevice(deviceObj, callback) { - if (config.checkNgsi2()) { + if (config.checkNgsi2() || config.checkNgsiLD()) { updateRegisterDeviceNgsi2(deviceObj, callback); } else { updateRegisterDeviceNgsi1(deviceObj, callback); diff --git a/lib/services/devices/registrationUtils.js b/lib/services/devices/registrationUtils.js index 14bfed6d0..03c276bcc 100644 --- a/lib/services/devices/registrationUtils.js +++ b/lib/services/devices/registrationUtils.js @@ -77,6 +77,47 @@ function createRegistrationHandler(unregister, deviceData, callback) { }; } +/** + * Generates a handler for the registration requests that checks all the possible errors derived from the registration. + * The parameter information is needed in order to fulfill error information. + * + * @param {Boolen} unregister Indicates whether this registration is an unregistration or register. + * @param {Object} deviceData Object containing all the deviceData needed to send the registration. + * @return {Function} The generated handler. + */ +function createRegistrationHandlerNgsiLD(unregister, deviceData, callback) { + return function handleRegistrationResponse(error, response, body) { + if (error) { + logger.error(context, 'ORION-002: Connection error sending registrations to the Context Broker: %s', error); + callback(error); + } else if (response && response.statusCode === 201 && response.headers.location && unregister === false) { + logger.debug(context, 'Registration success.'); + callback(null, {registrationId: + response.headers.location.substr(response.headers.location.lastIndexOf('/') + 1)}); + } else if (response && response.statusCode === 204 && unregister === true) { + logger.debug(context, 'Unregistration success.'); + callback(null, null); + } + else if (response && response.statusCode && response.statusCode !== 500) { + logger.error(context, 'Registration error connecting to the Context Broker: %j', response.statusCode); + callback(new errors.BadRequest(JSON.stringify(response.statusCode))); + } + else { + var errorObj; + + logger.error(context, 'ORION-003: Protocol error connecting to the Context Broker: %j', errorObj); + + if (unregister) { + errorObj = new errors.UnregistrationError(deviceData.id, deviceData.type); + } else { + errorObj = new errors.RegistrationError(deviceData.id, deviceData.type); + } + + callback(errorObj); + } + }; +} + /** * Generates a handler for the registration requests that checks all the possible errors derived from the registration. * The parameter information is needed in order to fulfill error information. @@ -239,25 +280,22 @@ function sendUnregistrationsNgsi2(deviceData, callback) { logger.debug(context, 'Sending device unregistrations to Context Broker at [%s]', options.url); logger.debug(context, 'Using the following request:\n\n%s\n\n', JSON.stringify(options, null, 4)); - deviceService.executeWithSecurity( + return deviceService.executeWithSecurity( options, deviceData, createRegistrationHandlerNgsi2(true, deviceData, callback)); - } else { - logger.debug(context, 'No Context Provider registrations found for unregister'); - callback(null, deviceData); } - - + + logger.debug(context, 'No Context Provider registrations found for unregister'); + return callback(null, deviceData); } /** - * Sends a Context Provider registration or unregistration request to the Context Broker using NGSIv2. + * Sends a Context Provider unregistration request to the Context Broker using NGSI-LD. * - * @param {Boolen} unregister Indicates whether this registration is an unregistration or register. * @param {Object} deviceData Object containing all the deviceData needed to send the registration. */ -function sendRegistrationsNgsi2(unregister, deviceData, callback) { +function sendUnregistrationsNgsiLD(deviceData, callback) { var cbHost = config.getConfig().contextBroker.url; if (deviceData.cbHost && deviceData.cbHost.indexOf('://') !== -1) { cbHost = deviceData.cbHost; @@ -265,30 +303,41 @@ function sendRegistrationsNgsi2(unregister, deviceData, callback) { cbHost = 'http://' + deviceData.cbHost; } var options = { - url: cbHost + '/v2/registrations', - method: 'POST', - json: { - dataProvided: { - entities: - [ - { - type: deviceData.type, - id: String(deviceData.name) - } - ], - attrs: [], - }, - provider: { - http: { - url: config.getConfig().providerUrl - } - } - }, + url: cbHost + '/ngsi-ld/v1/csourceRegistrations/' + deviceData.registrationId, + method: 'DELETE', + json: true, headers: { 'fiware-service': deviceData.service, 'fiware-servicepath': deviceData.subservice } }; + if (deviceData.cbHost && deviceData.cbHost.indexOf('://') !== -1) { + options.url = deviceData.cbHost + '/ngsi-ld/v1/csourceRegistrations/' + deviceData.registrationId; + } else if (deviceData.cbHost && deviceData.cbHost.indexOf('://') === -1) { + options.url = 'http://' + deviceData.cbHost + '/ngsi-ld/v1/csourceRegistrations/' + deviceData.registrationId; + } + if (deviceData.registrationId) { + logger.debug(context, 'Sending device unregistrations to Context Broker at [%s]', options.url); + logger.debug(context, 'Using the following request:\n\n%s\n\n', JSON.stringify(options, null, 4)); + + return deviceService.executeWithSecurity( + options, + deviceData, + createRegistrationHandlerNgsi2(true, deviceData, callback)); + } + + logger.debug(context, 'No Context Provider registrations found for unregister'); + return callback(null, deviceData); +} + +/** + * Sends a Context Provider registration or unregistration request to the Context Broker using NGSIv2. + * + * @param {Boolen} unregister Indicates whether this registration is an unregistration or register. + * @param {Object} deviceData Object containing all the deviceData needed to send the registration. + */ +function sendRegistrationsNgsi2(unregister, deviceData, callback) { + function formatAttributes(originalVector) { var attributeList = []; @@ -329,31 +378,145 @@ function sendRegistrationsNgsi2(unregister, deviceData, callback) { } if (unregister) { - sendUnregistrationsNgsi2(deviceData, callback); - } else { + return sendUnregistrationsNgsi2(deviceData, callback); + } + if (deviceData.registrationId) { + return updateRegistrationNgsi2(deviceData, callback); + } - if (deviceData.registrationId) { - updateRegistrationNgsi2(deviceData, callback); - } else { - options.json.dataProvided.attrs = options.json.dataProvided.attrs.concat( - formatAttributes(deviceData.lazy), - formatAttributes(deviceData.commands) - ).reduce(mergeWithSameName, []); - - if (options.json.dataProvided.attrs.length === 0) { - logger.debug(context, 'Registration with Context Provider is not needed.' + - 'Device without lazy atts or commands'); - callback(null, deviceData); - } else { - logger.debug(context, 'Sending device registrations to Context Broker at [%s]', options.url); - logger.debug(context, 'Using the following request:\n\n%s\n\n', JSON.stringify(options, null, 4)); - deviceService.executeWithSecurity( - options, - deviceData, - createRegistrationHandlerNgsi2(unregister, deviceData, callback)); + var cbHost = config.getConfig().contextBroker.url; + if (deviceData.cbHost && deviceData.cbHost.indexOf('://') !== -1) { + cbHost = deviceData.cbHost; + } else if (deviceData.cbHost && deviceData.cbHost.indexOf('://') === -1) { + cbHost = 'http://' + deviceData.cbHost; + } + var options = { + url: cbHost + '/v2/registrations', + method: 'POST', + json: { + dataProvided: { + entities: + [ + { + type: deviceData.type, + id: String(deviceData.name) + } + ], + attrs: [], + }, + provider: { + http: { + url: config.getConfig().providerUrl + } } + }, + headers: { + 'fiware-service': deviceData.service, + 'fiware-servicepath': deviceData.subservice } + }; + + + options.json.dataProvided.attrs = options.json.dataProvided.attrs.concat( + formatAttributes(deviceData.lazy), + formatAttributes(deviceData.commands) + ).reduce(mergeWithSameName, []); + + if (options.json.dataProvided.attrs.length === 0) { + logger.debug(context, 'Registration with Context Provider is not needed.' + + 'Device without lazy atts or commands'); + return callback(null, deviceData); + } + + + logger.debug(context, 'Sending device registrations to Context Broker at [%s]', options.url); + logger.debug(context, 'Using the following request:\n\n%s\n\n', JSON.stringify(options, null, 4)); + deviceService.executeWithSecurity( + options, + deviceData, + createRegistrationHandlerNgsi2(unregister, deviceData, callback)); + + + +} + + +/** + * Sends a Context Provider registration or unregistration request to the Context Broker using NGSI-LD. + * + * @param {Boolen} unregister Indicates whether this registration is an unregistration or register. + * @param {Object} deviceData Object containing all the deviceData needed to send the registration. + */ +function sendRegistrationsNgsiLD(unregister, deviceData, callback) { + + if (unregister) { + return sendUnregistrationsNgsiLD(deviceData, callback); + } + + var properties = []; + var additionalContext = { + 'ngsi-ld': 'https://uri.etsi.org/ngsi-ld/default-context/' + }; + var lazy = deviceData.lazy || []; + var commands = deviceData.commands || []; + + lazy.forEach(element => { + properties.push(element.name); + additionalContext[element.name] = 'ngsi-ld:' + element.name; + }); + commands.forEach(element => { + properties.push(element.name); + additionalContext[element.name] = 'ngsi-ld:' + element.name; + }); + + + if (properties.length === 0) { + logger.debug(context, 'Registration with Context Provider is not needed.' + + 'Device without lazy atts or commands'); + return callback(null, deviceData); + } + + var cbHost = config.getConfig().contextBroker.url; + + if (deviceData.cbHost && deviceData.cbHost.indexOf('://') !== -1) { + cbHost = deviceData.cbHost; + } else if (deviceData.cbHost && deviceData.cbHost.indexOf('://') === -1) { + cbHost = 'http://' + deviceData.cbHost; } + + var contexts = [additionalContext]; + if(config.getConfig().contextBroker.jsonLdContext){ + contexts.push(config.getConfig().contextBroker.jsonLdContext); + } + + var options = { + url: cbHost + '/ngsi-ld/v1/csourceRegistrations/', + method: 'POST', + json: { + type: 'ContextSourceRegistration', + information: [ + { + entities: [{type: deviceData.type, id: String(deviceData.name)}], + properties: properties + } + ], + endpoint: config.getConfig().providerUrl, + '@context' : contexts + }, + headers: { + 'fiware-service': deviceData.service, + 'fiware-servicepath': deviceData.subservice, + 'Content-Type' :'application/ld+json' + } + }; + + + logger.debug(context, 'Sending device registrations to Context Broker at [%s]', options.url); + logger.debug(context, 'Using the following request:\n\n%s\n\n', JSON.stringify(options, null, 4)); + deviceService.executeWithSecurity( + options, + deviceData, + createRegistrationHandlerNgsiLD(unregister, deviceData, callback)); } @@ -369,7 +532,9 @@ function sendRegistrationsNgsi2(unregister, deviceData, callback) { * @param {Object} deviceData Object containing all the deviceData needed to send the registration. */ function sendRegistrations(unregister, deviceData, callback) { - if (config.checkNgsi2()) { + if (config.checkNgsiLD()) { + sendRegistrationsNgsiLD(unregister, deviceData, callback); + } else if (config.checkNgsi2()) { sendRegistrationsNgsi2(unregister, deviceData, callback); } else { sendRegistrationsNgsi1(unregister, deviceData, callback); diff --git a/lib/services/ngsi/ngsiService.js b/lib/services/ngsi/ngsiService.js index a9b7a5ba9..375931ee4 100644 --- a/lib/services/ngsi/ngsiService.js +++ b/lib/services/ngsi/ngsiService.js @@ -193,6 +193,88 @@ function generateNGSI2OperationHandler(operationName, entityName, typeInformatio }; } + +/** + * Generate an operation handler for NGSI-LD-based operations (query and update). The handler takes care of identifiying + * the errors and calling the appropriate callback with a success or a failure depending on how the operation ended. + * + * Most of the parameters are passed for debugging purposes mainly. + * + * @param {String} operationName Name of the NGSI operation being performed. + * @param {String} entityName Name of the entity that was the target of the operation. + * @param {Object} typeInformation Information about the device the entity represents. + * @param {String} token Security token used to access the entity. + * @param {Object} options Object holding all the information about the HTTP request. + + * @return {Function} The generated handler. + */ +function generateNGSILDOperationHandler(operationName, entityName, typeInformation, token, options, callback) { + return function(error, response, body) { + if (error) { + logger.error(context, 'Error found executing ' + operationName + ' action in Context Broker: %s', error); + + alarms.raise(constants.ORION_ALARM, error); + callback(error); + } else if (body && body.orionError) { + logger.debug(context, 'Orion error found executing ' + operationName + ' action in Context Broker: %j', + body.orionError); + + callback(new errors.BadRequest(body.orionError.details)); + } else if (response && operationName === 'update' && (response.statusCode === 204)) { + logger.debug(context, 'Received the following response from the CB: Value updated successfully\n'); + alarms.release(constants.ORION_ALARM); + callback(null, body); + } else if (response && operationName === 'query' && body !== undefined && response.statusCode === 200) { + logger.debug(context, + 'Received the following response from the CB:\n\n%s\n\n', JSON.stringify(body, null, 4)); + logger.debug(context, 'Value queried successfully'); + alarms.release(constants.ORION_ALARM); + callback(null, body); + } else if (response && operationName === 'query' && response.statusCode === 204) { + logger.debug(context, + 'Received the following response from the CB:\n\n%s\n\n', JSON.stringify(body, null, 4)); + + logger.error(context, + 'Operation ' + operationName + ' bad status code from the CB: 204.' + + 'A query operation must always return a body'); + callback(new errors.BadAnswer(response.statusCode, operationName)); + } else if (response && (response.statusCode === 403 || response.statusCode === 401)) { + logger.debug(context, 'Access forbidden executing ' + operationName + ' operation'); + callback(new errors.AccessForbidden( + token, + options.headers['fiware-service'], + options.headers['fiware-servicepath'])); + } else if (response && body && response.statusCode === 404) { + logger.debug(context, + 'Received the following response from the CB:\n\n%s\n\n', JSON.stringify(body, null, 4)); + + logger.error(context, + 'Operation ' + operationName + ' error connecting to the Context Broker: %j', body); + + var errorField = ngsiParser.getErrorField(body); + if (response.statusCode && response.statusCode === 404 && + errorField.details.includes(typeInformation.type) ) { + callback(new errors.DeviceNotFound(entityName)); + } + else if (errorField.code && errorField.code === '404') { + callback(new errors.AttributeNotFound()); + } + else { + callback(new errors.EntityGenericError(entityName, typeInformation.type, body)); + } + } else { + logger.debug(context, 'Unknown error executing ' + operationName + ' operation'); + if (! (body instanceof Array || body instanceof Object)) + { + body = JSON.parse(body); + } + + callback(new errors.EntityGenericError(entityName, typeInformation.type, + body, response.statusCode)); + } + }; +} + /** * Create the request object used to communicate with the Context Broker, adding security and service information. * @@ -210,6 +292,12 @@ function createRequestObject(url, typeInformation, token) { 'fiware-servicepath': config.getConfig().subservice }; + if (config.checkNgsiLD()) { + headers['Content-Type'] = 'application/ld+json'; + delete headers['fiware-servicepath']; + } + + if (config.getConfig().authentication && config.getConfig().authentication.enabled) { headers[config.getConfig().authentication.header] = token; } @@ -397,6 +485,103 @@ function getMetaData(typeInformation, name, metadata){ return undefined; } +/** + * Determines if a value is of type float + * + * @param {String} value Value to be analyzed + * @return {boolean} True if float, False otherwise. + */ +function isFloat(value) { + return !isNaN(value) && value.toString().indexOf('.') !== -1; +} + +/** + * Determines if a value is a number - Not a Number replaced by Null + * + * @param {String} value Value to be analyzed + * @return {Number} + */ +function orBlank(value){ + return isNaN(value) ? {'@type': 'Intangible', '@value': null} : value; +} + + +/** + * Amends an NGSIv2 attribute to NGSI-LD format + * All native JSON types are respected and cast as Property values + * Relationships must be give the type relationship + * + * @param {String} attr Attribute to be analyzed + * @return {Object} an object containing the attribute in NGSI-LD + * format + */ +function convertNGSIv2ToLD(attr){ + var obj = {type: 'Property', value: attr.value}; + switch (attr.type) { + case 'Property': + break; + case 'GeoProperty': + // TODO GEOPROPERTY + obj.type = 'GeoProperty'; + break; + case 'Relationship': + obj.type = 'Relationship'; + obj.object = attr.value; + delete obj.value; + break; + case 'Number': + if (isFloat(attr.value)) { + obj.value = orBlank(Number.parseFloat (attr.value)); + } else { + obj.value = orBlank(Number.parseInt (attr.value)); + } + + break; + case 'Integer': + obj.value = orBlank(Number.parseInt(attr.value)); + break; + case 'Float': + obj.value = orBlank(Number.parseFloat (attr.value)); + break; + case 'Boolean': + obj.value = (!!attr.value); + break; + default: + obj.value = {'@type': attr.type, '@value': attr.value}; + } + + if (attr.metadata){ + Object.keys(attr.metadata).forEach(function(key) { + obj[key] = convertNGSIv2ToLD(attr.metadata[key]); + }); + } + + return obj; +} + +/** + * Amends an NGSIv2 payload to NGSI-LD format + * + * @param {Object} value JSON to be converted + * @return {Object} NGSI-LD payload + */ +function formatAsNGSILD(json){ + var obj = {'@context' : config.getConfig().contextBroker.jsonLdContext}; + Object.keys(json).forEach(function(key) { + switch (key) { + case 'id': + obj[key] = json[key]; + break; + case 'type': + obj[key] = json[key]; + break; + default: + obj[key] = convertNGSIv2ToLD(json[key]); + } + }); + return obj; +} + /** * It casts attribute values which are reported using JSON native types @@ -406,15 +591,7 @@ function getMetaData(typeInformation, name, metadata){ */ function castJsonNativeAttributes(payload) { - /** - * Determines if a value is of type float - * - * @param {String} value Value to be analyzed - * @return {boolean} True if float, False otherwise. - */ - function isFloat(value) { - return !isNaN(value) && value.toString().indexOf('.') !== -1; - } + if (!config.getConfig().autocast) { return payload; @@ -584,7 +761,7 @@ function sendUpdateValueNgsi2(entityName, attributes, typeInformation, token, ca } logger.debug(context, 'Updating device value in the Context Broker at [%s]', options.url); - logger.debug(context, 'Using the following request:\n\n%s\n\n', JSON.stringify(options, null, 4)); + logger.debug(context, 'Using the following NGSI v2 request:\n\n%s\n\n', JSON.stringify(options, null, 4)); request(options, generateNGSI2OperationHandler('update', entityName, typeInformation, token, options, callback)); @@ -592,6 +769,156 @@ function sendUpdateValueNgsi2(entityName, attributes, typeInformation, token, ca }); } + +/** + * Makes an update in the Device's entity in the context broker, with the values given in the 'attributes' array. This + * array should comply to the NGSI-LD's attribute format. + * + * @param {String} entityName Name of the entity to register. + * @param {Array} attributes Attribute array containing the values to update. + * @param {Object} typeInformation Configuration information for the device. + * @param {String} token User token to identify against the PEP Proxies (optional). + */ +function sendUpdateValueNgsiLD(entityName, attributes, typeInformation, token, callback) { + + var payload = {}; + + var url = '/ngsi-ld/v1/entities/' + entityName + '/attrs'; + + if (typeInformation.type) { + url += '?type=' + typeInformation.type; + } + + var options = createRequestObject(url, typeInformation, token); + + + if (typeInformation && typeInformation.staticAttributes) { + attributes = attributes.concat(typeInformation.staticAttributes); + } + + if (!typeInformation || !typeInformation.type) { + callback(new errors.TypeNotFound(null, entityName)); + return; + } + + payload.id = entityName; + payload.type = typeInformation.type; + + + for (var i = 0; i < attributes.length; i++) { + if (attributes[i].name && attributes[i].type) { + + payload[attributes[i].name] = { + 'value' : attributes[i].value, + 'type' : attributes[i].type + }; + var metadata = getMetaData(typeInformation, attributes[i].name, attributes[i].metadata); + if (metadata){ + payload[attributes[i].name].metadata = metadata; + } + + } else { + callback(new errors.BadRequest(null, entityName)); + return; + } + } + + payload = castJsonNativeAttributes(payload); + async.waterfall([ + apply(statsService.add, 'measureRequests', 1), + apply(applyMiddlewares, updateMiddleware, payload, typeInformation)], + function(error, result) { + if (error) { + callback(error); + } else { + if (result) { + // The payload has been transformed by multientity plugin. It is not a JSON object but an Array. + if (result instanceof Array) { + options = createRequestObject( + '/v2/op/update', + typeInformation, + token); + + if ( ('timestamp' in typeInformation && typeInformation.timestamp !== + undefined) ? typeInformation.timestamp : config.getConfig().timestamp) { + // jshint maxdepth:5 + if (!utils.isTimestampedNgsi2(result)) { + options.json = addTimestampNgsi2(result, typeInformation.timezone); + // jshint maxdepth:5 + } else if (!utils.IsValidTimestampedNgsi2(result)) { + logger.error(context, 'Invalid timestamp:%s', JSON.stringify(result)); + callback(new errors.BadTimestamp(result)); + return; + } + } + + options.json = { + actionType: 'append', + entities: result + }; + } else { + delete result.id; + delete result.type; + options.json = result; + logger.debug(context, 'typeInformation: %j', typeInformation); + if ( ('timestamp' in typeInformation && typeInformation.timestamp !== + undefined) ? typeInformation.timestamp : config.getConfig().timestamp) { + if (!utils.isTimestampedNgsi2(options.json)) { + options.json = addTimestampNgsi2(options.json, typeInformation.timezone); + } else if (!utils.IsValidTimestampedNgsi2(options.json)) { + logger.error(context, 'Invalid timestamp:%s', JSON.stringify(options.json)); + callback(new errors.BadTimestamp(options.json)); + return; + } + } + } + } else { + delete payload.id; + delete payload.type; + options.json = payload; + } + // Purge object_id from entities before sent to CB + // object_id was added by createNgsi2Entity to allow multientity + // with duplicate attribute names. + var att; + if (options.json.entities) { + for (var entity = 0; entity < options.json.entities.length; entity++) { + for (att in options.json.entities[entity]) { + /*jshint camelcase: false */ + if (options.json.entities[entity][att].object_id) { + /*jshint camelcase: false */ + delete options.json.entities[entity][att].object_id; + } + if (options.json.entities[entity][att].multi) { + delete options.json.entities[entity][att].multi; + } + } + } + } else { + for (att in options.json) { + /*jshint camelcase: false */ + if (options.json[att].object_id) { + /*jshint camelcase: false */ + delete options.json[att].object_id; + } + if (options.json[att].multi) { + delete options.json[att].multi; + } + } + } + + options.json = formatAsNGSILD(options.json); + options.method = 'PATCH'; + + logger.debug(context, 'Updating device value in the Context Broker at [%s]', options.url); + logger.debug(context, 'Using the following NGSI-LD request:\n\n%s\n\n', JSON.stringify(options, null, 4)); + + request(options, + generateNGSILDOperationHandler('update', entityName, typeInformation, token, options, callback)); + } + }); +} + /** * Makes an update in the Device's entity in the context broker, with the values given in the 'attributes' array. This * array should comply to the NGSIv1's attribute format. @@ -605,6 +932,8 @@ function sendUpdateValueNgsi1(entityName, attributes, typeInformation, token, ca var options = createRequestObject('/v1/updateContext', typeInformation, token), payload; + + if (typeInformation && typeInformation.staticAttributes) { attributes = attributes.concat(typeInformation.staticAttributes); } @@ -657,7 +986,7 @@ function sendUpdateValueNgsi1(entityName, attributes, typeInformation, token, ca } logger.debug(context, 'Updating device value in the Context Broker at [%s]', options.url); - logger.debug(context, 'Using the following request:\n\n%s\n\n', JSON.stringify(options, null, 4)); + logger.debug(context, 'Using the following NGSI-v1 request:\n\n%s\n\n', JSON.stringify(options, null, 4)); request(options, generateNGSIOperationHandler('update', entityName, typeInformation, token, options, callback)); @@ -675,7 +1004,9 @@ function sendUpdateValueNgsi1(entityName, attributes, typeInformation, token, ca * @param {String} token User token to identify against the PEP Proxies (optional). */ function sendUpdateValue(entityName, attributes, typeInformation, token, callback) { - if (config.checkNgsi2()) { + if (config.checkNgsiLD()) { + sendUpdateValueNgsiLD(entityName, attributes, typeInformation, token, callback); + } else if (config.checkNgsi2()) { sendUpdateValueNgsi2(entityName, attributes, typeInformation, token, callback); } else { sendUpdateValueNgsi1(entityName, attributes, typeInformation, token, callback); @@ -792,7 +1123,7 @@ function sendQueryValueNgsi1(entityName, attributes, typeInformation, token, cal * @param {String} token User token to identify against the PEP Proxies (optional). */ function sendQueryValue(entityName, attributes, typeInformation, token, callback) { - if (config.checkNgsi2()) { + if (config.checkNgsi2() || config.checkNgsiLD()) { sendQueryValueNgsi2(entityName, attributes, typeInformation, token, callback); } else { sendQueryValueNgsi1(entityName, attributes, typeInformation, token, callback); @@ -993,3 +1324,4 @@ exports.resetMiddlewares = intoTrans(context, resetMiddlewares); exports.setCommandResult = intoTrans(context, setCommandResult); exports.castJsonNativeAttributes = castJsonNativeAttributes; exports.updateTrust = updateTrust; +exports.formatAsNGSILD= formatAsNGSILD; diff --git a/lib/services/ngsi/subscriptionService.js b/lib/services/ngsi/subscriptionService.js index e1e5a3799..284e859f0 100644 --- a/lib/services/ngsi/subscriptionService.js +++ b/lib/services/ngsi/subscriptionService.js @@ -245,6 +245,58 @@ function subscribeNgsi2(device, triggers, content, callback) { device, createSubscriptionHandlerNgsi2(device, triggers, store, callback)); } + +/** + * Makes a subscription for the given device's entity using NGSI-LD, triggered by the given attributes. + * The contents of the notification can be selected using the "content" array (that can be left blank + * to notify the complete entity). + * + * @param {Object} device Object containing all the information about a particular device. + * @param {Object} triggers Array with the names of the attributes that would trigger the subscription + * @param {Object} content Array with the names of the attributes to retrieve in the notification. + */ +function subscribeNgsiLD(device, triggers, content, callback) { + var options = { + method: 'POST', + headers: { + 'fiware-service': device.service + }, + json: { + type: 'Subscription', + entities: [ + { + id: device.name, + type: device.type + } + ], + + watchedAttributes: triggers, + notification: { + http: { + url: config.getConfig().providerUrl + '/notify' + }, + attributes: content || [] + } + } + }; + + var store = true; + + if (content) { + store = false; + } + + if (device.cbHost && device.cbHost.indexOf('://') !== -1) { + options.uri = device.cbHost + '/ngsi-ld/v1/subscriptions/'; + } else if (device.cbHost && device.cbHost.indexOf('://') === -1) { + options.uri = 'http://' + device.cbHost + '/ngsi-ld/v1/subscriptions/'; + } else { + options.uri = config.getConfig().contextBroker.url + '/ngsi-ld/v1/subscriptions/'; + } + deviceService.executeWithSecurity(options, + device, createSubscriptionHandlerNgsi2(device, triggers, store, callback)); +} + /** * Makes a subscription for the given device's entity, triggered by the given attributes. * The contents of the notification can be selected using the "content" array (that can be left blank @@ -255,7 +307,9 @@ function subscribeNgsi2(device, triggers, content, callback) { * @param {Object} content Array with the names of the attributes to retrieve in the notification. */ function subscribe(device, triggers, content, callback) { - if (config.checkNgsi2()) { + if (config.checkNgsiLD()) { + subscribeNgsiLD(device, triggers, content, callback); + } else if (config.checkNgsi2()) { subscribeNgsi2(device, triggers, content, callback); } else { subscribeNgsi1(device, triggers, content, callback); @@ -407,7 +461,7 @@ function unsubscribeNgsi2(device, id, callback) { * @param {String} id ID of the subscription to remove. */ function unsubscribe(device, id, callback) { - if (config.checkNgsi2()) { + if (config.checkNgsi2() || config.checkNgsiLD()) { unsubscribeNgsi2(device, id, callback); } else { unsubscribeNgsi1(device, id, callback); diff --git a/lib/services/northBound/contextServer.js b/lib/services/northBound/contextServer.js index d757bc89b..834c78156 100644 --- a/lib/services/northBound/contextServer.js +++ b/lib/services/northBound/contextServer.js @@ -41,9 +41,11 @@ var async = require('async'), }, updateContextTemplateNgsi1 = require('../../templates/updateContextNgsi1.json'), updateContextTemplateNgsi2 = require('../../templates/updateContextNgsi2.json'), + updateContextTemplateNgsiLD = require('../../templates/updateContextNgsiLD.json'), queryContextTemplate = require('../../templates/queryContext.json'), notificationTemplateNgsi1 = require('../../templates/notificationTemplateNgsi1.json'), notificationTemplateNgsi2 = require('../../templates/notificationTemplateNgsi2.json'), + notificationTemplateNgsiLD = require('../../templates/notificationTemplateNgsiLD.json'), notificationMiddlewares = [], updateHandler, commandHandler, @@ -433,7 +435,49 @@ function generateUpdateActionsNgsi2(req, contextElement, callback) { } /** - * Express middleware to manage incoming update context requests using NGSIv2. + * Express middleware to manage incoming update requests using NGSI-LD. + * + * @param {Object} req Request that was handled in first place. + * @param {Object} res Response that will be sent. + */ +function handleUpdateNgsiLD(req, res, next) { + function reduceActions(actions, callback) { + callback(null, _.flatten(actions)); + } + + if (updateHandler || commandHandler) { + logger.debug(context, 'Handling update from [%s]', req.get('host')); + logger.debug(context, req.body); + + async.waterfall([ + apply(async.map, req.body.entities, apply(generateUpdateActionsNgsi2, req)), + reduceActions, + async.series + ], function(error, result) { + if (error) { + logger.debug(context, 'There was an error handling the update action: %s.', error); + + next(error); + } else { + logger.debug(context, 'Update action from [%s] handled successfully.', req.get('host')); + res.status(204).json(); + } + }); + } else { + logger.error(context, 'Tried to handle an update request before the update handler was stablished.'); + + var errorNotFound = new Error({ + message: 'Update handler not found' + }); + next(errorNotFound); + } +} + +/** + * Express middleware to manage incoming update requests using NGSIv2 + * + * @param {Object} req Request that was handled in first place. + * @param {Object} res Response that will be sent. */ function handleUpdateNgsi2(req, res, next) { function reduceActions(actions, callback) { @@ -705,7 +749,195 @@ function handleQueryNgsi1(req, res, next) { } /** - * Express middleware to manage incoming query context requests using NGSIv2. + * Express middleware to manage incoming query context requests using NGSI-LD. + * + * @param {Object} req Request that was handled in first place. + * @param {Object} res Response that will be sent. + */ +function handleQueryNgsiLD(req, res, next) { + function getName(element) { + return element.name; + } + + function addStaticAttributes(attributes, device, contextElement, callback) { + + function inAttributes(item) { + return item.name && attributes.indexOf(item.name) >= 0; + } + + if (device.staticAttributes) { + + var selectedAttributes = []; + if (attributes === undefined || attributes.length === 0) { + selectedAttributes = device.staticAttributes; + } + else { + selectedAttributes = device.staticAttributes.filter(inAttributes); + } + + for (var att in selectedAttributes) { + contextElement[selectedAttributes[att].name] = { + 'type' : selectedAttributes[att].type, + 'value' : selectedAttributes[att].value + }; + } + } + + callback(null, contextElement); + } + + function completeAttributes(attributes, device, callback) { + if (attributes && attributes.length !== 0) { + logger.debug(context, 'Handling received set of attributes: %j', attributes); + callback(null, attributes); + } else if (device.lazy) { + logger.debug(context, 'Handling stored set of attributes: %j', attributes); + var results = device.lazy.map(getName); + callback(null, results); + } else { + logger.debug(context, 'Couldn\'t find any attributes. Handling with null reference'); + callback(null, null); + } + } + + function finishQueryForDevice(attributes, contextEntity, actualHandler, device, callback) { + var contextId = contextEntity.id; + var contextType = contextEntity.type; + if(!contextId) { + contextId = device.id; + } + + if(!contextType) { + contextType = device.type; + } + + deviceService.findConfigurationGroup(device, function(error, group) { + var executeCompleteAttributes = apply( + completeAttributes, + attributes, + group + ), + executeQueryHandler = apply( + actualHandler, + contextId, + contextType, + req.headers['fiware-service'], + req.headers['fiware-servicepath'] + ), + executeAddStaticAttributes = apply( + addStaticAttributes, + attributes, + group + ); + + async.waterfall([ + executeCompleteAttributes, + executeQueryHandler, + executeAddStaticAttributes + ], callback); + }); + } + + function createQueryRequest(attributes, contextEntity, callback) { + var actualHandler; + var getFunction; + + if (queryHandler) { + actualHandler = queryHandler; + } else { + actualHandler = defaultQueryHandlerNgsi2; + } + + if (contextEntity.id) { + getFunction = apply( + deviceService.getDeviceByName, + contextEntity.id, + req.headers['fiware-service'], + req.headers['fiware-servicepath']); + } else { + getFunction = apply( + deviceService.listDevicesWithType, + contextEntity.type, + req.headers['fiware-service'], + req.headers['fiware-servicepath'], + null, + null); + } + + getFunction(function handleFindDevice(error, innerDevice) { + let deviceList = []; + if (!innerDevice) { + return callback(new errors.DeviceNotFound(contextEntity.id)); + } + + if(innerDevice.count) { + if (innerDevice.count === 0) { + return callback(null, []); + } else { + deviceList = innerDevice.devices; + } + } else { + deviceList = [innerDevice]; + } + + async.map(deviceList, async.apply( + finishQueryForDevice, attributes, contextEntity, actualHandler), function (error, results) { + if (error) { + callback(error); + } + else if(innerDevice.count) { + callback(null,results); + } else if(Array.isArray(results) && results.length > 0){ + callback(null, results); + } else { + callback(null, results); + } + }); + }); + + } + + function handleQueryContextRequests(error, result) { + if (error) { + logger.debug(context, 'There was an error handling the query: %s.', error); + next(error); + } else { + logger.debug(context, 'Query from [%s] handled successfully.', req.get('host')); + res.status(200).json(result); + } + } + + logger.debug(context, 'Handling query from [%s]', req.get('host')); + var contextEntity = {}; + + // At the present moment, IOTA supports query request with one entity and without patterns. This is aligned + // with the utilization cases in combination with ContextBroker. Other cases are returned as error + if (req.body.entities.length !== 1) + { + logger.warn('queries with entities number different to 1 are not supported (%d found)', + req.body.entities.length); + handleQueryContextRequests({code: 400, name: 'BadRequest', message: 'more than one entity in query'}); + return; + } + if (req.body.entities[0].idPattern) + { + logger.warn('queries with idPattern are not supported'); + handleQueryContextRequests({code: 400, name: 'BadRequest', message: 'idPattern usage in query'}); + return; + } + + contextEntity.id = req.body.entities[0].id; + contextEntity.type = req.body.entities[0].type; + var queryAtts = req.body.attrs; + createQueryRequest(queryAtts, contextEntity, handleQueryContextRequests); + +} + +/** + * Express middleware to manage incoming query context requests using NGSI-LD. + * + * @param {Object} req Request that was handled in first place. + * @param {Object} res Response that will be sent. */ function handleQueryNgsi2(req, res, next) { function getName(element) { @@ -886,6 +1118,12 @@ function handleQueryNgsi2(req, res, next) { } +/** + * Express middleware to manage incoming notification requests using NGSIv1. + * + * @param {Object} req Request that was handled in first place. + * @param {Object} res Response that will be sent. + */ function handleNotificationNgsi1(req, res, next) { function checkStatus(statusCode, callback) { @@ -957,6 +1195,91 @@ function handleNotificationNgsi1(req, res, next) { } } +/** + * Express middleware to manage incoming notification requests using NGSI-LD. + * + * @param {Object} req Request that was handled in first place. + * @param {Object} res Response that will be sent. + */ +function handleNotificationNgsiLD(req, res, next) { + function extractInformation(dataElement, callback) { + var atts = []; + for (var key in dataElement) { + if (dataElement.hasOwnProperty(key)) { + if (key !== 'id' && key !== 'type') { + var att = {}; + att.type = dataElement[key].type; + att.value = dataElement[key].value; + att.name = key; + atts.push(att); + } + } + } + deviceService.getDeviceByName( + dataElement.id, + req.headers['fiware-service'], + req.headers['fiware-servicepath'], + function(error, device) { + if (error) { + callback(error); + } else { + callback(null, device, atts); + } + }); + } + + function applyNotificationMiddlewares(device, values, callback) { + if (notificationMiddlewares.length > 0) { + var firstMiddleware = notificationMiddlewares.slice(0, 1)[0], + rest = notificationMiddlewares.slice(1), + startMiddleware = apply(firstMiddleware, device, values), + composedMiddlewares = [startMiddleware].concat(rest); + + async.waterfall(composedMiddlewares, callback); + } else { + callback(null, device, values); + } + } + + function createNotificationHandler(contextResponse, callback) { + async.waterfall([ + apply(extractInformation, contextResponse), + applyNotificationMiddlewares, + notificationHandler + ], callback); + } + + function handleNotificationRequests(error) { + if (error) { + logger.error(context, 'Error found when processing notification: %j', error); + next(error); + } else { + res.status(200).json({}); + } + } + + if (notificationHandler) { + logger.debug(context, 'Handling notification from [%s]', req.get('host')); + async.map(req.body.data, createNotificationHandler, handleNotificationRequests); + + } else { + var errorNotFound = new Error({ + message: 'Notification handler not found' + }); + + logger.error(context, 'Tried to handle a notification before notification handler was established.'); + + next(errorNotFound); + } + +} + +/** + * Express middleware to manage incoming notification requests using NGSIv2. + * + * @param {Object} req Request that was handled in first place. + * @param {Object} res Response that will be sent. + */ function handleNotificationNgsi2(req, res, next) { function extractInformation(dataElement, callback) { var atts = []; @@ -1086,6 +1409,13 @@ function setNotificationHandler(newHandler) { notificationHandler = newHandler; } +/** + * Error handler for NGSIv1 context query requests. + * + * @param {Object} error Incoming error + * @param {Object} req Request that was handled in first place. + * @param {Object} res Response that will be sent. + */ function queryErrorHandlingNgsi1(error, req, res, next) { var code = 500; @@ -1104,6 +1434,13 @@ function queryErrorHandlingNgsi1(error, req, res, next) { }); } +/** + * Error handler for NGSIv2 context query requests. + * + * @param {Object} error Incoming error + * @param {Object} req Request that was handled in first place. + * @param {Object} res Response that will be sent. + */ function queryErrorHandlingNgsi2(error, req, res, next) { var code = 500; @@ -1119,6 +1456,35 @@ function queryErrorHandlingNgsi2(error, req, res, next) { }); } +/** + * Error handler for NGSI-LD context query requests. + * + * @param {Object} error Incoming error + * @param {Object} req Request that was handled in first place. + * @param {Object} res Response that will be sent. + */ +function queryErrorHandlingNgsiLD(error, req, res, next) { + var code = 500; + + logger.debug(context, 'Query NGSI-LD error [%s] handling request: %s', error.name, error.message); + + if (error.code && String(error.code).match(/^[2345]\d\d$/)) { + code = error.code; + } + + res.status(code).json({ + error: error.name, + description: error.message.replace(/[<>\"\'=;\(\)]/g, '') + }); +} + +/** + * Error handler for NGSIv1 update requests. + * + * @param {Object} error Incoming error + * @param {Object} req Request that was handled in first place. + * @param {Object} res Response that will be sent. + */ function updateErrorHandlingNgsi1(error, req, res, next) { var code = 500; @@ -1144,6 +1510,13 @@ function updateErrorHandlingNgsi1(error, req, res, next) { ); } +/** + * Error handler for NGSIv2 update requests. + * + * @param {Object} error Incoming error + * @param {Object} req Request that was handled in first place. + * @param {Object} res Response that will be sent. + */ function updateErrorHandlingNgsi2(error, req, res, next) { var code = 500; @@ -1159,6 +1532,28 @@ function updateErrorHandlingNgsi2(error, req, res, next) { }); } +/** + * Error handler for NGSI-LD update requests. + * + * @param {Object} error Incoming error + * @param {Object} req Request that was handled in first place. + * @param {Object} res Response that will be sent. + */ +function updateErrorHandlingNgsiLD(error, req, res, next) { + var code = 500; + + logger.debug(context, 'Update NGSI-LD error [%s] handing request: %s', error.name, error.message); + + if (error.code && String(error.code).match(/^[2345]\d\d$/)) { + code = error.code; + } + + res.status(code).json({ + error: error.name, + description: error.message.replace(/[<>\"\'=;\(\)]/g, '') + }); +} + /** * Load the routes related to context dispatching (NGSI10 calls). * @@ -1178,6 +1573,12 @@ function loadContextRoutes(router) { handleUpdateNgsi2, updateErrorHandlingNgsi2 ], + updateMiddlewaresNgsiLD = [ + middlewares.ensureType, + middlewares.validateJson(updateContextTemplateNgsiLD), + handleUpdateNgsiLD, + updateErrorHandlingNgsiLD + ], queryMiddlewaresNgsi1 = [ middlewares.ensureType, middlewares.validateJson(queryContextTemplate), @@ -1188,6 +1589,10 @@ function loadContextRoutes(router) { handleQueryNgsi2, queryErrorHandlingNgsi2 ], + queryMiddlewaresNgsiLD = [ + handleQueryNgsiLD, + queryErrorHandlingNgsiLD + ], updatePathsNgsi1 = [ '/v1/updateContext', '/NGSI10/updateContext', @@ -1197,6 +1602,9 @@ function loadContextRoutes(router) { '/v2/op/update', '//op/update' ], + updatePathsNgsiLD = [ + '/ngsi-ld/v1/entities/:id/attrs/:attr' + ], queryPathsNgsi1 = [ '/v1/queryContext', '/NGSI10/queryContext', @@ -1205,13 +1613,30 @@ function loadContextRoutes(router) { queryPathsNgsi2 = [ '/v2/op/query', '//op/query' + ], + queryPathsNgsiLD = [ + '/ngsi-ld/v1/entities/:id' ]; // In a more evolved implementation, more endpoints could be added to queryPathsNgsi2 // according to http://fiware.github.io/specifications/ngsiv2/stable. - logger.info(context, 'Loading NGSI Contect server routes'); var i; - if (config.checkNgsi2()) { + if (config.checkNgsiLD()) { + logger.info(context, 'Loading NGSI-LD Context server routes'); + for (i = 0; i < updatePathsNgsiLD.length; i++) { + router.patch(updatePathsNgsiLD[i], updateMiddlewaresNgsiLD); + } + for (i = 0; i < queryPathsNgsiLD.length; i++) { + router.get(queryPathsNgsiLD[i], queryMiddlewaresNgsiLD); + } + router.post('/notify', [ + middlewares.ensureType, + middlewares.validateJson(notificationTemplateNgsiLD), + handleNotificationNgsiLD, + queryErrorHandlingNgsiLD + ]); + } else if (config.checkNgsi2()) { + logger.info(context, 'Loading NGSI-v2 Context server routes'); for (i = 0; i < updatePathsNgsi2.length; i++) { router.post(updatePathsNgsi2[i], updateMiddlewaresNgsi2); } @@ -1225,6 +1650,7 @@ function loadContextRoutes(router) { queryErrorHandlingNgsi2 ]); } else { + logger.info(context, 'Loading NGSI-v1 Context server routes'); for (i = 0; i < updatePathsNgsi1.length; i++) { router.post(updatePathsNgsi1[i], updateMiddlewaresNgsi1); } diff --git a/lib/services/northBound/northboundServer.js b/lib/services/northBound/northboundServer.js index f40bab39d..477c7b80b 100644 --- a/lib/services/northBound/northboundServer.js +++ b/lib/services/northBound/northboundServer.js @@ -56,6 +56,7 @@ function start(config, callback) { northboundServer.app.set('host', config.server.host || '0.0.0.0'); northboundServer.app.use(domainUtils.requestDomain); northboundServer.app.use(bodyParser.json()); + northboundServer.app.use(bodyParser.json({ type: 'application/*+json' })); if (config.logLevel && config.logLevel === 'DEBUG') { northboundServer.app.use(middlewares.traceRequest); diff --git a/lib/templates/notificationTemplateNgsiLD.json b/lib/templates/notificationTemplateNgsiLD.json new file mode 100644 index 000000000..e9ebce361 --- /dev/null +++ b/lib/templates/notificationTemplateNgsiLD.json @@ -0,0 +1,35 @@ +{ + "properties": { + "data": { + "description": "Content of the notification. List of entities with modified attributes.", + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "required": true + }, + "additionalProperties":{ + "type": "object", + "properties": { + "type":{ + "type": "string", + "required": true + }, + "value":{ + "type": "string", + "required": true + } + } + } + } + }, + "required": true + }, + "subscriptionId": { + "type": "string", + "required": true + } + } +} diff --git a/lib/templates/updateContextNgsiLD.json b/lib/templates/updateContextNgsiLD.json new file mode 100644 index 000000000..8fca57d21 --- /dev/null +++ b/lib/templates/updateContextNgsiLD.json @@ -0,0 +1,38 @@ +{ + "type": "object", + "properties": { + "actionType": { + "type": "string", + "enum": ["update"] + }, + "entities": { + "type": "array", + "items": { + "type": "object", + "properties": { + "id": { + "type": "string", + "required": true + }, + "type": { + "type": "string", + "required": true + }, + "additionalProperties":{ + "type": "object", + "properties": { + "type":{ + "type": "string", + "required": true + }, + "value":{ + "type": "string", + "required": true + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/test/.jshintrc b/test/.jshintrc index 2cbeefda0..cd425bbe4 100644 --- a/test/.jshintrc +++ b/test/.jshintrc @@ -30,6 +30,6 @@ }, "predef": [ - "describe", "beforeEach", "afterEach", "it" + "describe", "beforeEach", "afterEach", "it", "xdescribe", "xit" ] } \ No newline at end of file diff --git a/test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerIoTAgent1.json b/test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerIoTAgent1.json new file mode 100644 index 000000000..c0cc769cd --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerIoTAgent1.json @@ -0,0 +1,24 @@ +{ + "@context": [ + { + "ngsi-ld": "https://uri.etsi.org/ngsi-ld/default-context/", + "temperature": "ngsi-ld:temperature" + }, + "http://context.json-ld" + ], + "endpoint": "http://smartGondor.com", + "information": [ + { + "entities": [ + { + "id": "Light:light1", + "type": "Light" + } + ], + "properties": [ + "temperature" + ] + } + ], + "type": "ContextSourceRegistration" +} diff --git a/test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerIoTAgent2.json b/test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerIoTAgent2.json new file mode 100644 index 000000000..5fff2592d --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerIoTAgent2.json @@ -0,0 +1,18 @@ +{ + "dataProvided": { + "attrs": [ + "moving" + ], + "entities": [ + { + "id": "Motion:motion1", + "type": "Motion" + } + ] + }, + "provider": { + "http": { + "url": "http://smartGondor.com" + } + } +} diff --git a/test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerIoTAgent4.json b/test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerIoTAgent4.json new file mode 100644 index 000000000..a15a48455 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerIoTAgent4.json @@ -0,0 +1,18 @@ +{ + "dataProvided": { + "attrs": [ + "moving" + ], + "entities": [ + { + "id": "RobotPre:TestRobotPre", + "type": "RobotPre" + } + ] + }, + "provider": { + "http": { + "url": "http://smartGondor.com" + } + } +} diff --git a/test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerIoTAgentCommands.json b/test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerIoTAgentCommands.json new file mode 100644 index 000000000..7abb95d26 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerIoTAgentCommands.json @@ -0,0 +1,24 @@ +{ + "@context": [ + { + "ngsi-ld": "https://uri.etsi.org/ngsi-ld/default-context/", + "position": "ngsi-ld:position" + }, + "http://context.json-ld" + ], + "endpoint": "http://smartGondor.com", + "information": [ + { + "entities": [ + { + "id": "Robot:r2d2", + "type": "Robot" + } + ], + "properties": [ + "position" + ] + } + ], + "type": "ContextSourceRegistration" +} diff --git a/test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerProvisionedDevice.json b/test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerProvisionedDevice.json new file mode 100644 index 000000000..bf17a16cf --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerProvisionedDevice.json @@ -0,0 +1,26 @@ +{ + "@context": [ + { + "commandAttr": "ngsi-ld:commandAttr", + "luminance": "ngsi-ld:luminance", + "ngsi-ld": "https://uri.etsi.org/ngsi-ld/default-context/" + }, + "http://context.json-ld" + ], + "endpoint": "http://smartGondor.com", + "information": [ + { + "entities": [ + { + "id": "TheFirstLight", + "type": "TheLightType" + } + ], + "properties": [ + "luminance", + "commandAttr" + ] + } + ], + "type": "ContextSourceRegistration" +} diff --git a/test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerProvisionedDevice2.json b/test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerProvisionedDevice2.json new file mode 100644 index 000000000..3dbb8fda1 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerProvisionedDevice2.json @@ -0,0 +1,18 @@ +{ + "dataProvided": { + "attrs": [ + "luminance" + ], + "entities": [ + { + "id": "TheSecondLight", + "type": "TheLightType" + } + ] + }, + "provider": { + "http": { + "url": "http://smartGondor.com" + } + } +} diff --git a/test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerProvisionedDeviceWithGroup.json b/test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerProvisionedDeviceWithGroup.json new file mode 100644 index 000000000..cf258d9e7 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerProvisionedDeviceWithGroup.json @@ -0,0 +1,30 @@ +{ + "@context": [ + { + "commandAttr": "ngsi-ld:commandAttr", + "luminance": "ngsi-ld:luminance", + "luminescence": "ngsi-ld:luminescence", + "ngsi-ld": "https://uri.etsi.org/ngsi-ld/default-context/", + "wheel1": "ngsi-ld:wheel1" + }, + "http://context.json-ld" + ], + "endpoint": "http://smartGondor.com", + "information": [ + { + "entities": [ + { + "id": "TheFirstLight", + "type": "TheLightType" + } + ], + "properties": [ + "luminance", + "luminescence", + "commandAttr", + "wheel1" + ] + } + ], + "type": "ContextSourceRegistration" +} diff --git a/test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerProvisionedDeviceWithGroup2.json b/test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerProvisionedDeviceWithGroup2.json new file mode 100644 index 000000000..27ec47fdf --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerProvisionedDeviceWithGroup2.json @@ -0,0 +1,30 @@ +{ + "@context": [ + { + "commandAttr": "ngsi-ld:commandAttr", + "luminance": "ngsi-ld:luminance", + "luminescence": "ngsi-ld:luminescence", + "ngsi-ld": "https://uri.etsi.org/ngsi-ld/default-context/", + "wheel1": "ngsi-ld:wheel1" + }, + "http://context.json-ld" + ], + "endpoint": "http://smartGondor.com", + "information": [ + { + "entities": [ + { + "id": "TheFirstLight", + "type": "SensorMachine" + } + ], + "properties": [ + "luminance", + "luminescence", + "commandAttr", + "wheel1" + ] + } + ], + "type": "ContextSourceRegistration" +} diff --git a/test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerProvisionedDeviceWithGroup3.json b/test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerProvisionedDeviceWithGroup3.json new file mode 100644 index 000000000..5cd3919a3 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerProvisionedDeviceWithGroup3.json @@ -0,0 +1,24 @@ +{ + "@context": [ + { + "ngsi-ld": "https://uri.etsi.org/ngsi-ld/default-context/", + "temperature": "ngsi-ld:temperature" + }, + "http://context.json-ld" + ], + "endpoint": "http://smartGondor.com", + "information": [ + { + "entities": [ + { + "id": "light1", + "type": "Light" + } + ], + "properties": [ + "temperature" + ] + } + ], + "type": "ContextSourceRegistration" +} diff --git a/test/unit/ngsi-ld/examples/contextAvailabilityRequests/updateCommands1.json b/test/unit/ngsi-ld/examples/contextAvailabilityRequests/updateCommands1.json new file mode 100644 index 000000000..67a912ab5 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextAvailabilityRequests/updateCommands1.json @@ -0,0 +1,18 @@ +{ + "dataProvided": { + "attrs": [ + "move" + ], + "entities": [ + { + "id": "light1", + "type": "Light" + } + ] + }, + "provider": { + "http": { + "url": "http://smartGondor.com" + } + } +} diff --git a/test/unit/ngsi-ld/examples/contextAvailabilityRequests/updateIoTAgent1.json b/test/unit/ngsi-ld/examples/contextAvailabilityRequests/updateIoTAgent1.json new file mode 100644 index 000000000..cbbe2e967 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextAvailabilityRequests/updateIoTAgent1.json @@ -0,0 +1,18 @@ +{ + "dataProvided": { + "attrs": [ + "pressure" + ], + "entities": [ + { + "id": "light1", + "type": "Light" + } + ] + }, + "provider": { + "http": { + "url": "http://smartGondor.com" + } + } +} diff --git a/test/unit/ngsi-ld/examples/contextAvailabilityRequests/updateIoTAgent2.json b/test/unit/ngsi-ld/examples/contextAvailabilityRequests/updateIoTAgent2.json new file mode 100644 index 000000000..ff70ab3f4 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextAvailabilityRequests/updateIoTAgent2.json @@ -0,0 +1,19 @@ +{ + "dataProvided": { + "attrs": [ + "luminance", + "commandAttr" + ], + "entities": [ + { + "id": "ANewLightName", + "type": "TheLightType" + } + ] + }, + "provider": { + "http": { + "url": "http://smartGondor.com" + } + } +} diff --git a/test/unit/ngsi-ld/examples/contextAvailabilityRequests/updateIoTAgent3.json b/test/unit/ngsi-ld/examples/contextAvailabilityRequests/updateIoTAgent3.json new file mode 100644 index 000000000..870aca6af --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextAvailabilityRequests/updateIoTAgent3.json @@ -0,0 +1,18 @@ +{ + "dataProvided": { + "attrs": [ + "luminance" + ], + "entities": [ + { + "id": "ANewLightName", + "type": "TheLightType" + } + ] + }, + "provider": { + "http": { + "url": "http://smartGondor.com" + } + } +} diff --git a/test/unit/ngsi-ld/examples/contextProviderResponses/queryInformationResponse.json b/test/unit/ngsi-ld/examples/contextProviderResponses/queryInformationResponse.json new file mode 100644 index 000000000..4cfefcaba --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextProviderResponses/queryInformationResponse.json @@ -0,0 +1,10 @@ +[ + { + "dimming": { + "type": "Percentage", + "value": 19 + }, + "id": "Light:light1", + "type": "Light" + } +] diff --git a/test/unit/ngsi-ld/examples/contextProviderResponses/queryInformationResponseEmptyAttributes.json b/test/unit/ngsi-ld/examples/contextProviderResponses/queryInformationResponseEmptyAttributes.json new file mode 100644 index 000000000..c8e314f6f --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextProviderResponses/queryInformationResponseEmptyAttributes.json @@ -0,0 +1,10 @@ +[ + { + "id": "Light:light1", + "temperature": { + "type": "centigrades", + "value": 19 + }, + "type": "Light" + } +] diff --git a/test/unit/ngsi-ld/examples/contextProviderResponses/queryInformationStaticAttributesResponse.json b/test/unit/ngsi-ld/examples/contextProviderResponses/queryInformationStaticAttributesResponse.json new file mode 100644 index 000000000..8aab8039b --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextProviderResponses/queryInformationStaticAttributesResponse.json @@ -0,0 +1,14 @@ +[ + { + "id": "Motion:motion1", + "location": { + "type": "Vector", + "value": "(123,523)" + }, + "moving": { + "type": "Boolean", + "value": true + }, + "type": "Motion" + } +] diff --git a/test/unit/ngsi-ld/examples/contextProviderResponses/updateInformationResponse2.json b/test/unit/ngsi-ld/examples/contextProviderResponses/updateInformationResponse2.json new file mode 100644 index 000000000..40932769b --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextProviderResponses/updateInformationResponse2.json @@ -0,0 +1,8 @@ +{ + "id": "RobotPre:TestRobotPre", + "moving": { + "type": "string", + "value": "" + }, + "type": "RobotPre" +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/createAutoprovisionDevice.json b/test/unit/ngsi-ld/examples/contextRequests/createAutoprovisionDevice.json new file mode 100644 index 000000000..85795840e --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/createAutoprovisionDevice.json @@ -0,0 +1,13 @@ +[ + { + "TimeInstant": { + "type": "Property", + "value": { + "@type": "ISO8601", + "@value": " " + } + }, + "id": "eii01201aaa", + "type": "sensor" + } +] diff --git a/test/unit/ngsi-ld/examples/contextRequests/createBidirectionalDevice.json b/test/unit/ngsi-ld/examples/contextRequests/createBidirectionalDevice.json new file mode 100644 index 000000000..7c0afd51b --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/createBidirectionalDevice.json @@ -0,0 +1,13 @@ +[ + { + "id": "TheFirstLight", + "location": { + "type": "Property", + "value": { + "@type": "geo:point", + "@value": "0, 0" + } + }, + "type": "TheLightType" + } +] diff --git a/test/unit/ngsi-ld/examples/contextRequests/createDatetimeProvisionedDevice.json b/test/unit/ngsi-ld/examples/contextRequests/createDatetimeProvisionedDevice.json new file mode 100644 index 000000000..26514c0f4 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/createDatetimeProvisionedDevice.json @@ -0,0 +1,13 @@ +[ + { + "id": "FirstMicroLight", + "timestamp": { + "type": "Property", + "value": { + "@type": "DateTime", + "@value": "1970-01-01T00:00:00.000Z" + } + }, + "type": "MicroLights" + } +] diff --git a/test/unit/ngsi-ld/examples/contextRequests/createGeopointProvisionedDevice.json b/test/unit/ngsi-ld/examples/contextRequests/createGeopointProvisionedDevice.json new file mode 100644 index 000000000..4c2e37421 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/createGeopointProvisionedDevice.json @@ -0,0 +1,13 @@ +[ + { + "id": "FirstMicroLight", + "location": { + "type": "Property", + "value": { + "@type": "geo:point", + "@value": "0, 0" + } + }, + "type": "MicroLights" + } +] diff --git a/test/unit/ngsi-ld/examples/contextRequests/createMinimumProvisionedDevice.json b/test/unit/ngsi-ld/examples/contextRequests/createMinimumProvisionedDevice.json new file mode 100644 index 000000000..047cb1edb --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/createMinimumProvisionedDevice.json @@ -0,0 +1,13 @@ +[ + { + "attr_name": { + "type": "Property", + "value": { + "@type": "string", + "@value": " " + } + }, + "id": "FirstMicroLight", + "type": "MicroLights" + } +] diff --git a/test/unit/ngsi-ld/examples/contextRequests/createProvisionedDevice.json b/test/unit/ngsi-ld/examples/contextRequests/createProvisionedDevice.json new file mode 100644 index 000000000..6e84c22a5 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/createProvisionedDevice.json @@ -0,0 +1,34 @@ +[ + { + "attr_name": { + "type": "Property", + "value": { + "@type": "string", + "@value": " " + } + }, + "commandAttr_info": { + "type": "Property", + "value": { + "@type": "commandResult", + "@value": " " + } + }, + "commandAttr_status": { + "type": "Property", + "value": { + "@type": "commandStatus", + "@value": "UNKNOWN" + } + }, + "hardcodedAttr": { + "type": "Property", + "value": { + "@type": "hardcodedType", + "@value": "hardcodedValue" + } + }, + "id": "TheFirstLight", + "type": "TheLightType" + } +] diff --git a/test/unit/ngsi-ld/examples/contextRequests/createProvisionedDeviceMultientity.json b/test/unit/ngsi-ld/examples/contextRequests/createProvisionedDeviceMultientity.json new file mode 100644 index 000000000..6e84c22a5 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/createProvisionedDeviceMultientity.json @@ -0,0 +1,34 @@ +[ + { + "attr_name": { + "type": "Property", + "value": { + "@type": "string", + "@value": " " + } + }, + "commandAttr_info": { + "type": "Property", + "value": { + "@type": "commandResult", + "@value": " " + } + }, + "commandAttr_status": { + "type": "Property", + "value": { + "@type": "commandStatus", + "@value": "UNKNOWN" + } + }, + "hardcodedAttr": { + "type": "Property", + "value": { + "@type": "hardcodedType", + "@value": "hardcodedValue" + } + }, + "id": "TheFirstLight", + "type": "TheLightType" + } +] diff --git a/test/unit/ngsi-ld/examples/contextRequests/createProvisionedDeviceWithGroupAndStatic.json b/test/unit/ngsi-ld/examples/contextRequests/createProvisionedDeviceWithGroupAndStatic.json new file mode 100644 index 000000000..b49077284 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/createProvisionedDeviceWithGroupAndStatic.json @@ -0,0 +1,59 @@ +[ + { + "attr_name": { + "type": "Property", + "value": { + "@type": "string", + "@value": " " + } + }, + "bootstrapServer": { + "type": "Property", + "value": { + "@type": "Address", + "@value": "127.0.0.1" + } + }, + "commandAttr_info": { + "type": "Property", + "value": { + "@type": "commandResult", + "@value": " " + } + }, + "commandAttr_status": { + "type": "Property", + "value": { + "@type": "commandStatus", + "@value": "UNKNOWN" + } + }, + "hardcodedAttr": { + "type": "Property", + "value": { + "@type": "hardcodedType", + "@value": "hardcodedValue" + } + }, + "id": "TheFirstLight", + "status": { + "type": "Property", + "value": true + }, + "type": "TheLightType", + "wheel1_info": { + "type": "Property", + "value": { + "@type": "commandResult", + "@value": " " + } + }, + "wheel1_status": { + "type": "Property", + "value": { + "@type": "commandStatus", + "@value": "UNKNOWN" + } + } + } +] diff --git a/test/unit/ngsi-ld/examples/contextRequests/createProvisionedDeviceWithGroupAndStatic2.json b/test/unit/ngsi-ld/examples/contextRequests/createProvisionedDeviceWithGroupAndStatic2.json new file mode 100644 index 000000000..a8dff3fa1 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/createProvisionedDeviceWithGroupAndStatic2.json @@ -0,0 +1,59 @@ +[ + { + "attr_name": { + "type": "Property", + "value": { + "@type": "string", + "@value": " " + } + }, + "bootstrapServer": { + "type": "Property", + "value": { + "@type": "Address", + "@value": "127.0.0.1" + } + }, + "commandAttr_info": { + "type": "Property", + "value": { + "@type": "commandResult", + "@value": " " + } + }, + "commandAttr_status": { + "type": "Property", + "value": { + "@type": "commandStatus", + "@value": "UNKNOWN" + } + }, + "hardcodedAttr": { + "type": "Property", + "value": { + "@type": "hardcodedType", + "@value": "hardcodedValue" + } + }, + "id": "TheFirstLight", + "status": { + "type": "Property", + "value": true + }, + "type": "SensorMachine", + "wheel1_info": { + "type": "Property", + "value": { + "@type": "commandResult", + "@value": " " + } + }, + "wheel1_status": { + "type": "Property", + "value": { + "@type": "commandStatus", + "@value": "UNKNOWN" + } + } + } +] diff --git a/test/unit/ngsi-ld/examples/contextRequests/createProvisionedDeviceWithGroupAndStatic3.json b/test/unit/ngsi-ld/examples/contextRequests/createProvisionedDeviceWithGroupAndStatic3.json new file mode 100644 index 000000000..d04f2d60b --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/createProvisionedDeviceWithGroupAndStatic3.json @@ -0,0 +1,17 @@ +[ + { + "dimming": { + "type": "Property", + "value": { + "@type": "Percentage", + "@value": " " + } + }, + "id": "light1", + "state": { + "type": "Property", + "value": true + }, + "type": "Light" + } +] diff --git a/test/unit/ngsi-ld/examples/contextRequests/createTimeInstantMinimumDevice.json b/test/unit/ngsi-ld/examples/contextRequests/createTimeInstantMinimumDevice.json new file mode 100644 index 000000000..8991d7ad2 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/createTimeInstantMinimumDevice.json @@ -0,0 +1,13 @@ + [ + { + "id": "FirstMicroLight", + "type": "MicroLights", + "attr_name": { + "type": "Property", + "value": { + "@type": "string", + "@value": " " + } + } + } + ] \ No newline at end of file diff --git a/test/unit/ngsi-ld/examples/contextRequests/createTimeinstantDevice.json b/test/unit/ngsi-ld/examples/contextRequests/createTimeinstantDevice.json new file mode 100644 index 000000000..fe8217812 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/createTimeinstantDevice.json @@ -0,0 +1,13 @@ +[ + { + "TimeInstant": { + "type": "Property", + "value": { + "@type": "ISO8601", + "@value": " " + } + }, + "id": "eii01201ttt", + "type": "sensor" + } +] diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContext.json b/test/unit/ngsi-ld/examples/contextRequests/updateContext.json new file mode 100644 index 000000000..e3135dbf0 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContext.json @@ -0,0 +1,11 @@ +{ + "@context": "http://context.json-ld", + "dimming": { + "type": "Property", + "value": 87 + }, + "state": { + "type": "Property", + "value": true + } +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContext1.json b/test/unit/ngsi-ld/examples/contextRequests/updateContext1.json new file mode 100644 index 000000000..ccd60bc39 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContext1.json @@ -0,0 +1,14 @@ +{ + "@context": "http://context.json-ld", + "dimming": { + "type": "Property", + "value": { + "@type": "Percentage", + "@value": "87" + } + }, + "state": { + "type": "Property", + "value": true + } +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContext3WithStatic.json b/test/unit/ngsi-ld/examples/contextRequests/updateContext3WithStatic.json new file mode 100644 index 000000000..a36ae4b79 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContext3WithStatic.json @@ -0,0 +1,17 @@ +{ + "@context": "http://context.json-ld", + "bootstrapServer": { + "type": "Property", + "value": { + "@type": "Address", + "@value": "127.0.0.1" + } + }, + "status": { + "type": "Property", + "value": { + "@type": "String", + "@value": "STARTING" + } + } +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContext4.json b/test/unit/ngsi-ld/examples/contextRequests/updateContext4.json new file mode 100644 index 000000000..a36ae4b79 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContext4.json @@ -0,0 +1,17 @@ +{ + "@context": "http://context.json-ld", + "bootstrapServer": { + "type": "Property", + "value": { + "@type": "Address", + "@value": "127.0.0.1" + } + }, + "status": { + "type": "Property", + "value": { + "@type": "String", + "@value": "STARTING" + } + } +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextAliasPlugin1.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextAliasPlugin1.json new file mode 100644 index 000000000..c4738f31f --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextAliasPlugin1.json @@ -0,0 +1,19 @@ +{ + "@context": "http://context.json-ld", + "pressure": { + "type": "Property", + "unitCode": { + "type": "Property", + "value": "Hgmm" + }, + "value": 20071103 + }, + "temperature": { + "type": "Property", + "unitCode": { + "type": "Property", + "value": "CEL" + }, + "value": 52 + } +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextAliasPlugin2.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextAliasPlugin2.json new file mode 100644 index 000000000..3aa0714f1 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextAliasPlugin2.json @@ -0,0 +1,11 @@ +{ + "@context": "http://context.json-ld", + "luminance": { + "type": "Property", + "unitCode": { + "type": "Property", + "value": "CAL" + }, + "value": 9 + } +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextAliasPlugin3.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextAliasPlugin3.json new file mode 100644 index 000000000..41e5d601d --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextAliasPlugin3.json @@ -0,0 +1,7 @@ +{ + "@context": "http://context.json-ld", + "unix_timestamp": { + "type": "Property", + "value": 99823423 + } +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextAliasPlugin4.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextAliasPlugin4.json new file mode 100644 index 000000000..6e1161449 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextAliasPlugin4.json @@ -0,0 +1,7 @@ +{ + "@context": "http://context.json-ld", + "active_power": { + "type": "Property", + "value": 0.45 + } +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextAliasPlugin5.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextAliasPlugin5.json new file mode 100644 index 000000000..cbd5da54b --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextAliasPlugin5.json @@ -0,0 +1,7 @@ +{ + "@context": "http://context.json-ld", + "status": { + "type": "Property", + "value": false + } +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextAliasPlugin6.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextAliasPlugin6.json new file mode 100644 index 000000000..3f3059030 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextAliasPlugin6.json @@ -0,0 +1,10 @@ +{ + "@context": "http://context.json-ld", + "keep_alive": { + "type": "Property", + "value": { + "@type": "None", + "@value": null + } + } +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextAliasPlugin7.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextAliasPlugin7.json new file mode 100644 index 000000000..ba86572c2 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextAliasPlugin7.json @@ -0,0 +1,13 @@ +{ + "@context": "http://context.json-ld", + "tags": { + "type": "Property", + "value": { + "@type": "Array", + "@value": [ + "iot", + "device" + ] + } + } +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextAliasPlugin8.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextAliasPlugin8.json new file mode 100644 index 000000000..f40bc0f96 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextAliasPlugin8.json @@ -0,0 +1,15 @@ +{ + "@context": "http://context.json-ld", + "configuration": { + "type": "Property", + "value": { + "@type": "Object", + "@value": { + "firmware": { + "hash": "cf23df2207d99a74fbe169e3eba035e633b65d94", + "version": "1.1.0" + } + } + } + } +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextAliasPlugin9.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextAliasPlugin9.json new file mode 100644 index 000000000..622c370cb --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextAliasPlugin9.json @@ -0,0 +1,10 @@ +{ + "@context": "http://context.json-ld", + "configuration": { + "type": "Property", + "value": { + "@type": "Object", + "@value": "string_value" + } + } +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextAutocast1.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextAutocast1.json new file mode 100644 index 000000000..8fa030983 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextAutocast1.json @@ -0,0 +1,7 @@ +{ + "@context": "http://context.json-ld", + "pressure": { + "type": "Property", + "value": 23 + } +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextAutocast2.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextAutocast2.json new file mode 100644 index 000000000..3d2b08014 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextAutocast2.json @@ -0,0 +1,7 @@ +{ + "@context": "http://context.json-ld", + "temperature": { + "type": "Property", + "value": 14.4 + } +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextAutocast3.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextAutocast3.json new file mode 100644 index 000000000..b42802e8a --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextAutocast3.json @@ -0,0 +1,7 @@ +{ + "@context": "http://context.json-ld", + "status": { + "type": "Property", + "value": true + } +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextAutocast4.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextAutocast4.json new file mode 100644 index 000000000..cbd5da54b --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextAutocast4.json @@ -0,0 +1,7 @@ +{ + "@context": "http://context.json-ld", + "status": { + "type": "Property", + "value": false + } +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextAutocast5.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextAutocast5.json new file mode 100644 index 000000000..3f3059030 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextAutocast5.json @@ -0,0 +1,10 @@ +{ + "@context": "http://context.json-ld", + "keep_alive": { + "type": "Property", + "value": { + "@type": "None", + "@value": null + } + } +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextAutocast6.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextAutocast6.json new file mode 100644 index 000000000..ba86572c2 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextAutocast6.json @@ -0,0 +1,13 @@ +{ + "@context": "http://context.json-ld", + "tags": { + "type": "Property", + "value": { + "@type": "Array", + "@value": [ + "iot", + "device" + ] + } + } +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextAutocast7.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextAutocast7.json new file mode 100644 index 000000000..f40bc0f96 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextAutocast7.json @@ -0,0 +1,15 @@ +{ + "@context": "http://context.json-ld", + "configuration": { + "type": "Property", + "value": { + "@type": "Object", + "@value": { + "firmware": { + "hash": "cf23df2207d99a74fbe169e3eba035e633b65d94", + "version": "1.1.0" + } + } + } + } +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextCommandError.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextCommandError.json new file mode 100644 index 000000000..7dc987272 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextCommandError.json @@ -0,0 +1,10 @@ +{ + "position_info": { + "type": "commandResult", + "value": "Stalled" + }, + "position_status": { + "type": "commandStatus", + "value": "ERROR" + } +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextCommandExpired.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextCommandExpired.json new file mode 100644 index 000000000..3af9b884c --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextCommandExpired.json @@ -0,0 +1,10 @@ +{ + "position_info": { + "type": "commandResult", + "value": "EXPIRED" + }, + "position_status": { + "type": "commandStatus", + "value": "ERROR" + } +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextCommandFinish.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextCommandFinish.json new file mode 100644 index 000000000..01c7b8d1a --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextCommandFinish.json @@ -0,0 +1,10 @@ +{ + "position_info": { + "type": "commandResult", + "value": "[72, 368, 1]" + }, + "position_status": { + "type": "commandStatus", + "value": "FINISHED" + } +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextCommandStatus.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextCommandStatus.json new file mode 100644 index 000000000..9f0c4c859 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextCommandStatus.json @@ -0,0 +1,6 @@ +{ + "position_status": { + "type": "commandStatus", + "value": "PENDING" + } +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextCompressTimestamp1.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextCompressTimestamp1.json new file mode 100644 index 000000000..9f5035d28 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextCompressTimestamp1.json @@ -0,0 +1,14 @@ +{ + "@context": "http://context.json-ld", + "TheTargetValue": { + "type": "Property", + "value": { + "@type": "DateTime", + "@value": "+002007-11-03T13:18:05" + } + }, + "state": { + "type": "Property", + "value": true + } +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextCompressTimestamp2.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextCompressTimestamp2.json new file mode 100644 index 000000000..c9bb64732 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextCompressTimestamp2.json @@ -0,0 +1,21 @@ +{ + "@context": "http://context.json-ld", + "TheTargetValue": { + "type": "Property", + "value": { + "@type": "DateTime", + "@value": "+002007-11-03T13:18:05" + } + }, + "state": { + "TimeInstant": { + "type": "Property", + "value": { + "@type": "DateTime", + "@value": "+002007-11-03T13:18:05" + } + }, + "type": "Property", + "value": true + } +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin1.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin1.json new file mode 100644 index 000000000..e9a90159b --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin1.json @@ -0,0 +1,7 @@ +{ + "@context": "http://context.json-ld", + "pressure": { + "type": "Property", + "value": 1040 + } +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin10.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin10.json new file mode 100644 index 000000000..5b67a987a --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin10.json @@ -0,0 +1,7 @@ +{ + "@context": "http://context.json-ld", + "updated": { + "type": "Property", + "value": false + } +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin11.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin11.json new file mode 100644 index 000000000..217c3587a --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin11.json @@ -0,0 +1,7 @@ +{ + "@context": "http://context.json-ld", + "consumption": { + "type": "Property", + "value": 52 + } +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin12.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin12.json new file mode 100644 index 000000000..f905fce00 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin12.json @@ -0,0 +1,7 @@ +{ + "@context": "http://context.json-ld", + "consumption_x": { + "type": "Property", + "value": 0.44 + } +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin13.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin13.json new file mode 100644 index 000000000..230cd3f96 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin13.json @@ -0,0 +1,11 @@ +{ + "@context": "http://context.json-ld", + "consumption_x": { + "type": "Property", + "value": 200 + }, + "pressure": { + "type": "Property", + "value": 10 + } +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin2.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin2.json new file mode 100644 index 000000000..53c2276c2 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin2.json @@ -0,0 +1,21 @@ +{ + "@context": "http://context.json-ld", + "humidity": { + "type": "Property", + "value": { + "@type": "Percentage", + "@value": "12" + } + }, + "pressure": { + "type": "Property", + "value": 1040 + }, + "weather": { + "type": "Property", + "value": { + "@type": "Summary", + "@value": "Humidity 6 and pressure 1040" + } + } +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin3.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin3.json new file mode 100644 index 000000000..f745eae08 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin3.json @@ -0,0 +1,7 @@ +{ + "@context": "http://context.json-ld", + "consumption": { + "type": "Property", + "value": 0.44 + } +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin4.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin4.json new file mode 100644 index 000000000..4e4023a5a --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin4.json @@ -0,0 +1,21 @@ +{ + "@context": "http://context.json-ld", + "humidity12": { + "type": "Property", + "value": { + "@type": "Percentage", + "@value": "12" + } + }, + "pressure25": { + "type": "Property", + "value": 52 + }, + "weather": { + "type": "Property", + "value": { + "@type": "Summary", + "@value": "Humidity 6 and pressure 1040" + } + } +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin5.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin5.json new file mode 100644 index 000000000..5828c26da --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin5.json @@ -0,0 +1,10 @@ +{ + "@context": "http://context.json-ld", + "alive": { + "type": "Property", + "value": { + "@type": "None", + "@value": null + } + } +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin6.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin6.json new file mode 100644 index 000000000..766a93996 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin6.json @@ -0,0 +1,13 @@ +{ + "@context": "http://context.json-ld", + "manufacturer": { + "type": "Property", + "value": { + "@type": "Object", + "@value": { + "VAT": "U12345678", + "name": "Manufacturer1" + } + } + } +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin7.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin7.json new file mode 100644 index 000000000..0242ad0e1 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin7.json @@ -0,0 +1,14 @@ +{ + "@context": "http://context.json-ld", + "revisions": { + "type": "Property", + "value": { + "@type": "Array", + "@value": [ + "v0.1", + "v0.2", + "v0.3" + ] + } + } +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin8.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin8.json new file mode 100644 index 000000000..db3e85ec2 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin8.json @@ -0,0 +1,7 @@ +{ + "@context": "http://context.json-ld", + "consumption": { + "type": "Property", + "value": 8.8 + } +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin9.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin9.json new file mode 100644 index 000000000..46c3a594c --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin9.json @@ -0,0 +1,7 @@ +{ + "@context": "http://context.json-ld", + "updated": { + "type": "Property", + "value": true + } +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityPlugin1.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityPlugin1.json new file mode 100644 index 000000000..026ffb9d6 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityPlugin1.json @@ -0,0 +1,21 @@ +{ + "actionType": "append", + "entities": [ + { + "id": "ws4", + "pressure": { + "type": "Hgmm", + "value": "52" + }, + "type": "WeatherStation" + }, + { + "humidity": { + "type": "Percentage", + "value": "12" + }, + "id": "Higro2000", + "type": "Higrometer" + } + ] +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityPlugin2.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityPlugin2.json new file mode 100644 index 000000000..9d849dbc7 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityPlugin2.json @@ -0,0 +1,21 @@ +{ + "actionType": "append", + "entities": [ + { + "id": "ws4", + "pressure": { + "type": "Hgmm", + "value": "52" + }, + "type": "WeatherStation" + }, + { + "humidity": { + "type": "Percentage", + "value": "12" + }, + "id": "Higro2000", + "type": "WeatherStation" + } + ] +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityPlugin3.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityPlugin3.json new file mode 100644 index 000000000..c5475126c --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityPlugin3.json @@ -0,0 +1,25 @@ +{ + "actionType": "append", + "entities": [ + { + "id": "ws4", + "pressure": { + "type": "Hgmm", + "value": "52" + }, + "sn": { + "type": "Number", + "value": "5" + }, + "type": "WeatherStation" + }, + { + "humidity": { + "type": "Percentage", + "value": "12" + }, + "id": "Station Number 50", + "type": "WeatherStation" + } + ] +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityPlugin4.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityPlugin4.json new file mode 100644 index 000000000..915353035 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityPlugin4.json @@ -0,0 +1,17 @@ +{ + "actionType": "append", + "entities": [ + { + "id": "ws5", + "type": "WeatherStation" + }, + { + "id": "Higro2000", + "pressure": { + "type": "Hgmm", + "value": "16" + }, + "type": "Higrometer" + } + ] +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityPlugin5.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityPlugin5.json new file mode 100644 index 000000000..532967ad6 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityPlugin5.json @@ -0,0 +1,25 @@ +{ + "actionType": "append", + "entities": [ + { + "id": "ws6", + "type": "WeatherStation" + }, + { + "id": "Higro2002", + "pressure": { + "type": "Hgmm", + "value": "17" + }, + "type": "Higrometer" + }, + { + "id": "Higro2000", + "pressure": { + "type": "Hgmm", + "value": "16" + }, + "type": "Higrometer" + } + ] +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityPlugin6.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityPlugin6.json new file mode 100644 index 000000000..aaa1728bf --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityPlugin6.json @@ -0,0 +1,33 @@ +{ + "actionType": "append", + "entities": [ + { + "id": "Sensor", + "type": "Sensor" + }, + { + "id": "SO1", + "type": "WM", + "vol": { + "type": "number", + "value": "38" + } + }, + { + "id": "SO2", + "type": "WM" + }, + { + "id": "SO3", + "type": "WM" + }, + { + "id": "SO4", + "type": "WM" + }, + { + "id": "SO5", + "type": "WM" + } + ] +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityPlugin7.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityPlugin7.json new file mode 100644 index 000000000..a962e17ca --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityPlugin7.json @@ -0,0 +1,45 @@ +{ + "actionType": "append", + "entities": [ + { + "id": "Sensor", + "type": "Sensor" + }, + { + "id": "SO1", + "type": "WM", + "vol": { + "type": "number", + "value": "38" + } + }, + { + "id": "SO2", + "type": "WM", + "vol": { + "type": "number", + "value": "39" + } + }, + { + "id": "SO3", + "type": "WM", + "vol": { + "type": "number", + "value": "40" + } + }, + { + "id": "SO4", + "type": "WM" + }, + { + "id": "SO5", + "type": "WM", + "vol": { + "type": "number", + "value": "42" + } + } + ] +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityPlugin8.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityPlugin8.json new file mode 100644 index 000000000..6db2b63a7 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityPlugin8.json @@ -0,0 +1,31 @@ +{ + "actionType": "append", + "entities": [ + { + "id": "ws7", + "type": "WeatherStation" + }, + { + "id": "Higro2002", + "pressure": { + "metadata": { + "unitCode": { + "type": "Text", + "value": "Hgmm" + } + }, + "type": "Hgmm", + "value": "17" + }, + "type": "Higrometer" + }, + { + "id": "Higro2000", + "pressure": { + "type": "Hgmm", + "value": "16" + }, + "type": "Higrometer" + } + ] +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityTimestampPlugin1.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityTimestampPlugin1.json new file mode 100644 index 000000000..7a9e78dfa --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityTimestampPlugin1.json @@ -0,0 +1,37 @@ +{ + "actionType": "append", + "entities": [ + { + "TimeInstant": { + "type": "DateTime", + "value": "2016-05-30T16:25:22.304Z" + }, + "id": "ws4", + "pressure": { + "metadata": { + "TimeInstant": { + "type": "DateTime", + "value": "2016-05-30T16:25:22.304Z" + } + }, + "type": "Hgmm", + "value": "52" + }, + "type": "WeatherStation" + }, + { + "humidity": { + "metadata": { + "TimeInstant": { + "type": "DateTime", + "value": "2016-05-30T16:25:22.304Z" + } + }, + "type": "Percentage", + "value": "12" + }, + "id": "Higro2000", + "type": "Higrometer" + } + ] +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityTimestampPlugin2.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityTimestampPlugin2.json new file mode 100644 index 000000000..99d6ce449 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityTimestampPlugin2.json @@ -0,0 +1,27 @@ +{ + "actionType": "append", + "entities": [ + { + "id": "ws4", + "type": "WeatherStation" + }, + { + "TimeInstant": { + "type": "DateTime", + "value": "2018-06-13T13:28:34.611Z" + }, + "humidity": { + "metadata": { + "TimeInstant": { + "type": "DateTime", + "value": "2018-06-13T13:28:34.611Z" + } + }, + "type": "Percentage", + "value": "12" + }, + "id": "Higro2000", + "type": "Higrometer" + } + ] +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityTimestampPlugin3.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityTimestampPlugin3.json new file mode 100644 index 000000000..1189b71ec --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityTimestampPlugin3.json @@ -0,0 +1,31 @@ +{ + "actionType": "append", + "entities": [ + { + "TimeInstant": { + "type": "DateTime", + "value": "2018-06-13T13:28:34.611Z" + }, + "id": "ws5", + "type": "WeatherStation" + }, + { + "TimeInstant": { + "type": "DateTime", + "value": "2018-06-13T13:28:34.611Z" + }, + "humidity": { + "metadata": { + "TimeInstant": { + "type": "DateTime", + "value": "2018-06-13T13:28:34.611Z" + } + }, + "type": "Percentage", + "value": "16" + }, + "id": "Higro2000", + "type": "Higrometer" + } + ] +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityTimestampPlugin4.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityTimestampPlugin4.json new file mode 100644 index 000000000..69d98fa12 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityTimestampPlugin4.json @@ -0,0 +1,33 @@ +{ + "actionType": "append", + "entities": [ + { + "PING_info": { + "metadata": { + "TimeInstant": { + "type": "DateTime", + "value": "2015-08-05T07:35:01.468Z" + } + }, + "type": "commandResult", + "value": "1234567890" + }, + "PING_status": { + "metadata": { + "TimeInstant": { + "type": "DateTime", + "value": "2015-08-05T07:35:01.468Z" + } + }, + "type": "commandStatus", + "value": "OK" + }, + "TimeInstant": { + "type": "DateTime", + "value": "2015-08-05T07:35:01.468Z" + }, + "id": "sensorCommand", + "type": "SensorCommand" + } + ] +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextProcessTimestamp.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextProcessTimestamp.json new file mode 100644 index 000000000..a1614b84f --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextProcessTimestamp.json @@ -0,0 +1,21 @@ +{ + "@context": "http://context.json-ld", + "TimeInstant": { + "type": "Property", + "value": { + "@type": "DateTime", + "@value": "2016-05-30T16:25:22.304Z" + } + }, + "state": { + "TimeInstant": { + "type": "Property", + "value": { + "@type": "DateTime", + "@value": "2016-05-30T16:25:22.304Z" + } + }, + "type": "Property", + "value": true + } +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextStaticAttributes.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextStaticAttributes.json new file mode 100644 index 000000000..7d13cbc54 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextStaticAttributes.json @@ -0,0 +1,14 @@ +{ + "@context": "http://context.json-ld", + "location": { + "type": "Property", + "value": { + "@type": "geo:point", + "@value": "153,523" + } + }, + "moving": { + "type": "Property", + "value": true + } +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextStaticAttributesMetadata.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextStaticAttributesMetadata.json new file mode 100644 index 000000000..f49a4e295 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextStaticAttributesMetadata.json @@ -0,0 +1,19 @@ +{ + "@context": "http://context.json-ld", + "controlledProperty": { + "includes": { + "type": "Property", + "value": "bell" + }, + "type": "Property", + "value": "StaticValue" + }, + "luminosity": { + "type": "Property", + "unitCode": { + "type": "Property", + "value": "CAL" + }, + "value": 100 + } +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextTimestamp.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextTimestamp.json new file mode 100644 index 000000000..5dc057516 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextTimestamp.json @@ -0,0 +1,38 @@ +{ + "@context": "http://context.json-ld", + "TimeInstant": { + "type": "Property", + "value": { + "@type": "DateTime", + "@value": "2015-08-05T07:35:01.468Z" + } + }, + "dimming": { + "TimeInstant": { + "type": "Property", + "value": { + "@type": "DateTime", + "@value": "2015-08-05T07:35:01.468Z" + } + }, + "type": "Property", + "value": { + "@type": "number", + "@value": 87 + } + }, + "state": { + "TimeInstant": { + "type": "Property", + "value": { + "@type": "DateTime", + "@value": "2015-08-05T07:35:01.468Z" + } + }, + "type": "Property", + "value": { + "@type": "boolean", + "@value": true + } + } +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextTimestampOverride.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextTimestampOverride.json new file mode 100644 index 000000000..cebac97ef --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextTimestampOverride.json @@ -0,0 +1,17 @@ +{ + "@context": "http://context.json-ld", + "TimeInstant": { + "type": "Property", + "value": { + "@type": "DateTime", + "@value": "2015-12-14T08:06:01.468Z" + } + }, + "state": { + "type": "Property", + "value": { + "@type": "boolean", + "@value": true + } + } +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextTimestampOverrideWithoutMilis.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextTimestampOverrideWithoutMilis.json new file mode 100644 index 000000000..93bc61149 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextTimestampOverrideWithoutMilis.json @@ -0,0 +1,17 @@ +{ + "@context": "http://context.json-ld", + "TimeInstant": { + "type": "Property", + "value": { + "@type": "DateTime", + "@value": "2022-10-22T22:22:22Z" + } + }, + "state": { + "type": "Property", + "value": { + "@type": "boolean", + "@value": true + } + } +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextTimestampTimezone.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextTimestampTimezone.json new file mode 100644 index 000000000..64273c1e7 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextTimestampTimezone.json @@ -0,0 +1,38 @@ +{ + "@context": "http://context.json-ld", + "TimeInstant": { + "type": "Property", + "value": { + "@type": "DateTime", + "@value": "2015-08-05T00:35:01.468-07:00" + } + }, + "dimming": { + "TimeInstant": { + "type": "Property", + "value": { + "@type": "DateTime", + "@value": "2015-08-05T00:35:01.468-07:00" + } + }, + "type": "Property", + "value": { + "@type": "number", + "@value": 87 + } + }, + "state": { + "TimeInstant": { + "type": "Property", + "value": { + "@type": "DateTime", + "@value": "2015-08-05T00:35:01.468-07:00" + } + }, + "type": "Property", + "value": { + "@type": "boolean", + "@value": true + } + } +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateProvisionActiveAttributes1.json b/test/unit/ngsi-ld/examples/contextRequests/updateProvisionActiveAttributes1.json new file mode 100644 index 000000000..7f7787404 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateProvisionActiveAttributes1.json @@ -0,0 +1,6 @@ +{ + "temperature": { + "type": "centigrades", + "value": " " + } +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateProvisionCommands1.json b/test/unit/ngsi-ld/examples/contextRequests/updateProvisionCommands1.json new file mode 100644 index 000000000..fb700ff73 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateProvisionCommands1.json @@ -0,0 +1,14 @@ +{ + "move_info": { + "type": "commandResult", + "value": " " + }, + "move_status": { + "type": "commandStatus", + "value": "UNKNOWN" + }, + "temperature": { + "type": "centigrades", + "value": " " + } +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateProvisionDeviceStatic.json b/test/unit/ngsi-ld/examples/contextRequests/updateProvisionDeviceStatic.json new file mode 100644 index 000000000..5880339bf --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateProvisionDeviceStatic.json @@ -0,0 +1,24 @@ +{ + "cellID": { + "type": "Integer", + "value": "435" + }, + "location": { + "type": "geo:json", + "value": { + "coordinates": [ + -3.164485591715449, + 40.62785133667262 + ], + "type": "Point" + } + }, + "newAttribute": { + "type": "Integer", + "value": " " + }, + "serverURL": { + "type": "URL", + "value": "http://fakeserver.com" + } +} diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateProvisionMinimumDevice.json b/test/unit/ngsi-ld/examples/contextRequests/updateProvisionMinimumDevice.json new file mode 100644 index 000000000..a73bd1970 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateProvisionMinimumDevice.json @@ -0,0 +1,6 @@ +{ + "newAttribute": { + "type": "Integer", + "value": " " + } +} diff --git a/test/unit/ngsi-ld/examples/contextResponses/queryContext1Success.json b/test/unit/ngsi-ld/examples/contextResponses/queryContext1Success.json new file mode 100644 index 000000000..753358dc4 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextResponses/queryContext1Success.json @@ -0,0 +1,10 @@ +{ + "dimming": { + "type": "Percentage", + "value": "23" + }, + "state": { + "type": "Boolean", + "value": "False" + } +} diff --git a/test/unit/ngsi-ld/examples/contextResponses/queryContextCompressTimestamp1Success.json b/test/unit/ngsi-ld/examples/contextResponses/queryContextCompressTimestamp1Success.json new file mode 100644 index 000000000..7c2f1f002 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextResponses/queryContextCompressTimestamp1Success.json @@ -0,0 +1,12 @@ +{ + "TheTargetValue": { + "type": "DateTime", + "value": "+002007-11-03T13:18:05" + }, + "id": "light1", + "state": { + "type": "Boolean", + "value": "true" + }, + "type": "Light" +} diff --git a/test/unit/ngsi-ld/examples/contextResponses/updateContext1Failed.json b/test/unit/ngsi-ld/examples/contextResponses/updateContext1Failed.json new file mode 100644 index 000000000..a11021341 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextResponses/updateContext1Failed.json @@ -0,0 +1,4 @@ +{ + "description": "payload size: 1500000, max size supported: 1048576", + "error": "RequestEntityTooLarge" +} diff --git a/test/unit/ngsi-ld/examples/contextResponses/updateContext2Failed.json b/test/unit/ngsi-ld/examples/contextResponses/updateContext2Failed.json new file mode 100644 index 000000000..0b38e1a52 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextResponses/updateContext2Failed.json @@ -0,0 +1,4 @@ +{ + "description": "The incoming request is invalid in this context.", + "error": "BadRequest" +} diff --git a/test/unit/ngsi-ld/examples/iotamResponses/registrationSuccess.json b/test/unit/ngsi-ld/examples/iotamResponses/registrationSuccess.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/test/unit/ngsi-ld/examples/iotamResponses/registrationSuccess.json @@ -0,0 +1 @@ +{} diff --git a/test/unit/ngsi-ld/examples/subscriptionRequests/bidirectionalNotification.json b/test/unit/ngsi-ld/examples/subscriptionRequests/bidirectionalNotification.json new file mode 100644 index 000000000..6397da5b9 --- /dev/null +++ b/test/unit/ngsi-ld/examples/subscriptionRequests/bidirectionalNotification.json @@ -0,0 +1,13 @@ +{ + "data": [ + { + "id": "TheFirstLight", + "location": { + "type": "geo:point", + "value": "12.4, -9.6" + }, + "type": "TheLightType" + } + ], + "subscriptionId": "51c0ac9ed714fb3b37d7d5a8" +} diff --git a/test/unit/ngsi-ld/examples/subscriptionRequests/bidirectionalSubscriptionRequest.json b/test/unit/ngsi-ld/examples/subscriptionRequests/bidirectionalSubscriptionRequest.json new file mode 100644 index 000000000..544d5c79f --- /dev/null +++ b/test/unit/ngsi-ld/examples/subscriptionRequests/bidirectionalSubscriptionRequest.json @@ -0,0 +1,20 @@ +{ + "entities": [ + { + "id": "TheFirstLight", + "type": "TheLightType" + } + ], + "notification": { + "attributes": [ + "location" + ], + "http": { + "url": "http://smartGondor.com/notify" + } + }, + "type": "Subscription", + "watchedAttributes": [ + "location" + ] +} diff --git a/test/unit/ngsi-ld/examples/subscriptionRequests/errorNotification.json b/test/unit/ngsi-ld/examples/subscriptionRequests/errorNotification.json new file mode 100644 index 000000000..2ab64e298 --- /dev/null +++ b/test/unit/ngsi-ld/examples/subscriptionRequests/errorNotification.json @@ -0,0 +1,3 @@ +{ + "foo": "A very wrongly formated NGSIv2 notification..." +} diff --git a/test/unit/ngsi-ld/examples/subscriptionRequests/simpleNotification.json b/test/unit/ngsi-ld/examples/subscriptionRequests/simpleNotification.json new file mode 100644 index 000000000..2f9c3e6c1 --- /dev/null +++ b/test/unit/ngsi-ld/examples/subscriptionRequests/simpleNotification.json @@ -0,0 +1,13 @@ +{ + "data": [ + { + "attr_name": { + "type": "string", + "value": "The Attribute Value" + }, + "id": "FirstMicroLight", + "type": "MicroLights" + } + ], + "subscriptionId": "51c0ac9ed714fb3b37d7d5a8" +} diff --git a/test/unit/ngsi-ld/examples/subscriptionRequests/simpleSubscriptionRequest.json b/test/unit/ngsi-ld/examples/subscriptionRequests/simpleSubscriptionRequest.json new file mode 100644 index 000000000..060e5c893 --- /dev/null +++ b/test/unit/ngsi-ld/examples/subscriptionRequests/simpleSubscriptionRequest.json @@ -0,0 +1,18 @@ +{ + "entities": [ + { + "id": "FirstMicroLight", + "type": "MicroLights" + } + ], + "notification": { + "attributes": [], + "http": { + "url": "http://smartGondor.com/notify" + } + }, + "type": "Subscription", + "watchedAttributes": [ + "attr_name" + ] +} diff --git a/test/unit/ngsi-ld/examples/subscriptionRequests/simpleSubscriptionRequest2.json b/test/unit/ngsi-ld/examples/subscriptionRequests/simpleSubscriptionRequest2.json new file mode 100644 index 000000000..5983d01de --- /dev/null +++ b/test/unit/ngsi-ld/examples/subscriptionRequests/simpleSubscriptionRequest2.json @@ -0,0 +1,18 @@ +{ + "entities": [ + { + "id": "light1", + "type": "Light" + } + ], + "notification": { + "attributes": [], + "http": { + "url": "http://smartGondor.com/notify" + } + }, + "type": "Subscription", + "watchedAttributes": [ + "dimming" + ] +} diff --git a/test/unit/ngsi-ld/expressions/expressionBasedTransformations-test.js b/test/unit/ngsi-ld/expressions/expressionBasedTransformations-test.js new file mode 100644 index 000000000..2b7fce615 --- /dev/null +++ b/test/unit/ngsi-ld/expressions/expressionBasedTransformations-test.js @@ -0,0 +1,905 @@ +/* + * Copyright 2020 Telefonica Investigación y Desarrollo, S.A.U + * + * This file is part of fiware-iotagent-lib + * + * fiware-iotagent-lib 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. + * + * fiware-iotagent-lib 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 fiware-iotagent-lib. + * If not, see http://www.gnu.org/licenses/. + * + * For those usages not covered by the GNU Affero General Public License + * please contact with::[contacto@tid.es] + * + * Modified by: Jason Fox - FIWARE Foundation + */ + +/* jshint camelcase: false */ + +const iotAgentLib = require('../../../../lib/fiware-iotagent-lib'); +const utils = require('../../../tools/utils'); +const should = require('should'); +const logger = require('logops'); +const nock = require('nock'); +let contextBrokerMock; +const iotAgentConfig = { + logLevel: 'FATAL', + contextBroker: { + host: '192.168.1.1', + port: '1026', + ngsiVersion: 'ld', + jsonLdContext: 'http://context.json-ld' + }, + server: { + port: 4041 + }, + types: { + Light: { + commands: [], + type: 'Light', + lazy: [], + active: [ + { + object_id: 'p', + name: 'pressure', + type: 'Number' + }, + { + object_id: 'e', + name: 'consumption', + type: 'Number' + }, + { + object_id: 'a', + name: 'alive', + type: 'None' + }, + { + object_id: 'u', + name: 'updated', + type: 'Boolean' + }, + { + object_id: 'm', + name: 'manufacturer', + type: 'Object' + }, + { + object_id: 'r', + name: 'revisions', + type: 'Array' + }, + { + object_id: 'x', + name: 'consumption_x', + type: 'Number', + expression: '${@pressure * 20}' + } + ] + }, + LightError: { + commands: [], + type: 'Light', + lazy: [], + active: [ + { + object_id: 'p', + name: 'pressure', + type: 'Number', + expression: '${@pressure * / 20}' + } + ] + }, + WeatherStation: { + commands: [], + type: 'WeatherStation', + lazy: [], + active: [ + { + object_id: 'p', + name: 'pressure', + type: 'Number', + expression: '${@pressure * 20}' + }, + { + object_id: 'e', + name: 'consumption', + type: 'Number', + expression: '${@consumption * 20}' + }, + { + object_id: 'h', + name: 'humidity', + type: 'Percentage' + }, + { + name: 'weather', + type: 'Summary', + expression: 'Humidity ${@humidity / 2} and pressure ${@pressure * 20}' + }, + { + object_id: 'a', + name: 'alive', + type: 'None', + expression: '${@alive * 20}' + }, + { + object_id: 'u', + name: 'updated', + type: 'Boolean', + expression: '${@updated * 20}' + } + ] + }, + WeatherStationMultiple: { + commands: [], + type: 'WeatherStation', + lazy: [], + active: [ + { + object_id: 'p', + name: 'pressure', + type: 'Number', + expression: '${trim(@pressure)}' + }, + { + object_id: 'p25', + name: 'pressure25', + type: 'Number' + }, + { + object_id: 'e', + name: 'consumption', + type: 'Number', + expression: '${trim(@consumption)}' + }, + { + object_id: 'h', + name: 'humidity12', + type: 'Percentage' + }, + { + name: 'weather', + type: 'Summary', + expression: 'Humidity ${@humidity12 / 2} and pressure ${@pressure25 * 20}' + }, + { + object_id: 'a', + name: 'alive', + type: 'None', + expression: '${trim(@alive)}' + }, + { + object_id: 'u', + name: 'updated', + type: 'Boolean', + expression: '${trim(@updated)}' + } + ] + } + }, + service: 'smartGondor', + subservice: 'gardens', + providerUrl: 'http://smartGondor.com' +}; + +describe('NGSI-LD - Expression-based transformations plugin', function() { + beforeEach(function(done) { + logger.setLevel('FATAL'); + + iotAgentLib.activate(iotAgentConfig, function() { + iotAgentLib.clearAll(function() { + iotAgentLib.addUpdateMiddleware(iotAgentLib.dataPlugins.attributeAlias.update); + iotAgentLib.addQueryMiddleware(iotAgentLib.dataPlugins.attributeAlias.query); + iotAgentLib.addUpdateMiddleware(iotAgentLib.dataPlugins.expressionTransformation.update); + done(); + }); + }); + }); + + afterEach(function(done) { + iotAgentLib.clearAll(function() { + iotAgentLib.deactivate(done); + }); + }); + + describe('When an update comes for expressions with syntax errors', function() { + // Case: Update for an attribute with bad expression + const values = [ + { + name: 'p', + type: 'centigrades', + value: '52' + } + ]; + + it('should apply the expression before sending the values', function(done) { + iotAgentLib.update('light1', 'LightError', '', values, function(error) { + should.exist(error); + error.name.should.equal('INVALID_EXPRESSION'); + error.code.should.equal(400); + done(); + }); + }); + }); + + describe('When there are expression attributes that are just calculated (not sent by the device)', function() { + // Case: Expression which results is sent as a new attribute + const values = [ + { + name: 'p', + type: 'Number', + value: 52 + }, + { + name: 'h', + type: 'Percentage', + value: '12' + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .patch( + '/ngsi-ld/v1/entities/ws1/attrs', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin2.json' + ) + ) + .query({ type: 'WeatherStation' }) + .reply(204); + }); + + it('should calculate them and add them to the payload', function(done) { + iotAgentLib.update('ws1', 'WeatherStation', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an expression with multiple variables with numbers arrive', function() { + // Case: Update for integer and string attributes with expression + + const values = [ + { + name: 'p25', + type: 'Number', + value: 52 + }, + { + name: 'h', + type: 'percentage', + value: '12' + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .patch( + '/ngsi-ld/v1/entities/ws1/attrs', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin4.json' + ) + ) + .query({ type: 'WeatherStation' }) + .reply(204); + }); + + it('should calculate it and add it to the payload', function(done) { + iotAgentLib.update('ws1', 'WeatherStationMultiple', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an update comes for attributes without expressions and type integer', function() { + // Case: Update for an integer attribute without expression + const values = [ + { + name: 'e', + type: 'Number', + value: 52 + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .patch( + '/ngsi-ld/v1/entities/light1/attrs', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin11.json' + ) + ) + .query({ type: 'Light' }) + .reply(204); + }); + + it('should apply the expression before sending the values', function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an update comes for attributes with numeric expressions and type integer', function() { + // Case: Update for an integer attribute with arithmetic expression + const values = [ + { + name: 'p', + type: 'Number', + value: 52 + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .patch( + '/ngsi-ld/v1/entities/ws1/attrs', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin1.json' + ) + ) + .query({ type: 'WeatherStation' }) + .reply(204); + }); + + it('should apply the expression before sending the values', function(done) { + iotAgentLib.update('ws1', 'WeatherStation', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an update comes for attributes with string expression and type integer', function() { + // Case: Update for an integer attribute with string expression + const values = [ + { + name: 'e', + type: 'Number', + value: 52 + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .patch( + '/ngsi-ld/v1/entities/ws1/attrs', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin11.json' + ) + ) + .query({ type: 'WeatherStation' }) + .reply(204); + }); + + it('should apply the expression before sending the values', function(done) { + iotAgentLib.update('ws1', 'WeatherStationMultiple', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an update comes for attributes without expressions and type float', function() { + // Case: Update for a Float attribute without expressions + + const values = [ + { + name: 'e', + type: 'Number', + value: 0.44 + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .patch( + '/ngsi-ld/v1/entities/light1/attrs', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin3.json' + ) + ) + .query({ type: 'Light' }) + .reply(204); + }); + + it('should apply the expression before sending the values', function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an update comes for attributes with numeric expressions and type float', function() { + // Case: Update for a Float attribute with arithmetic expression + + const values = [ + { + name: 'e', + type: 'Number', + value: 0.44 + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .patch( + '/ngsi-ld/v1/entities/ws1/attrs', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin8.json' + ) + ) + .query({ type: 'WeatherStation' }) + .reply(204); + }); + + it('should apply the expression before sending the values', function(done) { + iotAgentLib.update('ws1', 'WeatherStation', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an update comes for attributes with string expressions and type float', function() { + // Case: Update for a Float attribute with string expression + + const values = [ + { + name: 'e', + type: 'Number', + value: 0.44 + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .patch( + '/ngsi-ld/v1/entities/ws1/attrs', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin3.json' + ) + ) + .query({ type: 'WeatherStation' }) + .reply(204); + }); + + it('should apply the expression before sending the values', function(done) { + iotAgentLib.update('ws1', 'WeatherStationMultiple', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an update comes for attributes without expressions and NULL type', function() { + // Case: Update for a Null attribute without expression + + const values = [ + { + name: 'a', + type: 'None', + value: null + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .patch( + '/ngsi-ld/v1/entities/light1/attrs', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin5.json' + ) + ) + .query({ type: 'Light' }) + .reply(204); + }); + + it('should apply the expression before sending the values', function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an update comes for attributes with numeric expressions and NULL type', function() { + // Case: Update for a Null attribute with arithmetic expression + + const values = [ + { + name: 'a', + type: 'None', + value: null + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .patch( + '/ngsi-ld/v1/entities/ws1/attrs', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin5.json' + ) + ) + .query({ type: 'WeatherStation' }) + .reply(204); + }); + + it('should apply the expression before sending the values', function(done) { + iotAgentLib.update('ws1', 'WeatherStation', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an update comes for attributes with string expressions and NULL type', function() { + // Case: Update for a Null attribute with string expression + + const values = [ + { + name: 'a', + type: 'None', + value: null + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .patch( + '/ngsi-ld/v1/entities/ws1/attrs', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin5.json' + ) + ) + .query({ type: 'WeatherStation' }) + .reply(204); + }); + + it('should apply the expression before sending the values', function(done) { + iotAgentLib.update('ws1', 'WeatherStationMultiple', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an update comes for attributes without expressions and Boolean type', function() { + // Case: Update for a Boolean attribute without expression + + const values = [ + { + name: 'u', + type: 'Boolean', + value: true + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .patch( + '/ngsi-ld/v1/entities/light1/attrs', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin9.json' + ) + ) + .query({ type: 'Light' }) + .reply(204); + }); + + it('should apply the expression before sending the values', function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an update comes for attributes with numeric expressions and Boolean type', function() { + // Case: Update for a Boolean attribute with arithmetic expression + + const values = [ + { + name: 'u', + type: 'Boolean', + value: true + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .patch( + '/ngsi-ld/v1/entities/ws1/attrs', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin10.json' + ) + ) + .query({ type: 'WeatherStation' }) + .reply(204); + }); + + it('should apply the expression before sending the values', function(done) { + iotAgentLib.update('ws1', 'WeatherStation', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an update comes for attributes with string expressions and Boolean type', function() { + // Case: Update for a Boolean attribute with string expression + const values = [ + { + name: 'u', + type: 'Boolean', + value: true + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .patch( + '/ngsi-ld/v1/entities/ws1/attrs', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin9.json' + ) + ) + .query({ type: 'WeatherStation' }) + .reply(204); + }); + + it('should apply the expression before sending the values', function(done) { + iotAgentLib.update('ws1', 'WeatherStationMultiple', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an update comes for attributes without expressions and Object type', function() { + // Case: Update for a JSON document attribute without expression + const values = [ + { + name: 'm', + type: 'Object', + value: { name: 'Manufacturer1', VAT: 'U12345678' } + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .patch( + '/ngsi-ld/v1/entities/light1/attrs', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin6.json' + ) + ) + .query({ type: 'Light' }) + .reply(204); + }); + + it('should apply the expression before sending the values', function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an update comes for attributes without expressions and Object type', function() { + // Case: Update for a JSON array attribute without expression + + const values = [ + { + name: 'r', + type: 'Object', + value: ['v0.1', 'v0.2', 'v0.3'] + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .patch( + '/ngsi-ld/v1/entities/light1/attrs', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin7.json' + ) + ) + .query({ type: 'Light' }) + .reply(204); + }); + + it('should apply the expression before sending the values', function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When there are expressions including other attributes and they are not updated', function() { + const values = [ + { + name: 'x', + type: 'Number', + value: 0.44 + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .patch( + '/ngsi-ld/v1/entities/light1/attrs', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin12.json' + ) + ) + .query({ type: 'Light' }) + .reply(204); + }); + + it('should apply the expression before sending the values', function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When there are expressions including other attributes and they are updated', function() { + const values = [ + { + name: 'p', + type: 'Number', + value: 10 + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .patch( + '/ngsi-ld/v1/entities/light1/attrs', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin13.json' + ) + ) + .query({ type: 'Light' }) + .reply(204); + }); + + it('should apply the expression before sending the values', function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe( + 'When there are expressions including other attributes and they are updated' + '(overriding situation)', + function() { + const values = [ + { + name: 'x', + type: 'Number', + value: 0.44 + }, + { + name: 'p', + type: 'Number', + value: 10 + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .patch( + '/ngsi-ld/v1/entities/light1/attrs', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin13.json' + ) + ) + .query({ type: 'Light' }) + .reply(204); + }); + + it('should apply the expression before sending the values', function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + } + ); +}); diff --git a/test/unit/ngsi-ld/general/contextBrokerOAuthSecurityAccess-test.js b/test/unit/ngsi-ld/general/contextBrokerOAuthSecurityAccess-test.js new file mode 100644 index 000000000..19e20a783 --- /dev/null +++ b/test/unit/ngsi-ld/general/contextBrokerOAuthSecurityAccess-test.js @@ -0,0 +1,872 @@ +/* + * Copyright 2020 Telefonica Investigación y Desarrollo, S.A.U + * + * This file is part of fiware-iotagent-lib + * + * fiware-iotagent-lib 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. + * + * fiware-iotagent-lib is dvistributed 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 fiware-iotagent-lib. + * If not, see http://www.gnu.org/licenses/. + * + * For those usages not covered by the GNU Affero General Public License + * please contact with::[contacto@tid.es] + * + * Modified by: Jason Fox - FIWARE Foundation + */ + +const iotAgentLib = require('../../../../lib/fiware-iotagent-lib'); +const utils = require('../../../tools/utils'); +const should = require('should'); +const logger = require('logops'); +const nock = require('nock'); +const request = require('request'); +const timekeeper = require('timekeeper'); +let contextBrokerMock; +let oauth2Mock; +const iotAgentConfig = { + contextBroker: { + host: '192.168.1.1', + port: '1026', + ngsiVersion: 'ld', + jsonLdContext: 'http://context.json-ld' + }, + server: { + port: 4041 + }, + authentication: { + type: 'oauth2', + url: 'http://192.168.1.1:3000', + header: 'Authorization', + clientId: 'context-broker', + clientSecret: 'c8d58d16-0a42-400e-9765-f32e154a5a9e', + tokenPath: '/auth/realms/default/protocol/openid-connect/token', + enabled: true + }, + types: { + Light: { + service: 'smartGondor', + subservice: 'electricity', + trust: 'eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJ3cHdWclJ3', + type: 'Light', + commands: [], + lazy: [ + { + name: 'temperature', + type: 'centigrades' + } + ], + active: [ + { + name: 'pressure', + type: 'Hgmm' + } + ] + }, + Termometer: { + commands: [], + type: 'Termometer', + lazy: [ + { + name: 'temp', + type: 'kelvin' + } + ], + active: [] + } + }, + service: 'smartGondor', + subservice: 'gardens', + providerUrl: 'http://smartGondor.com' +}; + +describe('NGSI-LD - Secured access to the Context Broker with OAuth2 provider', function() { + const values = [ + { + name: 'state', + type: 'Boolean', + value: 'true' + }, + { + name: 'dimming', + type: 'Percentage', + value: '87' + } + ]; + + beforeEach(function() { + logger.setLevel('FATAL'); + }); + + afterEach(function(done) { + iotAgentLib.deactivate(done); + nock.cleanAll(); + }); + + describe('When a measure is sent to the Context Broker via an Update Context operation', function() { + beforeEach(function(done) { + nock.cleanAll(); + + oauth2Mock = nock('http://192.168.1.1:3000') + .post( + '/auth/realms/default/protocol/openid-connect/token', + utils.readExampleFile('./test/unit/examples/oauthRequests/getTokenFromTrust.json', true) + ) + .reply(201, utils.readExampleFile('./test/unit/examples/oauthResponses/tokenFromTrust.json'), {}); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('Authorization', 'Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJ3cHdWclJ3') + .patch( + '/ngsi-ld/v1/entities/light1/attrs', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/updateContext1.json') + ) + .query({ type: 'Light' }) + .reply(204, {}); + + iotAgentLib.activate(iotAgentConfig, done); + }); + + it('should ask OAuth2 provider for a token based on the trust token', function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.not.exist(error); + oauth2Mock.done(); + done(); + }); + }); + it('should send the generated token in the auth header', function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + describe('When a measure is sent to the Context Broker and the access is forbidden', function() { + beforeEach(function(done) { + nock.cleanAll(); + + oauth2Mock = nock('http://192.168.1.1:3000') + .post( + '/auth/realms/default/protocol/openid-connect/token', + utils.readExampleFile('./test/unit/examples/oauthRequests/getTokenFromTrust.json', true) + ) + .reply(201, utils.readExampleFile('./test/unit/examples/oauthResponses/tokenFromTrust.json'), {}); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('Authorization', 'Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJ3cHdWclJ3') + .patch( + '/ngsi-ld/v1/entities/light1/attrs', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/updateContext1.json') + ) + .query({ type: 'Light' }) + .reply(403, {}); + + iotAgentLib.activate(iotAgentConfig, done); + }); + + it('it should return a ACCESS_FORBIDDEN error to the caller', function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.exist(error); + error.name.should.equal('ACCESS_FORBIDDEN'); + done(); + }); + }); + }); + describe('When a measure is sent and the trust is rejected asking for the token', function() { + beforeEach(function(done) { + nock.cleanAll(); + + oauth2Mock = nock('http://192.168.1.1:3000') + .post( + '/auth/realms/default/protocol/openid-connect/token', + utils.readExampleFile('./test/unit/examples/oauthRequests/getTokenFromTrust.json', true) + ) + .reply( + 400, + utils.readExampleFile('./test/unit/examples/oauthResponses/tokenFromTrustUnauthorized.json') + ); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('Authorization', 'Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJ3cHdWclJ3') + .patch( + '/ngsi-ld/v1/entities/light1/attrs', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/updateContext1.json') + ) + .reply(204, {}); + + iotAgentLib.activate(iotAgentConfig, done); + }); + + it('it should return a AUTHENTICATION_ERROR error to the caller', function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.exist(error); + error.name.should.equal('AUTHENTICATION_ERROR'); + done(); + }); + }); + }); + + describe('When the user requests information about a device in a protected CB', function() { + const attributes = ['state', 'dimming']; + + beforeEach(function(done) { + nock.cleanAll(); + + oauth2Mock = nock('http://192.168.1.1:3000') + .post( + '/auth/realms/default/protocol/openid-connect/token', + utils.readExampleFile('./test/unit/examples/oauthRequests/getTokenFromTrust.json', true) + ) + .reply(201, utils.readExampleFile('./test/unit/examples/oauthResponses/tokenFromTrust.json'), {}); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('Authorization', 'Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJ3cHdWclJ3') + .get('/ngsi-ld/v1/entities/light1/attrs?attrs=state,dimming&type=Light') + .reply( + 200, + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextResponses/queryContext1Success.json') + ); + + iotAgentLib.activate(iotAgentConfig, done); + }); + + xit('should send the Auth Token along with the information query', function(done) { + iotAgentLib.query('light1', 'Light', '', attributes, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + describe('When subscriptions are used on a protected Context Broker', function() { + beforeEach(function(done) { + const optionsProvision = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices', + method: 'POST', + json: utils.readExampleFile( + './test/unit/examples/deviceProvisioningRequests/provisionMinimumDevice3.json' + ), + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': 'electricity', + 'Content-Type': 'application/ld+json' + } + }; + + nock.cleanAll(); + + iotAgentLib.activate(iotAgentConfig, function() { + oauth2Mock = nock('http://192.168.1.1:3000') + .post( + '/auth/realms/default/protocol/openid-connect/token', + utils.readExampleFile('./test/unit/examples/oauthRequests/getTokenFromTrust.json', true) + ) + .times(3) + .reply(201, utils.readExampleFile('./test/unit/examples/oauthResponses/tokenFromTrust.json'), {}); + + contextBrokerMock = nock('http://192.168.1.1:1026'); + + contextBrokerMock + .post( + '/ngsi-ld/v1/csourceRegistrations/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/' + + 'contextAvailabilityRequests/registerProvisionedDeviceWithGroup3.json' + ) + ) + .reply(201, null, { Location: '/ngsi-ld/v1/csourceRegistrations//6319a7f5254b05844116584d' }); + + contextBrokerMock + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/' + + 'contextRequests/createProvisionedDeviceWithGroupAndStatic3.json' + ) + ) + .reply(200, {}); + + contextBrokerMock + .post( + '/ngsi-ld/v1/subscriptions/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples' + '/subscriptionRequests/simpleSubscriptionRequest2.json' + ) + ) + .matchHeader('Authorization', 'Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJ3cHdWclJ3') + .reply(201, null, { Location: '/v2/subscriptions/51c0ac9ed714fb3b37d7d5a8' }); + + iotAgentLib.clearAll(function() { + request(optionsProvision, function(error, result, body) { + done(); + }); + }); + }); + }); + + it('subscribe requests use auth header', function(done) { + iotAgentLib.getDevice('Light1', 'smartGondor', 'electricity', function(error, device) { + iotAgentLib.subscribe(device, ['dimming'], null, function(error) { + should.not.exist(error); + + contextBrokerMock.done(); + + done(); + }); + }); + }); + + it('unsubscribe requests use auth header', function(done) { + oauth2Mock + .post( + '/auth/realms/default/protocol/openid-connect/token', + utils.readExampleFile('./test/unit/examples/oauthRequests/getTokenFromTrust.json', true) + ) + .reply(201, utils.readExampleFile('./test/unit/examples/oauthResponses/tokenFromTrust.json'), {}); + + contextBrokerMock.delete('/v2/subscriptions/51c0ac9ed714fb3b37d7d5a8').reply(204); + + iotAgentLib.getDevice('Light1', 'smartGondor', 'electricity', function(error, device) { + iotAgentLib.subscribe(device, ['dimming'], null, function(error) { + iotAgentLib.unsubscribe(device, '51c0ac9ed714fb3b37d7d5a8', function(error) { + contextBrokerMock.done(); + done(); + }); + }); + }); + }); + }); +}); + +xdescribe('NGSI-LD - Secured access to the Context Broker with OAuth2 provider (FIWARE Keyrock IDM)', function() { + const values = [ + { + name: 'state', + type: 'Boolean', + value: 'true' + }, + { + name: 'dimming', + type: 'Percentage', + value: '87' + } + ]; + + beforeEach(function() { + logger.setLevel('FATAL'); + }); + + afterEach(function(done) { + iotAgentLib.deactivate(done); + nock.cleanAll(); + }); + + describe('When a measure is sent to the Context Broker via an Update Context operation', function() { + beforeEach(function(done) { + nock.cleanAll(); + + logger.setLevel('FATAL'); + oauth2Mock = nock('http://192.168.1.1:3000') + .post( + '/oauth2/token', + utils.readExampleFile('./test/unit/examples/oauthRequests/getTokenFromTrust.json', true) + ) + .reply( + 200, + utils.readExampleFile('./test/unit/examples/oauthResponses/tokenFromTrustKeyrock.json'), + {} + ); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('Authorization', 'Bearer c1b752e377680acd1349a3ed59db855a1db07605') + .patch( + '/ngsi-ld/v1/entities/light1/attrs', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/updateContext1.json') + ) + .query({ type: 'Light' }) + .reply(204, {}); + + iotAgentConfig.authentication.tokenPath = '/oauth2/token'; + iotAgentLib.activate(iotAgentConfig, done); + }); + + it('should ask OAuth2 provider for a token based on the trust token', function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.not.exist(error); + oauth2Mock.done(); + done(); + }); + }); + it('should send the generated token in the auth header', function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When the user requests information about a device in a protected CB', function() { + const attributes = ['state', 'dimming']; + + beforeEach(function(done) { + nock.cleanAll(); + + oauth2Mock = nock('http://192.168.1.1:3000') + .post( + '/oauth2/token', + utils.readExampleFile('./test/unit/examples/oauthRequests/getTokenFromTrust.json', true) + ) + .reply( + 200, + utils.readExampleFile('./test/unit/examples/oauthResponses/tokenFromTrustKeyrock.json'), + {} + ); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('Authorization', 'Bearer c1b752e377680acd1349a3ed59db855a1db07605') + .get('/ngsi-ld/v1/entities/light1/attrs?attrs=state,dimming&type=Light') + .reply( + 200, + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextResponses/queryContext1Success.json') + ); + + iotAgentLib.activate(iotAgentConfig, done); + }); + + xit('should send the Auth Token along with the information query', function(done) { + iotAgentLib.query('light1', 'Light', '', attributes, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When a measure is sent and the refresh token is not valid', function() { + beforeEach(function(done) { + nock.cleanAll(); + + oauth2Mock = nock('http://192.168.1.1:3000') + .post( + '/oauth2/token', + utils.readExampleFile('./test/unit/examples/oauthRequests/getTokenFromTrust.json', true) + ) + .reply( + 400, + utils.readExampleFile('./test/unit/examples/oauthResponses/tokenFromTrustUnauthorizedKeyrock.json') + ); + + iotAgentLib.activate(iotAgentConfig, done); + }); + + it('it should return a AUTHENTICATION_ERROR error to the caller', function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.exist(error); + error.name.should.equal('AUTHENTICATION_ERROR'); + done(); + }); + }); + }); + + describe('When a measure is sent to the Context Broker and the client credentials are invalid', function() { + beforeEach(function(done) { + nock.cleanAll(); + + oauth2Mock = nock('http://192.168.1.1:3000') + .post( + '/oauth2/token', + utils.readExampleFile('./test/unit/examples/oauthRequests/getTokenFromTrust.json', true) + ) + .reply( + 400, + utils.readExampleFile( + './test/unit/examples/oauthResponses/' + 'tokenFromTrustInvalidCredentialsKeyrock.json' + ), + {} + ); + + iotAgentLib.activate(iotAgentConfig, done); + }); + + it('it should return a AUTHENTICATION_ERROR error to the caller', function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.exist(error); + error.name.should.equal('AUTHENTICATION_ERROR'); + done(); + }); + }); + }); + + describe('When a measure is sent to the Context Broker and the access is unauthorized', function() { + beforeEach(function(done) { + nock.cleanAll(); + + oauth2Mock = nock('http://192.168.1.1:3000') + .post( + '/oauth2/token', + utils.readExampleFile('./test/unit/examples/oauthRequests/getTokenFromTrust.json', true) + ) + .reply( + 200, + utils.readExampleFile('./test/unit/examples/oauthResponses/tokenFromTrustKeyrock.json'), + {} + ); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('Authorization', 'Bearer c1b752e377680acd1349a3ed59db855a1db07605') + .patch( + '/ngsi-ld/v1/entities/light1/attrs', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/updateContext1.json') + ) + .query({ type: 'Light' }) + .reply(401, 'Auth-token not found in request header'); + + iotAgentLib.activate(iotAgentConfig, done); + }); + + it('it should return a ACCESS_FORBIDDEN error to the caller', function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.exist(error); + error.name.should.equal('ACCESS_FORBIDDEN'); + done(); + }); + }); + }); +}); + +describe( + 'NGSI-LD - Secured access to the Context Broker with OAuth2 provider (FIWARE Keyrock IDM)' + + 'configured through group provisioning', + function() { + const groupCreation = { + url: 'http://localhost:4041/iot/services', + method: 'POST', + json: utils.readExampleFile('./test/unit/examples/groupProvisioningRequests/provisionFullGroup.json'), + headers: { + 'fiware-service': 'TestService', + 'fiware-servicepath': '/testingPath' + } + }; + + const values = [ + { + name: 'status', + type: 'String', + value: 'STARTING' + } + ]; + + beforeEach(function() { + logger.setLevel('FATAL'); + }); + + afterEach(function(done) { + iotAgentLib.deactivate(done); + nock.cleanAll(); + }); + + describe('When a measure is sent to the Context Broker via an Update Context operation', function() { + let oauth2Mock2; + let contextBrokerMock2; + beforeEach(function(done) { + nock.cleanAll(); + oauth2Mock = nock('http://192.168.1.1:3000') + .post( + '/oauth2/token', + utils.readExampleFile( + './test/unit/examples/oauthRequests/getTokenFromTrustKeyrockGroup.json', + true + ) + ) + .reply( + 200, + utils.readExampleFile('./test/unit/examples/oauthResponses/tokenFromTrustKeyrock.json'), + {} + ); + + oauth2Mock2 = nock('http://192.168.1.1:3000') + .post( + '/oauth2/token', + utils.readExampleFile( + './test/unit/examples/oauthRequests/getTokenFromTrustKeyrockGroup2.json', + true + ) + ) + .reply( + 200, + utils.readExampleFile('./test/unit/examples/oauthResponses/tokenFromTrustKeyrock2.json'), + {} + ); + + contextBrokerMock = nock('http://unexistentHost:1026') + .matchHeader('fiware-service', 'TestService') + .matchHeader('Authorization', 'Bearer c1b752e377680acd1349a3ed59db855a1db07605') + .patch( + '/ngsi-ld/v1/entities/machine1/attrs', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContext3WithStatic.json' + ) + ) + .query({ type: 'SensorMachine' }) + .reply(204, {}); + + contextBrokerMock2 = nock('http://unexistentHost:1026') + .matchHeader('fiware-service', 'TestService') + .matchHeader('Authorization', 'Bearer bbb752e377680acd1349a3ed59db855a1db076aa') + .patch( + '/ngsi-ld/v1/entities/machine1/attrs', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContext3WithStatic.json' + ) + ) + .query({ type: 'SensorMachine' }) + .reply(204, {}); + + iotAgentConfig.authentication.tokenPath = '/oauth2/token'; + iotAgentLib.activate(iotAgentConfig, function() { + request(groupCreation, function(error, response, body) { + done(); + }); + }); + }); + it( + 'should ask OAuth2 provider for a token based on the' + + 'trust token and send the generated token in the auth header', + function(done) { + iotAgentLib.update('machine1', 'SensorMachine', '', values, function(error) { + should.not.exist(error); + oauth2Mock.done(); + contextBrokerMock.done(); + done(); + }); + } + ); + + it('should use the updated trust token in the following requests', function(done) { + iotAgentLib.update('machine1', 'SensorMachine', '', values, function(error) { + should.not.exist(error); + oauth2Mock2.done(); + contextBrokerMock2.done(); + done(); + }); + }); + }); + + describe('When a device is provisioned for a configuration contains an OAuth2 trust token', function() { + const values = [ + { + name: 'status', + type: 'String', + value: 'STARTING' + } + ]; + const deviceCreation = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices', + method: 'POST', + json: utils.readExampleFile('./test/unit/examples/deviceProvisioningRequests/provisionNewDevice2.json'), + headers: { + 'fiware-service': 'TestService', + 'fiware-servicepath': '/testingPath' + } + }; + let contextBrokerMock2; + let contextBrokerMock3; + beforeEach(function(done) { + const time = new Date(1438760101468); // 2015-08-05T07:35:01.468+00:00 + timekeeper.freeze(time); + nock.cleanAll(); + + oauth2Mock = nock('http://192.168.1.1:3000') + .post( + '/oauth2/token', + utils.readExampleFile( + './test/unit/examples/oauthRequests/getTokenFromTrustKeyrockGroup3.json', + true + ) + ) + .reply( + 200, + utils.readExampleFile('./test/unit/examples/oauthResponses/tokenFromTrustKeyrock3.json'), + {} + ) + .post( + '/oauth2/token', + utils.readExampleFile( + './test/unit/examples/oauthRequests/getTokenFromTrustKeyrockGroup4.json', + true + ) + ) + .reply( + 200, + utils.readExampleFile('./test/unit/examples/oauthResponses/tokenFromTrustKeyrock4.json'), + {} + ) + .post( + '/oauth2/token', + utils.readExampleFile( + './test/unit/examples/oauthRequests/getTokenFromTrustKeyrockGroup5.json', + true + ) + ) + .reply( + 200, + utils.readExampleFile('./test/unit/examples/oauthResponses/tokenFromTrustKeyrock5.json'), + {} + ); + + contextBrokerMock = nock('http://unexistenthost:1026') + .matchHeader('fiware-service', 'TestService') + .matchHeader('Authorization', 'Bearer asd752e377680acd1349a3ed59db855a1db07ere') + .post( + '/ngsi-ld/v1/csourceRegistrations/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/' + + 'contextAvailabilityRequests/registerProvisionedDeviceWithGroup2.json' + ) + ) + .reply(201, null, { Location: '/ngsi-ld/v1/csourceRegistrations/6319a7f5254b05844116584d' }); + + contextBrokerMock2 = nock('http://unexistenthost:1026') + .matchHeader('fiware-service', 'TestService') + .matchHeader('authorization', 'Bearer bea752e377680acd1349a3ed59db855a1db07zxc') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/' + + 'contextRequests/createProvisionedDeviceWithGroupAndStatic2.json' + ) + ) + .reply(200, {}); + + contextBrokerMock3 = nock('http://unexistentHost:1026') + .matchHeader('fiware-service', 'TestService') + .matchHeader('authorization', 'Bearer zzz752e377680acd1349a3ed59db855a1db07bbb') + .patch( + '/ngsi-ld/v1/entities/Light1/attrs', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/updateContext4.json') + ) + .query({ type: 'SensorMachine' }) + .reply(204, {}); + + iotAgentConfig.authentication.tokenPath = '/oauth2/token'; + iotAgentLib.activate(iotAgentConfig, function() { + done(); + }); + }); + + afterEach(function(done) { + timekeeper.reset(); + + done(); + }); + + it('should not raise any error', function(done) { + request(deviceCreation, function(error, response, body) { + should.not.exist(error); + response.statusCode.should.equal(201); + contextBrokerMock.done(); + contextBrokerMock2.done(); + done(); + }); + }); + + it('should send the mixed data to the Context Broker', function(done) { + iotAgentLib.update('Light1', 'SensorMachine', '', values, function(error) { + should.not.exist(error); + contextBrokerMock3.done(); + done(); + }); + }); + }); + } +); + +describe( + 'NGSI-LD - Secured access to the Context Broker with OAuth2 provider (FIWARE Keyrock IDM)' + + 'configured through group provisioning. Permanent token', + function() { + const groupCreation = { + url: 'http://localhost:4041/iot/services', + method: 'POST', + json: utils.readExampleFile('./test/unit/examples/groupProvisioningRequests/provisionFullGroup.json'), + headers: { + 'fiware-service': 'TestService', + 'fiware-servicepath': '/testingPath' + } + }; + + const values = [ + { + name: 'status', + type: 'String', + value: 'STARTING' + } + ]; + + beforeEach(function() { + logger.setLevel('FATAL'); + iotAgentConfig.authentication.permanentToken = true; + }); + + afterEach(function(done) { + iotAgentLib.deactivate(done); + nock.cleanAll(); + }); + + describe('When a measure is sent to the Context Broker via an Update Context operation', function() { + beforeEach(function(done) { + nock.cleanAll(); + + contextBrokerMock = nock('http://unexistentHost:1026') + .matchHeader('fiware-service', 'TestService') + .matchHeader('Authorization', 'Bearer 999210dacf913772606c95dd0b895d5506cbc988') + .patch( + '/ngsi-ld/v1/entities/machine1/attrs', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContext3WithStatic.json' + ) + ) + .query({ type: 'SensorMachine' }) + .reply(204, {}); + + iotAgentConfig.authentication.tokenPath = '/oauth2/token'; + iotAgentLib.activate(iotAgentConfig, function() { + request(groupCreation, function(error, response, body) { + done(); + }); + }); + }); + it('should send the permanent token in the auth header', function(done) { + iotAgentLib.update('machine1', 'SensorMachine', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + + it('should use the permanent trust token in the following requests', function(done) { + iotAgentLib.update('machine1', 'SensorMachine', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + } +); diff --git a/test/unit/ngsi-ld/general/deviceService-test.js b/test/unit/ngsi-ld/general/deviceService-test.js new file mode 100644 index 000000000..de90b46f8 --- /dev/null +++ b/test/unit/ngsi-ld/general/deviceService-test.js @@ -0,0 +1,260 @@ +/* + * Copyright 2020 Telefonica Investigación y Desarrollo, S.A.U + * + * This file is part of fiware-iotagent-lib + * + * fiware-iotagent-lib 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. + * + * fiware-iotagent-lib 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 fiware-iotagent-lib. + * If not, see http://www.gnu.org/licenses/. + * + * For those usages not covered by the GNU Affero General Public License + * please contact with::[contacto@tid.es] + * + * Modified by: Jason Fox - FIWARE Foundation + */ + +/* jshint camelcase: false */ + +const iotAgentLib = require('../../../../lib/fiware-iotagent-lib'); +const utils = require('../../../tools/utils'); +const should = require('should'); +const nock = require('nock'); +const request = require('request'); +const logger = require('logops'); +const async = require('async'); +const iotAgentConfig = { + contextBroker: { + host: '192.168.1.1', + port: '1026', + ngsiVersion: 'ld', + jsonLdContext: 'http://context.json-ld' + }, + server: { + port: 4041 + }, + types: { + Light: { + commands: [], + type: 'Light', + lazy: [ + { + name: 'temperature', + type: 'centigrades' + } + ], + active: [ + { + name: 'pressure', + type: 'Hgmm' + } + ] + }, + BrokenLight: { + commands: [], + lazy: [ + { + name: 'temperature', + type: 'centigrades' + } + ], + active: [ + { + name: 'pressure', + type: 'Hgmm' + } + ] + }, + Termometer: { + type: 'Termometer', + commands: [], + lazy: [ + { + name: 'temp', + type: 'kelvin' + } + ], + active: [] + }, + Humidity: { + type: 'Humidity', + cbHost: 'http://192.168.1.1:3024', + commands: [], + lazy: [], + active: [ + { + name: 'humidity', + type: 'percentage' + } + ] + }, + Motion: { + type: 'Motion', + commands: [], + lazy: [], + staticAttributes: [ + { + name: 'location', + type: 'Vector', + value: '(123,523)' + } + ], + active: [ + { + name: 'humidity', + type: 'percentage' + } + ] + } + }, + iotManager: { + host: 'localhost', + port: 8082, + path: '/protocols', + protocol: 'MQTT_UL', + description: 'MQTT Ultralight 2.0 IoT Agent (Node.js version)' + }, + service: 'smartGondor', + subservice: 'gardens', + providerUrl: 'http://smartGondor.com' +}; +const groupCreation = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/services', + method: 'POST', + json: { + services: [ + { + resource: '', + apikey: '801230BJKL23Y9090DSFL123HJK09H324HV8732', + entity_type: 'TheLightType', + trust: '8970A9078A803H3BL98PINEQRW8342HBAMS', + cbHost: 'http://unexistentHost:1026', + commands: [], + lazy: [], + attributes: [ + { + name: 'status', + type: 'Boolean' + } + ], + static_attributes: [] + } + ] + }, + headers: { + 'fiware-service': 'TestService', + 'fiware-servicepath': '/testingPath' + } +}; +const deviceCreation = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices', + method: 'POST', + json: utils.readExampleFile('./test/unit/examples/deviceProvisioningRequests/provisionNewDevice.json'), + headers: { + 'fiware-service': 'TestService', + 'fiware-servicepath': '/testingPath' + } +}; +let contextBrokerMock; +let iotamMock; + +/* jshint camelcase: false */ +describe('NGSI-LD - Device Service: utils', function() { + beforeEach(function(done) { + nock.cleanAll(); + logger.setLevel('FATAL'); + iotamMock = nock('http://localhost:8082') + .post('/protocols') + .reply(200, {}); + + iotAgentLib.activate(iotAgentConfig, done); + }); + + afterEach(function(done) { + nock.cleanAll(); + async.series([iotAgentLib.clearAll, iotAgentLib.deactivate], done); + }); + + describe('When an existing device tries to be retrieved with retrieveOrCreate()', function() { + beforeEach(function(done) { + // This mock does not check the payload since the aim of the test is not to verify + // device provisioning functionality. Appropriate verification is done in tests under + // provisioning folder + contextBrokerMock = nock('http://unexistenthost:1026') + .matchHeader('fiware-service', 'TestService') + .post('/ngsi-ld/v1/entityOperations/upsert/') + .reply(200); + + async.series([request.bind(request, groupCreation), request.bind(request, deviceCreation)], function( + error, + results + ) { + done(); + }); + }); + + it('should return the existing device', function(done) { + iotAgentLib.retrieveDevice('Light1', '801230BJKL23Y9090DSFL123HJK09H324HV8732', function(error, device) { + should.not.exist(error); + should.exist(device); + + device.id.should.equal('Light1'); + done(); + }); + }); + }); + + describe('When an unexisting device tries to be retrieved for an existing APIKey', function() { + beforeEach(function(done) { + // This mock does not check the payload since the aim of the test is not to verify + // device provisioning functionality. Appropriate verification is done in tests under + // provisioning folder + contextBrokerMock = nock('http://unexistenthost:1026') + .matchHeader('fiware-service', 'TestService') + .post('/ngsi-ld/v1/entityOperations/upsert/') + .reply(200); + + async.series([request.bind(request, groupCreation)], function(error, results) { + done(); + }); + }); + + it('should register the device and return it', function(done) { + iotAgentLib.retrieveDevice('UNEXISTENT_DEV', '801230BJKL23Y9090DSFL123HJK09H324HV8732', function( + error, + device + ) { + should.not.exist(error); + should.exist(device); + + device.id.should.equal('UNEXISTENT_DEV'); + should.exist(device.protocol); + device.protocol.should.equal('MQTT_UL'); + done(); + }); + }); + }); + + describe('When an unexisting device tries to be retrieved for an unexisting APIKey', function() { + it('should raise an error', function(done) { + iotAgentLib.retrieveDevice('UNEXISTENT_DEV_AND_GROUP', 'H2332Y909DSF3H346yh20JK092', function( + error, + device + ) { + should.exist(error); + error.name.should.equal('DEVICE_GROUP_NOT_FOUND'); + should.not.exist(device); + done(); + }); + }); + }); +}); diff --git a/test/unit/ngsi-ld/general/https-support-test.js b/test/unit/ngsi-ld/general/https-support-test.js new file mode 100644 index 000000000..ff97f1c90 --- /dev/null +++ b/test/unit/ngsi-ld/general/https-support-test.js @@ -0,0 +1,264 @@ +/* + * Copyright 2020 Telefonica Investigación y Desarrollo, S.A.U + * + * This file is part of fiware-iotagent-lib + * + * fiware-iotagent-lib 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. + * + * fiware-iotagent-lib 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 fiware-iotagent-lib. + * If not, see http://www.gnu.org/licenses/. + * + * For those usages not covered by the GNU Affero General Public License + * please contact with::[contacto@tid.es] + * + * Modified by: Federico M. Facca - Martel Innovate + * Modified by: Jason Fox - FIWARE Foundation + */ + +/* jshint camelcase: false */ + +const iotAgentLib = require('../../../../lib/fiware-iotagent-lib'); +const request = require('request'); +const nock = require('nock'); +const logger = require('logops'); +const utils = require('../../../tools/utils'); +const groupRegistryMemory = require('../../../../lib/services/groups/groupRegistryMemory'); +const should = require('should'); +const iotAgentConfig = { + logLevel: 'FATAL', + contextBroker: { + url: 'https://192.168.1.1:1026', + ngsiVersion: 'ld' + }, + server: { + port: 4041 + }, + types: { + Light: { + commands: [], + lazy: [ + { + name: 'temperature', + type: 'centigrades' + } + ], + active: [ + { + name: 'pressure', + type: 'Hgmm' + } + ], + service: 'smartGondor', + subservice: 'gardens' + }, + Termometer: { + commands: [], + lazy: [ + { + name: 'temp', + type: 'kelvin' + } + ], + active: [], + service: 'smartGondor', + subservice: 'gardens' + } + }, + service: 'smartGondor', + subservice: 'gardens', + providerUrl: 'http://smartGondor.com', + iotManager: { + url: 'https://mockediotam.com:9876', + path: '/protocols', + protocol: 'GENERIC_PROTOCOL', + description: 'A generic protocol', + agentPath: '/iot' + }, + defaultResource: '/iot/d' +}; +const groupCreation = { + service: 'theService', + subservice: 'theSubService', + resource: '/deviceTest', + apikey: '801230BJKL23Y9090DSFL123HJK09H324HV8732', + type: 'SensorMachine', + trust: '8970A9078A803H3BL98PINEQRW8342HBAMS', + commands: [ + { + name: 'wheel1', + type: 'Wheel' + } + ], + lazy: [ + { + name: 'luminescence', + type: 'Lumens' + } + ], + attributes: [ + { + name: 'status', + type: 'Boolean' + } + ] +}; +const device1 = { + id: 'light1', + type: 'Light', + service: 'smartGondor', + subservice: 'gardens' +}; +let contextBrokerMock; +let iotamMock; + +describe('NGSI-LD - HTTPS support tests IOTAM', function() { + describe('When the IoT Agents is started with https "iotManager" config', function() { + beforeEach(function(done) { + nock.cleanAll(); + + iotamMock = nock('https://mockediotam.com:9876') + .post( + '/protocols', + utils.readExampleFile('./test/unit/examples/iotamRequests/registrationWithGroupsWithoutCB.json') + ) + .reply(200, utils.readExampleFile('./test/unit/examples/iotamResponses/registrationSuccess.json')); + + groupRegistryMemory.create(groupCreation, done); + }); + + afterEach(function(done) { + nock.cleanAll(); + groupRegistryMemory.clear(function() { + iotAgentLib.deactivate(done); + }); + }); + + it('should register without errors to the IoT Manager', function(done) { + iotAgentLib.activate(iotAgentConfig, function(error) { + should.not.exist(error); + iotamMock.done(); + done(); + }); + }); + }); +}); + +describe('NGSI-LD - HTTPS support tests', function() { + describe('When subscription is sent to HTTPS context broker', function() { + beforeEach(function(done) { + logger.setLevel('FATAL'); + const optionsProvision = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices', + method: 'POST', + json: utils.readExampleFile( + './test/unit/examples/deviceProvisioningRequests/provisionMinimumDevice.json' + ), + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + } + }; + + nock.cleanAll(); + + iotAgentLib.activate(iotAgentConfig, function() { + contextBrokerMock = nock('https://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/' + 'contextRequests/createMinimumProvisionedDevice.json' + ) + ) + .reply(200); + + contextBrokerMock = nock('https://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + + .post( + '/ngsi-ld/v1/subscriptions/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples' + '/subscriptionRequests/simpleSubscriptionRequest.json' + ) + ) + .reply(201, null, { Location: '/v2/subscriptions/51c0ac9ed714fb3b37d7d5a8' }); + + iotAgentLib.clearAll(function() { + request(optionsProvision, function(error, result, body) { + done(); + }); + }); + }); + }); + + afterEach(function(done) { + nock.cleanAll(); + iotAgentLib.setNotificationHandler(); + iotAgentLib.clearAll(function() { + iotAgentLib.deactivate(done); + }); + }); + + it('should send the appropriate request to the Context Broker', function(done) { + iotAgentLib.getDevice('MicroLight1', 'smartGondor', '/gardens', function(error, device) { + iotAgentLib.subscribe(device, ['attr_name'], null, function(error) { + should.not.exist(error); + + contextBrokerMock.done(); + + done(); + }); + }); + }); + }); + + describe('When a new device is connected to the IoT Agent', function() { + beforeEach(function(done) { + nock.cleanAll(); + + // This mock does not check the payload since the aim of the test is not to verify + // device provisioning functionality. Appropriate verification is done in tests under + // provisioning folder + contextBrokerMock = nock('https://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .post('/ngsi-ld/v1/entityOperations/upsert/') + .reply(200); + + contextBrokerMock = nock('https://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .post('/ngsi-ld/v1/csourceRegistrations/') + .reply(201, null, { Location: '/v2/registrations/6319a7f5254b05844116584d' }); + + iotAgentLib.activate(iotAgentConfig, function(error) { + iotAgentLib.clearAll(done); + }); + }); + + it('should register as ContextProvider using HTTPS', function(done) { + iotAgentLib.register(device1, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + + afterEach(function(done) { + nock.cleanAll(); + iotAgentLib.clearAll(function() { + // We need to remove the registrationId so that the library does not consider next operatios as updates. + delete device1.registrationId; + iotAgentLib.deactivate(done); + }); + }); + }); +}); diff --git a/test/unit/ngsi-ld/general/iotam-autoregistration-test.js b/test/unit/ngsi-ld/general/iotam-autoregistration-test.js new file mode 100644 index 000000000..c95a1f498 --- /dev/null +++ b/test/unit/ngsi-ld/general/iotam-autoregistration-test.js @@ -0,0 +1,369 @@ +/* + * Copyright 2020 Telefonica Investigación y Desarrollo, S.A.U + * + * This file is part of fiware-iotagent-lib + * + * fiware-iotagent-lib 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. + * + * fiware-iotagent-lib 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 fiware-iotagent-lib. + * If not, see http://www.gnu.org/licenses/. + * + * For those usages not covered by the GNU Affero General Public License + * please contact with::[contacto@tid.es] + */ + +/* jshint camelcase: false */ + +const iotAgentLib = require('../../../../lib/fiware-iotagent-lib'); +const request = require('request'); +const nock = require('nock'); +const utils = require('../../../tools/utils'); +const groupRegistryMemory = require('../../../../lib/services/groups/groupRegistryMemory'); +const should = require('should'); +const iotAgentConfig = { + logLevel: 'FATAL', + contextBroker: { + host: '192.168.1.1', + port: '1026', + ngsiVersion: 'ld', + jsonLdContext: 'http://context.json-ld' + }, + server: { + port: 4041 + }, + types: { + Light: { + commands: [], + type: 'Light', + lazy: [ + { + name: 'temperature', + type: 'centigrades' + } + ], + attributes: [ + { + name: 'pressure', + type: 'Hgmm' + } + ] + } + }, + providerUrl: 'http://smartGondor.com', + iotManager: { + host: 'mockediotam.com', + port: 9876, + path: '/protocols', + protocol: 'GENERIC_PROTOCOL', + description: 'A generic protocol', + agentPath: '/iot' + }, + defaultResource: '/iot/d' +}; +const groupCreation = { + service: 'theService', + subservice: 'theSubService', + resource: '/deviceTest', + apikey: '801230BJKL23Y9090DSFL123HJK09H324HV8732', + type: 'SensorMachine', + trust: '8970A9078A803H3BL98PINEQRW8342HBAMS', + cbHost: 'http://unexistentHost:1026', + commands: [ + { + name: 'wheel1', + type: 'Wheel' + } + ], + lazy: [ + { + name: 'luminescence', + type: 'Lumens' + } + ], + attributes: [ + { + name: 'status', + type: 'Boolean' + } + ] +}; +const optionsCreation = { + url: 'http://localhost:4041/iot/services', + method: 'POST', + json: { + services: [ + { + resource: '/deviceTest', + apikey: '801230BJKL23Y9090DSFL123HJK09H324HV8732', + entity_type: 'SensorMachine', + trust: '8970A9078A803H3BL98PINEQRW8342HBAMS', + cbHost: 'http://unexistentHost:1026', + commands: [ + { + name: 'wheel1', + type: 'Wheel' + } + ], + lazy: [ + { + name: 'luminescence', + type: 'Lumens' + } + ], + attributes: [ + { + name: 'status', + type: 'Boolean' + } + ] + } + ] + }, + headers: { + 'fiware-service': 'theService', + 'fiware-servicepath': 'theSubService' + } +}; +const optionsCreationStatic = { + url: 'http://localhost:4041/iot/services', + method: 'POST', + json: { + services: [ + { + resource: '/deviceTest', + apikey: '801230BJKL23Y9090DSFL123HJK09H324HV8732', + entity_type: 'SensorMachine', + trust: '8970A9078A803H3BL98PINEQRW8342HBAMS', + cbHost: 'http://unexistentHost:1026', + commands: [ + { + name: 'wheel1', + type: 'Wheel' + } + ], + static_attributes: [ + { + name: 'position', + type: 'location', + values: '123,12' + } + ], + attributes: [ + { + name: 'status', + type: 'Boolean' + } + ] + } + ] + }, + headers: { + 'fiware-service': 'theService', + 'fiware-servicepath': 'theSubService' + } +}; +const optionsDelete = { + url: 'http://localhost:4041/iot/services', + method: 'DELETE', + json: {}, + headers: { + 'fiware-service': 'theService', + 'fiware-servicepath': 'theSubService' + }, + qs: { + resource: '/deviceTest', + apikey: '801230BJKL23Y9090DSFL123HJK09H324HV8732' + } +}; +let iotamMock; + +describe('NGSI-LD - IoT Manager autoregistration', function() { + describe('When the IoT Agent is started without a "iotManager" config parameter and empty services', function() { + beforeEach(function() { + nock.cleanAll(); + + iotamMock = nock('http://mockediotam.com:9876') + .post('/protocols', utils.readExampleFile('./test/unit/examples/iotamRequests/registrationEmpty.json')) + .reply(200, utils.readExampleFile('./test/unit/examples/iotamResponses/registrationSuccess.json')); + }); + + afterEach(function(done) { + iotAgentLib.deactivate(done); + }); + + it('should register itself to the provided IoT Manager URL', function(done) { + iotAgentLib.activate(iotAgentConfig, function(error) { + should.not.exist(error); + iotamMock.done(); + done(); + }); + }); + }); + + describe('When the IoT Agents is started with "iotManager" config with missing attributes', function() { + beforeEach(function() { + nock.cleanAll(); + + delete iotAgentConfig.providerUrl; + + iotamMock = nock('http://mockediotam.com:9876') + .post('/protocols', utils.readExampleFile('./test/unit/examples/iotamRequests/registrationEmpty.json')) + .reply(200, utils.readExampleFile('./test/unit/examples/iotamResponses/registrationSuccess.json')); + }); + + afterEach(function() { + iotAgentConfig.providerUrl = 'http://smartGondor.com'; + }); + + it('should fail with a MISSING_CONFIG_PARAMS error', function(done) { + iotAgentLib.activate(iotAgentConfig, function(error) { + should.exist(error); + error.name.should.equal('MISSING_CONFIG_PARAMS'); + done(); + }); + }); + }); + + describe('When the IoT Agents is started with "iotManager" config and multiple services', function() { + beforeEach(function(done) { + nock.cleanAll(); + + iotamMock = nock('http://mockediotam.com:9876') + .post( + '/protocols', + utils.readExampleFile('./test/unit/examples/iotamRequests/registrationWithGroups.json') + ) + .reply(200, utils.readExampleFile('./test/unit/examples/iotamResponses/registrationSuccess.json')); + + groupRegistryMemory.create(groupCreation, done); + }); + + afterEach(function(done) { + groupRegistryMemory.clear(function() { + iotAgentLib.deactivate(done); + }); + }); + + it('should send all the service information to the IoT Manager in the registration', function(done) { + iotAgentLib.activate(iotAgentConfig, function(error) { + should.not.exist(error); + iotamMock.done(); + done(); + }); + }); + }); + + describe('When a new service is created in the IoT Agent', function() { + beforeEach(function(done) { + nock.cleanAll(); + + iotamMock = nock('http://mockediotam.com:9876') + .post('/protocols', utils.readExampleFile('./test/unit/examples/iotamRequests/registrationEmpty.json')) + .reply(200, utils.readExampleFile('./test/unit/examples/iotamResponses/registrationSuccess.json')); + + iotamMock + .post( + '/protocols', + utils.readExampleFile('./test/unit/examples/iotamRequests/registrationWithGroups.json') + ) + .reply(200, utils.readExampleFile('./test/unit/examples/iotamResponses/registrationSuccess.json')); + + iotAgentLib.activate(iotAgentConfig, function(error) { + done(); + }); + }); + + afterEach(function(done) { + groupRegistryMemory.clear(function() { + iotAgentLib.deactivate(done); + }); + }); + + it('should update the registration in the IoT Manager', function(done) { + request(optionsCreation, function(error, result, body) { + should.not.exist(error); + iotamMock.done(); + done(); + }); + }); + }); + + describe('When a service is removed from the IoT Agent', function() { + beforeEach(function(done) { + nock.cleanAll(); + + iotamMock = nock('http://mockediotam.com:9876') + .post( + '/protocols', + utils.readExampleFile('./test/unit/examples/iotamRequests/registrationWithGroups.json') + ) + .reply(200, utils.readExampleFile('./test/unit/examples/iotamResponses/registrationSuccess.json')); + + iotamMock + .post('/protocols', utils.readExampleFile('./test/unit/examples/iotamRequests/registrationEmpty.json')) + .reply(200, utils.readExampleFile('./test/unit/examples/iotamResponses/registrationSuccess.json')); + + groupRegistryMemory.create(groupCreation, function() { + iotAgentLib.activate(iotAgentConfig, done); + }); + }); + + afterEach(function(done) { + groupRegistryMemory.clear(function() { + iotAgentLib.deactivate(done); + }); + }); + + it('should update the registration in the IoT Manager', function(done) { + request(optionsDelete, function(error, result, body) { + should.not.exist(error); + iotamMock.done(); + done(); + }); + }); + }); + + describe('When a new service with static attributes is created in the IoT Agent', function() { + beforeEach(function(done) { + nock.cleanAll(); + + iotamMock = nock('http://mockediotam.com:9876') + .post('/protocols', utils.readExampleFile('./test/unit/examples/iotamRequests/registrationEmpty.json')) + .reply(200, utils.readExampleFile('./test/unit/examples/iotamResponses/registrationSuccess.json')); + + iotamMock + .post( + '/protocols', + utils.readExampleFile('./test/unit/examples/iotamRequests/registrationWithStaticGroups.json') + ) + .reply(200, utils.readExampleFile('./test/unit/examples/iotamResponses/registrationSuccess.json')); + + iotAgentLib.activate(iotAgentConfig, function(error) { + done(); + }); + }); + + afterEach(function(done) { + groupRegistryMemory.clear(function() { + iotAgentLib.deactivate(done); + }); + }); + + it('should update the registration in the IoT Manager', function(done) { + request(optionsCreationStatic, function(error, result, body) { + should.not.exist(error); + iotamMock.done(); + done(); + }); + }); + }); +}); diff --git a/test/unit/ngsi-ld/general/startup-test.js b/test/unit/ngsi-ld/general/startup-test.js new file mode 100644 index 000000000..d9582b8c1 --- /dev/null +++ b/test/unit/ngsi-ld/general/startup-test.js @@ -0,0 +1,143 @@ +/* + * Copyright 2020 Telefonica Investigación y Desarrollo, S.A.U + * + * This file is part of fiware-iotagent-lib + * + * fiware-iotagent-lib 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. + * + * fiware-iotagent-lib 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 fiware-iotagent-lib. + * If not, see http://www.gnu.org/licenses/. + * + * For those usages not covered by the GNU Affero General Public License + * please contact with::[contacto@tid.es] + * + * Modified by: Jason Fox - FIWARE Foundation + */ + +const iotAgentLib = require('../../../../lib/fiware-iotagent-lib'); +const nock = require('nock'); +const utils = require('../../../tools/utils'); +const config = require('../../../../lib/commonConfig'); +const iotAgentConfig = { + logLevel: 'FATAL', + contextBroker: { + host: '192.168.1.1', + port: '1026' + }, + server: { + port: 4041 + }, + types: { + Light: { + commands: [], + type: 'Light', + lazy: [ + { + name: 'temperature', + type: 'centigrades' + } + ], + attributes: [ + { + name: 'pressure', + type: 'Hgmm' + } + ] + } + }, + providerUrl: 'http://smartGondor.com' +}; +let iotamMock; + +describe('NGSI-LD - Startup tests', function() { + describe('When the IoT Agent is started with environment variables', function() { + beforeEach(function() { + process.env.IOTA_CB_HOST = 'cbhost'; + process.env.IOTA_CB_PORT = '1111'; + process.env.IOTA_CB_NGSI_VERSION = 'v2'; + process.env.IOTA_NORTH_HOST = 'localhost'; + process.env.IOTA_NORTH_PORT = '2222'; + process.env.IOTA_PROVIDER_URL = 'provider:3333'; + process.env.IOTA_REGISTRY_TYPE = 'mongo'; + process.env.IOTA_LOG_LEVEL = 'FATAL'; + process.env.IOTA_TIMESTAMP = true; + process.env.IOTA_IOTAM_HOST = 'iotamhost'; + process.env.IOTA_IOTAM_PORT = '4444'; + process.env.IOTA_IOTAM_PATH = '/iotampath'; + process.env.IOTA_IOTAM_PROTOCOL = 'PDI_PROTOCOL'; + process.env.IOTA_IOTAM_DESCRIPTION = 'The IoTAM Protocol'; + process.env.IOTA_MONGO_HOST = 'mongohost'; + process.env.IOTA_MONGO_PORT = '5555'; + process.env.IOTA_MONGO_DB = 'themongodb'; + process.env.IOTA_MONGO_REPLICASET = 'customReplica'; + process.env.IOTA_DEFAULT_RESOURCE = '/iot/custom'; + + nock.cleanAll(); + + iotamMock = nock('http://iotamhost:4444') + .post('/iotampath') + .reply( + 200, + utils.readExampleFile('./test/unit/ngsi-ld/examples/iotamResponses/registrationSuccess.json') + ); + }); + + afterEach(function() { + delete process.env.IOTA_CB_HOST; + delete process.env.IOTA_CB_PORT; + delete process.env.IOTA_CB_NGSI_VERSION; + delete process.env.IOTA_NORTH_HOST; + delete process.env.IOTA_NORTH_PORT; + delete process.env.IOTA_PROVIDER_URL; + delete process.env.IOTA_REGISTRY_TYPE; + delete process.env.IOTA_LOG_LEVEL; + delete process.env.IOTA_TIMESTAMP; + delete process.env.IOTA_IOTAM_HOST; + delete process.env.IOTA_IOTAM_PORT; + delete process.env.IOTA_IOTAM_PATH; + delete process.env.IOTA_IOTAM_PROTOCOL; + delete process.env.IOTA_IOTAM_DESCRIPTION; + delete process.env.IOTA_MONGO_HOST; + delete process.env.IOTA_MONGO_PORT; + delete process.env.IOTA_MONGO_DB; + delete process.env.IOTA_MONGO_REPLICASET; + delete process.env.IOTA_DEFAULT_RESOURCE; + }); + + afterEach(function(done) { + iotAgentLib.deactivate(done); + }); + + it('should load the correct configuration parameters', function(done) { + iotAgentLib.activate(iotAgentConfig, function(error) { + config.getConfig().contextBroker.url.should.equal('http://cbhost:1111'); + config.getConfig().contextBroker.ngsiVersion.should.equal('v2'); + config.getConfig().server.host.should.equal('localhost'); + config.getConfig().server.port.should.equal('2222'); + config.getConfig().providerUrl.should.equal('provider:3333'); + config.getConfig().deviceRegistry.type.should.equal('mongo'); + config.getConfig().logLevel.should.equal('FATAL'); + config.getConfig().timestamp.should.equal(true); + config.getConfig().iotManager.url.should.equal('http://iotamhost:4444'); + config.getConfig().iotManager.path.should.equal('/iotampath'); + config.getConfig().iotManager.protocol.should.equal('PDI_PROTOCOL'); + config.getConfig().iotManager.description.should.equal('The IoTAM Protocol'); + config.getConfig().defaultResource.should.equal('/iot/custom'); + config.getConfig().mongodb.host.should.equal('mongohost'); + config.getConfig().mongodb.port.should.equal('5555'); + config.getConfig().mongodb.db.should.equal('themongodb'); + config.getConfig().mongodb.replicaSet.should.equal('customReplica'); + done(); + }); + }); + }); +}); diff --git a/test/unit/ngsi-ld/lazyAndCommands/active-devices-attribute-update-test.js b/test/unit/ngsi-ld/lazyAndCommands/active-devices-attribute-update-test.js new file mode 100644 index 000000000..23a4d01fe --- /dev/null +++ b/test/unit/ngsi-ld/lazyAndCommands/active-devices-attribute-update-test.js @@ -0,0 +1,156 @@ +/* + * Copyright 2020 Telefonica Investigación y Desarrollo, S.A.U + * + * This file is part of fiware-iotagent-lib + * + * fiware-iotagent-lib 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. + * + * fiware-iotagent-lib 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 fiware-iotagent-lib. + * 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: Jason Fox - FIWARE Foundation + */ + +const iotAgentLib = require('../../../../lib/fiware-iotagent-lib'); +const should = require('should'); +const logger = require('logops'); +const nock = require('nock'); +const mongoUtils = require('../../mongodb/mongoDBUtils'); +const request = require('request'); +let contextBrokerMock; +const iotAgentConfig = { + logLevel: 'FATAL', + contextBroker: { + host: '192.168.1.1', + port: '1026', + ngsiVersion: 'ld', + jsonLdContext: 'http://context.json-ld' + }, + server: { + port: 4041 + }, + types: { + Light: { + // commands are not defined + active: [ + { + name: 'pressure', + type: 'Hgmm' + } + ] + } + }, + service: 'smartGondor', + subservice: 'gardens', + providerUrl: 'http://smartGondor.com' +}; +const device = { + id: 'somelight', + type: 'Light', + service: 'smartGondor', + subservice: 'gardens' +}; + +describe('NGSI-LD - Update attribute functionalities', function() { + beforeEach(function(done) { + logger.setLevel('FATAL'); + + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .post('/ngsi-ld/v1/csourceRegistrations/') + .reply(201, null, { Location: '/v2/registrations/6319a7f5254b05844116584d' }); + + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post('/ngsi-ld/v1/entityOperations/upsert/') + .reply(200); + + iotAgentLib.activate(iotAgentConfig, done); + }); + + afterEach(function(done) { + iotAgentLib.clearAll(function() { + iotAgentLib.deactivate(function() { + mongoUtils.cleanDbs(function() { + nock.cleanAll(); + iotAgentLib.setDataUpdateHandler(); + iotAgentLib.setCommandHandler(); + done(); + }); + }); + }); + }); + + xdescribe('When a attribute update arrives to the IoT Agent as Context Provider', function() { + const options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/v2/op/update', + method: 'POST', + json: { + actionType: 'update', + entities: [ + { + id: 'Light:somelight', + type: 'Light', + pressure: { + type: 'Hgmm', + value: 200 + } + } + ] + }, + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': 'gardens' + } + }; + + beforeEach(function(done) { + iotAgentLib.register(device, function(error) { + if (error) { + done('Device registration failed'); + } + done(); + }); + }); + + it('should call the client handler with correct values, even if commands are not defined', function(done) { + let handlerCalled = false; + + iotAgentLib.setDataUpdateHandler(function(id, type, service, subservice, attributes, callback) { + id.should.equal('Light:somelight'); + type.should.equal('Light'); + should.exist(attributes); + attributes.length.should.equal(1); + attributes[0].name.should.equal('pressure'); + attributes[0].value.should.equal(200); + handlerCalled = true; + + callback(null, { + id, + type, + attributes + }); + }); + + request(options, function(error, response, body) { + should.not.exist(error); + handlerCalled.should.equal(true); + done(); + }); + }); + }); +}); diff --git a/test/unit/ngsi-ld/lazyAndCommands/command-test.js b/test/unit/ngsi-ld/lazyAndCommands/command-test.js new file mode 100644 index 000000000..b65b612c9 --- /dev/null +++ b/test/unit/ngsi-ld/lazyAndCommands/command-test.js @@ -0,0 +1,310 @@ +/* + * Copyright 2020 Telefonica Investigación y Desarrollo, S.A.U + * + * This file is part of fiware-iotagent-lib + * + * fiware-iotagent-lib 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. + * + * fiware-iotagent-lib 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 fiware-iotagent-lib. + * 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: Jason Fox - FIWARE Foundation + */ + +const iotAgentLib = require('../../../../lib/fiware-iotagent-lib'); +const utils = require('../../../tools/utils'); +const should = require('should'); +const logger = require('logops'); +const nock = require('nock'); +const mongoUtils = require('../../mongodb/mongoDBUtils'); +const request = require('request'); +const timekeeper = require('timekeeper'); +let contextBrokerMock; +let statusAttributeMock; +const iotAgentConfig = { + contextBroker: { + host: '192.168.1.1', + port: '1026', + ngsiVersion: 'ld', + jsonLdContext: 'http://context.json-ld' + }, + server: { + port: 4041 + }, + types: { + Light: { + commands: [], + lazy: [ + { + name: 'temperature', + type: 'centigrades' + } + ], + active: [ + { + name: 'pressure', + type: 'Hgmm' + } + ] + }, + Termometer: { + commands: [], + lazy: [ + { + name: 'temp', + type: 'kelvin' + } + ], + active: [] + }, + Motion: { + commands: [], + lazy: [ + { + name: 'moving', + type: 'Boolean' + } + ], + staticAttributes: [ + { + name: 'location', + type: 'Vector', + value: '(123,523)' + } + ], + active: [] + }, + Robot: { + commands: [ + { + name: 'position', + type: 'Array' + } + ], + lazy: [], + staticAttributes: [], + active: [] + } + }, + service: 'smartGondor', + subservice: 'gardens', + providerUrl: 'http://smartGondor.com' +}; +const device3 = { + id: 'r2d2', + type: 'Robot', + service: 'smartGondor', + subservice: 'gardens' +}; + +describe('NGSI-LD - Command functionalities', function() { + beforeEach(function(done) { + logger.setLevel('FATAL'); + const time = new Date(1438760101468); // 2015-08-05T07:35:01.468+00:00 + timekeeper.freeze(time); + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .post( + '/ngsi-ld/v1/csourceRegistrations/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerIoTAgentCommands.json' + ) + ) + .reply(201, null, { Location: '/v2/registrations/6319a7f5254b05844116584d' }); + + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post('/ngsi-ld/v1/entityOperations/upsert/') + .reply(200); + + iotAgentLib.activate(iotAgentConfig, done); + }); + + afterEach(function(done) { + timekeeper.reset(); + delete device3.registrationId; + iotAgentLib.clearAll(function() { + iotAgentLib.deactivate(function() { + mongoUtils.cleanDbs(function() { + nock.cleanAll(); + iotAgentLib.setDataUpdateHandler(); + iotAgentLib.setCommandHandler(); + done(); + }); + }); + }); + }); + + describe('When a device is preregistered with commands', function() { + it('should register as Context Provider of the commands', function(done) { + iotAgentLib.register(device3, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + xdescribe('When a command update arrives to the IoT Agent as Context Provider', function() { + const options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/v2/op/update', + method: 'POST', + json: { + actionType: 'update', + entities: [ + { + id: 'Robot:r2d2', + type: 'Robot', + position: { + type: 'Array', + value: '[28, -104, 23]' + } + } + ] + }, + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': 'gardens' + } + }; + + beforeEach(function(done) { + iotAgentLib.register(device3, function(error) { + done(); + }); + }); + + it('should call the client handler', function(done) { + let handlerCalled = false; + + iotAgentLib.setCommandHandler(function(id, type, service, subservice, attributes, callback) { + id.should.equal(device3.type + ':' + device3.id); + type.should.equal(device3.type); + attributes[0].name.should.equal('position'); + attributes[0].value.should.equal('[28, -104, 23]'); + handlerCalled = true; + callback(null, { + id, + type, + attributes: [ + { + name: 'position', + type: 'Array', + value: '[28, -104, 23]' + } + ] + }); + }); + + request(options, function(error, response, body) { + should.not.exist(error); + handlerCalled.should.equal(true); + done(); + }); + }); + it('should create the attribute with the "_status" prefix in the Context Broker', function(done) { + iotAgentLib.setCommandHandler(function(id, type, service, subservice, attributes, callback) { + callback(null, { + id, + type, + attributes: [ + { + name: 'position', + type: 'Array', + value: '[28, -104, 23]' + } + ] + }); + }); + + request(options, function(error, response, body) { + should.not.exist(error); + done(); + }); + }); + it('should create the attribute with the "_status" prefix in the Context Broker', function(done) { + let serviceAndSubservice = false; + + iotAgentLib.setCommandHandler(function(id, type, service, subservice, attributes, callback) { + serviceAndSubservice = service === 'smartGondor' && subservice === 'gardens'; + callback(null, { + id, + type, + attributes: [ + { + name: 'position', + type: 'Array', + value: '[28, -104, 23]' + } + ] + }); + }); + + request(options, function(error, response, body) { + serviceAndSubservice.should.equal(true); + done(); + }); + }); + }); + xdescribe('When an update arrives from the south bound for a registered command', function() { + beforeEach(function(done) { + statusAttributeMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .post( + '/ngsi-ld/v1/entities/r2d2/attrs?type=Robot', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextCommandFinish.json' + ) + ) + .reply(204); + + iotAgentLib.register(device3, function(error) { + done(); + }); + }); + + it('should update its value and status in the Context Broker', function(done) { + iotAgentLib.setCommandResult('r2d2', 'Robot', '', 'position', '[72, 368, 1]', 'FINISHED', function(error) { + should.not.exist(error); + statusAttributeMock.done(); + done(); + }); + }); + }); + xdescribe('When an error command arrives from the south bound for a registered command', function() { + beforeEach(function(done) { + statusAttributeMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .post( + '/ngsi-ld/v1/entities/r2d2/attrs?type=Robot', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/updateContextCommandError.json') + ) + .reply(204); + + iotAgentLib.register(device3, function(error) { + done(); + }); + }); + + it('should update its status in the Context Broker', function(done) { + iotAgentLib.setCommandResult('r2d2', 'Robot', '', 'position', 'Stalled', 'ERROR', function(error) { + should.not.exist(error); + statusAttributeMock.done(); + done(); + }); + }); + }); +}); diff --git a/test/unit/ngsi-ld/lazyAndCommands/lazy-devices-test.js b/test/unit/ngsi-ld/lazyAndCommands/lazy-devices-test.js new file mode 100644 index 000000000..b6627446f --- /dev/null +++ b/test/unit/ngsi-ld/lazyAndCommands/lazy-devices-test.js @@ -0,0 +1,797 @@ +/* + * Copyright 2020 Telefonica Investigación y Desarrollo, S.A.U + * + * This file is part of fiware-iotagent-lib + * + * fiware-iotagent-lib 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. + * + * fiware-iotagent-lib 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 fiware-iotagent-lib. + * 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: Jason Fox - FIWARE Foundation + */ + +const iotAgentLib = require('../../../../lib/fiware-iotagent-lib'); +const utils = require('../../../tools/utils'); +const async = require('async'); +const apply = async.apply; +const should = require('should'); +const logger = require('logops'); +const nock = require('nock'); +const mongoUtils = require('../../mongodb/mongoDBUtils'); +const request = require('request'); +const timekeeper = require('timekeeper'); +let contextBrokerMock; +const iotAgentConfig = { + contextBroker: { + host: '192.168.1.1', + port: '1026', + ngsiVersion: 'ld', + jsonLdContext: 'http://context.json-ld' + }, + server: { + port: 4041 + }, + types: { + Light: { + commands: [], + lazy: [ + { + name: 'temperature', + type: 'centigrades' + } + ], + active: [ + { + name: 'pressure', + type: 'Hgmm' + } + ] + }, + Termometer: { + commands: [], + lazy: [ + { + name: 'temp', + type: 'kelvin' + } + ], + active: [] + }, + Motion: { + commands: [], + lazy: [ + { + name: 'moving', + type: 'Boolean' + } + ], + staticAttributes: [ + { + name: 'location', + type: 'Vector', + value: '(123,523)' + } + ], + active: [] + }, + RobotPre: { + commands: [], + lazy: [ + { + name: 'moving', + type: 'Boolean' + } + ], + staticAttributes: [], + attributes: [], + internalAttributes: { + lwm2mResourceMapping: { + position: { + objectType: 9090, + objectInstance: 0, + objectResource: 0 + } + } + } + } + }, + service: 'smartGondor', + subservice: 'gardens', + providerUrl: 'http://smartGondor.com' +}; +const device1 = { + id: 'light1', + type: 'Light', + service: 'smartGondor', + subservice: 'gardens' +}; +const device2 = { + id: 'motion1', + type: 'Motion', + service: 'smartGondor', + subservice: 'gardens' +}; +const device3 = { + id: 'TestRobotPre', + type: 'RobotPre', + service: 'smartGondor', + subservice: 'gardens', + internalAttributes: { + lwm2mResourceMapping: { + position: { + objectType: 6789, + objectInstance: 0, + objectResource: 17 + } + } + } +}; + +describe('NGSI-LD - IoT Agent Lazy Devices', function() { + beforeEach(function(done) { + logger.setLevel('FATAL'); + + const time = new Date(1438760101468); // 2015-08-05T07:35:01.468+00:00 + timekeeper.freeze(time); + mongoUtils.cleanDbs(done); + + iotAgentLib.setDataQueryHandler(null); + }); + + afterEach(function(done) { + timekeeper.reset(); + delete device1.registrationId; + delete device2.registrationId; + delete device3.registrationId; + + iotAgentLib.clearAll(function() { + iotAgentLib.deactivate(function() { + mongoUtils.cleanDbs(done); + }); + }); + }); + + xdescribe('When the IoT Agent receives an update on the device data in JSON format', function() { + const options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/v2/op/update', + method: 'POST', + json: { + actionType: 'update', + entities: [ + { + id: 'Light:light1', + type: 'Light', + dimming: { + type: 'Percentage', + value: 12 + } + } + ] + }, + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': 'gardens' + } + }; + + beforeEach(function(done) { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .post( + '/ngsi-ld/v1/csourceRegistrations/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerIoTAgent1.json' + ) + ) + .reply(201, null, { Location: '/v2/registrations/6319a7f5254b05844116584d' }); + + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post('/ngsi-ld/v1/entityOperations/upsert/') + .reply(200); + + async.series([apply(iotAgentLib.activate, iotAgentConfig), apply(iotAgentLib.register, device1)], done); + }); + + it('should call the device handler with the received data', function(done) { + iotAgentLib.setDataUpdateHandler(function(id, type, service, subservice, attributes, callback) { + id.should.equal(device1.type + ':' + device1.id); + type.should.equal(device1.type); + attributes[0].value.should.equal(12); + callback(null); + }); + + request(options, function(error, response, body) { + should.not.exist(error); + response.statusCode.should.equal(204); + done(); + }); + }); + }); + + describe('When a IoT Agent receives an update on multiple contexts', function() { + it('should call the device handler for each of the contexts'); + }); + + xdescribe('When a context query arrives to the IoT Agent', function() { + const options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/v2/op/query', + method: 'POST', + json: true, + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': 'gardens' + }, + body: { + entities: [ + { + id: 'Light:light1' + } + ], + attrs: ['dimming'] + } + }; + const sensorData = [ + { + id: 'Light:light1', + type: 'Light', + dimming: { + type: 'Percentage', + value: 19 + } + } + ]; + + beforeEach(function(done) { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .post( + '/ngsi-ld/v1/csourceRegistrations/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerIoTAgent1.json' + ) + ) + .reply(201, null, { Location: '/v2/registrations/6319a7f5254b05844116584d' }); + + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post('/ngsi-ld/v1/entityOperations/upsert/') + .reply(200); + + async.series([apply(iotAgentLib.activate, iotAgentConfig), apply(iotAgentLib.register, device1)], done); + }); + + it('should return the information querying the underlying devices', function(done) { + const expectedResponse = utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextProviderResponses/queryInformationResponse.json' + ); + + iotAgentLib.setDataQueryHandler(function(id, type, service, subservice, attributes, callback) { + id.should.equal(device1.type + ':' + device1.id); + type.should.equal(device1.type); + attributes[0].should.equal('dimming'); + callback(null, sensorData[0]); + }); + + request(options, function(error, response, body) { + should.not.exist(error); + body.should.eql(expectedResponse); + done(); + }); + }); + }); + + xdescribe('When a context query arrives to the IoT Agent and no handler is set', function() { + const options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/v2/op/query', + method: 'POST', + json: true, + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': 'gardens' + }, + body: { + entities: [ + { + id: 'Light:light1' + } + ], + attrs: ['dimming'] + } + }; + + beforeEach(function(done) { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .post( + '/ngsi-ld/v1/csourceRegistrations/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerIoTAgent1.json' + ) + ) + .reply(201, null, { Location: '/v2/registrations/6319a7f5254b05844116584d' }); + + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post('/ngsi-ld/v1/entityOperations/upsert/') + .reply(200); + + async.series([apply(iotAgentLib.activate, iotAgentConfig), apply(iotAgentLib.register, device1)], function( + error + ) { + done(); + }); + }); + + it('should not give any error', function(done) { + request(options, function(error, response, body) { + should.not.exist(error); + response.statusCode.should.equal(200); + done(); + }); + }); + + it('should return the empty value', function(done) { + request(options, function(error, response, body) { + const entities = body; + entities[0].dimming.value.should.equal(''); + done(); + }); + }); + }); + + xdescribe('When a query arrives to the IoT Agent without any attributes', function() { + const options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/v2/op/query', + method: 'POST', + json: true, + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': 'gardens' + }, + body: { + entities: [ + { + id: 'Light:light1' + } + ] + } + }; + const sensorData = [ + { + id: 'Light:light1', + type: 'Light', + temperature: { + type: 'centigrades', + value: 19 + } + } + ]; + + beforeEach(function(done) { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .post( + '/ngsi-ld/v1/csourceRegistrations/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerIoTAgent1.json' + ) + ) + .reply(201, null, { Location: '/v2/registrations/6319a7f5254b05844116584d' }); + + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post('/ngsi-ld/v1/entityOperations/upsert/') + .reply(200); + + async.series([apply(iotAgentLib.activate, iotAgentConfig), apply(iotAgentLib.register, device1)], done); + }); + + it('should return the information of all the attributes', function(done) { + const expectedResponse = utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextProviderResponses/' + + 'queryInformationResponseEmptyAttributes.json' + ); + + iotAgentLib.setDataQueryHandler(function(id, type, service, subservice, attributes, callback) { + should.exist(attributes); + attributes.length.should.equal(1); + attributes[0].should.equal('temperature'); + callback(null, sensorData[0]); + }); + + request(options, function(error, response, body) { + should.not.exist(error); + body.should.eql(expectedResponse); + done(); + }); + }); + }); + + xdescribe('When a context query arrives to the IoT Agent for a type with static attributes', function() { + const options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/v2/op/query', + method: 'POST', + json: true, + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': 'gardens' + }, + body: { + entities: [ + { + id: 'Motion:motion1' + } + ], + attrs: ['moving', 'location'] + } + }; + const sensorData = [ + { + id: 'Motion:motion1', + type: 'Motion', + moving: { + type: 'Boolean', + value: true + } + } + ]; + + beforeEach(function(done) { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .post( + '/ngsi-ld/v1/csourceRegistrations/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerIoTAgent2.json' + ) + ) + .reply(201, null, { Location: '/v2/registrations/6319a7f5254b05844116584d' }); + + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post('/ngsi-ld/v1/entityOperations/upsert/') + .reply(200); + + async.series([apply(iotAgentLib.activate, iotAgentConfig), apply(iotAgentLib.register, device2)], done); + }); + + it('should return the information adding the static attributes', function(done) { + const expectedResponse = utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextProviderResponses/queryInformationStaticAttributesResponse.json' + ); + + iotAgentLib.setDataQueryHandler(function(id, type, service, subservice, attributes, callback) { + id.should.equal('Motion:motion1'); + type.should.equal('Motion'); + attributes[0].should.equal('moving'); + attributes[1].should.equal('location'); + callback(null, sensorData[0]); + }); + + request(options, function(error, response, body) { + should.not.exist(error); + body.should.eql(expectedResponse); + done(); + }); + }); + }); + + xdescribe( + 'When the IoT Agent receives an update on the device data in JSON format for a type with' + + 'internalAttributes', + function() { + const options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/v2/op/update', + method: 'POST', + json: { + actionType: 'update', + entities: [ + { + id: 'RobotPre:TestRobotPre', + type: 'RobotPre', + moving: { + type: 'Boolean', + value: true + } + } + ] + }, + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': 'gardens' + } + }; + + beforeEach(function(done) { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .post( + '/ngsi-ld/v1/csourceRegistrations/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerIoTAgent4.json' + ) + ) + .reply(201, null, { Location: '/v2/registrations/6319a7f5254b05844116584d' }); + + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post('/ngsi-ld/v1/entityOperations/upsert/') + .reply(200); + + async.series([apply(iotAgentLib.activate, iotAgentConfig), apply(iotAgentLib.register, device3)], done); + }); + + it('should call the device handler with the received data', function(done) { + iotAgentLib.setDataUpdateHandler(function(id, type, service, subservice, attributes, callback) { + id.should.equal(device3.type + ':' + device3.id); + type.should.equal(device3.type); + attributes[0].value.should.equal(true); + callback(null); + }); + + request(options, function(error, response, body) { + should.not.exist(error); + response.statusCode.should.equal(204); + done(); + }); + }); + } + ); + + xdescribe('When a context query arrives to the IoT Agent and id and type query params are not present', function() { + const options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/v2/op/query', + method: 'POST', + json: true, + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': 'gardens' + }, + body: { + entities: [ + { + idPattern: '.*' + } + ] + } + }; + + beforeEach(function(done) { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .post( + '/ngsi-ld/v1/csourceRegistrations/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerIoTAgent1.json' + ) + ) + .reply(201, null, { Location: '/v2/registrations/6319a7f5254b05844116584d' }); + + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post( + '/ngsi-ld/v1/csourceRegistrations/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerIoTAgent2.json' + ) + ) + .reply(201, null, { Location: '/v2/registrations/6319a7f5254b05844116584d' }); + + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post( + '/ngsi-ld/v1/csourceRegistrations/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerIoTAgent4.json' + ) + ) + .reply(201, null, { Location: '/v2/registrations/6319a7f5254b05844116584d' }); + + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post('/ngsi-ld/v1/entityOperations/upsert/') + .times(3) + .reply(204); + + async.series( + [ + apply(iotAgentLib.activate, iotAgentConfig), + apply(iotAgentLib.register, device1), + apply(iotAgentLib.register, device2), + apply(iotAgentLib.register, device3) + ], + done + ); + }); + + it('should return error as idPattern is not supported', function(done) { + request(options, function(error, response, body) { + should.not.exist(error); + response.statusCode.should.equal(400); + body.error.should.equal('BadRequest'); + body.description.should.equal('idPattern usage in query'); + done(); + }); + }); + }); + + xdescribe('When a context query arrives to the IoT Agent and id query param is not present', function() { + const options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/v2/op/query', + method: 'POST', + json: true, + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': 'gardens' + }, + body: { + entities: [ + { + idPattern: '.*', + type: 'Light' + } + ] + } + }; + + beforeEach(function(done) { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .post( + '/ngsi-ld/v1/csourceRegistrations/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerIoTAgent1.json' + ) + ) + .reply(201, null, { Location: '/v2/registrations/6319a7f5254b05844116584d' }); + + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post( + '/ngsi-ld/v1/csourceRegistrations/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerIoTAgent2.json' + ) + ) + .reply(201, null, { Location: '/v2/registrations/6319a7f5254b05844116584d' }); + + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post( + '/ngsi-ld/v1/csourceRegistrations/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerIoTAgent4.json' + ) + ) + .reply(201, null, { Location: '/v2/registrations/6319a7f5254b05844116584d' }); + + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post('/ngsi-ld/v1/entityOperations/upsert/') + .times(3) + .reply(204); + + async.series( + [ + apply(iotAgentLib.activate, iotAgentConfig), + apply(iotAgentLib.register, device1), + apply(iotAgentLib.register, device2), + apply(iotAgentLib.register, device3) + ], + done + ); + }); + + it('should return error as idPattern is not supported', function(done) { + request(options, function(error, response, body) { + should.not.exist(error); + response.statusCode.should.equal(400); + body.error.should.equal('BadRequest'); + body.description.should.equal('idPattern usage in query'); + done(); + }); + }); + }); + + xdescribe('When a query arrives to the IoT Agent with id, type and attributes', function() { + const options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/v2/op/query', + method: 'POST', + json: true, + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': 'gardens' + }, + body: { + entities: [ + { + id: 'Light:light1', + type: 'Light' + } + ], + attrs: ['temperature'] + } + }; + const sensorData = [ + { + id: 'Light:light1', + type: 'Light', + temperature: { + type: 'centigrades', + value: 19 + } + } + ]; + + beforeEach(function(done) { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .post( + '/ngsi-ld/v1/csourceRegistrations/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerIoTAgent1.json' + ) + ) + .reply(201, null, { Location: '/v2/registrations/6319a7f5254b05844116584d' }); + + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post('/ngsi-ld/v1/entityOperations/upsert/') + .reply(200); + + async.series([apply(iotAgentLib.activate, iotAgentConfig), apply(iotAgentLib.register, device1)], done); + }); + + it('should return the information of all the attributes', function(done) { + const expectedResponse = utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextProviderResponses/' + + 'queryInformationResponseEmptyAttributes.json' + ); + + iotAgentLib.setDataQueryHandler(function(id, type, service, subservice, attributes, callback) { + should.exist(attributes); + attributes.length.should.equal(1); + attributes[0].should.equal('temperature'); + callback(null, sensorData[0]); + }); + + request(options, function(error, response, body) { + should.not.exist(error); + body.should.eql(expectedResponse); + done(); + }); + }); + }); +}); diff --git a/test/unit/ngsi-ld/lazyAndCommands/polling-commands-test.js b/test/unit/ngsi-ld/lazyAndCommands/polling-commands-test.js new file mode 100644 index 000000000..922546ef0 --- /dev/null +++ b/test/unit/ngsi-ld/lazyAndCommands/polling-commands-test.js @@ -0,0 +1,389 @@ +/* + * Copyright 2020 Telefonica Investigación y Desarrollo, S.A.U + * + * This file is part of fiware-iotagent-lib + * + * fiware-iotagent-lib 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. + * + * fiware-iotagent-lib 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 fiware-iotagent-lib. + * 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: Jason Fox - FIWARE Foundation + */ + +const iotAgentLib = require('../../../../lib/fiware-iotagent-lib'); +const utils = require('../../../tools/utils'); +const should = require('should'); +const logger = require('logops'); +const nock = require('nock'); +const mongoUtils = require('../../mongodb/mongoDBUtils'); +const request = require('request'); +let contextBrokerMock; +let statusAttributeMock; +const iotAgentConfig = { + contextBroker: { + host: '192.168.1.1', + port: '1026', + ngsiVersion: 'ld', + jsonLdContext: 'http://context.json-ld' + }, + server: { + port: 4041 + }, + types: { + Light: { + commands: [], + lazy: [ + { + name: 'temperature', + type: 'centigrades' + } + ], + active: [ + { + name: 'pressure', + type: 'Hgmm' + } + ] + }, + Termometer: { + commands: [], + lazy: [ + { + name: 'temp', + type: 'kelvin' + } + ], + active: [] + }, + Motion: { + commands: [], + lazy: [ + { + name: 'moving', + type: 'Boolean' + } + ], + staticAttributes: [ + { + name: 'location', + type: 'Vector', + value: '(123,523)' + } + ], + active: [] + }, + Robot: { + commands: [ + { + name: 'position', + type: 'Array' + } + ], + lazy: [], + staticAttributes: [], + active: [] + } + }, + deviceRegistry: { + type: 'mongodb' + }, + + mongodb: { + host: 'localhost', + port: '27017', + db: 'iotagent' + }, + service: 'smartGondor', + subservice: 'gardens', + providerUrl: 'http://smartGondor.com', + pollingExpiration: 200, + pollingDaemonFrequency: 20 +}; +const device3 = { + id: 'r2d2', + type: 'Robot', + service: 'smartGondor', + subservice: 'gardens', + polling: true +}; + +describe('NGSI-LD - Polling commands', function() { + beforeEach(function(done) { + logger.setLevel('FATAL'); + + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .post('/ngsi-ld/v1/csourceRegistrations/') + .reply(201, null, { Location: '/v2/registrations/6319a7f5254b05844116584d' }); + + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post('/ngsi-ld/v1/entityOperations/upsert/') + .reply(200); + + iotAgentLib.activate(iotAgentConfig, done); + }); + + afterEach(function(done) { + delete device3.registrationId; + iotAgentLib.clearAll(function() { + iotAgentLib.deactivate(function() { + mongoUtils.cleanDbs(function() { + nock.cleanAll(); + iotAgentLib.setDataUpdateHandler(); + iotAgentLib.setCommandHandler(); + done(); + }); + }); + }); + }); + + xdescribe('When a command update arrives to the IoT Agent for a device with polling', function() { + const options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/v2/op/update', + method: 'POST', + json: { + actionType: 'update', + entities: [ + { + id: 'Robot:r2d2', + type: 'Robot', + position: { + type: 'Array', + value: '[28, -104, 23]' + } + } + ] + }, + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': 'gardens' + } + }; + + beforeEach(function(done) { + statusAttributeMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + + .post( + '/ngsi-ld/v1/entities/Robot:r2d2/attrs?type=Robot', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextCommandStatus.json' + ) + ) + .reply(204); + + iotAgentLib.register(device3, function(error) { + done(); + }); + }); + + it('should not call the client handler', function(done) { + let handlerCalled = false; + + iotAgentLib.setCommandHandler(function(id, type, service, subservice, attributes, callback) { + handlerCalled = true; + callback(null, { + id, + type, + attributes: [ + { + name: 'position', + type: 'Array', + value: '[28, -104, 23]' + } + ] + }); + }); + + request(options, function(error, response, body) { + should.not.exist(error); + handlerCalled.should.equal(false); + done(); + }); + }); + it('should create the attribute with the "_status" prefix in the Context Broker', function(done) { + iotAgentLib.setCommandHandler(function(id, type, service, subservice, attributes, callback) { + callback(null); + }); + + request(options, function(error, response, body) { + should.not.exist(error); + statusAttributeMock.done(); + done(); + }); + }); + it('should store the commands in the queue', function(done) { + iotAgentLib.setCommandHandler(function(id, type, service, subservice, attributes, callback) { + callback(null); + }); + + request(options, function(error, response, body) { + iotAgentLib.commandQueue('smartGondor', 'gardens', 'r2d2', function(error, listCommands) { + should.not.exist(error); + listCommands.count.should.equal(1); + listCommands.commands[0].name.should.equal('position'); + listCommands.commands[0].type.should.equal('Array'); + listCommands.commands[0].value.should.equal('[28, -104, 23]'); + done(); + }); + }); + }); + }); + + xdescribe('When a command arrives with multiple values in the value field', function() { + const options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/v2/op/update', + method: 'POST', + json: { + actionType: 'update', + entities: [ + { + id: 'Robot:r2d2', + type: 'Robot', + position: { + type: 'Array', + value: { + attr1: 12, + attr2: 24 + } + } + } + ] + }, + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': 'gardens' + } + }; + + beforeEach(function(done) { + statusAttributeMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + + .post( + '/ngsi-ld/v1/entities/Robot:r2d2/attrs?type=Robot', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextCommandStatus.json' + ) + ) + .reply(204); + + iotAgentLib.register(device3, function(error) { + done(); + }); + }); + + it('should return a 200 OK both in HTTP and in the status code', function(done) { + iotAgentLib.setCommandHandler(function(id, type, service, subservice, attributes, callback) { + callback(null); + }); + + request(options, function(error, response, body) { + should.not.exist(error); + + response.statusCode.should.equal(204); + + done(); + }); + }); + }); + + xdescribe('When a polling command expires', function() { + const options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/v2/op/update', + method: 'POST', + json: { + actionType: 'update', + entities: [ + { + id: 'Robot:r2d2', + type: 'Robot', + position: { + type: 'Array', + value: '[28, -104, 23]' + } + } + ] + }, + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': 'gardens' + } + }; + + beforeEach(function(done) { + statusAttributeMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + + .post( + '/ngsi-ld/v1/entities/Robot:r2d2/attrs?type=Robot', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextCommandStatus.json' + ) + ) + .reply(204); + + statusAttributeMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + + .post( + '/ngsi-ld/v1/entities/Robot:r2d2/attrs?type=Robot', + utils.readExampleFile( + './test/unit//ngsiv2/examples/contextRequests/updateContextCommandExpired.json' + ) + ) + .reply(204); + + iotAgentLib.register(device3, function(error) { + done(); + }); + }); + + it('should remove it from the queue', function(done) { + iotAgentLib.setCommandHandler(function(id, type, service, subservice, attributes, callback) { + callback(null); + }); + + request(options, function(error, response, body) { + setTimeout(function() { + iotAgentLib.commandQueue('smartGondor', 'gardens', 'r2d2', function(error, listCommands) { + should.not.exist(error); + listCommands.count.should.equal(0); + done(); + }); + }, 300); + }); + }); + + it('should mark it as ERROR in the Context Broker', function(done) { + iotAgentLib.setCommandHandler(function(id, type, service, subservice, attributes, callback) { + callback(null); + }); + + request(options, function(error, response, body) { + setTimeout(function() { + iotAgentLib.commandQueue('smartGondor', 'gardens', 'r2d2', function(error, listCommands) { + statusAttributeMock.done(); + done(); + }); + }, 300); + }); + }); + }); +}); diff --git a/test/unit/ngsi-ld/ngsiService/active-devices-test.js b/test/unit/ngsi-ld/ngsiService/active-devices-test.js new file mode 100644 index 000000000..da8f5df92 --- /dev/null +++ b/test/unit/ngsi-ld/ngsiService/active-devices-test.js @@ -0,0 +1,721 @@ +/* + * Copyright 2020 Telefonica Investigación y Desarrollo, S.A.U + * + * This file is part of fiware-iotagent-lib + * + * fiware-iotagent-lib 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. + * + * fiware-iotagent-lib 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 fiware-iotagent-lib. + * If not, see http://www.gnu.org/licenses/. + * + * For those usages not covered by the GNU Affero General Public License + * please contact with::[contacto@tid.es] + * + * Modified by: Jason Fox - FIWARE Foundation + */ + +const iotAgentLib = require('../../../../lib/fiware-iotagent-lib'); +const utils = require('../../../tools/utils'); +const timekeeper = require('timekeeper'); +const should = require('should'); +const logger = require('logops'); +const nock = require('nock'); +let contextBrokerMock; +const iotAgentConfig = { + contextBroker: { + host: '192.168.1.1', + port: '1026', + ngsiVersion: 'ld', + jsonLdContext: 'http://context.json-ld' + }, + server: { + port: 4041 + }, + types: { + Light: { + commands: [], + type: 'Light', + lazy: [ + { + name: 'temperature', + type: 'centigrades' + } + ], + active: [ + { + name: 'pressure', + type: 'Hgmm' + } + ] + }, + BrokenLight: { + commands: [], + lazy: [ + { + name: 'temperature', + type: 'centigrades' + } + ], + active: [ + { + name: 'pressure', + type: 'Hgmm' + } + ] + }, + Termometer: { + type: 'Termometer', + commands: [], + lazy: [ + { + name: 'temp', + type: 'kelvin' + } + ], + active: [] + }, + Humidity: { + type: 'Humidity', + cbHost: 'http://192.168.1.1:3024', + commands: [], + lazy: [], + active: [ + { + name: 'humidity', + type: 'percentage' + } + ] + }, + Motion: { + type: 'Motion', + commands: [], + lazy: [], + staticAttributes: [ + { + name: 'location', + type: 'geo:point', + value: '153,523' + } + ], + active: [ + { + name: 'humidity', + type: 'percentage' + } + ] + }, + Lamp: { + type: 'Lamp', + commands: [], + lazy: [], + staticAttributes: [ + { + name: 'controlledProperty', + type: 'Property', + value: 'StaticValue', + metadata: { + includes: { type: 'Property', value: 'bell' } + } + } + ], + active: [ + { + name: 'luminosity', + type: 'Number', + metadata: { + unitCode: { type: 'Property', value: 'CAL' } + } + } + ] + } + }, + service: 'smartGondor', + subservice: 'gardens', + providerUrl: 'http://smartGondor.com' +}; + +describe('NGSI-LD - Active attributes test', function() { + const values = [ + { + name: 'state', + type: 'Boolean', + value: true + }, + { + name: 'dimming', + type: 'Number', + value: 87 + } + ]; + + beforeEach(function() { + logger.setLevel('FATAL'); + }); + + afterEach(function(done) { + iotAgentLib.deactivate(done); + }); + + describe('When the IoT Agent receives new information from a device', function() { + beforeEach(function(done) { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .patch( + '/ngsi-ld/v1/entities/light1/attrs', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/updateContext.json') + ) + .query({ type: 'Light' }) + .reply(204); + + iotAgentLib.activate(iotAgentConfig, done); + }); + + it('should change the value of the corresponding attribute in the context broker', function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When the IoT Agent receives new information and the timestamp flag is on', function() { + let modifiedValues; + + beforeEach(function(done) { + const time = new Date(1438760101468); // 2015-08-05T07:35:01.468+00:00 + + modifiedValues = [ + { + name: 'state', + type: 'boolean', + value: true + }, + { + name: 'dimming', + type: 'number', + value: 87 + } + ]; + + timekeeper.freeze(time); + + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .patch( + '/ngsi-ld/v1/entities/light1/attrs', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/updateContextTimestamp.json') + ) + .query({ type: 'Light' }) + .reply(204); + + iotAgentConfig.timestamp = true; + iotAgentLib.activate(iotAgentConfig, done); + }); + + afterEach(function(done) { + delete iotAgentConfig.timestamp; + timekeeper.reset(); + + done(); + }); + + it('should add the timestamp to the entity and all the attributes', function(done) { + iotAgentLib.update('light1', 'Light', '', modifiedValues, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When the IoTA gets a set of values with a TimeInstant which are not in ISO8601 format', function() { + let modifiedValues; + + beforeEach(function(done) { + modifiedValues = [ + { + name: 'state', + type: 'Boolean', + value: 'true' + }, + { + name: 'TimeInstant', + type: 'ISO8601', + value: '2018-10-05T11:03:56 00:00Z' + } + ]; + + nock.cleanAll(); + + iotAgentConfig.timestamp = true; + iotAgentLib.activate(iotAgentConfig, done); + }); + + afterEach(function(done) { + delete iotAgentConfig.timestamp; + done(); + }); + + it('should fail with a 400 BAD_TIMESTAMP error', function(done) { + iotAgentLib.update('light1', 'Light', '', modifiedValues, function(error) { + should.exist(error); + error.code.should.equal(400); + error.name.should.equal('BAD_TIMESTAMP'); + done(); + }); + }); + }); + + describe( + 'When the IoTA gets a set of values with a TimeInstant which are in ISO8601 format ' + 'without milis', + function() { + let modifiedValues; + + beforeEach(function(done) { + const time = new Date(1666477342000); // 2022-10-22T22:22:22Z + + modifiedValues = [ + { + name: 'state', + type: 'boolean', + value: true + }, + { + name: 'TimeInstant', + type: 'DateTime', + value: '2022-10-22T22:22:22Z' + } + ]; + + timekeeper.freeze(time); + + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .patch( + '/ngsi-ld/v1/entities/light1/attrs', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/' + + 'updateContextTimestampOverrideWithoutMilis.json' + ) + ) + .query({ type: 'Light' }) + .reply(204); + + iotAgentConfig.timestamp = true; + iotAgentLib.activate(iotAgentConfig, done); + }); + + afterEach(function(done) { + delete iotAgentConfig.timestamp; + timekeeper.reset(); + + done(); + }); + + it('should not fail', function(done) { + iotAgentLib.update('light1', 'Light', '', modifiedValues, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + } + ); + + describe( + 'When the IoT Agent receives new information, the timestamp flag is on' + 'and timezone is defined', + function() { + let modifiedValues; + + beforeEach(function(done) { + const time = new Date(1438760101468); // 2015-08-05T07:35:01.468+00:00 + + modifiedValues = [ + { + name: 'state', + type: 'boolean', + value: true + }, + { + name: 'dimming', + type: 'number', + value: 87 + } + ]; + + timekeeper.freeze(time); + + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .patch( + '/ngsi-ld/v1/entities/light1/attrs', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/' + 'updateContextTimestampTimezone.json' + ) + ) + .query({ type: 'Light' }) + .reply(204); + + iotAgentConfig.timestamp = true; + iotAgentConfig.types.Light.timezone = 'America/Los_Angeles'; + iotAgentLib.activate(iotAgentConfig, done); + }); + + afterEach(function(done) { + delete iotAgentConfig.timestamp; + delete iotAgentConfig.types.Light.timezone; + timekeeper.reset(); + + done(); + }); + + it('should add the timestamp to the entity and all the attributes', function(done) { + iotAgentLib.update('light1', 'Light', '', modifiedValues, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + } + ); + + describe('When the IoTA gets a set of values with a TimeInstant and the timestamp flag is on', function() { + let modifiedValues; + + beforeEach(function(done) { + const time = new Date(1438760101468); // 2015-08-05T07:35:01.468+00:00 + + modifiedValues = [ + { + name: 'state', + type: 'boolean', + value: true + }, + { + name: 'TimeInstant', + type: 'DateTime', + value: '2015-12-14T08:06:01.468Z' + } + ]; + + timekeeper.freeze(time); + + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .patch( + '/ngsi-ld/v1/entities/light1/attrs', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextTimestampOverride.json' + ) + ) + .query({ type: 'Light' }) + .reply(204); + + iotAgentConfig.timestamp = true; + iotAgentLib.activate(iotAgentConfig, done); + }); + + afterEach(function(done) { + delete iotAgentConfig.timestamp; + timekeeper.reset(); + + done(); + }); + + it('should not override the received instant and should not add metadatas for this request', function(done) { + iotAgentLib.update('light1', 'Light', '', modifiedValues, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe( + 'When the IoTA gets a set of values with a TimeInstant, the timestamp flag is on' + 'and timezone is defined', + function() { + let modifiedValues; + + beforeEach(function(done) { + const time = new Date(1438760101468); // 2015-08-05T07:35:01.468+00:00 + + modifiedValues = [ + { + name: 'state', + type: 'boolean', + value: true + }, + { + name: 'TimeInstant', + type: 'DateTime', + value: '2015-12-14T08:06:01.468Z' + } + ]; + + timekeeper.freeze(time); + + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .patch( + '/ngsi-ld/v1/entities/light1/attrs', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextTimestampOverride.json' + ) + ) + .query({ type: 'Light' }) + .reply(204); + + iotAgentConfig.timestamp = true; + iotAgentConfig.types.Light.timezone = 'America/Los_Angeles'; + iotAgentLib.activate(iotAgentConfig, done); + }); + + afterEach(function(done) { + delete iotAgentConfig.timestamp; + delete iotAgentConfig.types.Light.timezone; + timekeeper.reset(); + + done(); + }); + + it('should not override the received instant and should not add' + + ' metadatas for this request', function(done) { + iotAgentLib.update('light1', 'Light', '', modifiedValues, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + } + ); + + describe('When the IoT Agent receives information from a device whose type doesn\'t' + + ' have a type name', function() { + beforeEach(function(done) { + nock.cleanAll(); + + iotAgentLib.activate(iotAgentConfig, done); + }); + + it('should fail with a 500 TYPE_NOT_FOUND error', function(done) { + iotAgentLib.update('light1', 'BrokenLight', '', values, function(error) { + should.exist(error); + error.code.should.equal(500); + error.name.should.equal('TYPE_NOT_FOUND'); + done(); + }); + }); + }); + + describe('When the Context Broker returns an HTTP error code updating an entity', function() { + beforeEach(function(done) { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .patch( + '/ngsi-ld/v1/entities/light1/attrs', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/updateContext.json') + ) + .query({ type: 'Light' }) + .reply( + 413, + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextResponses/updateContext1Failed.json') + ); + + iotAgentLib.activate(iotAgentConfig, done); + }); + + it('should return ENTITY_GENERIC_ERROR an error to the caller', function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.exist(error); + should.exist(error.name); + error.code.should.equal(413); + error.details.description.should.equal('payload size: 1500000, max size supported: 1048576'); + error.details.error.should.equal('RequestEntityTooLarge'); + error.name.should.equal('ENTITY_GENERIC_ERROR'); + done(); + }); + }); + }); + + describe('When the Context Broker returns an application error code updating an entity', function() { + beforeEach(function(done) { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .patch( + '/ngsi-ld/v1/entities/light1/attrs', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/updateContext.json') + ) + .query({ type: 'Light' }) + .reply( + 400, + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextResponses/updateContext2Failed.json') + ); + + iotAgentLib.activate(iotAgentConfig, done); + }); + + it('should return ENTITY_GENERIC_ERROR an error to the caller', function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.exist(error); + should.exist(error.name); + error.name.should.equal('ENTITY_GENERIC_ERROR'); + done(); + }); + }); + }); + + describe('When there is a transport error connecting to the Context Broker', function() { + beforeEach(function(done) { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .patch( + '/ngsi-ld/v1/entities/light1/attrs', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/updateContext.json') + ) + .query({ type: 'Light' }) + .reply( + 500, + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextResponses/updateContext2Failed.json') + ); + + iotAgentLib.activate(iotAgentConfig, done); + }); + + it('should return a ENTITY_GENERIC_ERROR error to the caller', function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.exist(error); + should.exist(error.name); + error.name.should.equal('ENTITY_GENERIC_ERROR'); + should.exist(error.details); + should.exist(error.code); + error.code.should.equal(500); + done(); + }); + }); + }); + + describe('When the IoT Agent recieves information for a type with a configured Context Broker', function() { + beforeEach(function(done) { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:3024') + .matchHeader('fiware-service', 'smartGondor') + .patch( + '/ngsi-ld/v1/entities/humSensor/attrs', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/updateContext.json') + ) + .query({ type: 'Humidity' }) + .reply(204); + + iotAgentLib.activate(iotAgentConfig, done); + }); + + it('should use the Context Broker defined by the type', function(done) { + iotAgentLib.update('humSensor', 'Humidity', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an IoT Agent receives information for a type with static attributes', function() { + const newValues = [ + { + name: 'moving', + type: 'Boolean', + value: true + } + ]; + + beforeEach(function(done) { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .patch( + '/ngsi-ld/v1/entities/motion1/attrs', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/' + 'contextRequests/updateContextStaticAttributes.json' + ) + ) + .query({ type: 'Motion' }) + .reply(204); + + iotAgentLib.activate(iotAgentConfig, done); + }); + it('should decorate the entity with the static attributes', function(done) { + iotAgentLib.update('motion1', 'Motion', '', newValues, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an IoT Agent receives information for a type with static attributes with metadata', function() { + const newValues = [ + { + name: 'luminosity', + type: 'Number', + value: '100', + metadata: { + unitCode: { type: 'Property', value: 'CAL' } + } + } + ]; + + beforeEach(function(done) { + nock.cleanAll(); + + /* jshint maxlen: 200 */ + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .patch( + '/ngsi-ld/v1/entities/lamp1/attrs', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextStaticAttributesMetadata.json' + ) + ) + .query({ type: 'Lamp' }) + .reply(204); + + iotAgentLib.activate(iotAgentConfig, done); + }); + it('should decorate the entity with the static attributes', function(done) { + iotAgentLib.update('lamp1', 'Lamp', '', newValues, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); +}); diff --git a/test/unit/ngsi-ld/ngsiService/autocast-test.js b/test/unit/ngsi-ld/ngsiService/autocast-test.js new file mode 100644 index 000000000..3a8021da5 --- /dev/null +++ b/test/unit/ngsi-ld/ngsiService/autocast-test.js @@ -0,0 +1,343 @@ +/* + * Copyright 2020 Telefonica Investigación y Desarrollo, S.A.U + * + * This file is part of fiware-iotagent-lib + * + * fiware-iotagent-lib 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. + * + * fiware-iotagent-lib 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 fiware-iotagent-lib. + * If not, see http://www.gnu.org/licenses/. + * + * For those usages not covered by the GNU Affero General Public License + * please contact with::[contacto@tid.es] + * + * Modified by: Jason Fox - FIWARE Foundation + */ + +const iotAgentLib = require('../../../../lib/fiware-iotagent-lib'); +const utils = require('../../../tools/utils'); +const should = require('should'); +const logger = require('logops'); +const nock = require('nock'); +let contextBrokerMock; +const iotAgentConfig = { + autocast: true, + contextBroker: { + host: '192.168.1.1', + port: '1026', + ngsiVersion: 'ld', + jsonLdContext: 'http://context.json-ld' + }, + server: { + port: 4041 + }, + types: { + Light: { + commands: [], + type: 'Light', + active: [ + { + name: 'pressure', + type: 'Number' + }, + { + name: 'temperature', + type: 'Number' + }, + { + name: 'id', + type: 'String' + }, + { + name: 'status', + type: 'Boolean' + }, + { + name: 'keep_alive', + type: 'None' + }, + { + name: 'tags', + type: 'Array' + }, + { + name: 'configuration', + type: 'Object' + } + ] + } + }, + service: 'smartGondor', + subservice: 'gardens', + providerUrl: 'http://smartGondor.com' +}; + +describe('NGSI-LD - JSON native types autocast test', function() { + beforeEach(function() { + logger.setLevel('FATAL'); + }); + + afterEach(function(done) { + iotAgentLib.deactivate(done); + }); + + describe( + 'When the IoT Agent receives new information from a device.' + 'Observation with Number type and Integer value', + function() { + const values = [ + { + name: 'pressure', + type: 'Number', + value: '23' + } + ]; + + beforeEach(function(done) { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .patch( + '/ngsi-ld/v1/entities/light1/attrs', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextAutocast1.json' + ) + ) + .query({ type: 'Light' }) + .reply(204); + + iotAgentLib.activate(iotAgentConfig, done); + }); + + it('should change the value of the corresponding attribute in the context broker', function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + } + ); + + describe( + 'When the IoT Agent receives new information from a device.' + 'Observation with Number type and Float value', + function() { + const values = [ + { + name: 'temperature', + type: 'Number', + value: '14.4' + } + ]; + + beforeEach(function(done) { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .patch( + '/ngsi-ld/v1/entities/light1/attrs', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextAutocast2.json' + ) + ) + .query({ type: 'Light' }) + .reply(204); + + iotAgentLib.activate(iotAgentConfig, done); + }); + + it('should change the value of the corresponding attribute in the context broker', function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + } + ); + + describe( + 'When the IoT Agent receives new information from a device.' + 'Observation with Boolean type and True value', + function() { + const values = [ + { + name: 'status', + type: 'Boolean', + value: 'true' + } + ]; + + beforeEach(function(done) { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .patch( + '/ngsi-ld/v1/entities/light1/attrs', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextAutocast3.json' + ) + ) + .query({ type: 'Light' }) + .reply(204); + + iotAgentLib.activate(iotAgentConfig, done); + }); + + it('should change the value of the corresponding attribute in the context broker', function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + } + ); + + describe( + 'When the IoT Agent receives new information from a device.' + 'Observation with Boolean type and False value', + function() { + const values = [ + { + name: 'status', + type: 'Boolean', + value: 'false' + } + ]; + + beforeEach(function(done) { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .patch( + '/ngsi-ld/v1/entities/light1/attrs', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextAutocast4.json' + ) + ) + .query({ type: 'Light' }) + .reply(204); + + iotAgentLib.activate(iotAgentConfig, done); + }); + + it('should change the value of the corresponding attribute in the context broker', function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + } + ); + + describe('When the IoT Agent receives new information from a device. Observation with None type', function() { + const values = [ + { + name: 'keep_alive', + type: 'None', + value: 'null' + } + ]; + + beforeEach(function(done) { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .patch( + '/ngsi-ld/v1/entities/light1/attrs', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/updateContextAutocast5.json') + ) + .query({ type: 'Light' }) + .reply(204); + + iotAgentLib.activate(iotAgentConfig, done); + }); + + it('should change the value of the corresponding attribute in the context broker', function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When the IoT Agent receives new information from a device. Observation with Array type', function() { + const values = [ + { + name: 'tags', + type: 'Array', + value: '["iot","device"]' + } + ]; + + beforeEach(function(done) { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .patch( + '/ngsi-ld/v1/entities/light1/attrs', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/updateContextAutocast6.json') + ) + .query({ type: 'Light' }) + .reply(204); + + iotAgentLib.activate(iotAgentConfig, done); + }); + + it('should change the value of the corresponding attribute in the context broker', function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When the IoT Agent receives new information from a device. Observation with Object type', function() { + const values = [ + { + name: 'configuration', + type: 'Object', + value: '{"firmware": {"version": "1.1.0","hash": "cf23df2207d99a74fbe169e3eba035e633b65d94"}}' + } + ]; + + beforeEach(function(done) { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .patch( + '/ngsi-ld/v1/entities/light1/attrs', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/updateContextAutocast7.json') + ) + .query({ type: 'Light' }) + .reply(204); + + iotAgentLib.activate(iotAgentConfig, done); + }); + + it('should change the value of the corresponding attribute in the context broker', function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); +}); diff --git a/test/unit/ngsi-ld/ngsiService/staticAttributes-test.js b/test/unit/ngsi-ld/ngsiService/staticAttributes-test.js new file mode 100644 index 000000000..e787f940e --- /dev/null +++ b/test/unit/ngsi-ld/ngsiService/staticAttributes-test.js @@ -0,0 +1,148 @@ +/* + * Copyright 2020 Telefonica Investigación y Desarrollo, S.A.U + * + * This file is part of fiware-iotagent-lib + * + * fiware-iotagent-lib 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. + * + * fiware-iotagent-lib 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 fiware-iotagent-lib. + * If not, see http://www.gnu.org/licenses/. + * + * For those usages not covered by the GNU Affero General Public License + * please contact with::[contacto@tid.es] + * + * Modified by: Jason Fox - FIWARE Foundation + */ + +const iotAgentLib = require('../../../../lib/fiware-iotagent-lib'); +const async = require('async'); +const should = require('should'); +const logger = require('logops'); +const nock = require('nock'); +let contextBrokerMock; +const iotAgentConfig = { + contextBroker: { + host: '192.168.1.1', + port: '1026', + ngsiVersion: 'ld', + jsonLdContext: 'http://context.json-ld' + }, + server: { + port: 4041 + }, + types: { + Light: { + commands: [], + type: 'Light', + lazy: [ + { + name: 'temperature', + type: 'centigrades' + } + ], + active: [ + { + name: 'pressure', + type: 'Hgmm' + } + ], + staticAttributes: [ + { + name: 'attr1', + type: 'type1' + }, + { + name: 'attr2', + type: 'type2' + }, + { + name: 'attr3', + type: 'type3' + }, + { + name: 'attr4', + type: 'type4' + } + ] + } + }, + timestamp: true, + service: 'smartGondor', + subservice: 'gardens', + providerUrl: 'http://smartGondor.com' +}; + +xdescribe('NGSI-LD - Static attributes test', function() { + const values = [ + { + name: 'state', + type: 'boolean', + value: true + }, + { + name: 'dimming', + type: 'number', + value: 87 + } + ]; + + beforeEach(function() { + logger.setLevel('FATAL'); + }); + + afterEach(function(done) { + iotAgentLib.deactivate(done); + }); + + describe('When information from a device with multiple static attributes and metadata is sent', function() { + beforeEach(function(done) { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .patch('/ngsi-ld/v1/entities/light1/attrs') + .query({ type: 'Light' }) + .times(4) + .reply(204) + .patch('/ngsi-ld/v1/entities/light1/attrs', function(body) { + let metadatas = 0; + for (const i in body) { + if (body[i].metadata) { + metadatas += Object.keys(body[i].metadata).length; + } + } + return metadatas === Object.keys(body).length - 1; + }) + .query({ type: 'Light' }) + .reply(204); + + iotAgentLib.activate(iotAgentConfig, done); + }); + + it('should send a single TimeInstant per attribute', function(done) { + async.series( + [ + async.apply(iotAgentLib.update, 'light1', 'Light', '', values), + async.apply(iotAgentLib.update, 'light1', 'Light', '', values), + async.apply(iotAgentLib.update, 'light1', 'Light', '', values), + async.apply(iotAgentLib.update, 'light1', 'Light', '', values), + async.apply(iotAgentLib.update, 'light1', 'Light', '', values) + ], + function(error, results) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + } + ); + }); + }); +}); diff --git a/test/unit/ngsi-ld/ngsiService/subscriptions-test.js b/test/unit/ngsi-ld/ngsiService/subscriptions-test.js new file mode 100644 index 000000000..17ebd2294 --- /dev/null +++ b/test/unit/ngsi-ld/ngsiService/subscriptions-test.js @@ -0,0 +1,338 @@ +/* + * Copyright 2020 Telefonica Investigación y Desarrollo, S.A.U + * + * This file is part of fiware-iotagent-lib + * + * fiware-iotagent-lib 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. + * + * fiware-iotagent-lib 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 fiware-iotagent-lib. + * If not, see http://www.gnu.org/licenses/. + * + * For those usages not covered by the GNU Affero General Public License + * please contact with::[contacto@tid.es] + * + * Modified by: Jason Fox - FIWARE Foundation + */ + +const iotAgentLib = require('../../../../lib/fiware-iotagent-lib'); +const utils = require('../../../tools/utils'); +const should = require('should'); +const request = require('request'); +const nock = require('nock'); +let contextBrokerMock; +const iotAgentConfig = { + logLevel: 'FATAL', + contextBroker: { + host: '192.168.1.1', + port: '1026', + ngsiVersion: 'ld', + jsonLdContext: 'http://context.json-ld' + }, + server: { + port: 4041 + }, + types: {}, + service: 'smartGondor', + subservice: 'gardens', + providerUrl: 'http://smartGondor.com' +}; + +describe('NGSI-LD - Subscription tests', function() { + beforeEach(function(done) { + const optionsProvision = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices', + method: 'POST', + json: utils.readExampleFile('./test/unit/examples/deviceProvisioningRequests/provisionMinimumDevice.json'), + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + } + }; + + nock.cleanAll(); + + iotAgentLib.activate(iotAgentConfig, function() { + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/' + 'contextRequests/createMinimumProvisionedDevice.json' + ) + ) + .reply(200); + + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post( + '/ngsi-ld/v1/subscriptions/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples' + '/subscriptionRequests/simpleSubscriptionRequest.json' + ) + ) + .reply(201, null, { Location: '/v2/subscriptions/51c0ac9ed714fb3b37d7d5a8' }); + + iotAgentLib.clearAll(function() { + request(optionsProvision, function(error, result, body) { + done(); + }); + }); + }); + }); + + afterEach(function(done) { + nock.cleanAll(); + iotAgentLib.setNotificationHandler(); + iotAgentLib.clearAll(function() { + iotAgentLib.deactivate(done); + }); + }); + + describe('When a client invokes the subscribe() function for device', function() { + it('should send the appropriate request to the Context Broker', function(done) { + iotAgentLib.getDevice('MicroLight1', 'smartGondor', '/gardens', function(error, device) { + iotAgentLib.subscribe(device, ['attr_name'], null, function(error) { + should.not.exist(error); + + contextBrokerMock.done(); + + done(); + }); + }); + }); + it('should store the subscription ID in the Device Registry', function(done) { + iotAgentLib.getDevice('MicroLight1', 'smartGondor', '/gardens', function(error, device) { + iotAgentLib.subscribe(device, ['attr_name'], null, function(error) { + iotAgentLib.getDevice('MicroLight1', 'smartGondor', '/gardens', function(error, device) { + should.not.exist(error); + should.exist(device); + should.exist(device.subscriptions); + device.subscriptions.length.should.equal(1); + device.subscriptions[0].id.should.equal('51c0ac9ed714fb3b37d7d5a8'); + device.subscriptions[0].triggers[0].should.equal('attr_name'); + done(); + }); + }); + }); + }); + }); + describe('When a client invokes the unsubscribe() function for an entity', function() { + beforeEach(function(done) { + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .delete('/v2/subscriptions/51c0ac9ed714fb3b37d7d5a8') + .reply(204); + + done(); + }); + it('should delete the subscription from the CB', function(done) { + iotAgentLib.getDevice('MicroLight1', 'smartGondor', '/gardens', function(error, device) { + iotAgentLib.subscribe(device, ['attr_name'], null, function(error) { + iotAgentLib.unsubscribe(device, '51c0ac9ed714fb3b37d7d5a8', function(error) { + iotAgentLib.getDevice('MicroLight1', 'smartGondor', '/gardens', function(error, device) { + contextBrokerMock.done(); + done(); + }); + }); + }); + }); + }); + it('should remove the id from the subscriptions array', function(done) { + iotAgentLib.getDevice('MicroLight1', 'smartGondor', '/gardens', function(error, device) { + iotAgentLib.subscribe(device, ['attr_name'], null, function(error) { + iotAgentLib.unsubscribe(device, '51c0ac9ed714fb3b37d7d5a8', function(error) { + iotAgentLib.getDevice('MicroLight1', 'smartGondor', '/gardens', function(error, device) { + should.not.exist(error); + should.exist(device); + should.exist(device.subscriptions); + device.subscriptions.length.should.equal(0); + done(); + }); + }); + }); + }); + }); + }); + describe('When a client removes a device from the registry', function() { + beforeEach(function(done) { + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .delete('/v2/subscriptions/51c0ac9ed714fb3b37d7d5a8') + .reply(204); + + done(); + }); + + it('should delete the subscription from the CB', function(done) { + iotAgentLib.getDevice('MicroLight1', 'smartGondor', '/gardens', function(error, device) { + iotAgentLib.subscribe(device, ['attr_name'], null, function(error) { + iotAgentLib.unregister(device.id, 'smartGondor', '/gardens', function(error) { + contextBrokerMock.done(); + done(); + }); + }); + }); + }); + }); + describe('When a new notification comes to the IoTAgent', function() { + beforeEach(function(done) { + iotAgentLib.getDevice('MicroLight1', 'smartGondor', '/gardens', function(error, device) { + iotAgentLib.subscribe(device, ['attr_name'], null, function(error) { + done(); + }); + }); + }); + + it('should invoke the user defined callback', function(done) { + const notificationOptions = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/notify', + method: 'POST', + json: utils.readExampleFile( + './test/unit/ngsi-ld/examples/subscriptionRequests' + '/simpleNotification.json' + ), + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + } + }; + + let executedHandler = false; + + function mockedHandler(device, notification, callback) { + executedHandler = true; + callback(); + } + + iotAgentLib.setNotificationHandler(mockedHandler); + + request(notificationOptions, function(error, response, body) { + should.not.exist(error); + executedHandler.should.equal(true); + + done(); + }); + }); + it('should invoke all the notification middlewares before the user defined callback', function(done) { + const notificationOptions = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/notify', + method: 'POST', + json: utils.readExampleFile( + './test/unit/ngsi-ld/examples/subscriptionRequests' + '/simpleNotification.json' + ), + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + } + }; + let executedMiddlewares = false; + let executedHandler = false; + let modifiedData = false; + + function mockedHandler(device, notification, callback) { + executedHandler = true; + modifiedData = notification.length === 2; + callback(); + } + + function mockedMiddleware(device, notification, callback) { + executedMiddlewares = true; + notification.push({ + name: 'middlewareAttribute', + type: 'middlewareType', + value: 'middlewareValue' + }); + + callback(null, device, notification); + } + + iotAgentLib.addNotificationMiddleware(mockedMiddleware); + iotAgentLib.setNotificationHandler(mockedHandler); + + request(notificationOptions, function(error, response, body) { + should.not.exist(error); + executedHandler.should.equal(true); + executedMiddlewares.should.equal(true); + modifiedData.should.equal(true); + done(); + }); + }); + it('should get the correspondent device information', function(done) { + const notificationOptions = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/notify', + method: 'POST', + json: utils.readExampleFile( + './test/unit/ngsi-ld/examples/subscriptionRequests/' + 'simpleNotification.json' + ), + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + } + }; + let rightFields = false; + + function mockedHandler(device, data, callback) { + if ( + device && + device.id === 'MicroLight1' && + device.name === 'FirstMicroLight' && + data && + data.length === 1 && + data[0].name === 'attr_name' + ) { + rightFields = true; + } + + callback(); + } + + iotAgentLib.setNotificationHandler(mockedHandler); + + request(notificationOptions, function(error, response, body) { + should.not.exist(error); + rightFields.should.equal(true); + + done(); + }); + }); + }); + describe('When a wrong notification arrives at the IOTA', function() { + it('should not call the handler', function(done) { + const notificationOptions = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/notify', + method: 'POST', + json: utils.readExampleFile( + './test/unit/ngsi-ld/examples/subscriptionRequests/' + 'errorNotification.json' + ), + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + } + }; + + let executedHandler = false; + + function mockedHandler(device, notification, callback) { + executedHandler = true; + callback(); + } + + iotAgentLib.setNotificationHandler(mockedHandler); + + request(notificationOptions, function(error, response, body) { + should.not.exist(error); + executedHandler.should.equal(false); + + done(); + }); + }); + }); +}); diff --git a/test/unit/ngsi-ld/plugins/alias-plugin_test.js b/test/unit/ngsi-ld/plugins/alias-plugin_test.js new file mode 100644 index 000000000..8fffb1245 --- /dev/null +++ b/test/unit/ngsi-ld/plugins/alias-plugin_test.js @@ -0,0 +1,470 @@ +/* + * Copyright 2020 Telefonica Investigación y Desarrollo, S.A.U + * + * This file is part of fiware-iotagent-lib + * + * fiware-iotagent-lib 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. + * + * fiware-iotagent-lib 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 fiware-iotagent-lib. + * If not, see http://www.gnu.org/licenses/. + * + * For those usages not covered by the GNU Affero General Public License + * please contact with::[contacto@tid.es] + * + * Modified by: Jason Fox - FIWARE Foundation + */ + +/* jshint camelcase: false */ + +const iotAgentLib = require('../../../../lib/fiware-iotagent-lib'); +const utils = require('../../../tools/utils'); +const should = require('should'); +const logger = require('logops'); +const nock = require('nock'); +let contextBrokerMock; +const iotAgentConfig = { + autocast: true, + contextBroker: { + host: '192.168.1.1', + port: '1026', + ngsiVersion: 'ld', + jsonLdContext: 'http://context.json-ld' + }, + server: { + port: 4041 + }, + types: { + Light: { + commands: [], + type: 'Light', + lazy: [ + { + object_id: 't', + name: 'temperature', + type: 'Number', + metadata: { unitCode: { type: 'Property', value: 'CEL' } } + } + ], + active: [ + { + object_id: 'p', + name: 'pressure', + type: 'Number', + metadata: { unitCode: { type: 'Property', value: 'Hgmm' } } + }, + { + object_id: 'l', + name: 'luminance', + type: 'Number', + metadata: { unitCode: { type: 'Property', value: 'CAL' } } + }, + { + object_id: 'ut', + name: 'unix_timestamp', + type: 'Number' + }, + { + object_id: 'ap', + name: 'active_power', + type: 'Number' + }, + { + object_id: 'ap', + name: 'active_power', + type: 'Number' + }, + { + object_id: 's', + name: 'status', + type: 'Boolean' + }, + { + object_id: 'al', + name: 'keep_alive', + type: 'None' + }, + { + object_id: 'ta', + name: 'tags', + type: 'Array' + }, + { + object_id: 'c', + name: 'configuration', + type: 'Object' + } + ] + } + }, + service: 'smartGondor', + subservice: 'gardens', + providerUrl: 'http://smartGondor.com' +}; + +describe('NGSI-LD - Attribute alias plugin', function() { + beforeEach(function(done) { + logger.setLevel('FATAL'); + + iotAgentLib.activate(iotAgentConfig, function() { + iotAgentLib.clearAll(function() { + iotAgentLib.addUpdateMiddleware(iotAgentLib.dataPlugins.attributeAlias.update); + iotAgentLib.addQueryMiddleware(iotAgentLib.dataPlugins.attributeAlias.query); + done(); + }); + }); + }); + + afterEach(function(done) { + iotAgentLib.clearAll(function() { + iotAgentLib.deactivate(done); + }); + }); + describe('When an update comes for attributes with aliases', function() { + const values = [ + { + name: 't', + type: 'centigrades', + value: '52' + }, + { + name: 'p', + type: 'Hgmm', + value: '20071103T131805' + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .patch( + '/ngsi-ld/v1/entities/light1/attrs', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/updateContextAliasPlugin1.json') + ) + .query({ type: 'Light' }) + .reply(204); + }); + + it( + 'should rename the attributes as expected by the alias mappings' + 'and cast values to JSON native types', + function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + } + ); + }); + describe('When an update comes for attributes with aliases and a different type', function() { + const values = [ + { + name: 'l', + type: 'lums', + value: '9' + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .patch( + '/ngsi-ld/v1/entities/light1/attrs', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/updateContextAliasPlugin2.json') + ) + .query({ type: 'Light' }) + .reply(204); + }); + + it( + 'should rename the attributes as expected by the alias mappings' + 'and cast values to JSON native types', + function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + } + ); + }); + describe('When an update comes for attributes with aliases and integer type', function() { + const values = [ + { + name: 'ut', + type: 'Number', + value: '99823423' + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .patch( + '/ngsi-ld/v1/entities/light1/attrs', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/updateContextAliasPlugin3.json') + ) + .query({ type: 'Light' }) + .reply(204); + }); + + it('should rename the attributes as expected by the mappings', function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an update comes for attributes with aliases and integer type.', function() { + const values = [ + { + name: 'ut', + type: 'Number', + value: '99823423' + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .patch( + '/ngsi-ld/v1/entities/light1/attrs', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/updateContextAliasPlugin3.json') + ) + .query({ type: 'Light' }) + .reply(204); + }); + + it( + 'should rename the attributes as expected by the alias mappings' + 'and cast values to JSON native types', + function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + } + ); + }); + + describe('When an update comes for attributes with aliases and float type', function() { + const values = [ + { + name: 'ap', + type: 'Number', + value: '0.45' + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .patch( + '/ngsi-ld/v1/entities/light1/attrs', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/updateContextAliasPlugin4.json') + ) + .query({ type: 'Light' }) + .reply(204); + }); + + it( + 'should rename the attributes as expected by the alias mappings' + 'and cast values to JSON native types', + function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + } + ); + }); + + describe('When an update comes for attributes with aliases and boolean type', function() { + const values = [ + { + name: 's', + type: 'Boolean', + value: false + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .patch( + '/ngsi-ld/v1/entities/light1/attrs', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/updateContextAliasPlugin5.json') + ) + .query({ type: 'Light' }) + .reply(204); + }); + + it( + 'should rename the attributes as expected by the alias mappings' + 'and cast values to JSON native types', + function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + } + ); + }); + + describe('When an update comes for attributes with aliases and None type', function() { + const values = [ + { + name: 'al', + type: 'None', + value: 'null' + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .patch( + '/ngsi-ld/v1/entities/light1/attrs', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/updateContextAliasPlugin6.json') + ) + .query({ type: 'Light' }) + .reply(204); + }); + + it( + 'should rename the attributes as expected by the alias mappings' + 'and cast values to JSON native types', + function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + } + ); + }); + + describe('When an update comes for attributes with aliases and Array type', function() { + const values = [ + { + name: 'ta', + type: 'Array', + value: '["iot","device"]' + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .patch( + '/ngsi-ld/v1/entities/light1/attrs', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/updateContextAliasPlugin7.json') + ) + .query({ type: 'Light' }) + .reply(204); + }); + + it( + 'should rename the attributes as expected by the alias mappings' + 'and cast values to JSON native types', + function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + } + ); + }); + + describe('When an update comes for attributes with aliases and Object type', function() { + const values = [ + { + name: 'c', + type: 'Object', + value: '{"firmware": {"version": "1.1.0","hash": "cf23df2207d99a74fbe169e3eba035e633b65d94"}}' + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .patch( + '/ngsi-ld/v1/entities/light1/attrs', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/updateContextAliasPlugin8.json') + ) + .query({ type: 'Light' }) + .reply(204); + }); + + it( + 'should rename the attributes as expected by the alias mappings' + 'and cast values to JSON native types', + function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + } + ); + }); + + describe('When an update comes for attributes with aliases and Object type, but value is String', function() { + const values = [ + { + name: 'c', + type: 'Object', + value: 'string_value' + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .patch( + '/ngsi-ld/v1/entities/light1/attrs', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/updateContextAliasPlugin9.json') + ) + .query({ type: 'Light' }) + .reply(204); + }); + + it( + 'should rename the attributes as expected by the alias mappings' + 'and cast values to JSON native types', + function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + } + ); + }); +}); diff --git a/test/unit/ngsi-ld/plugins/bidirectional-plugin_test.js b/test/unit/ngsi-ld/plugins/bidirectional-plugin_test.js new file mode 100644 index 000000000..1451b8402 --- /dev/null +++ b/test/unit/ngsi-ld/plugins/bidirectional-plugin_test.js @@ -0,0 +1,472 @@ +/* + * Copyright 2020 Telefonica Investigación y Desarrollo, S.A.U + * + * This file is part of fiware-iotagent-lib + * + * fiware-iotagent-lib 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. + * + * fiware-iotagent-lib 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 fiware-iotagent-lib. + * If not, see http://www.gnu.org/licenses/. + * + * For those usages not covered by the GNU Affero General Public License + * please contact with::daniel.moranjimenez@telefonica.com + * + * Modified by: Jason Fox - FIWARE Foundation + */ + +const iotAgentLib = require('../../../../lib/fiware-iotagent-lib'); +const utils = require('../../../tools/utils'); +const should = require('should'); +const logger = require('logops'); +const nock = require('nock'); +const request = require('request'); +let contextBrokerMock; +const iotAgentConfig = { + contextBroker: { + host: '192.168.1.1', + port: '1026', + ngsiVersion: 'ld', + jsonLdContext: 'http://context.json-ld' + }, + server: { + port: 4041 + }, + types: {}, + service: 'smartGondor', + subservice: 'gardens', + providerUrl: 'http://smartGondor.com' +}; + +describe('NGSI-LD - Bidirectional data plugin', function() { + const options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices', + method: 'POST', + json: utils.readExampleFile( + './test/unit/examples/deviceProvisioningRequests/provisionBidirectionalDevice.json' + ), + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + } + }; + + beforeEach(function(done) { + iotAgentLib.activate(iotAgentConfig, function() { + iotAgentLib.clearAll(function() { + iotAgentLib.addDeviceProvisionMiddleware(iotAgentLib.dataPlugins.bidirectionalData.deviceProvision); + iotAgentLib.addConfigurationProvisionMiddleware( + iotAgentLib.dataPlugins.bidirectionalData.groupProvision + ); + iotAgentLib.addNotificationMiddleware(iotAgentLib.dataPlugins.bidirectionalData.notification); + done(); + }); + }); + }); + + afterEach(function(done) { + iotAgentLib.clearAll(function() { + iotAgentLib.deactivate(done); + }); + }); + + describe('When a new provisioning request arrives to the IoTA with bidirectionality', function() { + beforeEach(function() { + logger.setLevel('FATAL'); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .post( + '/ngsi-ld/v1/subscriptions/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/subscriptionRequests/bidirectionalSubscriptionRequest.json' + ) + ) + .reply(201, null, { Location: '/v2/subscriptions/51c0ac9ed714fb3b37d7d5a8' }); + + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/createBidirectionalDevice.json') + ) + .reply(200); + }); + + it('should subscribe to the modification of the combined attribute with all the variables', function(done) { + request(options, function(error, response, body) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When a device with bidirectionality subscriptions is removed', function() { + const deleteRequest = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices/Light1', + method: 'DELETE', + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + } + }; + + beforeEach(function() { + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post( + '/ngsi-ld/v1/subscriptions/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/subscriptionRequests/bidirectionalSubscriptionRequest.json' + ) + ) + .reply(201, null, { Location: '/v2/subscriptions/51c0ac9ed714fb3b37d7d5a8' }); + + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/createBidirectionalDevice.json') + ) + .reply(200); + + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .delete('/v2/subscriptions/51c0ac9ed714fb3b37d7d5a8') + .reply(204); + }); + + it('should remove its subscriptions from the Context Broker', function(done) { + request(options, function(error, response, body) { + request(deleteRequest, function(error, response, body) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + }); + + describe('When a notification arrives for a bidirectional attribute', function() { + const notificationOptions = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/notify', + method: 'POST', + json: utils.readExampleFile( + './test/unit/ngsi-ld/examples/subscriptionRequests/' + 'bidirectionalNotification.json' + ), + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + } + }; + let executedHandler = false; + + beforeEach(function() { + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post( + '/ngsi-ld/v1/subscriptions/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/subscriptionRequests/bidirectionalSubscriptionRequest.json' + ) + ) + .reply(201, null, { Location: '/v2/subscriptions/51c0ac9ed714fb3b37d7d5a8' }); + + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/createBidirectionalDevice.json') + ) + .reply(200); + }); + + afterEach(function() { + iotAgentLib.setNotificationHandler(); + }); + + it('should execute the original handler', function(done) { + function mockedHandler(device, notification, callback) { + executedHandler = true; + callback(); + } + + iotAgentLib.setNotificationHandler(mockedHandler); + + request(options, function(error, response, body) { + request(notificationOptions, function(error, response, body) { + executedHandler.should.equal(true); + contextBrokerMock.done(); + done(); + }); + }); + }); + + it('should return a 200 OK', function(done) { + function mockedHandler(device, notification, callback) { + executedHandler = true; + callback(); + } + + iotAgentLib.setNotificationHandler(mockedHandler); + + request(options, function(error, response, body) { + request(notificationOptions, function(error, response, body) { + response.statusCode.should.equal(200); + contextBrokerMock.done(); + done(); + }); + }); + }); + + it('should return the transformed values', function(done) { + let transformedHandler = false; + + function mockedHandler(device, values, callback) { + let latitudeFound = false; + let longitudeFound = false; + + for (let i = 0; i < values.length; i++) { + if (values[i].name === 'latitude' && values[i].type === 'string' && values[i].value === '-9.6') { + latitudeFound = true; + } + + if (values[i].name === 'longitude' && values[i].type === 'string' && values[i].value === '12.4') { + longitudeFound = true; + } + } + + transformedHandler = values.length >= 2 && longitudeFound && latitudeFound; + callback(); + } + + iotAgentLib.setNotificationHandler(mockedHandler); + + request(options, function(error, response, body) { + request(notificationOptions, function(error, response, body) { + contextBrokerMock.done(); + transformedHandler.should.equal(true); + done(); + }); + }); + }); + }); + + describe('When a new Group provisioning request arrives with bidirectional attributes', function() { + const provisionGroup = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/services', + method: 'POST', + json: utils.readExampleFile('./test/unit/examples/groupProvisioningRequests/bidirectionalGroup.json'), + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + } + }; + const provisionDevice = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices', + method: 'POST', + json: utils.readExampleFile( + './test/unit/examples/deviceProvisioningRequests/provisionDeviceBidirectionalGroup.json' + ), + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + } + }; + + beforeEach(function() { + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post( + '/ngsi-ld/v1/subscriptions/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/subscriptionRequests/bidirectionalSubscriptionRequest.json' + ) + ) + .reply(201, null, { Location: '/v2/subscriptions/51c0ac9ed714fb3b37d7d5a8' }); + + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/createBidirectionalDevice.json') + ) + .reply(200); + }); + it('should subscribe to the modification of the combined attribute with all the variables', function(done) { + request(provisionGroup, function(error, response, body) { + request(provisionDevice, function(error, response, body) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + }); + + describe('When a notification arrives for a bidirectional attribute in a Configuration Group', function() { + const provisionGroup = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/services', + method: 'POST', + json: utils.readExampleFile('./test/unit/examples/groupProvisioningRequests/bidirectionalGroup.json'), + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + } + }; + const notificationOptions = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/notify', + method: 'POST', + json: utils.readExampleFile( + './test/unit/ngsi-ld/examples/subscriptionRequests/' + 'bidirectionalNotification.json' + ), + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + } + }; + const provisionDevice = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices', + method: 'POST', + json: utils.readExampleFile( + './test/unit/examples/deviceProvisioningRequests/provisionDeviceBidirectionalGroup.json' + ), + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + } + }; + + beforeEach(function() { + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post( + '/ngsi-ld/v1/subscriptions/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/subscriptionRequests/bidirectionalSubscriptionRequest.json' + ) + ) + .reply(201, null, { Location: '/v2/subscriptions/51c0ac9ed714fb3b37d7d5a8' }); + + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/createBidirectionalDevice.json') + ) + .reply(200); + }); + + afterEach(function() { + iotAgentLib.setNotificationHandler(); + }); + + it('should return the transformed values', function(done) { + let transformedHandler = false; + + function mockedHandler(device, values, callback) { + let latitudeFound = false; + let longitudeFound = false; + + for (let i = 0; i < values.length; i++) { + if (values[i].name === 'latitude' && values[i].type === 'string' && values[i].value === '-9.6') { + latitudeFound = true; + } + + if (values[i].name === 'longitude' && values[i].type === 'string' && values[i].value === '12.4') { + longitudeFound = true; + } + } + + transformedHandler = values.length >= 2 && longitudeFound && latitudeFound; + callback(); + } + + iotAgentLib.setNotificationHandler(mockedHandler); + + request(provisionGroup, function(error, response, body) { + request(provisionDevice, function(error, response, body) { + request(notificationOptions, function(error, response, body) { + transformedHandler.should.equal(true); + done(); + }); + }); + }); + }); + }); +}); + +describe('NGSI-LD - Bidirectional data plugin and CB is defined using environment variables', function() { + const options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices', + method: 'POST', + json: utils.readExampleFile( + './test/unit/examples/deviceProvisioningRequests/provisionBidirectionalDevice.json' + ), + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + } + }; + + beforeEach(function(done) { + logger.setLevel('FATAL'); + process.env.IOTA_CB_HOST = 'cbhost'; + iotAgentLib.activate(iotAgentConfig, function() { + iotAgentLib.clearAll(function() { + iotAgentLib.addDeviceProvisionMiddleware(iotAgentLib.dataPlugins.bidirectionalData.deviceProvision); + iotAgentLib.addConfigurationProvisionMiddleware( + iotAgentLib.dataPlugins.bidirectionalData.groupProvision + ); + iotAgentLib.addNotificationMiddleware(iotAgentLib.dataPlugins.bidirectionalData.notification); + done(); + }); + }); + }); + + afterEach(function(done) { + process.env.IOTA_CB_HOST = ''; + iotAgentLib.clearAll(function() { + iotAgentLib.deactivate(done); + }); + }); + + describe('When a new provisioning request arrives to the IoTA with bidirectionality', function() { + beforeEach(function() { + contextBrokerMock = nock('http://cbhost:1026') + .matchHeader('fiware-service', 'smartGondor') + .post( + '/ngsi-ld/v1/subscriptions/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/subscriptionRequests/bidirectionalSubscriptionRequest.json' + ) + ) + .reply(201, null, { Location: '/v2/subscriptions/51c0ac9ed714fb3b37d7d5a8' }); + + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/createBidirectionalDevice.json') + ) + .reply(200); + }); + + it('should subscribe to the modification of the combined attribute with all the variables', function(done) { + request(options, function(error, response, body) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); +}); diff --git a/test/unit/ngsi-ld/plugins/compress-timestamp-plugin_test.js b/test/unit/ngsi-ld/plugins/compress-timestamp-plugin_test.js new file mode 100644 index 000000000..6d51d22e7 --- /dev/null +++ b/test/unit/ngsi-ld/plugins/compress-timestamp-plugin_test.js @@ -0,0 +1,248 @@ +/* + * Copyright 2020 Telefonica Investigación y Desarrollo, S.A.U + * + * This file is part of fiware-iotagent-lib + * + * fiware-iotagent-lib 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. + * + * fiware-iotagent-lib 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 fiware-iotagent-lib. + * If not, see http://www.gnu.org/licenses/. + * + * For those usages not covered by the GNU Affero General Public License + * please contact with::daniel.moranjimenez@telefonica.com + * + * Modified by: Jason Fox - FIWARE Foundation + */ + +const iotAgentLib = require('../../../../lib/fiware-iotagent-lib'); +const utils = require('../../../tools/utils'); +const should = require('should'); +const logger = require('logops'); +const nock = require('nock'); +let contextBrokerMock; +const iotAgentConfig = { + contextBroker: { + host: '192.168.1.1', + port: '1026', + ngsiVersion: 'ld', + jsonLdContext: 'http://context.json-ld' + }, + server: { + port: 4041 + }, + types: { + Light: { + commands: [], + type: 'Light', + lazy: [ + { + name: 'temperature', + type: 'centigrades' + } + ], + active: [ + { + name: 'pressure', + type: 'Hgmm' + } + ] + }, + BrokenLight: { + commands: [], + lazy: [ + { + name: 'temperature', + type: 'centigrades' + } + ], + active: [ + { + name: 'pressure', + type: 'Hgmm' + } + ] + }, + Termometer: { + type: 'Termometer', + commands: [], + lazy: [ + { + name: 'temp', + type: 'kelvin' + } + ], + active: [] + }, + Humidity: { + type: 'Humidity', + cbHost: 'http://192.168.1.1:3024', + commands: [], + lazy: [], + active: [ + { + name: 'humidity', + type: 'percentage' + } + ] + }, + Motion: { + type: 'Motion', + commands: [], + lazy: [], + staticAttributes: [ + { + name: 'location', + type: 'Vector', + value: '(123,523)' + } + ], + active: [ + { + name: 'humidity', + type: 'percentage' + } + ] + } + }, + service: 'smartGondor', + subservice: 'gardens', + providerUrl: 'http://smartGondor.com' +}; + +describe('NGSI-LD - Timestamp compression plugin', function() { + beforeEach(function(done) { + logger.setLevel('FATAL'); + iotAgentLib.activate(iotAgentConfig, function() { + iotAgentLib.clearAll(function() { + iotAgentLib.addUpdateMiddleware(iotAgentLib.dataPlugins.compressTimestamp.updateNgsi2); + iotAgentLib.addQueryMiddleware(iotAgentLib.dataPlugins.compressTimestamp.queryNgsi2); + done(); + }); + }); + }); + + afterEach(function(done) { + iotAgentLib.clearAll(function() { + iotAgentLib.deactivate(done); + }); + }); + describe('When an update comes with a timestamp through the plugin', function() { + const values = [ + { + name: 'state', + type: 'Boolean', + value: 'true' + }, + { + name: 'TheTargetValue', + type: 'DateTime', + value: '20071103T131805' + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .patch( + '/ngsi-ld/v1/entities/light1/attrs', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextCompressTimestamp1.json' + ) + ) + .query({ type: 'Light' }) + .reply(204); + }); + + it('should return an entity with all its timestamps expanded to have separators', function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an update comes with a timestamp through the plugin with metadata.', function() { + const values = [ + { + name: 'state', + type: 'Boolean', + value: true, + metadata: { + TimeInstant: { + type: 'DateTime', + value: '20071103T131805' + } + } + }, + { + name: 'TheTargetValue', + type: 'DateTime', + value: '20071103T131805' + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .patch( + '/ngsi-ld/v1/entities/light1/attrs', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextCompressTimestamp2.json' + ) + ) + .query({ type: 'Light' }) + .reply(204); + }); + + it('should return an entity with all its timestamps expanded to have separators', function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When a query comes for a timestamp through the plugin', function() { + const values = ['state', 'TheTargetValue']; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .get('/ngsi-ld/v1/entities/light1/attrs?attrs=state,TheTargetValue&type=Light') + .reply( + 200, + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextResponses/queryContextCompressTimestamp1Success.json' + ) + ); + }); + + xit('should return an entity with all its timestamps without separators (basic format)', function(done) { + iotAgentLib.query('light1', 'Light', '', values, function(error, response) { + should.not.exist(error); + should.exist(response); + should.exist(response.TheTargetValue); + should.exist(response.TheTargetValue.value); + response.TheTargetValue.value.should.equal('20071103T131805'); + done(); + }); + }); + }); +}); diff --git a/test/unit/ngsi-ld/plugins/event-plugin_test.js b/test/unit/ngsi-ld/plugins/event-plugin_test.js new file mode 100644 index 000000000..637ca6d5a --- /dev/null +++ b/test/unit/ngsi-ld/plugins/event-plugin_test.js @@ -0,0 +1,117 @@ +/* + * Copyright 2020 Telefonica Investigación y Desarrollo, S.A.U + * + * This file is part of fiware-iotagent-lib + * + * fiware-iotagent-lib 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. + * + * fiware-iotagent-lib 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 fiware-iotagent-lib. + * If not, see http://www.gnu.org/licenses/. + * + * For those usages not covered by the GNU Affero General Public License + * please contact with::daniel.moranjimenez@telefonica.com + * + * Modified by: Jason Fox - FIWARE Foundation + */ + +const iotAgentLib = require('../../../../lib/fiware-iotagent-lib'); +const should = require('should'); +const logger = require('logops'); +const nock = require('nock'); +let contextBrokerMock; +const iotAgentConfig = { + contextBroker: { + host: '192.168.1.1', + port: '1026', + ngsiVersion: 'ld', + jsonLdContext: 'http://context.json-ld' + }, + server: { + port: 4041 + }, + types: { + Light: { + commands: [], + type: 'Light', + lazy: [ + { + name: 'temperature', + type: 'centigrades' + } + ], + active: [ + { + name: 'pressure', + type: 'Hgmm' + } + ] + } + }, + service: 'smartGondor', + subservice: 'gardens', + providerUrl: 'http://smartGondor.com' +}; + +describe('NGSI-LD - Event plugin', function() { + beforeEach(function(done) { + logger.setLevel('FATAL'); + + iotAgentLib.activate(iotAgentConfig, function() { + iotAgentLib.clearAll(function() { + iotAgentLib.addUpdateMiddleware(iotAgentLib.dataPlugins.addEvents.update); + iotAgentLib.addQueryMiddleware(iotAgentLib.dataPlugins.addEvents.query); + done(); + }); + }); + }); + + afterEach(function(done) { + iotAgentLib.clearAll(function() { + iotAgentLib.deactivate(done); + }); + }); + describe('When an update comes with an event to the plugin', function() { + const values = [ + { + name: 'state', + type: 'Boolean', + value: 'true' + }, + { + name: 'activation', + type: 'Event', + value: '1' + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .patch('/ngsi-ld/v1/entities/light1/attrs', function(body) { + const dateRegex = /\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d.\d{3}Z/; + return body.activation.value['@value'].match(dateRegex); + }) + .query({ type: 'Light' }) + .reply(204); + }); + + it('should return an entity with all its timestamps expanded to have separators', function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); +}); diff --git a/test/unit/ngsi-ld/plugins/multientity-plugin_test.js b/test/unit/ngsi-ld/plugins/multientity-plugin_test.js new file mode 100644 index 000000000..de1767877 --- /dev/null +++ b/test/unit/ngsi-ld/plugins/multientity-plugin_test.js @@ -0,0 +1,785 @@ +/* + * Copyright 2020 Telefonica Investigación y Desarrollo, S.A.U + * + * This file is part of fiware-iotagent-lib + * + * fiware-iotagent-lib 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. + * + * fiware-iotagent-lib 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 fiware-iotagent-lib. + * If not, see http://www.gnu.org/licenses/. + * + * For those usages not covered by the GNU Affero General Public License + * please contact with::[contacto@tid.es] + * + * Modified by: Jason Fox - FIWARE Foundation + */ + +/* jshint camelcase: false */ + +const iotAgentLib = require('../../../../lib/fiware-iotagent-lib'); +const utils = require('../../../tools/utils'); +const should = require('should'); +const logger = require('logops'); +const nock = require('nock'); +const moment = require('moment'); +const timekeeper = require('timekeeper'); +let contextBrokerMock; +const iotAgentConfig = { + contextBroker: { + host: '192.168.1.1', + port: '1026', + ngsiVersion: 'ld', + jsonLdContext: 'http://context.json-ld' + }, + server: { + port: 4041 + }, + types: { + WeatherStation: { + commands: [], + type: 'WeatherStation', + lazy: [], + active: [ + { + object_id: 'p', + name: 'pressure', + type: 'Hgmm' + }, + { + object_id: 'h', + name: 'humidity', + type: 'Percentage', + entity_name: 'Higro2000', + entity_type: 'Higrometer' + } + ] + }, + WeatherStation2: { + commands: [], + type: 'WeatherStation', + lazy: [], + active: [ + { + object_id: 'p', + name: 'pressure', + type: 'Hgmm' + }, + { + object_id: 'h', + name: 'humidity', + type: 'Percentage', + entity_name: 'Higro2000' + } + ] + }, + WeatherStation3: { + commands: [], + type: 'WeatherStation', + lazy: [], + active: [ + { + object_id: 'p', + name: 'pressure', + type: 'Hgmm' + }, + { + object_id: 'h', + name: 'humidity', + type: 'Percentage', + entity_name: 'Station Number ${@sn * 10}' + } + ] + }, + WeatherStation5: { + commands: [], + type: 'WeatherStation', + lazy: [], + active: [ + { + object_id: 'p', + name: 'pressure', + type: 'Hgmm' + }, + { + object_id: 'h', + name: 'pressure', + type: 'Hgmm', + entity_name: 'Higro2000', + entity_type: 'Higrometer' + } + ] + }, + WeatherStation6: { + commands: [], + type: 'WeatherStation', + lazy: [], + active: [ + { + object_id: 'p', + name: 'pressure', + type: 'Hgmm', + entity_name: 'Higro2002', + entity_type: 'Higrometer' + }, + { + object_id: 'h', + name: 'pressure', + type: 'Hgmm', + entity_name: 'Higro2000', + entity_type: 'Higrometer' + } + ] + }, + WeatherStation7: { + commands: [], + type: 'WeatherStation', + lazy: [], + active: [ + { + object_id: 'p', + name: 'pressure', + type: 'Hgmm', + metadata: { + unitCode: { type: 'Text', value: 'Hgmm' } + }, + entity_name: 'Higro2002', + entity_type: 'Higrometer' + }, + { + object_id: 'h', + name: 'pressure', + type: 'Hgmm', + entity_name: 'Higro2000', + entity_type: 'Higrometer' + } + ] + }, + Sensor001: { + commands: [], + type: 'Sensor', + lazy: [], + active: [ + { + type: 'number', + name: 'vol', + object_id: 'cont1', + entity_name: 'SO1', + entity_type: 'WM' + }, + { + type: 'number', + name: 'vol', + object_id: 'cont2', + entity_name: 'SO2', + entity_type: 'WM' + }, + { + type: 'number', + name: 'vol', + object_id: 'cont3', + entity_name: 'SO3', + entity_type: 'WM' + }, + { + type: 'number', + name: 'vol', + object_id: 'cont4', + entity_name: 'SO4', + entity_type: 'WM' + }, + { + type: 'number', + name: 'vol', + object_id: 'cont5', + entity_name: 'SO5', + entity_type: 'WM' + } + ] + }, + SensorCommand: { + commands: [ + { + name: 'PING', + type: 'command' + } + ], + type: 'SensorCommand', + lazy: [] + } + }, + service: 'smartGondor', + subservice: 'gardens', + providerUrl: 'http://smartGondor.com' +}; + +xdescribe('NGSI-LD - Multi-entity plugin', function() { + beforeEach(function(done) { + logger.setLevel('FATAL'); + + iotAgentLib.activate(iotAgentConfig, function() { + iotAgentLib.clearAll(function() { + iotAgentLib.addUpdateMiddleware(iotAgentLib.dataPlugins.attributeAlias.update); + iotAgentLib.addQueryMiddleware(iotAgentLib.dataPlugins.attributeAlias.query); + iotAgentLib.addUpdateMiddleware(iotAgentLib.dataPlugins.multiEntity.update); + done(); + }); + }); + }); + + afterEach(function(done) { + iotAgentLib.clearAll(function() { + iotAgentLib.deactivate(done); + }); + }); + + describe('When an update comes for a multientity measurement', function() { + const values = [ + { + name: 'p', + type: 'centigrades', + value: '52' + }, + { + name: 'h', + type: 'Percentage', + value: '12' + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post( + '/v2/op/update', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityPlugin1.json' + ) + ) + .reply(204); + }); + + it('should send two context elements, one for each entity', function(done) { + iotAgentLib.update('ws4', 'WeatherStation', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an update comes for a multientity measurement with same attribute name', function() { + const values = [ + { + name: 'h', + type: 'Hgmm', + value: '16' + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post( + '/v2/op/update', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityPlugin4.json' + ) + ) + .reply(204); + }); + + it('should send context elements', function(done) { + iotAgentLib.update('ws5', 'WeatherStation5', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an update comes for a multientity multi measurement with same attribute name', function() { + const values = [ + { + name: 'h', + type: 'Hgmm', + value: '16' + }, + { + name: 'p', + type: 'Hgmm', + value: '17' + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post( + '/v2/op/update', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityPlugin5.json' + ) + ) + .reply(204); + }); + + it('should send context elements', function(done) { + iotAgentLib.update('ws6', 'WeatherStation6', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + /* jshint maxlen: 200 */ + describe('When an update comes for a multientity multi measurement with metadata and the same attribute name', function() { + const values = [ + { + name: 'h', + type: 'Hgmm', + value: '16' + }, + { + name: 'p', + type: 'Hgmm', + value: '17' + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post( + '/v2/op/update', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityPlugin8.json' + ) + ) + .reply(204); + }); + + it('should send context elements', function(done) { + iotAgentLib.update('ws7', 'WeatherStation7', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an update comes for a multientity defined with an expression', function() { + const values = [ + { + name: 'p', + type: 'centigrades', + value: '52' + }, + { + name: 'h', + type: 'Percentage', + value: '12' + }, + { + name: 'sn', + type: 'Number', + value: '5' + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post( + '/v2/op/update', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityPlugin3.json' + ) + ) + .reply(204); + }); + + it('should send the update value to the resulting value of the expression', function(done) { + iotAgentLib.update('ws4', 'WeatherStation3', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When an update comes for a multientity measurement without type for one entity', function() { + const values = [ + { + name: 'p', + type: 'centigrades', + value: '52' + }, + { + name: 'h', + type: 'Percentage', + value: '12' + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post( + '/v2/op/update', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityPlugin2.json' + ) + ) + .reply(204); + }); + + it('should use the device type as a default value', function(done) { + iotAgentLib.update('ws4', 'WeatherStation2', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe( + 'When an update comes for a multientity measurement and there are attributes with' + + ' the same name but different alias and mapped to different CB entities', + function() { + const values = [ + { + name: 'cont1', + type: 'number', + value: '38' + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post( + '/v2/op/update', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityPlugin6.json' + ) + ) + .reply(204); + }); + + it('should update only the appropriate CB entity', function(done) { + iotAgentLib.update('Sensor', 'Sensor001', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + } + ); + + describe( + 'When an update comes for a multientity multi measurement and there are attributes with' + + ' the same name but different alias and mapped to different CB entities', + function() { + const values = [ + { + name: 'cont1', + type: 'number', + value: '38' + }, + { + name: 'cont2', + type: 'number', + value: '39' + }, + { + name: 'cont3', + type: 'number', + value: '40' + }, + { + name: 'cont5', + type: 'number', + value: '42' + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post( + '/v2/op/update', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityPlugin7.json' + ) + ) + .reply(204); + }); + + it('should update only the appropriate CB entity', function(done) { + iotAgentLib.update('Sensor', 'Sensor001', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + } + ); +}); + +xdescribe('NGSI-LD - Multi-entity plugin is executed before timestamp process plugin', function() { + beforeEach(function(done) { + logger.setLevel('FATAL'); + + iotAgentConfig.timestamp = true; + iotAgentLib.activate(iotAgentConfig, function() { + iotAgentLib.clearAll(function() { + iotAgentLib.addUpdateMiddleware(iotAgentLib.dataPlugins.attributeAlias.update); + iotAgentLib.addQueryMiddleware(iotAgentLib.dataPlugins.attributeAlias.query); + iotAgentLib.addUpdateMiddleware(iotAgentLib.dataPlugins.multiEntity.update); + iotAgentLib.addUpdateMiddleware(iotAgentLib.dataPlugins.timestampProcess.update); + done(); + }); + }); + }); + + afterEach(function(done) { + iotAgentLib.clearAll(function() { + iotAgentLib.deactivate(done); + }); + }); + + describe('When an update comes for a multientity measurement and timestamp is enabled in config file', function() { + const values = [ + { + name: 'p', + type: 'centigrades', + value: '52' + }, + { + name: 'h', + type: 'Percentage', + value: '12' + }, + { + name: 'TimeInstant', + type: 'DateTime', + value: '2016-05-30T16:25:22.304Z' + } + ]; + + const singleValue = [ + { + name: 'h', + type: 'Percentage', + value: '12' + } + ]; + + beforeEach(function() { + nock.cleanAll(); + }); + + it('should send two context elements, one for each entity', function(done) { + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v2/op/update', function(body) { + const expectedBody = utils.readExampleFile( + './test/unit/ngsi-ld/examples' + + '/contextRequests/updateContextMultientityTimestampPlugin1.json' + ); + // Note that TimeInstant fields are not included in the json used by this mock as they are dynamic + // fields. The following code just checks that TimeInstant fields are present. + if (!body.entities[1].TimeInstant || !body.entities[1].humidity.metadata.TimeInstant) { + return false; + } + + const timeInstantEntity = body.entities[1].TimeInstant; + const timeInstantAtt = body.entities[1].humidity.metadata.TimeInstant; + if ( + moment(timeInstantEntity, 'YYYY-MM-DDTHH:mm:ss.SSSZ').isValid && + moment(timeInstantAtt, 'YYYY-MM-DDTHH:mm:ss.SSSZ').isValid + ) { + delete body.entities[1].TimeInstant; + delete body.entities[1].humidity.metadata.TimeInstant; + + delete expectedBody.entities[1].TimeInstant; + delete expectedBody.entities[1].humidity.metadata.TimeInstant; + return JSON.stringify(body) === JSON.stringify(expectedBody); + } + return false; + }) + .reply(204); + + iotAgentLib.update('ws4', 'WeatherStation', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + + it('should send two context elements, one for each entity', function(done) { + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/v2/op/update', function(body) { + const expectedBody = utils.readExampleFile( + './test/unit/ngsi-ld/examples' + + '/contextRequests/updateContextMultientityTimestampPlugin2.json' + ); + // Note that TimeInstant fields are not included in the json used by this mock as they are dynamic + // fields. The following code just checks that TimeInstant fields are present. + if (!body.entities[1].TimeInstant || !body.entities[1].humidity.metadata.TimeInstant) { + return false; + } + + const timeInstantEntity2 = body.entities[1].TimeInstant; + const timeInstantAtt = body.entities[1].humidity.metadata.TimeInstant; + if ( + moment(timeInstantEntity2, 'YYYY-MM-DDTHH:mm:ss.SSSZ').isValid && + moment(timeInstantAtt, 'YYYY-MM-DDTHH:mm:ss.SSSZ').isValid + ) { + delete body.entities[1].TimeInstant; + delete body.entities[1].humidity.metadata.TimeInstant; + + delete expectedBody.entities[1].TimeInstant; + delete expectedBody.entities[1].humidity.metadata.TimeInstant; + return JSON.stringify(body) === JSON.stringify(expectedBody); + } + return false; + }) + .reply(204); + + iotAgentLib.update('ws4', 'WeatherStation', '', singleValue, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + + it('should propagate user provider timestamp to mapped entities', function(done) { + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post( + '/v2/op/update', + utils.readExampleFile( + './test/unit/ngsi-ld/examples' + + '/contextRequests/updateContextMultientityTimestampPlugin3.json' + ) + ) + .reply(204); + + const tsValue = [ + { + name: 'h', + type: 'Percentage', + value: '16' + }, + { + // Note this timestamp is the one used at updateContextMultientityTimestampPlugin3.json + name: 'TimeInstant', + type: 'DateTime', + value: '2018-06-13T13:28:34.611Z' + } + ]; + + iotAgentLib.update('ws5', 'WeatherStation', '', tsValue, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); +}); + +xdescribe('NGSI-LD - Multi-entity plugin is executed for a command update for a regular entity ', function() { + beforeEach(function(done) { + logger.setLevel('FATAL'); + + iotAgentConfig.timestamp = true; + const time = new Date(1438760101468); // 2015-08-05T07:35:01.468+00:00 + timekeeper.freeze(time); + iotAgentLib.activate(iotAgentConfig, function() { + iotAgentLib.clearAll(function() { + iotAgentLib.addUpdateMiddleware(iotAgentLib.dataPlugins.attributeAlias.update); + iotAgentLib.addQueryMiddleware(iotAgentLib.dataPlugins.attributeAlias.query); + iotAgentLib.addUpdateMiddleware(iotAgentLib.dataPlugins.multiEntity.update); + iotAgentLib.addUpdateMiddleware(iotAgentLib.dataPlugins.timestampProcess.update); + done(); + }); + }); + }); + + afterEach(function(done) { + timekeeper.reset(); + iotAgentLib.clearAll(function() { + iotAgentLib.deactivate(done); + }); + }); + + it('Should send the update to the context broker', function(done) { + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post( + '/v2/op/update', + utils.readExampleFile( + './test/unit/ngsi-ld/examples' + '/contextRequests/updateContextMultientityTimestampPlugin4.json' + ) + ) + .reply(204); + const commands = [ + { + name: 'PING_status', + type: 'commandStatus', + value: 'OK' + }, + { + name: 'PING_info', + type: 'commandResult', + value: '1234567890' + } + ]; + + iotAgentLib.update('sensorCommand', 'SensorCommand', '', commands, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); +}); diff --git a/test/unit/ngsi-ld/plugins/timestamp-processing-plugin_test.js b/test/unit/ngsi-ld/plugins/timestamp-processing-plugin_test.js new file mode 100644 index 000000000..7a9562557 --- /dev/null +++ b/test/unit/ngsi-ld/plugins/timestamp-processing-plugin_test.js @@ -0,0 +1,119 @@ +/* + * Copyright 2020 Telefonica Investigación y Desarrollo, S.A.U + * + * This file is part of fiware-iotagent-lib + * + * fiware-iotagent-lib 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. + * + * fiware-iotagent-lib 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 fiware-iotagent-lib. + * If not, see http://www.gnu.org/licenses/. + * + * For those usages not covered by the GNU Affero General Public License + * please contact with::daniel.moranjimenez@telefonica.com + * + * Modified by: Jason Fox - FIWARE Foundation + */ + +const iotAgentLib = require('../../../../lib/fiware-iotagent-lib'); +const utils = require('../../../tools/utils'); +const should = require('should'); +const logger = require('logops'); +const nock = require('nock'); +let contextBrokerMock; +const iotAgentConfig = { + contextBroker: { + host: '192.168.1.1', + port: '1026', + ngsiVersion: 'ld', + jsonLdContext: 'http://context.json-ld' + }, + server: { + port: 4041 + }, + types: { + Light: { + commands: [], + type: 'Light', + lazy: [ + { + name: 'temperature', + type: 'centigrades' + } + ], + active: [ + { + name: 'pressure', + type: 'Hgmm' + } + ] + } + }, + service: 'smartGondor', + subservice: 'gardens', + providerUrl: 'http://smartGondor.com' +}; + +describe('NGSI-LD - Timestamp processing plugin', function() { + beforeEach(function(done) { + logger.setLevel('FATAL'); + + iotAgentLib.activate(iotAgentConfig, function() { + iotAgentLib.clearAll(function() { + iotAgentLib.addUpdateMiddleware(iotAgentLib.dataPlugins.timestampProcess.update); + done(); + }); + }); + }); + + afterEach(function(done) { + iotAgentLib.clearAll(function() { + iotAgentLib.deactivate(done); + }); + }); + describe('When an update comes with a timestamp through the plugin', function() { + const values = [ + { + name: 'state', + type: 'Boolean', + value: true + }, + { + name: 'TimeInstant', + type: 'DateTime', + value: '2016-05-30T16:25:22.304Z' + } + ]; + + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .patch( + '/ngsi-ld/v1/entities/light1/attrs', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextProcessTimestamp.json' + ) + ) + .query({ type: 'Light' }) + .reply(204); + }); + + it('should return an entity with all its timestamps expanded to have separators', function(done) { + iotAgentLib.update('light1', 'Light', '', values, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); +}); diff --git a/test/unit/ngsi-ld/provisioning/device-provisioning-api_test.js b/test/unit/ngsi-ld/provisioning/device-provisioning-api_test.js new file mode 100644 index 000000000..62707f1d7 --- /dev/null +++ b/test/unit/ngsi-ld/provisioning/device-provisioning-api_test.js @@ -0,0 +1,847 @@ +/* + * Copyright 2020 Telefonica Investigación y Desarrollo, S.A.U + * + * This file is part of fiware-iotagent-lib + * + * fiware-iotagent-lib 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. + * + * fiware-iotagent-lib 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 fiware-iotagent-lib. + * If not, see http://www.gnu.org/licenses/. + * + * For those usages not covered by the GNU Affero General Public License + * please contact with::[contacto@tid.es] + * + * Modified by: Jason Fox - FIWARE Foundation + */ + +const iotAgentLib = require('../../../../lib/fiware-iotagent-lib'); +const utils = require('../../../tools/utils'); +const should = require('should'); +const nock = require('nock'); +const request = require('request'); +const moment = require('moment'); +let contextBrokerMock; +const iotAgentConfig = { + logLevel: 'FATAL', + contextBroker: { + host: '192.168.1.1', + port: '1026', + ngsiVersion: 'ld', + jsonLdContext: 'http://context.json-ld' + }, + server: { + port: 4041, + baseRoot: '/' + }, + types: {}, + service: 'smartGondor', + subservice: 'gardens', + providerUrl: 'http://smartGondor.com' +}; + +describe('NGSI-LD - Device provisioning API: Provision devices', function() { + beforeEach(function(done) { + nock.cleanAll(); + + iotAgentLib.activate(iotAgentConfig, function() { + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .post( + '/ngsi-ld/v1/csourceRegistrations/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples' + '/contextAvailabilityRequests/registerProvisionedDevice.json' + ) + ) + .reply(201, null, { Location: '/v2/registrations/6319a7f5254b05844116584d' }); + + // This mockupsert does not check the payload since the aim of the test is not to verify + // device provisioning functionality. Appropriate verification is done in tests under + // provisioning folder + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post('/ngsi-ld/v1/entityOperations/upsert/') + .reply(200); + + iotAgentLib.clearAll(done); + }); + }); + + afterEach(function(done) { + nock.cleanAll(); + iotAgentLib.setProvisioningHandler(); + iotAgentLib.deactivate(done); + }); + + describe('When a device provisioning request with all the required data arrives to the IoT Agent', function() { + beforeEach(function() { + nock.cleanAll(); + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .post( + '/ngsi-ld/v1/csourceRegistrations/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples' + '/contextAvailabilityRequests/registerProvisionedDevice.json' + ) + ) + .reply(201, null, { Location: '/v2/registrations/6319a7f5254b05844116584d' }); + + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/createProvisionedDevice.json') + ) + .reply(200); + }); + + const options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices', + method: 'POST', + json: utils.readExampleFile('./test/unit/examples/deviceProvisioningRequests/provisionNewDevice.json'), + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + } + }; + + it('should add the device to the devices list', function(done) { + request(options, function(error, response, body) { + should.not.exist(error); + response.statusCode.should.equal(201); + + iotAgentLib.listDevices('smartGondor', '/gardens', function(error, results) { + results.devices.length.should.equal(1); + done(); + }); + }); + }); + + it('should call the device provisioning handler if present', function(done) { + let handlerCalled = false; + + iotAgentLib.setProvisioningHandler(function(device, callback) { + handlerCalled = true; + callback(null, device); + }); + + request(options, function(error, response, body) { + handlerCalled.should.equal(true); + done(); + }); + }); + + it('should store the device with the provided entity id, name and type', function(done) { + request(options, function(error, response, body) { + response.statusCode.should.equal(201); + iotAgentLib.listDevices('smartGondor', '/gardens', function(error, results) { + results.devices[0].id.should.equal('Light1'); + results.devices[0].name.should.equal('TheFirstLight'); + results.devices[0].type.should.equal('TheLightType'); + done(); + }); + }); + }); + it('should store the device with the per device information', function(done) { + request(options, function(error, response, body) { + response.statusCode.should.equal(201); + iotAgentLib.listDevices('smartGondor', '/gardens', function(error, results) { + should.exist(results.devices[0].timezone); + results.devices[0].timezone.should.equal('America/Santiago'); + should.exist(results.devices[0].endpoint); + results.devices[0].endpoint.should.equal('http://fakedEndpoint:1234'); + should.exist(results.devices[0].transport); + results.devices[0].transport.should.equal('MQTT'); + should.exist(results.devices[0].lazy); + results.devices[0].lazy.length.should.equal(1); + results.devices[0].lazy[0].name.should.equal('luminance'); + should.exist(results.devices[0].staticAttributes); + results.devices[0].commands.length.should.equal(1); + results.devices[0].commands[0].name.should.equal('commandAttr'); + should.exist(results.devices[0].staticAttributes); + results.devices[0].staticAttributes.length.should.equal(1); + results.devices[0].staticAttributes[0].name.should.equal('hardcodedAttr'); + should.exist(results.devices[0].active); + results.devices[0].active.length.should.equal(1); + results.devices[0].active[0].name.should.equal('attr_name'); + should.exist(results.devices[0].internalAttributes); + results.devices[0].internalAttributes.length.should.equal(1); + results.devices[0].internalAttributes[0].customField.should.equal('customValue'); + done(); + }); + }); + }); + + it('should store fill the device ID in case only the name is provided', function(done) { + /* jshint camelcase:false */ + request(options, function(error, response, body) { + response.statusCode.should.equal(201); + iotAgentLib.listDevices('smartGondor', '/gardens', function(error, results) { + results.devices[0].lazy[0].object_id.should.equal('luminance'); + results.devices[0].commands[0].object_id.should.equal('commandAttr'); + results.devices[0].active[0].object_id.should.equal('attr_name'); + done(); + }); + }); + }); + + it('should store service and subservice info from the headers along with the device data', function(done) { + request(options, function(error, response, body) { + response.statusCode.should.equal(201); + iotAgentLib.listDevices('smartGondor', '/gardens', function(error, results) { + should.exist(results.devices[0].service); + results.devices[0].service.should.equal('smartGondor'); + should.exist(results.devices[0].subservice); + results.devices[0].subservice.should.equal('/gardens'); + done(); + }); + }); + }); + + it('should create the initial entity in the Context Broker', function(done) { + request(options, function(error, response, body) { + response.statusCode.should.equal(201); + iotAgentLib.listDevices('smartGondor', '/gardens', function(error, results) { + contextBrokerMock.done(); + done(); + }); + }); + }); + }); + describe('When a device provisioning request with a TimeInstant attribute arrives to the IoTA', function() { + const options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices', + method: 'POST', + json: utils.readExampleFile('./test/unit/examples/deviceProvisioningRequests/provisionTimeInstant.json'), + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + } + }; + + beforeEach(function(done) { + iotAgentLib.deactivate(function() { + iotAgentConfig.timestamp = true; + iotAgentLib.activate(iotAgentConfig, done); + }); + }); + + afterEach(function() { + iotAgentConfig.timestamp = false; + }); + + beforeEach(function(done) { + nock.cleanAll(); + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/createTimeinstantDevice.json') + ) + .reply(200); + + done(); + }); + + it('should send the appropriate requests to the Context Broker', function(done) { + request(options, function(error, response, body) { + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When a device provisioning request with a timestamp provision attribute arrives to the IoTA', function() { + const options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices', + method: 'POST', + json: utils.readExampleFile('./test/unit/examples/deviceProvisioningRequests/provisionTimeInstant2.json'), + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + } + }; + + beforeEach(function(done) { + iotAgentLib.deactivate(function() { + iotAgentConfig.timestamp = false; + iotAgentLib.activate(iotAgentConfig, done); + }); + }); + + afterEach(function() { + iotAgentConfig.timestamp = false; + }); + + beforeEach(function(done) { + nock.cleanAll(); + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/createTimeinstantDevice.json') + ) + .reply(200); + + done(); + }); + + it('should send the appropriate requests to the Context Broker', function(done) { + request(options, function(error, response, body) { + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When a device provisioning request with a autoprovision attribute arrives to the IoTA', function() { + const options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices', + method: 'POST', + json: utils.readExampleFile('./test/unit/examples/deviceProvisioningRequests/provisionAutoprovision.json'), + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + } + }; + + beforeEach(function(done) { + iotAgentLib.deactivate(function() { + iotAgentConfig.appendMode = false; + iotAgentLib.activate(iotAgentConfig, done); + }); + }); + + afterEach(function() { + iotAgentConfig.appendMode = false; + }); + + beforeEach(function(done) { + nock.cleanAll(); + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/' + 'contextRequests/createAutoprovisionDevice.json' + ) + ) + .reply(200); + done(); + }); + + it('should send the appropriate requests to the Context Broker', function(done) { + request(options, function(error, response, body) { + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe( + 'When a device provisioning request arrives to the IoTA' + 'and timestamp is enabled in configuration', + function() { + const options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices', + method: 'POST', + json: utils.readExampleFile( + './test/unit/examples/deviceProvisioningRequests/provisionMinimumDevice.json' + ), + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + } + }; + + beforeEach(function(done) { + iotAgentLib.deactivate(function() { + iotAgentConfig.timestamp = true; + iotAgentLib.activate(iotAgentConfig, done); + }); + }); + + afterEach(function() { + iotAgentConfig.timestamp = false; + }); + + beforeEach(function(done) { + nock.cleanAll(); + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .post('/ngsi-ld/v1/entityOperations/upsert/', function(body) { + const expectedBody = utils.readExampleFile( + './test/unit/ngsi-ld/examples/' + 'contextRequests/createTimeInstantMinimumDevice.json' + ); + if (!body[0].TimeInstant.value['@value']) { + return false; + } else if (moment(body[0].TimeInstant.value['@value'], 'YYYY-MM-DDTHH:mm:ss.SSSZ').isValid()) { + const timeInstantDiff = moment().diff(body[0].TimeInstant.value['@value'], 'milliseconds'); + if (timeInstantDiff < 500) { + delete body[0].TimeInstant; + + return JSON.stringify(body) === JSON.stringify(expectedBody); + } + + return false; + } + + return false; + }) + .reply(204); + + done(); + }); + + it('should send the appropriate requests to the Context Broker', function(done) { + request(options, function(error, response, body) { + contextBrokerMock.done(); + done(); + }); + }); + } + ); + + describe('When a device provisioning request with the minimum required data arrives to the IoT Agent', function() { + const options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices', + method: 'POST', + json: utils.readExampleFile('./test/unit/examples/deviceProvisioningRequests/provisionMinimumDevice.json'), + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + } + }; + + beforeEach(function(done) { + nock.cleanAll(); + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/' + 'contextRequests/createMinimumProvisionedDevice.json' + ) + ) + .reply(200); + + done(); + }); + + it('should send the appropriate requests to the Context Broker', function(done) { + request(options, function(error, response, body) { + contextBrokerMock.done(); + done(); + }); + }); + + it('should add the device to the devices list', function(done) { + request(options, function(error, response, body) { + should.not.exist(error); + response.statusCode.should.equal(201); + + iotAgentLib.listDevices('smartGondor', '/gardens', function(error, results) { + results.devices.length.should.equal(1); + done(); + }); + }); + }); + + it('should store the device with the provided entity id, name and type', function(done) { + request(options, function(error, response, body) { + response.statusCode.should.equal(201); + iotAgentLib.listDevices('smartGondor', '/gardens', function(error, results) { + results.devices[0].id.should.equal('MicroLight1'); + results.devices[0].name.should.equal('FirstMicroLight'); + results.devices[0].type.should.equal('MicroLights'); + done(); + }); + }); + }); + }); + + describe('When a device provisioning request with geo:point attributes arrives', function() { + const options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices', + method: 'POST', + json: utils.readExampleFile('./test/unit/examples/deviceProvisioningRequests/provisionGeopointDevice.json'), + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + } + }; + + beforeEach(function(done) { + nock.cleanAll(); + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/createGeopointProvisionedDevice.json' + ) + ) + .reply(200); + + done(); + }); + + it('should send the appropriate initial values to the Context Broker', function(done) { + request(options, function(error, response, body) { + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When a device provisioning request with DateTime attributes arrives', function() { + const options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices', + method: 'POST', + json: utils.readExampleFile('./test/unit/examples/deviceProvisioningRequests/provisionDatetimeDevice.json'), + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + } + }; + + beforeEach(function(done) { + nock.cleanAll(); + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/createDatetimeProvisionedDevice.json' + ) + ) + .reply(204); + + done(); + }); + + it('should send the appropriate initial values to the Context Broker', function(done) { + request(options, function(error, response, body) { + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When two devices with the same ID but different services arrive to the agent', function() { + const options1 = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices', + method: 'POST', + json: utils.readExampleFile('./test/unit/examples/deviceProvisioningRequests/provisionMinimumDevice.json'), + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + } + }; + const options2 = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices', + method: 'POST', + json: utils.readExampleFile('./test/unit/examples/deviceProvisioningRequests/provisionMinimumDevice.json'), + headers: { + 'fiware-service': 'smartMordor', + 'fiware-servicepath': '/electricity' + } + }; + + beforeEach(function(done) { + nock.cleanAll(); + contextBrokerMock = nock('http://192.168.1.1:1026') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/' + 'contextRequests/createMinimumProvisionedDevice.json' + ) + ) + .reply(200); + + contextBrokerMock + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/' + 'contextRequests/createMinimumProvisionedDevice.json' + ) + ) + .reply(200); + + done(); + }); + + it('should accept both creations', function(done) { + request(options1, function(error, response, body) { + response.statusCode.should.equal(201); + + request(options2, function(error, response, body) { + response.statusCode.should.equal(201); + done(); + }); + }); + }); + + it('should show the new device in each list', function(done) { + request(options1, function(error, response, body) { + request(options2, function(error, response, body) { + iotAgentLib.listDevices('smartGondor', '/gardens', function(error, results) { + results.devices.length.should.equal(1); + results.devices[0].id.should.equal('MicroLight1'); + + iotAgentLib.listDevices('smartMordor', '/electricity', function(error, results) { + results.devices.length.should.equal(1); + results.devices[0].id.should.equal('MicroLight1'); + done(); + }); + }); + }); + }); + }); + }); + + describe('When there is a connection error with a String code connecting the CB', function() { + const options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices', + method: 'POST', + json: utils.readExampleFile('./test/unit/examples/deviceProvisioningRequests/provisionMinimumDevice.json'), + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + } + }; + + beforeEach(function(done) { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .post('/ngsi-ld/v1/csourceRegistrations/') + .reply(201, null, { Location: '/v2/registrations/6319a7f5254b05844116584d' }); + + // This mock does not check the payload since the aim of the test is not to verify + // device provisioning functionality. Appropriate verification is done in tests under + // provisioning folder + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post('/ngsi-ld/v1/entityOperations/upsert/') + .replyWithError({ message: 'Description of the error', code: 'STRING_CODE' }); + + done(); + }); + + it('should return a valid return code', function(done) { + request(options, function(error, response, body) { + should.not.exist(error); + response.statusCode.should.equal(500); + + done(); + }); + }); + }); + + describe('When there is a connection error with a Number code connecting the CB', function() { + const options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices', + method: 'POST', + json: utils.readExampleFile('./test/unit/examples/deviceProvisioningRequests/provisionMinimumDevice.json'), + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + } + }; + + beforeEach(function(done) { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .post('/ngsi-ld/v1/csourceRegistrations/') + .reply(201, null, { Location: '/v2/registrations/6319a7f5254b05844116584d' }); + + // This mock does not check the payload since the aim of the test is not to verify + // device provisioning functionality. Appropriate verification is done in tests under + // provisioning folder + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post('/ngsi-ld/v1/entityOperations/upsert/') + .replyWithError({ message: 'Description of the error', code: 123456789 }); + + done(); + }); + + it('should return a valid return code (three character number)', function(done) { + request(options, function(error, response, body) { + should.not.exist(error); + response.statusCode.should.equal(500); + + done(); + }); + }); + }); + + describe('When a device provisioning request with missing data arrives to the IoT Agent', function() { + const options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices', + method: 'POST', + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + }, + json: utils.readExampleFile( + './test/unit/examples/deviceProvisioningRequests/provisionDeviceMissingParameters.json' + ) + }; + + it('should raise a MISSING_ATTRIBUTES error, indicating the missing attributes', function(done) { + request(options, function(error, response, body) { + should.exist(body); + response.statusCode.should.equal(400); + body.name.should.equal('MISSING_ATTRIBUTES'); + body.message.should.match(/.*device_id.*/); + done(); + }); + }); + }); + describe('When two device provisioning requests with the same service and Device ID arrive', function() { + const options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices', + method: 'POST', + json: utils.readExampleFile('./test/unit/examples/deviceProvisioningRequests/provisionNewDevice.json'), + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + } + }; + + beforeEach(function(done) { + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .post('/ngsi-ld/v1/csourceRegistrations/') + .reply(201, null, { Location: '/v2/registrations/6319a7f5254b05844116584d' }); + + // This mock does not check the payload since the aim of the test is not to verify + // device provisioning functionality. Appropriate verification is done in tests under + // provisioning folder + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post('/ngsi-ld/v1/entityOperations/upsert/') + .reply(200); + + done(); + }); + + it('should raise a DUPLICATE_ID error, indicating the ID was already in use', function(done) { + request(options, function(error, response, body) { + request(options, function(error, response, body) { + should.exist(body); + response.statusCode.should.equal(409); + body.name.should.equal('DUPLICATE_DEVICE_ID'); + done(); + }); + }); + }); + }); + describe('When a device provisioning request is malformed', function() { + const options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices', + method: 'POST', + json: utils.readExampleFile( + './test/unit/examples/deviceProvisioningRequests/provisionNewDeviceMalformed1.json' + ), + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + } + }; + + it('should raise a WRONG_SYNTAX exception', function(done) { + request(options, function(error, response, body) { + request(options, function(error, response, body) { + should.exist(body); + response.statusCode.should.equal(400); + body.name.should.equal('WRONG_SYNTAX'); + done(); + }); + }); + }); + }); + describe('When an agent is activated with a different base root', function() { + const options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/newBaseRoot/iot/devices', + method: 'POST', + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + }, + json: utils.readExampleFile('./test/unit/examples/deviceProvisioningRequests/provisionNewDevice.json') + }; + + beforeEach(function(done) { + iotAgentLib.deactivate(function() { + iotAgentConfig.server.baseRoot = '/newBaseRoot'; + iotAgentLib.activate(iotAgentConfig, done); + }); + }); + + afterEach(function() { + iotAgentConfig.server.baseRoot = '/'; + }); + + it('should listen to requests in the new root', function(done) { + request(options, function(error, response, body) { + should.not.exist(error); + response.statusCode.should.equal(201); + + iotAgentLib.listDevices('smartGondor', '/gardens', function(error, results) { + results.devices.length.should.equal(1); + done(); + }); + }); + }); + }); + describe('When a device provisioning request without the mandatory headers arrives to the Agent', function() { + const options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices', + method: 'POST', + headers: {}, + json: utils.readExampleFile( + './test/unit/examples/deviceProvisioningRequests/provisionDeviceMissingParameters.json' + ) + }; + + it('should raise a MISSING_HEADERS error, indicating the missing attributes', function(done) { + request(options, function(error, response, body) { + should.exist(body); + response.statusCode.should.equal(400); + body.name.should.equal('MISSING_HEADERS'); + done(); + }); + }); + }); + describe('When a device delete request arrives to the Agent for a not existing device', function() { + const options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices/Light84', + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + }, + method: 'DELETE' + }; + + it('should return a 404 error', function(done) { + request(options, function(error, response, body) { + should.not.exist(error); + response.statusCode.should.equal(404); + done(); + }); + }); + }); +}); diff --git a/test/unit/ngsi-ld/provisioning/device-registration_test.js b/test/unit/ngsi-ld/provisioning/device-registration_test.js new file mode 100644 index 000000000..16f0039ba --- /dev/null +++ b/test/unit/ngsi-ld/provisioning/device-registration_test.js @@ -0,0 +1,354 @@ +/* + * Copyright 2020 Telefonica Investigación y Desarrollo, S.A.U + * + * This file is part of fiware-iotagent-lib + * + * fiware-iotagent-lib 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. + * + * fiware-iotagent-lib 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 fiware-iotagent-lib. + * If not, see http://www.gnu.org/licenses/. + * + * For those usages not covered by the GNU Affero General Public License + * please contact with::[contacto@tid.es] + * + * Modified by: Jason Fox - FIWARE Foundation + */ + +const iotAgentLib = require('../../../../lib/fiware-iotagent-lib'); +const utils = require('../../../tools/utils'); +const should = require('should'); +const logger = require('logops'); +const nock = require('nock'); +const async = require('async'); +let contextBrokerMock; +const iotAgentConfig = { + contextBroker: { + host: '192.168.1.1', + port: '1026', + ngsiVersion: 'ld', + jsonLdContext: 'http://context.json-ld' + }, + server: { + port: 4041 + }, + types: { + Light: { + commands: [], + lazy: [ + { + name: 'temperature', + type: 'centigrades' + } + ], + active: [ + { + name: 'pressure', + type: 'Hgmm' + } + ], + service: 'smartGondor', + subservice: 'gardens' + }, + Termometer: { + commands: [], + lazy: [ + { + name: 'temp', + type: 'kelvin' + } + ], + active: [], + service: 'smartGondor', + subservice: 'gardens' + } + }, + service: 'smartGondor', + subservice: 'gardens', + providerUrl: 'http://smartGondor.com' +}; +const device1 = { + id: 'light1', + type: 'Light', + service: 'smartGondor', + subservice: 'gardens' +}; +const device2 = { + id: 'term2', + type: 'Termometer', + service: 'smartGondor', + subservice: 'gardens' +}; + +describe('NGSI-LD - IoT Agent Device Registration', function() { + beforeEach(function() { + logger.setLevel('FATAL'); + }); + + afterEach(function(done) { + iotAgentLib.clearAll(function() { + // We need to remove the registrationId so that the library does not consider next operatios as updates. + delete device1.registrationId; + delete device2.registrationId; + iotAgentLib.deactivate(done); + }); + }); + + describe('When a new device is connected to the IoT Agent', function() { + beforeEach(function(done) { + nock.cleanAll(); + + // This mock does not check the payload since the aim of the test is not to verify + // device provisioning functionality. Appropriate verification is done in tests under + // provisioning folder + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .post('/ngsi-ld/v1/entityOperations/upsert/') + .reply(200); + + const nockBody = utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerIoTAgent1.json' + ); + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post('/ngsi-ld/v1/csourceRegistrations/', nockBody) + .reply(201, null, { Location: '/v2/registrations/6319a7f5254b05844116584d' }); + + iotAgentLib.activate(iotAgentConfig, function(error) { + iotAgentLib.clearAll(done); + }); + }); + + it('should register as ContextProvider of its lazy attributes', function(done) { + iotAgentLib.register(device1, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When the Context Broker returns a NGSI error while registering a device', function() { + beforeEach(function(done) { + nock.cleanAll(); + + const nockBody = utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerIoTAgent1.json' + ); + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .post('/ngsi-ld/v1/csourceRegistrations/', nockBody) + .reply(404); + + iotAgentLib.activate(iotAgentConfig, function(error) { + iotAgentLib.clearAll(done); + }); + }); + + it('should register as ContextProvider of its lazy attributes', function(done) { + iotAgentLib.register(device1, function(error) { + should.exist(error); + error.name.should.equal('BAD_REQUEST'); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When the Context Broker returns an HTTP transport error while registering a device', function() { + beforeEach(function(done) { + nock.cleanAll(); + const nockBody = utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerIoTAgent1.json' + ); + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .post('/ngsi-ld/v1/csourceRegistrations/', nockBody) + .reply(500); + + iotAgentLib.activate(iotAgentConfig, function(error) { + iotAgentLib.clearAll(done); + }); + }); + + it('should not register the device in the internal registry'); + it('should return a REGISTRATION_ERROR error to the caller', function(done) { + iotAgentLib.register(device1, function(error) { + should.exist(error); + should.exist(error.name); + error.name.should.equal('REGISTRATION_ERROR'); + + done(); + }); + }); + }); + + describe('When a device is requested to the library using its ID', function() { + beforeEach(function(done) { + nock.cleanAll(); + + const nockBody = utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerIoTAgent1.json' + ); + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .post('/ngsi-ld/v1/csourceRegistrations/', nockBody) + .reply(201, null, { Location: '/v2/registrations/6319a7f5254b05844116584d' }); + + // This mock does not check the payload since the aim of the test is not to verify + // device provisioning functionality. Appropriate verification is done in tests under + // provisioning folder + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post('/ngsi-ld/v1/entityOperations/upsert/') + .reply(200); + + iotAgentLib.activate(iotAgentConfig, function(error) { + iotAgentLib.clearAll(done); + }); + }); + + it('should return all the device\'s information', function(done) { + iotAgentLib.register(device1, function(error) { + iotAgentLib.getDevice('light1', 'smartGondor', 'gardens', function(error, data) { + should.not.exist(error); + should.exist(data); + data.type.should.equal('Light'); + data.name.should.equal('Light:light1'); + done(); + }); + }); + }); + }); + + describe('When an unexistent device is requested to the library using its ID', function() { + beforeEach(function(done) { + nock.cleanAll(); + + const nockBody = utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerIoTAgent1.json' + ); + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .post('/ngsi-ld/v1/csourceRegistrations/', nockBody) + .reply(201, null, { Location: '/v2/registrations/6319a7f5254b05844116584d' }); + + iotAgentLib.activate(iotAgentConfig, function(error) { + iotAgentLib.clearAll(done); + }); + }); + + it('should return a ENTITY_NOT_FOUND error', function(done) { + iotAgentLib.register(device1, function(error) { + iotAgentLib.getDevice('lightUnexistent', 'smartGondor', 'gardens', function(error, data) { + should.exist(error); + should.not.exist(data); + error.code.should.equal(404); + error.name.should.equal('DEVICE_NOT_FOUND'); + done(); + }); + }); + }); + }); + + xdescribe('When a device is removed from the IoT Agent', function() { + beforeEach(function(done) { + nock.cleanAll(); + contextBrokerMock = nock('http://192.168.1.1:1026') + .post('/ngsi-ld/v1/csourceRegistrations/') + .reply(201, null, { Location: '/v2/registrations/6319a7f5254b05844116584d' }); + + // This mock does not check the payload since the aim of the test is not to verify + // device provisioning functionality. Appropriate verification is done in tests under + // provisioning folder + contextBrokerMock.post('/ngsi-ld/v1/entityOperations/upsert/').reply(200); + + contextBrokerMock + .post('/ngsi-ld/v1/csourceRegistrations/') + .reply(201, null, { Location: '/v2/registrations/6319a7f5254b05844116584d' }); + + // This mock does not check the payload since the aim of the test is not to verify + // device provisioning functionality. Appropriate verification is done in tests under + // provisioning folder + contextBrokerMock.post('/ngsi-ld/v1/entityOperations/upsert/').reply(200); + + contextBrokerMock + .delete('/v2/registrations/6319a7f5254b05844116584d') + .reply(204, null, { Location: '/v2/registrations/6319a7f5254b05844116584d' }); + + iotAgentLib.activate(iotAgentConfig, function(error) { + async.series( + [ + async.apply(iotAgentLib.clearAll), + async.apply(iotAgentLib.register, device1), + async.apply(iotAgentLib.register, device2) + ], + done + ); + }); + }); + + it('should update the devices information in Context Broker', function(done) { + iotAgentLib.unregister(device1.id, 'smartGondor', 'gardens', function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + xdescribe('When the Context Broker returns an error while unregistering a device', function() { + beforeEach(function(done) { + nock.cleanAll(); + contextBrokerMock = nock('http://192.168.1.1:1026') + .post('/ngsi-ld/v1/csourceRegistrations/') + .reply(201, null, { Location: '/v2/registrations/6319a7f5254b05844116584d' }); + + // This mock does not check the payload since the aim of the test is not to verify + // device provisioning functionality. Appropriate verification is done in tests under + // provisioning folder + contextBrokerMock.post('/ngsi-ld/v1/entityOperations/upsert/').reply(200); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .post('/ngsi-ld/v1/csourceRegistrations/') + .reply(201, null, { Location: '/v2/registrations/8254b65a7d11650f45844319' }); + + // This mock does not check the payload since the aim of the test is not to verify + // device provisioning functionality. Appropriate verification is done in tests under + // provisioning folder + contextBrokerMock.post('/ngsi-ld/v1/entityOperations/upsert/').reply(200); + + contextBrokerMock.delete('/v2/registrations/6319a7f5254b05844116584d').reply(500); + + iotAgentLib.activate(iotAgentConfig, function(error) { + async.series( + [ + async.apply(iotAgentLib.clearAll), + async.apply(iotAgentLib.register, device1), + async.apply(iotAgentLib.register, device2) + ], + done + ); + }); + }); + + it('should not remove the device from the internal registry'); + it('should return a UNREGISTRATION_ERROR error to the caller', function(done) { + iotAgentLib.unregister(device1.id, 'smartGondor', 'gardens', function(error) { + should.exist(error); + should.exist(error.name); + error.name.should.equal('UNREGISTRATION_ERROR'); + + done(); + }); + }); + }); +}); diff --git a/test/unit/ngsi-ld/provisioning/device-update-registration_test.js b/test/unit/ngsi-ld/provisioning/device-update-registration_test.js new file mode 100644 index 000000000..483a9aedd --- /dev/null +++ b/test/unit/ngsi-ld/provisioning/device-update-registration_test.js @@ -0,0 +1,315 @@ +/* + * Copyright 2020 Telefonica Investigación y Desarrollo, S.A.U + * + * This file is part of fiware-iotagent-lib + * + * fiware-iotagent-lib 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. + * + * fiware-iotagent-lib 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 fiware-iotagent-lib. + * If not, see http://www.gnu.org/licenses/. + * + * For those usages not covered by the GNU Affero General Public License + * please contact with::[contacto@tid.es] + * + * Modified by: Jason Fox - FIWARE Foundation + */ + +const iotAgentLib = require('../../../../lib/fiware-iotagent-lib'); +const utils = require('../../../tools/utils'); +const should = require('should'); +const logger = require('logops'); +const nock = require('nock'); +let contextBrokerMock; +const iotAgentConfig = { + contextBroker: { + host: '192.168.1.1', + port: '1026', + ngsiVersion: 'ld', + jsonLdContext: 'http://context.json-ld' + }, + server: { + port: 4041 + }, + types: { + Light: { + commands: [], + lazy: [ + { + name: 'temperature', + type: 'centigrades' + } + ], + active: [ + { + name: 'pressure', + type: 'Hgmm' + } + ], + service: 'smartGondor', + subservice: 'gardens' + }, + Termometer: { + commands: [], + lazy: [ + { + name: 'temp', + type: 'kelvin' + } + ], + active: [], + service: 'smartGondor', + subservice: 'gardens' + } + }, + service: 'smartGondor', + subservice: 'gardens', + providerUrl: 'http://smartGondor.com' +}; +const device1 = { + id: 'light1', + type: 'Light', + service: 'smartGondor', + subservice: 'gardens' +}; +const deviceUpdated = { + id: 'light1', + type: 'Light', + name: 'light1', + service: 'smartGondor', + subservice: 'gardens', + internalId: 'newInternalId', + lazy: [ + { + name: 'pressure', + type: 'Hgmm' + } + ], + active: [ + { + name: 'temperature', + type: 'centigrades' + } + ] +}; +const deviceCommandUpdated = { + id: 'light1', + type: 'Light', + name: 'light1', + service: 'smartGondor', + subservice: 'gardens', + internalId: 'newInternalId', + commands: [ + { + name: 'move', + type: 'command' + } + ], + active: [ + { + name: 'temperature', + type: 'centigrades' + } + ] +}; +const unknownDevice = { + id: 'rotationSensor4', + type: 'Rotation', + name: 'Rotation4', + service: 'dumbMordor', + subservice: 'gardens', + internalId: 'unknownInternalId', + + lazy: [], + active: [] +}; + +xdescribe('NGSI-LD - IoT Agent Device Update Registration', function() { + beforeEach(function(done) { + delete device1.registrationId; + logger.setLevel('FATAL'); + + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/ngsi-ld/v1/csourceRegistrations/') + .reply(201, null, { Location: '/v2/registrations/6319a7f5254b05844116584d' }); + + // This mock does not check the payload since the aim of the test is not to verify + // device provisioning functionality. Appropriate verification is done in tests under + // provisioning folder + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/ngsi-ld/v1/entityOperations/upsert/') + .reply(204); + + iotAgentLib.activate(iotAgentConfig, function(error) { + iotAgentLib.register(device1, function(error) { + done(); + }); + }); + }); + + afterEach(function(done) { + nock.cleanAll(); + iotAgentLib.clearAll(function() { + iotAgentLib.deactivate(done); + }); + }); + + describe('When a device is preregistered and its registration information updated', function() { + beforeEach(function() { + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post( + '/ngsi-ld/v1/entities/Light:light1/attrs?type=Light', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateProvisionActiveAttributes1.json' + ) + ) + .reply(204); + + // FIXME: When https://github.com/telefonicaid/fiware-orion/issues/3007 is merged into master branch, + // this function should use the new API. This is just a temporary solution which implies deleting the + // registration and creating a new one. + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .delete('/v2/registrations/6319a7f5254b05844116584d') + .reply(204); + + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post( + '/ngsi-ld/v1/csourceRegistrations/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples' + '/contextAvailabilityRequests/updateIoTAgent1.json' + ) + ) + .reply(201, null, { Location: '/v2/registrations/6319a7f5254b05844116584d' }); + }); + + it('should register as ContextProvider of its lazy attributes', function(done) { + iotAgentLib.updateRegister(deviceUpdated, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + it('should store the new values in the registry', function(done) { + iotAgentLib.updateRegister(deviceUpdated, function(error, data) { + iotAgentLib.getDevice(deviceUpdated.id, 'smartGondor', 'gardens', function(error, deviceResult) { + should.not.exist(error); + should.exist(deviceResult); + deviceResult.internalId.should.equal(deviceUpdated.internalId); + deviceResult.lazy[0].name.should.equal('pressure'); + deviceResult.active[0].name.should.equal('temperature'); + done(); + }); + }); + }); + }); + + describe('When a device is preregistered and it is updated with new commands', function() { + beforeEach(function() { + delete deviceCommandUpdated.registrationId; + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post( + '/ngsi-ld/v1/entities/Light:light1/attrs?type=Light', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/updateProvisionCommands1.json') + ) + .reply(204); + + // FIXME: When https://github.com/telefonicaid/fiware-orion/issues/3007 is merged into master branch, + // this function should use the new API. This is just a temporary solution which implies deleting the + // registration and creating a new one. + + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .delete('/v2/registrations/6319a7f5254b05844116584d') + .reply(204); + + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post( + '/ngsi-ld/v1/csourceRegistrations/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples' + '/contextAvailabilityRequests/updateCommands1.json' + ) + ) + .reply(201, null, { Location: '/v2/registrations/6319a7f5254b05844116584d' }); + }); + + it('should register as ContextProvider of its commands and create the additional attributes', function(done) { + iotAgentLib.updateRegister(deviceCommandUpdated, function(error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + it('should store the new values in the registry', function(done) { + iotAgentLib.updateRegister(deviceCommandUpdated, function(error, data) { + iotAgentLib.getDevice(deviceCommandUpdated.id, 'smartGondor', 'gardens', function(error, deviceResult) { + should.not.exist(error); + should.exist(deviceResult); + deviceResult.internalId.should.equal(deviceUpdated.internalId); + deviceResult.commands[0].name.should.equal('move'); + deviceResult.active[0].name.should.equal('temperature'); + done(); + }); + }); + }); + }); + + describe('When a update action is executed in a non registered device', function() { + it('should return a DEVICE_NOT_FOUND error', function(done) { + iotAgentLib.updateRegister(unknownDevice, function(error) { + should.exist(error); + error.name.should.equal('DEVICE_NOT_FOUND'); + done(); + }); + }); + }); + describe('When a device register is updated in the Context Broker and the request fail to connect', function() { + beforeEach(function() { + // FIXME: When https://github.com/telefonicaid/fiware-orion/issues/3007 is merged into master branch, + // this function should use the new API. This is just a temporary solution which implies deleting the + // registration and creating a new one. + contextBrokerMock.delete('/v2/registrations/6319a7f5254b05844116584d').reply(500, {}); + + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .matchHeader('fiware-servicepath', 'gardens') + .post('/ngsi-ld/v1/entities/Light:light1/attrs?type=Light') + .reply(204); + }); + + it('should return a REGISTRATION_ERROR error in the update action', function(done) { + iotAgentLib.updateRegister(deviceUpdated, function(error) { + should.exist(error); + error.name.should.equal('UNREGISTRATION_ERROR'); + done(); + }); + }); + }); + describe('When a device register is updated in the Context Broker and the registration is not found', function() { + it('should create the registration anew'); + }); +}); diff --git a/test/unit/ngsi-ld/provisioning/listProvisionedDevices-test.js b/test/unit/ngsi-ld/provisioning/listProvisionedDevices-test.js new file mode 100644 index 000000000..8a675120f --- /dev/null +++ b/test/unit/ngsi-ld/provisioning/listProvisionedDevices-test.js @@ -0,0 +1,456 @@ +/* + * Copyright 2020 Telefonica Investigación y Desarrollo, S.A.U + * + * This file is part of fiware-iotagent-lib + * + * fiware-iotagent-lib 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. + * + * fiware-iotagent-lib 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 fiware-iotagent-lib. + * If not, see http://www.gnu.org/licenses/. + * + * For those usages not covered by the GNU Affero General Public License + * please contact with::[contacto@tid.es] + * + * Modified by: Jason Fox - FIWARE Foundation + */ + +/* jshint camelcase: false */ + +const iotAgentLib = require('../../../../lib/fiware-iotagent-lib'); +const utils = require('../../../tools/utils'); +const should = require('should'); +const nock = require('nock'); +const async = require('async'); +const request = require('request'); +let contextBrokerMock; +const iotAgentConfig = { + logLevel: 'FATAL', + contextBroker: { + host: '192.168.1.1', + port: '1026', + ngsiVersion: 'ld', + jsonLdContext: 'http://context.json-ld' + }, + server: { + port: 4041, + baseRoot: '/' + }, + types: {}, + service: 'smartGondor', + subservice: 'gardens', + providerUrl: 'http://smartGondor.com' +}; + +describe('NGSI-LD - Device provisioning API: List provisioned devices', function() { + let provisioning1Options; + let provisioning2Options; + let provisioning3Options; + let provisioning4Options; + + beforeEach(function(done) { + provisioning1Options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices', + method: 'POST', + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + }, + json: utils.readExampleFile('./test/unit/examples/deviceProvisioningRequests/provisionNewDevice.json') + }; + + provisioning2Options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices', + method: 'POST', + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + }, + json: utils.readExampleFile('./test/unit/examples/deviceProvisioningRequests/provisionAnotherDevice.json') + }; + + provisioning4Options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices', + method: 'POST', + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + }, + json: utils.readExampleFile('./test/unit/examples/deviceProvisioningRequests/provisionFullDevice.json') + }; + + iotAgentLib.activate(iotAgentConfig, function() { + contextBrokerMock = nock('http://192.168.1.1:1026') + .post('/ngsi-ld/v1/csourceRegistrations/') + .reply(201, null, { Location: '/v2/registrations/6319a7f5254b05844116584d' }); + + // This mock does not check the payload since the aim of the test is not to verify + // device provisioning functionality. Appropriate verification is done in tests under + // provisioning folder + contextBrokerMock.post('/ngsi-ld/v1/entityOperations/upsert/').reply(200); + + contextBrokerMock + .post('/ngsi-ld/v1/csourceRegistrations/') + .reply(201, null, { Location: '/v2/registrations/6319a7f5254b05844116584d' }); + + // This mock does not check the payload since the aim of the test is not to verify + // device provisioning functionality. Appropriate verification is done in tests under + // provisioning folder + contextBrokerMock.post('/ngsi-ld/v1/entityOperations/upsert/').reply(200); + + contextBrokerMock + .post('/ngsi-ld/v1/csourceRegistrations/') + .reply(201, null, { Location: '/v2/registrations/6319a7f5254b05844116584d' }); + + // This mock does not check the payload since the aim of the test is not to verify + // device provisioning functionality. Appropriate verification is done in tests under + // provisioning folder + contextBrokerMock.post('/ngsi-ld/v1/entityOperations/upsert/').reply(200); + + async.series( + [ + iotAgentLib.clearAll, + async.apply(request, provisioning1Options), + async.apply(request, provisioning2Options), + async.apply(request, provisioning4Options) + ], + function(error, results) { + done(); + } + ); + }); + }); + + afterEach(function(done) { + iotAgentLib.deactivate(done); + }); + + describe('When a request for the list of provisioned devices arrive', function() { + const options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices', + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + }, + method: 'GET' + }; + + it('should return all the provisioned devices', function(done) { + request(options, function(error, response, body) { + const parsedBody = JSON.parse(body); + should.not.exist(error); + should.exist(parsedBody.devices); + response.statusCode.should.equal(200); + parsedBody.devices.length.should.equal(3); + parsedBody.count.should.equal(3); + done(); + }); + }); + + it('should return all the appropriate field names', function(done) { + /* jshint camelcase:false */ + + request(options, function(error, response, body) { + const parsedBody = JSON.parse(body); + + should.exist(parsedBody.devices[0].attributes); + parsedBody.devices[0].attributes.length.should.equal(1); + + should.exist(parsedBody.devices[0].device_id); + parsedBody.devices[0].device_id.should.equal('Light1'); + + should.exist(parsedBody.devices[0].entity_name); + parsedBody.devices[0].entity_name.should.equal('TheFirstLight'); + + should.exist(parsedBody.devices[0].protocol); + parsedBody.devices[0].protocol.should.equal('GENERIC_PROTO'); + + should.exist(parsedBody.devices[0].static_attributes); + parsedBody.devices[0].static_attributes.length.should.equal(1); + + done(); + }); + }); + + it('should return all the plugin attributes', function(done) { + request(options, function(error, response, body) { + const parsedBody = JSON.parse(body); + + should.exist(parsedBody.devices[2].attributes[0].entity_name); + should.exist(parsedBody.devices[2].attributes[0].entity_type); + should.exist(parsedBody.devices[2].attributes[1].expression); + should.exist(parsedBody.devices[2].attributes[2].reverse); + parsedBody.devices[2].attributes[0].entity_name.should.equal('Higro2000'); + parsedBody.devices[2].attributes[0].entity_type.should.equal('Higrometer'); + parsedBody.devices[2].attributes[1].expression.should.equal('${@humidity * 20}'); + parsedBody.devices[2].attributes[2].reverse.length.should.equal(2); + done(); + }); + }); + }); + describe('When a request for the information about a specific device arrives', function() { + const options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices/Light1', + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + }, + method: 'GET' + }; + + it('should return all the information on that particular device', function(done) { + request(options, function(error, response, body) { + /* jshint camelcase:false */ + should.not.exist(error); + response.statusCode.should.equal(200); + + const parsedBody = JSON.parse(body); + parsedBody.entity_name.should.equal('TheFirstLight'); + parsedBody.device_id.should.equal('Light1'); + done(); + }); + }); + + it('should return the appropriate attribute fields', function(done) { + request(options, function(error, response, body) { + /* jshint camelcase:false */ + should.not.exist(error); + + const parsedBody = JSON.parse(body); + should.exist(parsedBody.attributes[0].object_id); + parsedBody.attributes[0].object_id.should.equal('attr_name'); + parsedBody.attributes[0].name.should.equal('attr_name'); + parsedBody.attributes[0].type.should.equal('string'); + done(); + }); + }); + }); + describe('When a request for a device with plugin attributes arrives', function() { + const options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices/LightFull', + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + }, + method: 'GET' + }; + + it('should return the appropriate attribute fields', function(done) { + request(options, function(error, response, body) { + /* jshint camelcase:false */ + should.not.exist(error); + + const parsedBody = JSON.parse(body); + should.exist(parsedBody.attributes[0].entity_name); + should.exist(parsedBody.attributes[0].entity_type); + should.exist(parsedBody.attributes[1].expression); + should.exist(parsedBody.attributes[2].reverse); + parsedBody.attributes[0].entity_name.should.equal('Higro2000'); + parsedBody.attributes[0].entity_type.should.equal('Higrometer'); + parsedBody.attributes[1].expression.should.equal('${@humidity * 20}'); + parsedBody.attributes[2].reverse.length.should.equal(2); + done(); + }); + }); + }); + describe('When a request for an unexistent device arrives', function() { + const options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices/Light84', + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + }, + method: 'GET' + }; + + it('should return a 404 error', function(done) { + request(options, function(error, response, body) { + should.not.exist(error); + response.statusCode.should.equal(404); + done(); + }); + }); + }); + + describe('When a request for listing all the devices with a limit of 3 arrives', function() { + const options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices?limit=3', + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + }, + method: 'GET' + }; + + function createDeviceRequest(i, callback) { + /* jshint camelcase: false */ + + const provisioningDeviceOptions = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices', + method: 'POST', + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + }, + json: utils.readExampleFile('./test/unit/examples/deviceProvisioningRequests/provisionNewDevice.json') + }; + + provisioningDeviceOptions.json.devices[0].device_id = + provisioningDeviceOptions.json.devices[0].device_id + '_' + i; + + request(provisioningDeviceOptions, callback); + } + + beforeEach(function(done) { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .post('/ngsi-ld/v1/csourceRegistrations/') + .times(10) + .reply(201, null, { Location: '/v2/registrations/6319a7f5254b05844116584d' }); + + // This mock does not check the payload since the aim of the test is not to verify + // device provisioning functionality. Appropriate verification is done in tests under + // provisioning folder + contextBrokerMock + .post('/ngsi-ld/v1/entityOperations/upsert/') + .times(10) + .reply(200); + + iotAgentLib.clearAll(function() { + async.times(10, createDeviceRequest, function(error, results) { + done(); + }); + }); + }); + + it('should return just 3 devices', function(done) { + request(options, function(error, response, body) { + const parsedBody = JSON.parse(body); + should.not.exist(error); + parsedBody.devices.length.should.equal(3); + done(); + }); + }); + + it('should return a count with the complete number of devices', function(done) { + request(options, function(error, response, body) { + const parsedBody = JSON.parse(body); + should.not.exist(error); + parsedBody.count.should.equal(10); + done(); + }); + }); + }); + + describe('When a request for listing all the devices with a offset of 3 arrives', function() { + const options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices?offset=3', + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + }, + method: 'GET' + }; + + function createDeviceRequest(i, callback) { + const provisioningDeviceOptions = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices', + method: 'POST', + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + }, + json: utils.readExampleFile('./test/unit/examples/deviceProvisioningRequests/provisionNewDevice.json') + }; + + provisioningDeviceOptions.json.devices[0].device_id = + provisioningDeviceOptions.json.devices[0].device_id + '_' + i; + + request(provisioningDeviceOptions, function(error, response, body) { + callback(); + }); + } + + beforeEach(function(done) { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .post('/ngsi-ld/v1/csourceRegistrations/') + .times(10) + .reply(201, null, { Location: '/v2/registrations/6319a7f5254b05844116584d' }); + + iotAgentLib.clearAll(function() { + async.timesSeries(10, createDeviceRequest, function(error, results) { + done(); + }); + }); + }); + + it('should skip the first 3 devices', function(done) { + request(options, function(error, response, body) { + const parsedBody = JSON.parse(body); + should.not.exist(error); + + for (let i = 0; i < parsedBody.devices.length; i++) { + ['Light1_0', 'Light1_1', 'Light1_2'].indexOf(parsedBody.devices[i].id).should.equal(-1); + } + + done(); + }); + }); + }); + + describe('When a listing request arrives and there are devices in other service and servicepath', function() { + const options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices', + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + }, + method: 'GET' + }; + + beforeEach(function(done) { + provisioning3Options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices', + method: 'POST', + headers: { + 'fiware-service': 'dumbMordor', + 'fiware-servicepath': '/gardens' + }, + json: utils.readExampleFile( + './test/unit/examples/deviceProvisioningRequests/provisionYetAnotherDevice.json' + ) + }; + + contextBrokerMock + .post('/ngsi-ld/v1/csourceRegistrations/') + .reply(201, null, { Location: '/v2/registrations/6319a7f5254b05844116584d' }); + + request(provisioning3Options, function(error) { + done(); + }); + }); + + it('should return just the ones in the selected service', function(done) { + request(options, function(error, response, body) { + const parsedBody = JSON.parse(body); + should.not.exist(error); + response.statusCode.should.equal(200); + parsedBody.devices.length.should.equal(3); + done(); + }); + }); + }); +}); diff --git a/test/unit/ngsi-ld/provisioning/provisionDeviceMultientity-test.js b/test/unit/ngsi-ld/provisioning/provisionDeviceMultientity-test.js new file mode 100644 index 000000000..9ecd5bac1 --- /dev/null +++ b/test/unit/ngsi-ld/provisioning/provisionDeviceMultientity-test.js @@ -0,0 +1,115 @@ +/* + * Copyright 2020 Telefonica Investigación y Desarrollo, S.A.U + * + * This file is part of fiware-iotagent-lib + * + * fiware-iotagent-lib 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. + * + * fiware-iotagent-lib 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 fiware-iotagent-lib. + * If not, see http://www.gnu.org/licenses/. + * + * For those usages not covered by the GNU Affero General Public License + * please contact with::[contacto@tid.es] + * + * Modified by: Jason Fox - FIWARE Foundation + */ + +const iotAgentLib = require('../../../../lib/fiware-iotagent-lib'); +const utils = require('../../../tools/utils'); + +const should = require('should'); +const nock = require('nock'); +const request = require('request'); +let contextBrokerMock; +const iotAgentConfig = { + logLevel: 'FATAL', + contextBroker: { + host: '192.168.1.1', + port: '1026', + ngsiVersion: 'ld', + jsonLdContext: 'http://context.json-ld' + }, + server: { + port: 4041, + baseRoot: '/' + }, + types: {}, + service: 'smartGondor', + subservice: 'gardens', + providerUrl: 'http://smartGondor.com' +}; + +describe('NGSI-LD - Device provisioning API: Provision devices', function() { + beforeEach(function(done) { + nock.cleanAll(); + + iotAgentLib.activate(iotAgentConfig, function() { + iotAgentLib.clearAll(done); + }); + }); + + afterEach(function(done) { + nock.cleanAll(); + iotAgentLib.setProvisioningHandler(); + iotAgentLib.deactivate(done); + }); + + describe('When a device provisioning request with all the required data arrives to the IoT Agent', function() { + beforeEach(function() { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .post( + '/ngsi-ld/v1/csourceRegistrations/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples' + '/contextAvailabilityRequests/registerProvisionedDevice.json' + ) + ) + .reply(201, null, { Location: '/v2/registrations/6319a7f5254b05844116584d' }); + + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/createProvisionedDeviceMultientity.json' + ) + ) + .reply(200); + }); + + const options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices', + method: 'POST', + json: utils.readExampleFile( + './test/unit/examples/' + 'deviceProvisioningRequests/provisionNewDeviceMultientity.json' + ), + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + } + }; + + it('should add the device to the devices list', function(done) { + request(options, function(error, response, body) { + should.not.exist(error); + response.statusCode.should.equal(201); + + iotAgentLib.listDevices('smartGondor', '/gardens', function(error, results) { + results.devices.length.should.equal(1); + done(); + }); + }); + }); + }); +}); diff --git a/test/unit/ngsi-ld/provisioning/removeProvisionedDevice-test.js b/test/unit/ngsi-ld/provisioning/removeProvisionedDevice-test.js new file mode 100644 index 000000000..2bae09ffc --- /dev/null +++ b/test/unit/ngsi-ld/provisioning/removeProvisionedDevice-test.js @@ -0,0 +1,236 @@ +/* + * Copyright 2020 Telefonica Investigación y Desarrollo, S.A.U + * + * This file is part of fiware-iotagent-lib + * + * fiware-iotagent-lib 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. + * + * fiware-iotagent-lib 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 fiware-iotagent-lib. + * If not, see http://www.gnu.org/licenses/. + * + * For those usages not covered by the GNU Affero General Public License + * please contact with::[contacto@tid.es] + * + * Modified by: Jason Fox - FIWARE Foundation + */ + +const iotAgentLib = require('../../../../lib/fiware-iotagent-lib'); +const utils = require('../../../tools/utils'); +const should = require('should'); +const nock = require('nock'); +const async = require('async'); +const request = require('request'); +let contextBrokerMock; +const iotAgentConfig = { + logLevel: 'FATAL', + contextBroker: { + host: '192.168.1.1', + port: '1026', + ngsiVersion: 'ld', + jsonLdContext: 'http://context.json-ld' + }, + server: { + port: 4041, + baseRoot: '/' + }, + types: {}, + service: 'smartGondor', + subservice: 'gardens', + providerUrl: 'http://smartGondor.com' +}; + +xdescribe('NGSI-LD - Device provisioning API: Remove provisioned devices', function() { + const provisioning1Options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices', + method: 'POST', + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + }, + json: utils.readExampleFile('./test/unit/examples/deviceProvisioningRequests/provisionNewDevice.json') + }; + const provisioning2Options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices', + method: 'POST', + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + }, + json: utils.readExampleFile('./test/unit/examples/deviceProvisioningRequests/provisionAnotherDevice.json') + }; + const provisioning3Options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices', + method: 'POST', + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + }, + json: utils.readExampleFile( + './test/unit/examples/deviceProvisioningRequests/' + 'provisionDeviceActiveAtts.json' + ) + }; + + beforeEach(function(done) { + iotAgentLib.activate(iotAgentConfig, function() { + const nockBody = utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerProvisionedDevice.json' + ); + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .post('/ngsi-ld/v1/csourceRegistrations/', nockBody) + .reply(201, null, { Location: '/v2/registrations/6319a7f5254b05844116584d' }); + + // This mock does not check the payload since the aim of the test is not to verify + // device provisioning functionality. Appropriate verification is done in tests under + // provisioning folder + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post('/ngsi-ld/v1/entityOperations/upsert/') + .reply(200); + + const nockBody2 = utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerProvisionedDevice2.json' + ); + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .post('/ngsi-ld/v1/csourceRegistrations/', nockBody2) + .reply(201, null, { Location: '/v2/registrations/6319a7f5254b05844116584d' }); + + // This mock does not check the payload since the aim of the test is not to verify + // device provisioning functionality. Appropriate verification is done in tests under + // provisioning folder + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post('/ngsi-ld/v1/entityOperations/upsert/') + .reply(200); + + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .delete('/v2/registrations/6319a7f5254b05844116584d') + .reply(204); + + // This mock does not check the payload since the aim of the test is not to verify + // device provisioning functionality. Appropriate verification is done in tests under + // provisioning folder + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post('/ngsi-ld/v1/entityOperations/upsert/') + .reply(200); + + async.series( + [ + iotAgentLib.clearAll, + async.apply(request, provisioning1Options), + async.apply(request, provisioning2Options), + async.apply(request, provisioning3Options) + ], + function(error, results) { + done(); + } + ); + }); + }); + + afterEach(function(done) { + iotAgentLib.deactivate(done); + }); + + describe('When a request to remove a provision device arrives', function() { + const options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices/Light1', + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + }, + method: 'DELETE' + }; + + it('should return a 200 OK and no errors', function(done) { + request(options, function(error, response, body) { + should.not.exist(error); + response.statusCode.should.equal(204); + done(); + }); + }); + + it('should remove the device from the provisioned devices list', function(done) { + request(options, function(error, response, body) { + const options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices', + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + }, + method: 'GET' + }; + + request(options, function(error, response, body) { + const parsedBody = JSON.parse(body); + parsedBody.devices.length.should.equal(2); + done(); + }); + }); + }); + + it('should return a 404 error when asking for the particular device', function(done) { + request(options, function(error, response, body) { + const options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices/Light1', + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + }, + method: 'GET' + }; + + request(options, function(error, response, body) { + should.not.exist(error); + response.statusCode.should.equal(404); + done(); + }); + }); + }); + + it('should call the device remove handler if present', function(done) { + let handlerCalled = false; + + iotAgentLib.setRemoveDeviceHandler(function(device, callback) { + handlerCalled = true; + callback(null, device); + }); + + request(options, function(error, response, body) { + handlerCalled.should.equal(true); + done(); + }); + }); + }); + + describe('When a request to remove a provision device arrives. Device without lazy atts or commands', function() { + const options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices/Light3', + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + }, + method: 'DELETE' + }; + + it('should return a 200 OK and no errors', function(done) { + request(options, function(error, response, body) { + should.not.exist(error); + response.statusCode.should.equal(204); + done(); + }); + }); + }); +}); diff --git a/test/unit/ngsi-ld/provisioning/singleConfigurationMode-test.js b/test/unit/ngsi-ld/provisioning/singleConfigurationMode-test.js new file mode 100644 index 000000000..4696884ab --- /dev/null +++ b/test/unit/ngsi-ld/provisioning/singleConfigurationMode-test.js @@ -0,0 +1,312 @@ +/* + * Copyright 2020 Telefonica Investigación y Desarrollo, S.A.U + * + * This file is part of fiware-iotagent-lib + * + * fiware-iotagent-lib 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. + * + * fiware-iotagent-lib 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 fiware-iotagent-lib. + * If not, see http://www.gnu.org/licenses/. + * + * For those usages not covered by the GNU Affero General Public License + * please contact with::[contacto@tid.es] + * + * Modified by: Jason Fox - FIWARE Foundation + */ + +/* jshint camelcase: false */ + +const iotAgentLib = require('../../../../lib/fiware-iotagent-lib'); +const utils = require('../../../tools/utils'); + +const should = require('should'); +const nock = require('nock'); +let contextBrokerMock; +const request = require('request'); +const iotAgentConfig = { + logLevel: 'FATAL', + contextBroker: { + host: '192.168.1.1', + port: '1026', + ngsiVersion: 'ld', + jsonLdContext: 'http://context.json-ld' + }, + server: { + port: 4041, + baseRoot: '/' + }, + types: {}, + service: 'smartGondor', + singleConfigurationMode: true, + subservice: 'gardens', + providerUrl: 'http://smartGondor.com' +}; +const groupCreation = { + url: 'http://localhost:4041/iot/services', + method: 'POST', + json: utils.readExampleFile('./test/unit/examples/groupProvisioningRequests/provisionFullGroup.json'), + headers: { + 'fiware-service': 'TestService', + 'fiware-servicepath': '/testingPath' + } +}; +const deviceCreation = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices', + method: 'POST', + json: utils.readExampleFile('./test/unit/examples/deviceProvisioningRequests/provisionNewDevice.json'), + headers: { + 'fiware-service': 'TestService', + 'fiware-servicepath': '/testingPath' + } +}; + +describe('NGSI-LD - Provisioning API: Single service mode', function() { + beforeEach(function(done) { + nock.cleanAll(); + + iotAgentLib.activate(iotAgentConfig, function() { + iotAgentLib.clearAll(done); + }); + }); + + afterEach(function(done) { + nock.cleanAll(); + iotAgentLib.setProvisioningHandler(); + iotAgentLib.deactivate(done); + }); + + describe('When a new configuration arrives to an already configured subservice', function() { + const groupCreationDuplicated = { + url: 'http://localhost:4041/iot/services', + method: 'POST', + json: utils.readExampleFile('./test/unit/examples/groupProvisioningRequests/provisionDuplicateGroup.json'), + headers: { + 'fiware-service': 'TestService', + 'fiware-servicepath': '/testingPath' + } + }; + + beforeEach(function(done) { + request(groupCreation, done); + }); + + it('should raise a DUPLICATE_GROUP error', function(done) { + request(groupCreationDuplicated, function(error, response, body) { + should.not.exist(error); + response.statusCode.should.equal(409); + should.exist(body.name); + body.name.should.equal('DUPLICATE_GROUP'); + done(); + }); + }); + }); + describe('When a device is provisioned with an ID that already exists in the configuration', function() { + const deviceCreationDuplicated = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices', + method: 'POST', + json: utils.readExampleFile('./test/unit/examples/deviceProvisioningRequests/provisionDuplicatedDev.json'), + headers: { + 'fiware-service': 'TestService', + 'fiware-servicepath': '/testingPath' + } + }; + + beforeEach(function(done) { + nock.cleanAll(); + + contextBrokerMock = nock('http://unexistentHost:1026') + .matchHeader('fiware-service', 'TestService') + .post('/ngsi-ld/v1/csourceRegistrations/') + .reply(201, null, { Location: '/v2/registrations/6319a7f5254b05844116584d' }); + + // This mock does not check the payload since the aim of the test is not to verify + // device provisioning functionality. Appropriate verification is done in tests under + // provisioning folder + contextBrokerMock + .matchHeader('fiware-service', 'TestService') + .post('/ngsi-ld/v1/entityOperations/upsert/') + .reply(200); + + request(groupCreation, function(error) { + request(deviceCreation, function(error, response, body) { + done(); + }); + }); + }); + + it('should raise a DUPLICATE_DEVICE_ID error', function(done) { + request(deviceCreationDuplicated, function(error, response, body) { + should.not.exist(error); + response.statusCode.should.equal(409); + should.exist(body.name); + body.name.should.equal('DUPLICATE_DEVICE_ID'); + done(); + }); + }); + }); + describe('When a device is provisioned with an ID that exists globally but not in the configuration', function() { + const alternativeDeviceCreation = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices', + method: 'POST', + json: utils.readExampleFile('./test/unit/examples/deviceProvisioningRequests/provisionNewDevice.json'), + headers: { + 'fiware-service': 'AlternateService', + 'fiware-servicepath': '/testingPath' + } + }; + const alternativeGroupCreation = { + url: 'http://localhost:4041/iot/services', + method: 'POST', + json: utils.readExampleFile('./test/unit/examples/groupProvisioningRequests/provisionFullGroup.json'), + headers: { + 'fiware-service': 'AlternateService', + 'fiware-servicepath': '/testingPath' + } + }; + + beforeEach(function(done) { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'TestService') + .post('/ngsi-ld/v1/csourceRegistrations/') + .reply(201, null, { Location: '/v2/registrations/6319a7f5254b05844116584d' }); + + // This mock does not check the payload since the aim of the test is not to verify + // device provisioning functionality. Appropriate verification is done in tests under + // provisioning folder + contextBrokerMock + .matchHeader('fiware-service', 'TestService') + .post('/ngsi-ld/v1/entityOperations/upsert/') + .reply(200); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'AlternateService') + .post('/ngsi-ld/v1/csourceRegistrations/') + .reply(201, null, { Location: '/v2/registrations/6319a7f5254b05844116584d' }); + + // This mock does not check the payload since the aim of the test is not to verify + // device provisioning functionality. Appropriate verification is done in tests under + // provisioning folder + contextBrokerMock + .matchHeader('fiware-service', 'AlternateService') + .post('/ngsi-ld/v1/entityOperations/upsert/') + .reply(200); + + request(groupCreation, function(error) { + request(deviceCreation, function(error, response, body) { + request(alternativeGroupCreation, function(error, response, body) { + done(); + }); + }); + }); + }); + + it('should return a 201 OK', function(done) { + request(alternativeDeviceCreation, function(error, response, body) { + should.not.exist(error); + response.statusCode.should.equal(201); + done(); + }); + }); + }); + describe('When a device is provisioned without a type and with a default configuration type', function() { + const getDevice = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices/Light1', + method: 'GET', + headers: { + 'fiware-service': 'TestService', + 'fiware-servicepath': '/testingPath' + } + }; + let oldType; + + beforeEach(function(done) { + nock.cleanAll(); + + contextBrokerMock = nock('http://unexistentHost:1026') + .matchHeader('fiware-service', 'TestService') + .post('/ngsi-ld/v1/csourceRegistrations/') + .reply(201, null, { Location: '/v2/registrations/6319a7f5254b05844116584d' }); + + // This mock does not check the payload since the aim of the test is not to verify + // device provisioning functionality. Appropriate verification is done in tests under + // provisioning folder + contextBrokerMock + .matchHeader('fiware-service', 'TestService') + .post('/ngsi-ld/v1/entityOperations/upsert/') + .reply(200); + + oldType = deviceCreation.json.devices[0].entity_type; + delete deviceCreation.json.devices[0].entity_type; + request(groupCreation, done); + }); + + afterEach(function() { + deviceCreation.json.devices[0].entity_type = oldType; + }); + + it('should be provisioned with the default type', function(done) { + request(deviceCreation, function(error, response, body) { + request(getDevice, function(error, response, body) { + const parsedBody = JSON.parse(body); + + parsedBody.entity_type.should.equal('SensorMachine'); + + done(); + }); + }); + }); + }); + describe('When a device is provisioned for a configuration', function() { + beforeEach(function(done) { + nock.cleanAll(); + contextBrokerMock = nock('http://unexistentHost:1026') + .matchHeader('fiware-service', 'TestService') + .post( + '/ngsi-ld/v1/csourceRegistrations/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples' + + '/contextAvailabilityRequests/registerProvisionedDeviceWithGroup.json' + ) + ) + .reply(201, null, { Location: '/v2/registrations/6319a7f5254b05844116584d' }); + + contextBrokerMock + .matchHeader('fiware-service', 'TestService') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/createProvisionedDeviceWithGroupAndStatic.json' + ) + ) + .reply(200); + + request(groupCreation, done); + }); + + it('should not raise any error', function(done) { + request(deviceCreation, function(error, response, body) { + should.not.exist(error); + response.statusCode.should.equal(201); + done(); + }); + }); + + it('should send the mixed data to the Context Broker', function(done) { + request(deviceCreation, function(error, response, body) { + contextBrokerMock.done(); + done(); + }); + }); + }); +}); diff --git a/test/unit/ngsi-ld/provisioning/updateProvisionedDevices-test.js b/test/unit/ngsi-ld/provisioning/updateProvisionedDevices-test.js new file mode 100644 index 000000000..d9d04125c --- /dev/null +++ b/test/unit/ngsi-ld/provisioning/updateProvisionedDevices-test.js @@ -0,0 +1,425 @@ +/* + * Copyright 2020 Telefonica Investigación y Desarrollo, S.A.U + * + * This file is part of fiware-iotagent-lib + * + * fiware-iotagent-lib 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. + * + * fiware-iotagent-lib 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 fiware-iotagent-lib. + * If not, see http://www.gnu.org/licenses/. + * + * For those usages not covered by the GNU Affero General Public License + * please contact with::[contacto@tid.es] + * + * Modified by: Jason Fox - FIWARE Foundation + */ + +const iotAgentLib = require('../../../../lib/fiware-iotagent-lib'); +const utils = require('../../../tools/utils'); +const should = require('should'); +const nock = require('nock'); +const async = require('async'); +const request = require('request'); +let contextBrokerMock; +const iotAgentConfig = { + logLevel: 'FATAL', + contextBroker: { + host: '192.168.1.1', + port: '1026', + ngsiVersion: 'ld', + jsonLdContext: 'http://context.json-ld' + }, + server: { + port: 4041, + baseRoot: '/' + }, + types: {}, + service: 'smartGondor', + subservice: 'gardens', + providerUrl: 'http://smartGondor.com' +}; + +xdescribe('NGSI-LD - Device provisioning API: Update provisioned devices', function() { + const provisioning1Options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices', + method: 'POST', + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + }, + json: utils.readExampleFile('./test/unit/examples/deviceProvisioningRequests/provisionNewDevice.json') + }; + const provisioning2Options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices', + method: 'POST', + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + }, + json: utils.readExampleFile('./test/unit/examples/deviceProvisioningRequests/provisionAnotherDevice.json') + }; + const provisioning3Options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices', + method: 'POST', + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + }, + json: utils.readExampleFile('./test/unit/examples/deviceProvisioningRequests/provisionMinimumDevice2.json') + }; + + beforeEach(function(done) { + nock.cleanAll(); + iotAgentLib.activate(iotAgentConfig, function() { + const nockBody = utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerProvisionedDevice.json' + ); + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .post('/ngsi-ld/v1/csourceRegistrations/', nockBody) + .reply(201, null, { Location: '/v2/registrations/6319a7f5254b05844116584d' }); + + // This mock does not check the payload since the aim of the test is not to verify + // device provisioning functionality. Appropriate verification is done in tests under + // provisioning folder + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post('/ngsi-ld/v1/entityOperations/upsert/') + .reply(200); + + const nockBody2 = utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerProvisionedDevice2.json' + ); + nockBody2.expires = /.+/i; + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post('/ngsi-ld/v1/csourceRegistrations/', nockBody2) + .reply(201, null, { Location: '/v2/registrations/6719a7f5254b058441165849' }); + + // This mock does not check the payload since the aim of the test is not to verify + // device provisioning functionality. Appropriate verification is done in tests under + // provisioning folder + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post('/ngsi-ld/v1/entityOperations/upsert/') + .reply(200); + + // FIXME: When https://github.com/telefonicaid/fiware-orion/issues/3007 is merged into master branch, + // this function should use the new API. This is just a temporary solution which implies deleting the + // registration and creating a new one. + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .delete('/v2/registrations/6719a7f5254b058441165849') + .reply(204); + + const nockBody3 = utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextAvailabilityRequests/updateIoTAgent2.json' + ); + nockBody3.expires = /.+/i; + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post('/ngsi-ld/v1/csourceRegistrations/', nockBody3) + .reply(201, null, { Location: '/v2/registrations/4419a7f5254b058441165849' }); + + async.series( + [ + iotAgentLib.clearAll, + async.apply(request, provisioning1Options), + async.apply(request, provisioning2Options) + ], + done + ); + }); + }); + + afterEach(function(done) { + iotAgentLib.clearAll(function() { + iotAgentLib.deactivate(done); + }); + }); + + describe('When a request to update a provision device arrives', function() { + const optionsUpdate = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices/Light1', + method: 'PUT', + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + }, + json: utils.readExampleFile('./test/unit/examples/deviceProvisioningRequests/updateProvisionDevice.json') + }; + + beforeEach(function() { + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post('/ngsi-ld/v1/entities/TheFirstLight/attrs?type=TheLightType', {}) + .reply(204); + + // FIXME: When https://github.com/telefonicaid/fiware-orion/issues/3007 is merged into master branch, + // this function should use the new API. This is just a temporary solution which implies deleting the + // registration and creating a new one. + + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .delete('/v2/registrations/6319a7f5254b05844116584d') + .reply(204); + + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post( + '/ngsi-ld/v1/csourceRegistrations/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextAvailabilityRequests/updateIoTAgent2.json' + ) + ) + .reply(201, null, { Location: '/v2/registrations/4419a7f5254b058441165849' }); + + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post( + '/ngsi-ld/v1/csourceRegistrations/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextAvailabilityRequests/updateIoTAgent3.json' + ) + ) + .reply(201, null, { Location: '/v2/registrations/4419a7f52546658441165849' }); + }); + + it('should return a 200 OK and no errors', function(done) { + request(optionsUpdate, function(error, response, body) { + should.not.exist(error); + response.statusCode.should.equal(204); + done(); + }); + }); + + it('should have updated the data when asking for the particular device', function(done) { + request(optionsUpdate, function(error, response, body) { + const options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices/Light1', + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + }, + method: 'GET' + }; + + request(options, function(error, response, body) { + /* jshint camelcase:false */ + + const parsedBody = JSON.parse(body); + parsedBody.entity_name.should.equal('ANewLightName'); + parsedBody.timezone.should.equal('Europe/Madrid'); + done(); + }); + }); + }); + + it('should not modify the attributes not present in the update request', function(done) { + request(optionsUpdate, function(error, response, body) { + const options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices/Light1', + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + }, + method: 'GET' + }; + + request(options, function(error, response, body) { + /* jshint camelcase:false */ + + const parsedBody = JSON.parse(body); + parsedBody.entity_type.should.equal('TheLightType'); + parsedBody.service.should.equal('smartGondor'); + done(); + }); + }); + }); + }); + describe('When an update request arrives with a new Device ID', function() { + const optionsUpdate = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices/Light1', + method: 'PUT', + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + }, + json: utils.readExampleFile( + './test/unit/examples/deviceProvisioningRequests/updateProvisionDeviceWithId.json' + ) + }; + + it('should raise a 400 error', function(done) { + request(optionsUpdate, function(error, response, body) { + should.not.exist(error); + response.statusCode.should.equal(400); + done(); + }); + }); + }); + describe('When a wrong update request payload arrives', function() { + const optionsUpdate = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices/Light1', + method: 'PUT', + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + }, + json: utils.readExampleFile( + './test/unit/examples/deviceProvisioningRequests/updateProvisionDeviceWrong.json' + ) + }; + + it('should raise a 400 error', function(done) { + request(optionsUpdate, function(error, response, body) { + should.not.exist(error); + response.statusCode.should.equal(400); + done(); + }); + }); + }); + + describe('When a device is provisioned without attributes and new ones are added through an update', function() { + const optionsUpdate = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices/MicroLight2', + method: 'PUT', + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + }, + json: utils.readExampleFile('./test/unit/examples/deviceProvisioningRequests/updateMinimumDevice.json') + }; + const optionsGetDevice = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices/MicroLight2', + method: 'GET', + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + } + }; + + beforeEach(function(done) { + nock.cleanAll(); + + // This mock does not check the payload since the aim of the test is not to verify + // device provisioning functionality. Appropriate verification is done in tests under + // provisioning folder + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .post('/ngsi-ld/v1/entityOperations/upsert/') + .reply(200); + + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post( + '/ngsi-ld/v1/entities/SecondMicroLight/attrs?type=MicroLights', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateProvisionMinimumDevice.json' + ) + ) + .reply(204); + + async.series([iotAgentLib.clearAll, async.apply(request, provisioning3Options)], done); + }); + + it('should not raise any error', function(done) { + request(optionsUpdate, function(error, response, body) { + should.not.exist(error); + response.statusCode.should.equal(204); + done(); + }); + }); + it('should provision the attributes appropriately', function(done) { + request(optionsUpdate, function(error, response, body) { + request(optionsGetDevice, function(error, response, body) { + should.not.exist(error); + response.statusCode.should.equal(200); + + const parsedBody = JSON.parse(body); + + parsedBody.attributes.length.should.equal(1); + parsedBody.attributes[0].name.should.equal('newAttribute'); + done(); + }); + }); + }); + it('should create the initial values for the attributes in the Context Broker', function(done) { + request(optionsUpdate, function(error, response, body) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When a device is updated to add static attributes', function() { + /* jshint camelcase: false */ + + const optionsUpdate = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices/MicroLight2', + method: 'PUT', + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + }, + json: utils.readExampleFile('./test/unit/examples/deviceProvisioningRequests/updateDeviceStatic.json') + }; + const optionsGetDevice = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices/MicroLight2', + method: 'GET', + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': '/gardens' + } + }; + + beforeEach(function(done) { + nock.cleanAll(); + + // This mock does not check the payload since the aim of the test is not to verify + // device provisioning functionality. Appropriate verification is done in tests under + // provisioning folder + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .post('/ngsi-ld/v1/entityOperations/upsert/') + .reply(200); + + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post( + '/ngsi-ld/v1/entities/SecondMicroLight/attrs?type=MicroLights', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateProvisionDeviceStatic.json' + ) + ) + .reply(204); + + async.series([iotAgentLib.clearAll, async.apply(request, provisioning3Options)], done); + }); + + it('should provision the attributes appropriately', function(done) { + request(optionsUpdate, function(error, response, body) { + request(optionsGetDevice, function(error, response, body) { + should.not.exist(error); + response.statusCode.should.equal(200); + + const parsedBody = JSON.parse(body); + + parsedBody.static_attributes.length.should.equal(3); + parsedBody.static_attributes[0].name.should.equal('cellID'); + done(); + }); + }); + }); + }); +});