iotagent-node-lib 4.7.0 → 4.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -26,63 +26,210 @@
26
26
  JEXL avaliable transformations*/
27
27
 
28
28
  const map = {
29
- jsonparse: (str) => JSON.parse(str),
30
- jsonstringify: (obj) => JSON.stringify(obj),
29
+ jsonparse: (val) => {
30
+ const safeOperation =
31
+ (fn) =>
32
+ (...args) => {
33
+ try {
34
+ return fn(...args);
35
+ } catch (e) {
36
+ return null;
37
+ }
38
+ };
39
+ return safeOperation(JSON.parse)(val);
40
+ },
41
+ jsonstringify: (val) => {
42
+ const safeOperation =
43
+ (fn) =>
44
+ (...args) => {
45
+ try {
46
+ return fn(...args);
47
+ } catch (e) {
48
+ return null;
49
+ }
50
+ };
51
+ return safeOperation(JSON.stringify)(val);
52
+ },
31
53
  indexOf: (val, char) => String(val).indexOf(char),
32
54
  length: (val) => String(val).length,
33
55
  trim: (val) => String(val).trim(),
34
56
  substr: (val, int1, int2) => String(val).substr(int1, int2),
35
- addreduce: (arr) => arr.reduce((i, v) => i + v),
57
+ addreduce: (arr) => {
58
+ const safeOperation =
59
+ (fn) =>
60
+ (...args) => {
61
+ try {
62
+ return fn(...args);
63
+ } catch (e) {
64
+ return null;
65
+ }
66
+ };
67
+ return safeOperation((arr) => arr.reduce((i, v) => i + v))(arr);
68
+ },
36
69
  lengtharray: (arr) => arr.length,
37
70
  typeof: (val) => typeof val,
38
- isarray: (arr) => Array.isArray(arr),
39
- isnan: (val) => isNaN(val),
40
- parseint: (val) => parseInt(val),
41
- parsefloat: (val) => parseFloat(val),
42
- toisodate: (val) => new Date(val).toISOString(),
43
- timeoffset: (isostr) => new Date(isostr).getTimezoneOffset(),
44
- tostring: (val) => val.toString(),
45
- urlencode: (val) => encodeURI(val),
46
- urldecode: (val) => decodeURI(val),
71
+ isarray: Array.isArray,
72
+ isnan: isNaN,
73
+ parseint: (val) => {
74
+ const safeParseNumber = (fn) => (val) => {
75
+ const result = fn(val);
76
+ return isNaN(result) ? null : result;
77
+ };
78
+ return safeParseNumber((val) => parseInt(val, 10))(val);
79
+ },
80
+ parsefloat: (val) => {
81
+ const safeParseNumber = (fn) => (val) => {
82
+ const result = fn(val);
83
+ return isNaN(result) ? null : result;
84
+ };
85
+ return safeParseNumber(parseFloat)(val);
86
+ },
87
+ toisodate: (val) => {
88
+ const safeDateOperation = (fn) => (val) => {
89
+ const date = new Date(val);
90
+ return isNaN(date.getTime()) ? null : fn(date);
91
+ };
92
+ return safeDateOperation((date) => date.toISOString())(val);
93
+ },
94
+ timeoffset: (val) => {
95
+ const safeDateOperation = (fn) => (val) => {
96
+ const date = new Date(val);
97
+ return isNaN(date.getTime()) ? null : fn(date);
98
+ };
99
+ return safeDateOperation((date) => date.getTimezoneOffset())(val);
100
+ },
101
+ tostring: (val) => {
102
+ const safeOperation =
103
+ (fn) =>
104
+ (...args) => {
105
+ try {
106
+ return fn(...args);
107
+ } catch (e) {
108
+ return null;
109
+ }
110
+ };
111
+ return safeOperation((val) => val.toString())(val);
112
+ },
113
+ urlencode: encodeURI,
114
+ urldecode: (val) => {
115
+ const safeOperation =
116
+ (fn) =>
117
+ (...args) => {
118
+ try {
119
+ return fn(...args);
120
+ } catch (e) {
121
+ return null;
122
+ }
123
+ };
124
+ return safeOperation(decodeURI)(val);
125
+ },
47
126
  replacestr: (str, from, to) => str.replace(from, to),
48
- replaceregexp: (str, reg, to) => str.replace(new RegExp(reg), to),
49
- replaceallstr: (str, from, to) => str.replaceAll(from, to),
50
- replaceallregexp: (str, reg, to) => str.replaceAll(new RegExp(reg, 'g'), to),
127
+ replaceregexp: (str, reg, to) => {
128
+ const safeOperation =
129
+ (fn) =>
130
+ (...args) => {
131
+ try {
132
+ return fn(...args);
133
+ } catch (e) {
134
+ return null;
135
+ }
136
+ };
137
+ return safeOperation((str, reg, to) => str.replace(new RegExp(reg), to))(str, reg, to);
138
+ },
139
+ replaceallregexp: (str, reg, to) => {
140
+ const safeOperation =
141
+ (fn) =>
142
+ (...args) => {
143
+ try {
144
+ return fn(...args);
145
+ } catch (e) {
146
+ return null;
147
+ }
148
+ };
149
+ return safeOperation((str, reg, to) => str.replace(new RegExp(reg, 'g'), to))(str, reg, to);
150
+ },
51
151
  split: (str, ch) => str.split(ch),
52
152
  joinarrtostr: (arr, ch) => arr.join(ch),
53
153
  concatarr: (arr, arr2) => arr.concat(arr2),
54
154
  mapper: (val, values, choices) => choices[values.findIndex((target) => target === val)],
55
155
  thmapper: (val, values, choices) =>
56
- choices[
57
- values.reduce((acc, curr, i) => (acc === 0 || acc ? acc : val <= curr ? (acc = i) : (acc = null)), null)
58
- ],
156
+ choices[values.reduce((acc, curr, i) => (acc !== null ? acc : val <= curr ? i : null), null)],
59
157
  bitwisemask: (i, mask, op, shf) =>
60
158
  (op === '&' ? parseInt(i) & mask : op === '|' ? parseInt(i) | mask : op === '^' ? parseInt(i) ^ mask : i) >>
61
159
  shf,
62
160
  slice: (arr, init, end) => arr.slice(init, end),
63
- addset: (arr, x) => {
64
- return Array.from(new Set(arr).add(x));
65
- },
161
+ addset: (arr, x) => Array.from(new Set(arr).add(x)),
66
162
  removeset: (arr, x) => {
67
- let s = new Set(arr);
163
+ const s = new Set(arr);
68
164
  s.delete(x);
69
165
  return Array.from(s);
70
166
  },
71
167
  touppercase: (val) => String(val).toUpperCase(),
72
168
  tolowercase: (val) => String(val).toLowerCase(),
73
- floor: (val) => Math.floor(val),
74
- ceil: (val) => Math.ceil(val),
75
- round: (val) => Math.round(val),
76
- tofixed: (val, decimals) => Number.parseFloat(val).toFixed(decimals),
77
- gettime: (d) => new Date(d).getTime(),
78
- toisostring: (d) => new Date(d).toISOString(),
79
- // https://developer.mozilla.org/es/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleString
80
- localestring: (d, timezone, options) => new Date(d).toLocaleString(timezone, options),
81
- now: () => Date.now(),
82
- hextostring: (val) =>
83
- new TextDecoder().decode(new Uint8Array(val.match(/.{1,2}/g).map((byte) => parseInt(byte, 16)))),
84
- valuePicker: (val,pick) => Object.entries(val).filter(([, v]) => v === pick).map(([k,]) => k),
85
- valuePickerMulti: (val,pick) => Object.entries(val).filter(([, v]) => pick.includes(v)).map(([k,]) => k)
169
+ floor: Math.floor,
170
+ ceil: Math.ceil,
171
+ round: Math.round,
172
+ tofixed: (val, decimals) => {
173
+ const num = Number.parseFloat(val);
174
+ const dec = Number.parseInt(decimals);
175
+ return isNaN(num) || isNaN(dec) ? null : num.toFixed(dec);
176
+ },
177
+ gettime: (val) => {
178
+ const safeDateOperation = (fn) => (val) => {
179
+ const date = new Date(val);
180
+ return isNaN(date.getTime()) ? null : fn(date);
181
+ };
182
+ return safeDateOperation((date) => date.getTime())(val);
183
+ },
184
+ toisostring: (val) => {
185
+ const safeDateOperation = (fn) => (val) => {
186
+ const date = new Date(val);
187
+ return isNaN(date.getTime()) ? null : fn(date);
188
+ };
189
+ return safeDateOperation((date) => date.toISOString())(val);
190
+ },
191
+ localestring: (d, timezone, options) => {
192
+ const safeOperation =
193
+ (fn) =>
194
+ (...args) => {
195
+ try {
196
+ return fn(...args);
197
+ } catch (e) {
198
+ return null;
199
+ }
200
+ };
201
+ return safeOperation((d, timezone, options) => new Date(d).toLocaleString(timezone, options))(
202
+ d,
203
+ timezone,
204
+ options
205
+ );
206
+ },
207
+ now: Date.now,
208
+ hextostring: (val) => {
209
+ const safeOperation =
210
+ (fn) =>
211
+ (...args) => {
212
+ try {
213
+ return fn(...args);
214
+ } catch (e) {
215
+ return null;
216
+ }
217
+ };
218
+ return safeOperation((val) => {
219
+ if (typeof val !== 'string' || !/^[0-9a-fA-F]+$/.test(val) || val.length % 2 !== 0) {
220
+ return null;
221
+ }
222
+ return new TextDecoder().decode(new Uint8Array(val.match(/.{1,2}/g).map((byte) => parseInt(byte, 16))));
223
+ })(val);
224
+ },
225
+ valuePicker: (val, pick) =>
226
+ Object.entries(val)
227
+ .filter(([, v]) => v === pick)
228
+ .map(([k]) => k),
229
+ valuePickerMulti: (val, pick) =>
230
+ Object.entries(val)
231
+ .filter(([, v]) => pick.includes(v))
232
+ .map(([k]) => k)
86
233
  };
