diff --git a/CHANGES_NEXT_RELEASE b/CHANGES_NEXT_RELEASE index f2430584..833d1cfd 100644 --- a/CHANGES_NEXT_RELEASE +++ b/CHANGES_NEXT_RELEASE @@ -5,8 +5,13 @@ - Upgrade dev dependency istanbul from ~0.1.34 to ~0.4.5 - Upgrade dev dependency mocha from 2.4.5 to to 5.2.0 - Remove: old unused development dependencies - * chai - * sinon - * sinon-chai * grunt and grunt related module * closure-linter-wrapper +- Change on the PERSEO_ORION_URL env var behaviour. Now it represents Context Broker base URL instead of the + updateContext endpoint +- Add: new ngsijs ~1.2.0 dependency +- Add: new rewire ~4.0.1 dev dependency +- Add: NGSIv2 support in both notification reception and CB update action +- Upgrade dev dependency chai from ~1.8.0 to ~4.1.2 +- Upgrade dev dependency sinon from ~1.7.3 to ~6.1.0 +- Upgrade dev dependency sinon-chai from 2.4.0 to ~3.2.0 \ No newline at end of file diff --git a/bin/perseo b/bin/perseo index 2f63492d..9148cd8a 100755 --- a/bin/perseo +++ b/bin/perseo @@ -29,7 +29,8 @@ var app = require('../lib/perseo'), logger = require('logops'), constants = require('../lib/constants'), config = require('../config'), - context = {op: 'perseo', comp: constants.COMPONENT_NAME}; + context = {op: 'perseo', comp: constants.COMPONENT_NAME}, + URL = require('url').URL; logger.format = logger.formatters.pipe; @@ -105,7 +106,7 @@ function loadConfiguration() { config.nextCore.noticesURL = process.env.PERSEO_NEXT_URL + '/perseo-core/events'; } if (process.env.PERSEO_ORION_URL) { - config.orion.URL = process.env.PERSEO_ORION_URL; + config.orion.URL = new URL(process.env.PERSEO_ORION_URL); } if (process.env.PERSEO_LOG_LEVEL) { config.logLevel = process.env.PERSEO_LOG_LEVEL; diff --git a/documentation/configuration.md b/documentation/configuration.md index c68207a0..61b597d1 100644 --- a/documentation/configuration.md +++ b/documentation/configuration.md @@ -60,7 +60,7 @@ In order to have perseo running, there are several basic pieces of information t * `config.smpp.password`: Password for the user of the SMPP server * `config.smpp.from`: Number from SMS are sending by SMPP server * `config.smpp.enabled`: SMPP is default method for SMS instead of use SMS gateway. -* `config.orion.URL`: URL for updating contexts at Orion (Context Broker). +* `config.orion.URL`: Context Broker base URL, e.g. https://orion.example.com:1026 * `config.mongo.URL`: URL for connecting mongoDB. * `config.executionsTTL`: Time-To-Live for documents of action executions (seconds). * `config.checkDB.delay`: Number of milliseconds to check DB connection (see [database aspects](admin.md#database-aspects) documentation for mode detail). diff --git a/documentation/plain_rules.md b/documentation/plain_rules.md index 64bfa820..5f36e5fe 100644 --- a/documentation/plain_rules.md +++ b/documentation/plain_rules.md @@ -157,7 +157,8 @@ The `parameters` map includes the following fields: * id: optional, the id of the entity which attribute is to be updated (by default the id of the entity that triggers the rule is used, i.e. `${id}`) * type: optional, the type of the entity which attribute is to be updated (by default the type of the entity that triggers the rule is usedi.e. `${type}`) -* isPattern: optional, `false` by default +* version: optional, The NGSI version for the update action. Set this attribute to `2` or `"2"` if you want to use NGSv2 format. `1` by default +* isPattern: optional, `false` by default. (Only for NGSIv1. If `version` is set to 2, this attribute will be ignored) * attributes: *mandatory*, array of target attributes to update. Each element of the array must contain the fields * **name**: *mandatory*, attribute name to set * **value**: *mandatory*, attribute value to set @@ -165,7 +166,7 @@ The `parameters` map includes the following fields: * actionType: optional, type of CB action: APPEND or UPDATE. By default is APPEND. * trust: optional, trust token for getting an access token from Auth Server which can be used to get to a Context Broker behind a PEP. - +NGSIv1 example: ```json "action":{ "type":"update", @@ -190,6 +191,197 @@ First time an update action using trust token is triggered, Perseo interacts wit It could happen (in theory) that a just got auth token also produce a 401 Not authorized, however this would be an abnormal situation: Perseo logs the problem with the update but doesn't try to get a new one from Keystone. Next time Perseo triggers the action, the process may repeat, i.e. first update attemp fails with 401, Perseo requests a fresh auth token to Keystone, the second update attemp fails with 401, Perseo logs the problem and doesn't retry again. +NGSIv2 example: +```json +"action":{ + "type":"update", + "parameters":{ + "id":"${id}_mirror", + "version": 2, + "attributes": [ + { + "name":"abnormal", + "type":"Number", + "value": 7 + } + ] + } + } +``` + +**Note:** NGSIv2 update actions ignore the trust token for now. + +When using NGSIv2 in the update actions, the value field perform [string substitution](#string-substitution-syntax). If `value` is a String, Perseo will parse the value taking into account the `type` field, this only applies to *`Number`*, *`Boolean`* and *`None`* types. + +**Data Types for NGSIv2:** + +With `Number` type attributes, Perseo can be able to manage a int/float number or a String to parse in value field. +- Number from variable: +```json +{ + "name":"numberFromValue", + "type": "Number", + "value": "${NumberValue}" +} +``` +If `NumberValue` value is for example `32.12`, this attribute will take `32.12` as value. + +- Literal Number: +```json +{ + "name":"numberLiteral", + "type": "Number", + "value": 12 +} +``` +This attribute will take `12` as value. + +- Number as String from variable: +```json +{ + "name":"numberFromStringValue", + "type": "Number", + "value": "${NumberValueAsString}" +} +``` +If `NumberValueAsString` value is for example `"4.67"`, this attribute will take `4.67` as value. + +- Number as String: +```json +{ + "name":"numberStringLiteral", + "type": "Number", + "value": "67.8" +} +``` +This attribute will take `67.8` as value. + + + +With `Text` type attributes, Perseo will put the value field parsed as string. + +- Text as variable: +```json +{ + "name":"textFromValue", + "type": "Text", + "value": "${varValue}" +} +``` +If `varValue` value is for example `"Good morning"`, this attribute will take `"Good morning"` as value. + +If `varValue` value is for example `1234`, this attribute will take `"1234"` as value. + +- Literal Text: +```json +{ + "name":"textLiteral", + "type": "Text", + "value": "Hello world" +} +``` +This attribute will take `"Hello world"` as value. + +- Literal Number: +```json +{ + "name":"textNumberLiteral", + "type": "Text", + "value": 67.8 +} +``` +This attribute will take `"67.8"` as value. + +- Literal Boolean: +```json +{ + "name":"textBoolLiteral", + "type": "Text", + "value": true +} +``` +This attribute will take `"true"` as value. + +With `DateTime` type attributes, Perseo will try to parse the value to DateTime format. + +Date as String: +```json +{ + "name":"dateString", + "type": "DateTime", + "value": "2018-12-05T11:31:39.00Z" +} +``` +This attribute will take `"2018-12-05T11:31:39.000Z"` as value. + +Date as Number in milliseconds: +```json +{ + "name":"dateString", + "type": "DateTime", + "value": 1548843229832 +} +``` +This attribute will take `"2019-01-30T10:13:49.832Z"` as value. + +Date from variable. +```json +{ + "name":"dateString", + "type": "DateTime", + "value": "${dateVar}" +} +``` +If `dateVar` value is for example `1548843229832` (as Number or String), this attribute will take `"2019-01-30T10:13:49.832Z"` as value. + +You can use the `__ts` field of a Perseo DateTime attribute to fill a DateTime attribute value without using any `cast()`. For example, if the var are defined as follow in the rule text, `ev.timestamp__ts? as dateVar`, `dateVar` will be a String with the Date in milliseconds, for example `"1548843060657"` and Perseo will parse this String with to a valid DateTime as `2019-01-30T10:11:00.657Z`. + +With `None` type attributes, Perseo will set the value to `null` in all cases. + +None Attribute: +```json +{ + "name":"nullAttribute", + "type": "None", + "value": "It does not matter what you put here" +} +``` +This attribute will take `null` as value. + +```json +{ + "name":"nullAttribute2", + "type": "None", + "value": null +} +``` +This attribute will take `null` as value. + + +**Complete example using NGSv2 update action in a rule:** + +```json +{ + "name":"blood_rule_update", + "text":"select *,\"blood_rule_update\" as ruleName, *, ev.BloodPressure? as Pressure from pattern [every ev=iotEvent(BloodPressure? > 1.5 and type=\"BloodMeter\")]", + "action":{ + "type":"update", + "parameters":{ + "id":"${id}_example", + "version": 2, + "attributes": [ + { + "name":"pressure", + "type":"Number", + "value": "${Pressure}" + } + ] + } + } +} +``` + +Note that using NGSIv2 the BloodPressure attribute is a Number and therefore it is not necessary to use `cast()`. ### HTTP request action Makes an HTTP request to an URL specified in `url` inside `parameters`, sending a body built from `template`. diff --git a/lib/models/notices.js b/lib/models/notices.js index f0ff8459..2e3f7c7f 100644 --- a/lib/models/notices.js +++ b/lib/models/notices.js @@ -19,6 +19,8 @@ * * For those usages not covered by the GNU Affero General Public License * please contact with::[contacto@tid.es] + * + * Modified by: Carlos Blanco - Future Internet Consulting and Development Solutions (FICODES) */ 'use strict'; @@ -33,15 +35,24 @@ var util = require('util'), converter = new UtmConverter(), errors = {}; +/** + * Parse Location attribute + * + * @param locStr The location string e.g: "40.418889, -3.691944" + * + * @return The parsed Location (object with 'lat', 'lon', 'x', and 'y' attributes) if location was parsed successfully; + * InvalidLocation, InvalidLatitude, or InvalidLongitude error otherwise + */ function parseLocation(locStr) { var position, lat, - lon, - utmResult; - - position = locStr.split(',', 3); + lon; + if (typeof locStr !== 'string') { + return new errors.InvalidLocation(locStr); + } + position = locStr.split(','); if (position.length !== 2) { - return new errors.InvalidLocation(position); + return new errors.InvalidLocation(locStr); } lat = parseFloat(position[0]); if (isNaN(lat) || !isFinite(lat)) { @@ -51,7 +62,12 @@ function parseLocation(locStr) { if (isNaN(lon) || !isFinite(lon)) { return new errors.InvalidLongitude(lon); } - utmResult = converter.toUtm({coord: [lon, lat]}); // CAUTION: Longitude first element + var utmResult; + try { + utmResult = converter.toUtm({coord: [lon, lat]}); // CAUTION: Longitude first element + } catch (e) { + return new errors.InvalidLocation(e); + } return { lat: lat, @@ -202,6 +218,110 @@ function processCBNotice(service, subservice, ncr, ix) { return n; } +/** + * Process NGSIv2 Context Broker Notice + * + * @param service The Fiware-Service + * @param subservice The Fiware-ServicePath + * @param ncr The notice data object + * @param ix The data index + * + * @return The processed notice if notice was correct; InvalidV2Notice error otherwise + */ +function processCBv2Notice(service, subservice, ncr, ix) { + + var n = {}, + pp, + temp, + location, + metalocation, + date, + metaDate; + + n.noticeId = uuid.v1(); + n.noticeTS = Date.now(); + + try { + n.id = ncr.data[ix].id; + n.type = ncr.data[ix].type; + n.isPattern = false; + n.subservice = subservice; + n.service = service; + + //Transform name-value-type + var attrList = ncr.data[ix]; + for (var attr in attrList) { + // Exclude id and type. NGSIv2 + if (attr === 'id' || attr === 'type') { + continue; + } + // each atttribute + var attrInfo = attrList[attr]; + n[attr + '__type'] = attrInfo.type; + + // NGSIv1 location attribute (only one should be present) + // see links in issues/198 + if (attrInfo.type === 'geo:point') { + location = parseLocation(attrInfo.value); + n[attr] = location; + } else if (attrInfo.type === 'DateTime') { + date = parseDate(attrInfo.value); + n[attr] = date; + } else { + n[attr] = attrInfo.value; + } + + for (var metaKey in attrInfo.metadata) { + + n[attr + '__metadata__' + metaKey + '__type'] = attrInfo.metadata[metaKey].type; + if (attrInfo.metadata[metaKey].type === 'DateTime') { + metaDate = parseDate(attrInfo.metadata[metaKey].value); + n[attr + '__metadata__' + metaKey] = metaDate; + } else if (attrInfo.metadata[metaKey].type === 'geo:point') { + metalocation = parseLocation(attrInfo.metadata[metaKey].value); + n[attr + '__metadata__' + metaKey] = metalocation; + } else { + n[attr + '__metadata__' + metaKey] = attrInfo.metadata[metaKey].value; + } + + } + } + // Add descriptive information in errors + if (date instanceof Error) { + date.message = 'Invalid DateTime attribute: ' + date.message; + return date; + } + if (location instanceof Error) { + location.message = 'Invalid geo:point attribute: ' + location.message; + return location; + } + if (metaDate instanceof Error) { + metaDate.message = 'Invalid DateTime attribute metadata: ' + metaDate.message; + return metaDate; + } + if (metalocation instanceof Error) { + metalocation.message = 'Invalid geo:point attribute metadata: ' + metalocation.message; + return metalocation; + } + Object.keys(n).forEach(function(p) { + //Change dots in key to double-underscore as in rules to avoid confusing EPL engine + pp = p.replace(/\./g, '__'); + temp = n[p]; + delete n[p]; + n[pp] = temp; + }); + + n = myutils.flattenMap('', n); + + } catch (ex) { + // Should never reach this catch + var localError = new errors.InvalidV2Notice(ex + ' (' + JSON.stringify(ncr) + ')'); + myutils.logErrorIf(localError); + return localError; + } + return n; +} + function DoNotice(orionN, callback) { var notices = [], noticesErr = [], @@ -209,23 +329,59 @@ function DoNotice(orionN, callback) { notice, sps; + if (Object.keys(orionN).length === 0 && orionN.constructor === Object) { + return callback(new errors.EmptyNotice('')); + } // ServicePath may be a comma-separated list of servicePath // when an initial notification for a just created subscription // is created + if (typeof orionN.subservice !== 'string') { + return callback(new errors.InvalidNotice('Subservice must be a comma-separated list of servicePath')); + } sps = orionN.subservice.split(','); - if (!util.isArray(orionN.contextResponses)) { + + var orionResponse = null; + + // Set NGSI 'version' in the notification object using an internal attribute. + // NGSIv2 notification body, contains the notification data array inside 'data' attribute + // see https://jsapi.apiary.io/previews/null/introduction/specification/notification-messages + // and https://fiware-orion.readthedocs.io/en/master/user/initial_notification/index.html#introduction + // NGSIv1 notification body, contains the notification data array inside 'contextResponses' attribute + // see http://telefonicaid.github.io/fiware-orion/api/v1 + // and https://fiware-orion.readthedocs.io/en/1.4.0/user/walkthrough_apiv1/#context-management-using-ngsi10 + if (orionN.data) { + orionN.version = 2; + orionResponse = orionN.data; + } else { + orionN.version = 1; + orionResponse = orionN.contextResponses; + } + + if (!util.isArray(orionResponse)) { + if (orionN.version === 2) { + return callback( + new errors.InvalidV2Notice('data must be an array, not a ' + typeof(orionResponse)) + ); + } return callback( - new errors.ContextResponsesNotArray('(' + typeof(orionN.contextResponses) + ')') + new errors.ContextResponsesNotArray('(' + typeof(orionResponse) + ')') ); } - if (orionN.contextResponses.length !== sps.length) { - return callback( - new errors.ServipathCountMismatch('(' + sps.length + ',' + orionN.contextResponses.length + ')') - ); + if (orionResponse.length !== sps.length) { + return callback( + new errors.ServipathCountMismatch('(' + sps.length + ',' + orionResponse.length + ')') + ); } + + // Iterates the collection. process 1 notice for each entity for (var j = 0; j < sps.length; j++) { - notice = processCBNotice(orionN.service, sps[j].trim(), orionN, j); + + if (orionN.version === 2) { + notice = processCBv2Notice(orionN.service, sps[j].trim(), orionN, j); + } else { + notice = processCBNotice(orionN.service, sps[j].trim(), orionN, j); + } if (notice instanceof Error) { myutils.logErrorIf(notice); noticesErr.push(notice); @@ -235,31 +391,31 @@ function DoNotice(orionN, callback) { } } async.each(notices, function(notice, cbEach) { - var h = {}; - h[constants.SUBSERVICE_HEADER] = notice.subservice; - myutils.requestHelperWOMetrics('post', { - url: config.perseoCore.noticesURL, + var h = {}; + h[constants.SUBSERVICE_HEADER] = notice.subservice; + myutils.requestHelperWOMetrics('post', { + url: config.perseoCore.noticesURL, + json: notice, + headers: h + }, + function(err, data) { + if (err) { + alarm.raise(alarm.POST_EVENT); + noticesErr.push(err); + } else { + alarm.release(alarm.POST_EVENT); + dataArr.push(data); + } + // Don't wait propagation to next core to finish, asynchronously ... + if (config.nextCore && config.nextCore.noticesURL) { + myutils.requestHelperWOMetrics('post', { + url: config.nextCore.noticesURL, json: notice, headers: h - }, - function(err, data) { - if (err) { - alarm.raise(alarm.POST_EVENT); - noticesErr.push(err); - } else { - alarm.release(alarm.POST_EVENT); - dataArr.push(data); - } - // Don't wait propagation to next core to finish, asynchronously ... - if (config.nextCore && config.nextCore.noticesURL) { - myutils.requestHelperWOMetrics('post', { - url: config.nextCore.noticesURL, - json: notice, - headers: h - }, myutils.logErrorIf); - } - cbEach(); - }); + }, myutils.logErrorIf); + } + cbEach(); + }); }, function endEach() { var msgArr = [], statusCode = 400; @@ -280,6 +436,8 @@ function DoNotice(orionN, callback) { module.exports.Do = DoNotice; module.exports.ParseLocation = parseLocation; module.exports.ProcessCBNotice = processCBNotice; + + /** * Constructors for possible errors from this module * @@ -293,6 +451,16 @@ module.exports.errors = errors; this.message = 'invalid notice format ' + msg; this.httpCode = 400; }; + errors.InvalidV2Notice = function InvalidV2Notice(msg) { + this.name = 'INVALID_NGSIV2_NOTICE'; + this.message = 'invalid NGSIv2 notice format ' + msg; + this.httpCode = 400; + }; + errors.EmptyNotice = function EmptyNotice(msg) { + this.name = 'EMPTY_NOTICE'; + this.message = 'Empty notice is not valid ' + msg; + this.httpCode = 400; + }; errors.IdAsAttribute = function IdAsAttribute(msg) { this.name = 'ID_ATTRIBUTE'; this.message = 'id as attribute ' + msg; diff --git a/lib/models/updateAction.js b/lib/models/updateAction.js index a5f68152..aa99b644 100644 --- a/lib/models/updateAction.js +++ b/lib/models/updateAction.js @@ -19,6 +19,8 @@ * * For those usages not covered by the GNU Affero General Public License * please contact with::[contacto@tid.es] + * + * Modified by: Carlos Blanco - Future Internet Consulting and Development Solutions (FICODES) */ 'use strict'; @@ -34,7 +36,9 @@ var util = require('util'), tokens = {}, newTokenEventName = 'new_token', MAX_LISTENERS = 10e3, - metrics = require('./metrics'); + metrics = require('./metrics'), + URL = require('url').URL, + NGSI = require('ngsijs'); function getCachedToken(service, subservice, name) { var emitter; @@ -159,7 +163,7 @@ function doRequest(action, event, token, callback) { logger.debug('body to post %j ', updateOrion); myutils.requestHelper('post', { - url: config.orion.URL, + url: new URL('v1/updateContext', config.orion.URL), body: updateOrion, json: true, headers: headers @@ -233,9 +237,113 @@ function doItWithToken(action, event, callback) { } } +/** + * Process NGSIv2 action do request option Params + * + * @param action The request update action information + * @param event The doIt event information + * + * @return changes object + */ +function processOptionParams(action, event) { + + // Default id -> last event ID + action.parameters.id = action.parameters.id || '${id}'; + // Default type -> last event Type + action.parameters.type = action.parameters.type || '${type}'; + + action.parameters.attributes = action.parameters.attributes || []; + + var changes = {}; + action.parameters.attributes.forEach(function(attr) { + + // Direct value for Text, DateTime, and others. Apply expandVar for strings + let theValue = myutils.expandVar(attr.value, event); + let theType = myutils.expandVar(attr.type, event); + // Metadata should be null or object + let theMeta = attr.metadata; + var date; + + switch(theType) { + case 'Text': + theValue = theValue.toString(); + break; + case 'Number': + theValue = parseFloat(theValue); + break; + case 'Boolean': + if (typeof theValue === 'string') { + theValue = theValue.toLowerCase().trim() === 'true'; + } + break; + case 'DateTime': + if (parseInt(theValue).toString() === theValue) { + // Parse String with number (timestamp__ts in Perseo events) + theValue = parseInt(theValue); + } + date = new Date(theValue); + theValue = isNaN(date.getTime()) ? theValue : date.toISOString(); + break; + case 'None': + theValue = null; + break; + } + var key = myutils.expandVar(attr.name, event); + changes[key] = { + value: theValue, + type: theType + }; + if (attr.metadata !== undefined) { + changes[key].metadata = theMeta; + } + }); + changes.id = myutils.expandVar(action.parameters.id, event); + changes.type = myutils.expandVar(action.parameters.type, event); + + return changes; +} + +/** + * Manage update action request for NGSv2 request + * + * @param action The request update action information + * @param event The doIt event information + * @param callback The update action request Callback + * + * @return changes object + */ +function doRequestV2(action, event, callback) { + + var options = { + 'service': event.service, + 'servicepath': event.subservice, + }; + + // TODO NGSIv2 update action with trust token (similar to doItWithToken for NGSIv1) + + var connection = new NGSI.Connection(config.orion.URL, options); + + var changes = processOptionParams(action, event); + metrics.IncMetrics(event.service, event.subservice, metrics.actionEntityUpdate); + + connection.v2.createEntity(changes, {upsert: true}).then( + (response) => { + metrics.IncMetrics(event.service, event.subservice, metrics.okActionEntityUpdate); + alarm.release(alarm.ORION); + callback(null, response); + }, (error) => { + metrics.IncMetrics(event.service, event.subservice, metrics.failedActionEntityUpdate); + alarm.raise(alarm.ORION, null, error); + callback(error, null); + } + ); +} + function doIt(action, event, callback) { try { - if (action.parameters.trust) { + if (action.parameters.version === '2' || action.parameters.version === 2) { + return doRequestV2(action, event, callback); + } else if (action.parameters.trust) { return doItWithToken(action, event, callback); } else { return doRequest(action, event, null, callback); diff --git a/package.json b/package.json index e7558f96..ca733390 100644 --- a/package.json +++ b/package.json @@ -32,9 +32,13 @@ "istanbul": "~0.4.5", "jshint": "~2.9.6", "mocha": "5.2.0", + "chai": "~4.1.2", "proxyquire": "0.5.1", + "rewire": "~4.0.1", "should": "8.2.2", - "watch": "~1.0.2" + "watch": "~1.0.2", + "sinon": "~6.1.0", + "sinon-chai": "~3.2.0" }, "keywords": [], "dependencies": { @@ -43,6 +47,7 @@ "express": "~4.16.1", "logops": "2.1.0", "mongodb": "~2.2.31", + "ngsijs": "~1.2.0", "nodemailer": "~1.11.0", "nodemailer-smtp-transport": "~0.1.13", "request": "~2.83.0", diff --git a/test/.jshintrc b/test/.jshintrc index dc305b63..f06dd11e 100644 --- a/test/.jshintrc +++ b/test/.jshintrc @@ -26,6 +26,7 @@ "after": true, "beforeEach": true, "afterEach": true, + "sinon": true, "mock": true }, "predef": diff --git a/test/component/actions_test.js b/test/component/actions_test.js index 389ce6e6..4fee69bf 100644 --- a/test/component/actions_test.js +++ b/test/component/actions_test.js @@ -35,7 +35,8 @@ var constants = require('../../lib/constants'), request = require('request'), config = require('../../config'), - EXEC_GRACE_PERIOD = 500; + EXEC_GRACE_PERIOD = 500, + URL = require('url').URL; describe('Actions', function() { beforeEach(testEnv.commonBeforeEach); @@ -81,7 +82,7 @@ describe('Actions', function() { it('should return ok with a valid action with a rule for update', function(done) { var rule = utilsT.loadExample('./test/data/good_rules/blood_rule_update.json'), action = utilsT.loadExample('./test/data/good_actions/action_update.json'); - utilsT.getConfig().orion.URL = 'http://thisshouldbenothingnotaCB'; + utilsT.getConfig().orion.URL = new URL('http://thisshouldbenothingnotaCB'); async.series([ function(callback) { clients.PostRule(rule, function(error, data) { diff --git a/test/component/auth_test.js b/test/component/auth_test.js index 7f2ffc7f..e452cc08 100644 --- a/test/component/auth_test.js +++ b/test/component/auth_test.js @@ -31,7 +31,8 @@ var utilsT = require('../utils/utilsT'), testEnv = require('../utils/testEnvironment'), EventEmitter = require('events').EventEmitter, - updateDone = new EventEmitter(); + updateDone = new EventEmitter(), + URL = require('url').URL; describe('Auth', function() { beforeEach(testEnv.commonBeforeEach); @@ -45,7 +46,7 @@ describe('Auth', function() { action.ev.id += date.getTime(); utilsT.getConfig().authentication.host = 'localhost'; utilsT.getConfig().authentication.port = utilsT.fakeHttpServerPort; - utilsT.getConfig().orion.URL = util.format('http://localhost:%s', utilsT.fakeHttpServerPort); + utilsT.getConfig().orion.URL = new URL(util.format('http://localhost:%s', utilsT.fakeHttpServerPort)); updateDone.once('updated_renew', done); updateDone.once('updated_first', function(error) { if (error) { diff --git a/test/component/loglevel_test.js b/test/component/loglevel_test.js index 6e05c183..428ef689 100644 --- a/test/component/loglevel_test.js +++ b/test/component/loglevel_test.js @@ -72,7 +72,7 @@ describe('LogLevel', function() { clients.GetLogLevel(function(error, response) { should.not.exist(error); response.should.have.property('statusCode', 200); - response.should.have.body; + response.should.have.property('body'); response.body.should.have.property('level', level); callback(null); }); diff --git a/test/component/metrics/metrics_actions_test.js b/test/component/metrics/metrics_actions_test.js index 71889221..ecd93601 100644 --- a/test/component/metrics/metrics_actions_test.js +++ b/test/component/metrics/metrics_actions_test.js @@ -30,7 +30,8 @@ var clients = require('../../utils/clients'), utilsT = require('../../utils/utilsT'), testEnv = require('../../utils/testEnvironment'), - metrics = require('../../../lib/models/metrics'); + metrics = require('../../../lib/models/metrics'), + URL = require('url').URL; describe('Metrics', function() { beforeEach(testEnv.commonBeforeEach); @@ -145,7 +146,7 @@ describe('Metrics', function() { it('should increment a successful action for update', function(done) { var rule = utilsT.loadExample('./test/data/good_rules/blood_rule_update.json'), action = utilsT.loadExample('./test/data/good_actions/action_update.json'); - utilsT.getConfig().orion.URL = util.format('http://localhost:%s', utilsT.fakeHttpServerPort); + utilsT.getConfig().orion.URL = new URL(util.format('http://localhost:%s', utilsT.fakeHttpServerPort)); metrics.GetDecorated(true); // reset metrics async.series([ function(callback) { @@ -182,7 +183,7 @@ describe('Metrics', function() { it('should increment a failed for update', function(done) { var rule = utilsT.loadExample('./test/data/good_rules/blood_rule_update.json'), action = utilsT.loadExample('./test/data/good_actions/action_update.json'); - utilsT.getConfig().orion.URL = ''; + utilsT.getConfig().orion.URL = new URL('http://inventedurl.notexists.com'); metrics.GetDecorated(true); // reset metrics async.series([ function(callback) { @@ -211,7 +212,7 @@ describe('Metrics', function() { should.equal(m.services.unknownt.sum.outgoingTransactions, 1); should.equal(m.services.unknownt.sum.outgoingTransactionsErrors, 1); return callback(); - }, 50); + }, 150); }); } ], done); diff --git a/test/component/myutils_test.js b/test/component/myutils_test.js index 44472eaa..ac4942e5 100644 --- a/test/component/myutils_test.js +++ b/test/component/myutils_test.js @@ -42,10 +42,12 @@ describe('Myutils', function() { describe('#RequestHelper()', function() { describe('When there is a network problem', function() { it('should return error', function(done) { - var url = 'http://incredibleifthishostexistsicantbelievemyeyes.io'; + var host = 'incredibleifthishostexistsicantbelievemyeyes.io'; + var url = 'http://' + host; myutils.requestHelper('get', {url: url}, function(error) { should.exist(error); - error.should.be.an.Error; + error.host.should.be.equal(host); + error.code.should.be.equal('ENOTFOUND'); }); done(); }); diff --git a/test/unit/notices_Do.js b/test/unit/notices_Do.js new file mode 100644 index 00000000..89884d04 --- /dev/null +++ b/test/unit/notices_Do.js @@ -0,0 +1,336 @@ +/* + * Copyright 2015 Telefonica Investigación y Desarrollo, S.A.U + * + * This file is part of perseo-fe + * + * perseo-fe 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. + * + * perseo-fe 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 perseo-fe. + * If not, see http://www.gnu.org/licenses/. + * + * For those usages not covered by the GNU Affero General Public License + * please contact with::[contacto@tid.es] + * + * Created by: Carlos Blanco - Future Internet Consulting and Development Solutions (FICODES) + */ +'use strict'; + +var should = require('should'); +var rewire = require('rewire'); +var notices = rewire('../../lib/models/notices'); +var chai = require('chai'); +var sinon = require('sinon'); +var sinonChai = require('sinon-chai'); +var expect = chai.expect; +chai.Should(); +chai.use(sinonChai); + +var noticeExampleV1 = JSON.stringify({ + 'subscriptionId': '5b34e37052a01bc4c7e67c34', + 'originator': 'localhost', + 'contextResponses': [ + { + 'contextElement': { + 'type': 'tipeExample1', + 'isPattern': 'false', + 'id': 'sensor-1', + 'attributes': [ + { + 'name': 'Attr1', + 'type': 'Number', + 'value': '123' + } + ] + } + } + ], + 'subservice': '/test/notices/unit', + 'service': 'utest' +}); + +var noticeExampleV2 = JSON.stringify({ + 'subscriptionId': '5b311ccb29adb333f843b5f3', + 'data': [ + { + 'id': 'sensorv2-1', + 'type': 'tipeExamplev21', + 'Attr1': { + 'type': 'Number', + 'value': 122, + 'metadata': {} + } + } + ], + 'subservice': '/test/notices/unitv2', + 'service': 'utestv2' +}); + +// Core mocks +var coreNotice1 = { + 'id': 'ent1', + 'type': 'Room', + 'service': 'utest', + 'subservice': '/test/notices/unit' +}; + +describe('Notices Do', function() { + + describe('#DoNotice', function() { + var v1notice, v2notice; + + beforeEach(function () { + v1notice = JSON.parse(noticeExampleV1); + v2notice = JSON.parse(noticeExampleV2); + }); + + it('should accept NGSIv1 entities', function (done) { + + var postEvent = 'POST_EVENT'; + var alarmReleaseMock = sinon.spy(function () {}); + var processCBNoticeMock = sinon.spy(function () { + return coreNotice1; + }); + var requestWOMetricsMock = sinon.spy( + function (method, option, callback) { + callback(null, {'httpCode': '200', 'message': 'all right'}); + } + ); + notices.__with__({ + 'processCBNotice': processCBNoticeMock, + 'myutils.requestHelperWOMetrics': requestWOMetricsMock, + 'config.perseoCore.noticesURL': 'http://mokedurl.org', + 'alarm.release': alarmReleaseMock, + 'alarm.POST_EVENT': postEvent + })(function () { + var callback = function (e, request) { + should.exist(request); + request.should.not.be.instanceof(Error); + should.equal(request.length, 1); + should.equal(request[0].httpCode, 200); + // Checking call to processCBNotice + processCBNoticeMock.should.have.been.calledWith('utest', '/test/notices/unit', v1notice, 0); + processCBNoticeMock.should.be.calledOnce; + // Checking call to requestWOMetrics + var h = {'fiware-servicepath': '/test/notices/unit'}; + requestWOMetricsMock.should.have.been.calledWith('post', { + url: 'http://mokedurl.org', + json: coreNotice1, + headers: h + }); + requestWOMetricsMock.should.be.calledOnce; + alarmReleaseMock.should.have.been.calledWith(postEvent); + alarmReleaseMock.should.be.calledOnce; + done(); + }; + notices.Do(v1notice, callback); + }); + }); + + it('should fail whith empty notice', function(done) { + + var callback = function (e, request) { + should.exist(e); + should.not.exist(request); + should.equal(e.httpCode, 400); + should.equal(e.message, 'Empty notice is not valid '); + should.equal(e.name, 'EMPTY_NOTICE'); + done(); + }; + notices.Do({}, callback); + }); + + it('should fail whith invalid subservice', function(done) { + + var callback = function (e, request) { + should.exist(e); + should.not.exist(request); + should.equal(e.httpCode, 400); + should.equal(e.message, 'invalid notice format Subservice must be' + + ' a comma-separated list of servicePath'); + done(); + }; + notices.Do({data:[], subservice:123}, callback); + }); + + it('should accept NGSIv2 entities', function(done) { + + var postEvent = 'POST_EVENT'; + var alarmReleaseMock = sinon.spy(function () {}); + var processCBv2NoticeMock = sinon.spy(function () { + return coreNotice1; + }); + var requestWOMetricsMock = sinon.spy( + function (method, option, callback) { + callback(null, {'httpCode': '200', 'message': 'all right'}); + } + ); + notices.__with__({ + 'myutils.requestHelperWOMetrics': requestWOMetricsMock, + 'config.perseoCore.noticesURL': 'http://mokedurl.org', + 'alarm.release': alarmReleaseMock, + 'alarm.POST_EVENT': postEvent, + 'processCBv2Notice': processCBv2NoticeMock + })(function () { + var callback = function (e, request) { + should.exist(request); + request.should.not.be.instanceof(Error); + should.equal(request.length, 1); + should.equal(request[0].httpCode, 200); + // Checking call to processCBv2NoticeMock + processCBv2NoticeMock.should.have.been.calledWith('utestv2', '/test/notices/unitv2', v2notice, 0); + processCBv2NoticeMock.should.be.calledOnce; + // Checking call to requestWOMetrics + var h = {'fiware-servicepath': '/test/notices/unit'}; + requestWOMetricsMock.should.have.been.calledWith('post', { + url: 'http://mokedurl.org', + json: coreNotice1, + headers: h + }); + requestWOMetricsMock.should.be.calledOnce; + alarmReleaseMock.should.have.been.calledWith(postEvent); + alarmReleaseMock.should.be.calledOnce; + done(); + }; + notices.Do(v2notice, callback); + }); + }); + + it('should process Errors correctly', function(done) { + + var postEvent = 'POST_EVENT'; + var alarmReleaseMock = sinon.spy(function () {}); + var errorLocNotice = new notices.errors.InvalidLocation('Location_Mock'); + errorLocNotice.httpCode = 500; + var processCBv2NoticeMock = sinon.spy(function () { + return errorLocNotice; + }); + var logErrorMock = sinon.spy(); + notices.__with__({ + 'processCBv2Notice': processCBv2NoticeMock, + 'myutils.logErrorIf': logErrorMock, + 'alarm.POST_EVENT': postEvent, + 'config.perseoCore.noticesURL': 'http://mokedurl.org', + 'alarm.release': alarmReleaseMock, + })(function () { + var callback = function (e, request) { + should.not.exists(request); + should.exist(e); + // Check invalid Location error + should.equal(e.httpCode, 500); + should.equal(e.message, 'invalid location Location_Mock'); + // Checking call to processCBv2Notice + processCBv2NoticeMock.should.have.been.calledWith('utestv2', '/test/notices/unitv2', v2notice, 0); + processCBv2NoticeMock.should.be.calledOnce; + // Checking logError + logErrorMock.should.have.been.calledWith(errorLocNotice); + logErrorMock.should.be.calledOnce; + done(); + }; + notices.Do(v2notice, callback); + }); + }); + + it('should process Error from core', function(done) { + + var postEvent = 'POST_EVENT'; + var alarmRaiseMock = sinon.spy(function () {}); + var processCBv2NoticeMock = sinon.spy(function () { + return coreNotice1; + }); + var requestWOMetricsMock = sinon.spy( + function (method, option, callback) { + callback('errorMock!'); + } + ); + var logErrorMock = sinon.spy( + function(notice) {} + ); + notices.__with__({ + 'myutils.requestHelperWOMetrics': requestWOMetricsMock, + 'config.perseoCore.noticesURL': 'http://mokedurl.org', + 'alarm.raise': alarmRaiseMock, + 'alarm.POST_EVENT': postEvent, + 'processCBv2Notice': processCBv2NoticeMock, + 'config.nextCore': {noticesURL: 'http://nextCoreMockURL'}, + 'myutils.logErrorIf': logErrorMock, + })(function () { + var callback = function (e, request) { + should.exist(e); + should.not.exists(request); + should.equal(e.httpCode, 400); + // Checking call to processCBv2NoticeMock + processCBv2NoticeMock.should.have.been.calledWith('utestv2', '/test/notices/unitv2', v2notice, 0); + processCBv2NoticeMock.should.be.calledOnce; + // Checking call to requestWOMetrics + var h = {'fiware-servicepath': '/test/notices/unit'}; + should.equal(requestWOMetricsMock.calledTwice, true); + expect(requestWOMetricsMock).to.have.been.calledWith('post', { + url: 'http://mokedurl.org', + json: coreNotice1, + headers: h + }); + expect(requestWOMetricsMock).to.have.been.calledWith('post', { + url: 'http://nextCoreMockURL', + json: coreNotice1, + headers: h + }); + alarmRaiseMock.should.have.been.calledWith(postEvent); + alarmRaiseMock.should.be.calledOnce; + // Checking logError + logErrorMock.should.have.been.calledWith('errorMock!'); + logErrorMock.should.be.calledOnce; + done(); + }; + notices.Do(v2notice, callback); + }); + }); + + it('should fail with invalid NGSIv2 data', function(done) { + + var callback = function (e, request) { + should.not.exists(request); + should.exist(e); + // Check invalid notice error + should.equal(e.httpCode, 400); + should.equal(e.message, 'invalid NGSIv2 notice format data must be an array, not a number'); + done(); + }; + v2notice.data =123; + notices.Do(v2notice, callback); + }); + it('should fail with invalid NGSIv1 contextResponses', function(done) { + var callback = function (e, request) { + should.not.exists(request); + should.exist(e); + // Check invalid Location error + should.equal(e.httpCode, 400); + should.equal(e.message, 'ContextResponses is not an array (number)'); + done(); + }; + v1notice.contextResponses =123; + notices.Do(v1notice, callback); + }); + + it('should fail whith invalid Servipaths', function(done) { + + var callback = function (e, request) { + should.exist(e); + should.not.exist(request); + should.equal(e.httpCode, 400); + should.equal(e.message, 'Number of servicepath items does not match ContextResponses(3,1)'); + done(); + }; + v1notice.subservice += ',extra/service/4fail, extra2/service'; + notices.Do(v1notice, callback); + }); + }); +}); \ No newline at end of file diff --git a/test/unit/notices_processCBNotice.js b/test/unit/notices_processCBNotice.js new file mode 100644 index 00000000..f4352374 --- /dev/null +++ b/test/unit/notices_processCBNotice.js @@ -0,0 +1,403 @@ +/* + * Copyright 2015 Telefonica Investigación y Desarrollo, S.A.U + * + * This file is part of perseo-fe + * + * perseo-fe 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. + * + * perseo-fe 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 perseo-fe. + * If not, see http://www.gnu.org/licenses/. + * + * For those usages not covered by the GNU Affero General Public License + * please contact with::[contacto@tid.es] + * + * Created by: Carlos Blanco - Future Internet Consulting and Development Solutions (FICODES) + */ +'use strict'; + +var rewire = require('rewire'); +var notices = rewire('../../lib/models/notices'); +var chai = require('chai'); +var sinon = require('sinon'); +var sinonChai = require('sinon-chai'); +var expect = chai.expect; +chai.use(sinonChai); +chai.Should(); + +var id = 'sensor-1'; +var type = 'tipeExample1'; + +var attrKey = 'Attr1'; +var attrType = 'Number'; +var attrValue = 122; + +var subservice = '/test/notices/unit'; +var service = 'utest'; + +var noticeExampleV1 = JSON.stringify({ + 'subscriptionId': '5b311ccb29adb333f843b5f3', + 'originator': 'localhost', + 'contextResponses': [ + { + 'contextElement': { + 'id': id, + 'type': type, + 'isPattern': 'false', + 'attributes': [ + { + 'name': attrKey, + 'type': attrType, + 'value': attrValue + } + ] + } + } + ], + 'subservice': subservice, + 'service': service +}); + +var processCBNotice = notices.__get__('processCBNotice'); + +// Mocks +var mockedUid = 'MockedUID_'; +var mockedDateMilis = 442796400000; +var uuidMock = sinon.spy(function () { + return mockedUid; +}); +var dateNowMock = sinon.spy(function() { + return mockedDateMilis; +}); + +// Date +var dateType = 'DateTime'; +var dateValue = '2018-06-03T09:31:26.296Z'; + +// Location +var locType = 'geo:point'; +var lat = 40.418889; +var long = -3.691944; +var x = 441298.13043762115; +var y = 4474481.316254241; +var locValue = lat + ', ' + long; + + +describe('Notices NGSIv1', function() { + var noticeExample; + beforeEach(function() { + // Default + noticeExample = JSON.parse(noticeExampleV1); + }); + describe('#processCBNotice', function() { + + it('should accept simple notice using Number type', function(done) { + + notices.__with__({ + 'uuid.v1': uuidMock, + 'Date.now': dateNowMock + })(function () { + var noticeResult = processCBNotice(service, subservice, noticeExample, 0); + expect(noticeResult.noticeId).to.equal(mockedUid); + expect(noticeResult.noticeTS).to.equal(mockedDateMilis); + expect(noticeResult.id).to.equal(id); + expect(noticeResult.type).to.equal(type); + expect(noticeResult.subservice).to.equal(subservice); + expect(noticeResult.service).to.equal(service); + expect(noticeResult.isPattern).to.equal('false'); + expect(noticeResult[attrKey + '__type']).to.equal(attrType); + expect(noticeResult[attrKey]).to.equal(attrValue); + done(); + }); + }); + + it('should accept simple notice using geo:point type', function(done) { + + var parseLocationMock = sinon.spy(function() { + return { + lat: lat, + lon: long, + x: x, + y: y + }; + }); + notices.__with__({ + 'uuid.v1': uuidMock, + 'Date.now': dateNowMock, + 'parseLocation': parseLocationMock + })(function () { + noticeExample.contextResponses[0].contextElement.attributes[0].type = locType; + noticeExample.contextResponses[0].contextElement.attributes[0].value = locValue; + var noticeResult = processCBNotice(service, subservice, noticeExample, 0); + expect(noticeResult.noticeId).to.equal(mockedUid); + expect(noticeResult.noticeTS).to.equal(mockedDateMilis); + expect(noticeResult.id).to.equal(id); + expect(noticeResult.type).to.equal(type); + expect(noticeResult.subservice).to.equal(subservice); + expect(noticeResult.service).to.equal(service); + expect(noticeResult.isPattern).to.equal('false'); + expect(noticeResult[attrKey + '__type']).to.equal(locType); + expect(noticeResult[attrKey]).to.equal(locValue); + expect(noticeResult[attrKey + '__lat']).to.equal(lat); + expect(noticeResult[attrKey + '__lon']).to.equal(long); + expect(noticeResult[attrKey + '__x']).to.equal(x); + expect(noticeResult[attrKey + '__y']).to.equal(y); + parseLocationMock.should.have.been.calledWith(locValue); + parseLocationMock.should.be.calledOnce; + done(); + }); + }); + + it('should accept simple notice using DateTime type', function(done) { + + var parseDateMock = sinon.spy(function() { + return { + 'ts': 1528018286296, + 'day': 3, + 'month': 6, + 'year': 2018, + 'hour': 11, + 'minute': 31, + 'second': 26 + }; + }); + notices.__with__({ + 'uuid.v1': uuidMock, + 'Date.now': dateNowMock, + 'parseDate': parseDateMock + })(function () { + noticeExample.contextResponses[0].contextElement.attributes[0].type = dateType; + noticeExample.contextResponses[0].contextElement.attributes[0].value = dateValue; + var noticeResult = processCBNotice(service, subservice, noticeExample, 0); + expect(noticeResult.noticeId).to.equal(mockedUid); + expect(noticeResult.noticeTS).to.equal(mockedDateMilis); + expect(noticeResult.id).to.equal(id); + expect(noticeResult.type).to.equal(type); + expect(noticeResult.subservice).to.equal(subservice); + expect(noticeResult.service).to.equal(service); + expect(noticeResult.isPattern).to.equal('false'); + expect(noticeResult[attrKey + '__type']).to.equal(dateType); + expect(noticeResult[attrKey]).to.equal(dateValue); + expect(noticeResult[attrKey + '__ts']).to.equal(1528018286296); + expect(noticeResult[attrKey + '__day']).to.equal(3); + expect(noticeResult[attrKey + '__month']).to.equal(6); + expect(noticeResult[attrKey + '__year']).to.equal(2018); + expect(noticeResult[attrKey + '__hour']).to.equal(11); + expect(noticeResult[attrKey + '__minute']).to.equal(31); + expect(noticeResult[attrKey + '__second']).to.equal(26); + parseDateMock.should.have.been.calledWith(dateValue); + parseDateMock.should.be.calledOnce; + done(); + }); + }); + + it('should accept notices including metadata without type', function(done) { + + var at = 'theMetaAttribute'; + var metaAtVal = 'mockedValue1234'; + notices.__with__({ + 'uuid.v1': uuidMock, + 'Date.now': dateNowMock, + })(function () { + noticeExample.contextResponses[0].contextElement.attributes[0].metadatas = [{ + 'name': at, + 'value': metaAtVal + }]; + var noticeResult = processCBNotice(service, subservice, noticeExample, 0); + expect(noticeResult.noticeId).to.equal(mockedUid); + expect(noticeResult.noticeTS).to.equal(mockedDateMilis); + expect(noticeResult.id).to.equal(id); + expect(noticeResult.type).to.equal(type); + expect(noticeResult.subservice).to.equal(subservice); + expect(noticeResult.service).to.equal(service); + expect(noticeResult.isPattern).to.equal('false'); + expect(noticeResult[attrKey + '__type']).to.equal(attrType); + expect(noticeResult[attrKey]).to.equal(attrValue); + expect(noticeResult[attrKey + '__metadata__' + at + '__type']).not.exist; + expect(noticeResult[attrKey + '__metadata__' + at]).to.equal(metaAtVal); + done(); + }); + }); + + it('should accept notice using DateTime metadata', function(done) { + + var at = 'theMetaAttribute'; + var parseDateMock = sinon.spy(function () { + return { + 'ts': 1528018286296, + 'day': 3, + 'month': 6, + 'year': 2018, + 'hour': 11, + 'minute': 31, + 'second': 26 + }; + }); + notices.__with__({ + 'uuid.v1': uuidMock, + 'Date.now': dateNowMock, + 'parseDate': parseDateMock + })(function () { + noticeExample.contextResponses[0].contextElement.attributes[0].metadatas = [{ + 'name': at, + 'value': dateValue, + 'type': dateType + }]; + var noticeResult = processCBNotice(service, subservice, noticeExample, 0); + expect(noticeResult.noticeId).to.equal(mockedUid); + expect(noticeResult.noticeTS).to.equal(mockedDateMilis); + expect(noticeResult.id).to.equal(id); + expect(noticeResult.type).to.equal(type); + expect(noticeResult.subservice).to.equal(subservice); + expect(noticeResult.service).to.equal(service); + expect(noticeResult.isPattern).to.equal('false'); // why not boolean?? + expect(noticeResult[attrKey + '__type']).to.equal(attrType); + expect(noticeResult[attrKey]).to.equal(attrValue); + expect(noticeResult[attrKey + '__metadata__' + at + '__type']).to.equal(dateType); + expect(noticeResult[attrKey + '__metadata__' + at + '__ts']).to.equal(1528018286296); + expect(noticeResult[attrKey + '__metadata__' + at + '__day']).to.equal(3); + expect(noticeResult[attrKey + '__metadata__' + at + '__month']).to.equal(6); + expect(noticeResult[attrKey + '__metadata__' + at + '__year']).to.equal(2018); + expect(noticeResult[attrKey + '__metadata__' + at + '__hour']).to.equal(11); + expect(noticeResult[attrKey + '__metadata__' + at + '__minute']).to.equal(31); + expect(noticeResult[attrKey + '__metadata__' + at + '__second']).to.equal(26); + parseDateMock.should.have.been.calledWith(dateValue); + parseDateMock.should.be.calledOnce; + done(); + }); + }); + + it('should fail when contains Id As Attribute', function() { + noticeExample.contextResponses[0].contextElement.attributes[0].name = 'id'; + var noticeResult = processCBNotice(service, subservice, noticeExample, 0); + noticeResult.should.be.instanceof(notices.errors.IdAsAttribute); + expect(noticeResult.name).to.equal('ID_ATTRIBUTE'); + expect(noticeResult.message).to.equal('id as attribute ' + JSON.stringify(noticeExample.contextResponses[0] + .contextElement.attributes[0])); + expect(noticeResult.httpCode).to.equal(400); + }); + + it('should fail when contains Type As Attribute', function() { + noticeExample.contextResponses[0].contextElement.attributes[0].name = 'type'; + var noticeResult = processCBNotice(service, subservice, noticeExample, 0); + noticeResult.should.be.instanceof(notices.errors.TypeAsAttribute); + expect(noticeResult.name).to.equal('TYPE_ATTRIBUTE'); + expect(noticeResult.message).to.equal('type as attribute ' + + JSON.stringify(noticeExample.contextResponses[0].contextElement.attributes[0])); + expect(noticeResult.httpCode).to.equal(400); + }); + + // Weird functionality + it('should accept notices and parse value as location when exist an attribute named location in metadata', + function(done) { + + // this feature does not seem to make sense + var at = 'location'; + var parseLocationMock = sinon.spy(function() { + return { + lat: lat, + lon: long, + x: x, + y: y + }; + }); + notices.__with__({ + 'uuid.v1': uuidMock, + 'Date.now': dateNowMock, + 'parseLocation': parseLocationMock + })(function () { + noticeExample.contextResponses[0].contextElement.attributes[0].metadatas = [{ + 'name': at, + 'value': locValue, + 'type': locType + }]; + var noticeResult = processCBNotice(service, subservice, noticeExample, 0); + expect(noticeResult.noticeId).to.equal(mockedUid); + expect(noticeResult.noticeTS).to.equal(mockedDateMilis); + expect(noticeResult.id).to.equal(id); + expect(noticeResult.type).to.equal(type); + expect(noticeResult.subservice).to.equal(subservice); + expect(noticeResult.service).to.equal(service); + expect(noticeResult.isPattern).to.equal('false'); + expect(noticeResult[attrKey + '__type']).to.equal(attrType); + expect(noticeResult[attrKey]).to.equal(attrValue); + expect(noticeResult[attrKey + '__metadata__' + at]).to.equal(locValue); + expect(noticeResult[attrKey + '__metadata__' + at + '__type']).to.equal(locType); + expect(noticeResult[attrKey + '__lat']).to.equal(lat); + expect(noticeResult[attrKey + '__lon']).to.equal(long); + expect(noticeResult[attrKey + '__x']).to.equal(x); + expect(noticeResult[attrKey + '__y']).to.equal(y); + + // Why call parselocation with the attribute value and not with location metadata attribute? + parseLocationMock.should.have.been.calledWith(attrValue); + parseLocationMock.should.be.calledOnce; + done(); + }); + } + ); + + it('should catch correctly errors', + function(done) { + + var error = new Error('fake error'); + var parseLocationMock = sinon.stub().throws(error); + var logErrorMock = sinon.spy( + function(notice) {} + ); + notices.__with__({ + 'uuid.v1': uuidMock, + 'Date.now': dateNowMock, + 'parseLocation': parseLocationMock, + 'myutils.logErrorIf': logErrorMock + })(function () { + noticeExample.contextResponses[0].contextElement.attributes[0].type = locType; + noticeExample.contextResponses[0].contextElement.attributes[0].value = locValue; + var noticeResult = processCBNotice(service, subservice, noticeExample, 0); + noticeResult.should.be.instanceof(notices.errors.InvalidNotice); + expect(noticeResult.name).to.equal('INVALID_NOTICE'); + expect(noticeResult.message).to.equal('invalid notice format ' + JSON.stringify(noticeExample)); + expect(noticeResult.httpCode).to.equal(400); + expect(parseLocationMock).to.throw(Error); + expect(parseLocationMock).to.have.been.calledWith(locValue); + // Checking logError + logErrorMock.should.have.been.calledWith(noticeResult); + logErrorMock.should.be.calledOnce; + done(); + }); + } + ); + + it('should fail parsing invalid location attribute', function(done) { + + var error = new notices.errors.InvalidLocation('fake error'); + var parseLocationMock = sinon.spy(function() { + return error; + }); + notices.__with__({ + 'uuid.v1': uuidMock, + 'Date.now': dateNowMock, + 'parseLocation': parseLocationMock + })(function () { + noticeExample.contextResponses[0].contextElement.attributes[0].type = locType; + noticeExample.contextResponses[0].contextElement.attributes[0].value = locValue; + var noticeResult = processCBNotice(service, subservice, noticeExample, 0); + noticeResult.should.be.instanceof(notices.errors.InvalidLocation); + expect(noticeResult.name).to.equal('INVALID_LOCATION'); + expect(noticeResult.message).to.equal(error.message); + expect(noticeResult.httpCode).to.equal(400); + parseLocationMock.should.have.been.calledWith(locValue); + parseLocationMock.should.be.calledOnce; + done(); + }); + }); + + }); +}); diff --git a/test/unit/notices_processCBv2Notice.js b/test/unit/notices_processCBv2Notice.js new file mode 100644 index 00000000..5b67a1e2 --- /dev/null +++ b/test/unit/notices_processCBv2Notice.js @@ -0,0 +1,608 @@ +/* + * Copyright 2015 Telefonica Investigación y Desarrollo, S.A.U + * + * This file is part of perseo-fe + * + * perseo-fe 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. + * + * perseo-fe 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 perseo-fe. + * If not, see http://www.gnu.org/licenses/. + * + * For those usages not covered by the GNU Affero General Public License + * please contact with::[contacto@tid.es] + * + * Created by: Carlos Blanco - Future Internet Consulting and Development Solutions (FICODES) + */ +'use strict'; + +var rewire = require('rewire'); +var notices = rewire('../../lib/models/notices'); +var chai = require('chai'); +var sinon = require('sinon'); +var sinonChai = require('sinon-chai'); +var expect = chai.expect; +chai.Should(); +chai.use(sinonChai); + +var id = 'sensor-1'; +var type = 'tipeExample1'; + +var attrType = 'Number'; +var attrValue = 122; + +var subservice = '/test/notices/unit'; +var service = 'utest'; + +var noticeExampleV2 = { + 'subscriptionId': '5b311ccb29adb333f843b5f3', + 'data': [ + { + 'id': id, + 'type': type, + } + ], + 'subservice': subservice, + 'service': service +}; +var attrKey = 'Attr1'; +noticeExampleV2.data[0][attrKey] = { + 'type': attrType, + 'value': attrValue +}; +noticeExampleV2 = JSON.stringify(noticeExampleV2); + +var processCBv2Notice = notices.__get__('processCBv2Notice'); + +// Mocks +var mockedUid = 'MockedUID_'; +var mockedDateMilis = 442796400000; +var uuidMock = sinon.spy(function () { + return mockedUid; +}); +var dateNowMock = sinon.spy(function() { + return mockedDateMilis; +}); + +// Date +var dateType = 'DateTime'; +var dateValue = '2018-06-03T09:31:26.296Z'; + +// Location +var locType = 'geo:point'; +var lat = 40.418889; +var long = -3.691944; +var x = 441298.13043762115; +var y = 4474481.316254241; +var locValue = lat + ', ' + long; + +describe('Notices NGSIv2', function() { + var noticeExample; + beforeEach(function() { + // Default + noticeExample = JSON.parse(noticeExampleV2); + }); + describe('#processCBv2Notice', function() { + + it('should accept simple notice using Number type', function(done) { + + notices.__with__({ + 'uuid.v1': uuidMock, + 'Date.now': dateNowMock + })(function () { + var noticeResult = processCBv2Notice(service, subservice, noticeExample, 0); + expect(noticeResult.noticeId).to.equal(mockedUid); + expect(noticeResult.noticeTS).to.equal(mockedDateMilis); + expect(noticeResult.id).to.equal(id); + expect(noticeResult.type).to.equal(type); + expect(noticeResult.subservice).to.equal(subservice); + expect(noticeResult.service).to.equal(service); + expect(noticeResult.isPattern).to.equal(false); + expect(noticeResult[attrKey + '__type']).to.equal(attrType); + expect(noticeResult[attrKey]).to.equal(attrValue); + done(); + }); + }); + + it('should accept simple notice using DateTime', function(done) { + + var parseDateMock = sinon.spy(function() { + return { + a: 123, + b: 456 + }; + }); + + notices.__with__({ + 'uuid.v1': uuidMock, + 'Date.now': dateNowMock, + 'parseDate': parseDateMock, + })(function () { + noticeExample.data[0][attrKey].type = dateType; + noticeExample.data[0][attrKey].value = dateValue; + var noticeResult = processCBv2Notice(service, subservice, noticeExample, 0); + expect(noticeResult.noticeId).to.equal(mockedUid); + expect(noticeResult.noticeTS).to.equal(mockedDateMilis); + expect(noticeResult.id).to.equal(id); + expect(noticeResult.type).to.equal(type); + expect(noticeResult.subservice).to.equal(subservice); + expect(noticeResult.service).to.equal(service); + expect(noticeResult.isPattern).to.equal(false); + expect(noticeResult[attrKey + '__type']).to.equal(dateType); + expect(noticeResult[attrKey + '__a']).to.equal(123); + expect(noticeResult[attrKey + '__b']).to.equal(456); + parseDateMock.should.have.been.calledWith(dateValue); + parseDateMock.should.be.calledOnce; + done(); + }); + }); + + it('should accept simple notice using geo:point type', function(done) { + + var parseLocationMock = sinon.spy(function() { + return { + lat: lat, + lon: long, + x: x, + y: y + }; + }); + + notices.__with__({ + 'uuid.v1': uuidMock, + 'Date.now': dateNowMock, + 'parseLocation': parseLocationMock + })(function () { + noticeExample.data[0][attrKey].type = locType; + noticeExample.data[0][attrKey].value = locValue; + var noticeResult = processCBv2Notice(service, subservice, noticeExample, 0); + expect(noticeResult.noticeId).to.equal(mockedUid); + expect(noticeResult.noticeTS).to.equal(mockedDateMilis); + expect(noticeResult.id).to.equal(id); + expect(noticeResult.type).to.equal(type); + expect(noticeResult.subservice).to.equal(subservice); + expect(noticeResult.service).to.equal(service); + expect(noticeResult.isPattern).to.equal(false); + expect(noticeResult[attrKey + '__type']).to.equal(locType); + expect(noticeResult[attrKey + '__lat']).to.equal(lat); + expect(noticeResult[attrKey + '__lon']).to.equal(long); + expect(noticeResult[attrKey + '__x']).to.equal(x); + expect(noticeResult[attrKey + '__y']).to.equal(y); + parseLocationMock.should.have.been.calledWith(locValue); + parseLocationMock.should.be.calledOnce; + done(); + }); + }); + + it('should accept notices including metadata without type', function(done) { + + var at = 'theAttribute'; + var metavalue = 'attMetaEXtraValue'; + notices.__with__({ + 'uuid.v1': uuidMock, + 'Date.now': dateNowMock + })(function () { + var meta = noticeExample.data[0].Attr1.metadata = {}; + meta[at] = { + 'value': metavalue + }; + var noticeResult = processCBv2Notice(service, subservice, noticeExample, 0); + expect(noticeResult.noticeId).to.equal(mockedUid); + expect(noticeResult.noticeTS).to.equal(mockedDateMilis); + expect(noticeResult.id).to.equal(id); + expect(noticeResult.type).to.equal(type); + expect(noticeResult.subservice).to.equal(subservice); + expect(noticeResult.service).to.equal(service); + expect(noticeResult.isPattern).to.equal(false); + expect(noticeResult[attrKey + '__type']).to.equal(attrType); + expect(noticeResult[attrKey]).to.equal(attrValue); + expect(noticeResult[attrKey + '__metadata__' + at + '__type']).not.exist; + expect(noticeResult[attrKey + '__metadata__' + at]).to.equal(metavalue); + done(); + }); + }); + + it('should accept notices including geo:point metadata', function(done) { + + var at = 'theAttribute'; + var parseLocationMock = sinon.spy(function() { + return { + lat: lat, + lon: long, + x: x, + y: y + }; + }); + notices.__with__({ + 'uuid.v1': uuidMock, + 'Date.now': dateNowMock, + 'parseLocation': parseLocationMock + })(function () { + var meta = noticeExample.data[0].Attr1.metadata = {}; + meta[at] = { + 'value': locValue, + 'type': locType + }; + var noticeResult = processCBv2Notice(service, subservice, noticeExample, 0); + expect(noticeResult.noticeId).to.equal(mockedUid); + expect(noticeResult.noticeTS).to.equal(mockedDateMilis); + expect(noticeResult.id).to.equal(id); + expect(noticeResult.type).to.equal(type); + expect(noticeResult.subservice).to.equal(subservice); + expect(noticeResult.service).to.equal(service); + expect(noticeResult.isPattern).to.equal(false); + expect(noticeResult[attrKey + '__type']).to.equal(attrType); + expect(noticeResult[attrKey]).to.equal(attrValue); + expect(noticeResult[attrKey + '__metadata__' + at + '__type']).to.equal(locType); + expect(noticeResult[attrKey + '__metadata__' + at + '__lat']).to.equal(lat); + expect(noticeResult[attrKey + '__metadata__' + at + '__lon']).to.equal(long); + expect(noticeResult[attrKey + '__metadata__' + at + '__x']).to.equal(x); + expect(noticeResult[attrKey + '__metadata__' + at + '__y']).to.equal(y); + parseLocationMock.should.have.been.calledWith(locValue); + parseLocationMock.should.be.calledOnce; + done(); + }); + }); + + it('should accept notices including DateTime metadata', function(done) { + + var at = 'theAttribute'; + var parseDateMock = sinon.spy(function() { + return { + 'ts': 1528018286296, + 'day': 3, + 'month': 6, + 'year': 2018, + 'hour': 11 + // ... + }; + }); + notices.__with__({ + 'uuid.v1': uuidMock, + 'Date.now': dateNowMock, + 'parseDate': parseDateMock, + })(function () { + var meta = noticeExample.data[0].Attr1.metadata = {}; + meta[at] = { + 'value': dateValue, + 'type': dateType + }; + var noticeResult = processCBv2Notice(service, subservice, noticeExample, 0); + expect(noticeResult.noticeId).to.equal(mockedUid); + expect(noticeResult.noticeTS).to.equal(mockedDateMilis); + expect(noticeResult.id).to.equal(id); + expect(noticeResult.type).to.equal(type); + expect(noticeResult.subservice).to.equal(subservice); + expect(noticeResult.service).to.equal(service); + expect(noticeResult.isPattern).to.equal(false); + expect(noticeResult[attrKey + '__type']).to.equal(attrType); + expect(noticeResult[attrKey]).to.equal(attrValue); + expect(noticeResult[attrKey + '__metadata__' + at + '__type']).to.equal(dateType); + expect(noticeResult[attrKey + '__metadata__' + at + '__ts']).to.equal(1528018286296); + expect(noticeResult[attrKey + '__metadata__' + at + '__day']).to.equal(3); + expect(noticeResult[attrKey + '__metadata__' + at + '__month']).to.equal(6); + expect(noticeResult[attrKey + '__metadata__' + at + '__year']).to.equal(2018); + expect(noticeResult[attrKey + '__metadata__' + at + '__hour']).to.equal(11); + parseDateMock.should.have.been.calledWith(dateValue); + parseDateMock.should.be.calledOnce; + done(); + }); + }); + + it('should fail parsing invalid DateTime metadata attribute', function(done) { + + var at = 'theMetaAttribute'; + var invalidMetaDate = '2018-96-03T09:31:26.296Z'; // invalid date for metadata + var errorDateNotice = new notices.errors.InvalidDateTime(invalidMetaDate); + var parseDateMock = sinon.spy(function() { + return errorDateNotice; + }); + notices.__with__({ + 'uuid.v1': uuidMock, + 'Date.now': dateNowMock, + 'parseDate': parseDateMock + })(function () { + // Set Invalid DateType metadata attribute + var meta = noticeExample.data[0].Attr1.metadata = {}; + meta[at] = { + 'value': invalidMetaDate, + 'type': dateType + }; + var noticeResult = processCBv2Notice(service, subservice, noticeExample, 0); + noticeResult.should.be.instanceof(notices.errors.InvalidDateTime); + expect(noticeResult.name).to.equal('INVALID_DATETIME'); + expect(noticeResult.message).to.equal('Invalid ' + dateType + + ' attribute metadata: datetime is not valid ' + invalidMetaDate); + expect(noticeResult.httpCode).to.equal(400); + parseDateMock.should.have.been.calledWith(invalidMetaDate); + parseDateMock.should.be.calledOnce; + done(); + }); + }); + + it('should fail parsing invalid DateTime attribute', function(done) { + + var invalidAttDate = '2018-08-32T09:31:26.296Z'; // invalid date for attribute + var errorDateNotice = new notices.errors.InvalidDateTime(invalidAttDate); + var parseDateMock = sinon.spy(function() { + return errorDateNotice; + }); + notices.__with__({ + 'uuid.v1': uuidMock, + 'Date.now': dateNowMock, + 'parseDate': parseDateMock + })(function () { + // Set Invalid DateType attribute + noticeExample.data[0].Attr1 = { + 'value': invalidAttDate, + 'type': dateType + }; + var noticeResult = processCBv2Notice(service, subservice, noticeExample, 0); + noticeResult.should.be.instanceof(notices.errors.InvalidDateTime); + expect(noticeResult.name).to.equal('INVALID_DATETIME'); + expect(noticeResult.message).to.equal('Invalid ' + dateType + + ' attribute: datetime is not valid ' + invalidAttDate); + expect(noticeResult.httpCode).to.equal(400); + parseDateMock.should.have.been.calledWith(invalidAttDate); + parseDateMock.should.be.calledOnce; + done(); + }); + }); + + it('should fail parsing invalid location attribute', function(done) { + + var invalidLoc = '47.418889, -3.691944, 12.123'; // invalid location for attribute + var locError = new notices.errors.InvalidLocation(invalidLoc); + var parseLocationMock = sinon.spy(function() { + return locError; + }); + notices.__with__({ + 'uuid.v1': uuidMock, + 'Date.now': dateNowMock, + 'parseLocation': parseLocationMock + })(function () { + // Set Invalid location attribute + noticeExample.data[0].Attr1 = { + 'value': invalidLoc, + 'type': locType + }; + var noticeResult = processCBv2Notice(service, subservice, noticeExample, 0); + noticeResult.should.be.instanceof(notices.errors.InvalidLocation); + expect(noticeResult.name).to.equal('INVALID_LOCATION'); + expect(noticeResult.message).to.equal('Invalid ' + locType + + ' attribute: invalid location ' + invalidLoc); + expect(noticeResult.httpCode).to.equal(400); + parseLocationMock.should.have.been.calledWith(invalidLoc); + parseLocationMock.should.be.calledOnce; + done(); + }); + }); + + it('should fail parsing invalid location metadata attribute', function(done) { + + var at = 'theMetaAttribute'; + var invalidLoc = '47.418889, -3.691944, 12.123'; // invalid location for attribute + var locError = new notices.errors.InvalidLocation(invalidLoc); + var parseLocationMock = sinon.spy(function() { + return locError; + }); + notices.__with__({ + 'uuid.v1': uuidMock, + 'Date.now': dateNowMock, + 'parseLocation': parseLocationMock + })(function () { + // Set Invalid location metadata attribute + var meta = noticeExample.data[0].Attr1.metadata = {}; + meta[at] = { + 'value': invalidLoc, + 'type': locType + }; + var noticeResult = processCBv2Notice(service, subservice, noticeExample, 0); + noticeResult.should.be.instanceof(notices.errors.InvalidLocation); + expect(noticeResult.name).to.equal('INVALID_LOCATION'); + expect(noticeResult.message).to.equal('Invalid ' + locType + + ' attribute metadata: invalid location ' + invalidLoc); + expect(noticeResult.httpCode).to.equal(400); + parseLocationMock.should.have.been.calledWith(invalidLoc); + parseLocationMock.should.be.calledOnce; + done(); + }); + }); + + it('should handle exception correctly', function(done) { + + var at = 'theMetaAttribute'; + var error = new Error('fake error'); + var parseLocationMock = sinon.stub().throws(error); + var logErrorMock = sinon.spy( + function(notice) {} + ); + + notices.__with__({ + 'uuid.v1': uuidMock, + 'Date.now': dateNowMock, + 'parseLocation': parseLocationMock, + 'myutils.logErrorIf': logErrorMock + })(function () { + // Set location metadata attribute + var meta = noticeExample.data[0].Attr1.metadata = {}; + meta[at] = { + 'value': locValue, + 'type': locType + }; + var noticeResult = processCBv2Notice(service, subservice, noticeExample, 0); + noticeResult.should.be.instanceof(notices.errors.InvalidV2Notice); + expect(noticeResult.name).to.equal('INVALID_NGSIV2_NOTICE'); + expect(noticeResult.message).to.equal('invalid NGSIv2 notice format ' + error + + ' (' + JSON.stringify(noticeExample) +')'); + expect(noticeResult.httpCode).to.equal(400); + expect(parseLocationMock).to.throw(Error); + expect(parseLocationMock).to.have.been.calledWith(locValue); + // Checking logError + logErrorMock.should.have.been.calledWith(noticeResult); + logErrorMock.should.be.calledOnce; + done(); + }); + }); + }); + + + describe('#Data types location and time', function() { + var noticeExample; + beforeEach(function() { + // Default + noticeExample = JSON.parse(noticeExampleV2); + }); + + it('should fail parsing invalid location attributes', function() { + var callback = function (e, request) { + expect(e).exist; + expect(request).not.exist; + expect(e.httpCode).to.equal(400); + expect(e.message[0]).to.equal('Invalid geo:point attribute: invalid location 47.41x8889, -3.691944, x'); + expect(e.message[1]).to.equal('Invalid geo:point attribute metadata: longitude is not valid NaN'); + expect(e.message[2]).to.equal('Invalid geo:point attribute metadata: invalid location Error: ' + + 'Longitude must be in range [-180, 180).'); + expect(e.message[3]).to.equal('Invalid geo:point attribute: invalid location Error: ' + + 'Latitude must be in range [-90, 90).'); + expect(e.message[4]).to.equal('Invalid geo:point attribute metadata: latitude is not valid NaN'); + expect(e.message[5]).to.equal('Invalid geo:point attribute: invalid location 4559'); + }; + noticeExample.data = [ + { + 'id': 'sensor-1', + 'type': 'tipeExample1', + 'Attr1': { + 'type': 'geo:point', + 'value': '47.41x8889, -3.691944, x', + 'metadata': { + 'metaAttr1': { + type: 'geo:point', + value: '47.55555, -ll3.333x-333' + } + } + } + }, + { + 'id': 'sensor-2', + 'type': 'tipeExample2', + 'Attr1': { + 'type': 'geo:point', + 'value': '43.41x8889, -5.691944', + 'metadata': { + 'metaAttr1': { + type: 'geo:point', + value: '47.55555, -ll3.333x-333' + } + } + } + }, + { + 'id': 'sensor-3', + 'type': 'tipeExample1', + 'Attr1': { + 'type': 'geo:point', + 'value': '47.418889, -3.691944', + 'metadata': { + 'metaAttr1': { + type: 'geo:point', + value: '47.55555, -333.333333' + } + } + } + }, + { + 'id': 'sensor-4', + 'type': 'tipeExample1', + 'Attr1': { + 'type': 'geo:point', + 'value': '470.418889, -3.691944', + 'metadata': {} + } + }, + { + 'id': 'sensor-5', + 'type': 'tipeExample2', + 'Attr1': { + 'type': 'geo:point', + 'value': '43.41x8889, -5.691944', + 'metadata': { + 'metaAttr1': { + type: 'geo:point', + value: 'x4x7x.5555x5-, -3.33333' + } + } + } + }, + { + 'id': 'sensor-6', + 'type': 'tipeExample2', + 'Attr1': { + 'type': 'geo:point', + 'value': 4559, + 'metadata': { + 'metaAttr1': { + type: 'geo:point', + value: '47.55555, 3.33333' + } + } + } + } + ]; + noticeExample.subservice = '/test/notices/unit,/test/notices/unit,/test/notices/unit,' + + '/test/notices/unit,/test/notices/unit,/test/notices/unit'; + notices.Do(noticeExample, callback); + }); + it('should fail parsing invalid DateTime attributes', function() { + var callback = function (e, request) { + expect(e).exist; + expect(request).not.exist; + expect(e.httpCode).to.equal(400); + expect(e.message[0]).to.equal('Invalid DateTime attribute metadata: datetime' + + ' is not valid 2018-96-03T09:31:26.296Z'); + expect(e.message[1]).to.equal('Invalid DateTime attribute: datetime' + + ' is not valid 2018-08-32T09:31:26.296Z'); + }; + noticeExample.data = [ + { + 'id': 'sensor-1', + 'type': 'tipeExample1', + 'Attr1': { + 'type': 'DateTime', + 'value': '2018-06-03T09:31:26.296Z', + 'metadata': { + 'metaAttr1': { + 'value': '2018-96-03T09:31:26.296Z', + 'type': 'DateTime' + } + } + } + }, + { + 'id': 'sensor-2', + 'type': 'tipeExample2', + 'Attr1': { + 'type': 'DateTime', + 'value': '2018-08-32T09:31:26.296Z', + 'metadata': { + 'metaAttr1': { + 'value': '2018-06-03T09:31:26.296Z', + 'type': 'DateTime' + } + } + } + } + ]; + noticeExample.subservice = '/test/notices/unit,/test/notices/unit'; + notices.Do(noticeExample, callback); + }); + }); +}); \ No newline at end of file diff --git a/test/unit/updateAction.js b/test/unit/updateAction.js new file mode 100644 index 00000000..60768265 --- /dev/null +++ b/test/unit/updateAction.js @@ -0,0 +1,451 @@ +/* + * Copyright 2015 Telefonica Investigación y Desarrollo, S.A.U + * + * This file is part of perseo-fe + * + * perseo-fe 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. + * + * perseo-fe 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 perseo-fe. + * If not, see http://www.gnu.org/licenses/. + * + * For those usages not covered by the GNU Affero General Public License + * please contact with iot_support at tid dot es + * + * Created by: Carlos Blanco - Future Internet Consulting and Development Solutions (FICODES) + */ + +'use strict'; + +var should = require('should'); +var rewire = require('rewire'); +var updateAction = rewire('../../lib/models/updateAction.js'); +var chai = require('chai'); +var sinon = require('sinon'); +var sinonChai = require('sinon-chai'); +chai.Should(); +chai.use(sinonChai); + + +var metaExample = { + 'timestamp': '2018-12-05T12:34:32.00Z' +}; +var action1 = { + 'type': 'update', + 'parameters': { + 'version': '2', + 'attributes': [ + { + 'name':'streetLightID', + 'type': 'Text', + 'value': '${id}' + }, + { + 'name':'textNumberLit', + 'type': 'Text', + 'value': 666 + }, + { + 'name':'textBoolLit', + 'type': 'Text', + 'value': false + }, + { + 'name':'textObjLit', + 'type': 'Text', + 'value': {a: 1, b: 2} + }, + { + 'name':'streetLightID', + 'type': 'Text', + 'value': '${id}' + }, + { + 'name':'illuminanceLevel', + 'type': 'Number', + 'value': '${lastLightIllumNumber}' + }, + { + 'name':'illuminanceLevel2', + 'type': 'Number', + 'value': '${lastLightIllumStringNumber}' + }, + { + 'name':'illuminanceLevel3', + 'type': 'Number', + 'value': 12.5 + }, + { + 'name':'lastchange', + 'type': 'DateTime', + 'value': '${stringDate}' + }, + { + 'name':'lastchange2', + 'type': 'DateTime', + 'value': '${stringDateMs}' + }, + { + 'name':'lastchange3', + 'type': 'DateTime', + 'value': '${numberDateMs}' + }, + { + 'name':'isBool1', + 'type': 'Boolean', + 'value': '${isconnected}', + 'metadata': metaExample + }, + { + 'name':'isBool2', + 'type': 'Boolean', + 'value': 'TRUE' + }, + { + 'name':'isBool3', + 'type': 'Boolean', + 'value': 'true' + }, + { + 'name':'isBool4', + 'type': 'Boolean', + 'value': 'False' + }, + { + 'name':'isBool5', + 'type': 'Boolean', + 'value': 'other' + }, + { + 'name':'isBool6', + 'type': 'Boolean', + 'value': true + }, + { + 'name':'isBool7', + 'type': 'Boolean', + 'value': false + }, + { + 'name':'district', + 'type': 'Text', + 'value': '${areaServed}' + }, + { + 'name':'status', + 'type': 'Text', + 'value': '${laststatus}' + }, + { + 'name':'address', + 'type': 'Address', + 'value': '${streetAddress}, ${addressLocality}' + }, + { + 'name':'powerState', + 'type': 'Text', + 'value': '${powerState}' + }, + { + 'name':'refNone', + 'type': '${refNoneType}', + 'value': 'futureNull' + }, + { + 'name':'refNone2', + 'type': '${refNoneType}', + 'value': '123' + }, + { + 'name':'refNone3', + 'type': '${refNoneType}', + 'value': null + } + ] + } +}; +var event1 = { + 'ruleName': 'switch_on', + 'id': 'AmbientLightSensor:1', + 'type': 'AmbientLightSensor', + 'lastLightIllumNumber': 80, + 'lastLightIllumStringNumber': '69', + 'isconnected': true, + 'streetAddress': 'Vasagatan 1', + 'addressLocality': 'Stockholm', + 'laststatus': 'allright', + 'powerState':'on', + 'stringDate': '2018-12-05T11:31:39.00Z', + 'stringDateMs': '1548843060657', + 'numberDateMs': 1548843229832, + 'subservice': '/', + 'areaServed': 'Stockholm center', + 'service': 'dev_test', + 'refNoneType': 'None', + 'fiwarePerseoContext': { + 'path': '/actions/do', + 'op': '/actions/do', + 'comp': 'perseo-fe', + 'trans': 'f8636710-5fc6-4070-9b1e-8d414fc6522a', + 'corr': 'd5f0a9cc-0258-11e9-b678-0242ac160003; perseocep=15', + 'srv': 'dev_test', + 'subsrv': '/' + } +}; + +var expectedChanges = { + 'address': { + 'value': 'Vasagatan 1, Stockholm', + 'type': 'Address' + }, + 'status': { + 'value': 'allright', + 'type': 'Text' + }, + 'textBoolLit': { + 'value': 'false', + 'type': 'Text' + }, + 'textNumberLit': { + 'value': '666', + 'type': 'Text' + }, + 'textObjLit': { + 'value': '[object Object]', + 'type': 'Text' + }, + 'refNone': { + 'value': null, + 'type': 'None' + }, + 'refNone2': { + 'value': null, + 'type': 'None' + }, + 'refNone3': { + 'value': null, + 'type': 'None' + }, + 'isBool1': { + 'value': true, + 'type': 'Boolean', + 'metadata': metaExample + }, + 'isBool2': { + 'value': true, + 'type': 'Boolean' + }, + 'isBool3': { + 'value': true, + 'type': 'Boolean' + }, + 'isBool4': { + 'value': false, + 'type': 'Boolean' + }, + 'isBool5': { + 'value': false, + 'type': 'Boolean' + }, + 'isBool6': { + 'value': true, + 'type': 'Boolean' + }, + 'isBool7': { + 'value': false, + 'type': 'Boolean' + }, + 'powerState': { + 'value': 'on', + 'type': 'Text' + }, + 'illuminanceLevel': { + 'value': 80, + 'type': 'Number' + }, + 'illuminanceLevel2': { + 'value': 69, + 'type': 'Number' + }, + 'illuminanceLevel3': { + 'value': 12.5, + 'type': 'Number' + }, + 'streetLightID': { + 'value': 'AmbientLightSensor:1', + 'type': 'Text' + }, + 'district': { + 'value': 'Stockholm center', + 'type': 'Text' + }, + 'lastchange': { + 'value': '2018-12-05T11:31:39.000Z', + 'type': 'DateTime' + }, + 'lastchange2': { + 'value': '2019-01-30T10:11:00.657Z', + 'type': 'DateTime' + }, + 'lastchange3': { + 'value': '2019-01-30T10:13:49.832Z', + 'type': 'DateTime' + } +}; + +describe('doIt', function() { + + describe('#NGSIv2 updateActions', function() { + + beforeEach(function () { + }); + + it('should accept NGSIv2 entities', function (done) { + + // Mocks + var createEntityThen = sinon.spy(function (successCB, errorCB) { + setTimeout(function () { + successCB({'httpCode': '200', 'message': 'all right'}); // success callback + }, 0); + return '__TEST'; + }); + var createEntityMock = sinon.spy( + function (changes, options) { + return {'then': createEntityThen}; + } + ); + var NGSICloseMock = sinon.spy( + function () { + return 'closed'; + } + ); + var NGSIConnectionMock = sinon.spy( + function () { + return { + 'v2': {'createEntity': createEntityMock}, + 'close': NGSICloseMock + }; + } + ); + + updateAction.__with__({ + 'NGSI.Connection': NGSIConnectionMock + })(function () { + var callback = function (e, request) { + should.exist(request); + should.not.exist(e); + should.equal(request.httpCode, 200); + expectedChanges.id = 'AmbientLightSensor:1_NGSIv2Test'; + expectedChanges.type = 'NGSIv2TypesTest'; + createEntityMock.should.be.calledOnceWith(expectedChanges, {upsert: true}); + done(); + }; + action1.parameters.id = '${id}_NGSIv2Test'; + action1.parameters.type = 'NGSIv2TypesTest'; + updateAction.doIt(action1, event1, callback); + }); + }); + + it('should accept NGSIv2 entities without type and id', function (done) { + + // Mocks + var createEntityThen = sinon.spy(function (successCB, errorCB) { + setTimeout(function () { + successCB({'httpCode': '200', 'message': 'all right'}); // success callback + }, 0); + return '__TEST'; + }); + var createEntityMock = sinon.spy( + function (changes, options) { + return {'then': createEntityThen}; + } + ); + var NGSICloseMock = sinon.spy( + function () { + return 'closed'; + } + ); + var NGSIConnectionMock = sinon.spy( + function () { + return { + 'v2': {'createEntity': createEntityMock}, + 'close': NGSICloseMock + }; + } + ); + + updateAction.__with__({ + 'NGSI.Connection': NGSIConnectionMock + })(function () { + var callback = function (e, request) { + should.exist(request); + should.not.exist(e); + should.equal(request.httpCode, 200); + expectedChanges.id = 'AmbientLightSensor:1'; + expectedChanges.type = 'AmbientLightSensor'; + createEntityMock.should.be.calledOnceWith(expectedChanges, {upsert: true}); + done(); + }; + delete action1.parameters.id; + delete action1.parameters.type; + updateAction.doIt(action1, event1, callback); + }); + }); + + it('should control failed update actions', function (done) { + + // Mocks + var theCBError = new Error(); + var createEntityThen = sinon.spy(function (successCB, errorCB) { + setTimeout(function () { + errorCB(theCBError); // success callback + }, 0); + return '__TEST'; + }); + var createEntityMock = sinon.spy( + function (changes, options) { + return {'then': createEntityThen}; + } + ); + var NGSICloseMock = sinon.spy( + function () { + return 'closed'; + } + ); + var NGSIConnectionMock = sinon.spy( + function () { + return { + 'v2': {'createEntity': createEntityMock}, + 'close': NGSICloseMock + }; + } + ); + + updateAction.__with__({ + 'NGSI.Connection': NGSIConnectionMock + })(function () { + var callback = function (e, request) { + should.not.exist(request); + should.exist(e); + e.should.be.instanceof(Error); + expectedChanges.id = 'AmbientLightSensor:1_NGSIv2Test'; + expectedChanges.type = 'NGSIv2TypesTest'; + createEntityMock.should.be.calledOnceWith(expectedChanges, {upsert: true}); + done(); + }; + action1.parameters.id = '${id}_NGSIv2Test'; + action1.parameters.type = 'NGSIv2TypesTest'; + updateAction.doIt(action1, event1, callback); + }); + }); + + }); +}); \ No newline at end of file diff --git a/test/unit/visualrules_utest.js b/test/unit/visualrules_utest.js index 7cecf36b..d87c069c 100644 --- a/test/unit/visualrules_utest.js +++ b/test/unit/visualrules_utest.js @@ -34,7 +34,7 @@ describe('VisualRules', function() { var cases = utilsT.loadDirExamples('./test/data/unit/vr_epl'); cases.forEach(function(c) { var rule = visualRules.vr2rule(c.object.VR); - rule.text.should.be.a.String; + rule.text.should.be.a.string; rule.text = rule.text.replace(/\s{2,}/g, ' '); c.object.text = c.object.text.replace(/\s{2,}/g, ' '); should.equal(c.object.text, rule.text); diff --git a/test/utils/utilsT.js b/test/utils/utilsT.js index 12262f61..ee1604b4 100644 --- a/test/utils/utilsT.js +++ b/test/utils/utilsT.js @@ -30,7 +30,8 @@ var fs = require('fs'), fakeServerPort = 9753, fakeServerCode = 200, fakeServerMessage = 'All right', - fakeServerCallback; + fakeServerCallback, + URL = require('url').URL; function loadExample(fileName) { var f = fs.readFileSync(fileName); @@ -76,7 +77,7 @@ function dropExecutions(callback) { remove(config.collections.executions, callback); } function dropEntities(callback) { - MongoClient.connect(config.orion.url, function(err, db) { + MongoClient.connect(new URL('v1/updateContext', config.orion.URL), function(err, db) { if (err) { return callback(err); }