diff --git a/CHANGES_NEXT_RELEASE b/CHANGES_NEXT_RELEASE index f9a3a1d1f..dee2a9af6 100644 --- a/CHANGES_NEXT_RELEASE +++ b/CHANGES_NEXT_RELEASE @@ -1,2 +1,3 @@ +- Add NGSI-LD Merge-Patch Support Handling #1283 - Update to offer NGSI-LD 1.6.1. Registrations #1302 - Document removal of support for NGSI-LD 1.3.1 Interface diff --git a/config.js b/config.js index 7cfde892d..49eb3a26a 100644 --- a/config.js +++ b/config.js @@ -29,7 +29,12 @@ var config = { }, server: { port: 4041, - host: '0.0.0.0' + host: '0.0.0.0', + ldSupport : { + null: true, + datasetId: true, + merge: false + } }, authentication: { enabled: true, diff --git a/doc/howto.md b/doc/howto.md index 13df1ccca..55e1fdace 100644 --- a/doc/howto.md +++ b/doc/howto.md @@ -430,6 +430,14 @@ iotAgentLib.setDataUpdateHandler(updateContextHandler); iotAgentLib.setDataQueryHandler(queryContextHandler); ``` +Where necessary, additional handlers to deal with command actuations and merge-patch operations may also be added when +necessary. + +```javascript +iotAgentLib.setCommandHandler(commandHandler); +iotAgentLib.setMergePatchHandler(mergePatchHandler); +``` + #### IOTA Testing In order to test it, we need to create an HTTP server simulating the device. The quickest way to do that may be using diff --git a/doc/installationguide.md b/doc/installationguide.md index 884620250..b2969de7f 100644 --- a/doc/installationguide.md +++ b/doc/installationguide.md @@ -72,6 +72,22 @@ overriding the group setting. } ``` +When connected to an **NGSI-LD** context broker, an IoT Agent is able to indicate whether it is willing to accept `null` +values and also whether it is able to process the **NGSI-LD** `datasetId` metadata element. Setting these +values to `false` will cause the IoT Agent to return a 400 **Bad Request** HTTP status code explaining that the IoT +Agent does not support nulls or multi-attribute requests if they are encountered. + +```javascript +{ + baseRoot: '/', + port: 4041, + ldSupport : { + null: true, + datasetId: true + } +} +``` + - **stats**: configure the periodic collection of statistics. Use `interval` in milliseconds to set the time between stats writings. @@ -301,6 +317,8 @@ overrides. | IOTA_CB_NGSI_VERSION | `contextBroker.ngsiVersion` | | IOTA_NORTH_HOST | `server.host` | | IOTA_NORTH_PORT | `server.port` | +| IOTA_LD_SUPPORT_NULL | `server.ldSupport.null` | +| IOTA_LD_SUPPORT_DATASET_ID | `server.ldSupport.datasetId` | | IOTA_PROVIDER_URL | `providerUrl` | | IOTA_AUTH_ENABLED | `authentication.enabled` | | IOTA_AUTH_TYPE | `authentication.type` | diff --git a/doc/usermanual.md b/doc/usermanual.md index 108ae07da..843f6f77a 100644 --- a/doc/usermanual.md +++ b/doc/usermanual.md @@ -354,6 +354,83 @@ values. The handler is expected to call its callback once with no parameters (failing to do so may cause unexpected behaviors in the IoT Agent). +##### iotagentLib.setCommandHandler() + +###### Signature + +```javascript +function setCommandHandler(newHandler) +``` + +###### Description + +Sets the new user handler for registered entity commands. This handler will be called whenever a command request arrives, with +the following parameters: (`id`, `type`, `service`, `subservice`, `attributes`, `callback`). The handler must retrieve +all the corresponding information from the devices and return a NGSI entity with the requested values. + +The callback must be invoked with the updated Context Element, using the information retrieved from the devices. E.g.: + +```javascript +callback(null, { + type: "TheType", + isPattern: false, + id: "EntityID", + attributes: [ + { + name: "lumniscence", + type: "Lumens", + value: "432" + } + ] +}); +``` + +In the case of NGSI requests affecting multiple entities, this handler will be called multiple times, one for each +entity, and all the results will be combined into a single response. Only IoT Agents which deal with actuator devices will include a handler for commands. + +###### Params + +- newHandler: User handler for command requests. + +##### iotagentLib.setMergePatchHandler() + +###### Signature + +```javascript +function setMergePatchHandler(newHandler) +``` + +###### Description + +Sets the new user handler for NGSI-LD Entity [merge-patch](https://datatracker.ietf.org/doc/html/rfc7386) requests. This handler will be called whenever a merge-patch request arrives, with +the following parameters: (`id`, `type`, `service`, `subservice`, `attributes`, `callback`). The handler must retrieve +all the corresponding information from the devices and return a NGSI entity with the requested values. + +The callback must be invoked with the updated Context Element, using the information retrieved from the devices. E.g.: + +```javascript +callback(null, { + type: "TheType", + isPattern: false, + id: "EntityID", + attributes: [ + { + name: "lumniscence", + type: "Lumens", + value: "432" + } + ] +}); +``` + +In the case of NGSI-LD requests affecting multiple entities, this handler will be +called multiple times. Since merge-patch is an advanced function, not all IoT Agents +will include a handler for merge-patch. + +###### Params + +- newHandler: User handler for merge-patch requests. + ##### iotagentLib.setProvisioningHandler() ###### Signature diff --git a/lib/commonConfig.js b/lib/commonConfig.js index 5dc23c76f..2fbed2e70 100644 --- a/lib/commonConfig.js +++ b/lib/commonConfig.js @@ -158,7 +158,9 @@ function processEnvironmentVariables() { 'IOTA_DEFAULT_ENTITY_NAME_CONJUNCTION', 'IOTA_JSON_LD_CONTEXT', 'IOTA_FALLBACK_TENANT', - 'IOTA_FALLBACK_PATH' + 'IOTA_FALLBACK_PATH', + 'IOTA_LD_SUPPORT_NULL', + 'IOTA_LD_SUPPORT_DATASET_ID' ]; const iotamVariables = [ 'IOTA_IOTAM_URL', @@ -263,6 +265,18 @@ function processEnvironmentVariables() { config.server.port = process.env.IOTA_NORTH_PORT; } + config.server.ldSupport = config.server.ldSupport || {null: true, datasetId: true, merge: false}; + + if (process.env.IOTA_LD_SUPPORT_NULL) { + config.server.ldSupport.null = process.env.IOTA_LD_SUPPORT_NULL === 'true'; + } + if (process.env.IOTA_LD_SUPPORT_DATASET_ID) { + config.server.ldSupport.datasetId = process.env.IOTA_LD_SUPPORT_DATASET_ID === 'true'; + } + if (process.env.IOTA_LD_SUPPORT_MERGE) { + config.server.ldSupport.datasetId = process.env.IOTA_LD_SUPPORT_MERGE === 'true'; + } + if (process.env.IOTA_PROVIDER_URL) { config.providerUrl = process.env.IOTA_PROVIDER_URL; } diff --git a/lib/constants.js b/lib/constants.js index dd38aa93b..b9b570274 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -60,6 +60,7 @@ module.exports = { SUBSERVICE_HEADER: 'fiware-servicepath', NGSI_LD_TENANT_HEADER: 'NGSILD-Tenant', NGSI_LD_PATH_HEADER: 'NGSILD-Path', + NGSI_LD_NULL: 'urn:ngsi-ld:null', //FIXME: check Keystone support this in lowercase, then change AUTH_HEADER: 'X-Auth-Token', X_FORWARDED_FOR_HEADER: 'x-forwarded-for', diff --git a/lib/fiware-iotagent-lib.js b/lib/fiware-iotagent-lib.js index cff647468..2d92bf984 100644 --- a/lib/fiware-iotagent-lib.js +++ b/lib/fiware-iotagent-lib.js @@ -323,6 +323,7 @@ exports.getConfigurationSilently = groupConfig.getSilently; exports.findConfiguration = groupConfig.find; exports.setDataUpdateHandler = contextServer.setUpdateHandler; exports.setCommandHandler = contextServer.setCommandHandler; +exports.setMergePatchHandler = contextServer.setMergePatchHandler; exports.setDataQueryHandler = contextServer.setQueryHandler; exports.setConfigurationHandler = contextServer.setConfigurationHandler; exports.setRemoveConfigurationHandler = contextServer.setRemoveConfigurationHandler; diff --git a/lib/services/devices/deviceService.js b/lib/services/devices/deviceService.js index a5a1a2ac9..c5f7ffd2b 100644 --- a/lib/services/devices/deviceService.js +++ b/lib/services/devices/deviceService.js @@ -363,6 +363,7 @@ function registerDevice(deviceObj, callback) { deviceObj.type = deviceData.type; deviceObj.staticAttributes = deviceData.staticAttributes; deviceObj.commands = deviceData.commands; + deviceObj.lazy = deviceData.lazy; if ('timestamp' in deviceData && deviceData.timestamp !== undefined) { deviceObj.timestamp = deviceData.timestamp; } diff --git a/lib/services/devices/registrationUtils.js b/lib/services/devices/registrationUtils.js index 7755fb921..10bbf48fe 100644 --- a/lib/services/devices/registrationUtils.js +++ b/lib/services/devices/registrationUtils.js @@ -314,6 +314,7 @@ function sendRegistrationsNgsiLD(unregister, deviceData, callback) { const properties = []; const lazy = deviceData.lazy || []; const commands = deviceData.commands || []; + const supportMerge = config.getConfig().server.ldSupport.merge; lazy.forEach((element) => { properties.push(element.name); @@ -328,6 +329,9 @@ function sendRegistrationsNgsiLD(unregister, deviceData, callback) { if (commands.length > 0){ operations.push('updateOps'); } + if (supportMerge){ + operations.push('mergeEntity'); + } if (properties.length === 0) { logger.debug(context, 'Registration with Context Provider is not needed. Device without lazy atts or commands'); diff --git a/lib/services/northBound/contextServer-NGSI-LD.js b/lib/services/northBound/contextServer-NGSI-LD.js index fbb71e62d..a1016b9b8 100644 --- a/lib/services/northBound/contextServer-NGSI-LD.js +++ b/lib/services/northBound/contextServer-NGSI-LD.js @@ -31,6 +31,7 @@ const async = require('async'); const apply = async.apply; const logger = require('logops'); const errors = require('../../errors'); +const constants = require('../../constants'); const deviceService = require('../devices/deviceService'); const middlewares = require('../common/genericMiddleware'); const _ = require('underscore'); @@ -41,11 +42,98 @@ const updateContextTemplateNgsiLD = require('../../templates/updateContextNgsiLD const notificationTemplateNgsiLD = require('../../templates/notificationTemplateNgsiLD.json'); const contextServerUtils = require('./contextServerUtils'); const ngsiLD = require('../ngsi/entities-NGSI-LD'); +const config = require('../../commonConfig'); const overwritePaths = ['/ngsi-ld/v1/entities/:entity/attrs', '/ngsi-ld/v1/entities/:entity/attrs/:attr']; const updatePaths = ['/ngsi-ld/v1/entities/:entity/attrs', '/ngsi-ld/v1/entities/:entity/attrs/:attr']; const queryPaths = ['/ngsi-ld/v1/entities/:entity']; + +/** + * Replacement of NGSI-LD Null placeholders with real null values + * + */ +function replaceNGSILDNull(payload){ + Object.keys(payload).forEach((key) =>{ + const value = payload[key]; + if ( value === constants.NGSI_LD_NULL){ + payload[key] = null; + } else if (typeof value === 'object' && + !Array.isArray(value) && + value !== null){ + payload[key] = replaceNGSILDNull(payload[key]); + } + }) + return payload; +} + +/** + * Check to see if the payload or its subattributes contain null values + * + */ +function containsNulls(payload, result){ + Object.keys(payload).forEach((key) =>{ + const value = payload[key]; + if ( value === null){ + result.nulls = true; + } else if (typeof value === 'object' && + !Array.isArray(value) && + value !== null){ + containsNulls(payload[key], result); + } + }) + return result; +} + +/** + * An Express middleware for preprocessing NGSI-LD payloads. Converts NGSI-LD Nulls + * to real nulls and checks for the presence of null and datasetId + * + */ +function preprocessNGSILD(req, res, next){ + res.locals.hasDatasetId = false; + const payload = req.body + if (payload && typeof payload === 'object'){ + Object.keys(payload).forEach((key) =>{ + if (_.isArray(payload[key])){ + payload[key].forEach((obj) => { + if (obj.datasetId){ + res.locals.hasDatasetId = true; + } + }); + } else if (payload[key] && payload[key].datasetId && payload[key].datasetId !== '@none'){ + res.locals.hasDatasetId = true; + } + }); + req.body = replaceNGSILDNull(payload); + const result = { nulls: false } + containsNulls(payload, result); + res.locals.hasNulls = result.nulls; + } + next(); +} + +/** + * A configurable Middleware that makes additional NGSI-LD checks within the payload. + + * + * @param {Boolean} supportNull Whether to support NGSI-LD nulls in the payload + * @param {Boolean} supportDatasetId Whether to support multiattributes in the payload. + * @return {Object} Express middleware used in request validation. + */ +function validateNGSILD(supportNull, supportDatasetId) { + return function validate(req, res, next) { + if (!supportNull && res.locals.hasNulls) { + next(new errors.BadRequest('NGSI-LD Null found within the payload. This IoT Agent does not support nulls for this endpoint.')); + } else if (!supportDatasetId && res.locals.hasDatasetId) { + next(new errors.BadRequest('datasetId found within the payload. This IoT Agent does not support multi-attribute requests.')); + } else { + next(); + } + }; +} + + /** * Extract metadata attributes from input. * @@ -318,6 +406,104 @@ function defaultQueryHandlerNgsiLD(id, type, service, subservice, attributes, ca }); } +/** + * Generate a merge-patch action corresponding to the request using NGSI-LD. + * Merge-patch is an NGSI-LD specific action. + * + * @param {Object} req Update request to generate Actions from + */ +function generateMergePatchActionNgsiLD(req, callback) { + + const entityId = req.params.entity; + + + function addAttributes(deviceData, body, attributes){ + const keys = Object.keys(body); + + for (const j in deviceData) { + if (keys.includes(deviceData[j].name)) { + const obj = body[deviceData[j].name] + if ( obj === null) { + attributes.push({ + type: deviceData[j].type, + value: null, + name: deviceData[j].name + }); + } else { + attributes.push({ + type: deviceData[j].type, + value: obj.value, + name: deviceData[j].name + }); + } + } + } + return attributes; + } + + + deviceService.getDeviceByName( + entityId, + contextServerUtils.getLDTenant(req), + contextServerUtils.getLDPath(req), + function (error, deviceObj) { + if (error) { + callback(error); + } else { + const attributes = []; + addAttributes(deviceObj.commands, req.body, attributes) + addAttributes(deviceObj.lazy, req.body, attributes) + const executeMergePatchHandler = apply( + contextServerUtils.mergePatchHandler, + entityId, + deviceObj.type, + contextServerUtils.getLDTenant(req), + contextServerUtils.getLDPath(req), + attributes + ); + async.waterfall( + [executeMergePatchHandler], + callback() + ); + } + } + ); +} + +/** + * Express middleware to manage incoming merge-patch requests using NGSI-LD. + * + * @param {Object} req Request that was handled in first place. + * @param {Object} res Response that will be sent. + */ +function handleMergePatchNgsiLD(req, res, next) { + + function handleMergePatchRequest(error, result) { + if (error) { + logger.debug(context, 'There was an error handling the merge-patch: %s.', error); + next(error); + } else { + logger.debug(context, 'Merge-patch from [%s] handled successfully.', req.get('host')); + res.status(200).json(result); + } + } + + logger.debug(context, 'Handling merge-patch from [%s]', req.get('host')); + if ((req.is('json') || req.is('application/ld+json')) === false) { + return handleMergePatchRequest(new errors.UnsupportedContentType(req.header('content-type'))); + } + + if (req.body) { + logger.debug(context, JSON.stringify(req.body, null, 4)); + } + + if (contextServerUtils.mergePatchHandler){ + generateMergePatchActionNgsiLD(req, handleMergePatchRequest); + } else { + return handleMergePatchRequest(new errors.MethodNotSupported(req.method, req.path)) + } +} + /** * Express middleware to manage incoming query context requests using NGSI-LD. * @@ -489,19 +675,21 @@ function handleQueryNgsiLD(req, res, next) { * @param {Object} req Request that was handled in first place. * @param {Object} res Response that will be sent. */ -function queryErrorHandlingNgsiLD(error, req, res, next) { - let code = 500; +function ErrorHandlingNgsiLD(action) { + return function errorHandle(error, req, res, next) { + let code = 500; - logger.debug(context, 'Query NGSI-LD error [%s] handling request: %s', error.name, error.message); + logger.debug(context, action + ' NGSI-LD error [%s] handling request: %s', error.name, error.message); - if (error.code && String(error.code).match(/^[2345]\d\d$/)) { - code = error.code; - } + 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, '') - }); + res.status(code).json({ + error: error.name, + description: error.message.replace(/[<>\"\'=;\(\)]/g, '') + }); + } } /** @@ -601,28 +789,6 @@ function handleNotificationNgsiLD(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) { - let 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 unsupported NGSI-LD entity routes and return proper NGSI-LD not supported responses * @@ -645,36 +811,53 @@ function loadUnsupportedEndpointsNGSILD(router) { */ function loadContextRoutesNGSILD(router) { // In a more evolved implementation, more endpoints could be added to queryPathsNgsi2 - // according to http://fiware.github.io/specifications/ngsiv2/stable. - + // according to https://www.etsi.org/standards-search#page=1&search=GS%20CIM%20009 + + const support = config.getConfig().server.ldSupport; let i; logger.info(context, 'Loading NGSI-LD Context server routes'); + // Update Patch endpoints - this endpoint may accept NGSI-LD Null for (i = 0; i < updatePaths.length; i++) { router.patch(updatePaths[i], [ middlewares.ensureType, middlewares.validateJson(updateContextTemplateNgsiLD), + preprocessNGSILD, + validateNGSILD(support.null, support.datasetId), handleUpdateNgsiLD, - updateErrorHandlingNgsiLD + ErrorHandlingNgsiLD('Partial Update') ]); } + // Merge Patch endpoints - this endpoint may accept NGSI-LD Null + router.patch('/ngsi-ld/v1/entities/:entity', [ + preprocessNGSILD, + validateNGSILD(support.null, support.datasetId), + handleMergePatchNgsiLD, + ErrorHandlingNgsiLD('Merge-Patch') + ]); + // Overwrite/PUT endpoints - this endpoint does not accept NGSI-LD Null for (i = 0; i < overwritePaths.length; i++) { router.put(overwritePaths[i], [ middlewares.ensureType, middlewares.validateJson(updateContextTemplateNgsiLD), + preprocessNGSILD, + validateNGSILD(false, support.datasetId), handleUpdateNgsiLD, - updateErrorHandlingNgsiLD + ErrorHandlingNgsiLD('Overwrite') ]); } + // Query/GET endpoints - no payload to check. for (i = 0; i < queryPaths.length; i++) { - router.get(queryPaths[i], [handleQueryNgsiLD, queryErrorHandlingNgsiLD]); + router.get(queryPaths[i], [handleQueryNgsiLD, ErrorHandlingNgsiLD('Query')]); } router.post('/notify', [ middlewares.ensureType, middlewares.validateJson(notificationTemplateNgsiLD), + preprocessNGSILD, + validateNGSILD(false, support.datasetId), handleNotificationNgsiLD, - queryErrorHandlingNgsiLD + ErrorHandlingNgsiLD('Notify') ]); loadUnsupportedEndpointsNGSILD(router); } diff --git a/lib/services/northBound/contextServer.js b/lib/services/northBound/contextServer.js index a84c7b119..f328e6730 100644 --- a/lib/services/northBound/contextServer.js +++ b/lib/services/northBound/contextServer.js @@ -64,7 +64,7 @@ function setUpdateHandler(newHandler) { } /** - * Sets the new user handler for commadn execution requests. This handler will be called whenever an update request + * Sets the new user handler for command execution requests. This handler will be called whenever an update request * arrives to a with the following parameters: (id, type, attributes, callback). The callback is in charge of updating * the corresponding values in the devices with the appropriate protocol. * @@ -77,6 +77,18 @@ function setCommandHandler(newHandler) { contextServerUtils.commandHandler = newHandler; } +/** + * Sets the new user handler for NGSI-LD merge-patch requests. This handler will be called whenever an update request + * arrives to a with the following parameters: (id, type, attributes, callback). The callback is in charge of updating + * the corresponding values in the devices with the appropriate protocol. + * + * + * @param {Function} newHandler User handler for update requests + */ +function setMergePatchHandler(newHandler) { + contextServerUtils.mergePatchHandler = newHandler; +} + /** * Sets the new user handler for Entity query requests. This handler will be called whenever an update request arrives * with the following parameters: (id, type, attributes, callback). The handler must retrieve all the corresponding @@ -140,6 +152,7 @@ function clear(callback) { exports.clear = clear; exports.loadContextRoutes = intoTrans(context, loadContextRoutes); exports.setUpdateHandler = intoTrans(context, setUpdateHandler); +exports.setMergePatchHandler = intoTrans(context, setMergePatchHandler); exports.setCommandHandler = intoTrans(context, setCommandHandler); exports.setNotificationHandler = intoTrans(context, setNotificationHandler); exports.addNotificationMiddleware = intoTrans(context, addNotificationMiddleware); diff --git a/lib/services/northBound/northboundServer.js b/lib/services/northBound/northboundServer.js index 63c5a1497..5ed44f8c4 100644 --- a/lib/services/northBound/northboundServer.js +++ b/lib/services/northBound/northboundServer.js @@ -110,6 +110,7 @@ function clear(callback) { async.series([deviceProvisioning.clear, groupProvisioning.clear, contextServer.clear], callback); } +exports.setMergePatchHandler = intoTrans(context, contextServer.setMergePatchHandler); exports.setUpdateHandler = intoTrans(context, contextServer.setUpdateHandler); exports.setQueryHandler = intoTrans(context, contextServer.setQueryHandler); exports.setCommandHandler = intoTrans(context, contextServer.setCommandHandler); diff --git a/test/unit/memoryRegistry/deviceRegistryMemory_test.js b/test/unit/memoryRegistry/deviceRegistryMemory_test.js index beab5b521..0d3f60185 100644 --- a/test/unit/memoryRegistry/deviceRegistryMemory_test.js +++ b/test/unit/memoryRegistry/deviceRegistryMemory_test.js @@ -241,7 +241,7 @@ describe('NGSI-v2 - In memory device registry', function () { should.exist(device.type); device.name.should.equal('name5'); device.type.should.equal('Light5'); - Object.keys(device).length.should.equal(11); + Object.keys(device).length.should.equal(12); done(); }); }); diff --git a/test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerIoTAgentCommandsAndLazy.json b/test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerIoTAgentCommandsAndLazy.json new file mode 100644 index 000000000..9f8b78ab9 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerIoTAgentCommandsAndLazy.json @@ -0,0 +1,32 @@ +{ + "type": "ContextSourceRegistration", + "information": [ + { + "entities": [ + { + "type": "Robot", + "id": "urn:ngsi-ld:Robot:r2d2" + } + ], + "propertyNames": [ + "batteryLevel", + "position", + "orientation" + ] + } + ], + "mode": "exclusive", + "operations": [ + "retrieveOps", + "updateOps", + "mergeEntity" + ], + "endpoint": "http://smartgondor.com", + "contextSourceInfo": [ + { + "key": "jsonldContext", + "value": "http://context.json-ld" + } + ], + "@context": "http://context.json-ld" +} diff --git a/test/unit/ngsi-ld/general/startup-test.js b/test/unit/ngsi-ld/general/startup-test.js index 9cbd5dd04..0cd81d683 100644 --- a/test/unit/ngsi-ld/general/startup-test.js +++ b/test/unit/ngsi-ld/general/startup-test.js @@ -66,7 +66,7 @@ describe('NGSI-LD - Startup tests', 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_CB_NGSI_VERSION = 'ld'; process.env.IOTA_NORTH_HOST = 'localhost'; process.env.IOTA_NORTH_PORT = '2222'; process.env.IOTA_PROVIDER_URL = 'provider:3333'; @@ -83,6 +83,11 @@ describe('NGSI-LD - Startup tests', function () { process.env.IOTA_MONGO_DB = 'themongodb'; process.env.IOTA_MONGO_REPLICASET = 'customReplica'; process.env.IOTA_DEFAULT_RESOURCE = '/iot/custom'; + process.env.IOTA_JSON_LD_CONTEXT = 'http://context.jsonld'; + process.env.IOTA_FALLBACK_TENANT = 'openiot'; + process.env.IOTA_FALLBACK_PATH = 'smartgondor'; + process.env.IOTA_LD_SUPPORT_NULL = 'false'; + process.env.IOTA_LD_SUPPORT_DATASET_ID = 'false'; nock.cleanAll(); @@ -114,6 +119,11 @@ describe('NGSI-LD - Startup tests', function () { delete process.env.IOTA_MONGO_DB; delete process.env.IOTA_MONGO_REPLICASET; delete process.env.IOTA_DEFAULT_RESOURCE; + delete process.env.IOTA_JSON_LD_CONTEXT; + delete process.env.IOTA_FALLBACK_TENANT; + delete process.env.IOTA_FALLBACK_PATH; + delete process.env.IOTA_LD_SUPPORT_NULL; + delete process.env.IOTA_LD_SUPPORT_DATASET_ID; }); afterEach(function (done) { @@ -123,7 +133,12 @@ describe('NGSI-LD - Startup tests', function () { 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().contextBroker.ngsiVersion.should.equal('ld'); + config.getConfig().contextBroker.jsonLdContext.should.equal('http://context.jsonld'); + config.getConfig().contextBroker.fallbackTenant.should.equal( 'openiot'); + config.getConfig().contextBroker.fallbackPath.should.equal('smartgondor'); + config.getConfig().server.ldSupport.null.should.equal(false); + config.getConfig().server.ldSupport.datasetId.should.equal(false); config.getConfig().server.host.should.equal('localhost'); config.getConfig().server.port.should.equal('2222'); config.getConfig().providerUrl.should.equal('provider:3333'); diff --git a/test/unit/ngsi-ld/lazyAndCommands/command-test.js b/test/unit/ngsi-ld/lazyAndCommands/command-test.js index 7656b8d92..c1724b0d4 100644 --- a/test/unit/ngsi-ld/lazyAndCommands/command-test.js +++ b/test/unit/ngsi-ld/lazyAndCommands/command-test.js @@ -184,7 +184,6 @@ describe('NGSI-LD - Command functionalities', function () { }; beforeEach(function (done) { - logger.setLevel('ERROR'); iotAgentLib.register(device3, function (error) { done(); }); @@ -287,7 +286,6 @@ describe('NGSI-LD - Command functionalities', function () { }; beforeEach(function (done) { - logger.setLevel('ERROR'); iotAgentLib.register(device3, function (error) { done(); }); @@ -368,7 +366,7 @@ describe('NGSI-LD - Command functionalities', function () { describe('When a sequential command with datasetId updates via PATCH /attrs/attr-name arrives to the IoT Agent', function () { const options = { url: 'http://localhost:' + iotAgentConfig.server.port + '/ngsi-ld/v1/entities/urn:ngsi-ld:Robot:r2d2/attrs', - method: 'PUT', + method: 'PATCH', json: { position: [ { @@ -390,7 +388,6 @@ describe('NGSI-LD - Command functionalities', function () { }; beforeEach(function (done) { - logger.setLevel('ERROR'); iotAgentLib.register(device3, function (error) { done(); }); @@ -496,7 +493,6 @@ describe('NGSI-LD - Command functionalities', function () { }; beforeEach(function (done) { - logger.setLevel('ERROR'); iotAgentLib.register(device3, function (error) { done(); }); @@ -594,7 +590,6 @@ describe('NGSI-LD - Command functionalities', function () { }; beforeEach(function (done) { - logger.setLevel('ERROR'); iotAgentLib.register(device3, function (error) { done(); }); @@ -691,7 +686,6 @@ describe('NGSI-LD - Command functionalities', function () { }; beforeEach(function (done) { - logger.setLevel('ERROR'); iotAgentLib.register(device3, function (error) { done(); }); @@ -794,7 +788,6 @@ describe('NGSI-LD - Command functionalities', function () { }; beforeEach(function (done) { - logger.setLevel('ERROR'); iotAgentLib.register(device3, function (error) { done(); }); @@ -902,7 +895,6 @@ describe('NGSI-LD - Command functionalities', function () { }; beforeEach(function (done) { - logger.setLevel('ERROR'); iotAgentLib.register(device3, function (error) { done(); }); @@ -1006,7 +998,6 @@ describe('NGSI-LD - Command functionalities', function () { }; beforeEach(function (done) { - logger.setLevel('ERROR'); iotAgentLib.register(device3, function (error) { done(); }); @@ -1104,7 +1095,6 @@ describe('NGSI-LD - Command functionalities', function () { }; beforeEach(function (done) { - logger.setLevel('ERROR'); iotAgentLib.register(device3, function (error) { done(); }); @@ -1246,7 +1236,6 @@ describe('NGSI-LD - Command functionalities', function () { }; beforeEach(function (done) { - logger.setLevel('ERROR'); iotAgentLib.register(device3, function (error) { done(); }); diff --git a/test/unit/ngsi-ld/lazyAndCommands/merge-patch-test.js b/test/unit/ngsi-ld/lazyAndCommands/merge-patch-test.js new file mode 100644 index 000000000..7dede5903 --- /dev/null +++ b/test/unit/ngsi-ld/lazyAndCommands/merge-patch-test.js @@ -0,0 +1,249 @@ +/* + * 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 + */ + +/* eslint-disable no-unused-vars */ + +const iotAgentLib = require('../../../../lib/fiware-iotagent-lib'); +const utils = require('../../../tools/utils'); +const request = utils.request; +const should = require('should'); +const logger = require('logops'); +const nock = require('nock'); +const mongoUtils = require('../../mongodb/mongoDBUtils'); + +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, + ldSupport: { + null: true, + datasetId: false, + merge: true + } + }, + types: { + Robot: { + internalAttributes:[], + commands:[ + { + name: 'position', + object_id: 'pos', + type: 'Object' + }, + { + name: 'orientation', + type: 'Object' + } + ], + lazy: [ + { + name: 'batteryLevel', + type: 'Object' + } + ], + staticAttributes: [], + active: [] + } + }, + service: 'smartgondor', + providerUrl: 'http://smartgondor.com' +}; +const device3 = { + id: 'r2d2', + type: 'Robot', + service: 'smartgondor' +}; + +describe('NGSI-LD - Merge-Patch functionalities', function () { + beforeEach(function (done) { + 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/registerIoTAgentCommandsAndLazy.json' + ) + ) + .reply(201, null, { Location: '/ngsi-ld/v1/csourceRegistrations/6319a7f5254b05844116584d' }); + + contextBrokerMock + .matchHeader('fiware-service', 'smartgondor') + .post('/ngsi-ld/v1/entityOperations/upsert/') + .reply(204); + + 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 merge-patch PATCH arrives to the IoT Agent as Context Provider', function () { + const options = { + url: + 'http://localhost:' + + iotAgentConfig.server.port + + '/ngsi-ld/v1/entities/urn:ngsi-ld:Robot:r2d2', + method: 'PATCH', + json: { + "position": { + "type": "Property", + "value": { + "moveTo" : [12,34], + "observedAt": "urn:ngsi-ld:null", + "precision": { + "value": 0.95, + "unitCode": "C62" + } + } + }, + "orientation" : "urn:ngsi-ld:null" + }, + headers: { + 'fiware-service': 'smartgondor', + 'content-type': 'application/ld+json' + } + }; + + beforeEach(function (done) { + iotAgentLib.register(device3, function (error) { + done(); + }); + }); + + it('should call the client handler once', function (done) { + let handlerCalled = 0; + + iotAgentLib.setMergePatchHandler(function (id, type, service, subservice, attributes, callback) { + id.should.equal('urn:ngsi-ld:' + device3.type + ':' + device3.id); + type.should.equal(device3.type); + attributes[0].name.should.equal('position'); + attributes[1].name.should.equal('orientation'); + should.equal(attributes[1].value, null); + handlerCalled++; + callback(null, { + id, + type, + attributes: [ + { + name: 'position', + type: 'Array', + value: '[28, -104, 23]' + } + ] + }); + }); + + request(options, function (error, response, body) { + should.not.exist(error); + response.statusCode.should.equal(200); + handlerCalled.should.equal(1); + done(); + }); + }); + }); + + + xdescribe('When a partial update PATCH with an NGSI-LD Null arrives to the IoT Agent as Context Provider', function () { + const options = { + url: + 'http://localhost:' + + iotAgentConfig.server.port + + '/ngsi-ld/v1/entities/urn:ngsi-ld:Robot:r2d2/attrs/position', + method: 'PATCH', + json: { + "type": "Property", + "value": "urn:ngsi-ld:null" + }, + headers: { + 'fiware-service': 'smartgondor', + 'content-type': 'application/ld+json' + } + }; + + beforeEach(function (done) { + logger.setLevel('FATAL'); + iotAgentLib.register(device3, function (error) { + done(); + }); + }); + + it('should call the client handler once', function (done) { + let handlerCalled = 0; + + iotAgentLib.setCommandHandler(function (id, type, service, subservice, attributes, callback) { + id.should.equal('urn:ngsi-ld:' + device3.type + ':' + device3.id); + type.should.equal(device3.type); + attributes[0].name.should.equal('position'); + should.equal(attributes[0].value, null); + handlerCalled++; + callback(null, { + id, + type, + attributes: [ + { + name: 'position', + type: 'Array', + value: null + } + ] + }); + }); + + request(options, function (error, response, body) { + console.error(error) + should.not.exist(error); + + + response.statusCode.should.equal(204); + handlerCalled.should.equal(1); + done(); + }); + }); + }); +}); diff --git a/test/unit/ngsi-ld/ngsiService/unsupported-endpoints-test.js b/test/unit/ngsi-ld/ngsiService/unsupported-endpoints-test.js index 162204541..c6b07abce 100644 --- a/test/unit/ngsi-ld/ngsiService/unsupported-endpoints-test.js +++ b/test/unit/ngsi-ld/ngsiService/unsupported-endpoints-test.js @@ -29,6 +29,7 @@ const iotAgentLib = require('../../../../lib/fiware-iotagent-lib'); const utils = require('../../../tools/utils'); const request = utils.request; const should = require('should'); +const nock = require('nock'); let contextBrokerMock; const iotAgentConfig = { @@ -47,6 +48,50 @@ const iotAgentConfig = { providerUrl: 'http://smartgondor.com' }; + +const iotAgentConfigWithLimitedSupport = { + contextBroker: { + host: '192.168.1.1', + port: '1026', + ngsiVersion: 'ld', + jsonLdContext: 'http://context.json-ld' + }, + server: { + port: 4041, + ldSupport : { + null: false, + datasetId: false + } + }, + types: { + Robot: { + commands: [ + { + name: 'position', + type: 'Array' + }, + { + name: 'orientation', + type: 'Array' + } + ], + lazy: [], + staticAttributes: [], + active: [] + } + }, + service: 'smartgondor', + subservice: 'gardens', + providerUrl: 'http://smartgondor.com' +}; + +const device = { + id: 'r2d2', + type: 'Robot', + service: 'smartgondor' +}; + + describe('NGSI-LD - Unsupported Endpoints', function () { beforeEach(function (done) { iotAgentLib.activate(iotAgentConfig, function () { @@ -107,5 +152,131 @@ describe('NGSI-LD - Unsupported Endpoints', function () { done(); }); }); + + + it('PUT /entities/ includes an NGSI-LD Null should return a valid NSGI-LD error message', function (done) { + const options = { + url: + 'http://localhost:' + + iotAgentConfig.server.port + + '/ngsi-ld/v1/entities/urn:ngsi-ld:entity/attrs/att', + method: 'PUT', + json: { + "value": "urn:ngsi-ld:null" + }, + headers: { + 'fiware-service': 'smartgondor', + 'content-type': 'application/ld+json' + } + }; + request(options, function (error, response, body) { + response.statusCode.should.equal(400); + done(); + }); + }); + }); +}); + +describe('NGSI-LD - Limiting Support', function () { + beforeEach(function (done) { + 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: '/ngsi-ld/v1/csourceRegistrations/6319a7f5254b05844116584d' }); + + contextBrokerMock + .matchHeader('fiware-service', 'smartgondor') + .post('/ngsi-ld/v1/entityOperations/upsert/') + .reply(204); + + + iotAgentLib.activate(iotAgentConfigWithLimitedSupport, function () { + iotAgentLib.clearAll(function () { + done(); + }); + }); + }); + + afterEach(function (done) { + iotAgentLib.clearAll(function () { + iotAgentLib.deactivate(done); + }); }); + + describe('When sending sending an NGSI-LD Null when nulls are unsupported', function () { + it('should return a valid NSGI-LD error message', function (done) { + const options = { + url: + 'http://localhost:' + + iotAgentConfig.server.port + + '/ngsi-ld/v1/entities/urn:ngsi-ld:Robot:r2d2/attrs/position', + method: 'PATCH', + json: { + "value": "urn:ngsi-ld:null" + }, + headers: { + 'fiware-service': 'smartgondor', + 'content-type': 'application/ld+json' + } + }; + + iotAgentLib.setCommandHandler(function (id, type, service, subservice, attributes, callback) { + callback(null, {}); + }); + + iotAgentLib.register(device, function (error) { + request(options, function (error, response, body) { + response.statusCode.should.equal(400); + done(); + }); + }); + + }); + }); + describe('When sending a payload including a datasetId when datasetIds are unsupported ', function () { + it('should return a valid NSGI-LD error message', function (done) { + const options = { + url: + 'http://localhost:' + + iotAgentConfig.server.port + + '/ngsi-ld/v1/entities/urn:ngsi-ld:Robot:r2d2/attrs/', + method: 'PATCH', + json: { + position: [ + { + type: 'Property', + value: [1, 2, 3], + datasetId: 'urn:ngsi-ld:this' + }, + { + type: 'Property', + value: [28, -104, 23], + datasetId: 'urn:ngsi-ld:that' + } + ] + }, + headers: { + 'fiware-service': 'smartgondor', + 'content-type': 'application/ld+json' + } + }; + + iotAgentLib.setCommandHandler(function (id, type, service, subservice, attributes, callback) { + callback(null, {}); + }); + + iotAgentLib.register(device, function (error) { + request(options, function (error, response, body) { + response.statusCode.should.equal(400); + done(); + }); + }); + + }); + }); });