diff --git a/CHANGES_NEXT_RELEASE b/CHANGES_NEXT_RELEASE index d986e61a8..29fea3471 100644 --- a/CHANGES_NEXT_RELEASE +++ b/CHANGES_NEXT_RELEASE @@ -19,4 +19,5 @@ Add husky and lint-staged Fix: Add support for lazy and internal_attributes in service notifications to Manager (#768) Fix: combine multi-entity and expressions with duplicate attribute name, by enabling expression over object_id (which are not duplicated in a attribute mapping contrary to name) (#941) Fix: bug in legacy and JEXL expression that was not converting "0" to 0 -Fix: support for mapping different attributes to the same entity_name \ No newline at end of file +Fix: support for mapping different attributes to the same entity_name +Fix: Ensure GeoJSON is correctly encoded in NGSI-v2 requests (#961) diff --git a/doc/advanced-topics.md b/doc/advanced-topics.md index 04a18cdfa..64b6fda70 100644 --- a/doc/advanced-topics.md +++ b/doc/advanced-topics.md @@ -46,6 +46,64 @@ curl http://${KEYSTONE_HOST}/v3/OS-TRUST/trusts \ Apart from the generation of the trust, the use of secured Context Brokers should be transparent to the user of the IoT Agent. +### GeoJSON support + +The defined `type` of any GeoJSON attribute can be any set to any of the standard NGSI-v2 GeoJSON types - (e.g. +`geo:json`, `geo:point`). NGSI-LD formats such as `GeoProperty`, `Point` and `LineString` are also accepted `type` +values. If the latitude and longitude are received as separate measures, the +[expression language](expressionLanguage.md) can be used to concatenate them. + +````json +{ + "entity_type": "GPS", + "resource": "/iot/d", + "protocol": "PDI-IoTA-JSON", +..etc + "attributes": [ + { + "name": "location", + "type": "geo:json", + "expression": "${@lng}, ${@lat}" + } + ] +} +```json + + +For `attributes` and `static_attributes` which need to be formatted as GeoJSON values, three separate input +formats are accepted. Provided the `type` is provisioned correctly, the `value` may be defined using any of +the following formats: + +- a comma delimited string + +```json +{ + "name": "location", + "value": "23, 12.5" +} +```` + +- an array of numbers + +```json +{ + "name": "location", + "value": [23, 12.5] +} +``` + +- an fully formatted GeoJSON object + +```json +{ + "name": "location", + "value": { + "type": "Point", + "coordinates": [23, 12.5] + } +} +``` + ### Metadata support Both `attributes` and `static_attributes` may be supplied with metadata when provisioning an IoT Agent, so that the diff --git a/lib/services/devices/devices-NGSI-v2.js b/lib/services/devices/devices-NGSI-v2.js index e8c2459ae..2bdb12440 100644 --- a/lib/services/devices/devices-NGSI-v2.js +++ b/lib/services/devices/devices-NGSI-v2.js @@ -25,6 +25,8 @@ * Modified by: Jason Fox - FIWARE Foundation */ +/* eslint-disable consistent-return */ + const request = require('request'); const async = require('async'); const apply = async.apply; @@ -38,6 +40,7 @@ const config = require('../../commonConfig'); const registrationUtils = require('./registrationUtils'); const _ = require('underscore'); const utils = require('../northBound/restUtils'); +const NGSIv2 = require('../ngsi/entities-NGSI-v2'); const moment = require('moment'); const context = { op: 'IoTAgentNGSI.Devices-v2' @@ -232,6 +235,15 @@ function createInitialEntityNgsi2(deviceData, newDevice, callback) { jsonConcat(options.json, formatAttributesNgsi2(deviceData.staticAttributes, true)); jsonConcat(options.json, formatCommandsNgsi2(deviceData.commands)); + for (const att in options.json) { + try { + // Format any GeoJSON attrs properly + options.json[att] = NGSIv2.formatGeoAttrs(options.json[att]); + } catch (error) { + return callback(new errors.BadGeocoordinates(JSON.stringify(options.json))); + } + } + logger.debug(context, 'deviceData: %j', deviceData); if ( ('timestamp' in deviceData && deviceData.timestamp !== undefined @@ -281,6 +293,15 @@ function updateEntityNgsi2(deviceData, updatedDevice, callback) { jsonConcat(options.json, formatAttributesNgsi2(deviceData.staticAttributes, true)); jsonConcat(options.json, formatCommandsNgsi2(deviceData.commands)); + for (const att in options.json) { + try { + // Format any GeoJSON attrs properly + options.json[att] = NGSIv2.formatGeoAttrs(options.json[att]); + } catch (error) { + return callback(new errors.BadGeocoordinates(JSON.stringify(options.json))); + } + } + if ( ('timestamp' in deviceData && deviceData.timestamp !== undefined ? deviceData.timestamp diff --git a/lib/services/ngsi/entities-NGSI-LD.js b/lib/services/ngsi/entities-NGSI-LD.js index 0dfa471d9..a2b145888 100644 --- a/lib/services/ngsi/entities-NGSI-LD.js +++ b/lib/services/ngsi/entities-NGSI-LD.js @@ -56,45 +56,6 @@ function valueOfOrNull(value) { return isNaN(value) ? NGSI_LD_NULL : value; } -/** - * @param {String/Array} value Comma separated list or array of values - * @return {Array} Array of Lat/Lngs for use as GeoJSON - */ -function splitLngLat(value) { - const lngLats = typeof value === 'string' || value instanceof String ? value.split(',') : value; - lngLats.forEach((element, index, lngLats) => { - if (Array.isArray(element)) { - lngLats[index] = splitLngLat(element); - } else if ((typeof element === 'string' || element instanceof String) && element.includes(',')) { - lngLats[index] = splitLngLat(element); - } else { - lngLats[index] = Number.parseFloat(element); - } - }); - return lngLats; -} - -/** - * @param {String} value Value to be analyzed - * @return {Array} split pairs of GeoJSON coordinates - */ -function getLngLats(value) { - const lngLats = _.flatten(splitLngLat(value)); - if (lngLats.length === 2) { - return lngLats; - } - - if (lngLats.length % 2 !== 0) { - logger.error(context, 'Bad attribute value type. Expecting geo-coordinates. Received:%s', value); - throw Error(); - } - const arr = []; - for (let i = 0, len = lngLats.length; i < len; i = i + 2) { - arr.push([lngLats[i], lngLats[i + 1]]); - } - return arr; -} - /** * Amends an NGSIv2 attribute to NGSI-LD format * All native JSON types are respected and cast as Property values @@ -155,33 +116,34 @@ function convertNGSIv2ToLD(attr) { case 'geoproperty': case 'point': case 'geo:point': + case 'geo:json': obj.type = 'GeoProperty'; - obj.value = { type: 'Point', coordinates: getLngLats(attr.value) }; + obj.value = NGSIUtils.getLngLats('Point', attr.value); break; case 'linestring': case 'geo:linestring': obj.type = 'GeoProperty'; - obj.value = { type: 'LineString', coordinates: getLngLats(attr.value) }; + obj.value = NGSIUtils.getLngLats('LineString', attr.value); break; case 'polygon': case 'geo:polygon': obj.type = 'GeoProperty'; - obj.value = { type: 'Polygon', coordinates: getLngLats(attr.value) }; + obj.value = NGSIUtils.getLngLats('Polygon', attr.value); break; case 'multipoint': case 'geo:multipoint': obj.type = 'GeoProperty'; - obj.value = { type: 'MultiPoint', coordinates: getLngLats(attr.value) }; + obj.value = NGSIUtils.getLngLats('MultiPoint', attr.value);; break; case 'multilinestring': case 'geo:multilinestring': obj.type = 'GeoProperty'; - obj.value = { type: 'MultiLineString', coordinates: attr.value }; + obj.value = NGSIUtils.getLngLats( 'MultiLineString', attr.value); break; case 'multipolygon': case 'geo:multipolygon': obj.type = 'GeoProperty'; - obj.value = { type: 'MultiPolygon', coordinates: attr.value }; + obj.value = NGSIUtils.getLngLats( 'MultiPolygon', attr.value); break; // Relationships diff --git a/lib/services/ngsi/entities-NGSI-v2.js b/lib/services/ngsi/entities-NGSI-v2.js index c3e624882..7e200acda 100644 --- a/lib/services/ngsi/entities-NGSI-v2.js +++ b/lib/services/ngsi/entities-NGSI-v2.js @@ -25,6 +25,8 @@ * Modified by: Jason Fox - FIWARE Foundation */ +/* eslint-disable consistent-return */ + const request = require('request'); const statsService = require('./../stats/statsRegistry'); const async = require('async'); @@ -35,11 +37,59 @@ const utils = require('../northBound/restUtils'); const config = require('../../commonConfig'); const constants = require('../../constants'); const moment = require('moment-timezone'); +const NGSIUtils = require('./ngsiUtils'); const logger = require('logops'); const context = { op: 'IoTAgentNGSI.Entities-v2' }; -const NGSIUtils = require('./ngsiUtils'); + +/** + * Amends an NGSIv2 Geoattribute from String to GeoJSON format + * + * @param {Object} attr Attribute to be analyzed + * @return {Object} GeoJSON version of the attribute + */ +function formatGeoAttrs(attr) { + const obj = attr; + if (attr.type) { + switch (attr.type.toLowerCase()) { + // GeoProperties + case 'geo:json': + case 'geoproperty': + case 'point': + case 'geo:point': + obj.type = 'geo:json'; + obj.value = NGSIUtils.getLngLats('Point', attr.value); + break; + case 'linestring': + case 'geo:linestring': + obj.type = 'geo:json'; + obj.value = NGSIUtils.getLngLats('LineString', attr.value); + break; + case 'polygon': + case 'geo:polygon': + obj.type = 'geo:json'; + obj.value = NGSIUtils.getLngLats('Polygon', attr.value); + break; + case 'multipoint': + case 'geo:multipoint': + obj.type = 'geo:json'; + obj.value = NGSIUtils.getLngLats('MultiPoint', attr.value); + break; + case 'multilinestring': + case 'geo:multilinestring': + obj.type = 'geo:json'; + obj.value = NGSIUtils.getLngLats('MultiLineString', attr.value); + break; + case 'multipolygon': + case 'geo:multipolygon': + obj.type = 'geo:json'; + obj.value = NGSIUtils.getLngLats('MultiPolygon', attr.value); + break; + } + } + return obj; +} function addTimestampNgsi2(payload, timezone) { function addTimestampEntity(entity, timezone) { @@ -387,6 +437,13 @@ function sendUpdateValueNgsi2(entityName, attributes, typeInformation, token, ca if (options.json.entities[entity][att].name) { delete options.json.entities[entity][att].name; } + + try { + // Format any GeoJSON attrs properly + options.json.entities[entity][att] = formatGeoAttrs(options.json.entities[entity][att]); + } catch (error) { + return callback(new errors.BadGeocoordinates(JSON.stringify(options.json))); + } } } } else { @@ -402,6 +459,13 @@ function sendUpdateValueNgsi2(entityName, attributes, typeInformation, token, ca if (options.json[att].name) { delete options[att].name; } + + try { + // Format any GeoJSON attrs properly + options.json[att] = formatGeoAttrs(options.json[att]); + } catch (error) { + return callback(new errors.BadGeocoordinates(JSON.stringify(options.json))); + } } } @@ -424,3 +488,4 @@ function sendUpdateValueNgsi2(entityName, attributes, typeInformation, token, ca exports.sendQueryValue = sendQueryValueNgsi2; exports.sendUpdateValue = sendUpdateValueNgsi2; exports.addTimestamp = addTimestampNgsi2; +exports.formatGeoAttrs = formatGeoAttrs; diff --git a/lib/services/ngsi/ngsiUtils.js b/lib/services/ngsi/ngsiUtils.js index 8b33a2dd2..64a5a82df 100644 --- a/lib/services/ngsi/ngsiUtils.js +++ b/lib/services/ngsi/ngsiUtils.js @@ -34,6 +34,51 @@ const _ = require('underscore'); const config = require('../../commonConfig'); const updateMiddleware = []; const queryMiddleware = []; + +/** + * @param {String/Array} value Comma separated list or array of values + * @return {Array} Array of Lat/Lngs for use as GeoJSON + */ +function splitLngLat(value) { + const lngLats = typeof value === 'string' || value instanceof String ? value.split(',') : value; + lngLats.forEach((element, index, lngLats) => { + if (Array.isArray(element)) { + lngLats[index] = splitLngLat(element); + } else if ((typeof element === 'string' || element instanceof String) && element.includes(',')) { + lngLats[index] = splitLngLat(element); + } else { + lngLats[index] = Number.parseFloat(element); + } + }); + return lngLats; +} + +/** + * @param {String} type GeoJSON + * @param {String} value Value to be analyzed + * @return {Array} split pairs of GeoJSON coordinates + */ +function getLngLats(type, value) { + if (typeof value !== 'string' && Array.isArray(value) === false) { + return value; + } + + const lngLats = _.flatten(splitLngLat(value)); + if (lngLats.length === 2) { + return { type, coordinates: lngLats }; + } + + if (lngLats.length % 2 !== 0) { + logger.error(context, 'Bad attribute value type. Expecting geo-coordinates. Received:%s', value); + throw Error(); + } + const arr = []; + for (let i = 0, len = lngLats.length; i < len; i = i + 2) { + arr.push([lngLats[i], lngLats[i + 1]]); + } + return { type, coordinates: arr }; +} + /** * Determines if a value is of type float * @@ -212,6 +257,7 @@ exports.getErrorField = intoTrans(context, getErrorField); exports.createRequestObject = createRequestObject; exports.applyMiddlewares = applyMiddlewares; exports.getMetaData = getMetaData; +exports.getLngLats = getLngLats; exports.castJsonNativeAttributes = castJsonNativeAttributes; exports.isFloat = isFloat; exports.updateMiddleware = updateMiddleware; diff --git a/package.json b/package.json index d8c337824..45a84fb96 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,7 @@ "prettier": "prettier --config .prettierrc.json --write '**/**/**/**/*.js' '**/**/**/*.js' '**/**/*.js' '**/*.js' '*.js'", "prettier:text": "prettier 'README.md' 'doc/*.md' 'doc/**/*.md' --no-config --tab-width 4 --print-width 120 --write --prose-wrap always", "test": "nyc --reporter=text mocha --recursive 'test/**/*.js' --reporter spec --timeout 5000 --ui bdd --exit --color true", - "test:debug": "mocha --recursive 'test/**/*.js' --reporter spec --inspect-brk --timeout 30000 --ui bdd --exit" , + "test:debug": "mocha --recursive 'test/**/*.js' --reporter spec --inspect-brk --timeout 30000 --ui bdd --exit", "test:coverage": "nyc --reporter=lcov mocha -- --recursive 'test/**/*.js' --reporter spec --timeout 5000 --exit", "test:coveralls": "npm run test:coverage && cat ./coverage/lcov.info | coveralls && rm -rf ./coverage", "test:watch": "npm run test -- -w ./lib", diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin20.json b/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin20.json new file mode 100644 index 000000000..1a89d3696 --- /dev/null +++ b/test/unit/ngsi-ld/examples/contextRequests/updateContextExpressionPlugin20.json @@ -0,0 +1,25 @@ +[ + { + "@context": "http://context.json-ld", + "longitude": { + "type": "Property", + "value": 0.44 + }, + "latitude": { + "type": "Property", + "value": 10 + }, + "location": { + "type": "GeoProperty", + "value": { + "type": "Point", + "coordinates": [ + 0.44, + 10 + ] + } + }, + "id": "urn:ngsi-ld:WeatherStation:ws1", + "type": "WeatherStation" + } +] diff --git a/test/unit/ngsi-ld/examples/contextRequests/updateProvisionDeviceStatic.json b/test/unit/ngsi-ld/examples/contextRequests/updateProvisionDeviceStatic.json index c528e2883..bc19107b9 100644 --- a/test/unit/ngsi-ld/examples/contextRequests/updateProvisionDeviceStatic.json +++ b/test/unit/ngsi-ld/examples/contextRequests/updateProvisionDeviceStatic.json @@ -7,16 +7,13 @@ }, "id": "urn:ngsi-ld:MicroLights:SecondMicroLight", "location": { - "type": "Property", + "type": "GeoProperty", "value": { - "@type": "geo:json", - "@value": { - "coordinates": [ - -3.164485591715449, - 40.62785133667262 - ], - "type": "Point" - } + "coordinates": [ + -3.164485591715449, + 40.62785133667262 + ], + "type": "Point" } }, "newAttribute": { diff --git a/test/unit/ngsiv2/examples/contextRequests/createBidirectionalDevice.json b/test/unit/ngsiv2/examples/contextRequests/createBidirectionalDevice.json index 985375c55..46e2330e3 100644 --- a/test/unit/ngsiv2/examples/contextRequests/createBidirectionalDevice.json +++ b/test/unit/ngsiv2/examples/contextRequests/createBidirectionalDevice.json @@ -2,7 +2,13 @@ "id": "TheFirstLight", "type": "TheLightType", "location": { - "type": "geo:point", - "value": "0, 0" + "type": "geo:json", + "value": { + "coordinates": [ + 0, + 0 + ], + "type": "Point" + } } } \ No newline at end of file diff --git a/test/unit/ngsiv2/examples/contextRequests/createGeopointProvisionedDevice.json b/test/unit/ngsiv2/examples/contextRequests/createGeopointProvisionedDevice.json index 58e0ccd38..a8a690119 100644 --- a/test/unit/ngsiv2/examples/contextRequests/createGeopointProvisionedDevice.json +++ b/test/unit/ngsiv2/examples/contextRequests/createGeopointProvisionedDevice.json @@ -2,7 +2,13 @@ "id": "FirstMicroLight", "type": "MicroLights", "location": { - "type": "geo:point", - "value": "0, 0" + "type": "geo:json", + "value": { + "coordinates": [ + 0, + 0 + ], + "type": "Point" + } } } \ No newline at end of file diff --git a/test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin14.json b/test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin14.json new file mode 100644 index 000000000..23b277d8e --- /dev/null +++ b/test/unit/ngsiv2/examples/contextRequests/updateContextExpressionPlugin14.json @@ -0,0 +1,20 @@ +{ + "longitude": { + "type": "Number", + "value": 0.44 + }, + "latitude": { + "type": "Number", + "value": 10 + }, + "location": { + "type": "geo:json", + "value": { + "type": "Point", + "coordinates": [ + 0.44, + 10 + ] + } + } +} \ No newline at end of file diff --git a/test/unit/ngsiv2/examples/contextRequests/updateContextGeoproperties1.json b/test/unit/ngsiv2/examples/contextRequests/updateContextGeoproperties1.json new file mode 100644 index 000000000..a6616ec71 --- /dev/null +++ b/test/unit/ngsiv2/examples/contextRequests/updateContextGeoproperties1.json @@ -0,0 +1,13 @@ +{ + "location": { + "value": { + "type": "Point", + "coordinates": [ + 23, + 12.5 + ] + }, + "type": "geo:json" + } +} + diff --git a/test/unit/ngsiv2/examples/contextRequests/updateContextGeoproperties2.json b/test/unit/ngsiv2/examples/contextRequests/updateContextGeoproperties2.json new file mode 100644 index 000000000..11b461806 --- /dev/null +++ b/test/unit/ngsiv2/examples/contextRequests/updateContextGeoproperties2.json @@ -0,0 +1,18 @@ +{ + "location": { + "type": "geo:json", + "value": { + "coordinates": [ + [ + 23, + 12.5 + ], + [ + 22, + 12.5 + ] + ], + "type": "LineString" + } + } +} diff --git a/test/unit/ngsiv2/examples/contextRequests/updateContextGeoproperties3.json b/test/unit/ngsiv2/examples/contextRequests/updateContextGeoproperties3.json new file mode 100644 index 000000000..8702c84cf --- /dev/null +++ b/test/unit/ngsiv2/examples/contextRequests/updateContextGeoproperties3.json @@ -0,0 +1,6 @@ +{ + "location": { + "value": null, + "type": "None" + } +} \ No newline at end of file diff --git a/test/unit/ngsiv2/examples/contextRequests/updateContextGeoproperties4.json b/test/unit/ngsiv2/examples/contextRequests/updateContextGeoproperties4.json new file mode 100644 index 000000000..3066c60e2 --- /dev/null +++ b/test/unit/ngsiv2/examples/contextRequests/updateContextGeoproperties4.json @@ -0,0 +1,23 @@ +{ + "location": { + "value": { + "type": "Polygon", + "coordinates": [ + [ + 23, + 12.5 + ], + [ + 22, + 13.5 + ], + [ + 22, + 13.5 + ] + ] + }, + "type": "geo:json" + } +} + diff --git a/test/unit/ngsiv2/examples/contextRequests/updateContextGeoproperties5.json b/test/unit/ngsiv2/examples/contextRequests/updateContextGeoproperties5.json new file mode 100644 index 000000000..56b087376 --- /dev/null +++ b/test/unit/ngsiv2/examples/contextRequests/updateContextGeoproperties5.json @@ -0,0 +1,15 @@ +[ + { + "location": { + "type": "GeoProperty", + "value": { + "coordinates": [ + 23, + 12.5 + ], + "type": "Point" + } + }, + "type": "Light" + } +] diff --git a/test/unit/ngsiv2/examples/contextRequests/updateContextGeoproperties6.json b/test/unit/ngsiv2/examples/contextRequests/updateContextGeoproperties6.json new file mode 100644 index 000000000..56b087376 --- /dev/null +++ b/test/unit/ngsiv2/examples/contextRequests/updateContextGeoproperties6.json @@ -0,0 +1,15 @@ +[ + { + "location": { + "type": "GeoProperty", + "value": { + "coordinates": [ + 23, + 12.5 + ], + "type": "Point" + } + }, + "type": "Light" + } +] diff --git a/test/unit/ngsiv2/examples/contextRequests/updateContextGeoproperties7.json b/test/unit/ngsiv2/examples/contextRequests/updateContextGeoproperties7.json new file mode 100644 index 000000000..56b087376 --- /dev/null +++ b/test/unit/ngsiv2/examples/contextRequests/updateContextGeoproperties7.json @@ -0,0 +1,15 @@ +[ + { + "location": { + "type": "GeoProperty", + "value": { + "coordinates": [ + 23, + 12.5 + ], + "type": "Point" + } + }, + "type": "Light" + } +] diff --git a/test/unit/ngsiv2/examples/contextRequests/updateContextGeoproperties8.json b/test/unit/ngsiv2/examples/contextRequests/updateContextGeoproperties8.json new file mode 100644 index 000000000..56b087376 --- /dev/null +++ b/test/unit/ngsiv2/examples/contextRequests/updateContextGeoproperties8.json @@ -0,0 +1,15 @@ +[ + { + "location": { + "type": "GeoProperty", + "value": { + "coordinates": [ + 23, + 12.5 + ], + "type": "Point" + } + }, + "type": "Light" + } +] diff --git a/test/unit/ngsiv2/examples/contextRequests/updateContextGeoproperties9.json b/test/unit/ngsiv2/examples/contextRequests/updateContextGeoproperties9.json new file mode 100644 index 000000000..56b087376 --- /dev/null +++ b/test/unit/ngsiv2/examples/contextRequests/updateContextGeoproperties9.json @@ -0,0 +1,15 @@ +[ + { + "location": { + "type": "GeoProperty", + "value": { + "coordinates": [ + 23, + 12.5 + ], + "type": "Point" + } + }, + "type": "Light" + } +] diff --git a/test/unit/ngsiv2/examples/contextRequests/updateContextStaticAttributes.json b/test/unit/ngsiv2/examples/contextRequests/updateContextStaticAttributes.json index 6eb35074c..296300997 100644 --- a/test/unit/ngsiv2/examples/contextRequests/updateContextStaticAttributes.json +++ b/test/unit/ngsiv2/examples/contextRequests/updateContextStaticAttributes.json @@ -4,7 +4,10 @@ "type": "boolean" }, "location":{ - "value": "153,523", - "type": "geo:point" + "value":{ + "type":"Point", + "coordinates":[153,523] + }, + "type": "geo:json" } } \ No newline at end of file diff --git a/test/unit/ngsiv2/ngsiService/geoproperties-test.js b/test/unit/ngsiv2/ngsiService/geoproperties-test.js new file mode 100644 index 000000000..7addad551 --- /dev/null +++ b/test/unit/ngsiv2/ngsiService/geoproperties-test.js @@ -0,0 +1,425 @@ +/* + * Copyright 2014 Telefonica Investigación y Desarrollo, S.A.U + * + * This file is part of fiware-iotagent-lib + * + * fiware-iotagent-lib is free software: you can redistribute it and/or + * modify it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the License, + * or (at your option) any later version. + * + * fiware-iotagent-lib is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. + * See the GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public + * License along with fiware-iotagent-lib. + * If not, see http://www.gnu.org/licenses/. + * + * For those usages not covered by the GNU Affero General Public License + * please contact with::[contacto@tid.es] + * + * Modified by: Daniel Calvo - ATOS Research & Innovation + */ + +const iotAgentLib = require('../../../../lib/fiware-iotagent-lib'); +const utils = require('../../../tools/utils'); +const should = require('should'); +const logger = require('logops'); +const nock = require('nock'); +let contextBrokerMock; +const iotAgentConfig = { + autocast: true, + contextBroker: { + host: '192.168.1.1', + port: '1026', + ngsiVersion: 'v2' + }, + server: { + port: 4041 + }, + types: { + Light: { + commands: [], + type: 'Light', + active: [ + { + name: 'location', + type: 'geo:json' + } + ] + } + }, + service: 'smartGondor', + subservice: 'gardens', + providerUrl: 'http://smartGondor.com' +}; + +describe('NGSI-v2 - Geo-JSON types autocast test', function () { + beforeEach(function () { + logger.setLevel('FATAL'); + }); + + afterEach(function (done) { + iotAgentLib.deactivate(done); + }); + + describe( + 'When the IoT Agent receives new geo-information from a device.' + + 'Location with geo:json type and String value', + function () { + const values = [ + { + name: 'location', + type: 'geo:json', + value: '23,12.5' + } + ]; + + beforeEach(function (done) { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .post( + '/v2/entities/light1/attrs', + utils.readExampleFile( + './test/unit/ngsiv2/examples/contextRequests/updateContextGeoproperties1.json' + ) + ) + .query({ type: 'Light' }) + .reply(204); + + iotAgentLib.activate(iotAgentConfig, done); + }); + + it('should change the value of the corresponding attribute in the context broker', function (done) { + iotAgentLib.update('light1', 'Light', '', values, function (error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + } + ); + + describe( + 'When the IoT Agent receives new geo-information from a device.' + + 'Location with geo:json type and GeoJSON object value', + function () { + const values = [ + { + name: 'location', + type: 'geo:json', + value: { + type: 'Point', + coordinates: [23, 12.5] + } + } + ]; + + beforeEach(function (done) { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .post( + '/v2/entities/light1/attrs', + utils.readExampleFile( + './test/unit/ngsiv2/examples/contextRequests/updateContextGeoproperties1.json' + ) + ) + .query({ type: 'Light' }) + .reply(204); + + iotAgentLib.activate(iotAgentConfig, done); + }); + + it('should change the value of the corresponding attribute in the context broker', function (done) { + iotAgentLib.update('light1', 'Light', '', values, function (error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + } + ); + + describe('When the IoT Agent receives new geo-information from a device. Location with Point type and Array value', function () { + const values = [ + { + name: 'location', + type: 'Point', + value: [23, 12.5] + } + ]; + + beforeEach(function (done) { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .post( + '/v2/entities/light1/attrs', + utils.readExampleFile( + './test/unit/ngsiv2/examples/contextRequests/updateContextGeoproperties1.json' + ) + ) + .query({ type: 'Light' }) + .reply(204); + + iotAgentLib.activate(iotAgentConfig, done); + }); + + it('should change the value of the corresponding attribute in the context broker', function (done) { + iotAgentLib.update('light1', 'Light', '', values, function (error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe( + 'When the IoT Agent receives new geo-information from a device.' + + 'Location with LineString type and Array value', + function () { + const values = [ + { + name: 'location', + type: 'LineString', + value: [ + [23, 12.5], + [22, 12.5] + ] + } + ]; + + beforeEach(function (done) { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .post( + '/v2/entities/light1/attrs', + utils.readExampleFile( + './test/unit/ngsiv2/examples/contextRequests/updateContextGeoproperties2.json' + ) + ) + .query({ type: 'Light' }) + .reply(204); + + iotAgentLib.activate(iotAgentConfig, done); + }); + + it('should change the value of the corresponding attribute in the context broker', function (done) { + iotAgentLib.update('light1', 'Light', '', values, function (error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + } + ); + + describe( + 'When the IoT Agent receives new geo-information from a device.' + + 'Location with LineString type and Array of Strings', + function () { + const values = [ + { + name: 'location', + type: 'LineString', + value: ['23,12.5', '22,12.5'] + } + ]; + + beforeEach(function (done) { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .post( + '/v2/entities/light1/attrs', + utils.readExampleFile( + './test/unit/ngsiv2/examples/contextRequests/updateContextGeoproperties2.json' + ) + ) + .query({ type: 'Light' }) + .reply(204); + + iotAgentLib.activate(iotAgentConfig, done); + }); + + it('should change the value of the corresponding attribute in the context broker', function (done) { + iotAgentLib.update('light1', 'Light', '', values, function (error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + } + ); + + describe('When the IoT Agent receives new geo-information from a device. Location with None type', function () { + const values = [ + { + name: 'location', + type: 'None', + value: 'null' + } + ]; + + beforeEach(function (done) { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .post( + '/v2/entities/light1/attrs', + utils.readExampleFile( + './test/unit/ngsiv2/examples/contextRequests/updateContextGeoproperties3.json' + ) + ) + .query({ type: 'Light' }) + .reply(204); + + iotAgentLib.activate(iotAgentConfig, done); + }); + + it('should change the value of the corresponding attribute in the context broker', function (done) { + iotAgentLib.update('light1', 'Light', '', values, function (error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + }); + + describe( + 'When the IoT Agent receives new geo-information from a device.' + + 'Location with Polygon type - Array of coordinates', + function () { + const values = [ + { + name: 'location', + type: 'Polygon', + value: [ + [23, 12.5], + [22, 13.5], + [22, 13.5] + ] + } + ]; + + beforeEach(function (done) { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .post( + '/v2/entities/light1/attrs', + utils.readExampleFile( + './test/unit/ngsiv2/examples/contextRequests/updateContextGeoproperties4.json' + ) + ) + .query({ type: 'Light' }) + .reply(204); + + iotAgentLib.activate(iotAgentConfig, done); + }); + + it('should change the value of the corresponding attribute in the context broker', function (done) { + iotAgentLib.update('light1', 'Light', '', values, function (error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + } + ); + + describe( + 'When the IoT Agent receives new geo-information from a device.' + + 'Location with Polygon type - list of coordinates', + function () { + const values = [ + { + name: 'location', + type: 'Polygon', + value: '23,12.5,22,13.5,22,13.5' + } + ]; + + beforeEach(function (done) { + nock.cleanAll(); + + contextBrokerMock = nock('http://192.168.1.1:1026') + .matchHeader('fiware-service', 'smartGondor') + .post( + '/v2/entities/light1/attrs', + utils.readExampleFile( + './test/unit/ngsiv2/examples/contextRequests/updateContextGeoproperties4.json' + ) + ) + .query({ type: 'Light' }) + .reply(204); + + iotAgentLib.activate(iotAgentConfig, done); + }); + + it('should change the value of the corresponding attribute in the context broker', function (done) { + iotAgentLib.update('light1', 'Light', '', values, function (error) { + should.not.exist(error); + contextBrokerMock.done(); + done(); + }); + }); + } + ); + + describe('When the IoT Agent receives new geo-information from a device. Location with a missing latitude', function () { + const values = [ + { + name: 'location', + type: 'Point', + value: '23,12.5,22,13.5,22' + } + ]; + + beforeEach(function (done) { + nock.cleanAll(); + iotAgentLib.activate(iotAgentConfig, done); + }); + + it('should throw a BadGeocoordinates Error', function (done) { + iotAgentLib.update('light1', 'Light', '', values, function (error) { + should.exist(error); + done(); + }); + }); + }); + + describe('When the IoT Agent receives new geo-information from a device. Location invalid coordinates', function () { + const values = [ + { + name: 'location', + type: 'Point', + value: '2016-04-30Z' + } + ]; + + beforeEach(function (done) { + nock.cleanAll(); + iotAgentLib.activate(iotAgentConfig, done); + }); + + it('should throw a BadGeocoordinates Error', function (done) { + iotAgentLib.update('light1', 'Light', '', values, function (error) { + should.exist(error); + done(); + }); + }); + }); +});