87
234
 
88
235
  exports.map = map;
@@ -31,6 +31,15 @@ const Command = new Schema({
31
31
  value: Object,
32
32
  service: { type: String, lowercase: true },
33
33
  subservice: String,
34
+ execTs: { type: Date },
35
+ status: String,
36
+ info: String,
37
+ onDelivered: Object,
38
+ onOk: Object,
39
+ onError: Object,
40
+ onInfo: Object,
41
+ cmdExecution: Boolean,
42
+ dateExpiration: { type: Date },
34
43
  creationDate: { type: Date, default: Date.now }
35
44
  });
36
45
 
@@ -61,6 +61,7 @@ const Device = new Schema({
61
61
  });
62
62
 
63
63
  function load() {
64
+ Device.index({ service: 1, subservice: 1, apikey: 1, id: 1 }, { unique: true });
64
65
  module.exports.model = mongoose.model('Device', Device);
65
66
  module.exports.internalSchema = Device;
66
67
  }
@@ -89,7 +89,21 @@ function updateCommand(service, subservice, deviceId, command, callback) {
89
89
  function createCommand(service, subservice, deviceId, command, callback) {
90
90
  /* eslint-disable-next-line new-cap */
91
91
  const commandObj = new Command.model();
92
- const attributeList = ['name', 'type', 'value'];
92
+ const attributeList = [
93
+ 'name',
94
+ 'type',
95
+ 'value',
96
+ // new Command fields
97
+ 'execTs',
98
+ 'status',
99
+ 'info',
100
+ 'onDelivered',
101
+ 'onOk',
102
+ 'onError',
103
+ 'onInfo',
104
+ 'cmdExecution',
105
+ 'dateExpiration'
106
+ ];
93
107
 
94
108
  for (let i = 0; i < attributeList.length; i++) {
95
109
  commandObj[attributeList[i]] = command[attributeList[i]];
@@ -43,7 +43,7 @@ function listCommands(service, subservice, deviceId, callback) {
43
43
  }
44
44
 
45
45
  function addCommand(service, subservice, deviceId, command, callback) {
46
- logger.debug(context, 'Adding command [%j] to the queue for device [%s]', command, deviceId);
46
+ logger.debug(context, 'Adding command [%j] to the queue for deviceId [%s]', command, deviceId);
47
47
  config.getCommandRegistry().add(service, subservice, deviceId, command, callback);
48
48
  }
49
49
 
@@ -73,13 +73,13 @@ function addCommandDevice(service, subservice, device, command, callback) {
73
73
  }
74
74
 
75
75
  function updateCommand(service, subservice, deviceId, name, value, callback) {
76
- logger.debug(context, 'Updating command [%s] for device [%s] with value [%s]', name, deviceId, value);
76
+ logger.debug(context, 'Updating command [%s] for deviceId [%s] with value [%s]', name, deviceId, value);
77
77
 
78
78
  config.getCommandRegistry().update(service, subservice, deviceId, value, callback);
79
79
  }
80
80
 
81
81
  function removeCommand(service, subservice, deviceId, name, callback) {
82
- logger.debug(context, 'Removing command [%s] from device [%s]', name, deviceId);
82
+ logger.debug(context, 'Removing command [%s] from deviceId [%s]', name, deviceId);
83
83
 
84
84
  config.getCommandRegistry().remove(service, subservice, deviceId, name, callback);
85
85
  }
@@ -90,7 +90,7 @@ function storeDevice(newDevice, callback) {
90
90
  if (error.code === 11000) {
91
91
  logger.debug(context, 'Tried to insert a device with duplicate ID in the database: %s', error);
92
92
 
93
- callback(new errors.DuplicateDeviceId(newDevice));
93
+ callback(new errors.DuplicateDeviceId(newDevice), newDevice);
94
94
  } else {
95
95
  logger.debug(context, 'Error storing device information: %s', error);
96
96
 
@@ -40,6 +40,8 @@ const registrationUtils = require('./registrationUtils');
40
40
  const subscriptions = require('../ngsi/subscriptionService');
41
41
  const expressionPlugin = require('./../../plugins/expressionPlugin');
42
42
  const pluginUtils = require('./../../plugins/pluginUtils');
43
+ const alarms = require('../common/alarmManagement');
44
+ const constants = require('../../constants');
43
45
  const _ = require('underscore');
44
46
  const context = {
45
47
  op: 'IoTAgentNGSI.DeviceService'
@@ -250,7 +252,7 @@ function registerDevice(deviceObj, callback) {
250
252
  /* eslint-disable-next-line no-unused-vars */
251
253
  function (error, device) {
252
254
  if (!error) {
253
- innerCb(new errors.DuplicateDeviceId(deviceObj));
255
+ innerCb(new errors.DuplicateDeviceId(deviceObj), device);
254
256
  } else {
255
257
  innerCb();
256
258
  }
@@ -664,7 +666,14 @@ function findOrCreate(deviceId, apikey, group, callback) {
664
666
  if (group.autoprovision === undefined || group.autoprovision === true) {
665
667
  logger.debug(context, 'Registering autoprovision of Device %j for its conf %j', newDevice, group);
666
668
  registerDevice(newDevice, function (error, device) {
667
- callback(error, device, group);
669
+ if (error && error.name === 'DUPLICATE_DEVICE_ID') {
670
+ alarms.release(constants.MONGO_ALARM, error);
671
+ logger.warn(context, 'Error %j already registered autoprovisioned device: %j ', error, device);
672
+ callback(null, device, group);
673
+ } else {
674
+ logger.debug(context, 'registered autoprovisioned device: %j ', device);
675
+ callback(error, device, group);
676
+ }
668
677
  });
669
678
  } else {
670
679
  logger.info(
@@ -292,9 +292,18 @@ function handleNotificationNgsi2(req, res, next) {
292
292
  }
293
293
  }
294
294
  }
295
+ logger.debug(context, 'extracted atts %j from dataElement %j', atts, dataElement);
296
+ var id = null;
297
+ var type = null;
298
+ if (dataElement.targetEntityId && dataElement.targetEntityId.value) {
299
+ id = dataElement.targetEntityId.value;
300
+ }
301
+ if (dataElement.targetEntityType && dataElement.targetEntityType.value) {
302
+ type = dataElement.targetEntityType.value;
303
+ }
295
304
  deviceService.getDeviceByNameAndType(
296
- dataElement.id,
297
- dataElement.type,
305
+ id,
306
+ type,
298
307
  req.headers['fiware-service'],
299
308
  req.headers['fiware-servicepath'],
300
309
  function (error, device) {
@@ -336,7 +345,7 @@ function handleNotificationNgsi2(req, res, next) {
336
345
  logger.error(context, 'Error found when processing notification: %j', error);
337
346
  next(error);
338
347
  } else {
339
- res.status(200).json({});
348
+ res.status(204).json();
340
349
  }
341
350
  }
342
351
 
@@ -30,7 +30,7 @@ const context = {
30
30
  op: 'IoTAgentNGSI.ContextServer'
31
31
  };
32
32
  const contextServerUtils = require('./contextServerUtils');
33
-
33
+ const executeUpdateSideEffects = contextServerUtils.executeUpdateSideEffects;
34
34
  let contextServerHandler;
35
35
 
36
36
  /**
@@ -157,4 +157,5 @@ exports.setCommandHandler = intoTrans(context, setCommandHandler);
157
157
  exports.setNotificationHandler = intoTrans(context, setNotificationHandler);
158
158
  exports.addNotificationMiddleware = intoTrans(context, addNotificationMiddleware);
159
159
  exports.setQueryHandler = intoTrans(context, setQueryHandler);
160
+ exports.executeUpdateSideEffects = intoTrans(context, executeUpdateSideEffects);
160
161
  exports.init = init;
@@ -125,6 +125,7 @@ exports.setRemoveDeviceHandler = intoTrans(context, deviceProvisioning.setRemove
125
125
  exports.addDeviceProvisionMiddleware = deviceProvisioning.addDeviceProvisionMiddleware;
126
126
  exports.addConfigurationProvisionMiddleware = groupProvisioning.addConfigurationProvisionMiddleware;
127
127
  exports.addNotificationMiddleware = contextServer.addNotificationMiddleware;
128
+ exports.executeUpdateSideEffects = contextServer.executeUpdateSideEffects;
128
129
  exports.clear = clear;
129
130
  exports.start = intoTrans(context, start);
130
131
  exports.stop = intoTrans(context, stop);
@@ -167,6 +167,10 @@
167
167
  "description": "Content type",
168
168
  "type": "string"
169
169
  },
170
+ "headers": {
171
+ "description": "Optional headers to include with command",
172
+ "type": "object"
173
+ },
170
174
  "mqtt": {
171
175
  "description": "Mqtt properties",
172
176
  "type": "object",
@@ -145,6 +145,10 @@
145
145
  "description": "Optional expression for command transformation",
146
146
  "type": "string"
147
147
  },
148
+ "headers": {
149
+ "description": "Optional headers to include with command",
150
+ "type": "object"
151
+ },
148
152
  "payloadType": {
149
153
  "description": "Payload type",
150
154
  "type": "string"
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "iotagent-node-lib",
3
3
  "license": "AGPL-3.0-only",
4
4
  "description": "IoT Agent library to interface with NGSI Context Broker",
5
- "version": "4.7.0",
5
+ "version": "4.8.0",
6
6
  "homepage": "https://github.com/telefonicaid/iotagent-node-lib",
7
7
  "keywords": [
8
8
  "fiware",
@@ -74,16 +74,27 @@ function sendMeasureIotaLib(measure, provision) {
74
74
  * This is not a problem for the tests using other transports than Lib, in that case, the type will be retrieved
75
75
  * from the real provision.
76
76
  */
77
+ let typeInformation = {
78
+ service: provision.headers['fiware-service'],
79
+ subservice: provision.headers['fiware-servicepath']
80
+ };
77
81
  let type;
82
+ let staticAttrs;
78
83
  if (Array.isArray(provision.json.services) && provision.json.services.length > 0) {
79
84
  type = provision.json.services[0].entity_type;
85
+ staticAttrs = provision.json.services[0].static_attributes;
80
86
  } else {
81
87
  type = DEF_TYPE;
82
88
  }
89
+ typeInformation.type = type;
90
+ if (staticAttrs) {
91
+ typeInformation.staticAttributes = staticAttrs;
92
+ }
93
+ typeInformation.id = measure.qs.i;
83
94
  iotAgentLib.update(
84
95
  type + ':' + measure.qs.i,
85
96
  type,
86
- '',
97
+ typeInformation,
87
98
  jsonToIotaMeasures(measure.json),
88
99
  function (error, result, body) {
89
100
  error ? reject(error) : resolve(result);
@@ -230,7 +241,7 @@ async function testCase(measure, expectation, provision, env, config, type, tran
230
241
  if (transport === 'MQTT') {
231
242
  try {
232
243
  let client = await MQTT.connectAsync('mqtt://' + config.mqtt.host);
233
- await client.publish('/' + measure.qs.k + '/' + measure.qs.i + '/attrs', JSON.stringify(measure.json));
244
+ await client.publish('/json/' + measure.qs.k + '/' + measure.qs.i + '/attrs', JSON.stringify(measure.json));
234
245
  await client.end();
235
246
  } catch (error) {
236
247
  expect.fail(ERR_MQTT + error);
@@ -40,7 +40,12 @@ describe('Jexl expression interpreter', function () {
40
40
  object: {
41
41
  name: 'John',
42
42
  surname: 'Doe'
43
- }
43
+ },
44
+ invalidJson: '{"name": "John", surname": "Doe"}', // Invalid JSON for jsonparse
45
+ invalidDate: 'invalid-date', // Invalid date for toisodate and other date functions
46
+ emptyArray: [], // Empty array for addreduce
47
+ invalidHex: 'zz', // Invalid hex string for hextostring
48
+ nonNumeric: 'not-a-number' // Non-numeric input for parseint, parsefloat
44
49
  };
45
50
 
46
51
  describe('When a expression with a single value is parsed', function () {
@@ -392,4 +397,163 @@ describe('Jexl expression interpreter', function () {
392
397
  });
393
398
  });
394
399
  });
400
+
401
+ // Check errors
402
+ describe('When invalid inputs are used', function () {
403
+ describe('When an invalid JSON string is parsed', function () {
404
+ it('should return null for jsonparse', function (done) {
405
+ expressionParser.parse('invalidJson|jsonparse', scope, function (error, result) {
406
+ should.not.exist(error);
407
+ should(result).be.Null();
408
+ done();
409
+ });
410
+ });
411
+ });
412
+
413
+ describe('When an invalid date string is parsed', function () {
414
+ it('should return null for toisodate', function (done) {
415
+ expressionParser.parse('invalidDate|toisodate', scope, function (error, result) {
416
+ should.not.exist(error);
417
+ should(result).be.Null();
418
+ done();
419
+ });
420
+ });
421
+
422
+ it('should return null for timeoffset', function (done) {
423
+ expressionParser.parse('invalidDate|timeoffset', scope, function (error, result) {
424
+ should.not.exist(error);
425
+ should(result).be.Null();
426
+ done();
427
+ });
428
+ });
429
+ });
430
+
431
+ describe('When an empty array is processed with addreduce', function () {
432
+ it('should return null for addreduce', function (done) {
433
+ expressionParser.parse('emptyArray|addreduce', scope, function (error, result) {
434
+ should.not.exist(error);
435
+ should(result).be.Null();
436
+ done();
437
+ });
438
+ });
439
+ });
440
+
441
+ describe('When a non-numeric string is parsed by parseint and parsefloat', function () {
442
+ it('should return null for parseint', function (done) {
443
+ expressionParser.parse('nonNumeric|parseint', scope, function (error, result) {
444
+ should.not.exist(error);
445
+ should(result).be.Null();
446
+ done();
447
+ });
448
+ });
449
+
450
+ it('should return null for parsefloat', function (done) {
451
+ expressionParser.parse('nonNumeric|parsefloat', scope, function (error, result) {
452
+ should.not.exist(error);
453
+ should(result).be.Null();
454
+ done();
455
+ });
456
+ });
457
+ });
458
+
459
+ describe('When an invalid hex string is processed by hextostring', function () {
460
+ it('should return null for hextostring', function (done) {
461
+ expressionParser.parse('invalidHex|hextostring', scope, function (error, result) {
462
+ should.not.exist(error);
463
+ should(result).be.Null();
464
+ done();
465
+ });
466
+ });
467
+ });
468
+
469
+ describe('When invalid regular expressions are used in replace functions', function () {
470
+ it('should return null for replaceregexp with invalid regex', function (done) {
471
+ expressionParser.parse(
472
+ 'theString|replaceregexp("[a-z", "replacement")',
473
+ scope,
474
+ function (error, result) {
475
+ should.not.exist(error);
476
+ should(result).be.Null();
477
+ done();
478
+ }
479
+ );
480
+ });
481
+
482
+ it('should return null for replaceallregexp with invalid regex', function (done) {
483
+ expressionParser.parse(
484
+ 'theString|replaceallregexp("[a-z", "replacement")',
485
+ scope,
486
+ function (error, result) {
487
+ should.not.exist(error);
488
+ should(result).be.Null();
489
+ done();
490
+ }
491
+ );
492
+ });
493
+ });
494
+
495
+ describe('When tostring is used with null or undefined', function () {
496
+ it('should return null for tostring(null)', function (done) {
497
+ expressionParser.parse('null|tostring', scope, function (error, result) {
498
+ should.not.exist(error);
499
+ should(result).be.Null();
500
+ done();
501
+ });
502
+ });
503
+
504
+ it('should return null for tostring(undefined)', function (done) {
505
+ expressionParser.parse('undefined|tostring', scope, function (error, result) {
506
+ should.not.exist(error);
507
+ should(result).be.Null();
508
+ done();
509
+ });
510
+ });
511
+ });
512
+
513
+ describe('When urldecode is used with an invalid URI', function () {
514
+ it('should return null for urldecode with malformed URI', function (done) {
515
+ expressionParser.parse('"%%%invalidURI"|urldecode', scope, function (error, result) {
516
+ should.not.exist(error);
517
+ should(result).be.Null();
518
+ done();
519
+ });
520
+ });
521
+ });
522
+
523
+ describe('When tofixed is used with invalid inputs', function () {
524
+ it('should return null for tofixed with non-numeric value', function (done) {
525
+ expressionParser.parse('"notANumber"|tofixed(2)', scope, function (error, result) {
526
+ should.not.exist(error);
527
+ should(result).be.Null();
528
+ done();
529
+ });
530
+ });
531
+
532
+ it('should return null for tofixed with invalid decimal value', function (done) {
533
+ expressionParser.parse('"123.456"|tofixed("invalid")', scope, function (error, result) {
534
+ should.not.exist(error);
535
+ should(result).be.Null();
536
+ done();
537
+ });
538
+ });
539
+ });
540
+
541
+ describe('When gettime is used with invalid date', function () {
542
+ it('should return null for gettime with invalid date string', function (done) {
543
+ expressionParser.parse('"invalidDate"|gettime', scope, function (error, result) {
544
+ should.not.exist(error);
545
+ should(result).be.Null();
546
+ done();
547
+ });
548
+ });
549
+
550
+ it('should return null for gettime with null', function (done) {
551
+ expressionParser.parse('null|gettime', scope, function (error, result) {
552
+ should.not.exist(error);
553
+ should(result).be.Null();
554
+ done();
555
+ });
556
+ });
557
+ });
558
+ });
395
559
  });