diff --git a/README.md b/README.md index 49e006401..f9add3366 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,10 @@ decommission devices. [API](doc/api.md). Contributions to development can be found [here](doc/devel/development.md) - additional contributions are welcome. +If you are whishing to test the library, or include new tests (either as part of a contribution or as a new feature or +as a bug report), you can use the functional tests suite included in the project. The tests are described using a JSON +file. You can find more information about the test suite in the [Functional Tests Guide](test/functional/README.md). + ### Agent Console A command-line client to experiment with the library is packed with it. The command-line client can be started using the diff --git a/package.json b/package.json index 0ac395519..b8650633f 100644 --- a/package.json +++ b/package.json @@ -33,6 +33,7 @@ "prettier": "prettier --config .prettierrc.json --write '**/**/**/**/*.js' '**/**/**/*.js' '**/**/*.js' '**/*.js' '*.js'", "prettier:text": "prettier 'README.md' 'doc/*.md' 'doc/**/*.md' --no-config --tab-width 4 --print-width 120 --write --prose-wrap always", "test": "nyc --reporter=text mocha --recursive 'test/**/*.js' --reporter spec --timeout 8000 --ui bdd --exit --color true", + "test:functional": "nyc --reporter=text mocha --recursive 'test/functional/*.js' --reporter spec --timeout 5000 --ui bdd --exit --color true", "test:expression": "nyc --reporter=text mocha --recursive 'test/unit/expressions/*.js' --reporter spec --timeout 5000 --ui bdd --exit --color true", "test:multientity": "nyc --reporter=text mocha --recursive 'test/unit/ngsiv2/plugins/multientity-plugin_test.js' --reporter spec --timeout 5000 --ui bdd --exit --color true", "test:debug": "mocha --recursive 'test/**/*.js' --reporter spec --inspect-brk --timeout 30000 --ui bdd --exit", @@ -58,6 +59,9 @@ "uuid": "~8.3.2" }, "devDependencies": { + "async-mqtt": "~2.6.3", + "chai": "~4.3.10", + "chai-match-pattern": "~1.3.0", "coveralls": "~3.1.1", "eslint": "~8.18.0", "eslint-config-tamia": "~8.0.0", diff --git a/test/functional/README.md b/test/functional/README.md new file mode 100644 index 000000000..76fd22fc6 --- /dev/null +++ b/test/functional/README.md @@ -0,0 +1,378 @@ +## Functional test suite + +This directory contains the functional test suite for the IoTA Node Lib. This test suite is based on mocha and chai. For +mocks, we use the nock library. Additionally, it uses some specific functions to ease to implement the test. Helper +functions are located in the `testUtils.js` file. + +There are 2 tests files in this directory: + +- `fuctional-tests.js`: This file contains the test defined in the "classic way". This means, coded in the JS file as + any other mocha test. It uses the functions defined in the `testUtils.js` file to simplify the tests. +- `functional-tests-runner.js`: This file contains the test runner that executes the tests defined as JSON in a + separate file (`testCases.js`) in a "automatic way". This file is loaded by the test suite and the test cases are + automatically executed. + +The recommended way is to use `testCases.js` (run by `functional-tests-runner.js`). The `fuctional-tests.js` file is +provides as basically as an example in the case the "old way" needs to be used (I.E: when the test follow a different +pattern than the one supported by the test runner). + +### Automatic test cases + +Each test case is defined as a JSON object in the `testCases.js` file. This file is loaded by the test suite and the +test cases are automatically generated. Each test case is defined as an object with the following elements: + +- `describeName`: The name of the `DESCRIBE` test case. This will be used to generate the test case name in the mocha. Note this name is prefixed by a pure number (e.g `0010 - Simple group without attributes`) which specifies the group to which the test belong (usually meaning a feature) or by a number preced by the `#` symbol to refer to an issue number (e.g. `#1234 - Bug foobar`). + test suite. +- `provision`: The JSON object that will be sent to the IoTA JSON provisioning API. This will be used to create the + group. It contains the following elements: + - `url`: The URL of the provisioning API (group) + - `method`: The HTTP method to use (POST) + - `json`: The JSON object that defines the group + - `headers`: The headers to send to the provisioning API. This should contain the `fiware-service` and + `fiware-servicepath` headers. + - `skip`: optional. If set to `true`, the test case (`describe`) will be skipped. This is useful to skip test + cases that are not supported by the agent. It can also have a string value `"lib"`. This will skip the test case + only if the is executed in the IoTA Node lib repo. This is useful to skip test cases that are not supported by + the lib (I.E: all tests related to the transport). +- `should`: The array of test cases to execute. Each test case is defined as an object with the following elements: + - `transport`: The transport to use to send the measure. This can be `HTTP` or `MQTT`. It uses `HTTP` by default + or if the `transport` element is not defined. See the "Advanced features" section for more information. + - `shouldName`: The name of the `IT` test case. This will be used to generate the test case name in the mocha test + suite. + - `type`: The type of the test case. This can be `single` or `multientity`. See the "Advanced features" section + for more information. + - `measure`: The JSON object that will be sent to the IoTA JSON measure API. This will be used to send the + measure. It contains the following elements: + - `url`: The URL of the measure API (group) + - `method`: The HTTP method to use (POST) + - `qs`: The query string to send to the measure API. This should contain the `i` and `k` parameters. + - `json`: The JSON object that defines the measure + - `expectation`: The JSON object that defines the expectation. This will be used to check that the measure has + been correctly sent to the Context Broker. + - `loglevel`: optional. If set to `debug`, the agent will log all the debug messages to the console. This is + useful to check the messages sent to the Context Broker. See the "Debugging automated tests" section for more + information. + - `skip`: optional. If set to `true`, the test case (`it`) will be skipped. Same as the `skip` element in the + `provision` element. + - `isRegex`: optional. If set to `true`, then the expectation will be treated as a regular expression. This is + useful to check that the measure has been correctly sent to the Context Broker when the measure contains a a + variable parameter like a timestamp. See the "Advanced features" section for more information. + +#### Example + +```javascript +{ + describeName: 'Basic group provision with attributes', + provision: { + url: 'http://localhost:' + config.iota.server.port + '/iot/services', + method: 'POST', + json: { + services: [ + { + resource: '/iot/json', + apikey: '123456', + entity_type: 'TheLightType2', + cbHost: 'http://192.168.1.1:1026', + commands: [], + lazy: [], + attributes: [ + { + object_id: 's', + name: 'status', + type: 'Boolean' + }, + { + object_id: 't', + name: 'temperature', + type: 'Number' + } + ], + static_attributes: [] + } + ] + }, + headers: { + 'fiware-service': 'smartgondor', + 'fiware-servicepath': '/gardens' + } + }, + should:[ + { + shouldName: 'should send its value to the Context Broker', + config:{ + type: 'single' + }, + measure: { + url: 'http://localhost:' + config.http.port + '/iot/json', + method: 'POST', + qs: { + i: 'MQTT_2', + k: '123456' + }, + json: { + s: false, + t: 10 + } + }, + expectation: { + id: 'TheLightType2:MQTT_2', + type: 'TheLightType2', + temperature: { + value: 10, + type: 'Number' + }, + status: { + value: false, + type: 'Boolean' + } + } + }, + { + transport: 'MQTT', + shouldName: 'should send its value to the Context Broker when using MQTT', + config:{ + type: 'single' + }, + measure: { + url: 'http://localhost:' + config.http.port + '/iot/json', + method: 'POST', + qs: { + i: 'MQTT_2', + k: '123456' + }, + json: { + s: false, + t: 10 + } + }, + expectation: { + id: 'TheLightType2:MQTT_2', + type: 'TheLightType2', + temperature: { + value: 10, + type: 'Number' + }, + status: { + value: false, + type: 'Boolean' + } + } + } + ] +} +``` + +### Advanced features + +#### Multientity + +This test suite support the multientity feature. To test this feature, you need to set add to the test case the +parameter `type: 'multientity'`. This will automatically take care of the multientity feature. This means that the suite +will configure the mock to listen to `/v2/op/update` instead of `/v2/entities?options=upsert`. + +In particular, it will configure the mock to listen the correct URL. You should define the expectation for the test case +as a batch operation (see the following example). + +```javascript +{ + "entities": [ + { + "id": "TheLightType2:MQTT_2", + "type": "TheLightType2", + "status": { + "value": false, + "type": "Boolean" + } + }, + { + "id": "TheLightType2:MQTT_3", + "type": "TheLightType2", + "temperature": { + "value": 10, + "type": "Number" + } + } + ], + "actionType": "append" +} +``` + +#### Multimeasures + +It is also supported to test cases in which is sent more than one measure. To do so, you need to define the test case +`expectation` as an array, with one object for each measurement. Then, the suite will recognize the array length and will +expect the same number of NGSI requests. I.E: + +```js +[ + { + id: 'TheLightType2:MQTT_2', + type: 'TheLightType2', + temperature: { + value: 10, + type: 'Number' + }, + status: { + value: false, + type: 'Boolean' + } + }, + { + id: 'TheLightType2:MQTT_2', + type: 'TheLightType2', + temperature: { + value: 20, + type: 'Number' + }, + status: { + value: true, + type: 'Boolean' + } + } +]; +``` + +You also should define the measure as multimeasure. This is done by defining the `measure` JSON element as an array of +objects. Each object will be a measure that will be sent to the Context Broker in a different request. I.E: + +```javascript +measure: { + url: 'http://localhost:' + config.http.port + '/iot/json', + method: 'POST', + qs: { + i: 'MQTT_2', + k: '123456' + }, + json: [ + { + s: false, + t: 10 + }, + { + s: true, + t: 20 + } + ] +} +``` + +#### Transport + +The test suite supports using the internal node lib function `iotAgentLib.update`, `HTTP` or `MQTT` for measure sending. +In order to select the specific way to send the measure you should add a `transport` element to each should case having +the value set to `MQTT`. By doing so, the suite will automatically configure the mock to connect to the MQTT broker and +send the measure to the correct topic based on the `i` and `k` parameters. It will ignore the `url` and `method` +parameters present in the measure JSON element. I.E: + +```javascript +should: [ + { + transport: 'MQTT', + shouldName: 'should send its value to the Context Broker when using MQTT', + type: 'single', + measure: { + url: 'http://localhost:' + config.http.port + '/iot/json', + method: 'POST', + qs: { + i: 'MQTT_2', + k: '123456' + }, + json: { + s: false, + t: 10 + } + }, + expectation: { + id: 'TheLightType2:MQTT_2', + type: 'TheLightType2', + temperature: { + value: 10, + type: 'Number' + }, + status: { + value: false, + type: 'Boolean' + } + } + } +]; +``` + +#### No payload reception + +The test suite also supports the case in which the Context Broker does not receive any payload. This is done by defining +the expectation as an empty object. I.E: + +```javascript + ... + expectation: [] + ... +``` + +Note that this means the CB *must not* receive any payload. In other words, if the CB would receive any payload, the test will fail. + +### Debugging automated tests + +It is possible to debug the automated tests by using the loglevel parameter set to `debug` for each should case. This +parameter configures the IoTA log level. By setting it to `debug`, the agent will log all the debug messages to the +console. This is useful to check the messages sent to the Context Broker. + +It is also useful to debug the test by adding a breakpoint in the `testUtils.js` (read the comments in the file to know +where to add the breakpoint). This will allow you to debug just that particular test case stopping the execution in the +breakpoint. + +Example of a test case with the loglevel set to `debug`: + +```javascript +should:[ + { + shouldName: 'should send its value to the Context Broker when using MQTT', + loglevel: 'debug', + transport: 'MQTT', + config:{ + type: 'single' + }, + measure: {...}, + expectation: {...} + } +] +``` + +### Using expressions in the expectation + +It is possible to use expressions in the expectation. This is useful to check that the measure has been correctly sent +to the Context Broker when the measure contains a variable parameter like a timestamp. To do so, you need to set the +`isRegex` parameter to `true` in the test case. This will tell the test suite that the expectation can contain some +expressions. The expression support is based on [Chai match pattern plugin](https://github.com/mjhm/chai-match-pattern). +This plugin relies on [loadash match pattern](https://github.com/mjhm/lodash-match-pattern) to support the expressions. +For further information about the expression support, check the documentation of the previous links. + +Even if setting the `isRegex` parameter to `true` make the test pass if the expectation does not contain any expression, +it is recommended to do not set this parameter to `true` if the expectation does not contain any expression because the +test result will be less clear if fails (it will show the error message of the expression library instead of the +expected value). + +Here, you can find an example of a test case using expressions: + +```javascript + expectation: { + id: globalEnv.entity_name, + type: globalEnv.entity_type, + attr_a: { + value: 21, + type: 'Number', + metadata: { + TimeInstant: { + type: 'DateTime', + value: _.isDateString + } + } + }, + TimeInstant: { + type: 'DateTime', + value: _.isDateString + } + } +``` + +The previous example will check that the `TimeInstant` value and `attr_a.metadata.TimeInstant.value` are valid date. diff --git a/test/functional/config-test.js b/test/functional/config-test.js new file mode 100644 index 000000000..2bebb2c6b --- /dev/null +++ b/test/functional/config-test.js @@ -0,0 +1,70 @@ +/* + * Copyright 2023 Telefonica Investigación y Desarrollo, S.A.U + * + * This file is part of iotagent-json + * + * iotagent-json is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the License, + * or (at your option) any later version. + * + * iotagent-json is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with iotagent-json. + * If not, seehttp://www.gnu.org/licenses/. + * + * For those usages not covered by the GNU Affero General Public License + * please contact with::[contacto@tid.es] + * + * Modified by: Miguel Angel Pedraza + */ + +/* eslint-disable no-unused-vars */ + +const config = {}; + +config.mqtt = { + host: 'localhost', + port: 1883 +}; + +config.http = { + port: 7896, + host: 'localhost' +}; + +config.amqp = { + port: 5672, + exchange: 'amq.topic', + queue: 'iota_queue', + options: { durable: true } +}; + +config.iota = { + logLevel: 'FATAL', + contextBroker: { + host: '192.168.1.1', + port: '1026', + ngsiVersion: 'v2' + }, + server: { + port: 4041, + host: 'localhost' + }, + deviceRegistry: { + type: 'memory' + }, + service: 'smartgondor', + subservice: '/gardens', + providerUrl: 'http://localhost:4041', + types: {} +}; + +config.defaultKey = '1234'; +config.defaultTransport = 'MQTT'; + +module.exports = config; diff --git a/test/functional/functional-tests-runner.js b/test/functional/functional-tests-runner.js new file mode 100755 index 000000000..5a04ac759 --- /dev/null +++ b/test/functional/functional-tests-runner.js @@ -0,0 +1,126 @@ +/* + * Copyright 2023 Telefonica Investigación y Desarrollo, S.A.U + * + * This file is part of iotagent-json + * + * iotagent-json is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the License, + * or (at your option) any later version. + * + * iotagent-json is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with iotagent-json. + * If not, seehttp://www.gnu.org/licenses/. + * + * For those usages not covered by the GNU Affero General Public License + * please contact with::[contacto@tid.es] + * + * Modified by: Miguel Angel Pedraza + */ + +/* eslint-disable no-unused-vars*/ +/* eslint-disable no-unused-expressions*/ + +const config = require('./config-test.js'); +const nock = require('nock'); +const chai = require('chai'); +const expect = chai.expect; +const iotAgentLib = require('../../lib/fiware-iotagent-lib'); +const testUtils = require('./testUtils'); +const logger = require('logops'); +chai.config.truncateThreshold = 0; + +const baseTestCases = require('./testCases.js').testCases; + +const env = { + service: 'smartgondor', + servicePath: '/gardens' +}; + +// You can add here your own test cases to be executed in addition to the base ones +// It is useful to test new features or to test specific scenarios. If you are going +// to add a new test case, please, add it to the testCases.js file instead of adding +// it here. +let testCases = []; + +// If you want to execute only the test cases defined above, you can comment +// the following line. Otherwise, the tests defined in testCases.js will be +// executed as well. +testCases = testCases.concat(baseTestCases); + +describe('FUNCTIONAL TESTS AUTO', function () { + testCases.forEach((testCase) => { + describe(testCase.describeName, function () { + beforeEach(function (done) { + if (testCase.skip && testUtils.checkSkip(testCase.skip, 'lib')) { + this.skip(); + } + if (testCase.loglevel) { + logger.setLevel(testCase.loglevel); + } + let type = testUtils.groupToIoTAConfigType( + testCase.provision.json.services[0], + testCase.provision.headers.fiwareService, + testCase.provision.headers.fiwareServicepath + ); + config.iota.types[type.name] = type.type; + iotAgentLib.activate(config.iota, function (error) { + done(error); + }); + }); + + afterEach(function (done) { + logger.setLevel('FATAL'); + nock.cleanAll(); + iotAgentLib.clearAll(function () { + iotAgentLib.deactivate(function () { + iotAgentLib.setDataUpdateHandler(); + iotAgentLib.setCommandHandler(); + done(); + }); + }); + }); + + testCase.should.forEach((should) => { + it(should.shouldName, async function () { + if (testCase.skip && testUtils.checkSkip(testCase.skip, 'lib')) { + this.skip(); + } + // Skip the test if the transport is specified (IoTA Lib does not support any transport) + if ( + should.transport && + (should.transport === 'MQTT' || should.transport === 'AMQP' || should.transport === 'HTTP') + ) { + this.skip(); + } + + this.retries(2); // Retry if the test fails + + if (should.loglevel) { + // You can use this line to set a breakpoint in the test in order to debug it + // You just need to add a loglevel element to the test case with the desired log level + // and then set a breakpoint in the next line. By default, the log level is FATAL and + // the following line will never be executed + logger.setLevel(should.loglevel); + } else { + await testUtils.testCase( + should.measure, + should.expectation, + testCase.provision, + env, + config, + should.type ? should.type : 'single', + should.transport, + should.isRegex ? should.isRegex : false + ); + } + }); + }); + }); + }); +}); diff --git a/test/functional/functional-tests.js b/test/functional/functional-tests.js new file mode 100755 index 000000000..27445ef03 --- /dev/null +++ b/test/functional/functional-tests.js @@ -0,0 +1,241 @@ +/* + * Copyright 2023 Telefonica Investigación y Desarrollo, S.A.U + * + * This file is part of iotagent-json + * + * iotagent-json is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the License, + * or (at your option) any later version. + * + * iotagent-json is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with iotagent-json. + * If not, seehttp://www.gnu.org/licenses/. + * + * For those usages not covered by the GNU Affero General Public License + * please contact with::[contacto@tid.es] + * + * Modified by: Miguel Angel Pedraza + */ + +/* eslint-disable no-unused-vars*/ +/* eslint-disable no-unused-expressions*/ + +const config = require('./config-test.js'); +const nock = require('nock'); +const chai = require('chai'); +const expect = chai.expect; +const iotAgentLib = require('../../lib/fiware-iotagent-lib'); +const async = require('async'); +const utils = require('../tools/utils'); +const testUtils = require('./testUtils'); +const request = utils.request; +const logger = require('logops'); +var chaiMatchPattern = require('chai-match-pattern'); + +chai.use(chaiMatchPattern); +var _ = chaiMatchPattern.getLodashModule(); +let contextBrokerMock; + +const globalEnv = { + service: 'smartgondor', + servicePath: '/gardens', + apikey: '123456', + entity_type: 'TestType', + entity_name: 'TestType:TestDevice', + deviceId: 'TestDevice' +}; + +describe('FUNCTIONAL TESTS', function () { + describe('Basic group provision with attributes', function () { + const provision = { + url: 'http://localhost:' + config.iota.server.port + '/iot/services', + method: 'POST', + json: { + services: [ + { + resource: '/iot/json', + apikey: globalEnv.apikey, + entity_type: globalEnv.entity_type, + commands: [], + lazy: [], + attributes: [ + { + object_id: 's', + name: 'status', + type: 'Boolean' + }, + { + object_id: 't', + name: 'temperature', + type: 'Number' + } + ], + static_attributes: [] + } + ] + }, + headers: { + 'fiware-service': globalEnv.service, + 'fiware-servicepath': globalEnv.servicePath + } + }; + + const measure = { + url: 'http://localhost:' + config.http.port + '/iot/json', + method: 'POST', + qs: { + i: globalEnv.deviceId, + k: globalEnv.apikey + }, + json: { + s: false, + t: 10 + } + }; + + const expectation = { + id: globalEnv.entity_name, + type: globalEnv.entity_type, + temperature: { + value: 10, + type: 'Number' + }, + status: { + value: false, + type: 'Boolean' + } + }; + + beforeEach(function (done) { + let type = testUtils.groupToIoTAConfigType( + provision.json.services[0], + provision.headers.fiwareService, + provision.headers.fiwareServicepath + ); + config.iota.types[type.name] = type.type; + iotAgentLib.activate(config.iota, function (error) { + done(error); + }); + }); + + afterEach(function (done) { + nock.cleanAll(); + iotAgentLib.clearAll(function () { + iotAgentLib.deactivate(function () { + iotAgentLib.setDataUpdateHandler(); + iotAgentLib.setCommandHandler(); + done(); + }); + }); + }); + + it('should send its value to the Context Broker', async function () { + await testUtils.testCase(measure, expectation, provision, globalEnv, config, 'single', ''); + }); + }); + + describe('Basic group provision with attributes and multientity', function () { + const provision = { + url: 'http://localhost:' + config.iota.server.port + '/iot/services', + method: 'POST', + json: { + services: [ + { + resource: '/iot/json', + apikey: globalEnv.apikey, + entity_type: globalEnv.entity_type, + commands: [], + lazy: [], + attributes: [ + { + object_id: 's', + name: 'status', + type: 'Boolean' + }, + { + object_id: 't', + name: 'temperature', + type: 'Number', + entity_name: 'TheLightType2:MQTT_3', + entity_type: 'TheLightType2' + } + ], + static_attributes: [] + } + ] + }, + headers: { + 'fiware-service': globalEnv.service, + 'fiware-servicepath': globalEnv.servicePath + } + }; + + const measure = { + url: 'http://localhost:' + config.http.port + '/iot/json', + method: 'POST', + qs: { + i: globalEnv.deviceId, + k: globalEnv.apikey + }, + json: { + s: false, + t: 10 + } + }; + + const expectation = { + entities: [ + { + id: globalEnv.entity_name, + type: globalEnv.entity_type, + status: { + value: false, + type: 'Boolean' + } + }, + { + id: 'TheLightType2:MQTT_3', + type: 'TheLightType2', + temperature: { + value: 10, + type: 'Number' + } + } + ], + actionType: 'append' + }; + + beforeEach(function (done) { + let type = testUtils.groupToIoTAConfigType( + provision.json.services[0], + provision.headers.fiwareService, + provision.headers.fiwareServicepath + ); + config.iota.types[type.name] = type.type; + iotAgentLib.activate(config.iota, function (error) { + done(error); + }); + }); + + afterEach(function (done) { + nock.cleanAll(); + iotAgentLib.clearAll(function () { + iotAgentLib.deactivate(function () { + iotAgentLib.setDataUpdateHandler(); + iotAgentLib.setCommandHandler(); + done(); + }); + }); + }); + + it('should send its value to the Context Broker', async function () { + await testUtils.testCase(measure, expectation, provision, globalEnv, config, 'multientity', ''); + }); + }); +}); diff --git a/test/functional/testCases.js b/test/functional/testCases.js new file mode 100644 index 000000000..ecad8f86c --- /dev/null +++ b/test/functional/testCases.js @@ -0,0 +1,2314 @@ +/* + * Copyright 2023 Telefonica Investigación y Desarrollo, S.A.U + * + * This file is part of iotagent-json + * + * iotagent-json is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the License, + * or (at your option) any later version. + * + * iotagent-json is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with iotagent-json. + * If not, seehttp://www.gnu.org/licenses/. + * + * For those usages not covered by the GNU Affero General Public License + * please contact with::[contacto@tid.es] + * + * Modified by: Miguel Angel Pedraza + */ + +/* eslint-disable no-unused-vars*/ +/* eslint-disable no-unused-expressions*/ + +const config = require('./config-test.js'); +var chai = require('chai'); +var chaiMatchPattern = require('chai-match-pattern'); +chai.use(chaiMatchPattern); +var _ = chaiMatchPattern.getLodashModule(); +chai.config.truncateThreshold = 0; + +const globalEnv = { + service: 'smartgondor', + servicePath: '/gardens', + apikey: '123456', + entity_type: 'TestType', + entity_name: 'TestType:TestDevice', + deviceId: 'TestDevice' +}; + +const testCases = [ + // BASIC TESTS + { + describeName: '0010 - Simple group without attributes', + provision: { + url: 'http://localhost:' + config.iota.server.port + '/iot/services', + method: 'POST', + json: { + services: [ + { + resource: '/iot/json', + apikey: globalEnv.apikey, + entity_type: globalEnv.entity_type, + commands: [], + lazy: [], + attributes: [], + static_attributes: [] + } + ] + }, + headers: { + 'fiware-service': globalEnv.service, + 'fiware-servicepath': globalEnv.servicePath + } + }, + should: [ + { + // loglevel: 'debug', + shouldName: + 'A - WHEN sending measures through http IT should send measures to Context Broker preserving value types', + config: { + type: 'single' + }, + measure: { + url: 'http://localhost:' + config.http.port + '/iot/json', + method: 'POST', + qs: { + i: globalEnv.deviceId, + k: globalEnv.apikey + }, + json: { + a: false, + b: 10, + c: 'text', + d: 10.5, + e: [1, 2], + f: { a: 1, b: 2 } + } + }, + expectation: { + id: globalEnv.entity_name, + type: globalEnv.entity_type, + a: { + value: false, + type: 'string' + }, + b: { + value: 10, + type: 'string' + }, + c: { + type: 'string', + value: 'text' + }, + d: { + type: 'string', + value: 10.5 + }, + e: { + type: 'string', + value: [1, 2] + }, + f: { + type: 'string', + value: { + a: 1, + b: 2 + } + } + } + }, + { + transport: 'MQTT', + shouldName: + 'B - WHEN sending measures through mqtt IT should send measures to Context Broker preserving value types', + type: 'single', + measure: { + url: 'http://localhost:' + config.http.port + '/iot/json', + method: 'POST', + qs: { + i: globalEnv.deviceId, + k: globalEnv.apikey + }, + json: { + a: false, + b: 10, + c: 'text', + d: 10.5, + e: [1, 2], + f: { a: 1, b: 2 } + } + }, + expectation: { + id: globalEnv.entity_name, + type: globalEnv.entity_type, + a: { + value: false, + type: 'string' + }, + b: { + value: 10, + type: 'string' + }, + c: { + type: 'string', + value: 'text' + }, + d: { + type: 'string', + value: 10.5 + }, + e: { + type: 'string', + value: [1, 2] + }, + f: { + type: 'string', + value: { + a: 1, + b: 2 + } + } + } + } + ] + }, + { + describeName: '0020 - Simple group with active attributes', + provision: { + url: 'http://localhost:' + config.iota.server.port + '/iot/services', + method: 'POST', + json: { + services: [ + { + resource: '/iot/json', + apikey: globalEnv.apikey, + entity_type: globalEnv.entity_type, + commands: [], + lazy: [], + attributes: [ + { + object_id: 'a', + name: 'attr_a', + type: 'Boolean' + }, + { + object_id: 'b', + name: 'attr_b', + type: 'Integer' + }, + { + object_id: 'c', + name: 'attr_c', + type: 'Text' + }, + { + object_id: 'd', + name: 'attr_d', + type: 'Float' + }, + { + object_id: 'e', + name: 'attr_e', + type: 'Array' + }, + { + object_id: 'f', + name: 'attr_f', + type: 'Object' + } + ], + static_attributes: [] + } + ] + }, + headers: { + 'fiware-service': globalEnv.service, + 'fiware-servicepath': globalEnv.servicePath + } + }, + should: [ + { + shouldName: + 'A - WHEN sending defined object_ids (measures) through http IT should send measures to Context Broker preserving value types and name mappings', + type: 'single', + measure: { + url: 'http://localhost:' + config.http.port + '/iot/json', + method: 'POST', + qs: { + i: globalEnv.deviceId, + k: globalEnv.apikey + }, + json: { + a: false, + b: 10, + c: 'text', + d: 10.5, + e: [1, 2], + f: { a: 1, b: 2 } + } + }, + expectation: { + id: globalEnv.entity_name, + type: globalEnv.entity_type, + attr_a: { + value: false, + type: 'Boolean' + }, + attr_b: { + value: 10, + type: 'Integer' + }, + attr_c: { + type: 'Text', + value: 'text' + }, + attr_d: { + type: 'Float', + value: 10.5 + }, + attr_e: { + type: 'Array', + value: [1, 2] + }, + attr_f: { + type: 'Object', + value: { + a: 1, + b: 2 + } + } + } + }, + { + shouldName: + 'B - WHEN sending defined object_ids (measures) through mqtt IT should send measures to Context Broker preserving value types and name mappings', + transport: 'MQTT', + type: 'single', + measure: { + url: 'http://localhost:' + config.http.port + '/iot/json', + method: 'POST', + qs: { + i: globalEnv.deviceId, + k: globalEnv.apikey + }, + json: { + a: false, + b: 10, + c: 'text', + d: 10.5, + e: [1, 2], + f: { a: 1, b: 2 } + } + }, + expectation: { + id: globalEnv.entity_name, + type: globalEnv.entity_type, + attr_a: { + value: false, + type: 'Boolean' + }, + attr_b: { + value: 10, + type: 'Integer' + }, + attr_c: { + type: 'Text', + value: 'text' + }, + attr_d: { + type: 'Float', + value: 10.5 + }, + attr_e: { + type: 'Array', + value: [1, 2] + }, + attr_f: { + type: 'Object', + value: { + a: 1, + b: 2 + } + } + } + }, + { + shouldName: + 'C - WHEN sending undefined object_ids (measures) through http IT should send measures to Context Broker preserving value types', + type: 'single', + measure: { + url: 'http://localhost:' + config.http.port + '/iot/json', + method: 'POST', + qs: { + i: globalEnv.deviceId, + k: globalEnv.apikey + }, + json: { + u: false, + v: 10, + w: 'text', + y: 10.5, + x: [1, 2], + z: { a: 1, b: 2 } + } + }, + expectation: { + id: globalEnv.entity_name, + type: globalEnv.entity_type, + u: { + value: false, + type: 'string' + }, + v: { + value: 10, + type: 'string' + }, + w: { + type: 'string', + value: 'text' + }, + y: { + type: 'string', + value: 10.5 + }, + x: { + type: 'string', + value: [1, 2] + }, + z: { + type: 'string', + value: { + a: 1, + b: 2 + } + } + } + }, + { + shouldName: + 'D - WHEN sending undefined object_ids (measures) through mqtt IT should send measures to Context Broker preserving value types', + transport: 'MQTT', + type: 'single', + measure: { + url: 'http://localhost:' + config.http.port + '/iot/json', + method: 'POST', + qs: { + i: globalEnv.deviceId, + k: globalEnv.apikey + }, + json: { + u: false, + v: 10, + w: 'text', + y: 10.5, + x: [1, 2], + z: { a: 1, b: 2 } + } + }, + expectation: { + id: globalEnv.entity_name, + type: globalEnv.entity_type, + u: { + value: false, + type: 'string' + }, + v: { + value: 10, + type: 'string' + }, + w: { + type: 'string', + value: 'text' + }, + y: { + type: 'string', + value: 10.5 + }, + x: { + type: 'string', + value: [1, 2] + }, + z: { + type: 'string', + value: { + a: 1, + b: 2 + } + } + } + } + ] + }, + // JEXL TESTS + { + describeName: '0030 - Simple group with active attribute + JEXL expression boolean (!)', + provision: { + url: 'http://localhost:' + config.iota.server.port + '/iot/services', + method: 'POST', + json: { + services: [ + { + resource: '/iot/json', + apikey: globalEnv.apikey, + entity_type: globalEnv.entity_type, + commands: [], + lazy: [], + attributes: [ + { + object_id: 'a', + name: 'attr_a', + type: 'Boolean', + expression: '!a' + } + ], + static_attributes: [] + } + ] + }, + headers: { + 'fiware-service': globalEnv.service, + 'fiware-servicepath': globalEnv.servicePath + } + }, + should: [ + { + shouldName: + 'A - WHEN sending a boolean value (false) through http IT should send to Context Broker the value true ', + type: 'single', + measure: { + url: 'http://localhost:' + config.http.port + '/iot/json', + method: 'POST', + qs: { + i: globalEnv.deviceId, + k: globalEnv.apikey + }, + json: { + a: false + } + }, + expectation: { + id: globalEnv.entity_name, + type: globalEnv.entity_type, + attr_a: { + value: true, + type: 'Boolean' + } + } + }, + { + shouldName: + 'B - WHEN sending a numeric value (3) through http IT should send to Context Broker the value false ', + type: 'single', + measure: { + url: 'http://localhost:' + config.http.port + '/iot/json', + method: 'POST', + qs: { + i: globalEnv.deviceId, + k: globalEnv.apikey + }, + json: { + a: 3 + } + }, + expectation: { + id: globalEnv.entity_name, + type: globalEnv.entity_type, + attr_a: { + value: false, + type: 'Boolean' + } + } + }, + { + shouldName: + 'C - WHEN sending a text value (abcd) through http IT should send to Context Broker the value false ', + type: 'single', + measure: { + url: 'http://localhost:' + config.http.port + '/iot/json', + method: 'POST', + qs: { + i: globalEnv.deviceId, + k: globalEnv.apikey + }, + json: { + a: 'abcd' + } + }, + expectation: { + id: globalEnv.entity_name, + type: globalEnv.entity_type, + attr_a: { + value: false, + type: 'Boolean' + } + } + }, + { + shouldName: + 'D - WHEN not sending the object ID (undefined) required by the expression through http IT should send to Context Broker the value true ', + type: 'single', + measure: { + url: 'http://localhost:' + config.http.port + '/iot/json', + method: 'POST', + qs: { + i: globalEnv.deviceId, + k: globalEnv.apikey + }, + json: { + b: 1 + } + }, + expectation: { + id: globalEnv.entity_name, + type: globalEnv.entity_type, + attr_a: { + value: true, + type: 'Boolean' + }, + b: { + value: 1, + type: 'string' + } + } + } + ] + }, + { + describeName: '0040 - Simple group with active attribute + JEXL expression numeric (+3)', + provision: { + url: 'http://localhost:' + config.iota.server.port + '/iot/services', + method: 'POST', + json: { + services: [ + { + resource: '/iot/json', + apikey: globalEnv.apikey, + entity_type: globalEnv.entity_type, + commands: [], + lazy: [], + attributes: [ + { + object_id: 'a', + name: 'attr_a', + type: 'Number', + expression: 'a+3' + } + ], + static_attributes: [] + } + ] + }, + headers: { + 'fiware-service': globalEnv.service, + 'fiware-servicepath': globalEnv.servicePath + } + }, + should: [ + { + shouldName: + 'A - WHEN sending a boolean value (true) through http IT should send to Context Broker the value 4 ', + type: 'single', + measure: { + url: 'http://localhost:' + config.http.port + '/iot/json', + method: 'POST', + qs: { + i: globalEnv.deviceId, + k: globalEnv.apikey + }, + json: { + a: true + } + }, + expectation: { + id: globalEnv.entity_name, + type: globalEnv.entity_type, + attr_a: { + value: 4, + type: 'Number' + } + } + }, + { + shouldName: + 'B - WHEN sending a boolean value (false) through http IT should send to Context Broker the value 3 ', + type: 'single', + measure: { + url: 'http://localhost:' + config.http.port + '/iot/json', + method: 'POST', + qs: { + i: globalEnv.deviceId, + k: globalEnv.apikey + }, + json: { + a: false + } + }, + expectation: { + id: globalEnv.entity_name, + type: globalEnv.entity_type, + attr_a: { + value: 3, + type: 'Number' + } + } + }, + { + shouldName: + 'C - WHEN sending a numeric value (-7) through http IT should send to Context Broker the value -4 ', + type: 'single', + measure: { + url: 'http://localhost:' + config.http.port + '/iot/json', + method: 'POST', + qs: { + i: globalEnv.deviceId, + k: globalEnv.apikey + }, + json: { + a: -7 + } + }, + expectation: { + id: globalEnv.entity_name, + type: globalEnv.entity_type, + attr_a: { + value: -4, + type: 'Number' + } + } + }, + { + shouldName: + 'D - WHEN sending a text value (abcd) through http IT should send to Context Broker the value abcd3', + type: 'single', + measure: { + url: 'http://localhost:' + config.http.port + '/iot/json', + method: 'POST', + qs: { + i: globalEnv.deviceId, + k: globalEnv.apikey + }, + json: { + a: 'abcd' + } + }, + expectation: { + id: globalEnv.entity_name, + type: globalEnv.entity_type, + attr_a: { + value: 'abcd3', + type: 'Number' + } + } + }, + { + shouldName: + 'E - WHEN not sending the object ID (undefined) required by the expression through http IT should not send that attribute to Context Broker', + type: 'single', + measure: { + url: 'http://localhost:' + config.http.port + '/iot/json', + method: 'POST', + qs: { + i: globalEnv.deviceId, + k: globalEnv.apikey + }, + json: { + b: 1 + } + }, + expectation: { + id: globalEnv.entity_name, + type: globalEnv.entity_type, + b: { + value: 1, + type: 'string' + } + } + } + ] + }, + { + describeName: '0050 - Simple group with active attribute + JEXL expression numeric (*3)', + provision: { + url: 'http://localhost:' + config.iota.server.port + '/iot/services', + method: 'POST', + json: { + services: [ + { + resource: '/iot/json', + apikey: globalEnv.apikey, + entity_type: globalEnv.entity_type, + commands: [], + lazy: [], + attributes: [ + { + object_id: 'a', + name: 'attr_a', + type: 'Number', + expression: 'a*3' + } + ], + static_attributes: [] + } + ] + }, + headers: { + 'fiware-service': globalEnv.service, + 'fiware-servicepath': globalEnv.servicePath + } + }, + should: [ + { + shouldName: + 'A - WHEN sending a boolean value (true) through http IT should send to Context Broker the value 3 ', + type: 'single', + measure: { + url: 'http://localhost:' + config.http.port + '/iot/json', + method: 'POST', + qs: { + i: globalEnv.deviceId, + k: globalEnv.apikey + }, + json: { + a: true + } + }, + expectation: { + id: globalEnv.entity_name, + type: globalEnv.entity_type, + attr_a: { + value: 3, + type: 'Number' + } + } + }, + { + shouldName: + 'B - WHEN sending a boolean value (false) through http IT should send to Context Broker the value 0', + type: 'single', + measure: { + url: 'http://localhost:' + config.http.port + '/iot/json', + method: 'POST', + qs: { + i: globalEnv.deviceId, + k: globalEnv.apikey + }, + json: { + a: false + } + }, + expectation: { + id: globalEnv.entity_name, + type: globalEnv.entity_type, + attr_a: { + value: 0, + type: 'Number' + } + } + }, + { + shouldName: + 'C - WHEN sending a numeric value (-7) through http IT should send to Context Broker the value -21 ', + type: 'single', + measure: { + url: 'http://localhost:' + config.http.port + '/iot/json', + method: 'POST', + qs: { + i: globalEnv.deviceId, + k: globalEnv.apikey + }, + json: { + a: -7 + } + }, + expectation: { + id: globalEnv.entity_name, + type: globalEnv.entity_type, + attr_a: { + value: -21, + type: 'Number' + } + } + }, + { + shouldName: + 'D - WHEN sending a text value (abcd) through http IT should not send to anything to Context Broker', + type: 'single', + measure: { + url: 'http://localhost:' + config.http.port + '/iot/json', + method: 'POST', + qs: { + i: globalEnv.deviceId, + k: globalEnv.apikey + }, + json: { + a: 'abcd' + } + }, + expectation: [] + }, + { + shouldName: + 'E - WHEN not sending the object ID (undefined) required by the expression through http IT should not send that attribute to Context Broker', + type: 'single', + measure: { + url: 'http://localhost:' + config.http.port + '/iot/json', + method: 'POST', + qs: { + i: globalEnv.deviceId, + k: globalEnv.apikey + }, + json: { + b: 1 + } + }, + expectation: { + id: globalEnv.entity_name, + type: globalEnv.entity_type, + b: { + value: 1, + type: 'string' + } + } + } + ] + }, + { + describeName: '0060 - Simple group with active attribute + JEXL expression text (a|substr(0,2))', + provision: { + url: 'http://localhost:' + config.iota.server.port + '/iot/services', + method: 'POST', + json: { + services: [ + { + resource: '/iot/json', + apikey: globalEnv.apikey, + entity_type: globalEnv.entity_type, + commands: [], + lazy: [], + attributes: [ + { + object_id: 'a', + name: 'attr_a', + type: 'Number', + expression: 'a|substr(0,3)' + } + ], + static_attributes: [] + } + ] + }, + headers: { + 'fiware-service': globalEnv.service, + 'fiware-servicepath': globalEnv.servicePath + } + }, + should: [ + { + shouldName: + 'A - WHEN sending a boolean value (true) through http IT should send to Context Broker the value "tru" ', + type: 'single', + measure: { + url: 'http://localhost:' + config.http.port + '/iot/json', + method: 'POST', + qs: { + i: globalEnv.deviceId, + k: globalEnv.apikey + }, + json: { + a: true + } + }, + expectation: { + id: globalEnv.entity_name, + type: globalEnv.entity_type, + attr_a: { + value: 'tru', + type: 'Number' + } + } + }, + { + shouldName: + 'B - WHEN sending a boolean value (false) through http IT should send to Context Broker the value "fal"', + type: 'single', + measure: { + url: 'http://localhost:' + config.http.port + '/iot/json', + method: 'POST', + qs: { + i: globalEnv.deviceId, + k: globalEnv.apikey + }, + json: { + a: false + } + }, + expectation: { + id: globalEnv.entity_name, + type: globalEnv.entity_type, + attr_a: { + value: 'fal', + type: 'Number' + } + } + }, + { + shouldName: + 'C - WHEN sending a numeric value (-7) through http IT should send to Context Broker the value "-7" ', + type: 'single', + measure: { + url: 'http://localhost:' + config.http.port + '/iot/json', + method: 'POST', + qs: { + i: globalEnv.deviceId, + k: globalEnv.apikey + }, + json: { + a: -7 + } + }, + expectation: { + id: globalEnv.entity_name, + type: globalEnv.entity_type, + attr_a: { + value: '-7', + type: 'Number' + } + } + }, + { + shouldName: + 'D - WHEN sending a text value (abcd) through http IT should not send to anything to Context Broker', + type: 'single', + measure: { + url: 'http://localhost:' + config.http.port + '/iot/json', + method: 'POST', + qs: { + i: globalEnv.deviceId, + k: globalEnv.apikey + }, + json: { + a: 'abcd' + } + }, + expectation: { + id: globalEnv.entity_name, + type: globalEnv.entity_type, + attr_a: { + value: 'abc', + type: 'Number' + } + } + }, + { + shouldName: + 'E - WHEN not sending the object ID (undefined) required by the expression through http IT should not send that attribute to Context Broker', + type: 'single', + measure: { + url: 'http://localhost:' + config.http.port + '/iot/json', + method: 'POST', + qs: { + i: globalEnv.deviceId, + k: globalEnv.apikey + }, + json: { + b: 1 + } + }, + expectation: { + id: globalEnv.entity_name, + type: globalEnv.entity_type, + attr_a: { + value: 'und', + type: 'Number' + }, + b: { + value: 1, + type: 'string' + } + } + } + ] + }, + { + describeName: '0070 - Simple group with active attribute + chained JEXL expression text (a|substr(0,2))', + provision: { + url: 'http://localhost:' + config.iota.server.port + '/iot/services', + method: 'POST', + json: { + services: [ + { + resource: '/iot/json', + apikey: globalEnv.apikey, + entity_type: globalEnv.entity_type, + commands: [], + lazy: [], + attributes: [ + { + object_id: 'a', + name: 'a', + type: 'Text', + expression: 'a | trim | replacestr("hello","hi")' + } + ], + static_attributes: [] + } + ] + }, + headers: { + 'fiware-service': globalEnv.service, + 'fiware-servicepath': globalEnv.servicePath + } + }, + should: [ + { + shouldName: + 'A - WHEN sending a expected value ("Say hello and smile") through http IT should send to Context Broker the value "Say hi and smile" ', + type: 'single', + measure: { + url: 'http://localhost:' + config.http.port + '/iot/json', + method: 'POST', + qs: { + i: globalEnv.deviceId, + k: globalEnv.apikey + }, + json: { + a: 'Say hello and smile' + } + }, + expectation: { + id: globalEnv.entity_name, + type: globalEnv.entity_type, + a: { + value: 'Say hi and smile', + type: 'Text' + } + } + } + ] + }, + { + describeName: '0080 - Simple group with active attribute + JEXL expression text reusing previous values)', + provision: { + url: 'http://localhost:' + config.iota.server.port + '/iot/services', + method: 'POST', + json: { + services: [ + { + resource: '/iot/json', + apikey: globalEnv.apikey, + entity_type: globalEnv.entity_type, + commands: [], + lazy: [], + attributes: [ + { + object_id: 'a', + name: 'attr_a', + type: 'Number', + expression: 'a*10' + }, + { + object_id: 'b', + name: 'attr_b', + type: 'Number', + expression: 'attr_a*10' + } + ], + static_attributes: [] + } + ] + }, + headers: { + 'fiware-service': globalEnv.service, + 'fiware-servicepath': globalEnv.servicePath + } + }, + should: [ + { + shouldName: + 'A - WHEN sending a value (a:3) through http IT should apply nested expressions and send to Context Broker the value "attr_b=300" ', + type: 'single', + measure: { + url: 'http://localhost:' + config.http.port + '/iot/json', + method: 'POST', + qs: { + i: globalEnv.deviceId, + k: globalEnv.apikey + }, + json: { + a: 3 + } + }, + expectation: { + id: globalEnv.entity_name, + type: globalEnv.entity_type, + attr_a: { + value: 30, + type: 'Number' + }, + attr_b: { + value: 300, + type: 'Number' + } + } + } + ] + }, + { + describeName: '0090 - Simple group with active attribute + JEXL expression referencing static attributes', + provision: { + url: 'http://localhost:' + config.iota.server.port + '/iot/services', + method: 'POST', + json: { + services: [ + { + resource: '/iot/json', + apikey: globalEnv.apikey, + entity_type: globalEnv.entity_type, + commands: [], + lazy: [], + attributes: [ + { + object_id: 'a', + name: 'attr_a', + type: 'Number', + expression: 'a*coef' + } + ], + static_attributes: [ + { + name: 'coef', + type: 'Number', + value: 1.5 + } + ] + } + ] + }, + headers: { + 'fiware-service': globalEnv.service, + 'fiware-servicepath': globalEnv.servicePath + } + }, + should: [ + { + shouldName: + 'A - WHEN sending a value (a:6) through http IT should apply the expression using the static attribute value and send to Context Broker the value "attr_a:9" ', + type: 'single', + measure: { + url: 'http://localhost:' + config.http.port + '/iot/json', + method: 'POST', + qs: { + i: globalEnv.deviceId, + k: globalEnv.apikey + }, + json: { + a: 6 + } + }, + expectation: { + id: globalEnv.entity_name, + type: globalEnv.entity_type, + attr_a: { + value: 9, + type: 'Number' + }, + coef: { + value: 1.5, + type: 'Number' + } + } + } + ] + }, + { + describeName: '0100 - Simple group with active attribute + JEXL expression referencing context attributes', + skip: 'lib', // Explanation in #1523 + provision: { + url: 'http://localhost:' + config.iota.server.port + '/iot/services', + method: 'POST', + json: { + services: [ + { + resource: '/iot/json', + apikey: globalEnv.apikey, + entity_type: globalEnv.entity_type, + commands: [], + lazy: [], + attributes: [ + { + object_id: 'a', + name: 'attr_a', + type: 'Text', + expression: 'a+":"+service+subservice+id+type' + } + ] + } + ] + }, + headers: { + 'fiware-service': globalEnv.service, + 'fiware-servicepath': globalEnv.servicePath + } + }, + should: [ + { + shouldName: + 'A - WHEN sending a value (text) through http IT should apply the expression using the context attributes value and send to Context Broker the value "text:smartgondor/gardensTestDeviceTestType" ', + type: 'single', + measure: { + url: 'http://localhost:' + config.http.port + '/iot/json', + method: 'POST', + qs: { + i: globalEnv.deviceId, + k: globalEnv.apikey + }, + json: { + a: 'text' + } + }, + expectation: { + id: globalEnv.entity_name, + type: globalEnv.entity_type, + attr_a: { + value: + 'text:' + + globalEnv.service + + globalEnv.servicePath + + globalEnv.deviceId + + globalEnv.entity_type, + type: 'Text' + } + } + } + ] + }, + { + describeName: '0110 - Simple group with active attributes + JEXL multiples expressions at same time', + skip: 'lib', // Explanation in #1523 + provision: { + url: 'http://localhost:' + config.iota.server.port + '/iot/services', + method: 'POST', + json: { + services: [ + { + resource: '/iot/json', + apikey: globalEnv.apikey, + entity_type: globalEnv.entity_type, + commands: [], + lazy: [], + attributes: [ + { + object_id: 'a', + name: 'attr_a', + type: 'Boolean', + expression: '!a' + }, + { + object_id: 'b', + name: 'attr_b', + type: 'Integer', + expression: 'b+1' + }, + { + object_id: 'c', + name: 'attr_c', + type: 'Text', + expression: 'c+":"+service+subservice+id+type' + }, + { + object_id: 'd', + name: 'attr_d', + type: 'Float', + expression: 'd/2+attr_b' + }, + { + object_id: 'e', + name: 'attr_e', + type: 'Array', + expression: 'e|concatarr([3,4])' + }, + { + object_id: 'f', + name: 'attr_f', + type: 'Object', + expression: '{coordinates: [f.a,f.b], type: "Point"}' + } + ], + static_attributes: [] + } + ] + }, + headers: { + 'fiware-service': globalEnv.service, + 'fiware-servicepath': globalEnv.servicePath + } + }, + should: [ + { + shouldName: + 'A - WHEN sending multiples object_ids (measures) through http IT should send measures to Context Broker applying all expressions at same time', + type: 'single', + measure: { + url: 'http://localhost:' + config.http.port + '/iot/json', + method: 'POST', + qs: { + i: globalEnv.deviceId, + k: globalEnv.apikey + }, + json: { + a: false, + b: 10, + c: 'text', + d: 10.5, + e: [1, 2], + f: { a: 1, b: 2 } + } + }, + expectation: { + id: globalEnv.entity_name, + type: globalEnv.entity_type, + attr_a: { + value: true, + type: 'Boolean' + }, + attr_b: { + value: 11, + type: 'Integer' + }, + attr_c: { + type: 'Text', + value: + 'text:' + + globalEnv.service + + globalEnv.servicePath + + globalEnv.deviceId + + globalEnv.entity_type + }, + attr_d: { + type: 'Float', + value: 16.25 + }, + attr_e: { + type: 'Array', + value: [1, 2, 3, 4] + }, + attr_f: { + type: 'Object', + value: { coordinates: [1, 2], type: 'Point' } + } + } + } + ] + }, + { + describeName: + '00X0 - Simple group with JEXL expression using static attribute - addressing structured values (JSON)', + provision: { + url: 'http://localhost:' + config.iota.server.port + '/iot/services', + method: 'POST', + json: { + services: [ + { + resource: '/iot/json', + apikey: globalEnv.apikey, + entity_type: globalEnv.entity_type, + commands: [], + lazy: [], + attributes: [ + { + object_id: 'a', + name: 'attr_a', + type: 'Boolean', + expression: 'a?threshold[90|tostring].max:true' + } + ], + static_attributes: [ + { + name: 'threshold', + type: 'Object', + value: { + '90': { max: 10, min: 1 }, + '92': { max: 12, min: 2 }, + '93': { max: 13, min: 3 } + } + } + ], + explicitAttrs: "['attr_a']" + } + ] + }, + headers: { + 'fiware-service': globalEnv.service, + 'fiware-servicepath': globalEnv.servicePath + } + }, + should: [ + { + shouldName: + 'A - WHEN sending a numeric value () through http IT should send to Context Broker the value true ', + type: 'single', + isRegex: true, + measure: { + url: 'http://localhost:' + config.http.port + '/iot/json', + method: 'POST', + qs: { + i: globalEnv.deviceId, + k: globalEnv.apikey + }, + json: { + a: false + } + }, + expectation: { + id: globalEnv.entity_name, + type: globalEnv.entity_type, + attr_a: { + value: true, + type: 'Boolean' + } + } + } + ] + }, + // TIMESTAMP TESTS + { + describeName: '0150 - Simple group with active attribute + timestamp:false', + provision: { + url: 'http://localhost:' + config.iota.server.port + '/iot/services', + method: 'POST', + json: { + services: [ + { + resource: '/iot/json', + apikey: globalEnv.apikey, + entity_type: globalEnv.entity_type, + timestamp: false, + commands: [], + lazy: [], + attributes: [ + { + object_id: 'a', + name: 'attr_a', + type: 'Number' + } + ] + } + ] + }, + headers: { + 'fiware-service': globalEnv.service, + 'fiware-servicepath': globalEnv.servicePath + } + }, + should: [ + { + shouldName: + 'A - WHEN sending a measure not named TimeInstant through http IT should not add the timestamp to the attributes sent to Context Broker', + type: 'single', + measure: { + url: 'http://localhost:' + config.http.port + '/iot/json', + method: 'POST', + qs: { + i: globalEnv.deviceId, + k: globalEnv.apikey + }, + json: { + a: 23 + } + }, + expectation: { + id: globalEnv.entity_name, + type: globalEnv.entity_type, + attr_a: { + value: 23, + type: 'Number' + } + } + }, + { + shouldName: + 'B - WHEN sending a measure named TimeInstant through http IT should not add the timestamp to the other attributes sent to Context Broker', + type: 'single', + measure: { + url: 'http://localhost:' + config.http.port + '/iot/json', + method: 'POST', + qs: { + i: globalEnv.deviceId, + k: globalEnv.apikey + }, + json: { + TimeInstant: '2015-12-14T08:06:01.468Z', + a: 23 + } + }, + expectation: { + id: globalEnv.entity_name, + type: globalEnv.entity_type, + attr_a: { + value: 23, + type: 'Number' + }, + TimeInstant: { + value: '2015-12-14T08:06:01.468Z', + type: 'DateTime' + } + } + } + ] + }, + { + describeName: '0160 - Simple group with active attribute + timestamp:true', + provision: { + url: 'http://localhost:' + config.iota.server.port + '/iot/services', + method: 'POST', + json: { + services: [ + { + resource: '/iot/json', + apikey: globalEnv.apikey, + entity_type: globalEnv.entity_type, + timestamp: true, + commands: [], + lazy: [], + attributes: [ + { + object_id: 'a', + name: 'attr_a', + type: 'Number' + } + ] + } + ] + }, + headers: { + 'fiware-service': globalEnv.service, + 'fiware-servicepath': globalEnv.servicePath + } + }, + should: [ + { + shouldName: + 'A - WHEN sending a measure not named TimeInstant through http IT should add the timestamp to the attributes sent to Context Broker', + type: 'single', + isRegex: true, + measure: { + url: 'http://localhost:' + config.http.port + '/iot/json', + method: 'POST', + qs: { + i: globalEnv.deviceId, + k: globalEnv.apikey + }, + json: { + a: 21 + } + }, + expectation: { + id: globalEnv.entity_name, + type: globalEnv.entity_type, + attr_a: { + value: 21, + type: 'Number', + metadata: { + TimeInstant: { + type: 'DateTime', + value: _.isDateString + } + } + }, + TimeInstant: { + type: 'DateTime', + value: _.isDateString + } + } + }, + { + shouldName: + 'B - WHEN sending a measure named TimeInstant through http IT should not add the timestamp to the other attributes sent to Context Broker', + type: 'single', + measure: { + url: 'http://localhost:' + config.http.port + '/iot/json', + method: 'POST', + qs: { + i: globalEnv.deviceId, + k: globalEnv.apikey + }, + json: { + TimeInstant: '2015-12-14T08:06:01.468Z', + a: 23 + } + }, + expectation: { + id: globalEnv.entity_name, + type: globalEnv.entity_type, + attr_a: { + value: 23, + type: 'Number', + metadata: { + TimeInstant: { + type: 'DateTime', + value: '2015-12-14T08:06:01.468Z' + } + } + }, + TimeInstant: { + value: '2015-12-14T08:06:01.468Z', + type: 'DateTime' + } + } + } + ] + }, + { + describeName: '0170 - Simple group with active attribute + timestamp not defined', + provision: { + url: 'http://localhost:' + config.iota.server.port + '/iot/services', + method: 'POST', + json: { + services: [ + { + resource: '/iot/json', + apikey: globalEnv.apikey, + entity_type: globalEnv.entity_type, + commands: [], + lazy: [], + attributes: [ + { + object_id: 'a', + name: 'attr_a', + type: 'Number' + } + ] + } + ] + }, + headers: { + 'fiware-service': globalEnv.service, + 'fiware-servicepath': globalEnv.servicePath + } + }, + should: [ + { + shouldName: + 'A - WHEN sending a measure not named TimeInstant through http IT should not add the timestamp to the attributes or metadata sent to Context Broker', + type: 'single', + measure: { + url: 'http://localhost:' + config.http.port + '/iot/json', + method: 'POST', + qs: { + i: globalEnv.deviceId, + k: globalEnv.apikey + }, + json: { + a: 21 + } + }, + expectation: { + id: globalEnv.entity_name, + type: globalEnv.entity_type, + attr_a: { + value: 21, + type: 'Number' + } + } + }, + { + shouldName: + 'B - WHEN sending a measure named TimeInstant through http IT should not add the timestamp to the other attributes sent to Context Broker', + type: 'single', + measure: { + url: 'http://localhost:' + config.http.port + '/iot/json', + method: 'POST', + qs: { + i: globalEnv.deviceId, + k: globalEnv.apikey + }, + json: { + TimeInstant: '2015-12-14T08:06:01.468Z', + a: 23 + } + }, + expectation: { + id: globalEnv.entity_name, + type: globalEnv.entity_type, + attr_a: { + value: 23, + type: 'Number' + }, + TimeInstant: { + value: '2015-12-14T08:06:01.468Z', + type: 'DateTime' + } + } + } + ] + }, + // STATIC ATTRIBUTES TESTS + { + describeName: + '0200 - Simple group with active attributes + Static attributes + JEXL expression using static attribute value', + provision: { + url: 'http://localhost:' + config.iota.server.port + '/iot/services', + method: 'POST', + json: { + services: [ + { + resource: '/iot/json', + apikey: globalEnv.apikey, + entity_type: globalEnv.entity_type, + commands: [], + lazy: [], + attributes: [ + { + object_id: 'a', + name: 'attr_a', + type: 'Number', + expression: 'a*static_a' + } + ], + static_attributes: [ + { + name: 'static_a', + type: 'Number', + value: 3 + } + ] + } + ] + }, + headers: { + 'fiware-service': globalEnv.service, + 'fiware-servicepath': globalEnv.servicePath + } + }, + should: [ + { + shouldName: + 'A - WHEN sending not provisioned object_ids (measures) through http IT should store static attributes into Context Broker', + type: 'single', + measure: { + url: 'http://localhost:' + config.http.port + '/iot/json', + method: 'POST', + qs: { + i: globalEnv.deviceId, + k: globalEnv.apikey + }, + json: { + b: 10 + } + }, + expectation: { + id: globalEnv.entity_name, + type: globalEnv.entity_type, + b: { + value: 10, + type: 'string' + }, + static_a: { + value: 3, + type: 'Number' + } + } + }, + { + shouldName: + 'B - WHEN sending provisioned object_ids (measures) through http IT should store static attributes into Context Broker + calculate expression based on static', + type: 'single', + measure: { + url: 'http://localhost:' + config.http.port + '/iot/json', + method: 'POST', + qs: { + i: globalEnv.deviceId, + k: globalEnv.apikey + }, + json: { + a: 10 + } + }, + expectation: { + id: globalEnv.entity_name, + type: globalEnv.entity_type, + attr_a: { + value: 30, + type: 'Number' + }, + static_a: { + value: 3, + type: 'Number' + } + } + } + ] + }, + { + describeName: '0210 - Simple group with active attributes + JSON value Static attributes ', + provision: { + url: 'http://localhost:' + config.iota.server.port + '/iot/services', + method: 'POST', + json: { + services: [ + { + resource: '/iot/json', + apikey: globalEnv.apikey, + entity_type: globalEnv.entity_type, + commands: [], + lazy: [], + attributes: [], + static_attributes: [ + { + name: 'static_a', + type: 'Number', + value: { + v1: 1, + v2: { + v3: 3, + v4: 4 + } + } + } + ] + } + ] + }, + headers: { + 'fiware-service': globalEnv.service, + 'fiware-servicepath': globalEnv.servicePath + } + }, + should: [ + { + shouldName: + 'A - WHEN sending measures through http IT should store complex static attributes into Context Broker', + type: 'single', + measure: { + url: 'http://localhost:' + config.http.port + '/iot/json', + method: 'POST', + qs: { + i: globalEnv.deviceId, + k: globalEnv.apikey + }, + json: { + a: 10 + } + }, + expectation: { + id: globalEnv.entity_name, + type: globalEnv.entity_type, + a: { + value: 10, + type: 'string' + }, + static_a: { + value: { + v1: 1, + v2: { + v3: 3, + v4: 4 + } + }, + type: 'Number' + } + } + } + ] + }, + // EXPLICIT ATTRIBUTES TESTS + { + describeName: '0300 - Group with explicit attrs:false (boolean) + active atributes', + provision: { + url: 'http://localhost:' + config.iota.server.port + '/iot/services', + method: 'POST', + json: { + services: [ + { + resource: '/iot/json', + apikey: globalEnv.apikey, + entity_type: globalEnv.entity_type, + explicitAttrs: false, + commands: [], + lazy: [], + attributes: [ + { + object_id: 'a', + name: 'attr_a', + type: 'Number' + } + ], + static_attributes: [] + } + ] + }, + headers: { + 'fiware-service': globalEnv.service, + 'fiware-servicepath': globalEnv.servicePath + } + }, + should: [ + { + shouldName: + 'A - WHEN sending both provisioned and not object_ids (measures) through http IT should store all attributes into Context Broker', + type: 'single', + measure: { + url: 'http://localhost:' + config.http.port + '/iot/json', + method: 'POST', + qs: { + i: globalEnv.deviceId, + k: globalEnv.apikey + }, + json: { + a: 3, + b: 10 + } + }, + expectation: { + id: globalEnv.entity_name, + type: globalEnv.entity_type, + b: { + value: 10, + type: 'string' + }, + attr_a: { + value: 3, + type: 'Number' + } + } + } + ] + }, + { + describeName: '0310 - Group without explicit (not defined) + active atributes', + provision: { + url: 'http://localhost:' + config.iota.server.port + '/iot/services', + method: 'POST', + json: { + services: [ + { + resource: '/iot/json', + apikey: globalEnv.apikey, + entity_type: globalEnv.entity_type, + commands: [], + lazy: [], + attributes: [ + { + object_id: 'a', + name: 'attr_a', + type: 'Number' + } + ], + static_attributes: [] + } + ] + }, + headers: { + 'fiware-service': globalEnv.service, + 'fiware-servicepath': globalEnv.servicePath + } + }, + should: [ + { + shouldName: + 'A - WHEN sending both provisioned and not object_ids (measures) through http IT should store all attributes into Context Broker', + type: 'single', + measure: { + url: 'http://localhost:' + config.http.port + '/iot/json', + method: 'POST', + qs: { + i: globalEnv.deviceId, + k: globalEnv.apikey + }, + json: { + a: 3, + b: 10 + } + }, + expectation: { + id: globalEnv.entity_name, + type: globalEnv.entity_type, + b: { + value: 10, + type: 'string' + }, + attr_a: { + value: 3, + type: 'Number' + } + } + } + ] + }, + { + describeName: '0320 - Group with explicit attrs:true (boolean) + active atributes', + provision: { + url: 'http://localhost:' + config.iota.server.port + '/iot/services', + method: 'POST', + json: { + services: [ + { + resource: '/iot/json', + apikey: globalEnv.apikey, + entity_type: globalEnv.entity_type, + explicitAttrs: true, + commands: [], + lazy: [], + attributes: [ + { + object_id: 'a', + name: 'attr_a', + type: 'Number' + } + ], + static_attributes: [] + } + ] + }, + headers: { + 'fiware-service': globalEnv.service, + 'fiware-servicepath': globalEnv.servicePath + } + }, + should: [ + { + shouldName: + 'A - WHEN sending both provisioned and not object_ids (measures) through http IT should store all attributes into Context Broker', + type: 'single', + measure: { + url: 'http://localhost:' + config.http.port + '/iot/json', + method: 'POST', + qs: { + i: globalEnv.deviceId, + k: globalEnv.apikey + }, + json: { + a: 3, + b: 10 + } + }, + expectation: { + id: globalEnv.entity_name, + type: globalEnv.entity_type, + attr_a: { + value: 3, + type: 'Number' + } + } + } + ] + }, + { + describeName: + '0330 - Group with explicit attrs: JEXL expression based on measure resulting boolean + active attributes + static attributes', + provision: { + url: 'http://localhost:' + config.iota.server.port + '/iot/services', + method: 'POST', + json: { + services: [ + { + resource: '/iot/json', + apikey: globalEnv.apikey, + entity_type: globalEnv.entity_type, + explicitAttrs: 'c?true:false', + commands: [], + lazy: [], + attributes: [ + { + name: 'attr_a', + object_id: 'a', + type: 'Number' + }, + { + name: 'attr_b', + object_id: 'b', + type: 'Number' + } + ], + static_attributes: [ + { + name: 'static_a', + type: 'Number', + value: 3 + }, + { + name: 'static_b', + type: 'Number', + value: 4 + } + ] + } + ] + }, + headers: { + 'fiware-service': globalEnv.service, + 'fiware-servicepath': globalEnv.servicePath + } + }, + should: [ + { + shouldName: + 'A - WHEN sending both provisioned and not object_ids (measures) through http and being the explicitAttrs JEXL result true IT should store only explicit attrs into Context Broker', + type: 'single', + measure: { + url: 'http://localhost:' + config.http.port + '/iot/json', + method: 'POST', + qs: { + i: globalEnv.deviceId, + k: globalEnv.apikey + }, + json: { + a: 3, + b: 10, + c: 11, + d: 12 + } + }, + expectation: { + id: globalEnv.entity_name, + type: globalEnv.entity_type, + attr_a: { + value: 3, + type: 'Number' + }, + attr_b: { + value: 10, + type: 'Number' + }, + static_a: { + value: 3, + type: 'Number' + }, + static_b: { + value: 4, + type: 'Number' + } + } + }, + { + shouldName: + 'A - WHEN sending both provisioned and not object_ids (measures) through http and being the explicitAttrs JEXL result false IT should store all attrs into Context Broker', + type: 'single', + measure: { + url: 'http://localhost:' + config.http.port + '/iot/json', + method: 'POST', + qs: { + i: globalEnv.deviceId, + k: globalEnv.apikey + }, + json: { + a: 3, + b: 10, + d: 12 + } + }, + expectation: { + id: globalEnv.entity_name, + type: globalEnv.entity_type, + attr_a: { + value: 3, + type: 'Number' + }, + attr_b: { + value: 10, + type: 'Number' + }, + d: { + value: 12, + type: 'string' + }, + static_a: { + value: 3, + type: 'Number' + }, + static_b: { + value: 4, + type: 'Number' + } + } + } + ] + }, + { + describeName: '0340 - Group with explicit attrs:[array] + active attributes + static attributes', + provision: { + url: 'http://localhost:' + config.iota.server.port + '/iot/services', + method: 'POST', + json: { + services: [ + { + resource: '/iot/json', + apikey: globalEnv.apikey, + entity_type: globalEnv.entity_type, + explicitAttrs: "['attr_a','static_a']", + commands: [], + lazy: [], + attributes: [ + { + name: 'attr_a', + object_id: 'a', + type: 'Number' + }, + { + name: 'attr_b', + object_id: 'b', + type: 'Number' + } + ], + static_attributes: [ + { + name: 'static_a', + type: 'Number', + value: 3 + }, + { + name: 'static_b', + type: 'Number', + value: 4 + } + ] + } + ] + }, + headers: { + 'fiware-service': globalEnv.service, + 'fiware-servicepath': globalEnv.servicePath + } + }, + should: [ + { + shouldName: + 'A - WHEN sending both provisioned and not object_ids (measures) through http IT should store only defined in explicitAttrs array into Context Broker', + type: 'single', + measure: { + url: 'http://localhost:' + config.http.port + '/iot/json', + method: 'POST', + qs: { + i: globalEnv.deviceId, + k: globalEnv.apikey + }, + json: { + a: 3, + b: 10, + c: 11 + } + }, + expectation: { + id: globalEnv.entity_name, + type: globalEnv.entity_type, + attr_a: { + value: 3, + type: 'Number' + }, + static_a: { + value: 3, + type: 'Number' + } + } + } + ] + } +]; + +exports.testCases = testCases; diff --git a/test/functional/testUtils.js b/test/functional/testUtils.js new file mode 100644 index 000000000..8f1859f8b --- /dev/null +++ b/test/functional/testUtils.js @@ -0,0 +1,251 @@ +/* + * Copyright 2023 Telefonica Investigación y Desarrollo, S.A.U + * + * This file is part of iotagent-json + * + * iotagent-json is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the License, + * or (at your option) any later version. + * + * iotagent-json is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with iotagent-json. + * If not, seehttp://www.gnu.org/licenses/. + * + * For those usages not covered by the GNU Affero General Public License + * please contact with::[contacto@tid.es] + * + * Modified by: Miguel Angel Pedraza + */ + +/* eslint-disable no-unused-vars*/ +/* eslint-disable no-unused-expressions*/ + +const nock = require('nock'); +const utils = require('../tools/utils'); +const request = utils.request; +const async = require('async'); +const chai = require('chai'); +const MQTT = require('async-mqtt'); +const iotAgentLib = require('../../lib/fiware-iotagent-lib'); + +var chaiMatchPattern = require('chai-match-pattern'); +chai.use(chaiMatchPattern); +var _ = chaiMatchPattern.getLodashModule(); +var expect = chai.expect; +chai.config.truncateThreshold = 0; + +// Error messages +const ERR_CB_EXPECTATION_DIFFER = 'Assertion Error - Context Broker received payload differs from expectation'; +const ERR_MEAS_BODY = 'Assertion Error - Measure response is not empty'; +const ERR_MEAS_CODE = 'Assertion Error - Measure response status code differs from 200'; +const ERR_MQTT = 'Error with MQTT: '; +const ERR_CB_NOT_EMPTY = 'Assertion Error - unexpected Context Broker request received (no request expected)'; +const DEF_TYPE = 'TestType'; +/** + * @brief Sends a measure to the IoT Agent and returns a promise with the response + * + * @param {Object} measure Measure to be sent to the IoT Agent + */ +function sendMeasureHttp(measure) { + return new Promise((resolve, reject) => { + request(measure, function (error, result, body) { + error ? reject(error) : resolve(result); + }); + }); +} + +/** + * @brief Sends a measure to the IoT Agent and returns a promise with the response + * + * @param {Object} measure Measure to be sent to the IoT Agent + */ +function sendMeasureIotaLib(measure, provision) { + return new Promise((resolve, reject) => { + /** + * WARNING: This is kind of a hack, only required for the tests using Lib, since the method iotAgentLib.update + * requires a type and does not check the type of the group. For this purpose, this function uses the + * provision type, setting the measure type to the same type as the provision. + * This is not a problem for the tests using other transports than Lib, in that case, the type will be retrieved + * from the real provision. + */ + let type; + if (Array.isArray(provision.json.services) && provision.json.services.length > 0) { + type = provision.json.services[0].entity_type; + } else { + type = DEF_TYPE; + } + iotAgentLib.update( + type + ':' + measure.qs.i, + type, + '', + jsonToIotaMeasures(measure.json), + function (error, result, body) { + error ? reject(error) : resolve(result); + } + ); + }); +} + +/** + * @brief Converts a IOTA JSON object to an array of measures as expected by the IOTA Lib + * + * @param {Object} json + * @returns {Array} measures + */ +function jsonToIotaMeasures(json) { + let measures = []; + for (let key in json) { + /* eslint-disable-next-line no-prototype-builtins */ + if (json.hasOwnProperty(key)) { + let measure = { + name: key, + value: json[key] + }; + // A bit of Magic. If the key is TimeInstant, we set the type to DateTime. + // When sending the data through iot + if (key === 'TimeInstant') { + measure.type = 'DateTime'; + } else { + measure.type = 'string'; + } + measures.push(measure); + } + } + return measures; +} + +/** + * @brief Delays the execution of the code for the specified time in ms + * + * @param {Number} time in ms + * @returns + */ +const delay = (time) => new Promise((res) => setTimeout(res, time)); + +function groupToIoTAConfigType(group, service, subservice) { + let type = {}; + for (var key in group) { + /* eslint-disable-next-line no-prototype-builtins */ + if (group.hasOwnProperty(key)) { + if (key === 'attributes') { + type.active = group.attributes; + } else if (key === 'entity_type') { + type.type = group.entity_type; + } else if (key === 'static_attributes') { + type.staticAttributes = group.static_attributes; + } else if (key !== 'resource') { + type[key] = group[key]; + } + } + } + type.service = service; + type.subservice = subservice; + return { name: group.entity_type, type: type }; +} + +/** + * Test Case function + * @brief Sends a measure to the IoT Agent and validates the response + * and validates the Context Broker expectation + * + * @param {Object} measure Measure to be sent to the IoT Agent + * @param {Object} expectation Expectation for the Context Broker + * @param {Object} env Environment variables + * @param {Object} config IoTA Configuration object + * @param {String} type Type of test (multientity or multimeasure) + * @param {String} transport Transport to be used (Lib, HTTP or MQTT). If not specified, Lib is used. + * @param {Boolean} regex If true, the expectation is treated as a regex + */ +async function testCase(measure, expectation, provision, env, config, type, transport, regex) { + let receivedContext = []; + let cbMockRoute = ''; + // Set the correct route depending if the test is multientity or not + if (type === 'multientity') { + cbMockRoute = '/v2/op/update'; + } else { + cbMockRoute = '/v2/entities?options=upsert'; + } + + // Set the correct mock times depending if the test is multimeasure or not + // based on the length of the expectation array + let mockTimes = 1; + if (expectation.length > 1) { + mockTimes = expectation.length; + } + + let contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', env.service) + .matchHeader('fiware-servicepath', env.servicePath) + .post(cbMockRoute, function (body) { + mockTimes === 1 ? (receivedContext = body) : receivedContext.push(body); // Save the received body for later comparison + return true; + }) + .times(mockTimes) + .reply(204); + + // Send a measure to the IoT Agent and wait for the response + if (transport === 'MQTT') { + try { + let client = await MQTT.connectAsync('mqtt://' + config.mqtt.host); + await client.publish('/' + measure.qs.k + '/' + measure.qs.i + '/attrs', JSON.stringify(measure.json)); + await client.end(); + } catch (error) { + expect.fail(ERR_MQTT + error); + } + } else if (transport === 'HTTP') { + // HTTP + const response = await sendMeasureHttp(measure); + // Validate the response status code and the response body + expect(response.statusCode, ERR_MEAS_CODE).to.equal(200); + expect(response.body, ERR_MEAS_BODY).to.be.empty; + } else { + const response = await sendMeasureIotaLib(measure, provision); + } + + // Validate Context Broker Expectation + if ((Array.isArray(expectation) && expectation.length > 0) || !Array.isArray(expectation)) { + // Filter empty expectations + regex && regex === true + ? expect(receivedContext, ERR_CB_EXPECTATION_DIFFER).to.matchPattern(expectation) + : expect(receivedContext, ERR_CB_EXPECTATION_DIFFER).to.deep.equal(expectation); + contextBrokerMock.done(); // Ensure the request was made, no matter the body content + } else { + // If empty expectation, ensure no request was made + expect(contextBrokerMock.isDone(), ERR_CB_NOT_EMPTY).to.be.false; + expect(receivedContext, ERR_CB_NOT_EMPTY).to.be.empty; + } +} + +/** + * + * @param {*} skip skip string from test case. I.E: "lib, !json" + * @param {*} matchPattern skip pattern to check. I.E: "lib" + * @returns true if the test should be skipped. False otherwise + */ +function checkSkip(skip, matchPattern) { + var isMatch = false; + // Separate tokens by comma or space, and remove empty tokens + var tokens = skip.split(/[ , ]+/).filter(function (value, index, arr) { + return value !== '' && !value.match(/[* ]+/); + }); + // Check if the skip pattern is in the tokens array, or there is a token starting with ! without the pattern (negative match -!b) + tokens.forEach((element) => { + if (element === matchPattern || (element[0] === '!' && element.substr(1) !== matchPattern)) { + isMatch = true; + } + }); + return isMatch; +} + +exports.checkSkip = checkSkip; +exports.sendMeasureHttp = sendMeasureHttp; +exports.sendMeasureIotaLib = sendMeasureIotaLib; +exports.delayMs = delay; +exports.testCase = testCase; +exports.groupToIoTAConfigType = groupToIoTAConfigType;