diff --git a/README.md b/README.md index 9e814014b..c4e3f5192 100644 --- a/README.md +++ b/README.md @@ -124,6 +124,8 @@ To run tests, type npm test ``` +Please have a look to extra information about functional tests in [this specific document](test/functional/README.md). + #### Requirements All the tests are designed to test end-to-end scenarios, and there are some requirements for its current execution: diff --git a/package.json b/package.json index e60a6386e..748e8f341 100644 --- a/package.json +++ b/package.json @@ -29,12 +29,16 @@ "prettier:text": "prettier 'README.md' 'docs/*.md' 'docs/**/*.md' --no-config --tab-width 4 --print-width 120 --write --prose-wrap always", "start": "node ./bin/iotagent-json", "test": "nyc --reporter=text mocha --recursive 'test/**/*.js' --reporter spec --timeout 5000 --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:coverage": "nyc --reporter=lcov mocha -- --recursive 'test/**/*.js' --reporter spec --exit", "test:coveralls": "npm run test:coverage && cat ./coverage/lcov.info | coveralls && rm -rf ./coverage", "test:watch": "npm run test -- -w ./lib", "watch": "watch 'npm test && npm run lint' ./lib ./test" }, "devDependencies": { + "async-mqtt": "~2.6.3", + "chai": "~4.3.10", + "chai-match-pattern": "~1.3.0", "coveralls": "~3.1.0", "eslint": "~7.5.0", "eslint-config-tamia": "~7.2.5", diff --git a/test/functional/README.md b/test/functional/README.md new file mode 100644 index 000000000..a4a7cdbff --- /dev/null +++ b/test/functional/README.md @@ -0,0 +1,18 @@ +## Functional test suite + +This directory contains the functional test suite for the project. This relies on the IoT Agent Node Lib Functional test +suite. For further information, visit the +[documentation](https://github.com/telefonicaid/iotagent-node-lib/tree/master/test/functional). + +The `functional-tests-runner.js` script is used to run the functional test suite. It is similar to the one used in the +IoT Agent Node Lib, but it has been adapted to the particularities of the IoT Agent JSON. This script imports both the +IoT Agent Node Lib `testCases.js` and the local `testCases.js` file. The latter contains the test cases that are +specific to the IoT Agent JSON, while the former contains the test cases that are common to all IoT Agents. + +If you plan to include a tests for an specific feature of the IoT Agent JSON, please, consider added it into the IoTA +Node Lib `testCases.js` file and use the skip feature to avoid running it for other agents that do not support it. You +can check the documentation of the IoT Agent Node Lib Functional test suite linked previously for further information. + +Additionally, the `functional-tests.js` file is a simple example of how to implement code bases tests using the IoT +Agent Node Lib Functional test suite utilities. (This test is coded implemented and suites more complex cases than the +ones contained in the `testCases.js` file). diff --git a/test/functional/config-test.js b/test/functional/config-test.js new file mode 100644 index 000000000..f312e3d3a --- /dev/null +++ b/test/functional/config-test.js @@ -0,0 +1,73 @@ +/* + * 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' + }, + types: {}, + service: 'howtoservice', + subservice: '/howto', + providerUrl: 'http://localhost:4041', + deviceRegistrationDuration: 'P1M', + defaultType: 'Thing', + defaultResource: '/iot/json' +}; + +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..2f5d3f5d1 --- /dev/null +++ b/test/functional/functional-tests-runner.js @@ -0,0 +1,130 @@ +/* + * 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 iotaJson = require('../../lib/iotagent-json'); +const config = require('./config-test.js'); +const nock = require('nock'); +const chai = require('chai'); +const expect = chai.expect; +const iotAgentLib = require('iotagent-node-lib'); +const async = require('async'); +const utils = require('../utils'); +const testUtils = require('../../node_modules/iotagent-node-lib/test/functional/testUtils.js'); +const request = utils.request; +const logger = require('logops'); +const chaiMatchPattern = require('chai-match-pattern'); +const e = require('express'); +chai.config.truncateThreshold = 0; + +const baseTestCases = require('../../node_modules/iotagent-node-lib/test/functional/testCases.js').testCases; +const jsonTestCases = 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); + +// Add specific test cases for IoTA JSON +testCases = testCases.concat(jsonTestCases); + +describe('FUNCTIONAL TESTS', function () { + beforeEach(function (done) { + // Check if the test case should be skipped + iotaJson.start(config, function (error) { + done(error); + }); + }); + + afterEach(function (done) { + async.series([iotAgentLib.clearAll, iotaJson.stop], done); + }); + + testCases.forEach((testCase) => { + describe(testCase.describeName, function () { + beforeEach(function (done) { + if (testCase.skip && testUtils.checkSkip(testCase.skip, 'json')) { + this.skip(); + } + if (testCase.loglevel) { + logger.setLevel(testCase.loglevel); + } + request(testCase.provision, function (error, response, body) { + let err = null; + if (response.statusCode !== 201) { + err = new Error('Error creating the device group'); + } + done(err); + }); + }); + + afterEach(function () { + logger.setLevel('FATAL'); + nock.cleanAll(); + }); + + testCase.should.forEach((should) => { + it(should.shouldName, async function () { + if (testCase.skip && testUtils.checkSkip(testCase.skip, 'json')) { + this.skip(); + } + + this.retries(2); // pass the maximum no of retries + 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); + } + + await testUtils.testCase( + should.measure, + should.expectation, + testCase.provision, + env, + config, + should.type ? should.type : 'single', + should.transport ? should.transport : 'HTTP', + 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..531b20d7c --- /dev/null +++ b/test/functional/functional-tests.js @@ -0,0 +1,237 @@ +/* + * 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 iotaJson = require('../..'); +const config = require('./config-test.js'); +const nock = require('nock'); +const chai = require('chai'); +const expect = chai.expect; +const iotAgentLib = require('iotagent-node-lib'); +const async = require('async'); +const utils = require('../utils'); +const testUtils = require('../../node_modules/iotagent-node-lib/test/functional/testUtils.js'); +const request = utils.request; +const logger = require('logops'); +var chaiMatchPattern = require('chai-match-pattern'); + +chai.use(chaiMatchPattern); +var _ = chaiMatchPattern.getLodashModule(); +let contextBrokerMock; + +const env = { + service: 'smartgondor', + servicePath: '/gardens' +}; + +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'; + +describe('FUNCTIONAL TESTS', function () { + beforeEach(function (done) { + iotaJson.start(config, function (error) { + done(error); + }); + }); + + afterEach(function (done) { + async.series([iotAgentLib.clearAll, iotaJson.stop], done); + }); + + 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: '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' + } + }; + + const measure = { + url: 'http://localhost:' + config.http.port + '/iot/json', + method: 'POST', + qs: { + i: 'MQTT_2', + k: '123456' + }, + json: { + s: false, + t: 10 + } + }; + + const expectation = { + id: 'TheLightType2:MQTT_2', + type: 'TheLightType2', + temperature: { + value: 10, + type: 'Number' + }, + status: { + value: false, + type: 'Boolean' + } + }; + + beforeEach(function (done) { + request(provision, function (error, response, body) { + let err = null; + if (response.statusCode !== 201) { + err = new Error('Error creating the device group'); + } + done(err); + }); + }); + + afterEach(function () { + nock.cleanAll(); + }); + + it('should send its value to the Context Broker', async function () { + await testUtils.testCase(measure, expectation, provision, env, config, 'single', 'HTTP'); + }); + }); + + 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: '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', + entity_name: 'TheLightType2:MQTT_3', + entity_type: 'TheLightType2' + } + ], + static_attributes: [] + } + ] + }, + headers: { + 'fiware-service': 'smartgondor', + 'fiware-servicepath': '/gardens' + } + }; + + const measure = { + url: 'http://localhost:' + config.http.port + '/iot/json', + method: 'POST', + qs: { + i: 'MQTT_2', + k: '123456' + }, + json: { + s: false, + t: 10 + } + }; + + const expectation = { + 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' + }; + + beforeEach(function (done) { + request(provision, function (error, response, body) { + let err = null; + if (response.statusCode !== 201) { + err = new Error('Error creating the device group'); + } + done(err); + }); + }); + + afterEach(function () { + nock.cleanAll(); + }); + + it('should send its value to the Context Broker', async function () { + // logger.setLevel('DEBUG'); + await testUtils.testCase(measure, expectation, provision, env, config, 'multientity', 'HTTP'); + }); + }); +}); diff --git a/test/functional/testCases.js b/test/functional/testCases.js new file mode 100644 index 000000000..e5612fe68 --- /dev/null +++ b/test/functional/testCases.js @@ -0,0 +1,41 @@ +/* + * 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 = require('./config-test.js'); + +const globalEnv = { + service: 'smartgondor', + servicePath: '/gardens', + apikey: '123456', + entity_type: 'TestType', + entity_name: 'TestType:TestDevice', + deviceId: 'TestDevice' +}; + +const testCases = []; + +exports.testCases = testCases;