diff --git a/CHANGES_NEXT_RELEASE b/CHANGES_NEXT_RELEASE index 400c995c7..fed877c7d 100644 --- a/CHANGES_NEXT_RELEASE +++ b/CHANGES_NEXT_RELEASE @@ -1,3 +1,11 @@ +Add basic NGSI-LD support as experimental feature (#842) +- Active measures +- GeoJSON and DateTime, unitCode and observedAt NGSI-LD support + - The NGSI v2 `TimeInstant` element has been mapped onto the NGSI-LD `observedAt` property + - The NGSI v2 `metadata.unitCode` attribute has been mapped onto the NGSI-LD `unitCode` property +- Multi-measures +- Lazy Attributes +- Commands Update codebase to use ES6 - Remove JSHint and jshint overrides - Add esLint using standard tamia presets diff --git a/doc/advanced-topics.md b/doc/advanced-topics.md index 1d44bd120..04a18cdfa 100644 --- a/doc/advanced-topics.md +++ b/doc/advanced-topics.md @@ -83,6 +83,50 @@ e.g.: } ``` +#### NGSI-LD data and metadata considerations + +When provisioning devices for an NGSI-LD Context Broker, `type` values should typically correspond to one of the +following: + +- `Property`, `Relationship`, `Geoproperty` +- Native JSON types (e.g. `String`, `Boolean`, `Float` , `Integer` `Number`) +- Temporal Properties (e.g. `Datetime`, `Date` , `Time`) +- GeoJSON types (e.g `Point`, `LineString`, `Polygon`, `MultiPoint`, `MultiLineString`, `MultiPolygon`) + +Most NGSI-LD attributes are sent to the Context Broker as _properties_. If a GeoJSON type or native JSON type is +defined, the data will be converted to the appropriate type. Temporal properties should always be expressed in UTC, +using ISO 8601. This ISO 8601 conversion is applied automatically for the `observedAt` _property-of-a-property_ metadata +where present. + +Data for any attribute defined as a _relationship_ must be a valid URN. + +Note that when the `unitCode` metadata attribute is supplied in the provisioning data under NGSI-LD, the standard +`unitCode` _property-of-a-property_ `String` attribute is created. + +Other unrecognised `type` attributes will be passed as NGSI-LD data using the following JSON-LD format: + +```json + "": { + "type" : "Property", + "value": { + "@type": "", + "@value": { string or object} + } + } +``` + +`null` values will be passed in the following format: + +```json + "": { + "type" : "Property", + "value": { + "@type": "Intangible", + "@value": null + } + } +``` + ### Data mapping plugins The IoT Agent Library provides a plugin mechanism in order to facilitate reusing code that makes small transformations @@ -166,8 +210,9 @@ events in the IoT Agent with the configured type name will be marked as events. ##### Timestamp Processing Plugin (timestampProcess) -This plugin processes the entity attributes looking for a TimeInstant attribute. If one is found, the plugin add a -TimeInstant attribute as metadata for every other attribute in the same request. +This plugin processes the entity attributes looking for a `TimeInstant` attribute. If one is found, for NGSI-v1/NGSIv2, +the plugin adds a `TimeInstant` attribute as metadata for every other attribute in the same request. With NGSI-LD, the +Standard `observedAt` property-of-a-property is used instead. ##### Expression Translation plugin (expressionTransformation) diff --git a/doc/api.md b/doc/api.md index 47b8f7578..5f8673011 100644 --- a/doc/api.md +++ b/doc/api.md @@ -75,23 +75,23 @@ information configured: The table below shows the information held in the service group provisioning resource. The table also contains the correspondence between the API resource fields and the same fields in the database model. -| Payload Field | DB Field | Definition | -| --------------------- | -------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `service` | `service` | Service of the devices of this type | -| `subservice` | `subservice` | Subservice of the devices of this type. | -| `resource` | `resource` | string representing the Southbound resource that will be used to assign a type to a device (e.g.: pathname in the southbound port). | -| `apikey` | `apikey` | API Key string. | -| `timestamp` | `timestamp` | Optional flagw whether to include the `TimeInstant`within each entity created, as well as a `TimeInstant` metadata to each attribute, with the current timestamp | -| `entity_type` | `entity_type` | name of the Entity `type` to assign to the group. | -| `trust` | `trust` | trust token to use for secured access to the Context Broker for this type of devices (optional; only needed for secured scenarios). | -| `cbHost` | `cbHost` | Context Broker connection information. This options can be used to override the global ones for specific types of devices. | -| `lazy` | `lazy` | list of common lazy attributes of the device. For each attribute, its `name` and `type` must be provided. | -| `commands` | `commands` | list of common commands attributes of the device. For each attribute, its `name` and `type` must be provided, additional `metadata` is optional. | -| `attributes` | `attributes` | list of common active attributes of the device. For each attribute, its `name` and `type` must be provided, additional `metadata` is optional. | -| `static_attributes` | `staticAttributes` | this attributes will be added to all the entities of this group 'as is', additional `metadata` is optional. | -| `internal_attributes` | `internalAttributes` | optional section with free format, to allow specific IoT Agents to store information along with the devices in the Device Registry. | -| `expressionLanguage` | `expresionLanguage` | optional boolean value, to set expression language used to compute expressions, possible values are: legacy or jexl. When not set or wrongly set, legacy is used as default value. | -| `explicitAttrs` | `explicitAttrs` | optional boolean value, to support selective ignore of measures so that IOTA doesn’t progress. If not specified default is false. | +| Payload Field | DB Field | Definition | +| --------------------- | -------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `service` | `service` | Service of the devices of this type | +| `subservice` | `subservice` | Subservice of the devices of this type. | +| `resource` | `resource` | string representing the Southbound resource that will be used to assign a type to a device (e.g.: pathname in the southbound port). | +| `apikey` | `apikey` | API Key string. | +| `timestamp` | `timestamp` | Optional flag about whether or not to add the `TimeInstant` attribute to the device entity created, as well as a `TimeInstant` metadata to each attribute, with the current timestamp. With NGSI-LD, the Standard `observedAt` property-of-a-property is created instead. | true | +| `entity_type` | `entity_type` | name of the Entity `type` to assign to the group. | +| `trust` | `trust` | trust token to use for secured access to the Context Broker for this type of devices (optional; only needed for secured scenarios). | +| `cbHost` | `cbHost` | Context Broker connection information. This options can be used to override the global ones for specific types of devices. | +| `lazy` | `lazy` | list of common lazy attributes of the device. For each attribute, its `name` and `type` must be provided. | +| `commands` | `commands` | list of common commands attributes of the device. For each attribute, its `name` and `type` must be provided, additional `metadata` is optional. | +| `attributes` | `attributes` | list of common active attributes of the device. For each attribute, its `name` and `type` must be provided, additional `metadata` is optional. | +| `static_attributes` | `staticAttributes` | this attributes will be added to all the entities of this group 'as is', additional `metadata` is optional. | +| `internal_attributes` | `internalAttributes` | optional section with free format, to allow specific IoT Agents to store information along with the devices in the Device Registry. | +| `expressionLanguage` | `expresionLanguage` | optional boolean value, to set expression language used to compute expressions, possible values are: legacy or jexl. When not set or wrongly set, legacy is used as default value. | +| `explicitAttrs` | `explicitAttrs` | optional boolean value, to support selective ignore of measures so that IOTA doesn’t progress. If not specified default is false. | ### Service Group Endpoint @@ -205,27 +205,26 @@ Note that there is a 1:1 correspondence between payload fields and DB fields (bu The table below shows the information held in the Device resource. The table also contains the correspondence between the API resource fields and the same fields in the database model. -| Payload Field | DB Field | Definition | Example of value | -| ------------------------- | -------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------- | -| `device_id` | `id` | Device ID that will be used to identify the device. | UO834IO | -| `service` | `service` | Name of the service the device belongs to (will be used in the fiware-service header). | smartGondor | -| `service_path` | `subservice` | Name of the subservice the device belongs to (used in the fiware-servicepath header). | /gardens | -| `entity_name` | `name` | Name of the entity representing the device in the Context Broker | ParkLamplight12 | -| `entity_type` | `type` | Type of the entity in the Context Broker | Lamplights | -| `timezone` | `timezone` | Time zone of the sensor if it has any | America/Santiago | -| `timestamp` | `timestamp` | Optional flag about whether or not to addthe TimeInstant attribute to the device entity created, as well as a TimeInstant metadata to each attribute, with the current timestamp | true | -| `apikey` | `apikey` | Optional Apikey key string to use instead of group apikey | -| 9n4hb1vpwbjozzmw9f0flf9c2 | -| `endpoint` | `endpoint` | Endpoint where the device is going to receive commands, if any. | http://theDeviceUrl:1234/commands | -| `protocol` | `protocol` | Name of the device protocol, for its use with an IoT Manager. | IoTA-UL | -| `transport` | `transport` | Name of the device transport protocol, for the IoT Agents with multiple transport protocols. | MQTT | -| `attributes` | `active` | List of active attributes of the device | `[ { "name": "attr_name", "type": "Text" } ]` | -| `lazy` | `lazy` | List of lazy attributes of the device | `[ { "name": "attr_name", "type": "Text" } ]` | -| `commands` | `commands` | List of commands of the device | `[ { "name": "attr_name", "type": "Text" } ]` | -| `internal_attributes` | `internalAttributes` | List of internal attributes with free format for specific IoT Agent configuration | LWM2M mappings from object URIs to attributes | -| `static_attributes` | `staticAttributes` | List of static attributes to append to the entity. All the updateContext requests to the CB will have this set of attributes appended. | `[ { "name": "attr_name", "type": "Text" } ]` | -| `expressionLanguage` | `expresionLanguage` | optional boolean value, to set expression language used to compute expressions, possible values are: legacy or jexl. When not set or wrongly set, legacy is used as default value. | -| `explicitAttrs` | `explicitAttrs` | Boolean value to support selective ignore of measures for device so that IOTA doesn’t progress. If not specified default is false. | `true/false` | +| Payload Field | DB Field | Definition | Example of value | +| --------------------- | -------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :-------------------------------------------- | +| `device_id` | `id` | Device ID that will be used to identify the device. | UO834IO | +| `service` | `service` | Name of the service the device belongs to (will be used in the fiware-service header). | smartGondor | +| `service_path` | `subservice` | Name of the subservice the device belongs to (used in the fiware-servicepath header). | /gardens | +| `entity_name` | `name` | Name of the entity representing the device in the Context Broker | ParkLamplight12 | +| `entity_type` | `type` | Type of the entity in the Context Broker | Lamplights | +| `timezone` | `timezone` | Time zone of the sensor if it has any | America/Santiago | +| `timestamp` | `timestamp` | Optional flag about whether or not to add the `TimeInstant` attribute to the device entity created, as well as a `TimeInstant` metadata to each attribute, with the current timestamp. With NGSI-LD, the Standard `observedAt` property-of-a-property is created instead. | true | +| `apikey` | `apikey` | Optional Apikey key string to use instead of group apikey | 9n4hb1vpwbjozzmw9f0flf9c2 | +| `endpoint` | `endpoint` | Endpoint where the device is going to receive commands, if any. | http://theDeviceUrl:1234/commands | +| `protocol` | `protocol` | Name of the device protocol, for its use with an IoT Manager. | IoTA-UL | +| `transport` | `transport` | Name of the device transport protocol, for the IoT Agents with multiple transport protocols. | MQTT | +| `attributes` | `active` | List of active attributes of the device | `[ { "name": "attr_name", "type": "Text" } ]` | +| `lazy` | `lazy` | List of lazy attributes of the device | `[ { "name": "attr_name", "type": "Text" } ]` | +| `commands` | `commands` | List of commands of the device | `[ { "name": "attr_name", "type": "Text" } ]` | +| `internal_attributes` | `internalAttributes` | List of internal attributes with free format for specific IoT Agent configuration | LWM2M mappings from object URIs to attributes | +| `static_attributes` | `staticAttributes` | List of static attributes to append to the entity. All the updateContext requests to the CB will have this set of attributes appended. | `[ { "name": "attr_name", "type": "Text" } ]` | +| `expressionLanguage` | `expresionLanguage` | optional boolean value, to set expression language used to compute expressions, possible values are: legacy or jexl. When not set or wrongly set, legacy is used as default value. | +| `explicitAttrs` | `explicitAttrs` | Boolean value to support selective ignore of measures for device so that IOTA doesn’t progress. If not specified default is false. | `true/false` | #### Attribute lists diff --git a/doc/architecture.md b/doc/architecture.md index b975d153d..7b8d76fae 100644 --- a/doc/architecture.md +++ b/doc/architecture.md @@ -214,14 +214,15 @@ device to a command update to the device. As part of the device to entity mapping process the IoT Agent creates and updates automatically a special timestamp. This timestamp is represented as two different properties of the mapped entity:: -- An attribute metadata named `TimeInstant` per dynamic attribute mapped, which captures as an ISO8601 timestamp when - the associated measurement (represented as attribute value) was observed. +- With NGSIv1/NGSI-v2, an attribute metadata named `TimeInstant` (per dynamic attribute mapped, which captures as an + ISO8601 timestamp when the associated measurement (represented as attribute value) was observed. With NGSI-LD, the + Standard `observedAt` property-of-a-property is used instead. -- An entity attribute named `TimeInstant` which captures as an ISO8601 timestamp when the last measurement received - from the device was observed. +- For NGSIv1/NGSI-v2 only, an additional attribute `TimeInstant` is added to the entity which captures as an ISO8601 + timestamp when the last measurement received from the device was observed. If no information about the measurement timestamp is received by the IoT Agent, the arrival time of the measurement will -be used to generate a `TimeInstant` for both the entity and the attribute's metadata. +be used to generate a `TimeInstant` for both the entity attribute and the attribute metadata. Take into account that: diff --git a/doc/development.md b/doc/development.md index 0afb3f6c2..50093274d 100644 --- a/doc/development.md +++ b/doc/development.md @@ -97,7 +97,8 @@ npm run lint:md ### Documentation Spell-checking -Uses the provided `.textlintrc` flag file. To check the markdown documentation for spelling and grammar errors, dead links & etc. +Uses the provided `.textlintrc` flag file. To check the markdown documentation for spelling and grammar errors, dead +links & etc. ```bash # Use git-bash on Windows diff --git a/doc/installationguide.md b/doc/installationguide.md index d451aa315..386487a8a 100644 --- a/doc/installationguide.md +++ b/doc/installationguide.md @@ -30,6 +30,21 @@ 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' // or ['http://context1.json-ld','http://context2.json-ld'] if you need more than one +} +``` + +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.: @@ -129,7 +144,7 @@ used for the same purpose. For instance: ```javascript { - type: 'mongodb'; + type: "mongodb"; } ``` @@ -152,6 +167,7 @@ used for the same purpose. For instance: retryTime: 5 } ``` + ```javascript { host: 'mongodb-0,mongodb-1,mongodb-2', @@ -213,9 +229,10 @@ used for the same purpose. For instance: any unexpected error. - **singleConfigurationMode**: enables the Single Configuration mode for backwards compatibility (see description in the Overview). Default to false. -- **timestamp**: if this flag is activated, the IoT Agent will add a 'TimeInstant' metadata attribute to all the - attributes updated from device information. This flag is overwritten by `timestamp` flag in group or device - provision. +- **timestamp**: if this flag is activated: + - For NGSIv1/NGSIv2, the IoT Agent will add a `TimeInstant` metadata attribute to all the attributes updated from + device information. This flag is overwritten by `timestamp` flag in group or device + - With NGSI-LD, the standard `observedAt` property-of-a-property is created instead. - **defaultResource**: default string to use as resource for the registration of new Configurations (if no resource is provided). - **defaultKey**: default string to use as API Key for devices that do not belong to a particular Configuration. @@ -232,17 +249,25 @@ used for the same purpose. For instance: the IoTAgent runs in a single thread. For more details about multi-core functionality, please refer to the [Cluster](https://nodejs.org/api/cluster.html) module in Node.js and [this section](howto.md#iot-agent-in-multi-thread-mode) of the library documentation. -- **defaultExpressionLanguage**: the default expression language used to - compute expressions, possible values are: `legacy` or `jexl`. When not set or - wrongly set, `legacy` is used as default value. - -- **explicitAttrs**: if this flag is activated, only provisioned attributes will be processed to Context Broker. - This flag is overwritten by `explicitAttrs` flag in group or device provision. -- **relaxTemplateValidation**: if this flag is activated, `objectId` attributes for incoming devices are not validated, - and may exceptionally include characters (such as semi-colons) which are - [forbidden](https://fiware-orion.readthedocs.io/en/master/user/forbidden_characters/index.html) according to the NGSI - specification. When provisioning devices, it is necessary that the developer provides valid `objectId`-`name` mappings - whenever relaxed mode is used, to prevent the consumption of forbidden characters. +- **defaultExpressionLanguage**: the default expression language used to compute expressions, possible values are: + `legacy` or `jexl`. When not set or wrongly set, `legacy` is used as default value. +- **fallbackTenant** - For Linked Data Context Brokers which do not support multi-tenancy, this provides an + alternative mechanism for supplying the `NGSILD-Tenant` header. Note that NGSILD-Tenant has not yet been included in + the NGSI-LD standard (it has been proposed for the next update of the standard, but the final decision has yet been + confirmed), take into account it could change. Note that for backwards compatibility with NGSI v2, the + `fiware-service` header is already used as alternative if the `NGSILD-Tenant` header is not supplied. +- **fallbackPath** - For Linked Data Context Brokers which do not support a service path, this provides an alternative + mechanism for suppling the `NGSILD-Path` header. Note that for backwards compatibility with NGSI v2, the + `fiware-servicepath` header is already used as alternative if the `NGSILD-Path` header is not supplied. Note that + NGSILD-Path has not yet been included in the NGSI-LD standard (it has been proposed for the next update of the + standard, but the final decision has yet been confirmed), take into account it could change +- **explicitAttrs**: if this flag is activated, only provisioned attributes will be processed to Context Broker. This + flag is overwritten by `explicitAttrs` flag in group or device provision. +- **relaxTemplateValidation**: if this flag is activated, `objectId` attributes for incoming devices are not + validated, and may exceptionally include characters (such as semi-colons) which are + [forbidden](https://fiware-orion.readthedocs.io/en/master/user/forbidden_characters/index.html) according to the + NGSI specification. When provisioning devices, it is necessary that the developer provides valid `objectId`-`name` + mappings whenever relaxed mode is used, to prevent the consumption of forbidden characters. ### Configuration using environment variables @@ -252,54 +277,62 @@ with container-based technologies, like Docker, Heroku, etc... The following table shows the accepted environment variables, as well as the configuration parameter the variable overrides. -| Environment variable | Configuration attribute | -| :------------------------ | :------------------------------ | -| IOTA_CB_URL | `contextBroker.url` | -| IOTA_CB_HOST | `contextBroker.host` | -| IOTA_CB_PORT | `contextBroker.port` | -| IOTA_CB_NGSI_VERSION | `contextBroker.ngsiVersion` | -| IOTA_NORTH_HOST | `server.host` | -| IOTA_NORTH_PORT | `server.port` | -| IOTA_PROVIDER_URL | `providerUrl` | -| IOTA_AUTH_ENABLED | `authentication.enabled` | -| IOTA_AUTH_TYPE | `authentication.type` | -| IOTA_AUTH_HEADER | `authentication.header` | -| IOTA_AUTH_URL | `authentication.url` | -| IOTA_AUTH_HOST | `authentication.host` | -| IOTA_AUTH_PORT | `authentication.port` | -| IOTA_AUTH_USER | `authentication.user` | -| IOTA_AUTH_PASSWORD | `authentication.password` | -| IOTA_AUTH_CLIENT_ID | `authentication.clientId` | -| IOTA_AUTH_CLIENT_SECRET | `authentication.clientSecret` | -| IOTA_AUTH_TOKEN_PATH | `authentication.tokenPath` | -| IOTA_AUTH_PERMANENT_TOKEN | `authentication.permanentToken` | -| IOTA_REGISTRY_TYPE | `deviceRegistry.type` | -| IOTA_LOG_LEVEL | `logLevel` | -| IOTA_TIMESTAMP | `timestamp` | -| IOTA_IOTAM_URL | `iotManager.url` | -| IOTA_IOTAM_HOST | `iotManager.host` | -| IOTA_IOTAM_PORT | `iotManager.port` | -| IOTA_IOTAM_PATH | `iotManager.path` | -| IOTA_IOTAM_AGENTPATH | `iotManager.agentPath` | -| IOTA_IOTAM_PROTOCOL | `iotManager.protocol` | -| IOTA_IOTAM_DESCRIPTION | `iotManager.description` | -| IOTA_MONGO_HOST | `mongodb.host` | -| IOTA_MONGO_PORT | `mongodb.port` | -| IOTA_MONGO_DB | `mongodb.db` | -| IOTA_MONGO_REPLICASET | `mongodb.replicaSet` | -| IOTA_MONGO_USER | `mongodb.user` | -| IOTA_MONGO_PASSWORD | `mongodb.password` | -| IOTA_MONGO_AUTH_SOURCE | `mongodb.authSource` | -| IOTA_MONGO_RETRIES | `mongodb.retries` | -| IOTA_MONGO_RETRY_TIME | `mongodb.retryTime` | -| IOTA_MONGO_SSL | `mongodb.ssl ` | -| IOTA_MONGO_EXTRAARGS | `mongodb.extraArgs` | -| IOTA_SINGLE_MODE | `singleConfigurationMode` | -| IOTA_APPEND_MODE | `appendMode` | -| IOTA_POLLING_EXPIRATION | `pollingExpiration` | -| IOTA_POLLING_DAEMON_FREQ | `pollingDaemonFrequency` | -| IOTA_AUTOCAST | `autocast` | -| IOTA_MULTI_CORE | `multiCore` | -| IOTA_DEFAULT_EXPRESSION_LANGUAGE | defaultExpressionLanguage | -| IOTA_EXPLICIT_ATTRS | `explicitAttrs` | -| IOTA_RELAX_TEMPLATE_VALIDATION | `relaxTemplateValidation` | +| Environment variable | Configuration attribute | +| :------------------------------- | :------------------------------ | +| IOTA_CB_URL | `contextBroker.url` | +| IOTA_CB_HOST | `contextBroker.host` | +| IOTA_CB_PORT | `contextBroker.port` | +| IOTA_CB_NGSI_VERSION | `contextBroker.ngsiVersion` | +| IOTA_NORTH_HOST | `server.host` | +| IOTA_NORTH_PORT | `server.port` | +| IOTA_PROVIDER_URL | `providerUrl` | +| IOTA_AUTH_ENABLED | `authentication.enabled` | +| IOTA_AUTH_TYPE | `authentication.type` | +| IOTA_AUTH_HEADER | `authentication.header` | +| IOTA_AUTH_URL | `authentication.url` | +| IOTA_AUTH_HOST | `authentication.host` | +| IOTA_AUTH_PORT | `authentication.port` | +| IOTA_AUTH_USER | `authentication.user` | +| IOTA_AUTH_PASSWORD | `authentication.password` | +| IOTA_AUTH_CLIENT_ID | `authentication.clientId` | +| IOTA_AUTH_CLIENT_SECRET | `authentication.clientSecret` | +| IOTA_AUTH_TOKEN_PATH | `authentication.tokenPath` | +| IOTA_AUTH_PERMANENT_TOKEN | `authentication.permanentToken` | +| IOTA_REGISTRY_TYPE | `deviceRegistry.type` | +| IOTA_LOG_LEVEL | `logLevel` | +| IOTA_TIMESTAMP | `timestamp` | +| IOTA_IOTAM_URL | `iotManager.url` | +| IOTA_IOTAM_HOST | `iotManager.host` | +| IOTA_IOTAM_PORT | `iotManager.port` | +| IOTA_IOTAM_PATH | `iotManager.path` | +| IOTA_IOTAM_AGENTPATH | `iotManager.agentPath` | +| IOTA_IOTAM_PROTOCOL | `iotManager.protocol` | +| IOTA_IOTAM_DESCRIPTION | `iotManager.description` | +| IOTA_MONGO_HOST | `mongodb.host` | +| IOTA_MONGO_PORT | `mongodb.port` | +| IOTA_MONGO_DB | `mongodb.db` | +| IOTA_MONGO_REPLICASET | `mongodb.replicaSet` | +| IOTA_MONGO_USER | `mongodb.user` | +| IOTA_MONGO_PASSWORD | `mongodb.password` | +| IOTA_MONGO_AUTH_SOURCE | `mongodb.authSource` | +| IOTA_MONGO_RETRIES | `mongodb.retries` | +| IOTA_MONGO_RETRY_TIME | `mongodb.retryTime` | +| IOTA_MONGO_SSL | `mongodb.ssl` | +| IOTA_MONGO_EXTRAARGS | `mongodb.extraArgs` | +| IOTA_SINGLE_MODE | `singleConfigurationMode` | +| IOTA_APPEND_MODE | `appendMode` | +| IOTA_POLLING_EXPIRATION | `pollingExpiration` | +| IOTA_POLLING_DAEMON_FREQ | `pollingDaemonFrequency` | +| IOTA_AUTOCAST | `autocast` | +| IOTA_MULTI_CORE | `multiCore` | +| IOTA_JSON_LD_CONTEXT | `jsonLdContext` | +| IOTA_FALLBACK_TENANT | `fallbackTenant` | +| IOTA_FALLBACK_PATH | `fallbackPath` | +| IOTA_DEFAULT_EXPRESSION_LANGUAGE | `defaultExpressionLanguage` | +| IOTA_EXPLICIT_ATTRS | `explicitAttrs` | +| IOTA_RELAX_TEMPLATE_VALIDATION | `relaxTemplateValidation` | + +Note: + +- If you need to pass more than one JSON-LD context, you can define the IOTA_JSON_LD_CONTEXT environment variable as a + comma separated list of contexts (e.g. `'http://context1.json-ld,http://context2.json-ld'`) diff --git a/lib/commonConfig.js b/lib/commonConfig.js index 18a5eeb58..8a8a3373e 100644 --- a/lib/commonConfig.js +++ b/lib/commonConfig.js @@ -154,7 +154,10 @@ function processEnvironmentVariables() { 'IOTA_POLLING_DAEMON_FREQ', 'IOTA_MULTI_CORE', 'IOTA_DEFAULT_EXPRESSION_LANGUAGE', - 'IOTA_RELAX_TEMPLATE_VALIDATION' + 'IOTA_RELAX_TEMPLATE_VALIDATION', + 'IOTA_JSON_LD_CONTEXT', + 'IOTA_FALLBACK_TENANT', + 'IOTA_FALLBACK_PATH' ]; const iotamVariables = [ 'IOTA_IOTAM_URL', @@ -235,6 +238,14 @@ function processEnvironmentVariables() { 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.split(',').map((ctx) => ctx.trim()); + } + + config.contextBroker.fallbackTenant = + process.env.IOTA_FALLBACK_TENANT || config.contextBroker.service || 'iotagent'; + config.contextBroker.fallbackPath = process.env.IOTA_FALLBACK_PATH || config.contextBroker.subservice || '/'; + // North Port Configuration (ensuring the configuration sub-object exists before start using it) if (config.server === undefined) { config.server = {}; @@ -505,6 +516,18 @@ 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; } @@ -522,6 +545,7 @@ exports.getGroupRegistry = getGroupRegistry; exports.setCommandRegistry = setCommandRegistry; exports.getCommandRegistry = getCommandRegistry; exports.checkNgsi2 = checkNgsi2; +exports.checkNgsiLD = checkNgsiLD; exports.setSecurityService = setSecurityService; exports.getSecurityService = getSecurityService; exports.getSecretData = getSecretData; diff --git a/lib/constants.js b/lib/constants.js index b3ca4e0cf..7469ea52c 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -23,12 +23,43 @@ * Modified by: Daniel Calvo - ATOS Research & Innovation */ +const LOCATION_DEFAULT = '0, 0'; +const DATETIME_DEFAULT = '1970-01-01T00:00:00.000Z'; +const ATTRIBUTE_DEFAULT = ' '; + +/** + * Provides a default value for DateTime, GeoProperty and Property Attributes. + * + * @param {String} type The type of attribute being created. + * @return {String} A default value to use in the entity + */ +function getInitialValueForType(type) { + switch (type.toLowerCase()) { + case 'geoproperty': + return LOCATION_DEFAULT; + case 'point': + return LOCATION_DEFAULT; + case 'geo:point': + return LOCATION_DEFAULT; + case 'datetime': + return DATETIME_DEFAULT; + case 'date': + return DATETIME_DEFAULT; + case 'time': + return DATETIME_DEFAULT; + default: + return ATTRIBUTE_DEFAULT; + } +} + module.exports = { TIMESTAMP_ATTRIBUTE: 'TimeInstant', TIMESTAMP_TYPE: 'ISO8601', TIMESTAMP_TYPE_NGSI2: 'DateTime', SERVICE_HEADER: 'fiware-service', SUBSERVICE_HEADER: 'fiware-servicepath', + NGSI_LD_TENANT_HEADER: 'NGSILD-Tenant', + NGSI_LD_PATH_HEADER: 'NGSILD-Path', //FIXME: check Keystone support this in lowercase, then change AUTH_HEADER: 'X-Auth-Token', X_FORWARDED_FOR_HEADER: 'x-forwarded-for', @@ -48,14 +79,11 @@ module.exports = { DEFAULT_MONGODB_RETRIES: 5, DEFAULT_MONGODB_RETRY_TIME: 5, - ATTRIBUTE_DEFAULT: ' ', - - LOCATION_TYPE: 'geo:point', - LOCATION_DEFAULT: '0, 0', - DATETIME_TYPE: 'DateTime', - DATETIME_DEFAULT: '1970-01-01T00:00:00.000Z', - MONGO_ALARM: 'MONGO-ALARM', ORION_ALARM: 'ORION-ALARM', - IOTAM_ALARM: 'IOTAM-ALARM' + IOTAM_ALARM: 'IOTAM-ALARM', + ATTRIBUTE_DEFAULT, + DATETIME_DEFAULT, + + getInitialValueForType }; diff --git a/lib/errors.js b/lib/errors.js index 23eaf406c..4affe7f7d 100644 --- a/lib/errors.js +++ b/lib/errors.js @@ -22,6 +22,7 @@ * * Modified by: Daniel Calvo - ATOS Research & Innovation */ + class RegistrationError { constructor(id, type) { this.name = 'REGISTRATION_ERROR'; @@ -71,7 +72,7 @@ class BadRequest { class UnsupportedContentType { constructor(type) { this.name = 'UNSUPPORTED_CONTENT_TYPE'; - this.message = 'Unsuported content type in the context request: ' + type; + this.message = 'Unsupported content type in the context request: ' + type; this.code = 400; } } @@ -95,7 +96,6 @@ class DeviceNotFound { this.code = 404; } } - class AttributeNotFound { constructor() { this.name = 'ATTRIBUTE_NOT_FOUND'; @@ -103,7 +103,6 @@ class AttributeNotFound { this.code = 404; } } - class DuplicateDeviceId { constructor(id) { this.name = 'DUPLICATE_DEVICE_ID'; @@ -166,7 +165,6 @@ class MismatchedService { /* eslint-disable-next-line no-unused-vars */ constructor(service, subservice) { this.name = 'MISMATCHED_SERVICE'; - this.message = "The declared service didn't match the stored one in the entity"; this.code = 403; } @@ -211,7 +209,7 @@ class DeviceGroupNotFound { } else if (fields) { this.message = 'Couldn\t find device group for fields: ' + JSON.stringify(fields); } else { - this.message = "Couldn't find device group"; + this.message = 'Couldn\t find device group'; } this.code = 404; } @@ -251,6 +249,13 @@ class BadTimestamp { this.code = 400; } } +class BadGeocoordinates { + constructor(payload) { + this.name = 'BAD_GEOCOORDINATES'; + this.message = 'Invalid rfc7946 coordinates [' + payload + ']'; + this.code = 400; + } +} module.exports = { RegistrationError, @@ -263,8 +268,8 @@ module.exports = { UnsupportedContentType, TypeNotFound, MissingAttributes, - AttributeNotFound, DeviceNotFound, + AttributeNotFound, DuplicateDeviceId, DuplicateGroup, SecurityInformationMissing, @@ -283,5 +288,6 @@ module.exports = { WrongExpressionType, InvalidExpression, BadAnswer, - BadTimestamp + BadTimestamp, + BadGeocoordinates }; diff --git a/lib/fiware-iotagent-lib.js b/lib/fiware-iotagent-lib.js index d06cef072..9e6f1ab07 100644 --- a/lib/fiware-iotagent-lib.js +++ b/lib/fiware-iotagent-lib.js @@ -27,6 +27,7 @@ const ngsi = require('./services/ngsi/ngsiService'); const intoTrans = require('./services/common/domain').intoTrans; const middlewares = require('./services/common/genericMiddleware'); const db = require('./model/dbConn'); +const ngsiService = require('./services/ngsi/ngsiService'); const subscriptions = require('./services/ngsi/subscriptionService'); const statsRegistry = require('./services/stats/statsRegistry'); const domainUtils = require('./services/common/domain'); @@ -175,6 +176,10 @@ function doActivate(newConfig, callback) { config.setRegistry(registry); config.setGroupRegistry(groupRegistry); config.setCommandRegistry(commandRegistry); + deviceService.init(); + subscriptions.init(); + contextServer.init(); + ngsiService.init(); commands.start(); diff --git a/lib/plugins/attributeAlias.js b/lib/plugins/attributeAlias.js index c1b87409e..5f3501196 100644 --- a/lib/plugins/attributeAlias.js +++ b/lib/plugins/attributeAlias.js @@ -23,17 +23,14 @@ * Modified by: Daniel Calvo - ATOS Research & Innovation */ -/* eslint-disable no-unused-vars */ - const config = require('../commonConfig'); const utils = require('./pluginUtils'); -/*jshint unused:false*/ +/* eslint-disable no-unused-vars */ const logger = require('logops'); -/*jshint unused:false*/ const context = { op: 'IoTAgentNGSI.attributeAlias' }; -const ngsiService = require('../services/ngsi/ngsiService'); +const ngsiUtils = require('../services/ngsi/ngsiUtils'); function extractSingleMapping(previous, current) { /* jshint camelcase: false */ @@ -76,7 +73,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,11 +93,11 @@ function applyAlias(mappings) { */ function updateAttribute(entity, typeInformation, callback) { const mappings = extractAllMappings(typeInformation); - if (config.checkNgsi2()) { + if (config.checkNgsi2() || config.checkNgsiLD()) { let attsArray = utils.extractAttributesArrayFromNgsi2Entity(entity); attsArray = attsArray.map(applyAlias(mappings)); entity = utils.createNgsi2Entity(entity.id, entity.type, attsArray, true); - ngsiService.castJsonNativeAttributes(entity); + ngsiUtils.castJsonNativeAttributes(entity); } else { entity.contextElements[0].attributes = entity.contextElements[0].attributes.map(applyAlias(mappings)); } diff --git a/lib/plugins/bidirectionalData.js b/lib/plugins/bidirectionalData.js index 67b3c469b..e7ea9a70c 100644 --- a/lib/plugins/bidirectionalData.js +++ b/lib/plugins/bidirectionalData.js @@ -130,7 +130,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 3a9dcd34f..9e5455030 100644 --- a/lib/plugins/expressionParser.js +++ b/lib/plugins/expressionParser.js @@ -49,7 +49,6 @@ const grammar = { ['substr', 'return "SUBSTR";'], ['trim', 'return "TRIM";'], ['"[a-zA-Z0-9\\s,]+"', 'return "STRING";'], - ["'[a-zA-Z0-9\\s,]+'", 'return "STRING";'], ['$', 'return "EOF";'] ] @@ -109,8 +108,9 @@ function parse(expression, context, type, callback) { if (callback) { return callback(error); + } else { + throw error; } - throw error; } if (callback) { @@ -175,7 +175,8 @@ function expressionApplier(context, typeInformation) { type: attribute.type }; - if (config.checkNgsi2() && attribute.object_id) { + /*jshint camelcase: false */ + 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 d85342d05..701b13a01 100644 --- a/lib/plugins/expressionPlugin.js +++ b/lib/plugins/expressionPlugin.js @@ -24,15 +24,12 @@ * Modified by: Federico M. Facca - Martel Innovate */ -/* eslint-disable no-unused-vars */ - const _ = require('underscore'); const legacyParser = require('./expressionParser'); const jexlParser = require('./jexlParser'); const config = require('../commonConfig'); -/*jshint unused:false*/ +/* eslint-disable no-unused-vars */ const logger = require('logops'); -/*jshint unused:false*/ const context = { op: 'IoTAgentNGSI.expressionPlugin' }; @@ -115,7 +112,7 @@ function update(entity, typeInformation, callback) { } try { - if (config.checkNgsi2()) { + if (config.checkNgsi2() || config.checkNgsiLD()) { let 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 c2850e9c5..ef036b232 100644 --- a/lib/plugins/multiEntity.js +++ b/lib/plugins/multiEntity.js @@ -288,7 +288,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 7f8381556..778d719fc 100644 --- a/lib/plugins/pluginUtils.js +++ b/lib/plugins/pluginUtils.js @@ -103,7 +103,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. @@ -149,7 +149,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); @@ -184,7 +184,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 4ce004cf2..d4e21a28c 100644 --- a/lib/plugins/timestampProcessPlugin.js +++ b/lib/plugins/timestampProcessPlugin.js @@ -130,7 +130,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 3c61d46f1..24d5ae8ff 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)); } @@ -104,6 +104,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'])); } @@ -117,7 +119,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')) { const errorList = revalidator.validate(req.body, template); if (errorList.valid) { diff --git a/lib/services/devices/deviceRegistryMongoDB.js b/lib/services/devices/deviceRegistryMongoDB.js index 9cd812dcd..9912e856c 100644 --- a/lib/services/devices/deviceRegistryMongoDB.js +++ b/lib/services/devices/deviceRegistryMongoDB.js @@ -302,8 +302,9 @@ function clear(callback) { function itemToObject(i) { if (i.toObject) { return i.toObject(); + } else { + return i; } - return i; } function getDevicesByAttribute(name, value, service, subservice, callback) { diff --git a/lib/services/devices/deviceService.js b/lib/services/devices/deviceService.js index 5d906722b..8c829a4bc 100644 --- a/lib/services/devices/deviceService.js +++ b/lib/services/devices/deviceService.js @@ -29,456 +29,33 @@ /* eslint-disable prefer-rest-params */ /* eslint-disable consistent-return */ -const request = require('request'); const async = require('async'); const apply = async.apply; -const uuid = require('uuid'); -const constants = require('../../constants'); -const domain = require('domain'); const intoTrans = require('../common/domain').intoTrans; -const alarms = require('../common/alarmManagement'); const groupService = require('../groups/groupService'); -const ngsiService = require('../ngsi/ngsiService'); const errors = require('../../errors'); const logger = require('logops'); const config = require('../../commonConfig'); -const ngsiParser = require('./../ngsi/ngsiParser'); const registrationUtils = require('./registrationUtils'); const subscriptions = require('../ngsi/subscriptionService'); const _ = require('underscore'); -const utils = require('../northBound/restUtils'); -const moment = require('moment'); const context = { op: 'IoTAgentNGSI.DeviceService' }; -/** - * Process the response from a Register Context request for a device, extracting the 'registrationId' and creating the - * device object that will be stored in the registry. - * - * @param {Object} deviceData Object containing all the deviceData needed to send the registration. - * - */ -function processContextRegistration(deviceData, body, callback) { - const newDevice = _.clone(deviceData); - - if (body) { - newDevice.registrationId = body.registrationId; - } - - callback(null, newDevice); -} - -/** - * Creates the response handler for the initial entity creation request NGSIv1. - * 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 createInitialEntityHandlerNgsi1(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 && body && response.statusCode === 200) { - const errorField = ngsiParser.getErrorField(body); - - if (errorField) { - logger.error(context, 'Update error connecting to the Context Broker: %j', errorField); - callback(new errors.BadRequest(JSON.stringify(errorField))); - } else { - alarms.release(constants.ORION_ALARM); - logger.debug(context, 'Initial entity created successfully.'); - callback(null, newDevice); - } - } else { - logger.error( - context, - 'Protocol error connecting to the Context Broker [%d]: %s', - response.statusCode, - body - ); - - const errorObj = new errors.EntityGenericError(deviceData.id, deviceData.type, body); - - callback(errorObj); - } - }; -} - -/** - * Creates the response handler for the initial entity creation request using NGSIv2. - * 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 createInitialEntityHandlerNgsi2(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 === 204) { - alarms.release(constants.ORION_ALARM); - logger.debug(context, 'Initial entity created successfully.'); - callback(null, newDevice); - } else { - logger.error( - context, - 'Protocol error connecting to the Context Broker [%d]: %s', - response.statusCode, - body - ); - - const 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. - * - * @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. - * @return {function} Handler to pass to the request() function. - */ -function updateEntityHandlerNgsi2(deviceData, updatedDevice, callback) { - return function handleEntityResponse(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 === 204) { - alarms.release(constants.ORION_ALARM); - logger.debug(context, 'Entity updated successfully.'); - callback(null, updatedDevice); - } else { - logger.error( - context, - 'Protocol error connecting to the Context Broker [%d]: %s', - response.statusCode, - body - ); - - const errorObj = new errors.EntityGenericError(deviceData.id, deviceData.type, body); - - callback(errorObj); - } - }; -} - -function getInitialValueForType(type) { - switch (type) { - case constants.LOCATION_TYPE: - return constants.LOCATION_DEFAULT; - case constants.DATETIME_TYPE: - return constants.DATETIME_DEFAULT; - default: - return constants.ATTRIBUTE_DEFAULT; - } -} - -/** - * Concats or merges two JSON objects. - * - * @param {Object} json1 JSON object where objects will be merged. - * @param {Object} json2 JSON object to be merged. - */ -function jsonConcat(json1, json2) { - for (const key in json2) { - if (json2.hasOwnProperty(key)) { - json1[key] = json2[key]; - } - } -} - -/** - * Formats device's attributes in NGSIv2 format. - * - * @param {Object} originalVector Original vector which contains all the device information and attributes. - * @param {Object} staticAtts Flag that defined if the device'attributes are static. - * @return {Object} List of device's attributes formatted in NGSIv2. - */ -function formatAttributesNgsi2(originalVector, staticAtts) { - const attributeList = {}; - - if (originalVector && originalVector.length) { - for (let i = 0; i < originalVector.length; i++) { - // (#628) check if attribute has entity_name: - // In that case attribute should not be appear in current entity - /*jshint camelcase: false */ - - if (!originalVector[i].entity_name) { - attributeList[originalVector[i].name] = { - type: originalVector[i].type, - value: getInitialValueForType(originalVector[i].type) - }; - if (staticAtts) { - attributeList[originalVector[i].name].value = originalVector[i].value; - } else { - attributeList[originalVector[i].name].value = getInitialValueForType(originalVector[i].type); - } - if (originalVector[i].metadata) { - attributeList[originalVector[i].name].metadata = originalVector[i].metadata; - } - } - } - } - - return attributeList; -} - -/** - * Formats device's commands in NGSIv2 format. - * - * @param {Object} originalVector Original vector which contains all the device information and attributes. - * @return {Object} List of device's commands formatted in NGSIv2. - */ -function formatCommandsNgsi2(originalVector) { - const attributeList = {}; - - if (originalVector && originalVector.length) { - for (let i = 0; i < originalVector.length; i++) { - attributeList[originalVector[i].name + constants.COMMAND_STATUS_SUFIX] = { - type: constants.COMMAND_STATUS, - value: 'UNKNOWN' - }; - attributeList[originalVector[i].name + constants.COMMAND_RESULT_SUFIX] = { - type: constants.COMMAND_RESULT, - value: ' ' - }; - } - } - - return attributeList; -} - -/** - * Executes a request operation using security information if available - * - * @param {String} requestOptions Request options to be sent. - * @param {String} deviceData Device data. - */ -function executeWithSecurity(requestOptions, deviceData, callback) { - logger.debug(context, 'executeWithSecurity'); - config.getGroupRegistry().getType(deviceData.type, function (error, deviceGroup) { - let typeInformation; - if (error) { - logger.debug(context, 'error %j in get group device', error); - } - - if (deviceGroup) { - typeInformation = deviceGroup; - } else { - typeInformation = config.getConfig().types[deviceData.type]; - } - - if (config.getConfig().authentication && config.getConfig().authentication.enabled) { - const security = config.getSecurityService(); - if (typeInformation && typeInformation.trust) { - async.waterfall( - [ - apply(security.auth, typeInformation.trust), - apply(ngsiService.updateTrust, deviceGroup, null, typeInformation.trust), - apply(security.getToken, typeInformation.trust) - ], - function (error, token) { - if (error) { - callback(new errors.SecurityInformationMissing(typeInformation.type)); - } else { - requestOptions.headers[config.getConfig().authentication.header] = token; - request(requestOptions, callback); - } - } - ); - } else { - callback( - new errors.SecurityInformationMissing(typeInformation ? typeInformation.type : deviceData.type) - ); - } - } else { - request(requestOptions, 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. - * - * @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 createInitialEntityNgsi2(deviceData, newDevice, callback) { - const options = { - url: config.getConfig().contextBroker.url + '/v2/entities?options=upsert', - method: 'POST', - json: { - id: String(deviceData.name), - type: deviceData.type - }, - headers: { - 'fiware-service': deviceData.service, - 'fiware-servicepath': deviceData.subservice, - 'fiware-correlator': (domain.active && domain.active.corr) || uuid.v4() - } - }; - - if (deviceData.cbHost && deviceData.cbHost.indexOf('://') !== -1) { - options.url = deviceData.cbHost + '/v2/entities?options=upsert'; - } else if (deviceData.cbHost && deviceData.cbHost.indexOf('://') === -1) { - options.url = 'http://' + deviceData.cbHost + '/v2/entities?options=upsert'; - } - - jsonConcat(options.json, formatAttributesNgsi2(deviceData.active, false)); - jsonConcat(options.json, formatAttributesNgsi2(deviceData.staticAttributes, true)); - jsonConcat(options.json, formatCommandsNgsi2(deviceData.commands)); - - logger.debug(context, 'deviceData: %j', deviceData); - if ( - ('timestamp' in deviceData && deviceData.timestamp !== undefined - ? deviceData.timestamp - : config.getConfig().timestamp) && - !utils.isTimestampedNgsi2(options.json) - ) { - logger.debug(context, 'config.timestamp %s %s', deviceData.timestamp, config.getConfig().timestamp); - options.json[constants.TIMESTAMP_ATTRIBUTE] = { - type: constants.TIMESTAMP_TYPE_NGSI2, - 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)); -} +let deviceHandler; /** - * Creates the initial entity representing the device in the Context Broker using NGSIv1. - * This is important mainly to allow the rest of the updateContext operations to be performed - * using an UPDATE action instead of an APPEND one. - * - * @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. + * Loads the correct device handler based on the current config. */ -function createInitialEntityNgsi1(deviceData, newDevice, callback) { - let 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; - } - const options = { - url: cbHost + '/v1/updateContext', - method: 'POST', - json: { - contextElements: [ - { - type: deviceData.type, - isPattern: 'false', - id: String(deviceData.name), - attributes: [] - } - ], - updateAction: 'APPEND' - }, - headers: { - 'fiware-service': deviceData.service, - 'fiware-servicepath': deviceData.subservice, - 'fiware-correlator': (domain.active && domain.active.corr) || uuid.v4() - } - }; - - function formatAttributes(originalVector) { - const attributeList = []; - - if (originalVector && originalVector.length) { - for (let i = 0; i < originalVector.length; i++) { - // (#628) check if attribute has entity_name: - // In that case attribute should not be appear in current entity - /*jshint camelcase: false */ - if (!originalVector[i].entity_name) { - attributeList.push({ - name: originalVector[i].name, - type: originalVector[i].type, - value: getInitialValueForType(originalVector[i].type) - }); - } - } - } - - return attributeList; - } - - function formatCommands(originalVector) { - const attributeList = []; - - if (originalVector && originalVector.length) { - for (let i = 0; i < originalVector.length; i++) { - attributeList.push({ - name: originalVector[i].name + constants.COMMAND_STATUS_SUFIX, - type: constants.COMMAND_STATUS, - value: 'UNKNOWN' - }); - attributeList.push({ - name: originalVector[i].name + constants.COMMAND_RESULT_SUFIX, - type: constants.COMMAND_RESULT, - value: ' ' - }); - } - } - - return attributeList; - } - - options.json.contextElements[0].attributes = [].concat( - formatAttributes(deviceData.active), - deviceData.staticAttributes, - formatCommands(deviceData.commands) - ); - - if ( - ('timestamp' in deviceData && deviceData.timestamp !== undefined - ? deviceData.timestamp - : config.getConfig().timestamp) && - !utils.isTimestamped(options.json) - ) { - options.json.contextElements[0].attributes.push({ - name: constants.TIMESTAMP_ATTRIBUTE, - type: constants.TIMESTAMP_TYPE, - value: ' ' - }); +function init() { + if (config.checkNgsiLD()) { + deviceHandler = require('./devices-NGSI-LD'); + } else if (config.checkNgsi2()) { + deviceHandler = require('./devices-NGSI-v2'); + } else { + deviceHandler = require('./devices-NGSI-v1'); } - - logger.debug(context, 'Creating initial entity in the Context Broker:\n %s', JSON.stringify(options, null, 4)); - executeWithSecurity(options, newDevice, createInitialEntityHandlerNgsi1(deviceData, newDevice, callback)); } /** @@ -489,65 +66,7 @@ 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()) { - createInitialEntityNgsi2(deviceData, newDevice, callback); - } else { - createInitialEntityNgsi1(deviceData, newDevice, callback); - } -} - -/** - * Updates the entity representing the device in the Context Broker using NGSIv2. - * - * @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 updateEntityNgsi2(deviceData, updatedDevice, callback) { - const options = { - url: config.getConfig().contextBroker.url + '/v2/entities/' + String(deviceData.name) + '/attrs', - method: 'POST', - json: {}, - headers: { - 'fiware-service': deviceData.service, - 'fiware-servicepath': deviceData.subservice, - 'fiware-correlator': (domain.active && domain.active.corr) || uuid.v4() - } - }; - - if (deviceData.cbHost && deviceData.cbHost.indexOf('://') !== -1) { - options.url = deviceData.cbHost + '/v2/entities/' + String(deviceData.name) + '/attrs'; - } else if (deviceData.cbHost && deviceData.cbHost.indexOf('://') === -1) { - options.url = 'http://' + deviceData.cbHost + '/v2/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() - }; - } - - // 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)); - } + deviceHandler.createInitialEntity(deviceData, newDevice, callback); } /** @@ -610,7 +129,6 @@ function mergeDeviceWithConfiguration(fields, defaults, deviceData, configuratio logger.debug(context, 'deviceData before merge with conf: %j', deviceData); for (let i = 0; i < fields.length; i++) { const confField = fields[i] === 'active' ? 'attributes' : fields[i]; - if (deviceData && deviceData[fields[i]] && ['active', 'lazy', 'commands'].indexOf(fields[i]) >= 0) { deviceData[fields[i]] = deviceData[fields[i]].map(setDefaultAttributeIds); } else if (deviceData && deviceData[fields[i]] && ['internalAttributes'].indexOf(fields[i]) >= 0) { @@ -696,16 +214,14 @@ function findConfigurationGroup(deviceObj, callback) { */ function registerDevice(deviceObj, callback) { function checkDuplicates(deviceObj, innerCb) { - config - .getRegistry() - /* eslint-disable-next-line no-unused-vars */ - .getSilently(deviceObj.id, deviceObj.service, deviceObj.subservice, function (error, device) { - if (!error) { - innerCb(new errors.DuplicateDeviceId(deviceObj.id)); - } else { - innerCb(); - } - }); + /* eslint-disable-next-line no-unused-vars */ + config.getRegistry().get(deviceObj.id, deviceObj.service, deviceObj.subservice, function (error, device) { + if (!error) { + innerCb(new errors.DuplicateDeviceId(deviceObj.id)); + } else { + innerCb(); + } + }); } function prepareDeviceData(deviceObj, configuration, callback) { @@ -730,6 +246,9 @@ function registerDevice(deviceObj, callback) { if (!deviceData.name) { deviceData.name = deviceData.type + ':' + deviceData.id; + if (config.checkNgsiLD()) { + deviceData.name = 'urn:ngsi-ld:' + deviceData.type + ':' + deviceData.id; + } logger.debug(context, 'Device name not found, falling back to deviceType:deviceId [%s]', deviceData.name); } @@ -738,7 +257,6 @@ function registerDevice(deviceObj, callback) { } else { selectedConfiguration = configuration; } - callback(null, deviceData, selectedConfiguration); } @@ -752,7 +270,7 @@ function registerDevice(deviceObj, callback) { async.waterfall( [ apply(registrationUtils.sendRegistrations, false, deviceData), - apply(processContextRegistration, deviceData), + apply(registrationUtils.processContextRegistration, deviceData), apply(createInitialEntity, deviceData) ], function (error, results) { @@ -861,213 +379,8 @@ function unregisterDevice(id, service, subservice, callback) { }); } -/** - * Updates the register of an existing device identified by the Id and Type in the Context Broker, and the internal - * registry. It uses NGSIv1. - * - * The device id and type are required fields for a registration updated. Only the following attributes will be - * updated: lazy, active and internalId. Any other change will be ignored. The registration for the lazy attributes - * of the updated entity will be updated if existing, and created if not. If new active attributes are created, - * the entity will be updated creating the new attributes. - * - * @param {Object} deviceObj Object with all the device information (mandatory). - */ -function updateRegisterDeviceNgsi1(deviceObj, callback) { - if (!deviceObj.id || !deviceObj.type) { - callback(new errors.MissingAttributes('Id or device missing')); - return; - } - - logger.debug(context, 'Update provisioned device in Device Service'); - - function combineWithNewDevice(newDevice, oldDevice, callback) { - if (oldDevice) { - oldDevice.internalId = newDevice.internalId; - oldDevice.lazy = newDevice.lazy; - oldDevice.commands = newDevice.commands; - oldDevice.staticAttributes = newDevice.staticAttributes; - oldDevice.active = newDevice.active; - oldDevice.name = newDevice.name; - oldDevice.type = newDevice.type; - oldDevice.polling = newDevice.polling; - oldDevice.timezone = newDevice.timezone; - if ('timestamp' in newDevice && newDevice.timestamp !== undefined) { - oldDevice.timestamp = newDevice.timestamp; - } - if ('autoprovision' in newDevice && newDevice.autoprovision !== undefined) { - oldDevice.autoprovision = newDevice.autoprovision; - } - oldDevice.endpoint = newDevice.endpoint || oldDevice.endpoint; - - callback(null, oldDevice); - } else { - callback(new errors.DeviceNotFound(newDevice.id)); - } - } - - function getAttributeDifference(oldArray, newArray) { - let oldActiveKeys; - let newActiveKeys; - let updateKeys; - let result; - - if (oldArray && newArray) { - newActiveKeys = _.pluck(newArray, 'name'); - oldActiveKeys = _.pluck(oldArray, 'name'); - - updateKeys = _.difference(newActiveKeys, oldActiveKeys); - - result = newArray.filter(function (attribute) { - return updateKeys.indexOf(attribute.name) >= 0; - }); - } else if (newArray) { - result = newArray; - } else { - result = []; - } - - return result; - } - - function extractDeviceDifference(newDevice, oldDevice, callback) { - const deviceData = { - id: oldDevice.id, - name: oldDevice.name, - type: oldDevice.type, - service: oldDevice.service, - subservice: oldDevice.subservice - }; - - deviceData.active = getAttributeDifference(oldDevice.active, newDevice.active); - deviceData.lazy = getAttributeDifference(oldDevice.lazy, newDevice.lazy); - deviceData.commands = getAttributeDifference(oldDevice.commands, newDevice.commands); - deviceData.staticAttributes = getAttributeDifference(oldDevice.staticAttributes, newDevice.staticAttributes); - - callback(null, deviceData, oldDevice); - } - - async.waterfall( - [ - apply(config.getRegistry().get, deviceObj.id, deviceObj.service, deviceObj.subservice), - apply(extractDeviceDifference, deviceObj), - createInitialEntity, - apply(combineWithNewDevice, deviceObj), - apply(registrationUtils.sendRegistrations, false), - apply(processContextRegistration, deviceObj), - config.getRegistry().update - ], - callback - ); -} - -/** - * Updates the register of an existing device identified by the Id and Type in the Context Broker, and the internal - * registry. It uses NGSIv2. - * - * The device id and type are required fields for a registration updated. Only the following attributes will be - * updated: lazy, active and internalId. Any other change will be ignored. The registration for the lazy attributes - * of the updated entity will be updated if existing, and created if not. If new active attributes are created, - * the entity will be updated creating the new attributes. - * - * @param {Object} deviceObj Object with all the device information (mandatory). - */ -function updateRegisterDeviceNgsi2(deviceObj, callback) { - if (!deviceObj.id || !deviceObj.type) { - callback(new errors.MissingAttributes('Id or device missing')); - return; - } - - logger.debug(context, 'Update provisioned device in Device Service'); - - function combineWithNewDevice(newDevice, oldDevice, callback) { - if (oldDevice) { - oldDevice.internalId = newDevice.internalId; - oldDevice.lazy = newDevice.lazy; - oldDevice.commands = newDevice.commands; - oldDevice.staticAttributes = newDevice.staticAttributes; - oldDevice.active = newDevice.active; - oldDevice.name = newDevice.name; - oldDevice.type = newDevice.type; - oldDevice.polling = newDevice.polling; - oldDevice.timezone = newDevice.timezone; - if ('timestamp' in newDevice && newDevice.timestamp !== undefined) { - oldDevice.timestamp = newDevice.timestamp; - } - if ('autoprovision' in newDevice && newDevice.autoprovision !== undefined) { - oldDevice.autoprovision = newDevice.autoprovision; - } - if ('explicitAttrs' in newDevice && newDevice.explicitAttrs !== undefined) { - oldDevice.explicitAttrs = newDevice.explicitAttrs; - } - oldDevice.endpoint = newDevice.endpoint || oldDevice.endpoint; - - callback(null, oldDevice); - } else { - callback(new errors.DeviceNotFound(newDevice.id)); - } - } - - function getAttributeDifference(oldArray, newArray) { - let oldActiveKeys; - let newActiveKeys; - let updateKeys; - let result; - - if (oldArray && newArray) { - newActiveKeys = _.pluck(newArray, 'name'); - oldActiveKeys = _.pluck(oldArray, 'name'); - - updateKeys = _.difference(newActiveKeys, oldActiveKeys); - - result = newArray.filter(function (attribute) { - return updateKeys.indexOf(attribute.name) >= 0; - }); - } else if (newArray) { - result = newArray; - } else { - result = []; - } - - return result; - } - - function extractDeviceDifference(newDevice, oldDevice, callback) { - const deviceData = { - id: oldDevice.id, - name: oldDevice.name, - type: oldDevice.type, - service: oldDevice.service, - subservice: oldDevice.subservice - }; - - deviceData.active = getAttributeDifference(oldDevice.active, newDevice.active); - deviceData.lazy = getAttributeDifference(oldDevice.lazy, newDevice.lazy); - deviceData.commands = getAttributeDifference(oldDevice.commands, newDevice.commands); - deviceData.staticAttributes = getAttributeDifference(oldDevice.staticAttributes, newDevice.staticAttributes); - - 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 - ); -} - function updateRegisterDevice(deviceObj, callback) { - if (config.checkNgsi2()) { - updateRegisterDeviceNgsi2(deviceObj, callback); - } else { - updateRegisterDeviceNgsi1(deviceObj, callback); - } + deviceHandler.updateRegisterDevice(deviceObj, callback); } /** @@ -1278,4 +591,4 @@ exports.clearRegistry = intoTrans(context, checkRegistry)(clearRegistry); exports.retrieveDevice = intoTrans(context, checkRegistry)(retrieveDevice); exports.mergeDeviceWithConfiguration = mergeDeviceWithConfiguration; exports.findConfigurationGroup = findConfigurationGroup; -exports.executeWithSecurity = executeWithSecurity; +exports.init = init; diff --git a/lib/services/devices/devices-NGSI-LD.js b/lib/services/devices/devices-NGSI-LD.js new file mode 100644 index 000000000..e160427d3 --- /dev/null +++ b/lib/services/devices/devices-NGSI-LD.js @@ -0,0 +1,371 @@ +/* + * 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 request = require('request'); +const async = require('async'); +const apply = async.apply; +const constants = require('../../constants'); +const alarms = require('../common/alarmManagement'); +const errors = require('../../errors'); +const logger = require('logops'); +const config = require('../../commonConfig'); +const ngsiLD = require('../ngsi/entities-NGSI-LD'); +const utils = require('../northBound/restUtils'); +const moment = require('moment'); +const _ = require('underscore'); +const registrationUtils = require('./registrationUtils'); +const NGSIv2 = require('./devices-NGSI-v2'); +const context = { + op: 'IoTAgentNGSI.Devices-LD' +}; + +/** + * Concats or merges two JSON objects. + * + * @param {Object} json1 JSON object where objects will be merged. + * @param {Object} json2 JSON object to be merged. + */ +function jsonConcat(json1, json2) { + for (const key in json2) { + /* eslint-disable-next-line no-prototype-builtins */ + if (json2.hasOwnProperty(key)) { + json1[key] = json2[key]; + } + } +} + +/** + * 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); + } + // Handling different response codes for batch entity upsert in NGSI-LD specification: + // - In v1.2.1, response code is 200 + // - In v1.3.1, response code is 201 if some created entities, 204 if just updated existing + else if ( + response && + (response.statusCode === 200 || response.statusCode === 201 || response.statusCode === 204) + ) { + alarms.release(constants.ORION_ALARM); + logger.debug(context, 'Initial entity created successfully.'); + callback(null, newDevice); + } else { + logger.error( + context, + 'Protocol error connecting to the Context Broker [%d]: %s', + response.statusCode, + body + ); + + const errorObj = new errors.EntityGenericError(deviceData.id, deviceData.type, body); + + callback(errorObj); + } + }; +} + +/** + * Creates the response handler for the update entity 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} updatedDevice Device object that will be stored in the database. + * @return {function} Handler to pass to the request() function. + */ +function updateEntityHandlerNgsiLD(deviceData, updatedDevice, callback) { + return function handleEntityResponse(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); + } + // Handling different response codes for batch entity upsert in NGSI-LD specification: + // - In v1.2.1, response code is 200 + // - In v1.3.1, response code is 204 (not handling 201 as entities already previously created) + else if (response && (response.statusCode === 200 || response.statusCode === 204)) { + alarms.release(constants.ORION_ALARM); + logger.debug(context, 'Entity updated successfully.'); + callback(null, updatedDevice); + } else { + logger.error( + context, + 'Protocol error connecting to the Context Broker [%d]: %s', + response.statusCode, + body + ); + + const errorObj = new errors.EntityGenericError(deviceData.id, deviceData.type, body); + + callback(errorObj); + } + }; +} + +/** + * 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) { + let json = { + id: String(deviceData.name), + type: deviceData.type + }; + + jsonConcat(json, NGSIv2.formatAttributes(deviceData.active, false)); + jsonConcat(json, NGSIv2.formatAttributes(deviceData.staticAttributes, true)); + jsonConcat(json, NGSIv2.formatCommands(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 = ngsiLD.formatAsNGSILD(json); + + const options = { + url: config.getConfig().contextBroker.url + '/ngsi-ld/v1/entityOperations/upsert/', + method: 'POST', + json: [json], + headers: { + 'fiware-service': deviceData.service, + 'fiware-servicepath': deviceData.subservice, + 'NGSILD-Tenant': deviceData.service, + 'NGSILD-Path': deviceData.subservice, + 'Content-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)); + utils.executeWithSecurity(options, newDevice, createInitialEntityHandlerNgsiLD(deviceData, newDevice, 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) { + const options = { + url: config.getConfig().contextBroker.url + '/ngsi-ld/v1/entityOperations/upsert/?options=update', + method: 'POST', + json: {}, + headers: { + 'fiware-service': deviceData.service, + 'fiware-servicepath': deviceData.subservice, + 'NGSILD-Tenant': deviceData.service, + 'NGSILD-Path': deviceData.subservice, + 'Content-Type': 'application/ld+json' + } + }; + + if (deviceData.cbHost && deviceData.cbHost.indexOf('://') !== -1) { + options.url = deviceData.cbHost + '/ngsi-ld/v1/entityOperations/upsert/?options=update'; + } else if (deviceData.cbHost && deviceData.cbHost.indexOf('://') === -1) { + options.url = 'http://' + deviceData.cbHost + '/ngsi-ld/v1/entityOperations/upsert/?options=update'; + } + + /*if (deviceData.type) { + options.url += '?type=' + deviceData.type; + }*/ + + jsonConcat(options.json, NGSIv2.formatAttributes(deviceData.active, false)); + jsonConcat(options.json, NGSIv2.formatAttributes(deviceData.staticAttributes, true)); + jsonConcat(options.json, NGSIv2.formatCommands(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.id = String(deviceData.name); + options.json.type = deviceData.type; + options.json = [ngsiLD.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, updateEntityHandlerNgsiLD(deviceData, updatedDevice, callback)); + } +} + +/** + * Updates the register of an existing device identified by the Id and Type in the Context Broker, and the internal + * registry. It uses NGSIv2. + * + * The device id and type are required fields for a registration updated. Only the following attributes will be + * updated: lazy, active and internalId. Any other change will be ignored. The registration for the lazy attributes + * of the updated entity will be updated if existing, and created if not. If new active attributes are created, + * the entity will be updated creating the new attributes. + * + * @param {Object} deviceObj Object with all the device information (mandatory). + */ +function updateRegisterDeviceNgsiLD(deviceObj, callback) { + if (!deviceObj.id || !deviceObj.type) { + callback(new errors.MissingAttributes('Id or device missing')); + return; + } + + logger.debug(context, 'Update provisioned LD device in Device Service'); + + function combineWithNewDevice(newDevice, oldDevice, callback) { + if (oldDevice) { + oldDevice.internalId = newDevice.internalId; + oldDevice.lazy = newDevice.lazy; + oldDevice.commands = newDevice.commands; + oldDevice.staticAttributes = newDevice.staticAttributes; + oldDevice.active = newDevice.active; + oldDevice.name = newDevice.name; + oldDevice.type = newDevice.type; + oldDevice.polling = newDevice.polling; + oldDevice.timezone = newDevice.timezone; + if ('timestamp' in newDevice && newDevice.timestamp !== undefined) { + oldDevice.timestamp = newDevice.timestamp; + } + if ('autoprovision' in newDevice && newDevice.autoprovision !== undefined) { + oldDevice.autoprovision = newDevice.autoprovision; + } + if ('explicitAttrs' in newDevice && newDevice.explicitAttrs !== undefined) { + oldDevice.explicitAttrs = newDevice.explicitAttrs; + } + + oldDevice.endpoint = newDevice.endpoint || oldDevice.endpoint; + + callback(null, oldDevice); + } else { + callback(new errors.DeviceNotFound(newDevice.id)); + } + } + + function getAttributeDifference(oldArray, newArray) { + let oldActiveKeys; + let newActiveKeys; + let updateKeys; + let result; + + if (oldArray && newArray) { + newActiveKeys = _.pluck(newArray, 'name'); + oldActiveKeys = _.pluck(oldArray, 'name'); + + updateKeys = _.difference(newActiveKeys, oldActiveKeys); + + result = newArray.filter(function (attribute) { + return updateKeys.indexOf(attribute.name) >= 0; + }); + } else if (newArray) { + result = newArray; + } else { + result = []; + } + + return result; + } + + function extractDeviceDifference(newDevice, oldDevice, callback) { + const deviceData = { + id: oldDevice.id, + name: oldDevice.name, + type: oldDevice.type, + service: oldDevice.service, + subservice: oldDevice.subservice + }; + + deviceData.active = getAttributeDifference(oldDevice.active, newDevice.active); + deviceData.lazy = getAttributeDifference(oldDevice.lazy, newDevice.lazy); + deviceData.commands = getAttributeDifference(oldDevice.commands, newDevice.commands); + deviceData.staticAttributes = getAttributeDifference(oldDevice.staticAttributes, newDevice.staticAttributes); + + callback(null, deviceData, oldDevice); + } + + async.waterfall( + [ + apply(config.getRegistry().get, deviceObj.id, deviceObj.service, deviceObj.subservice), + apply(extractDeviceDifference, deviceObj), + updateEntityNgsiLD, + apply(combineWithNewDevice, deviceObj), + apply(registrationUtils.sendRegistrations, false), + apply(registrationUtils.processContextRegistration, deviceObj), + config.getRegistry().update + ], + callback + ); +} + +exports.createInitialEntity = createInitialEntityNgsiLD; +exports.updateRegisterDevice = updateRegisterDeviceNgsiLD; diff --git a/lib/services/devices/devices-NGSI-v1.js b/lib/services/devices/devices-NGSI-v1.js new file mode 100644 index 000000000..330ec43e4 --- /dev/null +++ b/lib/services/devices/devices-NGSI-v1.js @@ -0,0 +1,293 @@ +/* + * 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: Federico M. Facca - Martel Innovate + * Modified by: Daniel Calvo - ATOS Research & Innovation + * Modified by: Jason Fox - FIWARE Foundation + */ + +const async = require('async'); +const apply = async.apply; +const uuid = require('uuid'); +const constants = require('../../constants'); +const domain = require('domain'); +const alarms = require('../common/alarmManagement'); +const errors = require('../../errors'); +const logger = require('logops'); +const config = require('../../commonConfig'); +const ngsiUtils = require('./../ngsi/ngsiUtils'); +const registrationUtils = require('./registrationUtils'); +const _ = require('underscore'); +const utils = require('../northBound/restUtils'); +const context = { + op: 'IoTAgentNGSI.Devices-v1' +}; + +/** + * Creates the response handler for the initial entity creation request NGSIv1. + * 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 createInitialEntityHandlerNgsi1(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 && body && response.statusCode === 200) { + const errorField = ngsiUtils.getErrorField(body); + + if (errorField) { + logger.error(context, 'Update error connecting to the Context Broker: %j', errorField); + callback(new errors.BadRequest(JSON.stringify(errorField))); + } else { + alarms.release(constants.ORION_ALARM); + logger.debug(context, 'Initial entity created successfully.'); + callback(null, newDevice); + } + } else { + logger.error( + context, + 'Protocol error connecting to the Context Broker [%d]: %s', + response.statusCode, + body + ); + + const errorObj = new errors.EntityGenericError(deviceData.id, deviceData.type, body); + + callback(errorObj); + } + }; +} + +/** + * Creates the initial entity representing the device in the Context Broker using NGSIv1. + * This is important mainly to allow the rest of the updateContext operations to be performed + * using an UPDATE action instead of an APPEND one. + * + * @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 createInitialEntityNgsi1(deviceData, newDevice, callback) { + let 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; + } + const options = { + url: cbHost + '/v1/updateContext', + method: 'POST', + json: { + contextElements: [ + { + type: deviceData.type, + isPattern: 'false', + id: String(deviceData.name), + attributes: [] + } + ], + updateAction: 'APPEND' + }, + headers: { + 'fiware-service': deviceData.service, + 'fiware-servicepath': deviceData.subservice, + 'fiware-correlator': (domain.active && domain.active.corr) || uuid.v4() + } + }; + + function formatAttributes(originalVector) { + const attributeList = []; + + if (originalVector && originalVector.length) { + for (let i = 0; i < originalVector.length; i++) { + // (#628) check if attribute has entity_name: + // In that case attribute should not be appear in current entity + /*jshint camelcase: false */ + if (!originalVector[i].entity_name) { + attributeList.push({ + name: originalVector[i].name, + type: originalVector[i].type, + value: constants.getInitialValueForType(originalVector[i].type) + }); + } + } + } + + return attributeList; + } + + function formatCommands(originalVector) { + const attributeList = []; + + if (originalVector && originalVector.length) { + for (let i = 0; i < originalVector.length; i++) { + attributeList.push({ + name: originalVector[i].name + constants.COMMAND_STATUS_SUFIX, + type: constants.COMMAND_STATUS, + value: 'UNKNOWN' + }); + attributeList.push({ + name: originalVector[i].name + constants.COMMAND_RESULT_SUFIX, + type: constants.COMMAND_RESULT, + value: ' ' + }); + } + } + + return attributeList; + } + + options.json.contextElements[0].attributes = [].concat( + formatAttributes(deviceData.active), + deviceData.staticAttributes, + formatCommands(deviceData.commands) + ); + + if ( + ('timestamp' in deviceData && deviceData.timestamp !== undefined + ? deviceData.timestamp + : config.getConfig().timestamp) && + !utils.isTimestamped(options.json) + ) { + options.json.contextElements[0].attributes.push({ + name: constants.TIMESTAMP_ATTRIBUTE, + type: constants.TIMESTAMP_TYPE, + value: ' ' + }); + } + + logger.debug(context, 'Creating initial entity in the Context Broker:\n %s', JSON.stringify(options, null, 4)); + utils.executeWithSecurity(options, newDevice, createInitialEntityHandlerNgsi1(deviceData, newDevice, callback)); +} + +/** + * Updates the register of an existing device identified by the Id and Type in the Context Broker, and the internal + * registry. It uses NGSIv1. + * + * The device id and type are required fields for a registration updated. Only the following attributes will be + * updated: lazy, active and internalId. Any other change will be ignored. The registration for the lazy attributes + * of the updated entity will be updated if existing, and created if not. If new active attributes are created, + * the entity will be updated creating the new attributes. + * + * @param {Object} deviceObj Object with all the device information (mandatory). + */ +function updateRegisterDeviceNgsi1(deviceObj, callback) { + if (!deviceObj.id || !deviceObj.type) { + callback(new errors.MissingAttributes('Id or device missing')); + return; + } + + logger.debug(context, 'Update provisioned v1 device in Device Service'); + + function combineWithNewDevice(newDevice, oldDevice, callback) { + if (oldDevice) { + oldDevice.internalId = newDevice.internalId; + oldDevice.lazy = newDevice.lazy; + oldDevice.commands = newDevice.commands; + oldDevice.staticAttributes = newDevice.staticAttributes; + oldDevice.active = newDevice.active; + oldDevice.name = newDevice.name; + oldDevice.type = newDevice.type; + oldDevice.polling = newDevice.polling; + oldDevice.timezone = newDevice.timezone; + if ('timestamp' in newDevice && newDevice.timestamp !== undefined) { + oldDevice.timestamp = newDevice.timestamp; + } + if ('autoprovision' in newDevice && newDevice.autoprovision !== undefined) { + oldDevice.autoprovision = newDevice.autoprovision; + } + oldDevice.endpoint = newDevice.endpoint || oldDevice.endpoint; + + callback(null, oldDevice); + } else { + callback(new errors.DeviceNotFound(newDevice.id)); + } + } + + function getAttributeDifference(oldArray, newArray) { + let oldActiveKeys; + let newActiveKeys; + let updateKeys; + let result; + + if (oldArray && newArray) { + newActiveKeys = _.pluck(newArray, 'name'); + oldActiveKeys = _.pluck(oldArray, 'name'); + + updateKeys = _.difference(newActiveKeys, oldActiveKeys); + + result = newArray.filter(function (attribute) { + return updateKeys.indexOf(attribute.name) >= 0; + }); + } else if (newArray) { + result = newArray; + } else { + result = []; + } + + return result; + } + + function extractDeviceDifference(newDevice, oldDevice, callback) { + const deviceData = { + id: oldDevice.id, + name: oldDevice.name, + type: oldDevice.type, + service: oldDevice.service, + subservice: oldDevice.subservice + }; + + deviceData.active = getAttributeDifference(oldDevice.active, newDevice.active); + deviceData.lazy = getAttributeDifference(oldDevice.lazy, newDevice.lazy); + deviceData.commands = getAttributeDifference(oldDevice.commands, newDevice.commands); + deviceData.staticAttributes = getAttributeDifference(oldDevice.staticAttributes, newDevice.staticAttributes); + + callback(null, deviceData, oldDevice); + } + + async.waterfall( + [ + apply(config.getRegistry().get, deviceObj.id, deviceObj.service, deviceObj.subservice), + apply(extractDeviceDifference, deviceObj), + createInitialEntityNgsi1, + apply(combineWithNewDevice, deviceObj), + apply(registrationUtils.sendRegistrations, false), + apply(registrationUtils.processContextRegistration, deviceObj), + config.getRegistry().update + ], + callback + ); +} + +exports.createInitialEntity = createInitialEntityNgsi1; +exports.updateRegisterDevice = updateRegisterDeviceNgsi1; diff --git a/lib/services/devices/devices-NGSI-v2.js b/lib/services/devices/devices-NGSI-v2.js new file mode 100644 index 000000000..e8c2459ae --- /dev/null +++ b/lib/services/devices/devices-NGSI-v2.js @@ -0,0 +1,413 @@ +/* + * 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: Federico M. Facca - Martel Innovate + * Modified by: Daniel Calvo - ATOS Research & Innovation + * Modified by: Jason Fox - FIWARE Foundation + */ + +const request = require('request'); +const async = require('async'); +const apply = async.apply; +const uuid = require('uuid'); +const constants = require('../../constants'); +const domain = require('domain'); +const alarms = require('../common/alarmManagement'); +const errors = require('../../errors'); +const logger = require('logops'); +const config = require('../../commonConfig'); +const registrationUtils = require('./registrationUtils'); +const _ = require('underscore'); +const utils = require('../northBound/restUtils'); +const moment = require('moment'); +const context = { + op: 'IoTAgentNGSI.Devices-v2' +}; + +/** + * Concats or merges two JSON objects. + * + * @param {Object} json1 JSON object where objects will be merged. + * @param {Object} json2 JSON object to be merged. + */ +function jsonConcat(json1, json2) { + for (const key in json2) { + /* eslint-disable-next-line no-prototype-builtins */ + if (json2.hasOwnProperty(key)) { + json1[key] = json2[key]; + } + } +} + +/** + * Creates the response handler for the initial entity creation request using NGSIv2. + * 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 createInitialEntityHandlerNgsi2(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 === 204) { + alarms.release(constants.ORION_ALARM); + logger.debug(context, 'Initial entity created successfully.'); + callback(null, newDevice); + } else { + logger.error( + context, + 'Protocol error connecting to the Context Broker [%d]: %s', + response.statusCode, + body + ); + + const 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. + * + * @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. + * @return {function} Handler to pass to the request() function. + */ +function updateEntityHandlerNgsi2(deviceData, updatedDevice, callback) { + return function handleEntityResponse(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 === 204) { + alarms.release(constants.ORION_ALARM); + logger.debug(context, 'Entity updated successfully.'); + callback(null, updatedDevice); + } else { + logger.error( + context, + 'Protocol error connecting to the Context Broker [%d]: %s', + response.statusCode, + body + ); + + const errorObj = new errors.EntityGenericError(deviceData.id, deviceData.type, body); + + callback(errorObj); + } + }; +} + +/** + * Formats device's attributes in NGSIv2 format. + * + * @param {Object} originalVector Original vector which contains all the device information and attributes. + * @param {Object} staticAtts Flag that defined if the device'attributes are static. + * @return {Object} List of device's attributes formatted in NGSIv2. + */ +function formatAttributesNgsi2(originalVector, staticAtts) { + const attributeList = {}; + + if (originalVector && originalVector.length) { + for (let i = 0; i < originalVector.length; i++) { + // (#628) check if attribute has entity_name: + // In that case attribute should not be appear in current entity + /*jshint camelcase: false */ + + if (!originalVector[i].entity_name) { + attributeList[originalVector[i].name] = { + type: originalVector[i].type, + value: constants.getInitialValueForType(originalVector[i].type) + }; + if (staticAtts) { + attributeList[originalVector[i].name].value = originalVector[i].value; + } else { + attributeList[originalVector[i].name].value = constants.getInitialValueForType( + originalVector[i].type + ); + } + if (originalVector[i].metadata) { + attributeList[originalVector[i].name].metadata = originalVector[i].metadata; + } + } + } + } + + return attributeList; +} + +/** + * Formats device's commands in NGSIv2 format. + * + * @param {Object} originalVector Original vector which contains all the device information and attributes. + * @return {Object} List of device's commands formatted in NGSIv2. + */ +function formatCommandsNgsi2(originalVector) { + const attributeList = {}; + + if (originalVector && originalVector.length) { + for (let i = 0; i < originalVector.length; i++) { + attributeList[originalVector[i].name + constants.COMMAND_STATUS_SUFIX] = { + type: constants.COMMAND_STATUS, + value: 'UNKNOWN' + }; + attributeList[originalVector[i].name + constants.COMMAND_RESULT_SUFIX] = { + type: constants.COMMAND_RESULT, + value: ' ' + }; + } + } + + return attributeList; +} + +/** + * 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. + * + * @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 createInitialEntityNgsi2(deviceData, newDevice, callback) { + const options = { + url: config.getConfig().contextBroker.url + '/v2/entities?options=upsert', + method: 'POST', + json: { + id: String(deviceData.name), + type: deviceData.type + }, + headers: { + 'fiware-service': deviceData.service, + 'fiware-servicepath': deviceData.subservice, + 'fiware-correlator': (domain.active && domain.active.corr) || uuid.v4() + } + }; + + if (deviceData.cbHost && deviceData.cbHost.indexOf('://') !== -1) { + options.url = deviceData.cbHost + '/v2/entities?options=upsert'; + } else if (deviceData.cbHost && deviceData.cbHost.indexOf('://') === -1) { + options.url = 'http://' + deviceData.cbHost + '/v2/entities?options=upsert'; + } + + jsonConcat(options.json, formatAttributesNgsi2(deviceData.active, false)); + jsonConcat(options.json, formatAttributesNgsi2(deviceData.staticAttributes, true)); + jsonConcat(options.json, formatCommandsNgsi2(deviceData.commands)); + + logger.debug(context, 'deviceData: %j', deviceData); + if ( + ('timestamp' in deviceData && deviceData.timestamp !== undefined + ? deviceData.timestamp + : config.getConfig().timestamp) && + !utils.isTimestampedNgsi2(options.json) + ) { + logger.debug(context, 'config.timestamp %s %s', deviceData.timestamp, config.getConfig().timestamp); + options.json[constants.TIMESTAMP_ATTRIBUTE] = { + type: constants.TIMESTAMP_TYPE_NGSI2, + value: moment() + }; + } + logger.debug(context, 'Creating initial entity in the Context Broker:\n %s', JSON.stringify(options, null, 4)); + utils.executeWithSecurity(options, newDevice, createInitialEntityHandlerNgsi2(deviceData, newDevice, callback)); +} + +/** + * Updates the entity representing the device in the Context Broker using NGSIv2. + * + * @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 updateEntityNgsi2(deviceData, updatedDevice, callback) { + const options = { + url: config.getConfig().contextBroker.url + '/v2/entities/' + String(deviceData.name) + '/attrs', + method: 'POST', + json: {}, + headers: { + 'fiware-service': deviceData.service, + 'fiware-servicepath': deviceData.subservice, + 'fiware-correlator': (domain.active && domain.active.corr) || uuid.v4() + } + }; + + if (deviceData.cbHost && deviceData.cbHost.indexOf('://') !== -1) { + options.url = deviceData.cbHost + '/v2/entities/' + String(deviceData.name) + '/attrs'; + } else if (deviceData.cbHost && deviceData.cbHost.indexOf('://') === -1) { + options.url = 'http://' + deviceData.cbHost + '/v2/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() + }; + } + + // 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)); + } +} + +/** + * Updates the register of an existing device identified by the Id and Type in the Context Broker, and the internal + * registry. It uses NGSIv2. + * + * The device id and type are required fields for a registration updated. Only the following attributes will be + * updated: lazy, active and internalId. Any other change will be ignored. The registration for the lazy attributes + * of the updated entity will be updated if existing, and created if not. If new active attributes are created, + * the entity will be updated creating the new attributes. + * + * @param {Object} deviceObj Object with all the device information (mandatory). + */ +function updateRegisterDeviceNgsi2(deviceObj, callback) { + if (!deviceObj.id || !deviceObj.type) { + callback(new errors.MissingAttributes('Id or device missing')); + return; + } + + logger.debug(context, 'Update provisioned v2 device in Device Service'); + + function combineWithNewDevice(newDevice, oldDevice, callback) { + if (oldDevice) { + oldDevice.internalId = newDevice.internalId; + oldDevice.lazy = newDevice.lazy; + oldDevice.commands = newDevice.commands; + oldDevice.staticAttributes = newDevice.staticAttributes; + oldDevice.active = newDevice.active; + oldDevice.name = newDevice.name; + oldDevice.type = newDevice.type; + oldDevice.polling = newDevice.polling; + oldDevice.timezone = newDevice.timezone; + if ('timestamp' in newDevice && newDevice.timestamp !== undefined) { + oldDevice.timestamp = newDevice.timestamp; + } + if ('autoprovision' in newDevice && newDevice.autoprovision !== undefined) { + oldDevice.autoprovision = newDevice.autoprovision; + } + if ('explicitAttrs' in newDevice && newDevice.explicitAttrs !== undefined) { + oldDevice.explicitAttrs = newDevice.explicitAttrs; + } + + oldDevice.endpoint = newDevice.endpoint || oldDevice.endpoint; + + callback(null, oldDevice); + } else { + callback(new errors.DeviceNotFound(newDevice.id)); + } + } + + function getAttributeDifference(oldArray, newArray) { + let oldActiveKeys; + let newActiveKeys; + let updateKeys; + let result; + + if (oldArray && newArray) { + newActiveKeys = _.pluck(newArray, 'name'); + oldActiveKeys = _.pluck(oldArray, 'name'); + + updateKeys = _.difference(newActiveKeys, oldActiveKeys); + + result = newArray.filter(function (attribute) { + return updateKeys.indexOf(attribute.name) >= 0; + }); + } else if (newArray) { + result = newArray; + } else { + result = []; + } + + return result; + } + + function extractDeviceDifference(newDevice, oldDevice, callback) { + const deviceData = { + id: oldDevice.id, + name: oldDevice.name, + type: oldDevice.type, + service: oldDevice.service, + subservice: oldDevice.subservice + }; + + deviceData.active = getAttributeDifference(oldDevice.active, newDevice.active); + deviceData.lazy = getAttributeDifference(oldDevice.lazy, newDevice.lazy); + deviceData.commands = getAttributeDifference(oldDevice.commands, newDevice.commands); + deviceData.staticAttributes = getAttributeDifference(oldDevice.staticAttributes, newDevice.staticAttributes); + + 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(registrationUtils.processContextRegistration, deviceObj), + config.getRegistry().update + ], + callback + ); +} + +exports.createInitialEntity = createInitialEntityNgsi2; +exports.updateRegisterDevice = updateRegisterDeviceNgsi2; +exports.formatCommands = formatCommandsNgsi2; +exports.formatAttributes = formatAttributesNgsi2; +exports.updateEntityHandler = updateEntityHandlerNgsi2; diff --git a/lib/services/devices/registrationUtils.js b/lib/services/devices/registrationUtils.js index d0884326f..cb1573cb5 100644 --- a/lib/services/devices/registrationUtils.js +++ b/lib/services/devices/registrationUtils.js @@ -24,17 +24,21 @@ * Modified by: Daniel Calvo - ATOS Research & Innovation */ +/* eslint-disable consistent-return */ + const errors = require('../../errors'); const logger = require('logops'); const _ = require('underscore'); const intoTrans = require('../common/domain').intoTrans; const config = require('../../commonConfig'); -const ngsiParser = require('./../ngsi/ngsiParser'); +const ngsiUtils = require('./../ngsi/ngsiUtils'); const context = { - op: 'IoTAgentNGSI.DeviceService' + op: 'IoTAgentNGSI.Registration' }; const async = require('async'); -const deviceService = require('./deviceService'); +const utils = require('../northBound/restUtils'); + +const NGSI_LD_URN = 'urn:ngsi-ld:'; /** * Generates a handler for the registration requests that checks all the possible errors derived from the registration. @@ -50,7 +54,7 @@ function createRegistrationHandler(unregister, deviceData, callback) { logger.error(context, 'ORION-002: Connection error sending registrations to the Context Broker: %s', error); callback(error); } else if (response && body && response.statusCode === 200) { - const errorField = ngsiParser.getErrorField(body); + const errorField = ngsiUtils.getErrorField(body); if (errorField) { logger.error(context, 'Registration error connecting to the Context Broker: %j', errorField); @@ -75,6 +79,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) { + /* eslint-disable-next-line no-unused-vars */ + 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 { + let 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. @@ -196,14 +241,10 @@ function sendRegistrationsNgsi1(unregister, deviceData, callback) { logger.debug(context, 'No Context Provider registrations found for unregister'); callback(null, deviceData); } else { - logger.debug(context, 'Sending device registrations to Context Broker at [%s]', options.url); + logger.debug(context, 'Sending v1 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, - createRegistrationHandler(unregister, deviceData, callback) - ); + utils.executeWithSecurity(options, deviceData, createRegistrationHandler(unregister, deviceData, callback)); } } @@ -234,27 +275,26 @@ function sendUnregistrationsNgsi2(deviceData, callback) { options.url = 'http://' + deviceData.cbHost + '/v2/registrations/' + deviceData.registrationId; } if (deviceData.registrationId) { - logger.debug(context, 'Sending device unregistrations to Context Broker at [%s]', options.url); + logger.debug(context, 'Sending v2 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 utils.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) { let cbHost = config.getConfig().contextBroker.url; if (deviceData.cbHost && deviceData.cbHost.indexOf('://') !== -1) { cbHost = deviceData.cbHost; @@ -262,30 +302,43 @@ function sendRegistrationsNgsi2(unregister, deviceData, callback) { cbHost = 'http://' + deviceData.cbHost; } const 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 + 'fiware-servicepath': deviceData.subservice, + 'NGSILD-Tenant': deviceData.service, + 'NGSILD-Path': 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 LD 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 utils.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) { const attributeList = []; @@ -324,30 +377,122 @@ function sendRegistrationsNgsi2(unregister, deviceData, callback) { } if (unregister) { - sendUnregistrationsNgsi2(deviceData, callback); - } else 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) - ); + return sendUnregistrationsNgsi2(deviceData, callback); + } + if (deviceData.registrationId) { + return updateRegistrationNgsi2(deviceData, callback); + } + + let 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; + } + const 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 v2 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)); + utils.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); + } + + const properties = []; + const lazy = deviceData.lazy || []; + const commands = deviceData.commands || []; + + lazy.forEach((element) => { + properties.push(element.name); + }); + commands.forEach((element) => { + properties.push(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); + } + + let 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; + } + + let id = String(deviceData.name); + id = id.startsWith(NGSI_LD_URN) ? id : NGSI_LD_URN + deviceData.type + ':' + id; + + const options = { + url: cbHost + '/ngsi-ld/v1/csourceRegistrations/', + method: 'POST', + json: { + type: 'ContextSourceRegistration', + information: [ + { + entities: [{ type: deviceData.type, id }], + properties + } + ], + endpoint: config.getConfig().providerUrl, + '@context': config.getConfig().contextBroker.jsonLdContext + }, + headers: { + 'fiware-service': deviceData.service, + 'fiware-servicepath': deviceData.subservice, + 'NGSILD-Tenant': deviceData.service, + 'NGSILD-Path': deviceData.subservice, + 'Content-Type': 'application/ld+json' + } + }; + + logger.debug(context, 'Sending LD 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)); + + utils.executeWithSecurity(options, deviceData, createRegistrationHandlerNgsiLD(unregister, deviceData, callback)); } /** @@ -361,12 +506,32 @@ 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); } } +/** + * Process the response from a Register Context request for a device, extracting the 'registrationId' and creating the + * device object that will be stored in the registry. + * + * @param {Object} deviceData Object containing all the deviceData needed to send the registration. + * + */ +function processContextRegistration(deviceData, body, callback) { + const newDevice = _.clone(deviceData); + + if (body) { + newDevice.registrationId = body.registrationId; + } + + callback(null, newDevice); +} + exports.sendRegistrations = intoTrans(context, sendRegistrations); exports.createRegistrationHandler = intoTrans(context, createRegistrationHandler); +exports.processContextRegistration = processContextRegistration; diff --git a/lib/services/ngsi/entities-NGSI-LD.js b/lib/services/ngsi/entities-NGSI-LD.js new file mode 100644 index 000000000..1df32821e --- /dev/null +++ b/lib/services/ngsi/entities-NGSI-LD.js @@ -0,0 +1,549 @@ +/* + * 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 + */ + +/* eslint-disable consistent-return */ + +const request = require('request'); +const statsService = require('./../stats/statsRegistry'); +const async = require('async'); +const apply = async.apply; +const alarms = require('../common/alarmManagement'); +const errors = require('../../errors'); +const utils = require('../northBound/restUtils'); +const config = require('../../commonConfig'); +const constants = require('../../constants'); +const moment = require('moment-timezone'); +const logger = require('logops'); +const _ = require('underscore'); +const context = { + op: 'IoTAgentNGSI.Entities-LD' +}; +const NGSIv2 = require('./entities-NGSI-v2'); +const NGSIUtils = require('./ngsiUtils'); + +const NGSI_LD_NULL = { '@type': 'Intangible', '@value': null }; +const NGSI_LD_URN = 'urn:ngsi-ld:'; + +/** + * Determines if a value is a number - Not a Number replaced by Null + * + * @param {String} value Value to be analyzed + * @return {Number} + */ +function valueOfOrNull(value) { + return isNaN(value) ? NGSI_LD_NULL : value; +} + +/** + * @param {String/Array} value Comma separated list or array of values + * @return {Array} Array of Lat/Lngs for use as GeoJSON + */ +function splitLngLat(value) { + const lngLats = typeof value === 'string' || value instanceof String ? value.split(',') : value; + lngLats.forEach((element, index, lngLats) => { + if (Array.isArray(element)) { + lngLats[index] = splitLngLat(element); + } else if ((typeof element === 'string' || element instanceof String) && element.includes(',')) { + lngLats[index] = splitLngLat(element); + } else { + lngLats[index] = Number.parseFloat(element); + } + }); + return lngLats; +} + +/** + * @param {String} value Value to be analyzed + * @return {Array} split pairs of GeoJSON coordinates + */ +function getLngLats(value) { + const lngLats = _.flatten(splitLngLat(value)); + if (lngLats.length === 2) { + return lngLats; + } + + if (lngLats.length % 2 !== 0) { + logger.error(context, 'Bad attribute value type. Expecting geo-coordinates. Received:%s', value); + throw Error(); + } + const arr = []; + for (let i = 0, len = lngLats.length; i < len; i = i + 2) { + arr.push([lngLats[i], lngLats[i + 1]]); + } + return arr; +} + +/** + * 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) { + const obj = { type: 'Property', value: attr.value }; + switch (attr.type.toLowerCase()) { + // Properties + case 'property': + case 'string': + break; + + // Other Native JSON Types + case 'boolean': + obj.value = !!attr.value; + break; + case 'float': + obj.value = valueOfOrNull(Number.parseFloat(attr.value)); + break; + case 'integer': + obj.value = valueOfOrNull(Number.parseInt(attr.value)); + break; + case 'number': + if (NGSIUtils.isFloat(attr.value)) { + obj.value = valueOfOrNull(Number.parseFloat(attr.value)); + } else { + obj.value = valueOfOrNull(Number.parseInt(attr.value)); + } + break; + + // Temporal Properties + case 'datetime': + obj.value = { + '@type': 'DateTime', + '@value': moment.tz(attr.value, 'Etc/UTC').toISOString() + }; + break; + case 'date': + obj.value = { + '@type': 'Date', + '@value': moment.tz(attr.value, 'Etc/UTC').format(moment.HTML5_FMT.DATE) + }; + break; + case 'time': + obj.value = { + '@type': 'Time', + '@value': moment.tz(attr.value, 'Etc/UTC').format(moment.HTML5_FMT.TIME_SECONDS) + }; + break; + + // GeoProperties + case 'geoproperty': + case 'point': + case 'geo:point': + obj.type = 'GeoProperty'; + obj.value = { type: 'Point', coordinates: getLngLats(attr.value) }; + break; + case 'linestring': + case 'geo:linestring': + obj.type = 'GeoProperty'; + obj.value = { type: 'LineString', coordinates: getLngLats(attr.value) }; + break; + case 'polygon': + case 'geo:polygon': + obj.type = 'GeoProperty'; + obj.value = { type: 'Polygon', coordinates: getLngLats(attr.value) }; + break; + case 'multipoint': + case 'geo:multipoint': + obj.type = 'GeoProperty'; + obj.value = { type: 'MultiPoint', coordinates: getLngLats(attr.value) }; + break; + case 'multilinestring': + case 'geo:multilinestring': + obj.type = 'GeoProperty'; + obj.value = { type: 'MultiLineString', coordinates: attr.value }; + break; + case 'multipolygon': + case 'geo:multipolygon': + obj.type = 'GeoProperty'; + obj.value = { type: 'MultiPolygon', coordinates: attr.value }; + break; + + // Relationships + case 'relationship': + obj.type = 'Relationship'; + obj.object = attr.value; + delete obj.value; + break; + + default: + obj.value = { '@type': attr.type, '@value': attr.value }; + } + + if (attr.metadata) { + let timestamp; + Object.keys(attr.metadata).forEach(function (key) { + switch (key) { + case constants.TIMESTAMP_ATTRIBUTE: + timestamp = attr.metadata[key].value; + if (timestamp === constants.ATTRIBUTE_DEFAULT || !moment(timestamp).isValid()) { + obj.observedAt = constants.DATETIME_DEFAULT; + } else { + obj.observedAt = moment.tz(timestamp, 'Etc/UTC').toISOString(); + } + break; + case 'unitCode': + obj.unitCode = attr.metadata[key].value; + break; + default: + obj[key] = convertNGSIv2ToLD(attr.metadata[key]); + } + }); + delete obj.TimeInstant; + } + 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) { + const obj = { '@context': config.getConfig().contextBroker.jsonLdContext }; + let id; + Object.keys(json).forEach(function (key) { + switch (key) { + case 'id': + id = json[key]; + obj[key] = id.startsWith(NGSI_LD_URN) ? id : NGSI_LD_URN + json.type + ':' + id; + break; + case 'type': + obj[key] = json[key]; + break; + case constants.TIMESTAMP_ATTRIBUTE: + // Timestamp should not be added as a root + // element for NSGI-LD. + break; + default: + obj[key] = convertNGSIv2ToLD(json[key]); + } + }); + + delete obj.TimeInstant; + return obj; +} + +/** + * Generate an operation handler for NGSIv2-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) { + const bodyAsString = body ? JSON.stringify(body, null, 4) : ''; + + 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 === 200 || response.statusCode === 204) + ) { + logger.info(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.info(context, 'Received the following response from the CB:\n\n%s\n\n', bodyAsString); + logger.debug(context, 'Value queried successfully'); + alarms.release(constants.ORION_ALARM); + callback(null, body); + } else if (response && operationName === 'query' && response.statusCode === 204) { + logger.info(context, 'Received the following response from the CB:\n\n%s\n\n', bodyAsString); + + 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.info(context, 'Received the following response from the CB:\n\n%s\n\n', bodyAsString); + + logger.error(context, 'Operation ' + operationName + ' error connecting to the Context Broker: %j', body); + + const errorField = NGSIUtils.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)); + } + }; +} + +/** + * Makes a query to the Device's entity in the context broker using NGSI-LD, with the list + * of attributes given by the 'attributes' array. + * + * @param {String} entityName Name of the entity to query. + * @param {Array} attributes Attribute array containing the names of the attributes to query. + * @param {Object} typeInformation Configuration information for the device. + * @param {String} token User token to identify against the PEP Proxies (optional). + */ +function sendQueryValueNgsiLD(entityName, attributes, typeInformation, token, callback) { + let url = '/ngsi-ld/v1/entities/urn:ngsi-ld:' + typeInformation.type + ':' + entityName; + + if (attributes && attributes.length > 0) { + url = url + '?attrs=' + attributes.join(','); + } + + const options = NGSIUtils.createRequestObject(url, typeInformation, token); + options.method = 'GET'; + options.json = true; + + if (!typeInformation || !typeInformation.type) { + callback(new errors.TypeNotFound(null, entityName)); + return; + } + + logger.debug(context, 'Querying values of the device 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)); + + request( + options, + generateNGSILDOperationHandler('query', entityName, typeInformation, token, options, function (error, result) { + if (error) { + callback(error); + } else { + NGSIUtils.applyMiddlewares(NGSIUtils.queryMiddleware, result, typeInformation, 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 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) { + let payload = {}; + + /*var url = '/ngsi-ld/v1/entities/' + entityName + '/attrs'; + + if (typeInformation.type) { + url += '?type=' + typeInformation.type; + }*/ + + const url = '/ngsi-ld/v1/entityOperations/upsert/'; + + const options = NGSIUtils.createRequestObject(url, typeInformation, token); + options.method = 'POST'; + + 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 (let 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 + }; + const metadata = NGSIUtils.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 = NGSIUtils.castJsonNativeAttributes(payload); + async.waterfall( + [ + apply(statsService.add, 'measureRequests', 1), + apply(NGSIUtils.applyMiddlewares, NGSIUtils.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) { + if ( + 'timestamp' in typeInformation && typeInformation.timestamp !== undefined + ? typeInformation.timestamp + : config.getConfig().timestamp + ) { + if (!utils.isTimestampedNgsi2(result)) { + options.json = NGSIv2.addTimestamp(result, typeInformation.timezone); + } else if (!utils.IsValidTimestampedNgsi2(result)) { + logger.error(context, 'Invalid timestamp:%s', JSON.stringify(result)); + callback(new errors.BadTimestamp(result)); + return; + } + } + + options.json = 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 = NGSIv2.addTimestamp(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. + let att; + if (options.json) { + for (let entity = 0; entity < options.json.length; entity++) { + for (att in options.json[entity]) { + /*jshint camelcase: false */ + if (options.json[entity][att].object_id) { + /*jshint camelcase: false */ + delete options.json[entity][att].object_id; + } + if (options.json[entity][att].multi) { + delete options.json[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; + } + } + } + + try { + if (result instanceof Array) { + options.json = _.map(options.json, formatAsNGSILD); + } else { + options.json.id = entityName; + options.json.type = typeInformation.type; + options.json = [formatAsNGSILD(options.json)]; + } + } catch (error) { + return callback(new errors.BadGeocoordinates(JSON.stringify(payload))); + } + + 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) + ); + } + } + ); +} + +exports.formatAsNGSILD = formatAsNGSILD; +exports.sendUpdateValue = sendUpdateValueNgsiLD; +exports.sendQueryValue = sendQueryValueNgsiLD; diff --git a/lib/services/ngsi/entities-NGSI-v1.js b/lib/services/ngsi/entities-NGSI-v1.js new file mode 100644 index 000000000..599a0492d --- /dev/null +++ b/lib/services/ngsi/entities-NGSI-v1.js @@ -0,0 +1,300 @@ +/* + * 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: Federico M. Facca - Martel Innovate + * Modified by: Daniel Calvo - ATOS Research & Innovation + * Modified by: Jason Fox - FIWARE Foundation + */ + +const request = require('request'); +const statsService = require('./../stats/statsRegistry'); +const async = require('async'); +const apply = async.apply; +const alarms = require('../common/alarmManagement'); +const errors = require('../../errors'); +const utils = require('../northBound/restUtils'); +const config = require('../../commonConfig'); +const constants = require('../../constants'); +const moment = require('moment-timezone'); +const logger = require('logops'); +const context = { + op: 'IoTAgentNGSI.Entities-v1' +}; +const ngsiUtils = require('./ngsiUtils'); + +/** + * Generate an operation handler for NGSI-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 generateNGSIOperationHandler(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 && body && response.statusCode === 200) { + const errorField = ngsiUtils.getErrorField(body); + + logger.debug( + context, + 'Received the following request from the CB:\n\n%s\n\n', + JSON.stringify(body, null, 4) + ); + + if (errorField) { + logger.error( + context, + 'Operation ' + operationName + ' error connecting to the Context Broker: %j', + errorField + ); + + if (errorField.code && errorField.code === '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, errorField)); + } + } else { + logger.debug(context, 'Value updated successfully'); + alarms.release(constants.ORION_ALARM); + callback(null, body); + } + } 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 { + logger.debug(context, 'Unknown error executing ' + operationName + ' operation'); + + callback( + new errors.EntityGenericError( + entityName, + typeInformation.type, + { + details: body + }, + response.statusCode + ) + ); + } + }; +} + +function addTimestamp(payload, timezone) { + const timestamp = { + name: constants.TIMESTAMP_ATTRIBUTE, + type: constants.TIMESTAMP_TYPE + }; + + if (!timezone) { + timestamp.value = new Date().toISOString(); + } else { + timestamp.value = moment().tz(timezone).format('YYYY-MM-DD[T]HH:mm:ss.SSSZ'); + } + + function addMetadata(attribute) { + let timestampFound = false; + + if (!attribute.metadatas) { + attribute.metadatas = []; + } + + for (let i = 0; i < attribute.metadatas.length; i++) { + if ( + attribute.metadatas[i].type === constants.TIMESTAMP_TYPE && + attribute.metadatas[i].name === constants.TIMESTAMP_ATTRIBUTE + ) { + attribute.metadatas[i].value = timestamp.value; + timestampFound = true; + break; + } + } + + if (!timestampFound) { + attribute.metadatas.push(timestamp); + } + + return attribute; + } + + payload.contextElements[0].attributes.map(addMetadata); + payload.contextElements[0].attributes.push(timestamp); + return payload; +} + +/** + * Makes a query to the Device's entity in the context broker using NGSIv1, with the list of + * attributes given by the 'attributes' array. + * + * @param {String} entityName Name of the entity to query. + * @param {Array} attributes Attribute array containing the names of the attributes to query. + * @param {Object} typeInformation Configuration information for the device. + * @param {String} token User token to identify against the PEP Proxies (optional). + */ +function sendQueryValueNgsi1(entityName, attributes, typeInformation, token, callback) { + const options = ngsiUtils.createRequestObject('/v1/queryContext', typeInformation, token); + + if (!typeInformation || !typeInformation.type) { + callback(new errors.TypeNotFound(null, entityName)); + return; + } + + options.json = { + entities: [ + { + type: typeInformation.type, + isPattern: 'false', + id: entityName + } + ], + attributes + }; + + logger.debug(context, 'Querying values of the device 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)); + + request( + options, + generateNGSIOperationHandler('query', entityName, typeInformation, token, options, function (error, result) { + if (error) { + callback(error); + } else { + ngsiUtils.applyMiddlewares(ngsiUtils.queryMiddleware, result, typeInformation, 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. + * + * @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 sendUpdateValueNgsi1(entityName, attributes, typeInformation, token, callback) { + const options = ngsiUtils.createRequestObject('/v1/updateContext', typeInformation, token); + + if (typeInformation && typeInformation.staticAttributes) { + attributes = attributes.concat(typeInformation.staticAttributes); + } + + if (!typeInformation || !typeInformation.type) { + callback(new errors.TypeNotFound(null, entityName)); + return; + } + + const payload = { + contextElements: [ + { + type: typeInformation.type, + isPattern: 'false', + id: entityName, + attributes + } + ] + }; + if ( + 'autoprovision' in typeInformation && + /* jshint -W101 */ + typeInformation.autoprovision === undefined + ? typeInformation.autoprovision === true + : config.getConfig().appendMode === true + ) { + payload.updateAction = 'APPEND'; + } else { + payload.updateAction = 'UPDATE'; + } + async.waterfall( + [ + apply(statsService.add, 'measureRequests', 1), + apply(ngsiUtils.applyMiddlewares, ngsiUtils.updateMiddleware, payload, typeInformation) + ], + function (error, result) { + if (error) { + callback(error); + } else { + if (result) { + options.json = result; + } else { + options.json = payload; + } + + if ( + 'timestamp' in typeInformation && typeInformation.timestamp !== undefined + ? typeInformation.timestamp + : config.getConfig().timestamp + ) { + if (!utils.isTimestamped(options.json)) { + options.json = addTimestamp(options.json, typeInformation.timezone); + } else if (!utils.IsValidTimestamped(options.json)) { + logger.error(context, 'Invalid timestamp:%s', JSON.stringify(options.json)); + callback(new errors.BadTimestamp(options.json)); + return; + } + } + + logger.debug(context, 'Updating device value in the Context Broker at [%s]', options.url); + 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) + ); + } + } + ); +} + +exports.sendQueryValue = sendQueryValueNgsi1; +exports.sendUpdateValue = sendUpdateValueNgsi1; diff --git a/lib/services/ngsi/entities-NGSI-v2.js b/lib/services/ngsi/entities-NGSI-v2.js new file mode 100644 index 000000000..edc290328 --- /dev/null +++ b/lib/services/ngsi/entities-NGSI-v2.js @@ -0,0 +1,420 @@ +/* + * 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: Federico M. Facca - Martel Innovate + * Modified by: Daniel Calvo - ATOS Research & Innovation + * Modified by: Jason Fox - FIWARE Foundation + */ + +const request = require('request'); +const statsService = require('./../stats/statsRegistry'); +const async = require('async'); +const apply = async.apply; +const alarms = require('../common/alarmManagement'); +const errors = require('../../errors'); +const utils = require('../northBound/restUtils'); +const config = require('../../commonConfig'); +const constants = require('../../constants'); +const moment = require('moment-timezone'); +const logger = require('logops'); +const context = { + op: 'IoTAgentNGSI.Entities-v2' +}; +const NGSIUtils = require('./ngsiUtils'); + +function addTimestampNgsi2(payload, timezone) { + function addTimestampEntity(entity, timezone) { + const timestamp = { + type: constants.TIMESTAMP_TYPE_NGSI2 + }; + + if (!timezone) { + timestamp.value = new Date().toISOString(); + } else { + timestamp.value = moment().tz(timezone).format('YYYY-MM-DD[T]HH:mm:ss.SSSZ'); + } + + function addMetadata(attribute) { + let timestampFound = false; + + if (!attribute.metadata) { + attribute.metadata = {}; + } + + for (let i = 0; i < attribute.metadata.length; i++) { + if (attribute.metadata[i] === constants.TIMESTAMP_ATTRIBUTE) { + if ( + attribute.metadata[constants.TIMESTAMP_ATTRIBUTE].type === constants.TIMESTAMP_TYPE_NGSI2 && + attribute.metadata[constants.TIMESTAMP_ATTRIBUTE].value === timestamp.value + ) { + timestampFound = true; + break; + } + } + } + + if (!timestampFound) { + attribute.metadata[constants.TIMESTAMP_ATTRIBUTE] = timestamp; + } + + return attribute; + } + let keyCount = 0; + for (const key in entity) { + /* eslint-disable-next-line no-prototype-builtins */ + if (entity.hasOwnProperty(key) && key !== 'id' && key !== 'type') { + addMetadata(entity[key]); + keyCount += 1; + } + } + // Add timestamp just to entity with attrs: multientity plugin could + // create empty entities just with id and type. + if (keyCount > 0) { + entity[constants.TIMESTAMP_ATTRIBUTE] = timestamp; + } + + return entity; + } + + if (payload instanceof Array) { + for (let i = 0; i < payload.length; i++) { + if (!utils.isTimestampedNgsi2(payload[i])) { + payload[i] = addTimestampEntity(payload[i], timezone); + } + } + + return payload; + } + return addTimestampEntity(payload, timezone); +} + +/** + * Generate an operation handler for NGSIv2-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 generateNGSI2OperationHandler(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.info(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.info( + 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.info( + 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); + + const errorField = NGSIUtils.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)); + } + }; +} + +/** + * Makes a query to the Device's entity in the context broker using NGSIv2, with the list + * of attributes given by the 'attributes' array. + * + * @param {String} entityName Name of the entity to query. + * @param {Array} attributes Attribute array containing the names of the attributes to query. + * @param {Object} typeInformation Configuration information for the device. + * @param {String} token User token to identify against the PEP Proxies (optional). + */ +function sendQueryValueNgsi2(entityName, attributes, typeInformation, token, callback) { + let url = '/v2/entities/' + entityName + '/attrs'; + + if (attributes && attributes.length > 0) { + let attributesQueryParam = ''; + + for (let i = 0; i < attributes.length; i++) { + attributesQueryParam = attributesQueryParam + attributes[i]; + if (i < attributes.length - 1) { + attributesQueryParam = attributesQueryParam + ','; + } + } + + url = url + '?attrs=' + attributesQueryParam; + } + + if (typeInformation.type) { + if (attributes && attributes.length > 0) { + url += '&type=' + typeInformation.type; + } else { + url += '?type=' + typeInformation.type; + } + } + + const options = NGSIUtils.createRequestObject(url, typeInformation, token); + options.method = 'GET'; + options.json = true; + + if (!typeInformation || !typeInformation.type) { + callback(new errors.TypeNotFound(null, entityName)); + return; + } + + logger.debug(context, 'Querying values of the device 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)); + + request( + options, + generateNGSI2OperationHandler('query', entityName, typeInformation, token, options, function (error, result) { + if (error) { + callback(error); + } else { + NGSIUtils.applyMiddlewares(NGSIUtils.queryMiddleware, result, typeInformation, 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 NGSIv2'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 sendUpdateValueNgsi2(entityName, attributes, typeInformation, token, callback) { + let payload = {}; + + let url = '/v2/entities/' + entityName + '/attrs'; + + if (typeInformation.type) { + url += '?type=' + typeInformation.type; + } + + let options = NGSIUtils.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 (let 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 + }; + const metadata = NGSIUtils.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 = NGSIUtils.castJsonNativeAttributes(payload); + async.waterfall( + [ + apply(statsService.add, 'measureRequests', 1), + apply(NGSIUtils.applyMiddlewares, NGSIUtils.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 = NGSIUtils.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. + let att; + if (options.json.entities) { + for (let 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; + } + } + } + + logger.debug(context, 'Updating device value in the Context Broker at [%s]', options.url); + 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) + ); + } + } + ); +} + +exports.sendQueryValue = sendQueryValueNgsi2; +exports.sendUpdateValue = sendUpdateValueNgsi2; +exports.addTimestamp = addTimestampNgsi2; diff --git a/lib/services/ngsi/ngsiParser.js b/lib/services/ngsi/ngsiParser.js deleted file mode 100644 index 012b03611..000000000 --- a/lib/services/ngsi/ngsiParser.js +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright 2014 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] - */ - -const intoTrans = require('../common/domain').intoTrans; -const context = { - op: 'IoTAgentNGSI.NGSIParser' -}; - -/** - * Given a NGSI Body, determines whether it contains any NGSI error. - * - * @param {String} body String representing a NGSI body in JSON format. - * @return {Number|*} - */ -function getErrorField(body) { - let errorField = body.errorCode || body.orionError; - - if (body && body.contextResponses) { - for (const i in body.contextResponses) { - if (body.contextResponses[i].statusCode && body.contextResponses[i].statusCode.code !== '200') { - errorField = body.contextResponses[i].statusCode; - } - } - } - - return errorField; -} - -exports.getErrorField = intoTrans(context, getErrorField); diff --git a/lib/services/ngsi/ngsiService.js b/lib/services/ngsi/ngsiService.js index b24907d85..236fd61aa 100644 --- a/lib/services/ngsi/ngsiService.js +++ b/lib/services/ngsi/ngsiService.js @@ -24,695 +24,33 @@ * Modified by: Daniel Calvo - ATOS Research & Innovation */ -/* eslint-disable no-prototype-builtins */ - -const request = require('request'); -const statsService = require('./../stats/statsRegistry'); const async = require('async'); const apply = async.apply; const intoTrans = require('../common/domain').intoTrans; const fillService = require('./../common/domain').fillService; -const alarms = require('../common/alarmManagement'); const errors = require('../../errors'); -const utils = require('../northBound/restUtils'); const config = require('../../commonConfig'); const constants = require('../../constants'); -const moment = require('moment-timezone'); const logger = require('logops'); -const ngsiParser = require('./ngsiParser'); +const ngsiUtils = require('./ngsiUtils'); const _ = require('underscore'); const context = { op: 'IoTAgentNGSI.NGSIService' }; -let updateMiddleware = []; -let queryMiddleware = []; - -/** - * Generate an operation handler for NGSI-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 generateNGSIOperationHandler(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 && body && response.statusCode === 200) { - const errorField = ngsiParser.getErrorField(body); - - logger.debug( - context, - 'Received the following request from the CB:\n\n%s\n\n', - JSON.stringify(body, null, 4) - ); - - if (errorField) { - logger.error( - context, - 'Operation ' + operationName + ' error connecting to the Context Broker: %j', - errorField - ); - - if (errorField.code && errorField.code === '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, errorField)); - } - } else { - logger.debug(context, 'Value updated successfully'); - alarms.release(constants.ORION_ALARM); - callback(null, body); - } - } 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 { - logger.debug(context, 'Unknown error executing ' + operationName + ' operation'); - - callback( - new errors.EntityGenericError( - entityName, - typeInformation.type, - { - details: body - }, - response.statusCode - ) - ); - } - }; -} - -/** - * Generate an operation handler for NGSIv2-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 generateNGSI2OperationHandler(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.info(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.info( - 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.info( - 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.info( - 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); - - const 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. - * - * @param {String} url Path for the Context Broker operation. - * @param {Object} typeInformation Object containing information about the device: service, security, etc. - * @param {String} token If present, security information needed to access the CB. - * @return {Object} Containing all the information of the request but the payload.c - */ -function createRequestObject(url, typeInformation, token) { - let cbHost = config.getConfig().contextBroker.url; - const serviceContext = {}; - const headers = { - 'fiware-service': config.getConfig().service, - 'fiware-servicepath': config.getConfig().subservice - }; - - if (config.getConfig().authentication && config.getConfig().authentication.enabled) { - headers[config.getConfig().authentication.header] = token; - } - logger.debug(context, 'typeInformation %j', typeInformation); - if (typeInformation) { - if (typeInformation.service) { - headers['fiware-service'] = typeInformation.service; - serviceContext.service = typeInformation.service; - } - - if (typeInformation.subservice) { - headers['fiware-servicepath'] = typeInformation.subservice; - serviceContext.subservice = typeInformation.subservice; - } - - if (typeInformation.cbHost && typeInformation.cbHost.indexOf('://') !== -1) { - cbHost = typeInformation.cbHost; - } else if (typeInformation.cbHost && typeInformation.cbHost.indexOf('://') === -1) { - cbHost = 'http://' + typeInformation.cbHost; - } - } - - const options = { - url: cbHost + url, - method: 'POST', - headers - }; - - return intoTrans(serviceContext, function () { - return options; - })(); -} - -function applyMiddlewares(middlewareCollection, entity, typeInformation, callback) { - function emptyMiddleware(callback) { - callback(null, entity, typeInformation); - } - - function endMiddleware(entity, typeInformation, callback) { - callback(null, entity); - } - - if (middlewareCollection && middlewareCollection.length > 0) { - const middlewareList = _.clone(middlewareCollection); - - middlewareList.unshift(emptyMiddleware); - middlewareList.push(endMiddleware); - - async.waterfall(middlewareList, callback); - } else { - callback(null, entity); - } -} - -function addTimestamp(payload, timezone) { - const timestamp = { - name: constants.TIMESTAMP_ATTRIBUTE, - type: constants.TIMESTAMP_TYPE - }; - - if (!timezone) { - timestamp.value = new Date().toISOString(); - } else { - timestamp.value = moment().tz(timezone).format('YYYY-MM-DD[T]HH:mm:ss.SSSZ'); - } - - function addMetadata(attribute) { - let timestampFound = false; - - if (!attribute.metadatas) { - attribute.metadatas = []; - } - - for (let i = 0; i < attribute.metadatas.length; i++) { - if ( - attribute.metadatas[i].type === constants.TIMESTAMP_TYPE && - attribute.metadatas[i].name === constants.TIMESTAMP_ATTRIBUTE - ) { - attribute.metadatas[i].value = timestamp.value; - timestampFound = true; - break; - } - } - - if (!timestampFound) { - attribute.metadatas.push(timestamp); - } - - return attribute; - } - - payload.contextElements[0].attributes.map(addMetadata); - payload.contextElements[0].attributes.push(timestamp); - return payload; -} - -function addTimestampNgsi2(payload, timezone) { - function addTimestampEntity(entity, timezone) { - const timestamp = { - type: constants.TIMESTAMP_TYPE_NGSI2 - }; - - if (!timezone) { - timestamp.value = new Date().toISOString(); - } else { - timestamp.value = moment().tz(timezone).format('YYYY-MM-DD[T]HH:mm:ss.SSSZ'); - } - - function addMetadata(attribute) { - let timestampFound = false; - - if (!attribute.metadata) { - attribute.metadata = {}; - } - - for (let i = 0; i < attribute.metadata.length; i++) { - if (attribute.metadata[i] === constants.TIMESTAMP_ATTRIBUTE) { - if ( - attribute.metadata[constants.TIMESTAMP_ATTRIBUTE].type === constants.TIMESTAMP_TYPE_NGSI2 && - attribute.metadata[constants.TIMESTAMP_ATTRIBUTE].value === timestamp.value - ) { - timestampFound = true; - break; - } - } - } - - if (!timestampFound) { - attribute.metadata[constants.TIMESTAMP_ATTRIBUTE] = timestamp; - } - - return attribute; - } - let keyCount = 0; - for (const key in entity) { - if (entity.hasOwnProperty(key) && key !== 'id' && key !== 'type') { - addMetadata(entity[key]); - keyCount += 1; - } - } - // Add timestamp just to entity with attrs: multientity plugin could - // create empty entities just with id and type. - if (keyCount > 0) { - entity[constants.TIMESTAMP_ATTRIBUTE] = timestamp; - } - - return entity; - } - - if (payload instanceof Array) { - for (let i = 0; i < payload.length; i++) { - if (!utils.isTimestampedNgsi2(payload[i])) { - payload[i] = addTimestampEntity(payload[i], timezone); - } - } - - return payload; - } - return addTimestampEntity(payload, timezone); -} - -function getMetaData(typeInformation, name, metadata) { - if (metadata) { - return metadata; - } - - let i; - if (typeInformation.active) { - for (i = 0; i < typeInformation.active.length; i++) { - /* jshint camelcase: false */ - if (name === typeInformation.active[i].object_id) { - return typeInformation.active[i].metadata; - } - } - } - if (typeInformation.staticAttributes) { - for (i = 0; i < typeInformation.staticAttributes.length; i++) { - if (name === typeInformation.staticAttributes[i].name) { - return typeInformation.staticAttributes[i].metadata; - } - } - } - return undefined; -} - -/** - * It casts attribute values which are reported using JSON native types - * - * @param {String} payload The payload - * @return {String} New payload where attributes's values are casted to the corresponding JSON types - */ -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; - } - - for (const key in payload) { - if ( - payload.hasOwnProperty(key) && - payload[key].value && - payload[key].type && - typeof payload[key].value === 'string' - ) { - if (payload[key].type === 'Number' && isFloat(payload[key].value)) { - payload[key].value = Number.parseFloat(payload[key].value); - } else if (payload[key].type === 'Number' && Number.parseInt(payload[key].value)) { - payload[key].value = Number.parseInt(payload[key].value); - } else if (payload[key].type === 'Boolean') { - payload[key].value = payload[key].value === 'true' || payload[key].value === '1'; - } else if (payload[key].type === 'None') { - payload[key].value = null; - } else if (payload[key].type === 'Array' || payload[key].type === 'Object') { - try { - const parsedValue = JSON.parse(payload[key].value); - payload[key].value = parsedValue; - } catch (e) { - logger.error( - context, - 'Bad attribute value type. Expecting JSON Array or JSON Object. Received:%s', - payload[key].value - ); - } - } - } - } - - return payload; -} +let entityHandler; /** - * 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 NGSIv2'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). + * Loads the correct ngsiService handler based on the current config. */ -function sendUpdateValueNgsi2(entityName, attributes, typeInformation, token, callback) { - let payload = {}; - - let url = '/v2/entities/' + entityName + '/attrs'; - - if (typeInformation.type) { - url += '?type=' + typeInformation.type; - } - - let 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 (let 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 - }; - const 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. - let att; - if (options.json.entities) { - for (let 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; - } - } - } - - logger.info(context, 'Updating device value in the Context Broker at [%s]', options.url); - logger.info(context, 'Using the following request:\n\n%s\n\n', JSON.stringify(options, null, 4)); - - request( - options, - generateNGSI2OperationHandler('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. - * - * @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 sendUpdateValueNgsi1(entityName, attributes, typeInformation, token, callback) { - const options = createRequestObject('/v1/updateContext', typeInformation, token); - - if (typeInformation && typeInformation.staticAttributes) { - attributes = attributes.concat(typeInformation.staticAttributes); - } - - if (!typeInformation || !typeInformation.type) { - callback(new errors.TypeNotFound(null, entityName)); - return; - } - - const payload = { - contextElements: [ - { - type: typeInformation.type, - isPattern: 'false', - id: entityName, - attributes - } - ] - }; - if ( - 'autoprovision' in typeInformation && - /* jshint -W101 */ - typeInformation.autoprovision === undefined - ? typeInformation.autoprovision === true - : config.getConfig().appendMode === true - ) { - payload.updateAction = 'APPEND'; +function init() { + if (config.checkNgsiLD()) { + entityHandler = require('./entities-NGSI-LD'); + } else if (config.checkNgsi2()) { + entityHandler = require('./entities-NGSI-v2'); } else { - payload.updateAction = 'UPDATE'; + entityHandler = require('./entities-NGSI-v1'); } - async.waterfall( - [ - apply(statsService.add, 'measureRequests', 1), - apply(applyMiddlewares, updateMiddleware, payload, typeInformation) - ], - function (error, result) { - if (error) { - callback(error); - } else { - if (result) { - options.json = result; - } else { - options.json = payload; - } - - if ( - 'timestamp' in typeInformation && typeInformation.timestamp !== undefined - ? typeInformation.timestamp - : config.getConfig().timestamp - ) { - if (!utils.isTimestamped(options.json)) { - options.json = addTimestamp(options.json, typeInformation.timezone); - } else if (!utils.IsValidTimestamped(options.json)) { - logger.error(context, 'Invalid timestamp:%s', JSON.stringify(options.json)); - callback(new errors.BadTimestamp(options.json)); - return; - } - } - - 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)); - - request( - options, - generateNGSIOperationHandler('update', entityName, typeInformation, token, options, callback) - ); - } - } - ); } /** @@ -725,111 +63,7 @@ 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()) { - sendUpdateValueNgsi2(entityName, attributes, typeInformation, token, callback); - } else { - sendUpdateValueNgsi1(entityName, attributes, typeInformation, token, callback); - } -} - -/** - * Makes a query to the Device's entity in the context broker using NGSIv2, with the list - * of attributes given by the 'attributes' array. - * - * @param {String} entityName Name of the entity to query. - * @param {Array} attributes Attribute array containing the names of the attributes to query. - * @param {Object} typeInformation Configuration information for the device. - * @param {String} token User token to identify against the PEP Proxies (optional). - */ -function sendQueryValueNgsi2(entityName, attributes, typeInformation, token, callback) { - let url = '/v2/entities/' + entityName + '/attrs'; - - if (attributes && attributes.length > 0) { - let attributesQueryParam = ''; - - for (let i = 0; i < attributes.length; i++) { - attributesQueryParam = attributesQueryParam + attributes[i]; - if (i < attributes.length - 1) { - attributesQueryParam = attributesQueryParam + ','; - } - } - - url = url + '?attrs=' + attributesQueryParam; - } - - if (typeInformation.type) { - if (attributes && attributes.length > 0) { - url += '&type=' + typeInformation.type; - } else { - url += '?type=' + typeInformation.type; - } - } - - const options = createRequestObject(url, typeInformation, token); - options.method = 'GET'; - options.json = true; - - if (!typeInformation || !typeInformation.type) { - callback(new errors.TypeNotFound(null, entityName)); - return; - } - - logger.debug(context, 'Querying values of the device 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)); - - request( - options, - generateNGSI2OperationHandler('query', entityName, typeInformation, token, options, function (error, result) { - if (error) { - callback(error); - } else { - applyMiddlewares(queryMiddleware, result, typeInformation, callback); - } - }) - ); -} - -/** - * Makes a query to the Device's entity in the context broker using NGSIv1, with the list of - * attributes given by the 'attributes' array. - * - * @param {String} entityName Name of the entity to query. - * @param {Array} attributes Attribute array containing the names of the attributes to query. - * @param {Object} typeInformation Configuration information for the device. - * @param {String} token User token to identify against the PEP Proxies (optional). - */ -function sendQueryValueNgsi1(entityName, attributes, typeInformation, token, callback) { - const options = createRequestObject('/v1/queryContext', typeInformation, token); - - if (!typeInformation || !typeInformation.type) { - callback(new errors.TypeNotFound(null, entityName)); - return; - } - - options.json = { - entities: [ - { - type: typeInformation.type, - isPattern: 'false', - id: entityName - } - ], - attributes - }; - - logger.debug(context, 'Querying values of the device 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)); - - request( - options, - generateNGSIOperationHandler('query', entityName, typeInformation, token, options, function (error, result) { - if (error) { - callback(error); - } else { - applyMiddlewares(queryMiddleware, result, typeInformation, callback); - } - }) - ); + entityHandler.sendUpdateValue(entityName, attributes, typeInformation, token, callback); } /** @@ -842,11 +76,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()) { - sendQueryValueNgsi2(entityName, attributes, typeInformation, token, callback); - } else { - sendQueryValueNgsi1(entityName, attributes, typeInformation, token, callback); - } + entityHandler.sendQueryValue(entityName, attributes, typeInformation, token, callback); } /** @@ -864,7 +94,6 @@ function sendQueryValue(entityName, attributes, typeInformation, token, callback function updateTrust(deviceGroup, deviceInformation, trust, response, callback) { if (deviceGroup && response && response.body && !config.getConfig().authentication.permanentToken) { const body = JSON.parse(response.body); - /* jshint camelcase: false */ if (body && body.refresh_token) { deviceGroup.trust = body.refresh_token; /* eslint-disable-next-line no-unused-vars */ @@ -1030,16 +259,16 @@ function setCommandResult( } function addUpdateMiddleware(middleware) { - updateMiddleware.push(middleware); + ngsiUtils.updateMiddleware.push(middleware); } function addQueryMiddleware(middleware) { - queryMiddleware.push(middleware); + ngsiUtils.queryMiddleware.push(middleware); } function resetMiddlewares(callback) { - updateMiddleware = []; - queryMiddleware = []; + ngsiUtils.updateMiddleware = []; + ngsiUtils.queryMiddleware = []; callback(); } @@ -1050,5 +279,5 @@ exports.addUpdateMiddleware = intoTrans(context, addUpdateMiddleware); exports.addQueryMiddleware = intoTrans(context, addQueryMiddleware); exports.resetMiddlewares = intoTrans(context, resetMiddlewares); exports.setCommandResult = intoTrans(context, setCommandResult); -exports.castJsonNativeAttributes = castJsonNativeAttributes; exports.updateTrust = updateTrust; +exports.init = init; diff --git a/lib/services/ngsi/ngsiUtils.js b/lib/services/ngsi/ngsiUtils.js new file mode 100644 index 000000000..9005fbab2 --- /dev/null +++ b/lib/services/ngsi/ngsiUtils.js @@ -0,0 +1,218 @@ +/* + * Copyright 2014 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] + */ + +/* eslint-disable no-unused-vars */ + +const async = require('async'); +const errors = require('../../errors'); +const logger = require('logops'); +const intoTrans = require('../common/domain').intoTrans; +const context = { + op: 'IoTAgentNGSI.NGSIUtils' +}; +const _ = require('underscore'); +const config = require('../../commonConfig'); +const updateMiddleware = []; +const queryMiddleware = []; +/** + * 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; +} + +/** + * It casts attribute values which are reported using JSON native types + * + * @param {String} payload The payload + * @return {String} New payload where attributes's values are casted to the corresponding JSON types + */ +function castJsonNativeAttributes(payload) { + if (!config.getConfig().autocast) { + return payload; + } + + for (const key in payload) { + if ( + /* eslint-disable-next-line no-prototype-builtins */ + payload.hasOwnProperty(key) && + payload[key].value && + payload[key].type && + typeof payload[key].value === 'string' + ) { + if (payload[key].type === 'Number' && isFloat(payload[key].value)) { + payload[key].value = Number.parseFloat(payload[key].value); + } else if (payload[key].type === 'Number' && Number.parseInt(payload[key].value)) { + payload[key].value = Number.parseInt(payload[key].value); + } else if (payload[key].type === 'Boolean') { + payload[key].value = payload[key].value === 'true' || payload[key].value === '1'; + } else if (payload[key].type === 'None') { + payload[key].value = null; + } else if (payload[key].type === 'Array' || payload[key].type === 'Object') { + try { + const parsedValue = JSON.parse(payload[key].value); + payload[key].value = parsedValue; + } catch (e) { + logger.error( + context, + 'Bad attribute value type. Expecting JSON Array or JSON Object. Received:%s', + payload[key].value + ); + } + } + } + } + return payload; +} + +/** + * Create the request object used to communicate with the Context Broker, adding security and service information. + * + * @param {String} url Path for the Context Broker operation. + * @param {Object} typeInformation Object containing information about the device: service, security, etc. + * @param {String} token If present, security information needed to access the CB. + * @return {Object} Containing all the information of the request but the payload.c + */ +function createRequestObject(url, typeInformation, token) { + let cbHost = config.getConfig().contextBroker.url; + const serviceContext = {}; + const headers = { + 'fiware-service': config.getConfig().service, + 'fiware-servicepath': config.getConfig().subservice + }; + + if (config.getConfig().authentication && config.getConfig().authentication.enabled) { + headers[config.getConfig().authentication.header] = token; + } + logger.debug(context, 'typeInformation %j', typeInformation); + if (typeInformation) { + if (typeInformation.service) { + headers['fiware-service'] = typeInformation.service; + serviceContext.service = typeInformation.service; + } + + if (typeInformation.subservice) { + headers['fiware-servicepath'] = typeInformation.subservice; + serviceContext.subservice = typeInformation.subservice; + } + + if (typeInformation.cbHost && typeInformation.cbHost.indexOf('://') !== -1) { + cbHost = typeInformation.cbHost; + } else if (typeInformation.cbHost && typeInformation.cbHost.indexOf('://') === -1) { + cbHost = 'http://' + typeInformation.cbHost; + } + } + + if (config.checkNgsiLD()) { + headers['Content-Type'] = 'application/ld+json'; + headers['NGSILD-Tenant'] = headers['fiware-service']; + headers['NGSILD-Path'] = headers['fiware-servicepath']; + } + + const options = { + url: cbHost + url, + method: 'POST', + headers + }; + + return intoTrans(serviceContext, function () { + return options; + })(); +} + +function applyMiddlewares(middlewareCollection, entity, typeInformation, callback) { + function emptyMiddleware(callback) { + callback(null, entity, typeInformation); + } + + function endMiddleware(entity, typeInformation, callback) { + callback(null, entity); + } + + if (middlewareCollection && middlewareCollection.length > 0) { + const middlewareList = _.clone(middlewareCollection); + + middlewareList.unshift(emptyMiddleware); + middlewareList.push(endMiddleware); + + async.waterfall(middlewareList, callback); + } else { + callback(null, entity); + } +} + +function getMetaData(typeInformation, name, metadata) { + if (metadata) { + return metadata; + } + + let i; + if (typeInformation.active) { + for (i = 0; i < typeInformation.active.length; i++) { + /* jshint camelcase: false */ + if (name === typeInformation.active[i].object_id) { + return typeInformation.active[i].metadata; + } + } + } + if (typeInformation.staticAttributes) { + for (i = 0; i < typeInformation.staticAttributes.length; i++) { + if (name === typeInformation.staticAttributes[i].name) { + return typeInformation.staticAttributes[i].metadata; + } + } + } + return undefined; +} + +/** + * Given a NGSI Body, determines whether it contains any NGSI error. + * + * @param {String} body String representing a NGSI body in JSON format. + * @return {Number|*} + */ +function getErrorField(body) { + let errorField = body.errorCode || body.orionError; + + if (body && body.contextResponses) { + for (const i in body.contextResponses) { + if (body.contextResponses[i].statusCode && body.contextResponses[i].statusCode.code !== '200') { + errorField = body.contextResponses[i].statusCode; + } + } + } + + return errorField; +} + +exports.getErrorField = intoTrans(context, getErrorField); +exports.createRequestObject = createRequestObject; +exports.applyMiddlewares = applyMiddlewares; +exports.getMetaData = getMetaData; +exports.castJsonNativeAttributes = castJsonNativeAttributes; +exports.isFloat = isFloat; +exports.updateMiddleware = updateMiddleware; +exports.queryMiddleware = queryMiddleware; diff --git a/lib/services/ngsi/subscription-NGSI-LD.js b/lib/services/ngsi/subscription-NGSI-LD.js new file mode 100644 index 000000000..6ea0ce151 --- /dev/null +++ b/lib/services/ngsi/subscription-NGSI-LD.js @@ -0,0 +1,228 @@ +/* + * 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 errors = require('../../errors'); +const logger = require('logops'); +const config = require('../../commonConfig'); +const utils = require('../northBound/restUtils'); +const context = { + op: 'IoTAgentNGSI.Subscription-LD' +}; + +/** + * Generate a new subscription request handler using NGSI-LD, based on the device and triggers given to the function. + * + * @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 {Boolean} store If set, store the subscription result in the device. Otherwise, return the ID. + * @param {Function} callback Callback to be called when the subscription handler ends. + * @return {Function} Returns a request handler for the given data. + */ +function createSubscriptionHandlerNgsiLD(device, triggers, store, callback) { + return function (error, response, body) { + if (error) { + logger.debug( + context, + 'Transport error found subscribing device with id [%s] to entity [%s]', + device.id, + device.name + ); + + callback(error); + } else if (response.statusCode !== 200 && response.statusCode !== 201) { + logger.debug( + context, + 'Unknown error subscribing device with id [%s] to entity [%s]: $s', + response.statusCode + ); + + callback( + new errors.EntityGenericError( + device.name, + device.type, + { + details: body + }, + response.statusCode + ) + ); + } else if (body && body.orionError) { + logger.debug( + context, + 'Orion found subscribing device with id [%s] to entity [%s]: %s', + device.id, + device.name, + body.orionError + ); + + callback(new errors.BadRequest(body.orionError.details)); + } else if (store) { + if (!device.subscriptions) { + device.subscriptions = []; + } + + device.subscriptions.push({ + id: response.headers.location.substr(response.headers.location.lastIndexOf('/') + 1), + triggers + }); + + config.getRegistry().update(device, callback); + } else { + callback(null, response.headers.location); + } + }; +} + +/** + * 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) { + const 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 || [] + } + } + }; + + let 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/'; + } + utils.executeWithSecurity(options, device, createSubscriptionHandlerNgsiLD(device, triggers, store, callback)); +} + +/** + * Generate a new unsubscription request handler using NGSI-LD, based on the device and subscription ID + * given to the function. + * + * @param {Object} device Object containing all the information about a particular device. + * @param {String} id ID of the subscription to remove. + * @param {Function} callback Callback to be called when the subscription handler ends. + * @return {Function} Returns a request handler for the given data. + */ +function createUnsubscribeHandlerNgsiLD(device, id, callback) { + return function (error, response, body) { + if (error) { + logger.debug( + context, + 'Transport error found subscribing device with id [%s] to entity [%s]', + device.id, + device.name + ); + + callback(error); + } else if (response.statusCode !== 204) { + logger.debug( + context, + 'Unknown error subscribing device with id [%s] to entity [%s]: $s', + response.statusCode + ); + + callback( + new errors.EntityGenericError( + device.name, + device.type, + { + details: body + }, + response.statusCode + ) + ); + } else if (body && body.orionError) { + logger.debug( + context, + 'Orion found subscribing device with id [%s] to entity [%s]: %s', + device.id, + device.name, + body.orionError + ); + + callback(new errors.BadRequest(body.orionError.details)); + } else { + device.subscriptions.splice(device.subscriptions.indexOf(id), 1); + config.getRegistry().update(device, callback); + } + }; +} + +/** + * Remove the subscription with the given ID from the Context Broker and from the device repository using NGSI-LD. + * + * @param {Object} device Object containing all the information about a particular device. + * @param {String} id ID of the subscription to remove. + */ +function unsubscribeNgsiLD(device, id, callback) { + const options = { + method: 'DELETE', + headers: { + 'fiware-service': device.service, + 'fiware-servicepath': device.subservice + } + }; + + if (device.cbHost && device.cbHost.indexOf('://') !== -1) { + options.uri = device.cbHost + '/ngsi-ld/v1/subscriptions/' + id; + } else if (device.cbHost && device.cbHost.indexOf('://') === -1) { + options.uri = 'http://' + device.cbHost + '/ngsi-ld/v1/subscriptions/' + id; + } else { + options.uri = config.getConfig().contextBroker.url + '/ngsi-ld/v1/subscriptions/' + id; + } + utils.executeWithSecurity(options, device, createUnsubscribeHandlerNgsiLD(device, id, callback)); +} + +exports.subscribe = subscribeNgsiLD; +exports.unsubscribe = unsubscribeNgsiLD; diff --git a/lib/services/ngsi/subscription-NGSI-v1.js b/lib/services/ngsi/subscription-NGSI-v1.js new file mode 100644 index 000000000..dc9f4daa7 --- /dev/null +++ b/lib/services/ngsi/subscription-NGSI-v1.js @@ -0,0 +1,234 @@ +/* + * 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: Federico M. Facca - Martel Innovate + * Modified by: Daniel Calvo - ATOS Research & Innovation + * Modified by: Jason Fox - FIWARE Foundation + */ + +const errors = require('../../errors'); +const logger = require('logops'); +const config = require('../../commonConfig'); +const utils = require('../northBound/restUtils'); +const context = { + op: 'IoTAgentNGSI.Subscription-v1' +}; + +/** + * Generate a new subscription request handler using NGSIv1, based on the device and triggers given to the function. + * + * @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 {Boolean} store If set, store the subscription result in the device. Otherwise, return the ID. + * @param {Function} callback Callback to be called when the subscription handler ends. + * @return {Function} Returns a request handler for the given data. + */ +function createSubscriptionHandlerNgsi1(device, triggers, store, callback) { + return function (error, response, body) { + if (error || !body) { + logger.debug( + context, + 'Transport error found subscribing device with id [%s] to entity [%s]', + device.id, + device.name + ); + + callback(error); + } else if (response.statusCode !== 200 && response.statusCode !== 201) { + logger.debug( + context, + 'Unknown error subscribing device with id [%s] to entity [%s]: $s', + response.statusCode + ); + + callback( + new errors.EntityGenericError( + device.name, + device.type, + { + details: body + }, + response.statusCode + ) + ); + } else if (body && body.orionError) { + logger.debug( + context, + 'Orion found subscribing device with id [%s] to entity [%s]: %s', + device.id, + device.name, + body.orionError + ); + + callback(new errors.BadRequest(body.orionError.details)); + } else if (store) { + if (!device.subscriptions) { + device.subscriptions = []; + } + + device.subscriptions.push({ + id: body.subscribeResponse.subscriptionId, + triggers + }); + + config.getRegistry().update(device, callback); + } else { + callback(null, body.subscribeResponse.subscriptionId); + } + }; +} + +/** + * Makes a subscription for the given device's entity using NGSIv1, 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 subscribeNgsi1(device, triggers, content, callback) { + const options = { + method: 'POST', + headers: { + 'fiware-service': device.service, + 'fiware-servicepath': device.subservice + }, + json: { + entities: [ + { + type: device.type, + isPattern: 'false', + id: device.name + } + ], + reference: config.getConfig().providerUrl + '/notify', + duration: config.getConfig().deviceRegistrationDuration || 'P100Y', + notifyConditions: [ + { + type: 'ONCHANGE', + condValues: triggers + } + ] + } + }; + let store = true; + + if (content) { + options.json.attributes = content; + store = false; + } + + if (device.cbHost && device.cbHost.indexOf('://') !== -1) { + options.uri = device.cbHost + '/v1/subscribeContext'; + } else if (device.cbHost && device.cbHost.indexOf('://') === -1) { + options.uri = 'http://' + device.cbHost + '/v1/subscribeContext'; + } else { + options.uri = config.getConfig().contextBroker.url + '/v1/subscribeContext'; + } + utils.executeWithSecurity(options, device, createSubscriptionHandlerNgsi1(device, triggers, store, callback)); +} + +/** + * Generate a new unsubscription request handler using NGSIv1, based on the device and subscription ID + * given to the function. + * + * @param {Object} device Object containing all the information about a particular device. + * @param {String} id ID of the subscription to remove. + * @param {Function} callback Callback to be called when the subscription handler ends. + * @return {Function} Returns a request handler for the given data. + */ +function createUnsubscribeHandlerNgsi1(device, id, callback) { + return function (error, response, body) { + if (error || !body) { + logger.debug( + context, + 'Transport error found subscribing device with id [%s] to entity [%s]', + device.id, + device.name + ); + + callback(error); + } else if (response.statusCode !== 200 && response.statusCode !== 201) { + logger.debug( + context, + 'Unknown error subscribing device with id [%s] to entity [%s]: $s', + response.statusCode + ); + + callback( + new errors.EntityGenericError( + device.name, + device.type, + { + details: body + }, + response.statusCode + ) + ); + } else if (body && body.orionError) { + logger.debug( + context, + 'Orion found subscribing device with id [%s] to entity [%s]: %s', + device.id, + device.name, + body.orionError + ); + + callback(new errors.BadRequest(body.orionError.details)); + } else { + device.subscriptions.splice(device.subscriptions.indexOf(id), 1); + config.getRegistry().update(device, callback); + } + }; +} + +/** + * Remove the subscription with the given ID from the Context Broker and from the device repository using NGSIv1. + * + * @param {Object} device Object containing all the information about a particular device. + * @param {String} id ID of the subscription to remove. + */ +function unsubscribeNgsi1(device, id, callback) { + const options = { + method: 'POST', + headers: { + 'fiware-service': device.service, + 'fiware-servicepath': device.subservice + }, + json: { + subscriptionId: id + } + }; + + if (device.cbHost && device.cbHost.indexOf('://') !== -1) { + options.uri = device.cbHost + '/v1/unsubscribeContext'; + } else if (device.cbHost && device.cbHost.indexOf('://') === -1) { + options.uri = 'http://' + device.cbHost + '/v1/unsubscribeContext'; + } else { + options.uri = config.getConfig().contextBroker.url + '/v1/unsubscribeContext'; + } + utils.executeWithSecurity(options, device, createUnsubscribeHandlerNgsi1(device, id, callback)); +} + +exports.subscribe = subscribeNgsi1; +exports.unsubscribe = unsubscribeNgsi1; diff --git a/lib/services/ngsi/subscription-NGSI-v2.js b/lib/services/ngsi/subscription-NGSI-v2.js new file mode 100644 index 000000000..aa31bede2 --- /dev/null +++ b/lib/services/ngsi/subscription-NGSI-v2.js @@ -0,0 +1,235 @@ +/* + * 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: Federico M. Facca - Martel Innovate + * Modified by: Daniel Calvo - ATOS Research & Innovation + * Modified by: Jason Fox - FIWARE Foundation + */ + +const errors = require('../../errors'); +const logger = require('logops'); +const config = require('../../commonConfig'); +const utils = require('../northBound/restUtils'); +const context = { + op: 'IoTAgentNGSI.Subscription-v2' +}; + +/** + * Generate a new subscription request handler using NGSIv2, based on the device and triggers given to the function. + * + * @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 {Boolean} store If set, store the subscription result in the device. Otherwise, return the ID. + * @param {Function} callback Callback to be called when the subscription handler ends. + * @return {Function} Returns a request handler for the given data. + */ +function createSubscriptionHandlerNgsi2(device, triggers, store, callback) { + return function (error, response, body) { + if (error) { + logger.debug( + context, + 'Transport error found subscribing device with id [%s] to entity [%s]', + device.id, + device.name + ); + + callback(error); + } else if (response.statusCode !== 200 && response.statusCode !== 201) { + logger.debug( + context, + 'Unknown error subscribing device with id [%s] to entity [%s]: $s', + response.statusCode + ); + + callback( + new errors.EntityGenericError( + device.name, + device.type, + { + details: body + }, + response.statusCode + ) + ); + } else if (body && body.orionError) { + logger.debug( + context, + 'Orion found subscribing device with id [%s] to entity [%s]: %s', + device.id, + device.name, + body.orionError + ); + + callback(new errors.BadRequest(body.orionError.details)); + } else if (store) { + if (!device.subscriptions) { + device.subscriptions = []; + } + + device.subscriptions.push({ + id: response.headers.location.substr(response.headers.location.lastIndexOf('/') + 1), + triggers + }); + + config.getRegistry().update(device, callback); + } else { + callback(null, response.headers.location); + } + }; +} + +/** + * Makes a subscription for the given device's entity using NGSIv2, 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 subscribeNgsi2(device, triggers, content, callback) { + const options = { + method: 'POST', + headers: { + 'fiware-service': device.service, + 'fiware-servicepath': device.subservice + }, + json: { + subject: { + entities: [ + { + id: device.name, + type: device.type + } + ], + + condition: { + attrs: triggers + } + }, + notification: { + http: { + url: config.getConfig().providerUrl + '/notify' + }, + attrs: content || [], + attrsFormat: 'normalized' + } + } + }; + + let store = true; + + if (content) { + store = false; + } + + if (device.cbHost && device.cbHost.indexOf('://') !== -1) { + options.uri = device.cbHost + '/v2/subscriptions'; + } else if (device.cbHost && device.cbHost.indexOf('://') === -1) { + options.uri = 'http://' + device.cbHost + '/v2/subscriptions'; + } else { + options.uri = config.getConfig().contextBroker.url + '/v2/subscriptions'; + } + utils.executeWithSecurity(options, device, createSubscriptionHandlerNgsi2(device, triggers, store, callback)); +} + +/** + * Generate a new unsubscription request handler using NGSIv2, based on the device and subscription ID + * given to the function. + * + * @param {Object} device Object containing all the information about a particular device. + * @param {String} id ID of the subscription to remove. + * @param {Function} callback Callback to be called when the subscription handler ends. + * @return {Function} Returns a request handler for the given data. + */ +function createUnsubscribeHandlerNgsi2(device, id, callback) { + return function (error, response, body) { + if (error) { + logger.debug( + context, + 'Transport error found subscribing device with id [%s] to entity [%s]', + device.id, + device.name + ); + + callback(error); + } else if (response.statusCode !== 204) { + logger.debug( + context, + 'Unknown error subscribing device with id [%s] to entity [%s]: $s', + response.statusCode + ); + + callback( + new errors.EntityGenericError( + device.name, + device.type, + { + details: body + }, + response.statusCode + ) + ); + } else if (body && body.orionError) { + logger.debug( + context, + 'Orion found subscribing device with id [%s] to entity [%s]: %s', + device.id, + device.name, + body.orionError + ); + + callback(new errors.BadRequest(body.orionError.details)); + } else { + device.subscriptions.splice(device.subscriptions.indexOf(id), 1); + config.getRegistry().update(device, callback); + } + }; +} + +/** + * Remove the subscription with the given ID from the Context Broker and from the device repository using NGSIv2. + * + * @param {Object} device Object containing all the information about a particular device. + * @param {String} id ID of the subscription to remove. + */ +function unsubscribeNgsi2(device, id, callback) { + const options = { + method: 'DELETE', + headers: { + 'fiware-service': device.service, + 'fiware-servicepath': device.subservice + } + }; + + if (device.cbHost && device.cbHost.indexOf('://') !== -1) { + options.uri = device.cbHost + '/v2/subscriptions/' + id; + } else if (device.cbHost && device.cbHost.indexOf('://') === -1) { + options.uri = 'http://' + device.cbHost + '/v2/subscriptions/' + id; + } else { + options.uri = config.getConfig().contextBroker.url + '/v2/subscriptions/' + id; + } + utils.executeWithSecurity(options, device, createUnsubscribeHandlerNgsi2(device, id, callback)); +} + +exports.subscribe = subscribeNgsi2; +exports.unsubscribe = unsubscribeNgsi2; diff --git a/lib/services/ngsi/subscriptionService.js b/lib/services/ngsi/subscriptionService.js index 29efc300a..46ac49fa4 100644 --- a/lib/services/ngsi/subscriptionService.js +++ b/lib/services/ngsi/subscriptionService.js @@ -24,255 +24,25 @@ * Modified by: Daniel Calvo - ATOS Research & Innovation */ -const errors = require('../../errors'); const intoTrans = require('../common/domain').intoTrans; -const logger = require('logops'); -const config = require('../../commonConfig'); -const deviceService = require('../devices/deviceService'); const context = { - op: 'IoTAgentNGSI.NGSIService' + op: 'IoTAgentNGSI.SubscriptionService' }; +const config = require('../../commonConfig'); -/** - * Generate a new subscription request handler using NGSIv1, based on the device and triggers given to the function. - * - * @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 {Boolean} store If set, store the subscription result in the device. Otherwise, return the ID. - * @param {Function} callback Callback to be called when the subscription handler ends. - * @return {Function} Returns a request handler for the given data. - */ -function createSubscriptionHandlerNgsi1(device, triggers, store, callback) { - return function (error, response, body) { - if (error || !body) { - logger.debug( - context, - 'Transport error found subscribing device with id [%s] to entity [%s]', - device.id, - device.name - ); - - callback(error); - } else if (response.statusCode !== 200 && response.statusCode !== 201) { - logger.debug( - context, - 'Unknown error subscribing device with id [%s] to entity [%s]: $s', - response.statusCode - ); - - callback( - new errors.EntityGenericError( - device.name, - device.type, - { - details: body - }, - response.statusCode - ) - ); - } else if (body && body.orionError) { - logger.debug( - context, - 'Orion found subscribing device with id [%s] to entity [%s]: %s', - device.id, - device.name, - body.orionError - ); - - callback(new errors.BadRequest(body.orionError.details)); - } else if (store) { - if (!device.subscriptions) { - device.subscriptions = []; - } - - device.subscriptions.push({ - id: body.subscribeResponse.subscriptionId, - triggers - }); - - config.getRegistry().update(device, callback); - } else { - callback(null, body.subscribeResponse.subscriptionId); - } - }; -} +let subscriptionHandler; /** - * Generate a new subscription request handler using NGSIv2, based on the device and triggers given to the function. - * - * @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 {Boolean} store If set, store the subscription result in the device. Otherwise, return the ID. - * @param {Function} callback Callback to be called when the subscription handler ends. - * @return {Function} Returns a request handler for the given data. + * Loads the correct subscription handler based on the current config. */ -function createSubscriptionHandlerNgsi2(device, triggers, store, callback) { - return function (error, response, body) { - if (error) { - logger.debug( - context, - 'Transport error found subscribing device with id [%s] to entity [%s]', - device.id, - device.name - ); - - callback(error); - } else if (response.statusCode !== 200 && response.statusCode !== 201) { - logger.debug( - context, - 'Unknown error subscribing device with id [%s] to entity [%s]: $s', - response.statusCode - ); - - callback( - new errors.EntityGenericError( - device.name, - device.type, - { - details: body - }, - response.statusCode - ) - ); - } else if (body && body.orionError) { - logger.debug( - context, - 'Orion found subscribing device with id [%s] to entity [%s]: %s', - device.id, - device.name, - body.orionError - ); - - callback(new errors.BadRequest(body.orionError.details)); - } else if (store) { - if (!device.subscriptions) { - device.subscriptions = []; - } - - device.subscriptions.push({ - id: response.headers.location.substr(response.headers.location.lastIndexOf('/') + 1), - triggers - }); - - config.getRegistry().update(device, callback); - } else { - callback(null, response.headers.location); - } - }; -} - -/** - * Makes a subscription for the given device's entity using NGSIv1, 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 subscribeNgsi1(device, triggers, content, callback) { - const options = { - method: 'POST', - headers: { - 'fiware-service': device.service, - 'fiware-servicepath': device.subservice - }, - json: { - entities: [ - { - type: device.type, - isPattern: 'false', - id: device.name - } - ], - reference: config.getConfig().providerUrl + '/notify', - duration: config.getConfig().deviceRegistrationDuration || 'P100Y', - notifyConditions: [ - { - type: 'ONCHANGE', - condValues: triggers - } - ] - } - }; - let store = true; - - if (content) { - options.json.attributes = content; - store = false; - } - - if (device.cbHost && device.cbHost.indexOf('://') !== -1) { - options.uri = device.cbHost + '/v1/subscribeContext'; - } else if (device.cbHost && device.cbHost.indexOf('://') === -1) { - options.uri = 'http://' + device.cbHost + '/v1/subscribeContext'; +function init() { + if (config.checkNgsiLD()) { + subscriptionHandler = require('./subscription-NGSI-LD'); + } else if (config.checkNgsi2()) { + subscriptionHandler = require('./subscription-NGSI-v2'); } else { - options.uri = config.getConfig().contextBroker.url + '/v1/subscribeContext'; + subscriptionHandler = require('./subscription-NGSI-v1'); } - deviceService.executeWithSecurity( - options, - device, - createSubscriptionHandlerNgsi1(device, triggers, store, callback) - ); -} - -/** - * Makes a subscription for the given device's entity using NGSIv2, 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 subscribeNgsi2(device, triggers, content, callback) { - const options = { - method: 'POST', - headers: { - 'fiware-service': device.service, - 'fiware-servicepath': device.subservice - }, - json: { - subject: { - entities: [ - { - id: device.name, - type: device.type - } - ], - - condition: { - attrs: triggers - } - }, - notification: { - http: { - url: config.getConfig().providerUrl + '/notify' - }, - attrs: content || [], - attrsFormat: 'normalized' - } - } - }; - - let store = true; - - if (content) { - store = false; - } - - if (device.cbHost && device.cbHost.indexOf('://') !== -1) { - options.uri = device.cbHost + '/v2/subscriptions'; - } else if (device.cbHost && device.cbHost.indexOf('://') === -1) { - options.uri = 'http://' + device.cbHost + '/v2/subscriptions'; - } else { - options.uri = config.getConfig().contextBroker.url + '/v2/subscriptions'; - } - deviceService.executeWithSecurity( - options, - device, - createSubscriptionHandlerNgsi2(device, triggers, store, callback) - ); } /** @@ -285,172 +55,7 @@ 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()) { - subscribeNgsi2(device, triggers, content, callback); - } else { - subscribeNgsi1(device, triggers, content, callback); - } -} - -/** - * Generate a new unsubscription request handler using NGSIv1, based on the device and subscription ID - * given to the function. - * - * @param {Object} device Object containing all the information about a particular device. - * @param {String} id ID of the subscription to remove. - * @param {Function} callback Callback to be called when the subscription handler ends. - * @return {Function} Returns a request handler for the given data. - */ -function createUnsubscribeHandlerNgsi1(device, id, callback) { - return function (error, response, body) { - if (error || !body) { - logger.debug( - context, - 'Transport error found subscribing device with id [%s] to entity [%s]', - device.id, - device.name - ); - - callback(error); - } else if (response.statusCode !== 200 && response.statusCode !== 201) { - logger.debug( - context, - 'Unknown error subscribing device with id [%s] to entity [%s]: $s', - response.statusCode - ); - - callback( - new errors.EntityGenericError( - device.name, - device.type, - { - details: body - }, - response.statusCode - ) - ); - } else if (body && body.orionError) { - logger.debug( - context, - 'Orion found subscribing device with id [%s] to entity [%s]: %s', - device.id, - device.name, - body.orionError - ); - - callback(new errors.BadRequest(body.orionError.details)); - } else { - device.subscriptions.splice(device.subscriptions.indexOf(id), 1); - config.getRegistry().update(device, callback); - } - }; -} - -/** - * Generate a new unsubscription request handler using NGSIv2, based on the device and subscription ID - * given to the function. - * - * @param {Object} device Object containing all the information about a particular device. - * @param {String} id ID of the subscription to remove. - * @param {Function} callback Callback to be called when the subscription handler ends. - * @return {Function} Returns a request handler for the given data. - */ -function createUnsubscribeHandlerNgsi2(device, id, callback) { - return function (error, response, body) { - if (error) { - logger.debug( - context, - 'Transport error found subscribing device with id [%s] to entity [%s]', - device.id, - device.name - ); - - callback(error); - } else if (response.statusCode !== 204) { - logger.debug( - context, - 'Unknown error subscribing device with id [%s] to entity [%s]: $s', - response.statusCode - ); - - callback( - new errors.EntityGenericError( - device.name, - device.type, - { - details: body - }, - response.statusCode - ) - ); - } else if (body && body.orionError) { - logger.debug( - context, - 'Orion found subscribing device with id [%s] to entity [%s]: %s', - device.id, - device.name, - body.orionError - ); - - callback(new errors.BadRequest(body.orionError.details)); - } else { - device.subscriptions.splice(device.subscriptions.indexOf(id), 1); - config.getRegistry().update(device, callback); - } - }; -} - -/** - * Remove the subscription with the given ID from the Context Broker and from the device repository using NGSIv1. - * - * @param {Object} device Object containing all the information about a particular device. - * @param {String} id ID of the subscription to remove. - */ -function unsubscribeNgsi1(device, id, callback) { - const options = { - method: 'POST', - headers: { - 'fiware-service': device.service, - 'fiware-servicepath': device.subservice - }, - json: { - subscriptionId: id - } - }; - - if (device.cbHost && device.cbHost.indexOf('://') !== -1) { - options.uri = device.cbHost + '/v1/unsubscribeContext'; - } else if (device.cbHost && device.cbHost.indexOf('://') === -1) { - options.uri = 'http://' + device.cbHost + '/v1/unsubscribeContext'; - } else { - options.uri = config.getConfig().contextBroker.url + '/v1/unsubscribeContext'; - } - deviceService.executeWithSecurity(options, device, createUnsubscribeHandlerNgsi1(device, id, callback)); -} - -/** - * Remove the subscription with the given ID from the Context Broker and from the device repository using NGSIv2. - * - * @param {Object} device Object containing all the information about a particular device. - * @param {String} id ID of the subscription to remove. - */ -function unsubscribeNgsi2(device, id, callback) { - const options = { - method: 'DELETE', - headers: { - 'fiware-service': device.service, - 'fiware-servicepath': device.subservice - } - }; - - if (device.cbHost && device.cbHost.indexOf('://') !== -1) { - options.uri = device.cbHost + '/v2/subscriptions/' + id; - } else if (device.cbHost && device.cbHost.indexOf('://') === -1) { - options.uri = 'http://' + device.cbHost + '/v2/subscriptions/' + id; - } else { - options.uri = config.getConfig().contextBroker.url + '/v2/subscriptions/' + id; - } - deviceService.executeWithSecurity(options, device, createUnsubscribeHandlerNgsi2(device, id, callback)); + subscriptionHandler.subscribe(device, triggers, content, callback); } /** @@ -460,12 +65,9 @@ function unsubscribeNgsi2(device, id, callback) { * @param {String} id ID of the subscription to remove. */ function unsubscribe(device, id, callback) { - if (config.checkNgsi2()) { - unsubscribeNgsi2(device, id, callback); - } else { - unsubscribeNgsi1(device, id, callback); - } + subscriptionHandler.unsubscribe(device, id, callback); } exports.subscribe = intoTrans(context, subscribe); exports.unsubscribe = intoTrans(context, unsubscribe); +exports.init = init; diff --git a/lib/services/northBound/contextServer-NGSI-LD.js b/lib/services/northBound/contextServer-NGSI-LD.js new file mode 100644 index 000000000..97d739fcd --- /dev/null +++ b/lib/services/northBound/contextServer-NGSI-LD.js @@ -0,0 +1,582 @@ +/* + * 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 + */ + +/* eslint-disable no-useless-escape */ +/* eslint-disable no-unused-vars */ +/* eslint-disable consistent-return */ + +const async = require('async'); +const apply = async.apply; +const logger = require('logops'); +const errors = require('../../errors'); +const deviceService = require('../devices/deviceService'); +const middlewares = require('../common/genericMiddleware'); +const _ = require('underscore'); +const context = { + op: 'IoTAgentNGSI.ContextServer-LD' +}; +const updateContextTemplateNgsiLD = require('../../templates/updateContextNgsiLD.json'); +const notificationTemplateNgsiLD = require('../../templates/notificationTemplateNgsiLD.json'); +const contextServerUtils = require('./contextServerUtils'); + +const updatePaths = ['/ngsi-ld/v1/entities/:entity/attrs/:attr']; +const queryPaths = ['/ngsi-ld/v1/entities/:entity']; + +/** + * Generate all the update actions corresponding to a update context request using Ngsi2. + * Update actions include updates in attributes and execution of commands. + * + * @param {Object} req Update request to generate Actions from + * @param {Object} contextElement Context Element whose actions will be extracted. + */ +function generateUpdateActionsNgsiLD(req, contextElement, callback) { + let entityId; + let entityType; + const attribute = req.params.attr; + const value = req.body.value; + + if (contextElement.id && contextElement.type) { + entityId = contextElement.id; + entityType = contextElement.type; + } else if (req.params.entity) { + entityId = req.params.entity; + } + + function splitUpdates(device, callback) { + const attributes = []; + const commands = []; + let found; + + if (device.commands) { + for (const j in device.commands) { + if (attribute === device.commands[j].name) { + commands.push({ + type: device.commands[j].type, + value, + name: attribute + }); + found = true; + } + } + } + + if (attribute && !found) { + attributes.push({ + type: 'Property', + value, + name: attribute + }); + } + callback(null, attributes, commands, device); + } + + function createActionsArray(attributes, commands, device, callback) { + const updateActions = []; + + if (!entityType) { + entityType = device.type; + } + + if (contextServerUtils.updateHandler) { + updateActions.push( + async.apply( + contextServerUtils.updateHandler, + entityId, + entityType, + contextServerUtils.getLDTenant(req), + contextServerUtils.getLDPath(req), + attributes + ) + ); + } + + if (contextServerUtils.commandHandler) { + if (device.polling) { + updateActions.push( + async.apply( + contextServerUtils.pushCommandsToQueue, + device, + entityId, + entityType, + contextServerUtils.getLDTenant(req), + contextServerUtils.getLDPath(req), + attributes + ) + ); + } else { + updateActions.push( + async.apply( + contextServerUtils.commandHandler, + entityId, + entityType, + contextServerUtils.getLDTenant(req), + contextServerUtils.getLDPath(req), + commands + ) + ); + } + } + + updateActions.push( + async.apply( + contextServerUtils.executeUpdateSideEffects, + device, + entityId, + entityType, + contextServerUtils.getLDTenant(req), + contextServerUtils.getLDPath(req), + attributes + ) + ); + + callback(null, updateActions); + } + + deviceService.getDeviceByName( + entityId, + contextServerUtils.getLDTenant(req), + contextServerUtils.getLDPath(req), + function (error, deviceObj) { + if (error) { + callback(error); + } else { + async.waterfall( + [ + apply(deviceService.findConfigurationGroup, deviceObj), + apply( + deviceService.mergeDeviceWithConfiguration, + ['lazy', 'internalAttributes', 'active', 'staticAttributes', 'commands', 'subscriptions'], + [null, null, [], [], [], [], []], + deviceObj + ), + splitUpdates, + createActionsArray + ], + callback + ); + } + } + ); +} + +/** 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 (contextServerUtils.updateHandler || contextServerUtils.commandHandler) { + logger.debug(context, 'Handling LD update from [%s]', req.get('host')); + if (req.body) { + logger.debug(context, JSON.stringify(req.body, null, 4)); + } + + async.waterfall( + [apply(async.map, req.body, apply(generateUpdateActionsNgsiLD, 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 established.'); + + const errorNotFound = new Error({ + message: 'Update handler not found' + }); + next(errorNotFound); + } +} + +/** + * Handle queries coming to the IoT Agent via de Context Provider API (as a consequence of a query to a passive + * attribute redirected by the Context Broker). + * + * @param {String} id Entity name of the selected entity in the query. + * @param {String} type Type of the entity. + * @param {String} service Service the device belongs to. + * @param {String} subservice Division inside the service. + * @param {Array} attributes List of attributes to read. + */ +function defaultQueryHandlerNgsiLD(id, type, service, subservice, attributes, callback) { + const contextElement = { + type, + id + }; + + deviceService.getDeviceByName(id, service, subservice, function (error, ngsiDevice) { + if (error) { + callback(error); + } else { + for (let i = 0; i < attributes.length; i++) { + const lazyAttribute = _.findWhere(ngsiDevice.lazy, { name: attributes[i] }); + const command = _.findWhere(ngsiDevice.commands, { name: attributes[i] }); + let attributeType = 'string'; + + if (command) { + attributeType = command.type; + } else if (lazyAttribute) { + attributeType = lazyAttribute.type; + } + + contextElement[attributes[i]] = { + type: attributeType, + value: '' + }; + } + + callback(null, contextElement); + } + }); +} + +/** + * 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) { + let selectedAttributes = []; + if (attributes === undefined || attributes.length === 0) { + selectedAttributes = device.staticAttributes; + } else { + selectedAttributes = device.staticAttributes.filter(inAttributes); + } + + for (const 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); + const 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) { + let contextId = contextEntity.id; + let contextType = contextEntity.type; + if (!contextId) { + contextId = device.id; + } + + if (!contextType) { + contextType = device.type; + } + + deviceService.findConfigurationGroup(device, function (error, group) { + const executeCompleteAttributes = apply(completeAttributes, attributes, group); + const executeQueryHandler = apply( + actualHandler, + contextId, + contextType, + contextServerUtils.getLDTenant(req), + contextServerUtils.getLDPath(req) + ); + const executeAddStaticAttributes = apply(addStaticAttributes, attributes, group); + + async.waterfall([executeCompleteAttributes, executeQueryHandler, executeAddStaticAttributes], callback); + }); + } + + function createQueryRequest(attributes, contextEntity, callback) { + let actualHandler; + let getFunction; + + if (contextServerUtils.queryHandler) { + actualHandler = contextServerUtils.queryHandler; + } else { + actualHandler = defaultQueryHandlerNgsiLD; + } + + if (contextEntity.id) { + getFunction = apply( + deviceService.getDeviceByName, + contextEntity.id, + contextServerUtils.getLDTenant(req), + contextServerUtils.getLDPath(req) + ); + } else { + getFunction = apply( + deviceService.listDevicesWithType, + contextEntity.type, + contextServerUtils.getLDTenant(req), + contextServerUtils.getLDPath(req), + 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, []); + } + 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 > 1) { + callback(null, results); + } else { + callback(null, results[0]); + } + } + ); + }); + } + + 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')); + if (req.body) { + logger.debug(context, JSON.stringify(req.body, null, 4)); + } + + const nss = req.params.entity.replace('urn:ngsi-ld:', ''); + const contextEntity = { + id: req.params.entity, + type: nss.substring(0, nss.indexOf(':')) + }; + + createQueryRequest(req.query.attrs ? req.query.attrs.split(',') : null, contextEntity, handleQueryContextRequests); +} + +/** + * 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) { + let 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, '') + }); +} + +/** + * 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) { + const atts = []; + for (const key in dataElement) { + /* eslint-disable-next-line no-prototype-builtins */ + if (dataElement.hasOwnProperty(key)) { + if (key !== 'id' && key !== 'type') { + const att = {}; + att.type = dataElement[key].type; + att.value = dataElement[key].value; + att.name = key; + atts.push(att); + } + } + } + deviceService.getDeviceByName( + dataElement.id, + contextServerUtils.getLDTenant(req), + contextServerUtils.getLDPath(req), + function (error, device) { + if (error) { + callback(error); + } else { + callback(null, device, atts); + } + } + ); + } + + function applyNotificationMiddlewares(device, values, callback) { + if (contextServerUtils.notificationMiddlewares.length > 0) { + const firstMiddleware = contextServerUtils.notificationMiddlewares.slice(0, 1)[0]; + const rest = contextServerUtils.notificationMiddlewares.slice(1); + const startMiddleware = apply(firstMiddleware, device, values); + const composedMiddlewares = [startMiddleware].concat(rest); + + async.waterfall(composedMiddlewares, callback); + } else { + callback(null, device, values); + } + } + + function createNotificationHandler(contextResponse, callback) { + async.waterfall( + [ + apply(extractInformation, contextResponse), + applyNotificationMiddlewares, + contextServerUtils.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 (contextServerUtils.notificationHandler) { + logger.debug(context, 'Handling notification from [%s]', req.get('host')); + async.map(req.body.data, createNotificationHandler, handleNotificationRequests); + } else { + const errorNotFound = new Error({ + message: 'Notification handler not found' + }); + + logger.error(context, 'Tried to handle a notification before notification handler was established.'); + + next(errorNotFound); + } +} + +/** + * 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 the routes related to context dispatching (NGSI10 calls). + * + * @param {Object} router Express request router object. + */ +function loadContextRoutesNGSILD(router) { + // In a more evolved implementation, more endpoints could be added to queryPathsNgsi2 + // according to http://fiware.github.io/specifications/ngsiv2/stable. + + let i; + + logger.info(context, 'Loading NGSI-LD Context server routes'); + for (i = 0; i < updatePaths.length; i++) { + router.patch(updatePaths[i], [ + middlewares.ensureType, + middlewares.validateJson(updateContextTemplateNgsiLD), + handleUpdateNgsiLD, + updateErrorHandlingNgsiLD + ]); + } + for (i = 0; i < queryPaths.length; i++) { + router.get(queryPaths[i], [handleQueryNgsiLD, queryErrorHandlingNgsiLD]); + } + router.post('/notify', [ + middlewares.ensureType, + middlewares.validateJson(notificationTemplateNgsiLD), + handleNotificationNgsiLD, + queryErrorHandlingNgsiLD + ]); +} + +exports.loadContextRoutes = loadContextRoutesNGSILD; diff --git a/lib/services/northBound/contextServer-NGSI-v1.js b/lib/services/northBound/contextServer-NGSI-v1.js new file mode 100644 index 000000000..1bc9ccef5 --- /dev/null +++ b/lib/services/northBound/contextServer-NGSI-v1.js @@ -0,0 +1,526 @@ +/* + * 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: Daniel Calvo - ATOS Research & Innovation + * Modified by: Jason Fox - FIWARE Foundation + */ + +/* eslint-disable no-useless-escape */ +/* eslint-disable no-unused-vars */ +/* eslint-disable no-prototype-builtins */ +/* eslint-disable consistent-return */ + +const async = require('async'); +const apply = async.apply; +const logger = require('logops'); +const errors = require('../../errors'); +const deviceService = require('../devices/deviceService'); +const middlewares = require('../common/genericMiddleware'); +const _ = require('underscore'); +const context = { + op: 'IoTAgentNGSI.ContextServer-v1' +}; +const updateContextTemplateNgsi1 = require('../../templates/updateContextNgsi1.json'); +const queryContextTemplate = require('../../templates/queryContext.json'); +const notificationTemplateNgsi1 = require('../../templates/notificationTemplateNgsi1.json'); +const contextServerUtils = require('./contextServerUtils'); + +const updatePaths = ['/v1/updateContext', '/NGSI10/updateContext', '//updateContext']; +const queryPaths = ['/v1/queryContext', '/NGSI10/queryContext', '//queryContext']; +/** + * Generate all the update actions corresponding to a update context request using Ngsi1. + * Update actions include updates in attributes and execution of commands. This action will + * be called once per Context Element in the request. + * + * @param {Object} req Update request to generate Actions from + * @param {Object} contextElement Context Element whose actions will be extracted. + */ +function generateUpdateActionsNgsi1(req, contextElement, callback) { + function splitUpdates(device, callback) { + let attributes = []; + const commands = []; + let found; + + if (device.commands) { + attributeLoop: for (const i in contextElement.attributes) { + for (const j in device.commands) { + if (contextElement.attributes[i].name === device.commands[j].name) { + commands.push(contextElement.attributes[i]); + found = true; + continue attributeLoop; + } + } + + attributes.push(contextElement.attributes[i]); + } + } else { + attributes = contextElement.attributes; + } + + callback(null, attributes, commands, device); + } + + function createActionsArray(attributes, commands, device, callback) { + const updateActions = []; + + if (contextServerUtils.updateHandler) { + updateActions.push( + async.apply( + contextServerUtils.updateHandler, + contextElement.id, + contextElement.type, + req.headers['fiware-service'], + req.headers['fiware-servicepath'], + attributes + ) + ); + } + + if (contextServerUtils.commandHandler) { + if (device.polling) { + updateActions.push( + async.apply( + contextServerUtils.pushCommandsToQueue, + device, + contextElement.id, + contextElement.type, + req.headers['fiware-service'], + req.headers['fiware-servicepath'], + contextElement.attributes + ) + ); + } else { + updateActions.push( + async.apply( + contextServerUtils.commandHandler, + contextElement.id, + contextElement.type, + req.headers['fiware-service'], + req.headers['fiware-servicepath'], + commands + ) + ); + } + } + + updateActions.push( + async.apply( + contextServerUtils.executeUpdateSideEffects, + device, + contextElement.id, + contextElement.type, + req.headers['fiware-service'], + req.headers['fiware-servicepath'], + contextElement.attributes + ) + ); + + callback(null, updateActions); + } + + deviceService.getDeviceByName( + contextElement.id, + req.headers['fiware-service'], + req.headers['fiware-servicepath'], + function (error, deviceObj) { + if (error) { + callback(error); + } else { + async.waterfall( + [ + apply(deviceService.findConfigurationGroup, deviceObj), + apply( + deviceService.mergeDeviceWithConfiguration, + ['lazy', 'internalAttributes', 'active', 'staticAttributes', 'commands', 'subscriptions'], + [null, null, [], [], [], [], []], + deviceObj + ), + splitUpdates, + createActionsArray + ], + callback + ); + } + } + ); +} + +/** + * Express middleware to manage incoming UpdateContext requests using NGSIv1. + * As NGSI10 requests can affect multiple entities, for each one of them a call + * to the user update handler function is made. + */ +function handleUpdateNgsi1(req, res, next) { + function reduceActions(actions, callback) { + callback(null, _.flatten(actions)); + } + + if (contextServerUtils.updateHandler || contextServerUtils.commandHandler) { + logger.debug(context, 'Handling v1 update from [%s]', req.get('host')); + if (req.body) { + logger.debug(context, JSON.stringify(req.body, null, 4)); + } + + async.waterfall( + [ + apply(async.map, req.body.contextElements, apply(generateUpdateActionsNgsi1, 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(200).json(contextServerUtils.createUpdateResponse(req, res, result)); + } + } + ); + } else { + logger.error(context, 'Tried to handle an update request before the update handler was stablished.'); + + const errorNotFound = new Error({ + message: 'Update handler not found' + }); + next(errorNotFound); + } +} + +/** + * Handle queries coming to the IoT Agent via de Context Provider API (as a consequence of a query to a passive + * attribute redirected by the Context Broker). + * + * @param {String} id Entity name of the selected entity in the query. + * @param {String} type Type of the entity. + * @param {String} service Service the device belongs to. + * @param {String} subservice Division inside the service. + * @param {Array} attributes List of attributes to read. + */ +function defaultQueryHandlerNgsi1(id, type, service, subservice, attributes, callback) { + const contextElement = { + type, + isPattern: false, + id, + attributes: [] + }; + + deviceService.getDeviceByName(id, service, subservice, function (error, ngsiDevice) { + if (error) { + callback(error); + } else { + for (let i = 0; i < attributes.length; i++) { + const lazyAttribute = _.findWhere(ngsiDevice.lazy, { name: attributes[i] }); + const command = _.findWhere(ngsiDevice.commands, { name: attributes[i] }); + let attributeType = 'string'; + + if (command) { + attributeType = command.type; + } else if (lazyAttribute) { + attributeType = lazyAttribute.type; + } + + contextElement.attributes.push({ + name: attributes[i], + type: attributeType, + value: '' + }); + } + + callback(null, contextElement); + } + }); +} + +/** + * Express middleware to manage incoming QueryContext requests using NGSIv1. + * As NGSI10 requests can affect multiple entities, for each one of them a call + * to the user query handler function is made. + */ +function handleQueryNgsi1(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) { + const selectedAttributes = device.staticAttributes.filter(inAttributes); + + if (selectedAttributes.length > 0) { + if (contextElement.attributes) { + contextElement.attributes = contextElement.attributes.concat(selectedAttributes); + } else { + contextElement.attributes = selectedAttributes; + } + } + } + + 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); + callback(null, device.lazy.map(getName)); + } else { + logger.debug(context, "Couldn't find any attributes. Handling with null reference"); + callback(null, null); + } + } + + function createQueryRequests(attributes, contextEntity, callback) { + let actualHandler; + + if (contextServerUtils.queryHandler) { + actualHandler = contextServerUtils.queryHandler; + } else { + actualHandler = defaultQueryHandlerNgsi1; + } + + async.waterfall( + [ + apply( + deviceService.getDeviceByName, + contextEntity.id, + req.headers['fiware-service'], + req.headers['fiware-servicepath'] + ), + deviceService.findConfigurationGroup + ], + function handleFindDevice(error, device) { + const executeCompleteAttributes = apply(completeAttributes, attributes, device); + const executeQueryHandler = apply( + actualHandler, + contextEntity.id, + contextEntity.type, + req.headers['fiware-service'], + req.headers['fiware-servicepath'] + ); + const executeAddStaticAttributes = apply(addStaticAttributes, attributes, device); + + callback( + error, + apply(async.waterfall, [executeCompleteAttributes, executeQueryHandler, executeAddStaticAttributes]) + ); + } + ); + } + + 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(contextServerUtils.createQueryResponse(req, res, result)); + } + } + + logger.debug(context, 'Handling query from [%s]', req.get('host')); + if (req.body) { + logger.debug(context, JSON.stringify(req.body, null, 4)); + } + + async.waterfall( + [apply(async.map, req.body.entities, apply(createQueryRequests, req.body.attributes)), async.series], + handleQueryContextRequests + ); +} + +/** + * 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) { + if (statusCode.code && statusCode.code === '200') { + callback(); + } else { + callback(new errors.NotificationError(statusCode.code)); + } + } + + function extractInformation(contextResponse, callback) { + deviceService.getDeviceByName( + contextResponse.contextElement.id, + req.headers['fiware-service'], + req.headers['fiware-servicepath'], + function (error, device) { + if (error) { + callback(error); + } else { + callback(null, device, contextResponse.contextElement.attributes); + } + } + ); + } + + function applyNotificationMiddlewares(device, values, callback) { + if (contextServerUtils.notificationMiddlewares.length > 0) { + const firstMiddleware = contextServerUtils.notificationMiddlewares.slice(0, 1)[0]; + const rest = contextServerUtils.notificationMiddlewares.slice(1); + const startMiddleware = apply(firstMiddleware, device, values); + const composedMiddlewares = [startMiddleware].concat(rest); + + async.waterfall(composedMiddlewares, callback); + } else { + callback(null, device, values); + } + } + + function createNotificationHandler(contextResponse, callback) { + async.waterfall( + [ + apply(checkStatus, contextResponse.statusCode), + apply(extractInformation, contextResponse), + applyNotificationMiddlewares, + contextServerUtils.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 (contextServerUtils.notificationHandler) { + logger.debug(context, 'Handling notification from [%s]', req.get('host')); + + async.map(req.body.contextResponses, createNotificationHandler, handleNotificationRequests); + } else { + const errorNotFound = new Error({ + message: 'Notification handler not found' + }); + + logger.error(context, 'Tried to handle a notification before notification handler was established.'); + + next(errorNotFound); + } +} + +/** + * 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) { + let code = 500; + + logger.debug(context, 'Query NGSIv1 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({ + errorCode: { + code, + reasonPhrase: error.name, + details: 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) { + let code = 500; + + logger.debug(context, 'Update NGSIv1 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({ + contextResponses: [ + { + contextElement: req.body, + statusCode: { + code, + reasonPhrase: error.name, + details: error.message.replace(/[<>\"\'=;\(\)]/g, '') + } + } + ] + }); +} + +/** + * Load the routes related to context dispatching (NGSI10 calls). + * + * @param {Object} router Express request router object. + */ +function loadContextRoutesNGSIv1(router) { + // In a more evolved implementation, more endpoints could be added to queryPathsNgsi2 + // according to http://fiware.github.io/specifications/ngsiv2/stable. + + let i; + logger.info(context, 'Loading NGSI-v1 Context server routes'); + for (i = 0; i < updatePaths.length; i++) { + router.post(updatePaths[i], [ + middlewares.ensureType, + middlewares.validateJson(updateContextTemplateNgsi1), + handleUpdateNgsi1, + updateErrorHandlingNgsi1 + ]); + } + for (i = 0; i < queryPaths.length; i++) { + router.post(queryPaths[i], [ + middlewares.ensureType, + middlewares.validateJson(queryContextTemplate), + handleQueryNgsi1, + queryErrorHandlingNgsi1 + ]); + } + router.post('/notify', [ + middlewares.ensureType, + middlewares.validateJson(notificationTemplateNgsi1), + handleNotificationNgsi1, + queryErrorHandlingNgsi1 + ]); +} + +exports.loadContextRoutes = loadContextRoutesNGSIv1; diff --git a/lib/services/northBound/contextServer-NGSI-v2.js b/lib/services/northBound/contextServer-NGSI-v2.js new file mode 100644 index 000000000..23b2a9ccb --- /dev/null +++ b/lib/services/northBound/contextServer-NGSI-v2.js @@ -0,0 +1,594 @@ +/* + * 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: Daniel Calvo - ATOS Research & Innovation + * Modified by: Jason Fox - FIWARE Foundation + */ + +/* eslint-disable no-useless-escape */ +/* eslint-disable no-unused-vars */ +/* eslint-disable no-prototype-builtins */ +/* eslint-disable consistent-return */ + +const async = require('async'); +const apply = async.apply; +const logger = require('logops'); +const errors = require('../../errors'); +const deviceService = require('../devices/deviceService'); +const middlewares = require('../common/genericMiddleware'); +const _ = require('underscore'); +const context = { + op: 'IoTAgentNGSI.ContextServer-v2' +}; +const updateContextTemplateNgsi2 = require('../../templates/updateContextNgsi2.json'); +const notificationTemplateNgsi2 = require('../../templates/notificationTemplateNgsi2.json'); +const contextServerUtils = require('./contextServerUtils'); + +const updatePaths = ['/v2/op/update', '//op/update']; +const queryPaths = ['/v2/op/query', '//op/query']; +/** + * Generate all the update actions corresponding to a update context request using Ngsi2. + * Update actions include updates in attributes and execution of commands. + * + * @param {Object} req Update request to generate Actions from + * @param {Object} contextElement Context Element whose actions will be extracted. + */ +function generateUpdateActionsNgsi2(req, contextElement, callback) { + let entityId; + let entityType; + + if (contextElement.id && contextElement.type) { + entityId = contextElement.id; + entityType = contextElement.type; + } else if (req.params.entity) { + entityId = req.params.entity; + } + + function splitUpdates(device, callback) { + const attributes = []; + const commands = []; + let found; + let newAtt; + let i; + + if (device.commands) { + attributeLoop: for (i in contextElement) { + for (const j in device.commands) { + if (i === device.commands[j].name) { + newAtt = {}; + newAtt[i] = contextElement[i]; + newAtt[i].name = i; + commands.push(newAtt[i]); + found = true; + continue attributeLoop; + } + } + } + } + + for (i in contextElement) { + if (i !== 'type' && i !== 'id') { + newAtt = {}; + newAtt = contextElement[i]; + newAtt.name = i; + attributes.push(newAtt); + } + } + + callback(null, attributes, commands, device); + } + + function createActionsArray(attributes, commands, device, callback) { + const updateActions = []; + + if (!entityType) { + entityType = device.type; + } + + if (contextServerUtils.updateHandler) { + updateActions.push( + async.apply( + contextServerUtils.updateHandler, + entityId, + entityType, + req.headers['fiware-service'], + req.headers['fiware-servicepath'], + attributes + ) + ); + } + + if (contextServerUtils.commandHandler) { + if (device.polling) { + updateActions.push( + async.apply( + contextServerUtils.pushCommandsToQueue, + device, + entityId, + entityType, + req.headers['fiware-service'], + req.headers['fiware-servicepath'], + attributes + ) + ); + } else { + updateActions.push( + async.apply( + contextServerUtils.commandHandler, + entityId, + entityType, + req.headers['fiware-service'], + req.headers['fiware-servicepath'], + commands + ) + ); + } + } + + updateActions.push( + async.apply( + contextServerUtils.executeUpdateSideEffects, + device, + entityId, + entityType, + req.headers['fiware-service'], + req.headers['fiware-servicepath'], + attributes + ) + ); + + callback(null, updateActions); + } + + deviceService.getDeviceByName(entityId, req.headers['fiware-service'], req.headers['fiware-servicepath'], function ( + error, + deviceObj + ) { + if (error) { + callback(error); + } else { + async.waterfall( + [ + apply(deviceService.findConfigurationGroup, deviceObj), + apply( + deviceService.mergeDeviceWithConfiguration, + ['lazy', 'internalAttributes', 'active', 'staticAttributes', 'commands', 'subscriptions'], + [null, null, [], [], [], [], []], + deviceObj + ), + splitUpdates, + createActionsArray + ], + callback + ); + } + }); +} + +/** 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) { + callback(null, _.flatten(actions)); + } + + if (contextServerUtils.updateHandler || contextServerUtils.commandHandler) { + logger.debug(context, 'Handling v2 update from [%s]', req.get('host')); + if (req.body) { + logger.debug(context, JSON.stringify(req.body, null, 4)); + } + + 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.'); + + const errorNotFound = new Error({ + message: 'Update handler not found' + }); + next(errorNotFound); + } +} + +/** + * Handle queries coming to the IoT Agent via de Context Provider API (as a consequence of a query to a passive + * attribute redirected by the Context Broker). + * + * @param {String} id Entity name of the selected entity in the query. + * @param {String} type Type of the entity. + * @param {String} service Service the device belongs to. + * @param {String} subservice Division inside the service. + * @param {Array} attributes List of attributes to read. + */ +function defaultQueryHandlerNgsi2(id, type, service, subservice, attributes, callback) { + const contextElement = { + type, + id + }; + + deviceService.getDeviceByName(id, service, subservice, function (error, ngsiDevice) { + if (error) { + callback(error); + } else { + for (let i = 0; i < attributes.length; i++) { + const lazyAttribute = _.findWhere(ngsiDevice.lazy, { name: attributes[i] }); + const command = _.findWhere(ngsiDevice.commands, { name: attributes[i] }); + let attributeType = 'string'; + + if (command) { + attributeType = command.type; + } else if (lazyAttribute) { + attributeType = lazyAttribute.type; + } + + contextElement[attributes[i]] = { + type: attributeType, + value: '' + }; + } + + callback(null, contextElement); + } + }); +} + +/** + * Express middleware to manage incoming query context 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) { + const atts = []; + for (const key in dataElement) { + if (dataElement.hasOwnProperty(key)) { + if (key !== 'id' && key !== 'type') { + const 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 (contextServerUtils.notificationMiddlewares.length > 0) { + const firstMiddleware = contextServerUtils.notificationMiddlewares.slice(0, 1)[0]; + const rest = contextServerUtils.notificationMiddlewares.slice(1); + const startMiddleware = apply(firstMiddleware, device, values); + const composedMiddlewares = [startMiddleware].concat(rest); + + async.waterfall(composedMiddlewares, callback); + } else { + callback(null, device, values); + } + } + + function createNotificationHandler(contextResponse, callback) { + async.waterfall( + [ + apply(extractInformation, contextResponse), + applyNotificationMiddlewares, + contextServerUtils.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 (contextServerUtils.notificationHandler) { + logger.debug(context, 'Handling notification from [%s]', req.get('host')); + async.map(req.body.data, createNotificationHandler, handleNotificationRequests); + } else { + const errorNotFound = new Error({ + message: 'Notification handler not found' + }); + + logger.error(context, 'Tried to handle a notification before notification handler was established.'); + + next(errorNotFound); + } +} + +/** + * 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) { + let code = 500; + + logger.debug(context, 'Query NGSIv2 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 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) { + let code = 500; + + logger.debug(context, 'Update NGSIv2 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, '') + }); +} + +/** + * Express middleware to manage incoming query context requests using NGSIv2. + */ +function handleQueryNgsi2(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) { + let selectedAttributes = []; + if (attributes === undefined || attributes.length === 0) { + selectedAttributes = device.staticAttributes; + } else { + selectedAttributes = device.staticAttributes.filter(inAttributes); + } + + for (const 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); + const 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) { + let contextId = contextEntity.id; + let contextType = contextEntity.type; + if (!contextId) { + contextId = device.id; + } + + if (!contextType) { + contextType = device.type; + } + + deviceService.findConfigurationGroup(device, function (error, group) { + const executeCompleteAttributes = apply(completeAttributes, attributes, group); + const executeQueryHandler = apply( + actualHandler, + contextId, + contextType, + req.headers['fiware-service'], + req.headers['fiware-servicepath'] + ); + const executeAddStaticAttributes = apply(addStaticAttributes, attributes, group); + + async.waterfall([executeCompleteAttributes, executeQueryHandler, executeAddStaticAttributes], callback); + }); + } + + function createQueryRequest(attributes, contextEntity, callback) { + let actualHandler; + let getFunction; + + if (contextServerUtils.queryHandler) { + actualHandler = contextServerUtils.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, []); + } + 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')); + if (req.body) { + logger.debug(context, JSON.stringify(req.body, null, 4)); + } + const 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; + const queryAtts = req.body.attrs; + createQueryRequest(queryAtts, contextEntity, handleQueryContextRequests); +} + +/** + * Load the routes related to context dispatching (NGSI10 calls). + * + * @param {Object} router Express request router object. + */ +function loadContextRoutesNGSIv2(router) { + // In a more evolved implementation, more endpoints could be added to queryPathsNgsi2 + // according to http://fiware.github.io/specifications/ngsiv2/stable. + + let i; + logger.info(context, 'Loading NGSI-v2 Context server routes'); + for (i = 0; i < updatePaths.length; i++) { + router.post(updatePaths[i], [ + middlewares.ensureType, + middlewares.validateJson(updateContextTemplateNgsi2), + handleUpdateNgsi2, + updateErrorHandlingNgsi2 + ]); + } + for (i = 0; i < queryPaths.length; i++) { + router.post(queryPaths[i], [handleQueryNgsi2, queryErrorHandlingNgsi2]); + } + router.post('/notify', [ + middlewares.ensureType, + middlewares.validateJson(notificationTemplateNgsi2), + handleNotificationNgsi2, + queryErrorHandlingNgsi2 + ]); +} + +exports.loadContextRoutes = loadContextRoutesNGSIv2; diff --git a/lib/services/northBound/contextServer.js b/lib/services/northBound/contextServer.js index ebf65d16e..b1a578e28 100644 --- a/lib/services/northBound/contextServer.js +++ b/lib/services/northBound/contextServer.js @@ -1,5 +1,5 @@ /* - * Copyright 2014 Telefonica Investigación y Desarrollo, S.A.U + * Copyright 2020 Telefonica Investigación y Desarrollo, S.A.U * * This file is part of fiware-iotagent-lib * @@ -21,997 +21,28 @@ * please contact with::daniel.moranjimenez@telefonica.com * * Modified by: Daniel Calvo - ATOS Research & Innovation + * Modified by: Jason Fox - FIWARE Foundation */ -/* eslint-disable no-useless-escape */ -/* eslint-disable no-unused-vars */ -/* eslint-disable no-prototype-builtins */ -/* eslint-disable consistent-return */ - -const async = require('async'); -const apply = async.apply; -const logger = require('logops'); -const constants = require('../../constants'); -const errors = require('../../errors'); -const ngsi = require('../ngsi/ngsiService'); const intoTrans = require('../common/domain').intoTrans; -const deviceService = require('../devices/deviceService'); -const commands = require('../commands/commandService'); -const middlewares = require('../common/genericMiddleware'); -const _ = require('underscore'); const config = require('../../commonConfig'); const context = { op: 'IoTAgentNGSI.ContextServer' }; -const updateContextTemplateNgsi1 = require('../../templates/updateContextNgsi1.json'); -const updateContextTemplateNgsi2 = require('../../templates/updateContextNgsi2.json'); -const queryContextTemplate = require('../../templates/queryContext.json'); -const notificationTemplateNgsi1 = require('../../templates/notificationTemplateNgsi1.json'); -const notificationTemplateNgsi2 = require('../../templates/notificationTemplateNgsi2.json'); -let notificationMiddlewares = []; -let updateHandler; -let commandHandler; -let queryHandler; -let notificationHandler; - -/** - * Create the response for an UpdateContext operation, based on the results of the individual updates. The signature - * retains the results object for homogeinity with the createQuery* version. - * - * @param {Object} req Request that was handled in first place. - * @param {Object} res Response that will be sent. - * @param {Object} results Ignored for this function. TODO: to be removed in later versions. - * @return {{contextResponses: Array}} - */ -function createUpdateResponse(req, res, results) { - const result = { - contextResponses: [] - }; - - for (let i = 0; i < req.body.contextElements.length; i++) { - const contextResponse = { - contextElement: { - attributes: req.body.contextElements[i].attributes, - id: req.body.contextElements[i].id, - isPattern: false, - type: req.body.contextElements[i].type - }, - statusCode: { - code: 200, - reasonPhrase: 'OK' - } - }; - - for (let j = 0; j < contextResponse.contextElement.attributes.length; j++) { - contextResponse.contextElement.attributes[i].value = ''; - } - - result.contextResponses.push(contextResponse); - } - - logger.debug(context, 'Generated update response: %j', result); - - return result; -} - -/** - * Create the response for a queryContext operation based on the individual results gathered from the query handlers. - * The returned response is in the NGSI Response format. - * - * @param {Object} req Request that was handled in first place. - * @param {Object} res Response that will be sent. - * @param {Object} results Individual Context Element results from the query handlers. - * @return {{contextResponses: Array}} - */ -function createQueryResponse(req, res, results) { - const result = { - contextResponses: [] - }; - - for (let i = 0; i < results.length; i++) { - const contextResponse = { - contextElement: results[i], - statusCode: { - code: 200, - reasonPhrase: 'OK' - } - }; - - contextResponse.contextElement.isPattern = false; - - result.contextResponses.push(contextResponse); - } - - logger.debug(context, 'Generated query response: %j', result); - - return result; -} - -/** - * Retrieve the Device that corresponds to a Context Update, and execute the update side effects - * if there were any (e.g.: creation of attributes related to comands). - * - * @param {String} device Object that contains all the information about the device. - * @param {String} id Entity ID of the device to find. - * @param {String} type Type of the device to find. - * @param {String} service Service of the device. - * @param {String} subservice Subservice of the device. - * @param {Array} attributes List of attributes to update with their types and values. - */ -function executeUpdateSideEffects(device, id, type, service, subservice, attributes, callback) { - const sideEffects = []; - - if (device.commands) { - for (let i = 0; i < device.commands.length; i++) { - for (let j = 0; j < attributes.length; j++) { - if (device.commands[i].name === attributes[j].name) { - const newAttributes = [ - { - name: device.commands[i].name + '_status', - type: constants.COMMAND_STATUS, - value: 'PENDING' - } - ]; - - sideEffects.push( - apply(ngsi.update, device.name, device.resource, device.apikey, newAttributes, device) - ); - } - } - } - } - - async.series(sideEffects, callback); -} - -/** - * Extract all the commands from the attributes section and add them to the Commands Queue. - * - * @param {String} device Object that contains all the information about the device. - * @param {String} id Entity ID of the device to find. - * @param {String} type Type of the device to find. - * @param {String} service Service of the device. - * @param {String} subservice Subservice of the device. - * @param {Array} attributes List of attributes to update with their types and values. - */ -function pushCommandsToQueue(device, id, type, service, subservice, attributes, callback) { - async.map(attributes, apply(commands.add, service, subservice, device.id), callback); -} - -/** - * Generate all the update actions corresponding to a update context request using Ngsi1. - * Update actions include updates in attributes and execution of commands. This action will - * be called once per Context Element in the request. - * - * @param {Object} req Update request to generate Actions from - * @param {Object} contextElement Context Element whose actions will be extracted. - */ -function generateUpdateActionsNgsi1(req, contextElement, callback) { - function splitUpdates(device, callback) { - let attributes = []; - const commands = []; - let found; - - if (device.commands) { - attributeLoop: for (const i in contextElement.attributes) { - for (const j in device.commands) { - if (contextElement.attributes[i].name === device.commands[j].name) { - commands.push(contextElement.attributes[i]); - found = true; - continue attributeLoop; - } - } - - attributes.push(contextElement.attributes[i]); - } - } else { - attributes = contextElement.attributes; - } - - callback(null, attributes, commands, device); - } - - function createActionsArray(attributes, commands, device, callback) { - const updateActions = []; - - if (updateHandler) { - updateActions.push( - async.apply( - updateHandler, - contextElement.id, - contextElement.type, - req.headers['fiware-service'], - req.headers['fiware-servicepath'], - attributes - ) - ); - } - - if (commandHandler) { - if (device.polling) { - updateActions.push( - async.apply( - pushCommandsToQueue, - device, - contextElement.id, - contextElement.type, - req.headers['fiware-service'], - req.headers['fiware-servicepath'], - contextElement.attributes - ) - ); - } else { - updateActions.push( - async.apply( - commandHandler, - contextElement.id, - contextElement.type, - req.headers['fiware-service'], - req.headers['fiware-servicepath'], - commands - ) - ); - } - } - - updateActions.push( - async.apply( - executeUpdateSideEffects, - device, - contextElement.id, - contextElement.type, - req.headers['fiware-service'], - req.headers['fiware-servicepath'], - contextElement.attributes - ) - ); - - callback(null, updateActions); - } - - deviceService.getDeviceByName( - contextElement.id, - req.headers['fiware-service'], - req.headers['fiware-servicepath'], - function (error, deviceObj) { - if (error) { - callback(error); - } else { - async.waterfall( - [ - apply(deviceService.findConfigurationGroup, deviceObj), - apply( - deviceService.mergeDeviceWithConfiguration, - ['lazy', 'internalAttributes', 'active', 'staticAttributes', 'commands', 'subscriptions'], - [null, null, [], [], [], [], []], - deviceObj - ), - splitUpdates, - createActionsArray - ], - callback - ); - } - } - ); -} - -/** - * Generate all the update actions corresponding to a update context request using Ngsi2. - * Update actions include updates in attributes and execution of commands. - * - * @param {Object} req Update request to generate Actions from - * @param {Object} contextElement Context Element whose actions will be extracted. - */ -function generateUpdateActionsNgsi2(req, contextElement, callback) { - let entityId; - let entityType; - - if (contextElement.id && contextElement.type) { - entityId = contextElement.id; - entityType = contextElement.type; - } else if (req.params.entity) { - entityId = req.params.entity; - } - - function splitUpdates(device, callback) { - const attributes = []; - const commands = []; - let found; - let newAtt; - let i; - - if (device.commands) { - attributeLoop: for (i in contextElement) { - for (const j in device.commands) { - if (i === device.commands[j].name) { - newAtt = {}; - newAtt[i] = contextElement[i]; - newAtt[i].name = i; - commands.push(newAtt[i]); - found = true; - continue attributeLoop; - } - } - } - } - - for (i in contextElement) { - if (i !== 'type' && i !== 'id') { - newAtt = {}; - newAtt = contextElement[i]; - newAtt.name = i; - attributes.push(newAtt); - } - } - - callback(null, attributes, commands, device); - } - - function createActionsArray(attributes, commands, device, callback) { - const updateActions = []; - - if (!entityType) { - entityType = device.type; - } - - if (updateHandler) { - updateActions.push( - async.apply( - updateHandler, - entityId, - entityType, - req.headers['fiware-service'], - req.headers['fiware-servicepath'], - attributes - ) - ); - } - - if (commandHandler) { - if (device.polling) { - updateActions.push( - async.apply( - pushCommandsToQueue, - device, - entityId, - entityType, - req.headers['fiware-service'], - req.headers['fiware-servicepath'], - attributes - ) - ); - } else { - updateActions.push( - async.apply( - commandHandler, - entityId, - entityType, - req.headers['fiware-service'], - req.headers['fiware-servicepath'], - commands - ) - ); - } - } - - updateActions.push( - async.apply( - executeUpdateSideEffects, - device, - entityId, - entityType, - req.headers['fiware-service'], - req.headers['fiware-servicepath'], - attributes - ) - ); - - callback(null, updateActions); - } - - deviceService.getDeviceByName(entityId, req.headers['fiware-service'], req.headers['fiware-servicepath'], function ( - error, - deviceObj - ) { - if (error) { - callback(error); - } else { - async.waterfall( - [ - apply(deviceService.findConfigurationGroup, deviceObj), - apply( - deviceService.mergeDeviceWithConfiguration, - ['lazy', 'internalAttributes', 'active', 'staticAttributes', 'commands', 'subscriptions'], - [null, null, [], [], [], [], []], - deviceObj - ), - splitUpdates, - createActionsArray - ], - callback - ); - } - }); -} - -/** - * Express middleware to manage incoming update context requests using NGSIv2. - */ -function handleUpdateNgsi2(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.'); - - const errorNotFound = new Error({ - message: 'Update handler not found' - }); - next(errorNotFound); - } -} - -/** - * Express middleware to manage incoming UpdateContext requests using NGSIv1. - * As NGSI10 requests can affect multiple entities, for each one of them a call - * to the user update handler function is made. - */ -function handleUpdateNgsi1(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.contextElements, apply(generateUpdateActionsNgsi1, 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(200).json(createUpdateResponse(req, res, result)); - } - } - ); - } else { - logger.error(context, 'Tried to handle an update request before the update handler was stablished.'); - - const errorNotFound = new Error({ - message: 'Update handler not found' - }); - next(errorNotFound); - } -} - -/** - * Handle queries coming to the IoT Agent via de Context Provider API (as a consequence of a query to a passive - * attribute redirected by the Context Broker). - * - * @param {String} id Entity name of the selected entity in the query. - * @param {String} type Type of the entity. - * @param {String} service Service the device belongs to. - * @param {String} subservice Division inside the service. - * @param {Array} attributes List of attributes to read. - */ -function defaultQueryHandlerNgsi1(id, type, service, subservice, attributes, callback) { - const contextElement = { - type, - isPattern: false, - id, - attributes: [] - }; - - deviceService.getDeviceByName(id, service, subservice, function (error, ngsiDevice) { - if (error) { - callback(error); - } else { - for (let i = 0; i < attributes.length; i++) { - const lazyAttribute = _.findWhere(ngsiDevice.lazy, { name: attributes[i] }); - const command = _.findWhere(ngsiDevice.commands, { name: attributes[i] }); - let attributeType; - - if (command) { - attributeType = command.type; - } else if (lazyAttribute) { - attributeType = lazyAttribute.type; - } else { - attributeType = 'string'; - } - - contextElement.attributes.push({ - name: attributes[i], - type: attributeType, - value: '' - }); - } - - callback(null, contextElement); - } - }); -} - -/** - * Handle queries coming to the IoT Agent via de Context Provider API (as a consequence of a query to a passive - * attribute redirected by the Context Broker). - * - * @param {String} id Entity name of the selected entity in the query. - * @param {String} type Type of the entity. - * @param {String} service Service the device belongs to. - * @param {String} subservice Division inside the service. - * @param {Array} attributes List of attributes to read. - */ -function defaultQueryHandlerNgsi2(id, type, service, subservice, attributes, callback) { - const contextElement = { - type, - id - }; - - deviceService.getDeviceByName(id, service, subservice, function (error, ngsiDevice) { - if (error) { - callback(error); - } else { - for (let i = 0; i < attributes.length; i++) { - const lazyAttribute = _.findWhere(ngsiDevice.lazy, { name: attributes[i] }); - const command = _.findWhere(ngsiDevice.commands, { name: attributes[i] }); - let attributeType; - - if (command) { - attributeType = command.type; - } else if (lazyAttribute) { - attributeType = lazyAttribute.type; - } else { - attributeType = 'string'; - } - - contextElement[attributes[i]] = { - type: attributeType, - value: '' - }; - } - - callback(null, contextElement); - } - }); -} - -/** - * Express middleware to manage incoming QueryContext requests using NGSIv1. - * As NGSI10 requests can affect multiple entities, for each one of them a call - * to the user query handler function is made. - */ -function handleQueryNgsi1(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) { - const selectedAttributes = device.staticAttributes.filter(inAttributes); - - if (selectedAttributes.length > 0) { - if (contextElement.attributes) { - contextElement.attributes = contextElement.attributes.concat(selectedAttributes); - } else { - contextElement.attributes = selectedAttributes; - } - } - } - - 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); - callback(null, device.lazy.map(getName)); - } else { - logger.debug(context, "Couldn't find any attributes. Handling with null reference"); - - callback(null, null); - } - } - - function createQueryRequests(attributes, contextEntity, callback) { - let actualHandler; +const contextServerUtils = require('./contextServerUtils'); - if (queryHandler) { - actualHandler = queryHandler; - } else { - actualHandler = defaultQueryHandlerNgsi1; - } - - async.waterfall( - [ - apply( - deviceService.getDeviceByName, - contextEntity.id, - req.headers['fiware-service'], - req.headers['fiware-servicepath'] - ), - deviceService.findConfigurationGroup - ], - function handleFindDevice(error, device) { - const executeCompleteAttributes = apply(completeAttributes, attributes, device); - const executeQueryHandler = apply( - actualHandler, - contextEntity.id, - contextEntity.type, - req.headers['fiware-service'], - req.headers['fiware-servicepath'] - ); - const executeAddStaticAttributes = apply(addStaticAttributes, attributes, device); - - callback( - error, - apply(async.waterfall, [executeCompleteAttributes, executeQueryHandler, executeAddStaticAttributes]) - ); - } - ); - } - - 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(createQueryResponse(req, res, result)); - } - } - - logger.debug(context, 'Handling query from [%s]', req.get('host')); - - async.waterfall( - [apply(async.map, req.body.entities, apply(createQueryRequests, req.body.attributes)), async.series], - handleQueryContextRequests - ); -} +let contextServerHandler; /** - * Express middleware to manage incoming query context requests using NGSIv2. + * Loads the correct context server handler based on the current config. */ -function handleQueryNgsi2(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) { - let selectedAttributes = []; - if (attributes === undefined || attributes.length === 0) { - selectedAttributes = device.staticAttributes; - } else { - selectedAttributes = device.staticAttributes.filter(inAttributes); - } - - for (const 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); - const 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) { - let contextId = contextEntity.id; - let contextType = contextEntity.type; - if (!contextId) { - contextId = device.id; - } - - if (!contextType) { - contextType = device.type; - } - - deviceService.findConfigurationGroup(device, function (error, group) { - const executeCompleteAttributes = apply(completeAttributes, attributes, group); - const executeQueryHandler = apply( - actualHandler, - contextId, - contextType, - req.headers['fiware-service'], - req.headers['fiware-servicepath'] - ); - const executeAddStaticAttributes = apply(addStaticAttributes, attributes, group); - - async.waterfall([executeCompleteAttributes, executeQueryHandler, executeAddStaticAttributes], callback); - }); - } - - function createQueryRequest(attributes, contextEntity, callback) { - let actualHandler; - let 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, []); - } - 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')); - const 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; - const queryAtts = req.body.attrs; - createQueryRequest(queryAtts, contextEntity, handleQueryContextRequests); -} - -function handleNotificationNgsi1(req, res, next) { - function checkStatus(statusCode, callback) { - if (statusCode.code && statusCode.code === '200') { - callback(); - } else { - callback(new errors.NotificationError(statusCode.code)); - } - } - - function extractInformation(contextResponse, callback) { - deviceService.getDeviceByName( - contextResponse.contextElement.id, - req.headers['fiware-service'], - req.headers['fiware-servicepath'], - function (error, device) { - if (error) { - callback(error); - } else { - callback(null, device, contextResponse.contextElement.attributes); - } - } - ); - } - - function applyNotificationMiddlewares(device, values, callback) { - if (notificationMiddlewares.length > 0) { - const firstMiddleware = notificationMiddlewares.slice(0, 1)[0]; - const rest = notificationMiddlewares.slice(1); - const startMiddleware = apply(firstMiddleware, device, values); - const composedMiddlewares = [startMiddleware].concat(rest); - - async.waterfall(composedMiddlewares, callback); - } else { - callback(null, device, values); - } - } - - function createNotificationHandler(contextResponse, callback) { - async.waterfall( - [ - apply(checkStatus, contextResponse.statusCode), - 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.contextResponses, createNotificationHandler, handleNotificationRequests); - } else { - const errorNotFound = new Error({ - message: 'Notification handler not found' - }); - - logger.error(context, 'Tried to handle a notification before notification handler was established.'); - - next(errorNotFound); - } -} - -function handleNotificationNgsi2(req, res, next) { - function extractInformation(dataElement, callback) { - const atts = []; - for (const key in dataElement) { - if (dataElement.hasOwnProperty(key)) { - if (key !== 'id' && key !== 'type') { - const 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) { - const firstMiddleware = notificationMiddlewares.slice(0, 1)[0]; - const rest = notificationMiddlewares.slice(1); - const startMiddleware = apply(firstMiddleware, device, values); - const 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); +function init() { + if (config.checkNgsiLD()) { + contextServerHandler = require('./contextServer-NGSI-LD'); + } else if (config.checkNgsi2()) { + contextServerHandler = require('./contextServer-NGSI-v2'); } else { - const errorNotFound = new Error({ - message: 'Notification handler not found' - }); - - logger.error(context, 'Tried to handle a notification before notification handler was established.'); - - next(errorNotFound); + contextServerHandler = require('./contextServer-NGSI-v1'); } } @@ -1026,7 +57,7 @@ function handleNotificationNgsi2(req, res, next) { * @param {Function} newHandler User handler for update requests */ function setUpdateHandler(newHandler) { - updateHandler = newHandler; + contextServerUtils.updateHandler = newHandler; } /** @@ -1040,7 +71,7 @@ function setUpdateHandler(newHandler) { * @param {Function} newHandler User handler for update requests */ function setCommandHandler(newHandler) { - commandHandler = newHandler; + contextServerUtils.commandHandler = newHandler; } /** @@ -1054,7 +85,7 @@ function setCommandHandler(newHandler) { * @param {Function} newHandler User handler for query requests */ function setQueryHandler(newHandler) { - queryHandler = newHandler; + contextServerUtils.queryHandler = newHandler; } /** @@ -1068,78 +99,7 @@ function setQueryHandler(newHandler) { * */ function setNotificationHandler(newHandler) { - notificationHandler = newHandler; -} - -function queryErrorHandlingNgsi1(error, req, res, next) { - let code = 500; - - logger.debug(context, 'Query NGSIv1 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({ - errorCode: { - code, - reasonPhrase: error.name, - details: error.message.replace(/[<>\"\'=;\(\)]/g, '') - } - }); -} - -function queryErrorHandlingNgsi2(error, req, res, next) { - let code = 500; - - logger.debug(context, 'Query NGSIv2 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, '') - }); -} - -function updateErrorHandlingNgsi1(error, req, res, next) { - let code = 500; - - logger.debug(context, 'Update NGSIv1 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({ - contextResponses: [ - { - contextElement: req.body, - statusCode: { - code, - reasonPhrase: error.name, - details: error.message.replace(/[<>\"\'=;\(\)]/g, '') - } - } - ] - }); -} - -function updateErrorHandlingNgsi2(error, req, res, next) { - let code = 500; - - logger.debug(context, 'Update NGSIv2 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, '') - }); + contextServerUtils.notificationHandler = newHandler; } /** @@ -1148,73 +108,26 @@ function updateErrorHandlingNgsi2(error, req, res, next) { * @param {Object} router Express request router object. */ function loadContextRoutes(router) { - //TODO: remove '//' paths when the appropriate patch comes to Orion - const updateMiddlewaresNgsi1 = [ - middlewares.ensureType, - middlewares.validateJson(updateContextTemplateNgsi1), - handleUpdateNgsi1, - updateErrorHandlingNgsi1 - ]; - const updateMiddlewaresNgsi2 = [ - middlewares.ensureType, - middlewares.validateJson(updateContextTemplateNgsi2), - handleUpdateNgsi2, - updateErrorHandlingNgsi2 - ]; - const queryMiddlewaresNgsi1 = [ - middlewares.ensureType, - middlewares.validateJson(queryContextTemplate), - handleQueryNgsi1, - queryErrorHandlingNgsi1 - ]; - const queryMiddlewaresNgsi2 = [handleQueryNgsi2, queryErrorHandlingNgsi2]; - const updatePathsNgsi1 = ['/v1/updateContext', '/NGSI10/updateContext', '//updateContext']; - const updatePathsNgsi2 = ['/v2/op/update', '//op/update']; - const queryPathsNgsi1 = ['/v1/queryContext', '/NGSI10/queryContext', '//queryContext']; - const queryPathsNgsi2 = ['/v2/op/query', '//op/query']; - // 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'); - let i; - if (config.checkNgsi2()) { - for (i = 0; i < updatePathsNgsi2.length; i++) { - router.post(updatePathsNgsi2[i], updateMiddlewaresNgsi2); - } - for (i = 0; i < queryPathsNgsi2.length; i++) { - router.post(queryPathsNgsi2[i], queryMiddlewaresNgsi2); - } - router.post('/notify', [ - middlewares.ensureType, - middlewares.validateJson(notificationTemplateNgsi2), - handleNotificationNgsi2, - queryErrorHandlingNgsi2 - ]); - } else { - for (i = 0; i < updatePathsNgsi1.length; i++) { - router.post(updatePathsNgsi1[i], updateMiddlewaresNgsi1); - } - for (i = 0; i < queryPathsNgsi1.length; i++) { - router.post(queryPathsNgsi1[i], queryMiddlewaresNgsi1); - } - router.post('/notify', [ - middlewares.ensureType, - middlewares.validateJson(notificationTemplateNgsi1), - handleNotificationNgsi1, - queryErrorHandlingNgsi1 - ]); - } + contextServerHandler.loadContextRoutes(router); } +/** Adds a new Express middleware to the notifications stack + * + * @param {Object} newMiddleware The middleware to be added + */ function addNotificationMiddleware(newMiddleware) { - notificationMiddlewares.push(newMiddleware); + contextServerUtils.notificationMiddlewares.push(newMiddleware); } +/** Cleans up - removes all middlewares + * + * @param {Function} callback Optional callback to return when complete. + */ function clear(callback) { - notificationMiddlewares = []; - notificationHandler = null; - commandHandler = null; - updateHandler = null; + contextServerUtils.notificationMiddlewares = []; + contextServerUtils.notificationHandler = null; + contextServerUtils.commandHandler = null; + contextServerUtils.updateHandler = null; if (callback) { callback(); @@ -1228,3 +141,4 @@ exports.setCommandHandler = intoTrans(context, setCommandHandler); exports.setNotificationHandler = intoTrans(context, setNotificationHandler); exports.addNotificationMiddleware = intoTrans(context, addNotificationMiddleware); exports.setQueryHandler = intoTrans(context, setQueryHandler); +exports.init = init; diff --git a/lib/services/northBound/contextServerUtils.js b/lib/services/northBound/contextServerUtils.js new file mode 100644 index 000000000..04d0d33d4 --- /dev/null +++ b/lib/services/northBound/contextServerUtils.js @@ -0,0 +1,207 @@ +/* + * 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: Daniel Calvo - ATOS Research & Innovation + * Modified by: Jason Fox - FIWARE Foundation + */ + +const async = require('async'); +const apply = async.apply; +const logger = require('logops'); +const constants = require('../../constants'); +const config = require('../../commonConfig'); +const ngsi = require('../ngsi/ngsiService'); +const commands = require('../commands/commandService'); +const context = { + op: 'IoTAgentNGSI.ContextServerUtils' +}; + +/** + * Returns the Current Tenant defined for the NGSI-LD Broker. Tenant is based on the request + * Headers - default to using the new NGSILD-Tenant header, fallback to the v2 fiware-service header + * and finally see if the config holds a defined tenant. Not all brokers are currently + * obliged to offer service headers - this is still being defined in the NGSI-LD specifications. + * + * @param {Object} req Request that was handled in first place. + * @return {String} The Tenant decribed in the request headers + */ +function getLDTenant(req) { + if (req.headers['NGSILD-Tenant']) { + return req.headers['NGSILD-Tenant']; + } else if (req.headers['fiware-service']) { + return req.headers['fiware-service']; + } + return config.getConfig().contextBroker.fallbackTenant; +} + +/** + * Returns the Current Path defined for the NGSI-LD Broker. Tenant is based on the request + * Headers - default to using the new NGSILD Path header, fallback to the v2 fiware-servicepath header + * see if the config holds a defined servicepath and finally try slashs. Not all brokers are currently + * obliged to offer service headers - this is still being defined in the NGSI-LD specifications. + */ +function getLDPath(req) { + if (req.headers['NGSILD-Path']) { + return req.headers['NGSILD-Path']; + } else if (req.headers['fiware-servicepath']) { + return req.headers['fiware-servicepath']; + } + return config.getConfig().contextBroker.fallbackPath; +} + +/** + * Create the response for an UpdateContext operation, based on the results of the individual updates. The signature + * retains the results object for homogeinity with the createQuery* version. + * + * @param {Object} req Request that was handled in first place. + * @param {Object} res Response that will be sent. + * @param {Object} results Ignored for this function. TODO: to be removed in later versions. + * @return {{contextResponses: Array}} + */ + +/* eslint-disable-next-line no-unused-vars */ +function createUpdateResponse(req, res, results) { + const result = { + contextResponses: [] + }; + + for (let i = 0; i < req.body.contextElements.length; i++) { + const contextResponse = { + contextElement: { + attributes: req.body.contextElements[i].attributes, + id: req.body.contextElements[i].id, + isPattern: false, + type: req.body.contextElements[i].type + }, + statusCode: { + code: 200, + reasonPhrase: 'OK' + } + }; + + for (let j = 0; j < contextResponse.contextElement.attributes.length; j++) { + contextResponse.contextElement.attributes[i].value = ''; + } + + result.contextResponses.push(contextResponse); + } + + logger.debug(context, 'Generated update response: %j', result); + + return result; +} + +/** + * Create the response for a queryContext operation based on the individual results gathered from the query handlers. + * The returned response is in the NGSI Response format. + * + * @param {Object} req Request that was handled in first place. + * @param {Object} res Response that will be sent. + * @param {Object} results Individual Context Element results from the query handlers. + * @return {{contextResponses: Array}} + */ +function createQueryResponse(req, res, results) { + const result = { + contextResponses: [] + }; + + for (let i = 0; i < results.length; i++) { + const contextResponse = { + contextElement: results[i], + statusCode: { + code: 200, + reasonPhrase: 'OK' + } + }; + + contextResponse.contextElement.isPattern = false; + + result.contextResponses.push(contextResponse); + } + + logger.debug(context, 'Generated query response: %j', result); + + return result; +} + +/** + * Retrieve the Device that corresponds to a Context Update, and execute the update side effects + * if there were any (e.g.: creation of attributes related to comands). + * + * @param {String} device Object that contains all the information about the device. + * @param {String} id Entity ID of the device to find. + * @param {String} type Type of the device to find. + * @param {String} service Service of the device. + * @param {String} subservice Subservice of the device. + * @param {Array} attributes List of attributes to update with their types and values. + */ +function executeUpdateSideEffects(device, id, type, service, subservice, attributes, callback) { + const sideEffects = []; + + if (device.commands) { + for (let i = 0; i < device.commands.length; i++) { + for (let j = 0; j < attributes.length; j++) { + if (device.commands[i].name === attributes[j].name) { + const newAttributes = [ + { + name: device.commands[i].name + '_status', + type: constants.COMMAND_STATUS, + value: 'PENDING' + } + ]; + + sideEffects.push( + apply(ngsi.update, device.name, device.resource, device.apikey, newAttributes, device) + ); + } + } + } + } + + async.series(sideEffects, callback); +} + +/** + * Extract all the commands from the attributes section and add them to the Commands Queue. + * + * @param {String} device Object that contains all the information about the device. + * @param {String} id Entity ID of the device to find. + * @param {String} type Type of the device to find. + * @param {String} service Service of the device. + * @param {String} subservice Subservice of the device. + * @param {Array} attributes List of attributes to update with their types and values. + */ +function pushCommandsToQueue(device, id, type, service, subservice, attributes, callback) { + async.map(attributes, apply(commands.add, service, subservice, device.id), callback); +} + +exports.notificationMiddlewares = []; +exports.updateHandler = null; +exports.commandHandler = null; +exports.queryHandler = null; +exports.notificationHandler = null; +exports.createUpdateResponse = createUpdateResponse; +exports.createQueryResponse = createQueryResponse; +exports.executeUpdateSideEffects = executeUpdateSideEffects; +exports.pushCommandsToQueue = pushCommandsToQueue; +exports.getLDTenant = getLDTenant; +exports.getLDPath = getLDPath; diff --git a/lib/services/northBound/northboundServer.js b/lib/services/northBound/northboundServer.js index e202e948d..0b17e4e5a 100644 --- a/lib/services/northBound/northboundServer.js +++ b/lib/services/northBound/northboundServer.js @@ -39,9 +39,10 @@ const context = { const bodyParser = require('body-parser'); function start(config, callback) { - deviceProvisioning.setConfiguration(config); let baseRoot = '/'; + deviceProvisioning.setConfiguration(config); + northboundServer = { server: null, app: express(), @@ -55,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); @@ -121,3 +123,4 @@ exports.addNotificationMiddleware = contextServer.addNotificationMiddleware; exports.clear = clear; exports.start = intoTrans(context, start); exports.stop = intoTrans(context, stop); +exports.init = contextServer.init; diff --git a/lib/services/northBound/restUtils.js b/lib/services/northBound/restUtils.js index 4ccca70f0..1ef5600b4 100644 --- a/lib/services/northBound/restUtils.js +++ b/lib/services/northBound/restUtils.js @@ -35,6 +35,11 @@ const context = { op: 'IoTAgentNGSI.RestUtils' }; const _ = require('underscore'); +const request = require('request'); +const async = require('async'); +const apply = async.apply; +const ngsiService = require('../ngsi/ngsiService'); +const config = require('../../commonConfig'); /** * Checks all the mandatory attributes in the selected array are present in the presented body object. @@ -187,7 +192,7 @@ function isTimestamped(payload) { /** * Checks if timestamp attributes are included in NGSIv2 entities. * - * @param {Object} payload NGSIv1 payload to be analyzed. + * @param {Object} payload NGSIv2 payload to be analyzed. * @return {Boolean} true if timestamp attributes are included. false if not. */ function isTimestampedNgsi2(payload) { @@ -215,6 +220,56 @@ function isTimestampedNgsi2(payload) { return isTimestampedNgsi2Entity(payload); } +/** + * Executes a request operation using security information if available + * + * @param {String} requestOptions Request options to be sent. + * @param {String} deviceData Device data. + */ +function executeWithSecurity(requestOptions, deviceData, callback) { + logger.debug(context, 'executeWithSecurity'); + config.getGroupRegistry().getType(deviceData.type, function (error, deviceGroup) { + let typeInformation; + if (error) { + logger.debug(context, 'error %j in get group device', error); + } + + if (deviceGroup) { + typeInformation = deviceGroup; + } else { + typeInformation = config.getConfig().types[deviceData.type]; + } + + if (config.getConfig().authentication && config.getConfig().authentication.enabled) { + const security = config.getSecurityService(); + if (typeInformation && typeInformation.trust) { + async.waterfall( + [ + apply(security.auth, typeInformation.trust), + apply(ngsiService.updateTrust, deviceGroup, null, typeInformation.trust), + apply(security.getToken, typeInformation.trust) + ], + function (error, token) { + if (error) { + callback(new errors.SecurityInformationMissing(typeInformation.type)); + } else { + requestOptions.headers[config.getConfig().authentication.header] = token; + request(requestOptions, callback); + } + } + ); + } else { + callback( + new errors.SecurityInformationMissing(typeInformation ? typeInformation.type : deviceData.type) + ); + } + } else { + request(requestOptions, callback); + } + }); +} + +exports.executeWithSecurity = executeWithSecurity; exports.checkMandatoryQueryParams = intoTrans(context, checkMandatoryQueryParams); exports.checkRequestAttributes = intoTrans(context, checkRequestAttributes); exports.checkBody = intoTrans(context, checkBody); 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 new file mode 100644 index 000000000..cd425bbe4 --- /dev/null +++ b/test/.jshintrc @@ -0,0 +1,35 @@ +{ + "curly": true, + "eqeqeq": true, + "immed": true, + "latedef": true, + "newcap": true, + "noarg": true, + "noempty": true, + "quotmark": "single", + "undef": true, + "unused": true, + "trailing": true, + "maxparams": 6, + "maxdepth": 4, + "camelcase": true, + "maxlen": 120, + "node": true, + "expr": true, + "unused": "vars", + "esversion": 6, + "globals": { + "describe":true, + "it": true, + "expect": true, + "before": true, + "after": true, + "beforeEach": true, + "afterEach": true, + "mock": true + }, + "predef": + [ + "describe", "beforeEach", "afterEach", "it", "xdescribe", "xit" + ] +} \ No newline at end of file diff --git a/test/tools/utils.js b/test/tools/utils.js index 2ec507dfe..33614b463 100644 --- a/test/tools/utils.js +++ b/test/tools/utils.js @@ -31,6 +31,7 @@ function readExampleFile(name, raw) { /* eslint-disable no-console */ console.error(JSON.stringify(e)); } + return raw ? text : JSON.parse(text); } diff --git a/test/unit/lazyAndCommands/lazy-devices-test.js b/test/unit/lazyAndCommands/lazy-devices-test.js index 50f24cba4..606776f23 100644 --- a/test/unit/lazyAndCommands/lazy-devices-test.js +++ b/test/unit/lazyAndCommands/lazy-devices-test.js @@ -718,7 +718,9 @@ describe('NGSI-v1 - IoT Agent Lazy Devices', function () { const parsedBody = JSON.parse(body); should.exist(parsedBody.errorCode); parsedBody.errorCode.code.should.equal(400); - parsedBody.errorCode.details.should.equal('Unsuported content type in the context request: text/plain'); + parsedBody.errorCode.details.should.equal( + 'Unsupported content type in the context request: text/plain' + ); parsedBody.errorCode.reasonPhrase.should.equal('UNSUPPORTED_CONTENT_TYPE'); done(); 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..7999397dd --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerIoTAgent1.json @@ -0,0 +1,18 @@ +{ + "@context": "http://context.json-ld", + "endpoint": "http://smartGondor.com", + "information": [ + { + "entities": [ + { + "id": "urn:ngsi-ld: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..b7709fe07 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerIoTAgent2.json @@ -0,0 +1,18 @@ +{ + "type": "ContextSourceRegistration", + "information": [ + { + "entities": [ + { + "type": "Motion", + "id": "urn:ngsi-ld:Motion:motion1" + } + ], + "properties": [ + "moving" + ] + } + ], + "endpoint": "http://smartGondor.com", + "@context": "http://context.json-ld" +} \ No newline at end of file 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..4e7ce815c --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerIoTAgent4.json @@ -0,0 +1,18 @@ +{ + "type": "ContextSourceRegistration", + "information": [ + { + "entities": [ + { + "type": "RobotPre", + "id": "urn:ngsi-ld:RobotPre:TestRobotPre" + } + ], + "properties": [ + "moving" + ] + } + ], + "endpoint": "http://smartGondor.com", + "@context": "http://context.json-ld" +} \ No newline at end of file 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..ea38afcc8 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerIoTAgentCommands.json @@ -0,0 +1,18 @@ +{ + "@context": "http://context.json-ld", + "endpoint": "http://smartGondor.com", + "information": [ + { + "entities": [ + { + "id": "urn:ngsi-ld: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..3da2eeb1c --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerProvisionedDevice.json @@ -0,0 +1,19 @@ +{ + "@context": "http://context.json-ld", + "endpoint": "http://smartGondor.com", + "information": [ + { + "entities": [ + { + "id": "urn:ngsi-ld:TheLightType: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..560a277bf --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerProvisionedDevice2.json @@ -0,0 +1,18 @@ +{ + "@context": "http://context.json-ld", + "endpoint": "http://smartGondor.com", + "information": [ + { + "entities": [ + { + "id": "urn:ngsi-ld:TheLightType:TheSecondLight", + "type": "TheLightType" + } + ], + "properties": [ + "luminance" + ] + } + ], + "type": "ContextSourceRegistration" +} 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..a0498a348 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerProvisionedDeviceWithGroup.json @@ -0,0 +1,21 @@ +{ + "@context": "http://context.json-ld", + "endpoint": "http://smartGondor.com", + "information": [ + { + "entities": [ + { + "id": "urn:ngsi-ld:TheLightType: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..93bc42c11 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerProvisionedDeviceWithGroup2.json @@ -0,0 +1,21 @@ +{ + "@context": "http://context.json-ld", + "endpoint": "http://smartGondor.com", + "information": [ + { + "entities": [ + { + "id": "urn:ngsi-ld:SensorMachine: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..7999397dd --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextAvailabilityRequests/registerProvisionedDeviceWithGroup3.json @@ -0,0 +1,18 @@ +{ + "@context": "http://context.json-ld", + "endpoint": "http://smartGondor.com", + "information": [ + { + "entities": [ + { + "id": "urn:ngsi-ld:Light: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..ef7e60fd5 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextAvailabilityRequests/updateCommands1.json @@ -0,0 +1,18 @@ +{ + "type":"ContextSourceRegistration", + "information":[ + { + "entities":[ + { + "type":"Light", + "id":"urn:ngsi-ld:Light:light1" + } + ], + "properties":[ + "move" + ] + } + ], + "endpoint":"http://smartGondor.com", + "@context": "http://context.json-ld" +} \ No newline at end of file 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..4c582b5d5 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextAvailabilityRequests/updateIoTAgent1.json @@ -0,0 +1,18 @@ +{ + "@context": "http://context.json-ld", + "endpoint": "http://smartGondor.com", + "information": [ + { + "entities": [ + { + "id": "urn:ngsi-ld:Light:light1", + "type": "Light" + } + ], + "properties": [ + "pressure" + ] + } + ], + "type": "ContextSourceRegistration" +} 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..fd00528d0 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextAvailabilityRequests/updateIoTAgent2.json @@ -0,0 +1,19 @@ +{ + "@context": "http://context.json-ld", + "endpoint": "http://smartGondor.com", + "information": [ + { + "entities": [ + { + "id": "urn:ngsi-ld:TheLightType:ANewLightName", + "type": "TheLightType" + } + ], + "properties": [ + "luminance", + "commandAttr" + ] + } + ], + "type": "ContextSourceRegistration" +} 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..b32135616 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextAvailabilityRequests/updateIoTAgent3.json @@ -0,0 +1,19 @@ +{ + "@context": "http://context.json-ld", + "dataProvided": { + "attrs": [ + "luminance" + ], + "entities": [ + { + "id": "urn:ngsi-ld:TheLightType: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..433a0018f --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextProviderResponses/queryInformationResponse.json @@ -0,0 +1,8 @@ +{ + "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..0392feba4 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextProviderResponses/queryInformationResponseEmptyAttributes.json @@ -0,0 +1,8 @@ +{ + "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..6cc106571 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextProviderResponses/queryInformationStaticAttributesResponse.json @@ -0,0 +1,12 @@ +{ + "id": "Motion:motion1", + "location": { + "type": "Vector", + "value": "(123,523)" + }, + "moving": { + "type": "Boolean", + "value": true + }, + "type": "Motion" +} \ No newline at end of file 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..3def79e9d --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/createAutoprovisionDevice.json @@ -0,0 +1,7 @@ +[ + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:sensor: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..f32c1f5e7 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/createBidirectionalDevice.json @@ -0,0 +1,17 @@ +[ + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:TheLightType:TheFirstLight", + "location": { + "type": "GeoProperty", + "value": { + "coordinates": [ + 0, + 0 + ], + "type": "Point" + } + }, + "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..911249cab --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/createDatetimeProvisionedDevice.json @@ -0,0 +1,14 @@ +[ + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:MicroLights: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..f06bc4831 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/createGeopointProvisionedDevice.json @@ -0,0 +1,17 @@ +[ + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:MicroLights:FirstMicroLight", + "location": { + "type": "GeoProperty", + "value": { + "coordinates": [ + 0, + 0 + ], + "type": "Point" + } + }, + "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..376c3f0c7 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/createMinimumProvisionedDevice.json @@ -0,0 +1,11 @@ +[ + { + "@context": "http://context.json-ld", + "attr_name": { + "type": "Property", + "value": " " + }, + "id": "urn:ngsi-ld:MicroLights: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..8f6dab919 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/createProvisionedDevice.json @@ -0,0 +1,32 @@ +[ + { + "@context": "http://context.json-ld", + "attr_name": { + "type": "Property", + "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": "urn:ngsi-ld:TheLightType: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..8f6dab919 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/createProvisionedDeviceMultientity.json @@ -0,0 +1,32 @@ +[ + { + "@context": "http://context.json-ld", + "attr_name": { + "type": "Property", + "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": "urn:ngsi-ld:TheLightType: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..dc380d561 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/createProvisionedDeviceWithGroupAndStatic.json @@ -0,0 +1,57 @@ +[ + { + "@context": "http://context.json-ld", + "attr_name": { + "type": "Property", + "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": "urn:ngsi-ld:TheLightType: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..9ed0cf6ea --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/createProvisionedDeviceWithGroupAndStatic2.json @@ -0,0 +1,57 @@ +[ + { + "@context": "http://context.json-ld", + "attr_name": { + "type": "Property", + "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": "urn:ngsi-ld:SensorMachine: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..8e73199c8 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/createProvisionedDeviceWithGroupAndStatic3.json @@ -0,0 +1,18 @@ +[ + { + "@context": "http://context.json-ld", + "dimming": { + "type": "Property", + "value": { + "@type": "Percentage", + "@value": " " + } + }, + "id": "urn:ngsi-ld:Light: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..376c3f0c7 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/createTimeInstantMinimumDevice.json @@ -0,0 +1,11 @@ +[ + { + "@context": "http://context.json-ld", + "attr_name": { + "type": "Property", + "value": " " + }, + "id": "urn:ngsi-ld:MicroLights:FirstMicroLight", + "type": "MicroLights" + } +] 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..62df2066a --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/createTimeinstantDevice.json @@ -0,0 +1,7 @@ +[ + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:sensor: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..8e9798711 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContext.json @@ -0,0 +1,15 @@ +[ + { + "@context": "http://context.json-ld", + "dimming": { + "type": "Property", + "value": 87 + }, + "id": "urn:ngsi-ld:Light:light1", + "state": { + "type": "Property", + "value": true + }, + "type": "Light" + } +] 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..c0f1143b8 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContext1.json @@ -0,0 +1,18 @@ +[ + { + "@context": "http://context.json-ld", + "dimming": { + "type": "Property", + "value": { + "@type": "Percentage", + "@value": "87" + } + }, + "id": "urn:ngsi-ld:Light:light1", + "state": { + "type": "Property", + "value": true + }, + "type": "Light" + } +] diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContext2.json b/test/unit/ngsi-ld/examples/contextRequests/updateContext2.json new file mode 100644 index 000000000..7a721e9f6 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContext2.json @@ -0,0 +1,15 @@ +[ + { + "@context": "http://context.json-ld", + "dimming": { + "type": "Property", + "value": 87 + }, + "id": "urn:ngsi-ld:Humidity:humSensor", + "state": { + "type": "Property", + "value": true + }, + "type": "Humidity" + } +] 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..e79a2174c --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContext3WithStatic.json @@ -0,0 +1,18 @@ +[ + { + "@context": "http://context.json-ld", + "bootstrapServer": { + "type": "Property", + "value": { + "@type": "Address", + "@value": "127.0.0.1" + } + }, + "id": "urn:ngsi-ld:SensorMachine:machine1", + "status": { + "type": "Property", + "value": "STARTING" + }, + "type": "SensorMachine" + } +] 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..f159dab26 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContext4.json @@ -0,0 +1,18 @@ +[ + { + "@context": "http://context.json-ld", + "bootstrapServer": { + "type": "Property", + "value": { + "@type": "Address", + "@value": "127.0.0.1" + } + }, + "id": "urn:ngsi-ld:Light:light1", + "status": { + "type": "Property", + "value": "STARTING" + }, + "type": "Light" + } +] diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContext5.json b/test/unit/ngsi-ld/examples/contextRequests/updateContext5.json new file mode 100644 index 000000000..367b0f9c5 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContext5.json @@ -0,0 +1,18 @@ +[ + { + "@context": "http://context.json-ld", + "bootstrapServer": { + "type": "Property", + "value": { + "@type": "Address", + "@value": "127.0.0.1" + } + }, + "id": "urn:ngsi-ld:SensorMachine:Light1", + "status": { + "type": "Property", + "value": "STARTING" + }, + "type": "SensorMachine" + } +] 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..b0f3045ed --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextAliasPlugin1.json @@ -0,0 +1,17 @@ +[ + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:Light:light1", + "pressure": { + "type": "Property", + "unitCode": "Hgmm", + "value": 20071103 + }, + "temperature": { + "type": "Property", + "unitCode": "CEL", + "value": 52 + }, + "type": "Light" + } +] 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..8877f99c3 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextAliasPlugin2.json @@ -0,0 +1,12 @@ +[ + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:Light:light1", + "luminance": { + "type": "Property", + "unitCode": "CAL", + "value": 9 + }, + "type": "Light" + } +] 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..fcd79740f --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextAliasPlugin3.json @@ -0,0 +1,11 @@ +[ + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:Light:light1", + "type": "Light", + "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..40b8bad3e --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextAliasPlugin4.json @@ -0,0 +1,11 @@ +[ + { + "@context": "http://context.json-ld", + "active_power": { + "type": "Property", + "value": 0.45 + }, + "id": "urn:ngsi-ld:Light:light1", + "type": "Light" + } +] 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..149eb0f0c --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextAliasPlugin5.json @@ -0,0 +1,11 @@ +[ + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:Light:light1", + "status": { + "type": "Property", + "value": false + }, + "type": "Light" + } +] 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..f19e2c84a --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextAliasPlugin6.json @@ -0,0 +1,14 @@ +[ + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:Light:light1", + "keep_alive": { + "type": "Property", + "value": { + "@type": "None", + "@value": null + } + }, + "type": "Light" + } +] 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..6e2a1a8b6 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextAliasPlugin7.json @@ -0,0 +1,17 @@ +[ + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:Light:light1", + "tags": { + "type": "Property", + "value": { + "@type": "Array", + "@value": [ + "iot", + "device" + ] + } + }, + "type": "Light" + } +] 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..0fee125e9 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextAliasPlugin8.json @@ -0,0 +1,19 @@ +[ + { + "@context": "http://context.json-ld", + "configuration": { + "type": "Property", + "value": { + "@type": "Object", + "@value": { + "firmware": { + "hash": "cf23df2207d99a74fbe169e3eba035e633b65d94", + "version": "1.1.0" + } + } + } + }, + "id": "urn:ngsi-ld:Light:light1", + "type": "Light" + } +] 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..cfde2a162 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextAliasPlugin9.json @@ -0,0 +1,14 @@ +[ + { + "@context": "http://context.json-ld", + "configuration": { + "type": "Property", + "value": { + "@type": "Object", + "@value": "string_value" + } + }, + "id": "urn:ngsi-ld:Light:light1", + "type": "Light" + } +] 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..c228cfb8c --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextAutocast1.json @@ -0,0 +1,11 @@ +[ + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:Light:light1", + "pressure": { + "type": "Property", + "value": 23 + }, + "type": "Light" + } +] diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextAutocast10.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextAutocast10.json new file mode 100644 index 000000000..a5f23d455 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextAutocast10.json @@ -0,0 +1,14 @@ +[ + { + "@context": "http://context.json-ld", + "configuration": { + "type": "Property", + "value": { + "@type": "Date", + "@value": "2016-04-30" + } + }, + "id": "urn:ngsi-ld:Light:light1", + "type": "Light" + } +] 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..8fc03d094 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextAutocast2.json @@ -0,0 +1,11 @@ +[ + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:Light:light1", + "temperature": { + "type": "Property", + "value": 14.4 + }, + "type": "Light" + } +] 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..69a6ec287 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextAutocast3.json @@ -0,0 +1,11 @@ +[ + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:Light:light1", + "status": { + "type": "Property", + "value": true + }, + "type": "Light" + } +] 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..149eb0f0c --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextAutocast4.json @@ -0,0 +1,11 @@ +[ + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:Light:light1", + "status": { + "type": "Property", + "value": false + }, + "type": "Light" + } +] 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..f19e2c84a --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextAutocast5.json @@ -0,0 +1,14 @@ +[ + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:Light:light1", + "keep_alive": { + "type": "Property", + "value": { + "@type": "None", + "@value": null + } + }, + "type": "Light" + } +] 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..6e2a1a8b6 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextAutocast6.json @@ -0,0 +1,17 @@ +[ + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:Light:light1", + "tags": { + "type": "Property", + "value": { + "@type": "Array", + "@value": [ + "iot", + "device" + ] + } + }, + "type": "Light" + } +] 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..0fee125e9 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextAutocast7.json @@ -0,0 +1,19 @@ +[ + { + "@context": "http://context.json-ld", + "configuration": { + "type": "Property", + "value": { + "@type": "Object", + "@value": { + "firmware": { + "hash": "cf23df2207d99a74fbe169e3eba035e633b65d94", + "version": "1.1.0" + } + } + } + }, + "id": "urn:ngsi-ld:Light:light1", + "type": "Light" + } +] diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextAutocast8.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextAutocast8.json new file mode 100644 index 000000000..fcdb89ed3 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextAutocast8.json @@ -0,0 +1,14 @@ +[ + { + "@context": "http://context.json-ld", + "configuration": { + "type": "Property", + "value": { + "@type": "Time", + "@value": "14:59:46" + } + }, + "id": "urn:ngsi-ld:Light:light1", + "type": "Light" + } +] diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextAutocast9.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextAutocast9.json new file mode 100644 index 000000000..6090aad8a --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextAutocast9.json @@ -0,0 +1,14 @@ +[ + { + "@context": "http://context.json-ld", + "configuration": { + "type": "Property", + "value": { + "@type": "DateTime", + "@value": "2016-04-30T00:00:00.000Z" + } + }, + "id": "urn:ngsi-ld:Light:light1", + "type": "Light" + } +] 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..5504bd98e --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextCommandError.json @@ -0,0 +1,21 @@ +[ + { + "@context": "http://context.json-ld", + "position_status": { + "type": "Property", + "value": { + "@type": "commandStatus", + "@value": "ERROR" + } + }, + "position_info": { + "type": "Property", + "value": { + "@type": "commandResult", + "@value": "Stalled" + } + }, + "id": "urn:ngsi-ld:Robot:r2d2", + "type": "Robot" + } +] \ No newline at end of file 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..7dab289fb --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextCommandExpired.json @@ -0,0 +1,21 @@ +[ + { + "@context":"http://context.json-ld", + "position_status": { + "type":"Property", + "value": { + "@type":"commandStatus", + "@value":"ERROR" + } + }, + "position_info": { + "type":"Property", + "value": { + "@type":"commandResult", + "@value":"EXPIRED" + } + }, + "id":"urn:ngsi-ld:Robot:r2d2", + "type":"Robot" + } +] 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..66bef5c77 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextCommandFinish.json @@ -0,0 +1,21 @@ +[ + { + "@context":"http://context.json-ld", + "position_status": { + "type":"Property", + "value": { + "@type":"commandStatus", + "@value":"FINISHED" + } + }, + "position_info": { + "type":"Property", + "value": { + "@type":"commandResult", + "@value":"[72, 368, 1]" + } + }, + "id":"urn:ngsi-ld:Robot:r2d2", + "type":"Robot" + } +] 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..a98d26784 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextCommandStatus.json @@ -0,0 +1,14 @@ +[ + { + "@context":"http://context.json-ld", + "position_status": { + "type":"Property", + "value": { + "@type":"commandStatus", + "@value":"PENDING" + } + }, + "id":"urn:ngsi-ld:Robot:r2d2", + "type":"Robot" + } +] diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextCommandStatus1.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextCommandStatus1.json new file mode 100644 index 000000000..e74217af2 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextCommandStatus1.json @@ -0,0 +1,21 @@ +[ + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:Robot:r2d2", + "type": "Robot", + "position_status": { + "type": "Property", + "value": { + "@type": "commandStatus", + "@value": "UNKNOWN" + } + }, + "position_info": { + "type": "Property", + "value": { + "@type": "commandResult", + "@value": " " + } + } + } +] \ No newline at end of file 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..9b5cdd7f9 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextCompressTimestamp1.json @@ -0,0 +1,18 @@ +[ + { + "@context": "http://context.json-ld", + "TheTargetValue": { + "type": "Property", + "value": { + "@type": "DateTime", + "@value": "2007-11-03T13:18:05.000Z" + } + }, + "id": "urn:ngsi-ld:Light:light1", + "state": { + "type": "Property", + "value": true + }, + "type": "Light" + } +] 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..d2bd8aa82 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextCompressTimestamp2.json @@ -0,0 +1,19 @@ +[ + { + "@context": "http://context.json-ld", + "TheTargetValue": { + "type": "Property", + "value": { + "@type": "DateTime", + "@value": "2007-11-03T13:18:05.000Z" + } + }, + "id": "urn:ngsi-ld:Light:light1", + "state": { + "observedAt": "2007-11-03T13:18:05.000Z", + "type": "Property", + "value": true + }, + "type": "Light" + } +] 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..8e28f5fa4 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin1.json @@ -0,0 +1,11 @@ +[ + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:WeatherStation:ws1", + "pressure": { + "type": "Property", + "value": 1040 + }, + "type": "WeatherStation" + } +] 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..bd7d5d96b --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin10.json @@ -0,0 +1,11 @@ +[ + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:WeatherStation:ws1", + "type": "WeatherStation", + "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..bfa124014 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin11.json @@ -0,0 +1,11 @@ +[ + { + "@context": "http://context.json-ld", + "consumption": { + "type": "Property", + "value": 52 + }, + "id": "urn:ngsi-ld:Light:light1", + "type": "Light" + } +] 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..4cbf0b9e8 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin12.json @@ -0,0 +1,11 @@ +[ + { + "@context": "http://context.json-ld", + "consumption_x": { + "type": "Property", + "value": 0.44 + }, + "id": "urn:ngsi-ld:Light:light1", + "type": "Light" + } +] 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..8fa9cafe6 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin13.json @@ -0,0 +1,15 @@ +[ + { + "@context": "http://context.json-ld", + "consumption_x": { + "type": "Property", + "value": 200 + }, + "id": "urn:ngsi-ld:Light:light1", + "pressure": { + "type": "Property", + "value": 10 + }, + "type": "Light" + } +] diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin14.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin14.json new file mode 100644 index 000000000..8fa9cafe6 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin14.json @@ -0,0 +1,15 @@ +[ + { + "@context": "http://context.json-ld", + "consumption_x": { + "type": "Property", + "value": 200 + }, + "id": "urn:ngsi-ld:Light:light1", + "pressure": { + "type": "Property", + "value": 10 + }, + "type": "Light" + } +] diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin15.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin15.json new file mode 100644 index 000000000..55df926df --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin15.json @@ -0,0 +1,11 @@ +[ + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:WeatherStation:ws1", + "type": "WeatherStation", + "updated": { + "type": "Property", + "value": true + } + } +] diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin16.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin16.json new file mode 100644 index 000000000..6f1f94f7f --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin16.json @@ -0,0 +1,14 @@ +[ + { + "@context": "http://context.json-ld", + "alive": { + "type": "Property", + "value": { + "@type": "None", + "@value": null + } + }, + "id": "urn:ngsi-ld:WeatherStation:ws1", + "type": "WeatherStation" + } +] diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin17.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin17.json new file mode 100644 index 000000000..6f1f94f7f --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin17.json @@ -0,0 +1,14 @@ +[ + { + "@context": "http://context.json-ld", + "alive": { + "type": "Property", + "value": { + "@type": "None", + "@value": null + } + }, + "id": "urn:ngsi-ld:WeatherStation:ws1", + "type": "WeatherStation" + } +] diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin18.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin18.json new file mode 100644 index 000000000..7b6a69c64 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin18.json @@ -0,0 +1,11 @@ +[ + { + "@context": "http://context.json-ld", + "consumption": { + "type": "Property", + "value": 52 + }, + "id": "urn:ngsi-ld:WeatherStation:ws1", + "type": "WeatherStation" + } +] diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin19.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin19.json new file mode 100644 index 000000000..5ca5ef345 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin19.json @@ -0,0 +1,11 @@ +[ + { + "@context": "http://context.json-ld", + "consumption": { + "type": "Property", + "value": 0.44 + }, + "id": "urn:ngsi-ld:WeatherStation:ws1", + "type": "WeatherStation" + } +] 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..cb9258b14 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin2.json @@ -0,0 +1,25 @@ +[ + { + "@context": "http://context.json-ld", + "humidity": { + "type": "Property", + "value": { + "@type": "Percentage", + "@value": "12" + } + }, + "id": "urn:ngsi-ld:WeatherStation:ws1", + "pressure": { + "type": "Property", + "value": 1040 + }, + "type": "WeatherStation", + "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..cacf8bf6d --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin3.json @@ -0,0 +1,11 @@ +[ + { + "@context": "http://context.json-ld", + "consumption": { + "type": "Property", + "value": 0.44 + }, + "id": "urn:ngsi-ld:Light:light1", + "type": "Light" + } +] 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..38ea05269 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin4.json @@ -0,0 +1,25 @@ +[ + { + "@context": "http://context.json-ld", + "humidity12": { + "type": "Property", + "value": { + "@type": "Percentage", + "@value": "12" + } + }, + "id": "urn:ngsi-ld:WeatherStation:ws1", + "pressure25": { + "type": "Property", + "value": 52 + }, + "type": "WeatherStation", + "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..dd1890729 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin5.json @@ -0,0 +1,14 @@ +[ + { + "@context": "http://context.json-ld", + "alive": { + "type": "Property", + "value": { + "@type": "None", + "@value": null + } + }, + "id": "urn:ngsi-ld:Light:light1", + "type": "Light" + } +] 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..abd2b49da --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin6.json @@ -0,0 +1,17 @@ +[ + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:Light:light1", + "manufacturer": { + "type": "Property", + "value": { + "@type": "Object", + "@value": { + "VAT": "U12345678", + "name": "Manufacturer1" + } + } + }, + "type": "Light" + } +] 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..bf01b5131 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin7.json @@ -0,0 +1,18 @@ +[ + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:Light:light1", + "revisions": { + "type": "Property", + "value": { + "@type": "Array", + "@value": [ + "v0.1", + "v0.2", + "v0.3" + ] + } + }, + "type": "Light" + } +] 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..aa390e5e9 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin8.json @@ -0,0 +1,11 @@ +[ + { + "@context": "http://context.json-ld", + "consumption": { + "type": "Property", + "value": 8.8 + }, + "id": "urn:ngsi-ld:WeatherStation:ws1", + "type": "WeatherStation" + } +] 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..701ab52c8 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin9.json @@ -0,0 +1,11 @@ +[ + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:Light:light1", + "type": "Light", + "updated": { + "type": "Property", + "value": true + } + } +] diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextGeoproperties1.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextGeoproperties1.json new file mode 100644 index 000000000..099b3b87d --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextGeoproperties1.json @@ -0,0 +1,17 @@ +[ + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:Light:light1", + "location": { + "type": "GeoProperty", + "value": { + "coordinates": [ + 23, + 12.5 + ], + "type": "Point" + } + }, + "type": "Light" + } +] diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextGeoproperties2.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextGeoproperties2.json new file mode 100644 index 000000000..49a5bdc31 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextGeoproperties2.json @@ -0,0 +1,23 @@ +[ + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:Light:light1", + "location": { + "type": "GeoProperty", + "value": { + "coordinates": [ + [ + 23, + 12.5 + ], + [ + 22, + 12.5 + ] + ], + "type": "LineString" + } + }, + "type": "Light" + } +] diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextGeoproperties3.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextGeoproperties3.json new file mode 100644 index 000000000..e80c4e101 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextGeoproperties3.json @@ -0,0 +1,14 @@ +[ + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:Light:light1", + "location": { + "type": "Property", + "value": { + "@type": "None", + "@value": null + } + }, + "type": "Light" + } +] diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextGeoproperties4.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextGeoproperties4.json new file mode 100644 index 000000000..2a8cee8b5 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextGeoproperties4.json @@ -0,0 +1,27 @@ +[ + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:Light:light1", + "location": { + "type": "GeoProperty", + "value": { + "coordinates": [ + [ + 23, + 12.5 + ], + [ + 22, + 13.5 + ], + [ + 22, + 13.5 + ] + ], + "type": "Polygon" + } + }, + "type": "Light" + } +] diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextGeoproperties5.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextGeoproperties5.json new file mode 100644 index 000000000..099b3b87d --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextGeoproperties5.json @@ -0,0 +1,17 @@ +[ + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:Light:light1", + "location": { + "type": "GeoProperty", + "value": { + "coordinates": [ + 23, + 12.5 + ], + "type": "Point" + } + }, + "type": "Light" + } +] diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextGeoproperties6.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextGeoproperties6.json new file mode 100644 index 000000000..099b3b87d --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextGeoproperties6.json @@ -0,0 +1,17 @@ +[ + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:Light:light1", + "location": { + "type": "GeoProperty", + "value": { + "coordinates": [ + 23, + 12.5 + ], + "type": "Point" + } + }, + "type": "Light" + } +] diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextGeoproperties7.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextGeoproperties7.json new file mode 100644 index 000000000..099b3b87d --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextGeoproperties7.json @@ -0,0 +1,17 @@ +[ + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:Light:light1", + "location": { + "type": "GeoProperty", + "value": { + "coordinates": [ + 23, + 12.5 + ], + "type": "Point" + } + }, + "type": "Light" + } +] diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextGeoproperties8.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextGeoproperties8.json new file mode 100644 index 000000000..099b3b87d --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextGeoproperties8.json @@ -0,0 +1,17 @@ +[ + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:Light:light1", + "location": { + "type": "GeoProperty", + "value": { + "coordinates": [ + 23, + 12.5 + ], + "type": "Point" + } + }, + "type": "Light" + } +] diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextGeoproperties9.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextGeoproperties9.json new file mode 100644 index 000000000..099b3b87d --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextGeoproperties9.json @@ -0,0 +1,17 @@ +[ + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:Light:light1", + "location": { + "type": "GeoProperty", + "value": { + "coordinates": [ + 23, + 12.5 + ], + "type": "Point" + } + }, + "type": "Light" + } +] 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..2b3d4dd04 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityPlugin1.json @@ -0,0 +1,26 @@ +[ + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:WeatherStation:ws4", + "pressure": { + "type": "Property", + "value": { + "@type": "Hgmm", + "@value": "52" + } + }, + "type": "WeatherStation" + }, + { + "@context": "http://context.json-ld", + "humidity": { + "type": "Property", + "value": { + "@type": "Percentage", + "@value": "12" + } + }, + "id": "urn:ngsi-ld:Higrometer: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..e5cd1461d --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityPlugin2.json @@ -0,0 +1,26 @@ +[ + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:WeatherStation:ws4", + "pressure": { + "type": "Property", + "value": { + "@type": "Hgmm", + "@value": "52" + } + }, + "type": "WeatherStation" + }, + { + "@context": "http://context.json-ld", + "humidity": { + "type": "Property", + "value": { + "@type": "Percentage", + "@value": "12" + } + }, + "id": "urn:ngsi-ld:WeatherStation: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..af10f62ab --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityPlugin3.json @@ -0,0 +1,30 @@ +[ + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:WeatherStation:ws4", + "pressure": { + "type": "Property", + "value": { + "@type": "Hgmm", + "@value": "52" + } + }, + "sn": { + "type": "Property", + "value": 5 + }, + "type": "WeatherStation" + }, + { + "@context": "http://context.json-ld", + "humidity": { + "type": "Property", + "value": { + "@type": "Percentage", + "@value": "12" + } + }, + "id": "urn:ngsi-ld:WeatherStation: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..5271e6c90 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityPlugin4.json @@ -0,0 +1,19 @@ +[ + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:WeatherStation:ws5", + "type": "WeatherStation" + }, + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:Higrometer:Higro2000", + "pressure": { + "type": "Property", + "value": { + "@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..8eb05da1d --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityPlugin5.json @@ -0,0 +1,31 @@ +[ + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:WeatherStation:ws6", + "type": "WeatherStation" + }, + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:Higrometer:Higro2002", + "pressure": { + "type": "Property", + "value": { + "@type": "Hgmm", + "@value": "17" + } + }, + "type": "Higrometer" + }, + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:Higrometer:Higro2000", + "pressure": { + "type": "Property", + "value": { + "@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..353a32aba --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityPlugin6.json @@ -0,0 +1,36 @@ +[ + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:Sensor:Sensor", + "type": "Sensor" + }, + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:WM:SO1", + "type": "WM", + "vol": { + "type": "Property", + "value": 38 + } + }, + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:WM:SO2", + "type": "WM" + }, + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:WM:SO3", + "type": "WM" + }, + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:WM:SO4", + "type": "WM" + }, + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:WM: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..f4034a594 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityPlugin7.json @@ -0,0 +1,48 @@ +[ + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:Sensor:Sensor", + "type": "Sensor" + }, + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:WM:SO1", + "type": "WM", + "vol": { + "type": "Property", + "value": 38 + } + }, + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:WM:SO2", + "type": "WM", + "vol": { + "type": "Property", + "value": 39 + } + }, + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:WM:SO3", + "type": "WM", + "vol": { + "type": "Property", + "value": 40 + } + }, + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:WM:SO4", + "type": "WM" + }, + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:WM:SO5", + "type": "WM", + "vol": { + "type": "Property", + "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..c32005bc6 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityPlugin8.json @@ -0,0 +1,32 @@ +[ + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:WeatherStation:ws7", + "type": "WeatherStation" + }, + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:Higrometer:Higro2002", + "pressure": { + "type": "Property", + "unitCode": "Hgmm", + "value": { + "@type": "Hgmm", + "@value": "17" + } + }, + "type": "Higrometer" + }, + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:Higrometer:Higro2000", + "pressure": { + "type": "Property", + "value": { + "@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..45889898a --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityTimestampPlugin1.json @@ -0,0 +1,27 @@ +[ + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:WeatherStation:ws4", + "type": "WeatherStation", + "pressure": { + "type": "Property", + "value": { + "@type": "Hgmm", + "@value": "52" + }, + "observedAt": "2016-05-30T16:25:22.304Z" + } + }, + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:Higrometer:Higro2000", + "type": "Higrometer", + "humidity": { + "type": "Property", + "value": { + "@type": "Percentage", + "@value": "12" + } + } + } +] 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..d48ccce3c --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityTimestampPlugin2.json @@ -0,0 +1,19 @@ +[ + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:WeatherStation:ws4", + "type": "WeatherStation" + }, + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:Higrometer:Higro2000", + "type": "Higrometer", + "humidity": { + "type": "Property", + "value": { + "@type": "Percentage", + "@value": "12" + } + } + } +] 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..ae42ecff4 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityTimestampPlugin3.json @@ -0,0 +1,20 @@ +[ + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:WeatherStation:ws5", + "type": "WeatherStation" + }, + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:Higrometer:Higro2000", + "type": "Higrometer", + "humidity": { + "type": "Property", + "value": { + "@type": "Percentage", + "@value": "16" + }, + "observedAt": "2018-06-13T13:28:34.611Z" + } + } +] 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..0878e85bc --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextMultientityTimestampPlugin4.json @@ -0,0 +1,23 @@ +[ + { + "@context": "http://context.json-ld", + "PING_info": { + "observedAt": "2015-08-05T07:35:01.468Z", + "type": "Property", + "value": { + "@type": "commandResult", + "@value": "1234567890" + } + }, + "PING_status": { + "observedAt": "2015-08-05T07:35:01.468Z", + "type": "Property", + "value": { + "@type": "commandStatus", + "@value": "OK" + } + }, + "id": "urn:ngsi-ld:SensorCommand: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..9f33a9b6d --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextProcessTimestamp.json @@ -0,0 +1,12 @@ +[ + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:Light:light1", + "state": { + "observedAt": "2016-05-30T16:25:22.304Z", + "type": "Property", + "value": true + }, + "type": "Light" + } +] 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..b534b1cb1 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextStaticAttributes.json @@ -0,0 +1,21 @@ +[ + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:Motion:motion1", + "location": { + "type": "GeoProperty", + "value": { + "coordinates": [ + 153, + 523 + ], + "type": "Point" + } + }, + "moving": { + "type": "Property", + "value": true + }, + "type": "Motion" + } +] 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..6ef5221df --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextStaticAttributesMetadata.json @@ -0,0 +1,20 @@ +[ + { + "@context": "http://context.json-ld", + "controlledProperty": { + "includes": { + "type": "Property", + "value": "bell" + }, + "type": "Property", + "value": "StaticValue" + }, + "id": "urn:ngsi-ld:Lamp:lamp1", + "luminosity": { + "type": "Property", + "unitCode": "CAL", + "value": 100 + }, + "type": "Lamp" + } +] 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..119f19437 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextTimestamp.json @@ -0,0 +1,17 @@ +[ + { + "@context": "http://context.json-ld", + "dimming": { + "observedAt": "2015-08-05T07:35:01.468Z", + "type": "Property", + "value": 87 + }, + "id": "urn:ngsi-ld:Light:light1", + "state": { + "observedAt": "2015-08-05T07:35:01.468Z", + "type": "Property", + "value": true + }, + "type": "Light" + } +] 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..9918408c5 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextTimestampOverride.json @@ -0,0 +1,11 @@ +[ + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:Light:light1", + "state": { + "type": "Property", + "value": true + }, + "type": "Light" + } +] 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..9918408c5 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextTimestampOverrideWithoutMilis.json @@ -0,0 +1,11 @@ +[ + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:Light:light1", + "state": { + "type": "Property", + "value": true + }, + "type": "Light" + } +] 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..119f19437 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextTimestampTimezone.json @@ -0,0 +1,17 @@ +[ + { + "@context": "http://context.json-ld", + "dimming": { + "observedAt": "2015-08-05T07:35:01.468Z", + "type": "Property", + "value": 87 + }, + "id": "urn:ngsi-ld:Light:light1", + "state": { + "observedAt": "2015-08-05T07:35:01.468Z", + "type": "Property", + "value": true + }, + "type": "Light" + } +] 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..14d416828 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateProvisionActiveAttributes1.json @@ -0,0 +1,14 @@ +[ + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:Light:light1", + "temperature": { + "type": "Property", + "value": { + "@type": "centigrades", + "@value": " " + } + }, + "type": "Light" + } +] 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..029eba449 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateProvisionCommands1.json @@ -0,0 +1,28 @@ +[ + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:Light:light1", + "move_info": { + "type": "Property", + "value": { + "@type": "commandResult", + "@value": " " + } + }, + "move_status": { + "type": "Property", + "value": { + "@type": "commandStatus", + "@value": "UNKNOWN" + } + }, + "temperature": { + "type": "Property", + "value": { + "@type": "centigrades", + "@value": " " + } + }, + "type": "Light" + } +] 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..c528e2883 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateProvisionDeviceStatic.json @@ -0,0 +1,38 @@ +[ + { + "@context": "http://context.json-ld", + "cellID": { + "type": "Property", + "value": 435 + }, + "id": "urn:ngsi-ld:MicroLights:SecondMicroLight", + "location": { + "type": "Property", + "value": { + "@type": "geo:json", + "@value": { + "coordinates": [ + -3.164485591715449, + 40.62785133667262 + ], + "type": "Point" + } + } + }, + "newAttribute": { + "type": "Property", + "value": { + "@type": "Intangible", + "@value": null + } + }, + "serverURL": { + "type": "Property", + "value": { + "@type": "URL", + "@value": "http://fakeserver.com" + } + }, + "type": "MicroLights" + } +] 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..d75432c70 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateProvisionMinimumDevice.json @@ -0,0 +1,14 @@ +[ + { + "@context": "http://context.json-ld", + "id": "urn:ngsi-ld:MicroLights:SecondMicroLight", + "newAttribute": { + "type": "Property", + "value": { + "@type": "Intangible", + "@value": null + } + }, + "type": "MicroLights" + } +] 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..1d847b31f --- /dev/null +++ b/test/unit/ngsi-ld/expressions/expressionBasedTransformations-test.js @@ -0,0 +1,884 @@ +/* + * 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 + */ + +/* eslint-disable no-unused-vars */ + +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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin2.json' + ) + ) + .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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin4.json' + ) + ) + .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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin11.json' + ) + ) + .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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin1.json' + ) + ) + .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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin18.json' + ) + ) + .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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin3.json' + ) + ) + .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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin8.json' + ) + ) + .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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin19.json' + ) + ) + .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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin5.json' + ) + ) + .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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin16.json' + ) + ) + .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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin17.json' + ) + ) + .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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin9.json' + ) + ) + .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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin10.json' + ) + ) + .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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin15.json' + ) + ) + .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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin6.json' + ) + ) + .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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin7.json' + ) + ) + .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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin12.json' + ) + ) + .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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin13.json' + ) + ) + .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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin14.json' + ) + ) + + .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/config-jsonld-contexts-test.js b/test/unit/ngsi-ld/general/config-jsonld-contexts-test.js new file mode 100644 index 000000000..4e0e669db --- /dev/null +++ b/test/unit/ngsi-ld/general/config-jsonld-contexts-test.js @@ -0,0 +1,153 @@ +/* + * Copyright 2016 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: Fernando López - FIWARE Foundation, e.V. + * + */ + +/* eslint-disable no-unused-vars */ + +const config = require('../../../../lib/commonConfig'); +const iotAgentConfig = { + logLevel: 'FATAL', + contextBroker: { + host: '192.168.1.1', + port: '1026', + ngsiVersion: 'ld', + // or ['http://context1.json-ld','http://context2.json-ld'] if you need more than one + 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', + deviceRegistrationDuration: 'P1M', + throttling: 'PT5S' +}; + +describe('NGSI-LD - JSON-LD @context parsing from environment variable', function () { + describe('When the context is provided as a semicolon separated list of contexts', function () { + beforeEach(function () { + process.env.IOTA_JSON_LD_CONTEXT = 'http://context1.json-ld,http://context2.json-ld'; + iotAgentConfig.contextBroker.jsonLdContext = 'http://whateverContext.json-ld'; + }); + + afterEach(function () { + delete process.env.IOTA_JSON_LD_CONTEXT; + }); + + it('should load the configuration as a list of contexts', function (done) { + config.setConfig(iotAgentConfig); + config + .getConfig() + .contextBroker.jsonLdContext.should.containDeep(['http://context1.json-ld', 'http://context2.json-ld']); + done(); + }); + }); + + describe('When the context is provided as a semicolon separated list of contexts with extra whitespace', function () { + beforeEach(function () { + process.env.IOTA_JSON_LD_CONTEXT = + 'http://context1.json-ld , http://context2.json-ld, http://context3.json-ld'; + iotAgentConfig.contextBroker.jsonLdContext = 'http://whateverContext.json-ld'; + }); + + afterEach(function () { + delete process.env.IOTA_JSON_LD_CONTEXT; + }); + + it('should load the configuration as a list of contexts and remove the extra whitespace', function (done) { + config.setConfig(iotAgentConfig); + config + .getConfig() + .contextBroker.jsonLdContext.should.containDeep([ + 'http://context1.json-ld', + 'http://context2.json-ld', + 'http://context3.json-ld' + ]); + done(); + }); + }); + + describe('When the context is provided as a string value', function () { + beforeEach(function () { + process.env.IOTA_JSON_LD_CONTEXT = 'http://context1.json-ld'; + iotAgentConfig.contextBroker.jsonLdContext = 'http://whateverContext.json-ld'; + }); + + afterEach(function () { + delete process.env.IOTA_JSON_LD_CONTEXT; + }); + + it('should load the configuration as a single entry list', function (done) { + config.setConfig(iotAgentConfig); + config.getConfig().contextBroker.jsonLdContext.should.containDeep(['http://context1.json-ld']); + done(); + }); + }); +}); + +describe('NGSI-LD - JSON-LD @context parsing from global configuration', function () { + describe('When the context is provided as a list of contexts', function () { + beforeEach(function () { + iotAgentConfig.contextBroker.jsonLdContext = ['http://context1.json-ld', 'http://context2.json-ld']; + }); + + it('should load the configuration as a list of contexts', function (done) { + config.setConfig(iotAgentConfig); + config + .getConfig() + .contextBroker.jsonLdContext.should.containDeep(['http://context1.json-ld', 'http://context2.json-ld']); + done(); + }); + }); + + describe('When the context is provided as a string value', function () { + beforeEach(function () { + iotAgentConfig.contextBroker.jsonLdContext = 'http://context1.json-ld'; + }); + + it('should load the configuration as a string', function (done) { + config.setConfig(iotAgentConfig); + config.getConfig().contextBroker.jsonLdContext.should.equal('http://context1.json-ld'); + 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..73fab3c82 --- /dev/null +++ b/test/unit/ngsi-ld/general/contextBrokerOAuthSecurityAccess-test.js @@ -0,0 +1,868 @@ +/* + * 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 + */ + +/* eslint-disable no-unused-vars */ + +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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/updateContext1.json') + ) + .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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/updateContext1.json') + ) + .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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + 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/urn:ngsi-ld:Light:light1?attrs=state,dimming') + .reply( + 200, + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextResponses/queryContext1Success.json') + ); + + iotAgentLib.activate(iotAgentConfig, done); + }); + + it('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(204); + + contextBrokerMock + .post( + '/ngsi-ld/v1/subscriptions/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/subscriptionRequests/simpleSubscriptionRequest2.json' + ) + ) + .matchHeader('Authorization', 'Bearer eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJ3cHdWclJ3') + .reply(201, null, { Location: '/ngsi-ld/v1/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('/ngsi-ld/v1/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(); + }); + }); + }); + }); + }); +}); + +describe('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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/updateContext1.json') + ) + .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/urn:ngsi-ld:Light:light1?attrs=state,dimming') + .reply( + 200, + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextResponses/queryContext1Success.json') + ); + + iotAgentLib.activate(iotAgentConfig, done); + }); + + it('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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/updateContext1.json') + ) + .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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContext3WithStatic.json' + ) + ) + .reply(204); + + contextBrokerMock2 = nock('http://unexistentHost:1026') + .matchHeader('fiware-service', 'TestService') + .matchHeader('Authorization', 'Bearer bbb752e377680acd1349a3ed59db855a1db076aa') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContext3WithStatic.json' + ) + ) + .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) { + logger.setLevel('FATAL'); + + 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(204); + + contextBrokerMock3 = nock('http://unexistentHost:1026') + .matchHeader('fiware-service', 'TestService') + .matchHeader('authorization', 'Bearer zzz752e377680acd1349a3ed59db855a1db07bbb') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/updateContext5.json') + ) + .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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContext3WithStatic.json' + ) + ) + .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..7a417cd10 --- /dev/null +++ b/test/unit/ngsi-ld/general/deviceService-test.js @@ -0,0 +1,258 @@ +/* + * 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 + */ + +/* eslint-disable no-unused-vars */ + +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(204); + + 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(204); + + 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..3cdf046e1 --- /dev/null +++ b/test/unit/ngsi-ld/general/https-support-test.js @@ -0,0 +1,265 @@ +/* + * 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 + */ + +/* eslint-disable no-unused-vars */ + +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', + 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', + 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(204); + + 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: '/ngsi-ld/v1/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(204); + + contextBrokerMock = nock('https://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .post('/ngsi-ld/v1/csourceRegistrations/') + .reply(201, null, { Location: '/ngsi-ld/v1/csourceRegistrations/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..c65224823 --- /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] + */ + +/* eslint-disable no-unused-vars */ + +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..e68a570ce --- /dev/null +++ b/test/unit/ngsi-ld/general/startup-test.js @@ -0,0 +1,145 @@ +/* + * 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 + */ + +/* eslint-disable no-unused-vars */ + +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..2551b94b8 --- /dev/null +++ b/test/unit/ngsi-ld/lazyAndCommands/active-devices-attribute-update-test.js @@ -0,0 +1,152 @@ +/* + * 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 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: '/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) { + iotAgentLib.clearAll(function () { + iotAgentLib.deactivate(function () { + mongoUtils.cleanDbs(function () { + nock.cleanAll(); + iotAgentLib.setDataUpdateHandler(); + iotAgentLib.setCommandHandler(); + done(); + }); + }); + }); + }); + + describe('When a attribute update arrives to the IoT Agent as Context Provider', function () { + const options = { + url: + 'http://localhost:' + + iotAgentConfig.server.port + + '/ngsi-ld/v1/entities/urn:ngsi-ld:Light:somelight/attrs/pressure', + method: 'PATCH', + json: { + type: 'Hgmm', + value: 200 + }, + headers: { + 'fiware-service': 'smartGondor', + 'content-type': 'application/ld+json' + } + }; + + 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('urn:ngsi-ld: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..8bd97d34b --- /dev/null +++ b/test/unit/ngsi-ld/lazyAndCommands/command-test.js @@ -0,0 +1,303 @@ +/* + * 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 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', + providerUrl: 'http://smartGondor.com' +}; +const device3 = { + id: 'r2d2', + type: 'Robot', + service: 'smartGondor' +}; + +describe('NGSI-LD - Command 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/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(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(); + }); + }); + }); + describe('When a command update 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: [28, -104, 23] + }, + headers: { + 'fiware-service': 'smartGondor', + 'content-type': 'application/ld+json' + } + }; + + beforeEach(function (done) { + logger.setLevel('ERROR'); + 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('urn:ngsi-ld:' + device3.type + ':' + device3.id); + type.should.equal(device3.type); + attributes[0].name.should.equal('position'); + JSON.stringify(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 serviceReceived = false; + iotAgentLib.setCommandHandler(function (id, type, service, subservice, attributes, callback) { + serviceReceived = service === 'smartGondor'; + callback(null, { + id, + type, + attributes: [ + { + name: 'position', + type: 'Array', + value: '[28, -104, 23]' + } + ] + }); + }); + + request(options, function (error, response, body) { + serviceReceived.should.equal(true); + done(); + }); + }); + }); + describe('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/entityOperations/upsert/', + 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(); + }); + }); + }); + describe('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/entityOperations/upsert/', + 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..65892e3f1 --- /dev/null +++ b/test/unit/ngsi-ld/lazyAndCommands/lazy-devices-test.js @@ -0,0 +1,546 @@ +/* + * 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 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); + }); + }); + }); + + describe('When the IoT Agent receives an update on the device data in JSON format', function () { + const options = { + url: + 'http://localhost:' + + iotAgentConfig.server.port + + '/ngsi-ld/v1/entities/urn:ngsi-ld:Light:light1/attrs/dimming', + method: 'PATCH', + json: { + 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: '/ngsi-ld/v1/csourceRegistrations/6319a7f5254b05844116584d' }); + + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post('/ngsi-ld/v1/entityOperations/upsert/') + .reply(204); + + 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('urn:ngsi-ld:' + 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'); + }); + + describe('When a context query arrives to the IoT Agent', function () { + const options = { + url: + 'http://localhost:' + + iotAgentConfig.server.port + + '/ngsi-ld/v1/entities/urn:ngsi-ld:Light:light1?attrs=dimming', + method: 'GET', + json: true, + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': 'gardens' + } + }; + 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: '/ngsi-ld/v1/csourceRegistrations/6319a7f5254b05844116584d' }); + + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post('/ngsi-ld/v1/entityOperations/upsert/') + .reply(204); + + 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.setDataUpdateHandler(function (id, type, service, subservice, attributes, callback) { + callback(null); + }); + + iotAgentLib.setDataQueryHandler(function (id, type, service, subservice, attributes, callback) { + id.should.equal('urn:ngsi-ld:' + 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(); + }); + }); + }); + + describe('When a context query arrives to the IoT Agent and no handler is set', function () { + const options = { + url: + 'http://localhost:' + + iotAgentConfig.server.port + + '/ngsi-ld/v1/entities/urn:ngsi-ld:Light:light1?attrs=dimming', + method: 'GET', + json: 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/registerIoTAgent1.json' + ) + ) + .reply(201, null, { Location: '/ngsi-ld/v1/csourceRegistrations/6319a7f5254b05844116584d' }); + + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post('/ngsi-ld/v1/entityOperations/upsert/') + .reply(204); + + 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.dimming.value.should.equal(''); + done(); + }); + }); + }); + + describe('When a query arrives to the IoT Agent without any attributes', function () { + const options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/ngsi-ld/v1/entities/urn:ngsi-ld:Light:light1', + method: 'GET', + json: true, + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': 'gardens' + } + }; + 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: '/ngsi-ld/v1/csourceRegistrations/6319a7f5254b05844116584d' }); + + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post('/ngsi-ld/v1/entityOperations/upsert/') + .reply(204); + + 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(); + }); + }); + }); + + describe('When a context query arrives to the IoT Agent for a type with static attributes', function () { + const options = { + url: + 'http://localhost:' + + iotAgentConfig.server.port + + '/ngsi-ld/v1/entities/urn:ngsi-ld:Motion:motion1?attrs=moving,location', + method: 'GET', + json: true, + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': 'gardens' + } + }; + 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: '/ngsi-ld/v1/csourceRegistrations/6319a7f5254b05844116584d' }); + + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post('/ngsi-ld/v1/entityOperations/upsert/') + .reply(204); + + 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('urn:ngsi-ld: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(); + }); + }); + }); + + describe('When a query arrives to the IoT Agent with id, type and attributes', function () { + const options = { + url: + 'http://localhost:' + + iotAgentConfig.server.port + + '/ngsi-ld/v1/entities/urn:ngsi-ld:Light:light1?attrs=temperature', + method: 'GET', + json: true, + headers: { + 'fiware-service': 'smartGondor', + 'fiware-servicepath': 'gardens' + } + }; + 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: '/ngsi-ld/v1/csourceRegistrations/6319a7f5254b05844116584d' }); + + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post('/ngsi-ld/v1/entityOperations/upsert/') + .reply(204); + + 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..e39b62399 --- /dev/null +++ b/test/unit/ngsi-ld/lazyAndCommands/polling-commands-test.js @@ -0,0 +1,373 @@ +/* + * 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 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: '/ngsi-ld/v1/csourceRegistrations/6319a7f5254b05844116584d' }); + + 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(); + }); + }); + }); + }); + + describe('When a command update arrives to the IoT Agent for a device with polling', 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: [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/entityOperations/upsert/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextCommandStatus1.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(); + }); + }); + xit('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(); + }); + }); + }); + }); + + describe('When a command arrives with multiple values in the value field', function () { + const options = { + url: + 'http://localhost:' + + iotAgentConfig.server.port + + '/ngsi-ld/v1/entities/urn:ngsi-ld:Robot:r2d2/attrs/position', + method: 'PATCH', + json: { + '@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/entityOperations/upsert/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextCommandStatus1.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(); + }); + }); + }); + + describe('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/entityOperations/upsert/', + 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/entityOperations/upsert/', + utils.readExampleFile( + './test/unit/ngsi-ld/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); + }); + }); + + xit('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..01a87ff4b --- /dev/null +++ b/test/unit/ngsi-ld/ngsiService/active-devices-test.js @@ -0,0 +1,763 @@ +/* + * 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 + */ + +/* eslint-disable no-unused-vars */ + +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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/updateContext.json') + ) + + .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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/updateContextTimestamp.json') + ) + + .reply(204); + + iotAgentConfig.timestamp = true; + iotAgentLib.activate(iotAgentConfig, done); + }); + + afterEach(function (done) { + delete iotAgentConfig.timestamp; + timekeeper.reset(); + + done(); + }); + + it('should calculate the timestamp for 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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/' + + 'contextRequests/updateContextTimestampOverrideWithoutMilis.json' + ) + ) + + .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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextTimestampTimezone.json' + ) + ) + + .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 calculate the timestamp for 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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextTimestampOverride.json' + ) + ) + + .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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextTimestampOverride.json' + ) + ) + + .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 unrecognized status code updating an entity', function () { + 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/updateContext.json') + ) + .reply(207, { notUpdated: 'someEntities' }); + + iotAgentLib.activate(iotAgentConfig, done); + }); + + it('should return an error message in the response body', function (done) { + iotAgentLib.update('light1', 'Light', '', values, function (error) { + should.exist(error); + should.exist(error.name); + error.code.should.equal(207); + error.details.notUpdated.should.equal('someEntities'); + error.message.should.equal('Error accesing entity data for device: light1 of type: Light'); + error.name.should.equal('ENTITY_GENERIC_ERROR'); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe('When the Context Broker returns a 200 status code (NGSI-LD v1.2.1) updating an entity', function () { + 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/updateContext.json') + ) + .reply(200); + + iotAgentLib.activate(iotAgentConfig, done); + }); + + it('should be considered as a successful update of the entity', function (done) { + iotAgentLib.update('light1', 'Light', '', values, function (error) { + should.not.exist(error); + contextBrokerMock.done(); + 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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/updateContext.json') + ) + + .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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/updateContext.json') + ) + + .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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/updateContext.json') + ) + + .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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/updateContext2.json') + ) + .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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextStaticAttributes.json' + ) + ) + .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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextStaticAttributesMetadata.json' + ) + ) + + .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..211d43247 --- /dev/null +++ b/test/unit/ngsi-ld/ngsiService/autocast-test.js @@ -0,0 +1,424 @@ +/* + * 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 + */ + +/* eslint-disable no-unused-vars */ + +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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/updateContextAutocast1.json') + ) + + .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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/updateContextAutocast2.json') + ) + + .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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/updateContextAutocast3.json') + ) + + .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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/updateContextAutocast4.json') + ) + + .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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/updateContextAutocast5.json') + ) + + .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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/updateContextAutocast6.json') + ) + + .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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/updateContextAutocast7.json') + ) + + .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 Time type', function () { + const values = [ + { + name: 'configuration', + type: 'Time', + value: '2016-04-30T14:59:46.000Z' + } + ]; + + 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/updateContextAutocast8.json') + ) + + .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 DateTime type', function () { + const values = [ + { + name: 'configuration', + type: 'DateTime', + value: '2016-04-30Z' + } + ]; + + 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/updateContextAutocast9.json') + ) + + .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 Date type', function () { + const values = [ + { + name: 'configuration', + type: 'Date', + value: '2016-04-30T14:59:46.000Z' + } + ]; + + 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/updateContextAutocast10.json') + ) + + .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/geoproperties-test.js b/test/unit/ngsi-ld/ngsiService/geoproperties-test.js new file mode 100644 index 000000000..ed4c74f07 --- /dev/null +++ b/test/unit/ngsi-ld/ngsiService/geoproperties-test.js @@ -0,0 +1,379 @@ +/* + * Copyright 2014 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: Daniel Calvo - ATOS Research & Innovation + */ + +/* eslint-disable no-unused-vars */ + +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: 'location', + type: 'GeoProperty' + } + ] + } + }, + service: 'smartGondor', + subservice: 'gardens', + providerUrl: 'http://smartGondor.com' +}; + +describe('NGSI-LD - Geo-JSON types autocast test', function () { + beforeEach(function () { + logger.setLevel('FATAL'); + }); + + afterEach(function (done) { + iotAgentLib.deactivate(done); + }); + + describe( + 'When the IoT Agent receives new geo-information from a device.' + + 'Location with GeoProperty type and String value', + function () { + const values = [ + { + name: 'location', + type: 'GeoProperty', + value: '23,12.5' + } + ]; + + 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/updateContextGeoproperties1.json' + ) + ) + .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 geo-information from a device. Location with Point type and Array value', function () { + const values = [ + { + name: 'location', + type: 'Point', + value: [23, 12.5] + } + ]; + + 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/updateContextGeoproperties1.json' + ) + ) + .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 geo-information from a device.' + + 'Location with LineString type and Array value', + function () { + const values = [ + { + name: 'location', + type: 'LineString', + value: [ + [23, 12.5], + [22, 12.5] + ] + } + ]; + + 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/updateContextGeoproperties2.json' + ) + ) + .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 geo-information from a device.' + + 'Location with LineString type and Array of Strings', + function () { + const values = [ + { + name: 'location', + type: 'LineString', + value: ['23,12.5', '22,12.5'] + } + ]; + + 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/updateContextGeoproperties2.json' + ) + ) + .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 geo-information from a device. Location with None type', function () { + const values = [ + { + name: 'location', + type: 'None', + value: 'null' + } + ]; + + 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/updateContextGeoproperties3.json' + ) + ) + .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 geo-information from a device.' + + 'Location with Polygon type - Array of coordinates', + function () { + const values = [ + { + name: 'location', + type: 'Polygon', + value: [ + [23, 12.5], + [22, 13.5], + [22, 13.5] + ] + } + ]; + + 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/updateContextGeoproperties4.json' + ) + ) + .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 geo-information from a device.' + + 'Location with Polygon type - list of coordinates', + function () { + const values = [ + { + name: 'location', + type: 'Polygon', + value: '23,12.5,22,13.5,22,13.5' + } + ]; + + 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/updateContextGeoproperties4.json' + ) + ) + .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 geo-information from a device. Location with a missing latitude', function () { + const values = [ + { + name: 'location', + type: 'Point', + value: '23,12.5,22,13.5,22' + } + ]; + + beforeEach(function (done) { + nock.cleanAll(); + iotAgentLib.activate(iotAgentConfig, done); + }); + + it('should throw a BadGeocoordinates Error', function (done) { + iotAgentLib.update('light1', 'Light', '', values, function (error) { + should.exist(error); + done(); + }); + }); + }); + + describe('When the IoT Agent receives new geo-information from a device. Location invalid coordinates', function () { + const values = [ + { + name: 'location', + type: 'Point', + value: '2016-04-30Z' + } + ]; + + beforeEach(function (done) { + nock.cleanAll(); + iotAgentLib.activate(iotAgentConfig, done); + }); + + it('should throw a BadGeocoordinates Error', function (done) { + iotAgentLib.update('light1', 'Light', '', values, function (error) { + should.exist(error); + 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..6986fccb7 --- /dev/null +++ b/test/unit/ngsi-ld/ngsiService/staticAttributes-test.js @@ -0,0 +1,151 @@ +/* + * 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 + */ + +/* eslint-disable no-unused-vars */ + +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' +}; + +describe('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') + .post('/ngsi-ld/v1/entityOperations/upsert/') + .times(4) + .reply(204) + .post('/ngsi-ld/v1/entityOperations/upsert/', function (body) { + // Since the TimeInstant plugin is in use, + // Each property should contain observedAt + // metadata. + let count = 0; + for (const i in body) { + if (body[i].observedAt) { + count++; + } + } + return count === Object.keys(body).length - 1; + }) + .reply(204); + + iotAgentLib.activate(iotAgentConfig, done); + }); + + it('should send a single observedAt 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..ea06411db --- /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 + */ + +/* eslint-disable no-unused-vars */ + +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(204); + + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post( + '/ngsi-ld/v1/subscriptions/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/subscriptionRequests/simpleSubscriptionRequest.json' + ) + ) + .reply(201, null, { Location: '/ngsi-ld/v1/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('/ngsi-ld/v1/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('/ngsi-ld/v1/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..2a42b92dd --- /dev/null +++ b/test/unit/ngsi-ld/plugins/alias-plugin_test.js @@ -0,0 +1,433 @@ +/* + * 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 + */ + +/* eslint-disable no-unused-vars */ + +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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/updateContextAliasPlugin1.json') + ) + .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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/updateContextAliasPlugin2.json') + ) + .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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/updateContextAliasPlugin3.json') + ) + .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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/updateContextAliasPlugin3.json') + ) + .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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/updateContextAliasPlugin4.json') + ) + .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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/updateContextAliasPlugin5.json') + ) + .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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/updateContextAliasPlugin6.json') + ) + .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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/updateContextAliasPlugin7.json') + ) + .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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/updateContextAliasPlugin8.json') + ) + .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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/updateContextAliasPlugin9.json') + ) + .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..f42725419 --- /dev/null +++ b/test/unit/ngsi-ld/plugins/bidirectional-plugin_test.js @@ -0,0 +1,474 @@ +/* + * 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 + */ + +/* eslint-disable no-unused-vars */ + +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: '/ngsi-ld/v1/subscriptions/51c0ac9ed714fb3b37d7d5a8' }); + + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/createBidirectionalDevice.json') + ) + .reply(204); + }); + + 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: '/ngsi-ld/v1/subscriptions/51c0ac9ed714fb3b37d7d5a8' }); + + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/createBidirectionalDevice.json') + ) + .reply(204); + + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .delete('/ngsi-ld/v1/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: '/ngsi-ld/v1/subscriptions/51c0ac9ed714fb3b37d7d5a8' }); + + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/createBidirectionalDevice.json') + ) + .reply(204); + }); + + 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: '/ngsi-ld/v1/subscriptions/51c0ac9ed714fb3b37d7d5a8' }); + + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/createBidirectionalDevice.json') + ) + .reply(204); + }); + 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: '/ngsi-ld/v1/subscriptions/51c0ac9ed714fb3b37d7d5a8' }); + + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/createBidirectionalDevice.json') + ) + .reply(204); + }); + + 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: '/ngsi-ld/v1/subscriptions/51c0ac9ed714fb3b37d7d5a8' }); + + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/createBidirectionalDevice.json') + ) + .reply(204); + }); + + 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..29e790776 --- /dev/null +++ b/test/unit/ngsi-ld/plugins/compress-timestamp-plugin_test.js @@ -0,0 +1,246 @@ +/* + * 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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextCompressTimestamp1.json' + ) + ) + .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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextCompressTimestamp2.json' + ) + ) + .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/urn:ngsi-ld:Light:light1?attrs=state,TheTargetValue') + .reply( + 200, + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextResponses/queryContextCompressTimestamp1Success.json' + ) + ); + }); + + it('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..2ac6cc9cc --- /dev/null +++ b/test/unit/ngsi-ld/plugins/event-plugin_test.js @@ -0,0 +1,116 @@ +/* + * 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') + .post('/ngsi-ld/v1/entityOperations/upsert/', function (body) { + const dateRegex = /\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d.\d{3}Z/; + return body[0].activation.value['@value'].match(dateRegex); + }) + .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..4f0d1f267 --- /dev/null +++ b/test/unit/ngsi-ld/plugins/multientity-plugin_test.js @@ -0,0 +1,760 @@ +/* + * 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' +}; + +describe('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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + 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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + 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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + 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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + 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') + + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + 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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + 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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + 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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + 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(); + }); + }); + } + ); +}); + +describe('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') + .post('/ngsi-ld/v1/entityOperations/upsert/', 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[1].humidity.observedAt) { + return false; + } + + const timeInstantAtt = body[1].humidity.observedAt; + if (moment(timeInstantAtt, 'YYYY-MM-DDTHH:mm:ss.SSSZ').isValid) { + delete body[1].humidity.observedAt; + delete expectedBody[1].humidity.observedAt; + 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') + .post('/ngsi-ld/v1/entityOperations/upsert/', 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[1].humidity.observedAt) { + return false; + } + + const timeInstantAtt = body[1].humidity.observedAt; + if (moment(timeInstantAtt, 'YYYY-MM-DDTHH:mm:ss.SSSZ').isValid) { + delete body[1].humidity.observedAt; + delete expectedBody[1].humidity.observedAt; + 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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + 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(); + }); + }); + }); +}); + +describe('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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + 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..2d3d14ab8 --- /dev/null +++ b/test/unit/ngsi-ld/plugins/timestamp-processing-plugin_test.js @@ -0,0 +1,118 @@ +/* + * 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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateContextProcessTimestamp.json' + ) + ) + .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..8fc61a9a2 --- /dev/null +++ b/test/unit/ngsi-ld/provisioning/device-provisioning-api_test.js @@ -0,0 +1,942 @@ +/* + * 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 + */ + +/* eslint-disable no-unused-vars */ + +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 () { + 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: '/ngsi-ld/v1/csourceRegistrations/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(204); + + 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: '/ngsi-ld/v1/csourceRegistrations/6319a7f5254b05844116584d' }); + + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/createProvisionedDevice.json') + ) + .reply(204); + }); + + 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(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 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(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 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(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 arrives to the IoTAand 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/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/createTimeInstantMinimumDevice.json' + ) + ) + .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(204); + + 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(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 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(204); + + contextBrokerMock + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/createMinimumProvisionedDevice.json' + ) + ) + .reply(204); + + 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 the Context Broker returns an unrecognized status code provisioning an entity', 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(207); + + done(); + }); + + it('should return an error message in the response body', function (done) { + request(options, function (error, response, body) { + should.not.exist(error); + response.body.name.should.equal('ENTITY_GENERIC_ERROR'); + response.body.message.should.equal( + 'Error accesing entity data for device: MicroLight1 of type: MicroLights' + ); + response.statusCode.should.equal(200); + + done(); + }); + }); + }); + + describe('When the Context Broker returns a 200 status code (NGSI-LD v1.2.1) provisioning an entity', 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 be considered as a successful provisioning of the entity', function (done) { + request(options, function (error, response, body) { + should.not.exist(error); + response.body.should.be.empty(); + response.statusCode.should.equal(201); + + done(); + }); + }); + }); + + describe('When the Context Broker returns a 201 status code (NGSI-LD v1.3.1 - created entities) provisioning an entity', 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(201); + + done(); + }); + + it('should be considered as a successful provisioning of the entity', function (done) { + request(options, function (error, response, body) { + should.not.exist(error); + response.body.should.be.empty(); + response.statusCode.should.equal(201); + + 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: '/ngsi-ld/v1/csourceRegistrations/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: '/ngsi-ld/v1/csourceRegistrations/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: '/ngsi-ld/v1/csourceRegistrations/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(204); + + 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..c152a2dea --- /dev/null +++ b/test/unit/ngsi-ld/provisioning/device-registration_test.js @@ -0,0 +1,356 @@ +/* + * 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 + */ + +/* eslint-disable no-unused-vars */ + +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(204); + + 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: '/ngsi-ld/v1/csourceRegistrations/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: '/ngsi-ld/v1/csourceRegistrations/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(204); + + 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('urn:ngsi-ld: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: '/ngsi-ld/v1/csourceRegistrations/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(); + }); + }); + }); + }); + + describe('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: '/ngsi-ld/v1/csourceRegistrations/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(204); + + contextBrokerMock + .post('/ngsi-ld/v1/csourceRegistrations/') + .reply(201, null, { Location: '/ngsi-ld/v1/csourceRegistrations/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(204); + + contextBrokerMock + .delete('/ngsi-ld/v1/csourceRegistrations/6319a7f5254b05844116584d') + .reply(204, null, { Location: '/ngsi-ld/v1/csourceRegistrations/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(); + }); + }); + }); + + describe('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: '/ngsi-ld/v1/csourceRegistrations/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(204); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .post('/ngsi-ld/v1/csourceRegistrations/') + .reply(201, null, { Location: '/ngsi-ld/v1/csourceRegistrations/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(204); + + contextBrokerMock.delete('/ngsi-ld/v1/csourceRegistrations/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..4b6471c61 --- /dev/null +++ b/test/unit/ngsi-ld/provisioning/device-update-registration_test.js @@ -0,0 +1,289 @@ +/* + * 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 + */ + +/* eslint-disable no-unused-vars */ + +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: [] +}; + +describe('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') + .post('/ngsi-ld/v1/csourceRegistrations/') + .reply(201, null, { Location: '/ngsi-ld/v1/csourceRegistrations/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(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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/?options=update', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/updateProvisionActiveAttributes1.json' + ) + ) + .reply(204); + + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post( + '/ngsi-ld/v1/csourceRegistrations/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextAvailabilityRequests/updateIoTAgent1.json' + ) + ) + .reply(201, null, { Location: '/ngsi-ld/v1/csourceRegistrations/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') + .post( + '/ngsi-ld/v1/entityOperations/upsert/?options=update', + utils.readExampleFile('./test/unit/ngsi-ld/examples/contextRequests/updateProvisionCommands1.json') + ) + .reply(204); + + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post( + '/ngsi-ld/v1/csourceRegistrations/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextAvailabilityRequests/updateCommands1.json' + ) + ) + .reply(201, null, { Location: '/ngsi-ld/v1/csourceRegistrations/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 () { + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post('/ngsi-ld/v1/entityOperations/upsert/?options=update') + .reply(400); + }); + + 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..07add5e4f --- /dev/null +++ b/test/unit/ngsi-ld/provisioning/listProvisionedDevices-test.js @@ -0,0 +1,453 @@ +/* + * 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 + */ + +/* eslint-disable no-unused-vars */ + +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: '/ngsi-ld/v1/csourceRegistrations/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(204); + + contextBrokerMock + .post('/ngsi-ld/v1/csourceRegistrations/') + .reply(201, null, { Location: '/ngsi-ld/v1/csourceRegistrations/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(204); + + contextBrokerMock + .post('/ngsi-ld/v1/csourceRegistrations/') + .reply(201, null, { Location: '/ngsi-ld/v1/csourceRegistrations/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(204); + + 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: '/ngsi-ld/v1/csourceRegistrations/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(204); + + 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: '/ngsi-ld/v1/csourceRegistrations/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: '/ngsi-ld/v1/csourceRegistrations/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..bdb0da88e --- /dev/null +++ b/test/unit/ngsi-ld/provisioning/provisionDeviceMultientity-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::[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 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: '/ngsi-ld/v1/csourceRegistrations/6319a7f5254b05844116584d' }); + + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/createProvisionedDeviceMultientity.json' + ) + ) + .reply(204); + }); + + 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..1bfe1afa0 --- /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 + */ + +/* eslint-disable no-unused-vars */ + +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: 'ERROR', + 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: 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: '/ngsi-ld/v1/csourceRegistrations/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(204); + + 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: '/ngsi-ld/v1/csourceRegistrations/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(204); + + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .delete('/ngsi-ld/v1/csourceRegistrations/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(204); + + 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..9d029d444 --- /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 + */ + +/* eslint-disable no-unused-vars */ + +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: '/ngsi-ld/v1/csourceRegistrations/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(204); + + 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: '/ngsi-ld/v1/csourceRegistrations/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(204); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'AlternateService') + .post('/ngsi-ld/v1/csourceRegistrations/') + .reply(201, null, { Location: '/ngsi-ld/v1/csourceRegistrations/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(204); + + 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: '/ngsi-ld/v1/csourceRegistrations/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(204); + + 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: '/ngsi-ld/v1/csourceRegistrations/6319a7f5254b05844116584d' }); + + contextBrokerMock + .matchHeader('fiware-service', 'TestService') + .post( + '/ngsi-ld/v1/entityOperations/upsert/', + utils.readExampleFile( + './test/unit/ngsi-ld/examples/contextRequests/createProvisionedDeviceWithGroupAndStatic.json' + ) + ) + .reply(204); + + 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..f3a85333a --- /dev/null +++ b/test/unit/ngsi-ld/provisioning/updateProvisionedDevices-test.js @@ -0,0 +1,433 @@ +/* + * 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 + */ + +/* eslint-disable no-unused-vars */ + +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', + providerUrl: 'http://smartGondor.com' +}; + +describe('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: '/ngsi-ld/v1/csourceRegistrations/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(204); + + 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: '/ngsi-ld/v1/csourceRegistrations/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(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('/ngsi-ld/v1/csourceRegistrations/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: '/ngsi-ld/v1/csourceRegistrations/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', { + '@context': 'http://context.json-ld' + }) + .reply(204); + + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post('/ngsi-ld/v1/entityOperations/upsert/?options=update') + .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('/ngsi-ld/v1/csourceRegistrations/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: '/ngsi-ld/v1/csourceRegistrations/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: '/ngsi-ld/v1/csourceRegistrations/4419a7f52546658441165849' }); + }); + + it('should return a 204 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(204); + + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post( + '/ngsi-ld/v1/entityOperations/upsert/?options=update', + 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(204); + + contextBrokerMock + .matchHeader('fiware-service', 'smartGondor') + .post( + '/ngsi-ld/v1/entityOperations/upsert/?options=update', + 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(); + }); + }); + }); + }); +});