diff --git a/.gitignore b/.gitignore index 23cfd3090..35bdbcabe 100644 --- a/.gitignore +++ b/.gitignore @@ -14,4 +14,4 @@ .metadata npm-debug.log .eslintcache - +package-lock.json diff --git a/CHANGES_NEXT_RELEASE b/CHANGES_NEXT_RELEASE index 353820b12..c426e7c6f 100644 --- a/CHANGES_NEXT_RELEASE +++ b/CHANGES_NEXT_RELEASE @@ -3,4 +3,6 @@ - Fix: service header to use uppercase in case of update and delete (#1528) - Fix: Allow to send to CB batch update for multimeasures for NGSI-LD (#1623) - Add: new JEXL transformations for including into an array keys that have a certain value: valuePicker and valuePickerMulti +- Add /iot/groups API endpoints (as equivalent to /iot/services) (#752) - Add: attribute metadata and static attributes metadata added to jexl context (#1630) +- Deprecated: /iot/services API routes \ No newline at end of file diff --git a/doc/admin.md b/doc/admin.md index b82c27f88..1ec8a5d70 100644 --- a/doc/admin.md +++ b/doc/admin.md @@ -125,9 +125,9 @@ allowing the computer to interpret the rest of the data with more clarity and de ``` Under mixed mode, **NGSI v2** payloads are used for context broker communications by default, but this payload may also -be switched to **NGSI LD** at group or device provisioning time using the `ngsiVersion` field in the -provisioning API. The `ngsiVersion` field switch may be added at either group or device level, with the device level -overriding the group setting. +be switched to **NGSI LD** at group or device provisioning time using the `ngsiVersion` field in the provisioning API. +The `ngsiVersion` field switch may be added at either group or device level, with the device level overriding the group +setting. #### `server` @@ -306,7 +306,8 @@ added `agentPath`: #### `types` -This parameter includes additional groups configuration as described into the [Config group API](api.md#config-group-api) section. +This parameter includes additional groups configuration as described into the +[Config group API](api.md#config-group-api) section. #### `service` @@ -415,7 +416,8 @@ IotAgents, as all Express applications that use the body-parser middleware, have size that the application will handle. This default limit for ioiotagnets are 1Mb. So, if your IotAgent receives a request with a body that exceeds this limit, the application will throw a “Error: Request entity too large”. -The 1Mb default can be changed setting the `expressLimit` configuration parameter (or equivalente `IOTA_EXPRESS_LIMIT` environment variable). +The 1Mb default can be changed setting the `expressLimit` configuration parameter (or equivalente `IOTA_EXPRESS_LIMIT` +environment variable). ### Configuration using environment variables diff --git a/doc/api.md b/doc/api.md index 4a2860526..079249c8c 100644 --- a/doc/api.md +++ b/doc/api.md @@ -136,9 +136,9 @@ For every config group, the pair (resource, apikey) _must_ be unique (as it is u which device). Those operations of the API targeting specific resources will need the use of the `resource` and `apikey` parameters to select the appropriate instance. -Config groups can be created with preconfigured sets of attributes, service and subservice information, security information and other -parameters. The specific parameters that can be configured for a given config group are described in the -[Config group datamodel](#config-group-datamodel) section. +Config groups can be created with preconfigured sets of attributes, service and subservice information, security +information and other parameters. The specific parameters that can be configured for a given config group are described +in the [Config group datamodel](#config-group-datamodel) section. ### Devices @@ -218,7 +218,7 @@ device preprovisioning). Device measures can have four different behaviors: an attribute in the device's entity, for which the IoT Agent will be regitered as Context Provider. The IoT Agent will return an immediate response to the Context Broker, and will be held responsible of contacting the device to perform the command itself using the device specific protocol. Special `status` and `info` attributes should be - update. For each command, its `name` and `type` must be provided. For further information, please refer to + update. For each command, its `name` and `type` must be provided. For further information, please refer to [Command execution](#command-execution) section. All of them have the same syntax, a list of objects with the following attributes: @@ -565,17 +565,18 @@ expression. In all cases the following data is available to all expressions: - `id`: device ID - `entity_name`: NGSI entity Name (principal) - `type`: NGSI entity type (principal) -- `service`: device service (`Fiware-Service`) +- `service`: device service (`Fiware-Service`) - `subservice`: device subservice (`Fiware-ServicePath`) - `staticAttributes`: static attributes defined in the device or config group Additionally, for attribute expressions (`expression`, `entity_name`), `entityNameExp` and metadata expressions (`expression`) the following is available in the **context** used to evalute: -- measures, as `` -- metadata (both for attribute measurement in the case of NGSI-v2 measurements and static attribute) are available in the **context** under the following convention: -`metadata..` or `metadata..` in a similar way of defined -for [Context Broker](https://github.com/telefonicaid/fiware-orion/blob/master/doc/manuals/orion-api.md#metadata-support) +- measures, as `` +- metadata (both for attribute measurement in the case of NGSI-v2 measurements and static attribute) are available in + the **context** under the following convention: `metadata..` or + `metadata..` in a similar way of defined for + [Context Broker](https://github.com/telefonicaid/fiware-orion/blob/master/doc/manuals/orion-api.md#metadata-support) ### Examples of JEXL expressions @@ -736,7 +737,8 @@ e.g.: } ``` -Note that there is no order into metadata structure and there is no warranty about which metadata attribute expression will be evaluated first. +Note that there is no order into metadata structure and there is no warranty about which metadata attribute expression +will be evaluated first. ## Measurement transformation @@ -1070,21 +1072,21 @@ As part of the device to entity mapping process, the IoT Agent creates and updat attribute called `TimeInstant`. This timestamp is represented as two different properties of the mapped entity: - An attribute `TimeInstant` is added to updated entities in the case of NGSI-v2, which captures as an ISO8601 - timestamp when the associated measurement was observed. With NGSI-LD, the Standard - `observedAt` property is used instead + timestamp when the associated measurement was observed. With NGSI-LD, the Standard `observedAt` property is used + instead -- With NGSI-v2, an attribute metadata named `TimeInstant` per active or lazy 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. +- With NGSI-v2, an attribute metadata named `TimeInstant` per active or lazy 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. If timestamp is not explicily defined when sending the measures through the IoT Agent (for further details on how to attach timestamp information to the measures, please refer to the particular IoT Agent implementation documentation), the arrival time on the server when receiving the measurement will be used to generate a `TimeInstant` for both the entity attribute and the attribute metadata. -This functionality can be turned on and off globaly through the use of the `timestamp` configuration flag or `IOTA_TIMESTAMP` -variable as well as `timestamp` flag in device or group provision (in this case, the device or group level flag takes -precedence over the global one). The default value is `true`. +This functionality can be turned on and off globaly through the use of the `timestamp` configuration flag or +`IOTA_TIMESTAMP` variable as well as `timestamp` flag in device or group provision (in this case, the device or group +level flag takes precedence over the global one). The default value is `true`. The `timestamp` configuration value used, according to the priority: @@ -1104,6 +1106,7 @@ the IoTA behaviour is described in the following table: | false | No | TimeInstant and metadata updated with server timestamp | | Not defined | Yes | TimeInstant and metadata updated with measure value | | Not defined | No | TimeInstant and metadata updated with server timestamp | + Some additional considerations to take into account: - If there is an attribute which maps a measure to `TimeInstant` attribute (after @@ -1724,7 +1727,7 @@ Config group is represented by a JSON object with the following fields: The following actions are available under the config group endpoint: -#### Retrieve config groups `GET /iot/services` +#### Retrieve config groups GET `/iot/groups` or `GET /iot/services` List all the config groups for the given `fiware-service` and `fiware-servicepath`. The config groups that match the `fiware-servicepath` are returned in any other case. @@ -1748,14 +1751,14 @@ Successful operations return `Content-Type` header with `application/json` value _**Response payload**_ -A JSON object with a services field that contains an array of services that match the request. See the +A JSON object with a groups or services field that contains an array of services that match the request. See the [config group datamodel](#service-group-datamodel) for more information. Example: ```json { - "services": [ + "groups": [ { "resource": "/deviceTest", "apikey": "801230BJKL23Y9090DSFL123HJK09H324HV8732", @@ -1792,7 +1795,7 @@ Example: } ``` -#### Create config group `POST /iot/services` +#### Create config group `POST /iot/groups` or `POST /iot/services` Creates a set of config groups for the given service and service path. The service and subservice information will taken from the headers, overwritting any preexisting values. @@ -1806,14 +1809,14 @@ _**Request headers**_ _**Request payload**_ -A JSON object with a `services` field. The value is an array of config groups objects to create. See the +A JSON object with a `groups` or `services` field. The value is an array of config groups objects to create. See the [config group datamodel](#service-group-datamodel) for more information. Example: ```json { - "services": [ + "groups": [ { "resource": "/deviceTest", "apikey": "801230BJKL23Y9090DSFL123HJK09H324HV8732", @@ -1847,11 +1850,11 @@ _**Response headers**_ Successful operations return `Content-Type` header with `application/json` value. -#### Modify config group `PUT /iot/services` +#### Modify config group `PUT /iot/groups` or `PUT /iot/services` -Modifies the information of a config group, identified by the `resource` and `apikey` query parameters. Takes a service -group body as the payload. The body does not have to be complete: for incomplete bodies, just the attributes included in -the JSON body will be updated. The rest of the attributes will remain unchanged. +Modifies the information of a config group, identified by the `resource` and `apikey` query parameters. Takes a +configuration/service group body as the payload. The body does not have to be complete: for incomplete bodies, just the +attributes included in the JSON body will be updated. The rest of the attributes will remain unchanged. _**Request query parameters**_ @@ -1887,7 +1890,7 @@ _**Response code**_ - 400 MISSING_HEADERS if any of the mandatory headers is not present. - 500 SERVER ERROR if there was any error not contemplated above.: -#### Remove config group `DELETE /iot/services` +#### Remove config group `DELETE /iot/groups` or `DELETE /iot/services` Removes a config group, identified by the `resource` and `apikey` query parameters. diff --git a/doc/deprecated.md b/doc/deprecated.md index 754bf9914..4fe3960de 100644 --- a/doc/deprecated.md +++ b/doc/deprecated.md @@ -26,6 +26,8 @@ A list of deprecated features and the version in which they were deprecated foll - Bidirectinal pluging (finally removed in 3.4.0) - appendMode configuration (`IOTA_APPEND_MODE` env var) (finally removed in 3.4.0) - `config.stats` section, and push-mode statistics. +- Services API routes (`/iot/services`) in favor of the `/iot/groups`. Both are still supported, but the former is + deprecated. The use of Node.js v14 is highly recommended. diff --git a/doc/devel/development.md b/doc/devel/development.md index 0c1d2243e..6824fad22 100644 --- a/doc/devel/development.md +++ b/doc/devel/development.md @@ -1831,9 +1831,9 @@ iotAgentLib.startServer(config, iotAgent, function (error) { ### Configuration management -For some IoT Agents, it will be useful to know what devices or config groups were registered in the Agent, or to do -some actions whenever a new device is registered. All this configuration and provisioning actions can be performed using -two mechanisms: the provisioning handlers and the provisioning API. +For some IoT Agents, it will be useful to know what devices or config groups were registered in the Agent, or to do some +actions whenever a new device is registered. All this configuration and provisioning actions can be performed using two +mechanisms: the provisioning handlers and the provisioning API. #### Provisioning handlers diff --git a/doc/getting-started.md b/doc/getting-started.md index 689ea189d..8973c5700 100644 --- a/doc/getting-started.md +++ b/doc/getting-started.md @@ -10,8 +10,8 @@ ## Introduction -In this guide we will be using the IoT Agent JSON (which is the reference IoTAgent using the IoTAgent Library) as an example to demonstrate how to provision config groups, devices -and how to receive measures from devices. +In this guide we will be using the IoT Agent JSON (which is the reference IoTAgent using the IoTAgent Library) as an +example to demonstrate how to provision config groups, devices and how to receive measures from devices. Be aware that every IoT Agent which uses the library is different, but the concepts for provisioning IoT devices remain the same regardless of protocol. @@ -52,8 +52,9 @@ config = { ``` In this case the context broker hostname is `orion` and is listening on port `1026`, the IoT Agent can be provisioned by -sending requests to port `4041` which is also the port used to receive NGSI requests. The IoT Agent is using the `iotagent` -database from a MongoDB instance at `localhost:27017` to store needed information (provisioned groups and devices, etc.). +sending requests to port `4041` which is also the port used to receive NGSI requests. The IoT Agent is using the +`iotagent` database from a MongoDB instance at `localhost:27017` to store needed information (provisioned groups and +devices, etc.). The remaining settings help to define the NGSI interactions - the IoT Agent will be using the `fiware-service=openiot` and `fiware-service-path=/`. The default `type`for each created entity is `Thing`, although this can be overridden as @@ -73,12 +74,12 @@ config group is shown below: ```bash curl -iX POST \ - 'http://localhost:4041/iot/services' \ + 'http://localhost:4041/iot/groups' \ -H 'Content-Type: application/json' \ -H 'fiware-service: openiot' \ -H 'fiware-servicepath: /' \ -d '{ - "services": [ + "groups": [ { "apikey": "4jggokgpepnvsb2uv4s40d59ov", "entity_type": "Device", @@ -95,8 +96,8 @@ In this case an `apiKey` for identifying devices has been created and all intera present this `apiKey` will be created as entities of `type=Device` rather than using the configuration default of `type=Thing`. -Additionally, the group has defined an attribute mapping for a measurement `t` to be mapped to `temperature` attribute when -receiving data from devices. +Additionally, the group has defined an attribute mapping for a measurement `t` to be mapped to `temperature` attribute +when receiving data from devices. The config group would usual hold additional attribute mappings, commands and common static attributes as well. @@ -132,12 +133,12 @@ curl -iX POST \ The device `motion001` has been provisioned to persist data to the Context Broker as an entity of `type=Motion` (instead of the default `type=Thing`). The destination entity is identified by the `entity_name` field, which is set to -`urn:ngsi-ld:Motion:001`. The device has a single attribute mapping for a measurement `c` to be mapped to `count` attribute, -additionally to one defined in the group mapping (`temperature`). The device also has a static attribute `refStore` -which is a relationship to the entity `urn:ngsi-ld:Store:001`. +`urn:ngsi-ld:Motion:001`. The device has a single attribute mapping for a measurement `c` to be mapped to `count` +attribute, additionally to one defined in the group mapping (`temperature`). The device also has a static attribute +`refStore` which is a relationship to the entity `urn:ngsi-ld:Store:001`. -This information is combined with the common config group information whenever a measurement is received at the IoT Agent -and used to create or update the relevant entity in the Context Broker. +This information is combined with the common config group information whenever a measurement is received at the IoT +Agent and used to create or update the relevant entity in the Context Broker. ## Receiving measures from devices @@ -163,8 +164,8 @@ The IoT Agent South port is listening to the path defined in the config group, a Because the `device_id` is also recognized, the provisioned device configuration is used and its settings are combined with the config group. -Mapping has been found to use the `c` measurement as `count` and the `t` measurement as `temperature` attributes values. The following -context entity is created in the context broker: +Mapping has been found to use the `c` measurement as `count` and the `t` measurement as `temperature` attributes values. +The following context entity is created in the context broker: ```json { @@ -179,10 +180,9 @@ context entity is created in the context broker: ### Receiving a measure from an anonymous Device When receiving a measure, it is not necessary to have the device provisioned. In this case, the IoT Agent will use the -config group configuration to create the device and the entity. This process is called "autoprovision" and it is enabled -by default in provisioned groups (for further information, review the [Autoprovision](api.md#autoprovision-configuration-autoprovision) -section in the API documentation). - +config group configuration to create the device and the entity. This process is called "autoprovision" and it is enabled +by default in provisioned groups (for further information, review the +[Autoprovision](api.md#autoprovision-configuration-autoprovision) section in the API documentation). Take as an example the following request from an anonymous device: diff --git a/doc/roadmap.md b/doc/roadmap.md index d8ccdd92e..3c99d5494 100644 --- a/doc/roadmap.md +++ b/doc/roadmap.md @@ -25,9 +25,9 @@ Disclaimer: The following list of features are planned to be addressed in the short term, and incorporated in a release of the product: -- cgroup literal in configuration groups management API ([#752](https://github.com/telefonicaid/iotagent-node-lib/issues/752)) - Add init and improve log traces ([#1452](https://github.com/telefonicaid/iotagent-node-lib/issues/1452)) -- Remove InMemory registry for Devices and Groups ([#1429](https://github.com/telefonicaid/iotagent-node-lib/issues/1429)) +- Remove InMemory registry for Devices and Groups + ([#1429](https://github.com/telefonicaid/iotagent-node-lib/issues/1429)) ### Medium term @@ -35,7 +35,8 @@ The following list of features are planned to be addressed in the medium term, t release(s) generated in the next 9 months after the next planned release: - Cache support ([#1467](https://github.com/telefonicaid/iotagent-node-lib/issues/1467)) -- MQTT per group advanced configuration (MQTT broker, topics) ([#1454](https://github.com/telefonicaid/iotagent-node-lib/issues/1454)) +- MQTT per group advanced configuration (MQTT broker, topics) + ([#1454](https://github.com/telefonicaid/iotagent-node-lib/issues/1454)) - Subscription based commands ([#1455](https://github.com/telefonicaid/iotagent-node-lib/issues/1455)) - Remove registration support ([#1456](https://github.com/telefonicaid/iotagent-node-lib/issues/1456)) @@ -45,9 +46,12 @@ The following list of features are proposals regarding the longer-term evolution development of these features has not yet been scheduled for a release in the near future. Please feel free to contact us if you wish to get involved in the implementation or influence the roadmap: -- Use the lightweight ingestion mechanism for connection oriented updates implemented in Context Broker (testing pending) ([#1457](https://github.com/telefonicaid/iotagent-node-lib/issues/1457)) -- Add support to other transport protocols (BacNET, Modbus, etc) ([#1458](https://github.com/telefonicaid/iotagent-node-lib/issues/1458)) -- Dynamic attribute generation (based on values array) ([#1459](https://github.com/telefonicaid/iotagent-node-lib/issues/1459)) +- Use the lightweight ingestion mechanism for connection oriented updates implemented in Context Broker (testing + pending) ([#1457](https://github.com/telefonicaid/iotagent-node-lib/issues/1457)) +- Add support to other transport protocols (BacNET, Modbus, etc) + ([#1458](https://github.com/telefonicaid/iotagent-node-lib/issues/1458)) +- Dynamic attribute generation (based on values array) + ([#1459](https://github.com/telefonicaid/iotagent-node-lib/issues/1459)) - Incremental introduccion of ECMAScript6 syntax (previous analysis of which sub-set of interesting aspect we want to take) @@ -55,7 +59,8 @@ us if you wish to get involved in the implementation or influence the roadmap: The following list contains all features that were in the roadmap and have already been implemented. -- Allow to add metadata to attributes from measures ([#1453](https://github.com/telefonicaid/iotagent-node-lib/issues/1453)) ([4.6.0](https://github.com/telefonicaid/iotagent-node-lib/releases/tag/4.0.0)) +- cgroup literal in configuration groups management API ([#752](https://github.com/telefonicaid/iotagent-node-lib/issues/752)) ([4.6.0](https://github.com/telefonicaid/iotagent-node-lib/releases/tag/4.6.0)) +- Allow to add metadata to attributes from measures ([#1453](https://github.com/telefonicaid/iotagent-node-lib/issues/1453)) ([4.6.0](https://github.com/telefonicaid/iotagent-node-lib/releases/tag/4.6.0)) - Refactor Append Mode & initial entity ([#1413](https://github.com/telefonicaid/iotagent-node-lib/issues/1413)) ([4.0.0](https://github.com/telefonicaid/iotagent-node-lib/releases/tag/4.0.0)) - Native support for NGSI-v2 and LD ingestion ([#1451](https://github.com/telefonicaid/iotagent-node-lib/issues/1451)) ([4.0.0](https://github.com/telefonicaid/iotagent-node-lib/releases/tag/4.0.0)) - Remove plugins structure (bidirectional plugin) ([#1413](https://github.com/telefonicaid/iotagent-node-lib/issues/1413)) ([4.0.0](https://github.com/telefonicaid/iotagent-node-lib/releases/tag/4.0.0)) @@ -64,14 +69,15 @@ The following list contains all features that were in the roadmap and have alrea - Improve command functionalities (binary data + expression + mapping) ([2.22.0](https://github.com/telefonicaid/iotagent-node-lib/releases/tag/2.22.0)) - Support for "delta" measures (i.e. "temperature _increased_ in 5 degress" instead of "temperature _is_ 25") - Allow to handle binary messages ([iota-ul#530](https://github.com/telefonicaid/iotagent-ul/issues/530)) -- Removal support for NGSIv1 ([#966](https://github.com/telefonicaid/iotagent-node-lib/issues/966)) ([2.18.0](https://github.com/telefonicaid/iotagent-node-lib/releases/tag/2.18.0)) +- Removal support for NGSIv1 ([#966](https://github.com/telefonicaid/iotagent-node-lib/issues/966)) + ([2.18.0](https://github.com/telefonicaid/iotagent-node-lib/releases/tag/2.18.0)) - Selectively ignore measure in the southbound interface ([iotagent-json#416](https://github.com/telefonicaid/iotagent-json/issues/416), [iotagent-ul#372](https://github.com/telefonicaid/iotagent-ul/issues/372)) ([2.13.0](https://github.com/telefonicaid/iotagent-node-lib/releases/tag/2.13.0)) -- JEXL support in expressions ([#801](https://github.com/telefonicaid/iotagent-node-lib/issues/801), - [#687](https://github.com/telefonicaid/iotagent-node-lib/issues/687), - [#868](https://github.com/telefonicaid/iotagent-node-lib/issues/868)) +- JEXL support in expressions ([#801](https://github.com/telefonicaid/iotagent-node-lib/issues/801), + [#687](https://github.com/telefonicaid/iotagent-node-lib/issues/687), + [#868](https://github.com/telefonicaid/iotagent-node-lib/issues/868)) ([2.13.0](https://github.com/telefonicaid/iotagent-node-lib/releases/tag/2.13.0)) - Add MongoDB authentication support ([#844](https://github.com/telefonicaid/iotagent-node-lib/issues/844)) ([2.12.0](https://github.com/telefonicaid/iotagent-node-lib/releases/tag/2.12.0)) diff --git a/lib/constants.js b/lib/constants.js index d1d2891e5..ab0ea3d00 100644 --- a/lib/constants.js +++ b/lib/constants.js @@ -88,5 +88,9 @@ module.exports = { ATTRIBUTE_DEFAULT, DATETIME_DEFAULT, + CONFIGGROUP_TERM: 'groups', + CONFIGGROUP_API_PATH: '/iot/groups', + CONFIG_GROUP_BODYELEMENT: '', + getInitialValueForType }; diff --git a/lib/fiware-iotagent-lib.js b/lib/fiware-iotagent-lib.js index 1c057e819..1117178d0 100644 --- a/lib/fiware-iotagent-lib.js +++ b/lib/fiware-iotagent-lib.js @@ -47,17 +47,20 @@ const context = { /* eslint-disable-next-line no-unused-vars */ function activateStatLogs(newConfig, callback) { - async.series([ - apply(statsRegistry.globalLoad, { - deviceCreationRequests: 0, - deviceRemovalRequests: 0, - measureRequests: 0, - raiseAlarm: 0, - releaseAlarm: 0, - updateEntityRequestsOk: 0, - updateEntityRequestsError: 0 - }) - ], callback); + async.series( + [ + apply(statsRegistry.globalLoad, { + deviceCreationRequests: 0, + deviceRemovalRequests: 0, + measureRequests: 0, + raiseAlarm: 0, + releaseAlarm: 0, + updateEntityRequestsOk: 0, + updateEntityRequestsError: 0 + }) + ], + callback + ); } /** @@ -315,7 +318,9 @@ exports.setCommandHandler = contextServer.setCommandHandler; exports.setMergePatchHandler = contextServer.setMergePatchHandler; exports.setDataQueryHandler = contextServer.setQueryHandler; exports.setConfigurationHandler = contextServer.setConfigurationHandler; +exports.setGroupConfigurationHandler = contextServer.setGroupConfigurationHandler; exports.setRemoveConfigurationHandler = contextServer.setRemoveConfigurationHandler; +exports.setRemoveGroupConfigurationHandler = contextServer.setRemoveGroupConfigurationHandler; exports.setProvisioningHandler = contextServer.setProvisioningHandler; exports.setUpdatingHandler = contextServer.setUpdatingHandler; exports.setRemoveDeviceHandler = contextServer.setRemoveDeviceHandler; diff --git a/lib/services/common/iotManagerService.js b/lib/services/common/iotManagerService.js index b56fe9858..9be3fc426 100644 --- a/lib/services/common/iotManagerService.js +++ b/lib/services/common/iotManagerService.js @@ -68,7 +68,7 @@ function register(callback) { } function getServices(callback) { - config.getGroupRegistry().list(null, null, null, function (error, results) { + config.getGroupRegistry().list(null, null, null, null, function (error, results) { if (error) { callback(error); } else { diff --git a/lib/services/groups/groupRegistryMemory.js b/lib/services/groups/groupRegistryMemory.js index 5d0173175..492966246 100644 --- a/lib/services/groups/groupRegistryMemory.js +++ b/lib/services/groups/groupRegistryMemory.js @@ -28,6 +28,7 @@ const logger = require('logops'); const intoTrans = require('../common/domain').intoTrans; const errors = require('../../errors'); const _ = require('underscore'); +const constants = require('../../constants'); const context = { op: 'IoTAgentNGSI.InMemoryGroupRegister' }; @@ -95,7 +96,7 @@ function getConfigurationsByService(service) { * @param {Number} limit Maximum number of entries to return. * @param {Number} offset Number of entries to skip for pagination. */ -function listGroups(service, limit, offset, callback) { +function listGroups(endpoint, service, limit, offset, callback) { const result = []; let skipped = 0; const filteredGroups = getConfigurationsByService(service); @@ -113,11 +114,17 @@ function listGroups(service, limit, offset, callback) { } } } - - callback(null, { - count: filteredGroups.length, - services: result - }); + if (endpoint != null && endpoint.toLowerCase().includes(constants.CONFIGGROUP_TERM.toLowerCase())) { + const configGroupsObject = {}; + configGroupsObject.count = filteredGroups.length; + configGroupsObject[constants.CONFIGGROUP_TERM] = result; + callback(null, configGroupsObject); + } else { + callback(null, { + count: filteredGroups.length, + services: result + }); + } } function init(newConfig, callback) { @@ -130,7 +137,7 @@ function clear(callback) { callback(); } -function find(service, subservice, callback) { +function find(endpoint, service, subservice, callback) { const result = []; for (const i in registeredGroups) { @@ -145,10 +152,17 @@ function find(service, subservice, callback) { if (result.length > 0) { logger.debug(context, 'groups found %j', result); - return callback(null, { - count: result.length, - services: result - }); + if (endpoint.toLowerCase().includes(constants.CONFIGGROUP_TERM.toLowerCase())) { + const configGroupsObject = {}; + configGroupsObject.count = result.length; + configGroupsObject[constants.CONFIGGROUP_TERM] = result; + return callback(null, configGroupsObject); + } else { + return callback(null, { + count: result.length, + services: result + }); + } } else { return callback(new errors.DeviceGroupNotFound(service, subservice)); } diff --git a/lib/services/groups/groupRegistryMongoDB.js b/lib/services/groups/groupRegistryMongoDB.js index 740a3fad7..93ca4f677 100644 --- a/lib/services/groups/groupRegistryMongoDB.js +++ b/lib/services/groups/groupRegistryMongoDB.js @@ -129,7 +129,7 @@ function createGroup(group, callback) { * @param {Number} limit Maximum number of entries to return. * @param {Number} offset Number of entries to skip for pagination. */ -function listGroups(service, limit, offset, callback) { +function listGroups(endpoint, service, limit, offset, callback) { const condition = {}; function toObjectFn(obj) { @@ -153,10 +153,17 @@ function listGroups(service, limit, offset, callback) { async.series( [query.exec.bind(query), Group.model.countDocuments.bind(Group.model, condition)], function (error, results) { - callback(error, { - count: results[1], - services: results[0].map(toObjectFn) - }); + if (endpoint.toLowerCase().includes(constants.CONFIGGROUP_TERM.toLowerCase())) { + const configGroupsObject = {}; + configGroupsObject.count = results[1]; + configGroupsObject[constants.CONFIGGROUP_TERM] = results[0].map(toObjectFn); + callback(error, configGroupsObject); + } else { + callback(error, { + count: results[1], + services: results[0].map(toObjectFn) + }); + } } ); } @@ -192,7 +199,7 @@ function getById(id, callback) { * @param {String} subservice Subservice used to filter the groups. * @param {Function} callback The callback function. */ -function find(service, subservice, callback) { +function find(endpoint, service, subservice, callback) { const condition = {}; function toObjectFn(obj) { @@ -207,10 +214,17 @@ function find(service, subservice, callback) { async.series( [query.exec.bind(query), Group.model.countDocuments.bind(Group.model, condition)], function (error, results) { - callback(error, { - count: results[1], - services: results[0].map(toObjectFn) - }); + if (endpoint.toLowerCase().includes(constants.CONFIGGROUP_TERM.toLowerCase())) { + const configGroupsObject = {}; + configGroupsObject.count = results[1]; + configGroupsObject[constants.CONFIGGROUP_TERM] = results[0].map(toObjectFn); + callback(error, configGroupsObject); + } else { + callback(error, { + count: results[1], + services: results[0].map(toObjectFn) + }); + } } ); } diff --git a/lib/services/groups/groupService.js b/lib/services/groups/groupService.js index 1faa6ccec..9fa77bee4 100644 --- a/lib/services/groups/groupService.js +++ b/lib/services/groups/groupService.js @@ -29,6 +29,7 @@ const deviceService = require('../devices/deviceService'); const logger = require('logops'); const config = require('../../commonConfig'); const errors = require('../../errors'); +const constants = require('../../constants'); const context = { op: 'IoTAgentNGSI.DeviceGroupService' }; @@ -94,12 +95,28 @@ function createGroup(groupSet, callback) { const insertions = []; const insertedGroups = []; - logger.debug(context, 'Creating new set of %d services', groupSet.services.length); + /* eslint-disable-next-line no-prototype-builtins */ + if (groupSet.hasOwnProperty(constants.CONFIGGROUP_TERM)) { + logger.debug( + context, + 'Creating new set of %d %s', + groupSet[constants.CONFIGGROUP_TERM].length, + constants.CONFIGGROUP_TERM + ); + + for (let i = 0; i < groupSet[constants.CONFIGGROUP_TERM].length; i++) { + insertions.push(async.apply(validateGroup, groupSet[constants.CONFIGGROUP_TERM][i])); + insertions.push(async.apply(config.getGroupRegistry().create, groupSet[constants.CONFIGGROUP_TERM][i])); + insertedGroups.push(groupSet[constants.CONFIGGROUP_TERM][i]); + } + } else { + logger.debug(context, 'Creating new set of %d services', groupSet.services.length); - for (let i = 0; i < groupSet.services.length; i++) { - insertions.push(async.apply(validateGroup, groupSet.services[i])); - insertions.push(async.apply(config.getGroupRegistry().create, groupSet.services[i])); - insertedGroups.push(groupSet.services[i]); + for (let i = 0; i < groupSet.services.length; i++) { + insertions.push(async.apply(validateGroup, groupSet.services[i])); + insertions.push(async.apply(config.getGroupRegistry().create, groupSet.services[i])); + insertedGroups.push(groupSet.services[i]); + } } insertions.push(iotManagerService.register); @@ -114,12 +131,13 @@ function createGroup(groupSet, callback) { * @param {Number} limit Maximum number of entries to return. * @param {Number} offset Number of entries to skip for pagination. */ -function listGroups(service, limit, offset, callback) { +function listGroups(endpoint, service, limit, offset, callback) { + endpoint = endpoint.toLowerCase(); if (!callback) { callback = service; } - config.getGroupRegistry().list(service, limit, offset, callback); + config.getGroupRegistry().list(endpoint, service, limit, offset, callback); } function checkServiceIdentity(service, subservice, deviceGroup, callback) { @@ -231,11 +249,12 @@ function update(service, subservice, resource, apikey, body, callback) { * @param {String} subservice Group subservice name. * @param {String} type Group type (optional). */ -function find(service, subservice, type, callback) { +function find(endpoint, service, subservice, type, callback) { + endpoint = endpoint.toLowerCase(); if (type) { config.getGroupRegistry().findType(service, subservice, type, callback); } else { - config.getGroupRegistry().find(service, subservice, callback); + config.getGroupRegistry().find(endpoint, service, subservice, callback); } } @@ -302,7 +321,7 @@ function getEffectiveApiKey(service, subservice, type, callback) { } } - find(service, subservice, type, handleFindGroup); + find(constants.CONFIGGROUP_TERM, service, subservice, type, handleFindGroup); } exports.create = intoTrans(context, createGroup); diff --git a/lib/services/ngsi/entities-NGSI-v2.js b/lib/services/ngsi/entities-NGSI-v2.js index 4a0d89460..b60aecff6 100644 --- a/lib/services/ngsi/entities-NGSI-v2.js +++ b/lib/services/ngsi/entities-NGSI-v2.js @@ -311,6 +311,7 @@ function sendUpdateValueNgsi2(entityName, originMeasures, originTypeInformation, const currentIsoDate = new Date().toISOString(); const currentMoment = moment(currentIsoDate); //Managing timestamp (mustInsertTimeInstant flag to decide if we should insert Timestamp later on) + const mustInsertTimeInstant = originTypeInformation.timestamp !== undefined ? originTypeInformation.timestamp : false; diff --git a/lib/services/northBound/deviceGroupAdministrationServer.js b/lib/services/northBound/deviceGroupAdministrationServer.js index 40829394f..dda028354 100644 --- a/lib/services/northBound/deviceGroupAdministrationServer.js +++ b/lib/services/northBound/deviceGroupAdministrationServer.js @@ -29,7 +29,8 @@ const restUtils = require('./restUtils'); const groupService = require('./../groups/groupService'); const async = require('async'); const apply = async.apply; -const templateGroup = require('../../templates/deviceGroup.json'); +const templateGroup = require('../../templates/deviceConfigGroup.json'); +const constants = require('../../constants'); let configurationHandler; let removeConfigurationHandler; let configurationMiddleware = []; @@ -37,6 +38,11 @@ const _ = require('underscore'); const mandatoryHeaders = ['fiware-service', 'fiware-servicepath']; const mandatoryParameters = ['resource', 'apikey']; +// Create new templeate for configuration groups replacing services by CONFIGGROUP_TERM +const templateConfigGroup = JSON.parse(JSON.stringify(templateGroup)); +templateConfigGroup.properties[constants.CONFIGGROUP_TERM] = templateConfigGroup.properties.services; +delete templateConfigGroup.properties.services; + const apiToInternal = { entity_type: 'type', internal_attributes: 'internalAttributes', @@ -70,6 +76,8 @@ function applyConfigurationHandler(newConfiguration, callback) { if (configurationHandler && newConfiguration) { if (newConfiguration.services) { async.map(newConfiguration.services, configurationHandler, callback); + } else if (newConfiguration[constants.CONFIGGROUP_TERM]) { + async.map(newConfiguration[constants.CONFIGGROUP_TERM], configurationHandler, callback); } else { configurationHandler(newConfiguration, callback); } @@ -118,12 +126,19 @@ function applyConfigurationMiddlewares(newConfiguration, callback) { * @param {Function} next Invokes the next middleware in the chain. */ function handleCreateDeviceGroup(req, res, next) { - for (let i = 0; i < req.body.services.length; i++) { + let lenCount = 0; + if (req.body.services) { + lenCount = req.body.services.length; + } else { + lenCount = req.body[constants.CONFIGGROUP_TERM].length; + req.body.services = req.body[constants.CONFIGGROUP_TERM]; + } + + for (let i = 0; i < lenCount; i++) { req.body.services[i] = applyMap(apiToInternal, req.body.services[i]); req.body.services[i].service = req.headers['fiware-service']; req.body.services[i].subservice = req.headers['fiware-servicepath']; } - async.series( [ apply(groupService.create, req.body), @@ -158,6 +173,10 @@ function handleListDeviceGroups(req, res, next) { if (group.services) { translatedGroup.services = group.services.map(applyMap.bind(null, internalToApi)); + } else if (group[constants.CONFIGGROUP_TERM]) { + translatedGroup[constants.CONFIGGROUP_TERM] = group[constants.CONFIGGROUP_TERM].map( + applyMap.bind(null, internalToApi) + ); } else { translatedGroup = applyMap(internalToApi, group); } @@ -167,9 +186,9 @@ function handleListDeviceGroups(req, res, next) { }; if (req.headers['fiware-servicepath'] === '/*') { - groupService.list(req.headers['fiware-service'], req.query.limit, req.query.offset, listHandler); + groupService.list(req.url, req.headers['fiware-service'], req.query.limit, req.query.offset, listHandler); } else { - groupService.find(req.headers['fiware-service'], req.headers['fiware-servicepath'], null, listHandler); + groupService.find(req.url, req.headers['fiware-service'], req.headers['fiware-servicepath'], null, listHandler); } } @@ -288,6 +307,35 @@ function loadContextRoutes(router) { ); } +function loadConfigContextRoutes(router) { + router.post( + constants.CONFIGGROUP_API_PATH, + restUtils.checkRequestAttributes('headers', mandatoryHeaders), + restUtils.checkBody(templateConfigGroup), + handleCreateDeviceGroup + ); + + router.get( + constants.CONFIGGROUP_API_PATH, + restUtils.checkRequestAttributes('headers', mandatoryHeaders), + handleListDeviceGroups + ); + + router.put( + constants.CONFIGGROUP_API_PATH, + restUtils.checkRequestAttributes('headers', mandatoryHeaders), + restUtils.checkRequestAttributes('query', mandatoryParameters), + handleModifyDeviceGroups + ); + + router.delete( + constants.CONFIGGROUP_API_PATH, + restUtils.checkRequestAttributes('headers', mandatoryHeaders), + restUtils.checkRequestAttributes('query', mandatoryParameters), + handleDeleteDeviceGroups + ); +} + function setConfigurationHandler(newHandler) { configurationHandler = newHandler; } @@ -307,6 +355,7 @@ function clear(callback) { } exports.loadContextRoutes = loadContextRoutes; +exports.loadConfigContextRoutes = loadConfigContextRoutes; exports.setConfigurationHandler = setConfigurationHandler; exports.setRemoveConfigurationHandler = setRemoveConfigurationHandler; exports.addConfigurationProvisionMiddleware = addConfigurationProvisionMiddleware; diff --git a/lib/services/northBound/northboundServer.js b/lib/services/northBound/northboundServer.js index c5f2d8b7d..9378e02e0 100644 --- a/lib/services/northBound/northboundServer.js +++ b/lib/services/northBound/northboundServer.js @@ -90,7 +90,7 @@ function start(config, callback) { contextServer.loadContextRoutes(northboundServer.router); deviceProvisioning.loadContextRoutes(northboundServer.router); groupProvisioning.loadContextRoutes(northboundServer.router); - + groupProvisioning.loadConfigContextRoutes(northboundServer.router); northboundServer.app.use(middlewares.handleError); northboundServer.server = http.createServer(northboundServer.app); diff --git a/lib/templates/deviceGroup.json b/lib/templates/deviceConfigGroup.json similarity index 100% rename from lib/templates/deviceGroup.json rename to lib/templates/deviceConfigGroup.json diff --git a/test/unit/examples/groupProvisioningRequests/multipleConfigGroupsCreation.json b/test/unit/examples/groupProvisioningRequests/multipleConfigGroupsCreation.json new file mode 100644 index 000000000..64b7748e7 --- /dev/null +++ b/test/unit/examples/groupProvisioningRequests/multipleConfigGroupsCreation.json @@ -0,0 +1,44 @@ +{ + "groups": [ + { + "resource": "/deviceTest", + "apikey": "801230BJKL23Y9090DSFL123HJK09H324HV8732", + "entity_type": "Light", + "trust": "8970A9078A803H3BL98PINEQRW8342HBAMS", + "cbHost": "http://unexistentHost:1026", + "commands": [ + { + "name": "wheel1", + "type": "Wheel" + } + ], + "lazy": [ + { + "name": "luminescence", + "type": "Lumens" + } + ], + "attributes": [ + { + "name": "status", + "type": "Boolean" + } + ] + }, + { + "resource": "/deviceTest", + "apikey": "23HJK3Y9090DSFL173209HV8801232", + "entity_type": "Termometer", + "trust": "BL9803H3QRW8342HBAMS8A8", + "cbHost": "http://unexistentHost:1026", + "commands": [ + { + "name": "temperature", + "type": "degrees" + } + ], + "lazy": [], + "attributes": [] + } + ] +} diff --git a/test/unit/examples/groupProvisioningRequests/provisionDuplicateConfigGroup.json b/test/unit/examples/groupProvisioningRequests/provisionDuplicateConfigGroup.json new file mode 100644 index 000000000..d87fdbc7e --- /dev/null +++ b/test/unit/examples/groupProvisioningRequests/provisionDuplicateConfigGroup.json @@ -0,0 +1,35 @@ +{ + "groups": [ + { + "resource": "/deviceDuplicateGroup", + "apikey": "JK09H3L12K09H3L123HJK0732L123HJK38K09H3", + "entity_type": "CommandMachinery", + "trust": "8970A9078A803H3BL98PINEQRW8342HBAMS", + "commands": [ + { + "name": "wheel1", + "type": "Wheel" + } + ], + "lazy": [ + { + "name": "luminescence", + "type": "Lumens" + } + ], + "attributes": [ + { + "name": "status", + "type": "Boolean" + } + ], + "static_attributes": [ + { + "name": "bootstrapServer", + "type": "Address", + "value": "127.0.0.1" + } + ] + } + ] +} diff --git a/test/unit/examples/groupProvisioningRequests/provisionFullConfigGroup.json b/test/unit/examples/groupProvisioningRequests/provisionFullConfigGroup.json new file mode 100644 index 000000000..5764460b5 --- /dev/null +++ b/test/unit/examples/groupProvisioningRequests/provisionFullConfigGroup.json @@ -0,0 +1,36 @@ +{ + "groups": [ + { + "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" + } + ], + "static_attributes": [ + { + "name": "bootstrapServer", + "type": "Address", + "value": "127.0.0.1" + } + ] + } + ] +} diff --git a/test/unit/examples/groupProvisioningRequests/provisionFullConfigGroupAlternate.json b/test/unit/examples/groupProvisioningRequests/provisionFullConfigGroupAlternate.json new file mode 100644 index 000000000..530fa0632 --- /dev/null +++ b/test/unit/examples/groupProvisioningRequests/provisionFullConfigGroupAlternate.json @@ -0,0 +1,36 @@ +{ + "groups": [ + { + "resource": "/deviceTest", + "apikey": "754KL23Y9090DSFL123HSFL12380KL23Y2", + "entity_type": "AnotherMachine", + "trust": "8970A9078A803H3BL98PINEQRW8342HBAMS", + "cbHost": "http://unexistentHost:1026", + "commands": [ + { + "name": "wheel1", + "type": "Wheel" + } + ], + "lazy": [ + { + "name": "luminescence", + "type": "Lumens" + } + ], + "attributes": [ + { + "name": "status", + "type": "Boolean" + } + ], + "static_attributes": [ + { + "name": "bootstrapServer", + "type": "Address", + "value": "127.0.0.1" + } + ] + } + ] +} diff --git a/test/unit/general/deviceService-test.js b/test/unit/general/deviceService-test.js index 9b4fb40e8..8078bae31 100644 --- a/test/unit/general/deviceService-test.js +++ b/test/unit/general/deviceService-test.js @@ -153,6 +153,36 @@ const groupCreation = { 'fiware-servicepath': '/testingPath' } }; + +const configGroupCreation = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/groups', + method: 'POST', + json: { + groups: [ + { + resource: '', + apikey: '801230BJKL23Y9090DSFL123HJK09H324HV8732', + entity_type: 'TheLightType', + trust: '8970A9078A803H3BL98PINEQRW8342HBAMS', + cbHost: 'http://192.168.1.1: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', @@ -180,6 +210,7 @@ describe('NGSI-v2 - Device Service: utils', function () { async.series([iotAgentLib.clearAll, iotAgentLib.deactivate], done); }); + //FIXME: this test will be removed if at the end /iot/services API (now Deprecated) is removed describe('When an existing device tries to be retrieved with retrieveOrCreate()', function () { beforeEach(function (done) { contextBrokerMock = nock('http://192.168.1.1:1026') @@ -213,6 +244,41 @@ describe('NGSI-v2 - Device Service: utils', function () { }); }); + describe('When an existing device tries to be retrieved with retrieveOrCreate()', function () { + beforeEach(function (done) { + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'testservice') + .matchHeader('fiware-servicepath', '/testingPath') + .post('/v2/registrations') + .reply(201, null, { Location: '/v2/registrations/6319a7f5254b05844116584d' }); + + contextBrokerMock + .matchHeader('fiware-service', 'testservice') + .matchHeader('fiware-servicepath', '/testingPath') + .post('/v2/entities?options=upsert') + .reply(204); + + async.series( + [request.bind(request, configGroupCreation), 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(); + }); + }); + }); + + //FIXME: this test will be removed if at the end /iot/services API (now Deprecated) is removed + describe('When an unexisting device tries to be retrieved for an existing APIKey', function () { beforeEach(function (done) { contextBrokerMock = nock('http://192.168.1.1:1026') @@ -249,6 +315,42 @@ describe('NGSI-v2 - Device Service: utils', function () { }); }); + describe('When an unexisting device tries to be retrieved for an existing APIKey', function () { + beforeEach(function (done) { + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'testservice') + .matchHeader('fiware-servicepath', '/testingPath') + .post('/v2/registrations') + .reply(201, null, { Location: '/v2/registrations/6319a7f5254b05844116584d' }); + + contextBrokerMock + .matchHeader('fiware-service', 'testservice') + .matchHeader('fiware-servicepath', '/testingPath') + .post('/v2/entities?options=upsert') + .reply(204); + + async.series([request.bind(request, configGroupCreation)], 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( diff --git a/test/unit/mongodb/mongodb-configGroup-registry-test.js b/test/unit/mongodb/mongodb-configGroup-registry-test.js new file mode 100644 index 000000000..26d1f3d84 --- /dev/null +++ b/test/unit/mongodb/mongodb-configGroup-registry-test.js @@ -0,0 +1,452 @@ +/* + * Copyright 2015 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 _ = require('underscore'); +const utils = require('../../tools/utils'); +const request = utils.request; +const async = require('async'); +const should = require('should'); +const iotAgentConfig = { + logLevel: 'FATAL', + contextBroker: { + host: '192.168.1.1', + port: '1026' + }, + server: { + name: 'testAgent', + port: 4041, + host: 'localhost', + baseRoot: '/' + }, + types: {}, + deviceRegistry: { + type: 'mongodb' + }, + mongodb: { + host: 'localhost', + port: '27017', + db: 'iotagent' + }, + service: 'smartgondor', + subservice: 'gardens', + providerUrl: 'http://smartgondor.com', + deviceRegistrationDuration: 'P1M' +}; +const mongo = require('mongodb').MongoClient; +const mongoUtils = require('./mongoDBUtils'); +const optionsCreation = { + url: 'http://localhost:4041/iot/groups', + method: 'POST', + json: { + groups: [ + { + resource: '/deviceTest', + apikey: '801230BJKL23Y9090DSFL123HJK09H324HV8732', + entity_type: 'Light', + trust: '8970A9078A803H3BL98PINEQRW8342HBAMS', + cbHost: 'http://unexistentHost:1026', + commands: [ + { + name: 'wheel1', + type: 'Wheel' + } + ], + lazy: [ + { + name: 'luminescence', + type: 'Lumens' + } + ], + attributes: [ + { + name: 'status', + type: 'Boolean' + } + ], + internal_attributes: [ + { + customField: 'customValue' + } + ] + } + ] + }, + headers: { + 'fiware-service': 'testservice', + 'fiware-servicepath': '/testingPath' + } +}; +const optionsDelete = { + url: 'http://localhost:4041/iot/groups', + method: 'DELETE', + json: {}, + headers: { + 'fiware-service': 'testservice', + 'fiware-servicepath': '/testingPath' + }, + qs: { + resource: '/deviceTest', + apikey: '801230BJKL23Y9090DSFL123HJK09H324HV8732' + } +}; +const optionsList = { + url: 'http://localhost:4041/iot/groups', + method: 'GET', + json: {}, + headers: { + 'fiware-service': 'testservice', + 'fiware-servicepath': '/*' + } +}; +const optionsUpdate = { + url: 'http://localhost:4041/iot/groups', + method: 'PUT', + json: { + apikey: '801230BJKL23Y9090DSFL123HJK09H324HV8732', + trust: '8970A9078A803H3BL98PINEQRW8342HBAMS', + cbHost: 'http://anotherUnexistentHost:1026', + commands: [ + { + name: 'wheel1', + type: 'Wheel' + } + ], + lazy: [ + { + name: 'luminescence', + type: 'Lumens' + } + ], + attributes: [ + { + name: 'status', + type: 'Boolean' + } + ], + static_attributes: [ + { + name: 'bootstrapServer', + type: 'Address', + value: '127.0.0.1' + } + ] + }, + headers: { + 'fiware-service': 'testservice', + 'fiware-servicepath': '/testingPath' + }, + qs: { + resource: '/deviceTest', + apikey: '801230BJKL23Y9090DSFL123HJK09H324HV8732' + } +}; +const optionsGet = { + url: 'http://localhost:4041/iot/groups', + method: 'GET', + json: {}, + headers: { + 'fiware-service': 'testservice', + 'fiware-servicepath': '/testingPath' + } +}; +let iotAgentDb; + +describe('MongoDB Group Registry test', function () { + beforeEach(function (done) { + mongoUtils.cleanDbs(function () { + iotAgentLib.activate(iotAgentConfig, function () { + mongo.connect('mongodb://localhost:27017/iotagent', { useNewUrlParser: true }, function (err, db) { + iotAgentDb = db; + done(); + }); + }); + }); + }); + + afterEach(function (done) { + iotAgentLib.deactivate(function () { + iotAgentDb.close(function (error) { + mongoUtils.cleanDbs(done); + }); + }); + }); + describe('When a new device group creation request arrives', function () { + it('should store it in the DB', function (done) { + request(optionsCreation, function (error, response, body) { + iotAgentDb + .db() + .collection('groups') + .find({}) + .toArray(function (err, docs) { + should.not.exist(err); + should.exist(docs); + should.exist(docs.length); + docs.length.should.equal(1); + should.exist(docs[0].type); + should.exist(docs[0].internalAttributes); + should.exist(docs[0].attributes); + should.exist(docs[0].apikey); + docs[0].type.should.equal('Light'); + docs[0].apikey.should.equal('801230BJKL23Y9090DSFL123HJK09H324HV8732'); + docs[0].internalAttributes.length.should.equal(1); + docs[0].internalAttributes[0].customField.should.equal('customValue'); + docs[0].attributes.length.should.equal(1); + docs[0].attributes[0].name.should.equal('status'); + done(); + }); + }); + }); + it('should store the service information from the headers into the DB', function (done) { + request(optionsCreation, function (error, response, body) { + iotAgentDb + .db() + .collection('groups') + .find({}) + .toArray(function (err, docs) { + should.not.exist(err); + should.exist(docs[0].service); + should.exist(docs[0].subservice); + docs[0].service.should.equal('testservice'); + docs[0].subservice.should.equal('/testingPath'); + done(); + }); + }); + }); + }); + + describe('When a new device group creation request arrives with an existant (apikey, resource) pair', function () { + it('should return a DUPLICATE_GROUP error', function (done) { + request(optionsCreation, function (error, response, body) { + request(optionsCreation, function (error, response, body) { + response.statusCode.should.equal(409); + body.name.should.equal('DUPLICATE_GROUP'); + done(); + }); + }); + }); + }); + + describe('When a device group removal request arrives', function () { + beforeEach(function (done) { + request(optionsCreation, done); + }); + + it('should remove it from the database', function (done) { + request(optionsDelete, function (error, response, body) { + iotAgentDb + .db() + .collection('groups') + .find({}) + .toArray(function (err, docs) { + should.not.exist(err); + should.exist(docs); + should.exist(docs.length); + docs.length.should.equal(0); + done(); + }); + }); + }); + + it('should return a 204 OK statusCode', function (done) { + request(optionsDelete, function (error, response, body) { + response.statusCode.should.equal(204); + done(); + }); + }); + }); + + describe('When a device group update request arrives', function () { + beforeEach(function (done) { + request(optionsCreation, done); + }); + + it('should update the values in the database', function (done) { + request(optionsUpdate, function (error, response, body) { + iotAgentDb + .db() + .collection('groups') + .find({}) + .toArray(function (err, docs) { + should.not.exist(err); + should.exist(docs); + should.exist(docs[0].cbHost); + docs[0].cbHost.should.equal('http://anotherUnexistentHost:1026'); + should.exist(docs[0].staticAttributes); + docs[0].staticAttributes.length.should.equal(1); + done(); + }); + }); + }); + }); + + describe('When a multiple device group creation arrives', function () { + const optionsMultipleCreation = _.clone(optionsCreation); + + beforeEach(function (done) { + optionsMultipleCreation.json = utils.readExampleFile( + './test/unit/examples/groupProvisioningRequests/multipleConfigGroupsCreation.json' + ); + + done(); + }); + + it('should create the values in the database', function (done) { + request(optionsMultipleCreation, function (error, response, body) { + iotAgentDb + .db() + .collection('groups') + .find({}) + .toArray(function (err, docs) { + should.not.exist(err); + should.exist(docs); + docs.length.should.equal(2); + done(); + }); + }); + }); + }); + + describe('When a device group listing request arrives', function () { + beforeEach(function (done) { + const optionsCreation1 = _.clone(optionsCreation); + const optionsCreation2 = _.clone(optionsCreation); + const optionsCreation3 = _.clone(optionsCreation); + + optionsCreation2.json = { groups: [] }; + optionsCreation3.json = { groups: [] }; + + optionsCreation2.json.groups[0] = _.clone(optionsCreation.json.groups[0]); + optionsCreation3.json.groups[0] = _.clone(optionsCreation.json.groups[0]); + + optionsCreation2.json.groups[0].apikey = 'qwertyuiop'; + optionsCreation3.json.groups[0].apikey = 'lkjhgfds'; + + async.series( + [ + async.apply(request, optionsCreation1), + async.apply(request, optionsCreation2), + async.apply(request, optionsCreation3) + ], + done + ); + }); + + it('should return all the configured device groups from the database', function (done) { + request(optionsList, function (error, response, body) { + body.count.should.equal(3); + done(); + }); + }); + }); + + describe('When a device group listing arrives with a limit', function () { + const optionsConstrained = { + url: 'http://localhost:4041/iot/groups', + method: 'GET', + qs: { + limit: 3, + offset: 2 + }, + json: {}, + headers: { + 'fiware-service': 'testservice', + 'fiware-servicepath': '/*' + } + }; + + beforeEach(function (done) { + const optionsCreationList = []; + const creationFns = []; + + for (let i = 0; i < 10; i++) { + optionsCreationList[i] = _.clone(optionsCreation); + optionsCreationList[i].json = { groups: [] }; + optionsCreationList[i].json.groups[0] = _.clone(optionsCreation.json.groups[0]); + optionsCreationList[i].json.groups[0].apikey = 'qwertyuiop' + i; + creationFns.push(async.apply(request, optionsCreationList[i])); + } + + async.series(creationFns, done); + }); + + it('should return the appropriate count of groups', function (done) { + request(optionsConstrained, function (error, response, body) { + body.count.should.equal(10); + done(); + }); + }); + }); + + describe('When a device info request arrives', function () { + beforeEach(function (done) { + async.series([async.apply(request, optionsCreation)], done); + }); + + it('should return all the configured device groups from the database', function (done) { + request(optionsGet, function (error, response, body) { + should.exist(body); + should.exist(body.count); + body.count.should.equal(1); + should.exist(body.groups); + should.exist(body.groups.length); + body.groups.length.should.equal(1); + body.groups[0].service.should.equal('testservice'); + done(); + }); + }); + }); + + describe('When a device info request arrives and multiple groups have been created', function () { + beforeEach(function (done) { + const optionsCreationList = []; + const creationFns = []; + + for (let i = 0; i < 10; i++) { + optionsCreationList[i] = _.clone(optionsCreation); + optionsCreationList[i].json = { groups: [] }; + optionsCreationList[i].json.groups[0] = _.clone(optionsCreation.json.groups[0]); + optionsCreationList[i].json.groups[0].apikey = 'qwertyuiop' + i; + creationFns.push(async.apply(request, optionsCreationList[i])); + } + + async.series(creationFns, done); + }); + + it('should return all the configured device groups from the database', function (done) { + request(optionsGet, function (error, response, body) { + should.exist(body); + should.exist(body.count); + body.count.should.equal(10); + should.exist(body.groups); + should.exist(body.groups.length); + body.groups.length.should.equal(10); + done(); + }); + }); + }); +}); diff --git a/test/unit/mongodb/mongodb-group-registry-test.js b/test/unit/mongodb/mongodb-group-registry-test.js index bec77d40d..59008d0b3 100644 --- a/test/unit/mongodb/mongodb-group-registry-test.js +++ b/test/unit/mongodb/mongodb-group-registry-test.js @@ -23,6 +23,7 @@ * Modified by: Daniel Calvo - ATOS Research & Innovation */ +// FIXME: parallel tests in mongodb-configGroup-registry-test.js. Remove this file if at the end /iot/services API (now Deprecated) is removed /* eslint-disable no-unused-vars */ const iotAgentLib = require('../../../lib/fiware-iotagent-lib'); diff --git a/test/unit/ngsiv2/general/deviceService-test.js b/test/unit/ngsiv2/general/deviceService-test.js index ce4a4043a..4680e705f 100644 --- a/test/unit/ngsiv2/general/deviceService-test.js +++ b/test/unit/ngsiv2/general/deviceService-test.js @@ -156,6 +156,34 @@ const groupCreation = { 'fiware-servicepath': '/testingPath' } }; +const configGroupCreation = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/groups', + method: 'POST', + json: { + groups: [ + { + resource: '', + apikey: '801230BJKL23Y9090DSFL123HJK09H324HV8732', + entity_type: 'TheLightType', + trust: '8970A9078A803H3BL98PINEQRW8342HBAMS', + cbHost: 'http://192.168.1.1: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', @@ -182,7 +210,7 @@ describe('NGSI-v2 - Device Service: utils', function () { nock.cleanAll(); async.series([iotAgentLib.clearAll, iotAgentLib.deactivate], done); }); - + //FIXME: this test will be removed if at the end /iot/services API (now Deprecated) is removed 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 @@ -212,7 +240,39 @@ describe('NGSI-v2 - Device Service: utils', function () { }); }); }); + 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://192.168.1.1:1026') + .matchHeader('fiware-service', 'testservice') + .matchHeader('fiware-servicepath', '/testingPath') + .post('/v2/entities?options=upsert') + .reply(204); + + async.series( + [ + utils.request.bind(utils.request, configGroupCreation), + utils.request.bind(utils.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(); + }); + }); + }); + //FIXME: this test will be removed if at the end /iot/services API (now Deprecated) is removed 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 @@ -246,6 +306,39 @@ describe('NGSI-v2 - Device Service: utils', function () { }); }); + 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://192.168.1.1:1026') + .matchHeader('fiware-service', 'testservice') + .matchHeader('fiware-servicepath', '/testingPath') + .post('/v2/entities?options=upsert') + .reply(204); + + async.series([utils.request.bind(utils.request, configGroupCreation)], 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( diff --git a/test/unit/ngsiv2/general/iotam-autoregistration-test.js b/test/unit/ngsiv2/general/iotam-autoregistration-test.js index 6c38e5771..1f15fa952 100644 --- a/test/unit/ngsiv2/general/iotam-autoregistration-test.js +++ b/test/unit/ngsiv2/general/iotam-autoregistration-test.js @@ -134,6 +134,43 @@ const optionsCreation = { 'fiware-servicepath': 'theSubService' } }; +const configGroupCreation = { + url: 'http://localhost:4041/iot/groups', + method: 'POST', + json: { + groups: [ + { + 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', @@ -172,6 +209,44 @@ const optionsCreationStatic = { 'fiware-servicepath': 'theSubService' } }; +const configGroupCreationStatic = { + url: 'http://localhost:4041/iot/groups', + method: 'POST', + json: { + groups: [ + { + 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', @@ -185,6 +260,19 @@ const optionsDelete = { apikey: '801230BJKL23Y9090DSFL123HJK09H324HV8732' } }; +const configGroupDelete = { + url: 'http://localhost:4041/iot/groups', + method: 'DELETE', + json: {}, + headers: { + 'fiware-service': 'theservice', + 'fiware-servicepath': 'theSubService' + }, + qs: { + resource: '/deviceTest', + apikey: '801230BJKL23Y9090DSFL123HJK09H324HV8732' + } +}; let iotamMock; describe('NGSI-v2 - IoT Manager autoregistration', function () { @@ -263,6 +351,7 @@ describe('NGSI-v2 - IoT Manager autoregistration', function () { }); }); + //FIXME: this test will be removed if at the end /iot/services API (now Deprecated) is removed describe('When a new service is created in the IoT Agent', function () { beforeEach(function (done) { nock.cleanAll(); @@ -298,6 +387,42 @@ describe('NGSI-v2 - IoT Manager autoregistration', function () { }); }); + describe('When a new configGroup 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(configGroupCreation, function (error, result, body) { + should.not.exist(error); + iotamMock.done(); + done(); + }); + }); + }); + + //FIXME: this test will be removed if at the end /iot/services API (now Deprecated) is removed describe('When a service is removed from the IoT Agent', function () { beforeEach(function (done) { nock.cleanAll(); @@ -333,6 +458,42 @@ describe('NGSI-v2 - IoT Manager autoregistration', function () { }); }); + describe('When a configGroup 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(configGroupDelete, function (error, result, body) { + should.not.exist(error); + iotamMock.done(); + done(); + }); + }); + }); + + //FIXME: this test will be removed if at the end /iot/services API (now Deprecated) is removed describe('When a new service with static attributes is created in the IoT Agent', function () { beforeEach(function (done) { nock.cleanAll(); @@ -367,4 +528,38 @@ describe('NGSI-v2 - IoT Manager autoregistration', function () { }); }); }); + describe('When a new configGroup 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(configGroupCreationStatic, function (error, result, body) { + should.not.exist(error); + iotamMock.done(); + done(); + }); + }); + }); }); diff --git a/test/unit/ngsiv2/provisioning/device-group-api-test.js b/test/unit/ngsiv2/provisioning/device-group-api-test.js index 4a2ae63a0..e2871cff7 100644 --- a/test/unit/ngsiv2/provisioning/device-group-api-test.js +++ b/test/unit/ngsiv2/provisioning/device-group-api-test.js @@ -23,6 +23,8 @@ /* eslint-disable no-unused-vars */ +// FIXME: parallel tests in device-provisioning-configGroup-api_test.js. + const iotAgentLib = require('../../../../lib/fiware-iotagent-lib'); const _ = require('underscore'); const async = require('async'); @@ -250,6 +252,20 @@ const optionsGet = { } }; +// Add new options using the literal groups instead of services +const configGroupTerm = 'groups'; + +const newOptionsCreation = JSON.parse(JSON.stringify(optionsCreation)); +newOptionsCreation.url = newOptionsCreation.url.replace('services', configGroupTerm); +newOptionsCreation.json[configGroupTerm] = newOptionsCreation.json.services; +delete newOptionsCreation.json.services; + +const newOptionsList = JSON.parse(JSON.stringify(optionsList)); +newOptionsList.url = newOptionsList.url.replace('services', configGroupTerm); + +const newOptionsGet = JSON.parse(JSON.stringify(optionsGet)); +newOptionsGet.url = newOptionsGet.url.replace('services', configGroupTerm); + describe('NGSI-v2 - Device Group Configuration API', function () { beforeEach(function (done) { iotAgentLib.activate(iotAgentConfig, function () { @@ -1156,4 +1172,87 @@ describe('NGSI-v2 - Device Group Configuration API', function () { }); }); }); + + describe('When a new device group creation request arrives with the NEW API endpoint ', function () { + it('should return a 200 OK', function (done) { + request(newOptionsCreation, function (error, response, body) { + should.not.exist(error); + response.statusCode.should.equal(201); + done(); + }); + }); + it('should be recovered using the OLD API endpoint', function (done) { + request(newOptionsCreation, function (error, response, body) { + request(optionsList, function (error, response, body) { + body.count.should.equal(1); + body.services[0].apikey.should.equal('801230BJKL23Y9090DSFL123HJK09H324HV8732'); + body.services[0].transport.should.equal('HTTP'); + body.services[0].endpoint.should.equal('http://myendpoint.com'); + + body.count.should.equal(1); + should.exist(body.services[0].attributes); + body.services[0].attributes.length.should.equal(1); + body.services[0].attributes[0].name.should.equal('status'); + + should.exist(body.services[0].lazy); + body.services[0].lazy.length.should.equal(1); + body.services[0].lazy[0].name.should.equal('luminescence'); + + should.exist(body.services[0].commands); + body.services[0].commands.length.should.equal(1); + body.services[0].commands[0].name.should.equal('wheel1'); + + should.exist(body.services[0].static_attributes); + body.services[0].static_attributes.length.should.equal(1); + body.services[0].static_attributes[0].name.should.equal('bootstrapServer'); + + body.count.should.equal(1); + body.services[0].service.should.equal('testservice'); + body.services[0].subservice.should.equal('/testingPath'); + done(); + }); + }); + }); + }); + describe('When a new device group creation request arrives with the NEW OLD endpoint ', function () { + it('should return a 200 OK', function (done) { + request(optionsCreation, function (error, response, body) { + should.not.exist(error); + response.statusCode.should.equal(201); + done(); + }); + }); + it('should be recovered using the NEW API endpoint', function (done) { + request(optionsCreation, function (error, response, body) { + request(newOptionsList, function (error, response, body) { + body.count.should.equal(1); + body[configGroupTerm][0].apikey.should.equal('801230BJKL23Y9090DSFL123HJK09H324HV8732'); + body[configGroupTerm][0].transport.should.equal('HTTP'); + body[configGroupTerm][0].endpoint.should.equal('http://myendpoint.com'); + + body.count.should.equal(1); + should.exist(body[configGroupTerm][0].attributes); + body[configGroupTerm][0].attributes.length.should.equal(1); + body[configGroupTerm][0].attributes[0].name.should.equal('status'); + + should.exist(body[configGroupTerm][0].lazy); + body[configGroupTerm][0].lazy.length.should.equal(1); + body[configGroupTerm][0].lazy[0].name.should.equal('luminescence'); + + should.exist(body[configGroupTerm][0].commands); + body[configGroupTerm][0].commands.length.should.equal(1); + body[configGroupTerm][0].commands[0].name.should.equal('wheel1'); + + should.exist(body[configGroupTerm][0].static_attributes); + body[configGroupTerm][0].static_attributes.length.should.equal(1); + body[configGroupTerm][0].static_attributes[0].name.should.equal('bootstrapServer'); + + body.count.should.equal(1); + body[configGroupTerm][0].service.should.equal('testservice'); + body[configGroupTerm][0].subservice.should.equal('/testingPath'); + done(); + }); + }); + }); + }); }); diff --git a/test/unit/ngsiv2/provisioning/device-provisioning-configGroup-api_test.js b/test/unit/ngsiv2/provisioning/device-provisioning-configGroup-api_test.js new file mode 100644 index 000000000..82a13a9b9 --- /dev/null +++ b/test/unit/ngsiv2/provisioning/device-provisioning-configGroup-api_test.js @@ -0,0 +1,1189 @@ +/* + * 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 nock = require('nock'); + +const request = utils.request; +const moment = require('moment'); +let contextBrokerMock; +const iotAgentConfig = { + logLevel: 'FATAL', + contextBroker: { + host: '192.168.1.1', + port: '1026', + ngsiVersion: 'v2' + }, + server: { + port: 4041, + host: 'localhost', + baseRoot: '/' + }, + types: {}, + service: 'smartgondor', + subservice: 'gardens', + providerUrl: 'http://smartgondor.com', + explicitAttrs: false +}; + +describe('NGSI-v2 - 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') + .matchHeader('fiware-servicepath', '/gardens') + .post( + '/v2/registrations', + utils.readExampleFile( + './test/unit/ngsiv2/examples/contextAvailabilityRequests/registerProvisionedDevice.json' + ) + ) + .reply(201, null, { Location: '/v2/registrations/6319a7f5254b05844116584d' }); + + // This mock does not check the payload since the aim of the test is not to verify + // device provisioning functionality. Appropriate verification is done in tests under + // provisioning folder + contextBrokerMock + .matchHeader('fiware-service', 'smartgondor') + .matchHeader('fiware-servicepath', '/gardens') + .post('/v2/entities?options=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') + .matchHeader('fiware-servicepath', '/gardens') + .post( + '/v2/registrations', + utils.readExampleFile( + './test/unit/ngsiv2/examples/contextAvailabilityRequests/registerProvisionedDevice.json' + ) + ) + .reply(201, null, { Location: '/v2/registrations/6319a7f5254b05844116584d' }); + }); + + 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 not 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(); + 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(); + 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 explicitAttrs 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/provisionExplicitAttrs.json'), + headers: { + 'fiware-service': 'smartgondor', + 'fiware-servicepath': '/gardens' + } + }; + + beforeEach(function (done) { + iotAgentLib.deactivate(function () { + iotAgentConfig.explicitAttrs = false; + iotAgentLib.activate(iotAgentConfig, done); + }); + }); + + afterEach(function () { + iotAgentConfig.explicitAttrs = false; + }); + + beforeEach(function (done) { + nock.cleanAll(); + done(); + }); + + it('should send the appropriate requests to the Context Broker', function (done) { + request(options, function (error, response, body) { + contextBrokerMock.done(); + done(); + }); + }); + it('should store the device with explicitAttrs:true device information', function (done) { + request(options, function (error, response, body) { + iotAgentLib.listDevices('smartgondor', '/gardens', function (error, results) { + should.exist(results.devices[0].explicitAttrs); + results.devices[0].explicitAttrs.should.equal(true); + done(); + }); + }); + }); + }); + + describe( + 'When a device provisioning request without explicitAttrs provision attribute arrives to the IoTA' + + ' and explicitAttrs is not configured at group level', + 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.explicitAttrs = false; + iotAgentLib.activate(iotAgentConfig, done); + }); + }); + + afterEach(function () { + iotAgentConfig.explicitAttrs = false; + }); + + beforeEach(function (done) { + nock.cleanAll(); + done(); + }); + + it('should send the appropriate requests to the Context Broker', function (done) { + request(options, function (error, response, body) { + contextBrokerMock.done(); + done(); + }); + }); + it('should store the device without explicitAttrs', function (done) { + request(options, function (error, response, body) { + iotAgentLib.listDevices('smartgondor', '/gardens', function (error, results) { + should.not.exist(results.devices[0].explicitAttrs); + done(); + }); + }); + }); + } + ); + + describe( + 'When a device provisioning request without explicitAttrs provision attribute arrives to the IoTA' + + ' and explicitAttrs is configured at group level', + 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' + } + }; + const groupCreation = { + url: 'http://localhost:4041/iot/groups', + method: 'POST', + json: { + groups: [ + { + resource: '/Thing', + apikey: '801230BJKL23Y9090DSFL123HJK09H324HV8732', + /*jshint camelcase: false */ + entity_type: 'MicroLights', + cbroker: 'http://192.168.1.1:1026', + explicitAttrs: 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') + .matchHeader('fiware-servicepath', '/gardens') + .post( + '/v2/entities?options=upsert', + utils.readExampleFile( + './test/unit/ngsiv2/examples/contextRequests/createMinimumProvisionedDevice.json' + ) + ) + .reply(204); + + done(); + }); + + it('should store the device without explicitAttrs value', function (done) { + request(groupCreation, function (error, response, body) { + request(options, function (error, response, body) { + iotAgentLib.listDevices('smartgondor', '/gardens', function (error, results) { + should.not.exist(results.devices[0].explicitAttrs); + done(); + }); + }); + }); + }); + } + ); + + describe( + 'When a device provisioning request without static attributes arrives to the IoTA' + + ' and static attribute is configured at group level', + 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' + } + }; + const groupCreation = { + url: 'http://localhost:4041/iot/groups', + method: 'POST', + json: { + groups: [ + { + resource: '/Thing', + apikey: '801230BJKL23Y9090DSFL123HJK09H324HV8732', + /*jshint camelcase: false */ + entity_type: 'MicroLights', + cbroker: 'http://192.168.1.1:1026', + explicitAttrs: true, + static_attributes: [ + { + name: 'bootstrapServer', + type: 'Address', + value: '127.0.0.1' + } + ] + } + ] + }, + headers: { + 'fiware-service': 'smartgondor', + 'fiware-servicepath': '/gardens' + } + }; + + beforeEach(function (done) { + nock.cleanAll(); + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartgondor') + .matchHeader('fiware-servicepath', '/gardens') + .post( + '/v2/entities?options=upsert', + utils.readExampleFile( + './test/unit/ngsiv2/examples/' + + 'contextRequests/createStaticAttributesProvisionDevice.json' + ) + ) + .reply(204); + + done(); + }); + + it('should not store the device with static attributes provided in configuration', function (done) { + request(groupCreation, function (error, response, body) { + request(options, function (error, response, body) { + iotAgentLib.listDevices('smartgondor', '/gardens', function (error, results) { + should.exist(results.devices[0].staticAttributes); + results.devices[0].staticAttributes.length.should.equal(0); + done(); + }); + }); + }); + }); + } + ); + describe( + 'When a device provisioning request with static attributes arrives to the IoTA' + + ' and static attribute is not configured at group level', + function () { + const options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices', + method: 'POST', + json: utils.readExampleFile( + './test/unit/examples/deviceProvisioningRequests/provisionStaticAttrsDevice.json' + ), + headers: { + 'fiware-service': 'smartgondor', + 'fiware-servicepath': '/gardens' + } + }; + const groupCreation = { + url: 'http://localhost:4041/iot/groups', + method: 'POST', + json: { + groups: [ + { + resource: '/Thing', + apikey: '801230BJKL23Y9090DSFL123HJK09H324HV8732', + /*jshint camelcase: false */ + entity_type: 'MicroLights', + cbroker: 'http://192.168.1.1:1026', + explicitAttrs: 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') + .matchHeader('fiware-servicepath', '/gardens') + .post( + '/v2/entities?options=upsert', + utils.readExampleFile( + './test/unit/ngsiv2/examples/' + + 'contextRequests/createStaticAttributesProvisionDevice.json' + ) + ) + .reply(204); + + done(); + }); + + it('should store the device with static attributes provided in device', function (done) { + request(groupCreation, function (error, response, body) { + request(options, function (error, response, body) { + iotAgentLib.listDevices('smartgondor', '/gardens', function (error, results) { + should.exist(results.devices[0].staticAttributes); + results.devices[0].staticAttributes[0].name.should.equal('bootstrapServer'); + done(); + }); + }); + }); + }); + } + ); + + describe( + 'When a device provisioning request with static attributes arrives to the IoTA' + + ' and static attribute is also configured at group level', + function () { + const options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices', + method: 'POST', + json: utils.readExampleFile( + './test/unit/examples/deviceProvisioningRequests/provisionStaticAttrsDevice2.json' + ), + headers: { + 'fiware-service': 'smartgondor', + 'fiware-servicepath': '/gardens' + } + }; + const groupCreation = { + url: 'http://localhost:4041/iot/groups', + method: 'POST', + json: { + groups: [ + { + resource: '/Thing', + apikey: '801230BJKL23Y9090DSFL123HJK09H324HV8732', + /*jshint camelcase: false */ + entity_type: 'MicroLights', + cbroker: 'http://192.168.1.1:1026', + explicitAttrs: true, + static_attributes: [ + { + name: 'bootstrapServer', + type: 'Address', + value: '127.0.0.1' + } + ] + } + ] + }, + headers: { + 'fiware-service': 'smartgondor', + 'fiware-servicepath': '/gardens' + } + }; + + beforeEach(function (done) { + nock.cleanAll(); + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartgondor') + .matchHeader('fiware-servicepath', '/gardens') + .post( + '/v2/entities?options=upsert', + utils.readExampleFile( + './test/unit/ngsiv2/examples/' + + 'contextRequests/createStaticAttributesProvisionDevice2.json' + ) + ) + .reply(204); + + done(); + }); + + it('should store the device with static attributes provided in configuration as well as device', function (done) { + request(groupCreation, function (error, response, body) { + request(options, function (error, response, body) { + iotAgentLib.listDevices('smartgondor', '/gardens', function (error, results) { + should.exist(results.devices[0].staticAttributes); + results.devices[0].staticAttributes.length.should.equal(1); + done(); + }); + }); + }); + }); + } + ); + + describe( + 'When a device provisioning request with static attributes arrives to the IoTA' + + ' and same static attribute is also configured at group level', + function () { + const options = { + url: 'http://localhost:' + iotAgentConfig.server.port + '/iot/devices', + method: 'POST', + json: utils.readExampleFile( + './test/unit/examples/deviceProvisioningRequests/provisionStaticAttrsDevice3.json' + ), + headers: { + 'fiware-service': 'smartgondor', + 'fiware-servicepath': '/gardens' + } + }; + const groupCreation = { + url: 'http://localhost:4041/iot/groups', + method: 'POST', + json: { + groups: [ + { + resource: '/Thing', + apikey: '801230BJKL23Y9090DSFL123HJK09H324HV8732', + /*jshint camelcase: false */ + entity_type: 'MicroLights', + cbroker: 'http://192.168.1.1:1026', + explicitAttrs: true, + static_attributes: [ + { + name: 'bootstrapServer', + type: 'Address', + value: '127.0.0.1' + } + ] + } + ] + }, + headers: { + 'fiware-service': 'smartgondor', + 'fiware-servicepath': '/gardens' + } + }; + + beforeEach(function (done) { + nock.cleanAll(); + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartgondor') + .matchHeader('fiware-servicepath', '/gardens') + .post( + '/v2/entities?options=upsert', + utils.readExampleFile( + './test/unit/ngsiv2/examples/' + + 'contextRequests/createStaticAttributesProvisionDevice3.json' + ) + ) + .reply(204); + + done(); + }); + + it('should store the device with static attributes provided in device', function (done) { + request(groupCreation, function (error, response, body) { + request(options, function (error, response, body) { + iotAgentLib.listDevices('smartgondor', '/gardens', function (error, results) { + should.exist(results.devices[0].staticAttributes); + results.devices[0].staticAttributes[0].value.should.equal('127.0.0.2'); + 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 () { + iotAgentLib.activate(iotAgentConfig, done); + }); + }); + + afterEach(function () {}); + + beforeEach(function (done) { + nock.cleanAll(); + done(); + }); + + it('should send the appropriate requests to the Context Broker', function (done) { + request(options, function (error, response, body) { + 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(); + + done(); + }); + + it('should send the any request to the Context Broker', function (done) { + request(options, function (error, response, body) { + 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(); + done(); + }); + + it('should send the any request to the Context Broker', function (done) { + request(options, function (error, response, body) { + 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(); + done(); + }); + + it('should send any initial values to the Context Broker', function (done) { + request(options, function (error, response, body) { + 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(); + done(); + }); + + it('should send any initial values to the Context Broker', function (done) { + request(options, function (error, response, body) { + 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( + '/v2/entities?options=upsert', + utils.readExampleFile( + './test/unit/ngsiv2/examples/contextRequests/createMinimumProvisionedDevice.json' + ) + ) + .reply(204); + + contextBrokerMock + .post( + '/v2/entities?options=upsert', + utils.readExampleFile( + './test/unit/ngsiv2/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 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') + .matchHeader('fiware-servicepath', '/gardens') + .post('/v2/registrations') + .reply(201, null, { Location: '/v2/registrations/6319a7f5254b05844116584d' }); + done(); + }); + + it('should return a valid return code', function (done) { + request(options, function (error, response, body) { + should.not.exist(error); + response.statusCode.should.equal(201); + + 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') + .matchHeader('fiware-servicepath', '/gardens') + .post('/v2/registrations') + .reply(201, null, { Location: '/v2/registrations/6319a7f5254b05844116584d' }); + + 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(201); + + 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') + .matchHeader('fiware-servicepath', '/gardens') + .post('/v2/registrations') + .reply(201, null, { Location: '/v2/registrations/6319a7f5254b05844116584d' }); + + // This mock does not check the payload since the aim of the test is not to verify + // device provisioning functionality. Appropriate verification is done in tests under + // provisioning folder + contextBrokerMock + .matchHeader('fiware-service', 'smartgondor') + .matchHeader('fiware-servicepath', '/gardens') + .post('/v2/entities?options=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(); + }); + }); + }); +});