botium-core 1.13.17 → 1.13.19

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.
Files changed (38) hide show
  1. package/README.md +0 -1
  2. package/dist/botium-cjs.js +118 -15
  3. package/dist/botium-cjs.js.map +1 -1
  4. package/dist/botium-es.js +118 -15
  5. package/dist/botium-es.js.map +1 -1
  6. package/package.json +2 -1
  7. package/src/BotDriver.js +2 -2
  8. package/src/Capabilities.js +6 -0
  9. package/src/containers/BaseContainer.js +7 -4
  10. package/src/containers/plugins/SimpleRestContainer.js +49 -0
  11. package/src/scripting/Convo.js +21 -0
  12. package/src/scripting/ScriptingProvider.js +19 -2
  13. package/src/scripting/logichook/LogicHookConsts.js +5 -1
  14. package/src/scripting/logichook/asserter/ButtonsAsserter.js +21 -8
  15. package/src/scripting/logichook/logichooks/ClearQueueLogicHook.js +0 -1
  16. package/src/scripting/logichook/logichooks/ConditionalBusinessHoursLogicHook.js +56 -0
  17. package/src/scripting/logichook/logichooks/ConditionalCapabilityValueBasedLogicHook.js +37 -0
  18. package/src/scripting/logichook/logichooks/ConditionalJsonPathBasedLogicHook.js +31 -0
  19. package/src/scripting/logichook/logichooks/ConditionalTimeBasedLogicHook.js +46 -0
  20. package/test/connectors/logicHook.js +0 -1
  21. package/test/connectors/simplerest.spec.js +79 -4
  22. package/test/scripting/asserters/buttonsAsserter.spec.js +84 -50
  23. package/test/scripting/logichooks/CustomConditionalLogicHook.js +21 -0
  24. package/test/scripting/logichooks/conditionalStepBusinessHoursLogicHook.spec.js +130 -0
  25. package/test/scripting/logichooks/conditionalStepCapabilityValueBasedLogicHook.spec.js +35 -0
  26. package/test/scripting/logichooks/conditionalStepJsonPathBasedLogicHook.spec.js +35 -0
  27. package/test/scripting/logichooks/conditionalStepTimeBasedLogicHook.spec.js +91 -0
  28. package/test/scripting/logichooks/convos/conditional_steps.convo.txt +12 -0
  29. package/test/scripting/logichooks/convos/conditional_steps_business_hours.convo.txt +16 -0
  30. package/test/scripting/logichooks/convos/conditional_steps_cap_value_based.convo.txt +12 -0
  31. package/test/scripting/logichooks/convos/conditional_steps_followed_by_bot_msg.convo.txt +15 -0
  32. package/test/scripting/logichooks/convos/conditional_steps_followed_by_me.convo.txt +18 -0
  33. package/test/scripting/logichooks/convos/conditional_steps_json_path_based.convo.txt.convo.txt +12 -0
  34. package/test/scripting/logichooks/convos/conditional_steps_multiple_condition_groups.convo.txt +20 -0
  35. package/test/scripting/logichooks/convos/conditional_steps_multiple_condition_groups_no_assertion.convo.txt +20 -0
  36. package/test/scripting/logichooks/convos/conditional_steps_time_based.convo.txt +12 -0
  37. package/test/scripting/logichooks/customConditionalStepLogicHook.spec.js +105 -0
  38. package/test/scripting/scriptingProvider.spec.js +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "botium-core",
3
- "version": "1.13.17",
3
+ "version": "1.13.19",
4
4
  "description": "The Selenium for Chatbots",
5
5
  "main": "index.js",
6
6
  "module": "dist/botium-es.js",
@@ -51,6 +51,7 @@
51
51
  "mime-types": "^2.1.35",
52
52
  "mkdirp": "^3.0.1",
53
53
  "moment": "^2.29.4",
54
+ "moment-timezone": "^0.5.43",
54
55
  "mustache": "^4.2.0",
55
56
  "promise-retry": "^2.0.1",
56
57
  "promise.allsettled": "^1.0.6",
package/src/BotDriver.js CHANGED
@@ -3,7 +3,7 @@ const fs = require('fs')
3
3
  const path = require('path')
4
4
  const async = require('async')
5
5
  const { rimraf } = require('rimraf')
6
- const { mkdirpSync } = require('mkdirp')
6
+ const mkdirp = require('mkdirp')
7
7
  const sanitize = require('sanitize-filename')
8
8
  const moment = require('moment')
9
9
  const randomize = require('randomatic')
@@ -130,7 +130,7 @@ module.exports = class BotDriver {
130
130
  (tempDirectoryCreated) => {
131
131
  tempDirectory = path.resolve(process.cwd(), this.caps[Capabilities.TEMPDIR], sanitize(`${this.caps[Capabilities.PROJECTNAME]} ${moment().format('YYYYMMDD HHmmss')} ${randomize('Aa0', 5)}`))
132
132
  try {
133
- mkdirpSync(tempDirectory)
133
+ mkdirp.sync(tempDirectory)
134
134
  tempDirectoryCreated()
135
135
  } catch (err) {
136
136
  tempDirectoryCreated(new Error(`Unable to create temp directory ${tempDirectory}: ${err.message}`))
@@ -68,6 +68,12 @@ module.exports = {
68
68
  SIMPLEREST_POLL_INTERVAL: 'SIMPLEREST_POLL_INTERVAL',
69
69
  SIMPLEREST_POLL_TIMEOUT: 'SIMPLEREST_PING_TIMEOUT',
70
70
  SIMPLEREST_POLL_UPDATE_CONTEXT: 'SIMPLEREST_POLL_UPDATE_CONTEXT',
71
+ SIMPLEREST_CONTEXT_IGNORE_JSONPATH: 'SIMPLEREST_CONTEXT_IGNORE_JSONPATH',
72
+ SIMPLEREST_CONTEXT_IGNORE_MATCH: 'SIMPLEREST_CONTEXT_IGNORE_MATCH',
73
+ SIMPLEREST_CONTEXT_SKIP_JSONPATH: 'SIMPLEREST_CONTEXT_SKIP_JSONPATH',
74
+ SIMPLEREST_CONTEXT_SKIP_MATCH: 'SIMPLEREST_CONTEXT_SKIP_MATCH',
75
+ SIMPLEREST_CONTEXT_CONTINUE_JSONPATH: 'SIMPLEREST_CONTEXT_CONTINUE_JSONPATH',
76
+ SIMPLEREST_CONTEXT_CONTINUE_MATCH: 'SIMPLEREST_CONTEXT_CONTINUE_MATCH',
71
77
  SIMPLEREST_BODY_JSONPATH: 'SIMPLEREST_BODY_JSONPATH',
72
78
  SIMPLEREST_RESPONSE_JSONPATH: 'SIMPLEREST_RESPONSE_JSONPATH',
73
79
  SIMPLEREST_RESPONSE_HOOK: 'SIMPLEREST_RESPONSE_HOOK',
@@ -1,6 +1,6 @@
1
1
  const util = require('util')
2
2
  const async = require('async')
3
- const { rimraf } = require('rimraf')
3
+ const rimraf = require('rimraf')
4
4
  const Bottleneck = require('bottleneck')
5
5
  const _ = require('lodash')
6
6
  const request = require('request')
@@ -173,9 +173,12 @@ module.exports = class BaseContainer {
173
173
  (rimraffed) => {
174
174
  if (this.caps[Capabilities.CLEANUPTEMPDIR]) {
175
175
  debug(`Cleanup rimrafing temp dir ${this.tempDirectory}`)
176
- rimraf(this.tempDirectory)
177
- .catch((err) => debug(`Cleanup temp dir ${this.tempDirectory} failed: ${util.inspect(err)}`))
178
- .finally(() => rimraffed())
176
+ try {
177
+ rimraf.sync(this.tempDirectory)
178
+ rimraffed()
179
+ } catch (err) {
180
+ rimraffed(new Error(`Cleanup temp directory ${this.tempDirectory} failed: ${util.inspect(err)}`))
181
+ }
179
182
  } else {
180
183
  rimraffed()
181
184
  }
@@ -293,6 +293,47 @@ module.exports = class SimpleRestContainer {
293
293
  debug(`current session context: ${util.inspect(this.view.context)}`)
294
294
  }
295
295
 
296
+ const _isAnyContextJsonPathMatch = (capName, capNameMatch) => {
297
+ const jsonPaths = getAllCapValues(capName, this.caps)
298
+ if (jsonPaths.length > 0) {
299
+ const jsonPathsMatch = getAllCapValues(capNameMatch, this.caps)
300
+ for (const [index, jsonPath] of jsonPaths.entries()) {
301
+ const contextNodes = jp.query(this.view.context, jsonPath)
302
+ if (_.isArray(contextNodes) && contextNodes.length > 0) {
303
+ if (jsonPathsMatch[index]) {
304
+ if (contextNodes[0] === jsonPathsMatch[index]) {
305
+ return {
306
+ jsonPath,
307
+ match: contextNodes[0]
308
+ }
309
+ }
310
+ } else {
311
+ return {
312
+ jsonPath
313
+ }
314
+ }
315
+ }
316
+ }
317
+ }
318
+ return null
319
+ }
320
+
321
+ const ignoreMatch = _isAnyContextJsonPathMatch(Capabilities.SIMPLEREST_CONTEXT_IGNORE_JSONPATH, Capabilities.SIMPLEREST_CONTEXT_IGNORE_MATCH)
322
+ if (ignoreMatch) {
323
+ if (ignoreMatch.match) debug(`ignoring response for context match: ${ignoreMatch.jsonPath} = ${ignoreMatch.match}`)
324
+ else debug(`ignoring response for context: ${ignoreMatch.jsonPath}`)
325
+ return
326
+ }
327
+
328
+ const skipMatch = _isAnyContextJsonPathMatch(Capabilities.SIMPLEREST_CONTEXT_SKIP_JSONPATH, Capabilities.SIMPLEREST_CONTEXT_SKIP_MATCH)
329
+ if (skipMatch) {
330
+ if (skipMatch.match) debug(`skipping response for context match: ${skipMatch.jsonPath} = ${skipMatch.match}`)
331
+ else debug(`skipping response for context: ${skipMatch.jsonPath}`)
332
+
333
+ setTimeout(() => this._doRequest({ messageText: '' }, true, true), 0)
334
+ return
335
+ }
336
+
296
337
  const result = []
297
338
  if (isFromUser) {
298
339
  const _extractFrom = (root, jsonPaths) => {
@@ -442,6 +483,14 @@ module.exports = class SimpleRestContainer {
442
483
  }
443
484
  }
444
485
  }
486
+
487
+ const continueMatch = _isAnyContextJsonPathMatch(Capabilities.SIMPLEREST_CONTEXT_CONTINUE_JSONPATH, Capabilities.SIMPLEREST_CONTEXT_CONTINUE_MATCH)
488
+ if (continueMatch) {
489
+ if (continueMatch.match) debug(`continue with next response for context match: ${continueMatch.jsonPath} = ${continueMatch.match}`)
490
+ else debug(`continue with next response for context: ${continueMatch.jsonPath}`)
491
+
492
+ setTimeout(() => this._doRequest({ messageText: '' }, true, true), 0)
493
+ }
445
494
  return result
446
495
  }
447
496
 
@@ -356,6 +356,7 @@ class Convo {
356
356
  throw failErr
357
357
  }
358
358
  } else if (convoStep.sender === 'bot') {
359
+ const previousWaitForBotSays = waitForBotSays
359
360
  if (waitForBotSays) {
360
361
  botMsg = null
361
362
  } else {
@@ -404,6 +405,26 @@ class Convo {
404
405
  throw failErr
405
406
  }
406
407
 
408
+ if (convoStep.conditional) {
409
+ const nextConvoStep = this.conversation[i + 1]
410
+
411
+ if (!previousWaitForBotSays) {
412
+ skipTranscriptStep = true
413
+ }
414
+ waitForBotSays = false
415
+ if (!nextConvoStep || nextConvoStep.sender !== 'bot' || !nextConvoStep.logicHooks || !nextConvoStep.logicHooks.some(lh => lh.name.toUpperCase().startsWith('CONDITIONAL_STEP'))) {
416
+ waitForBotSays = true
417
+ } else {
418
+ const conditionalLogicHook = convoStep.logicHooks.find(lh => lh.name.startsWith('CONDITIONAL_STEP'))
419
+ const nextConditionalLogicHook = nextConvoStep.logicHooks.find(lh => lh.name.startsWith('CONDITIONAL_STEP'))
420
+ waitForBotSays = conditionalLogicHook.args[1] !== nextConditionalLogicHook.args[1]
421
+ }
422
+
423
+ if (convoStep.conditional.skip) {
424
+ continue
425
+ }
426
+ }
427
+
407
428
  if (!botMsg || (!botMsg.messageText && !botMsg.media && !botMsg.buttons && !botMsg.cards && !botMsg.sourceData && !botMsg.nlp)) {
408
429
  const failErr = new BotiumError(`${this.header.name}/${convoStep.stepTag}: bot says nothing`)
409
430
  debug(failErr)
@@ -940,10 +940,16 @@ module.exports = class ScriptingProvider {
940
940
  convoFilter: null
941
941
  }, options)
942
942
  const expandedConvos = []
943
+ // The globalContext is going to keep the data even if the Object.assign which happening to create the myContext in _expandConvo function
944
+ const context = {
945
+ globalContext: {
946
+ totalConvoCount: 0
947
+ }
948
+ }
943
949
  debug(`ExpandConvos - Using utterances expansion mode: ${this.caps[Capabilities.SCRIPTING_UTTEXPANSION_MODE]}`)
944
950
  this.convos.forEach((convo) => {
945
951
  convo.expandPartialConvos()
946
- for (const expanded of this._expandConvo(convo, options, {})) {
952
+ for (const expanded of this._expandConvo(convo, options, context)) {
947
953
  expanded.header.assertionCount = this.GetAssertionCount(expanded)
948
954
  if (options.justHeader) {
949
955
  const ConvoWithOnlyHeader = {
@@ -959,6 +965,7 @@ module.exports = class ScriptingProvider {
959
965
  }
960
966
  })
961
967
  this.convos = expandedConvos
968
+ this.totalConvoCount = context.globalContext.totalConvoCount
962
969
  if (!options.justHeader) {
963
970
  this._sortConvos()
964
971
  } else {
@@ -971,17 +978,24 @@ module.exports = class ScriptingProvider {
971
978
  // drop unwanted convos
972
979
  convoFilter: null
973
980
  }, options)
981
+ // The globalContext is going to keep the data even if the Object.assign which happening to create the myContext in _expandConvo function
982
+ const context = {
983
+ globalContext: {
984
+ totalConvoCount: 0
985
+ }
986
+ }
974
987
  debug(`ExpandConvos - Using utterances expansion mode: ${this.caps[Capabilities.SCRIPTING_UTTEXPANSION_MODE]}`)
975
988
  // creating a nested generator, calling the other.
976
989
  // We hope this.convos does not changes while this iterator is used
977
990
  const _convosIterable = function * (options) {
978
991
  for (const convo of this.convos) {
979
992
  convo.expandPartialConvos()
980
- yield * this._expandConvo(convo, options, {})
993
+ yield * this._expandConvo(convo, options, context)
981
994
  }
982
995
  }.bind(this)
983
996
 
984
997
  this.convosIterable = _convosIterable(options)
998
+ this.totalConvoCount = context.globalContext.totalConvoCount
985
999
  }
986
1000
 
987
1001
  /**
@@ -1173,6 +1187,9 @@ module.exports = class ScriptingProvider {
1173
1187
  }
1174
1188
  } else {
1175
1189
  const expanded = Object.assign(_.cloneDeep(currentConvo), { conversation: _.cloneDeep(convoStepsStack) })
1190
+ if (!_.isNil(_.get(context, 'globalContext.totalConvoCount'))) {
1191
+ context.globalContext.totalConvoCount++
1192
+ }
1176
1193
  if (!options.convoFilter || options.convoFilter(expanded)) {
1177
1194
  yield expanded
1178
1195
  }
@@ -57,7 +57,11 @@ module.exports = {
57
57
  { name: 'ASSIGN_SCRIPTING_MEMORY', className: 'AssignScriptingMemoryLogicHook' },
58
58
  { name: 'UPDATE_CUSTOM', className: 'UpdateCustomLogicHook' },
59
59
  { name: 'SKIP_BOT_UNCONSUMED', className: 'ClearQueueLogicHook' },
60
- { name: LOGIC_HOOK_INCLUDE, className: 'IncludeLogicHook' }
60
+ { name: LOGIC_HOOK_INCLUDE, className: 'IncludeLogicHook' },
61
+ { name: 'CONDITIONAL_STEP_TIME_BASED', className: 'ConditionalTimeBasedLogicHook' },
62
+ { name: 'CONDITIONAL_STEP_BUSINESS_HOURS', className: 'ConditionalBusinessHoursLogicHook' },
63
+ { name: 'CONDITIONAL_STEP_CAPABILITY_VALUE_BASED', className: 'ConditionalCapabilityValueBasedLogicHook' },
64
+ { name: 'CONDITIONAL_STEP_JSON_PATH_BASED', className: 'ConditionalJsonPathBasedLogicHook.js' }
61
65
  ],
62
66
  DEFAULT_USER_INPUTS: [
63
67
  { name: 'BUTTON', className: 'ButtonInput' },
@@ -1,3 +1,4 @@
1
+ const _ = require('lodash')
1
2
  const { SCRIPTING_NORMALIZE_TEXT } = require('../../../Capabilities')
2
3
  const { BotiumError } = require('../../BotiumError')
3
4
  const { buttonsFromMsg } = require('../helpers')
@@ -11,17 +12,29 @@ module.exports = class ButtonsAsserter {
11
12
  }
12
13
 
13
14
  _evalButtons (args, botMsg) {
14
- const allButtons = buttonsFromMsg(botMsg, true).map(b => b.text).filter(b => b).map(b => normalizeText(b, !!this.caps[SCRIPTING_NORMALIZE_TEXT]))
15
+ const allButtons = buttonsFromMsg(botMsg, true).map(b => ({ text: b.text, payload: b.payload })).filter(b => b).map(b => ({ text: normalizeText(b.text, !!this.caps[SCRIPTING_NORMALIZE_TEXT]), payload: b.payload }))
15
16
  if (!args || args.length === 0) {
16
- return { allButtons, buttonsNotFound: [], buttonsFound: allButtons }
17
+ return { allButtons, buttonsNotFound: [], buttonsFound: allButtons.map(b => b.text) }
17
18
  }
18
19
  const buttonsNotFound = []
19
20
  const buttonsFound = []
21
+ const stringifyPayload = (payload) => {
22
+ if (_.isNil(payload)) {
23
+ return ''
24
+ }
25
+ try {
26
+ return JSON.stringify(JSON.parse(payload))
27
+ } catch (e) {
28
+ return JSON.stringify(payload)
29
+ }
30
+ }
20
31
  for (let i = 0; i < (args || []).length; i++) {
21
- if (allButtons.findIndex(b => this.context.Match(b, normalizeText(args[i], !!this.caps[SCRIPTING_NORMALIZE_TEXT]))) < 0) {
22
- buttonsNotFound.push(args[i])
23
- } else {
32
+ const matchByText = allButtons.some(b => this.context.Match(b.text, normalizeText(args[i], !!this.caps[SCRIPTING_NORMALIZE_TEXT])))
33
+ const matchByPayload = allButtons.some(b => this.context.Match(stringifyPayload(b.payload), normalizeText(args[i], !!this.caps[SCRIPTING_NORMALIZE_TEXT])))
34
+ if (matchByText || matchByPayload) {
24
35
  buttonsFound.push(args[i])
36
+ } else {
37
+ buttonsNotFound.push(args[i])
25
38
  }
26
39
  }
27
40
  return { allButtons, buttonsNotFound, buttonsFound }
@@ -42,7 +55,7 @@ module.exports = class ButtonsAsserter {
42
55
  cause: {
43
56
  not: true,
44
57
  expected: args,
45
- actual: allButtons,
58
+ actual: JSON.stringify(allButtons, null, 2),
46
59
  diff: buttonsFound
47
60
  }
48
61
  }
@@ -67,7 +80,7 @@ module.exports = class ButtonsAsserter {
67
80
  cause: {
68
81
  not: false,
69
82
  expected: args,
70
- actual: allButtons,
83
+ actual: JSON.stringify(allButtons, null, 2),
71
84
  diff: buttonsNotFound
72
85
  }
73
86
  }
@@ -85,7 +98,7 @@ module.exports = class ButtonsAsserter {
85
98
  cause: {
86
99
  not: false,
87
100
  expected: args,
88
- actual: allButtons,
101
+ actual: JSON.stringify(allButtons, null, 2),
89
102
  diff: buttonsNotFound
90
103
  }
91
104
  }
@@ -1,4 +1,3 @@
1
-
2
1
  module.exports = class ClearQueueLogicHook {
3
2
  constructor (context, caps = {}) {
4
3
  this.context = context
@@ -0,0 +1,56 @@
1
+ const util = require('util')
2
+ const moment = require('moment-timezone')
3
+ const debug = require('debug')('botium-core-ConditionalTimeBasedLogicHook')
4
+
5
+ module.exports = class ConditionalBusinessHoursLogicHook {
6
+ constructor (context, caps, globalArgs) {
7
+ this.context = context
8
+ this.caps = caps
9
+ this.globalArgs = globalArgs
10
+ }
11
+
12
+ _isBetween ({ now, start, end, days, timeZone }) {
13
+ if ((!start || !end) && (!days || days.length === 0)) {
14
+ throw new Error('ConditionalBusinessHoursLogicHook: Either start and end time or days array needs to be specified in params')
15
+ }
16
+ now.tz(timeZone)
17
+ if (days && days.length > 0 && !days.includes(now.format('dddd'))) {
18
+ return false
19
+ }
20
+
21
+ if (!start && !end && days && days.length > 0 && days.includes(now.format('dddd'))) {
22
+ return true
23
+ }
24
+
25
+ const momentStartTime = moment(start, 'HH:mm')
26
+ const startTime = now.clone().set({ hour: momentStartTime.hour(), minute: momentStartTime.minute() })
27
+ startTime.tz(timeZone)
28
+ const momentEndTime = moment(end, 'HH:mm')
29
+ const endTime = now.clone().set({ hour: momentEndTime.hour(), minute: momentEndTime.minute() })
30
+ endTime.tz(timeZone)
31
+ if (startTime.isSameOrAfter(endTime)) {
32
+ if (now.isSameOrAfter(startTime)) {
33
+ endTime.add(1, 'days')
34
+ } else {
35
+ startTime.add(-1, 'days')
36
+ }
37
+ }
38
+ return now.isBetween(startTime, endTime, 'minutes', '[]')
39
+ }
40
+
41
+ onBotPrepare ({ convo, convoStep, args }) {
42
+ const conditionGroupId = args[1]
43
+ let params
44
+ try {
45
+ params = JSON.parse(args[0])
46
+ } catch (e) {
47
+ throw new Error(`ConditionalBusinessHoursLogicHook: No parsable JSON object found in params: ${e}`)
48
+ }
49
+ convoStep.conditional = {
50
+ conditionGroupId
51
+ }
52
+ params.now = moment()
53
+ convoStep.conditional.skip = !this._isBetween(params)
54
+ debug(`ConditionalBusinessHoursLogicHook onBotPrepare ${convo.header.name}/${convoStep.stepTag}, args: ${util.inspect(args)}, convoStep.conditional: ${convoStep.conditional}`)
55
+ }
56
+ }
@@ -0,0 +1,37 @@
1
+ const util = require('util')
2
+ const jp = require('jsonpath')
3
+ const _ = require('lodash')
4
+ const debug = require('debug')('botium-core-ConditionalCapabilityValueBasedLogicHook')
5
+
6
+ module.exports = class ConditionalCapabilityValueBasedLogicHook {
7
+ constructor (context, caps, globalArgs) {
8
+ this.context = context
9
+ this.caps = caps
10
+ this.globalArgs = globalArgs
11
+ }
12
+
13
+ _isCapabilityValueEqual ({ capabilityName, jsonPath, value }) {
14
+ if (jsonPath) {
15
+ const capabilityObject = _.isObject(this.caps[capabilityName]) ? this.caps[capabilityName] : JSON.parse(this.caps[capabilityName])
16
+ const values = jp.query(capabilityObject, jsonPath)
17
+ return !!(values && values.length > 0 && values.includes(value))
18
+ } else {
19
+ return this.caps[capabilityName] === value
20
+ }
21
+ }
22
+
23
+ onBotPrepare ({ convo, convoStep, args }) {
24
+ const conditionGroupId = args[1]
25
+ let params
26
+ try {
27
+ params = JSON.parse(args[0])
28
+ } catch (e) {
29
+ throw new Error(`ConditionalCapabilityValueBasedLogicHook: No parsable JSON object found in params: ${e}`)
30
+ }
31
+ convoStep.conditional = {
32
+ conditionGroupId
33
+ }
34
+ convoStep.conditional.skip = !this._isCapabilityValueEqual(params)
35
+ debug(`ConditionalCapabilityValueBasedLogicHook onBotPrepare ${convo.header.name}/${convoStep.stepTag}, args: ${util.inspect(args)}, convoStep.conditional: ${convoStep.conditional}`)
36
+ }
37
+ }
@@ -0,0 +1,31 @@
1
+ const util = require('util')
2
+ const jp = require('jsonpath')
3
+ const debug = require('debug')('botium-core-ConditionalJsonPathBasedLogicHook')
4
+
5
+ module.exports = class ConditionalJsonPathBasedLogicHook {
6
+ constructor (context, caps, globalArgs) {
7
+ this.context = context
8
+ this.caps = caps
9
+ this.globalArgs = globalArgs
10
+ }
11
+
12
+ onBotPrepare ({ convo, convoStep, args, botMsg }) {
13
+ const conditionGroupId = args[1]
14
+ let params
15
+ try {
16
+ params = JSON.parse(args[0])
17
+ } catch (e) {
18
+ throw new Error(`ConditionalJsonPathBasedLogicHook: No parsable JSON object found in params: ${e}`)
19
+ }
20
+ convoStep.conditional = {
21
+ conditionGroupId
22
+ }
23
+ let skip = true
24
+ if (params.jsonPath) {
25
+ const values = jp.query(botMsg, params.jsonPath)
26
+ skip = !(values && values.length > 0 && values.includes(params.value))
27
+ }
28
+ convoStep.conditional.skip = skip
29
+ debug(`ConditionalJsonPathBasedLogicHook onBotPrepare ${convo.header.name}/${convoStep.stepTag}, args: ${util.inspect(args)}, convoStep.conditional: ${convoStep.conditional}`)
30
+ }
31
+ }
@@ -0,0 +1,46 @@
1
+ const util = require('util')
2
+ const moment = require('moment-timezone')
3
+ const debug = require('debug')('botium-core-ConditionalTimeBasedLogicHook')
4
+
5
+ module.exports = class ConditionalTimeBasedLogicHook {
6
+ constructor (context, caps, globalArgs) {
7
+ this.context = context
8
+ this.caps = caps
9
+ this.globalArgs = globalArgs
10
+ }
11
+
12
+ _isBetween ({ now, start, end, timeZone }) {
13
+ now.tz(timeZone)
14
+ const momentStartTime = moment(start, 'HH:mm')
15
+ const startTime = now.clone().set({ hour: momentStartTime.hour(), minute: momentStartTime.minute() })
16
+ startTime.tz(timeZone)
17
+ const momentEndTime = moment(end, 'HH:mm')
18
+ const endTime = now.clone().set({ hour: momentEndTime.hour(), minute: momentEndTime.minute() })
19
+ endTime.tz(timeZone)
20
+
21
+ if (startTime.isSameOrAfter(endTime)) {
22
+ if (now.isSameOrAfter(startTime)) {
23
+ endTime.add(1, 'days')
24
+ } else {
25
+ startTime.add(-1, 'days')
26
+ }
27
+ }
28
+ return now.isBetween(startTime, endTime, 'minutes', '[]')
29
+ }
30
+
31
+ onBotPrepare ({ convo, convoStep, args }) {
32
+ const conditionGroupId = args[1]
33
+ let params
34
+ try {
35
+ params = JSON.parse(args[0])
36
+ } catch (e) {
37
+ throw new Error(`ConditionalTimeBasedLogicHook: No parsable JSON object found in params: ${e}`)
38
+ }
39
+ convoStep.conditional = {
40
+ conditionGroupId
41
+ }
42
+ params.now = moment()
43
+ convoStep.conditional.skip = !this._isBetween(params)
44
+ debug(`ConditionalTimeBasedLogicHook onBotPrepare ${convo.header.name}/${convoStep.stepTag}, args: ${util.inspect(args)}, convoStep.conditional: ${convoStep.conditional}`)
45
+ }
46
+ }
@@ -1,4 +1,3 @@
1
-
2
1
  module.exports = ({ requestOptions, context }) => {
3
2
  let counter = 1
4
3
  requestOptions.body = { bodyFieldRequestHook: counter++ }
@@ -319,7 +319,6 @@ describe('connectors.simplerest', function () {
319
319
  scope2.persist(false)
320
320
  })
321
321
  })
322
-
323
322
  describe('build', function () {
324
323
  it('should build JSON GET url', async function () {
325
324
  const myCaps = Object.assign({}, myCapsGet)
@@ -997,7 +996,6 @@ describe('connectors.simplerest', function () {
997
996
  assert.deepEqual(values, ['$.1', '$.2', '$.3', '$.4'])
998
997
  })
999
998
  })
1000
-
1001
999
  describe('useresponse', function () {
1002
1000
  beforeEach(async function () {
1003
1001
  this.init = async (caps) => {
@@ -1151,7 +1149,6 @@ describe('connectors.simplerest', function () {
1151
1149
  }
1152
1150
  })
1153
1151
  })
1154
-
1155
1152
  describe('inbound', function () {
1156
1153
  it('should accept inbound message with matching jsonpath', async function () {
1157
1154
  const myCaps = Object.assign({}, myCapsGet)
@@ -1249,7 +1246,6 @@ describe('connectors.simplerest', function () {
1249
1246
  return result
1250
1247
  })
1251
1248
  })
1252
-
1253
1249
  describe('polling', function () {
1254
1250
  it('should poll HTTP url', async function () {
1255
1251
  const caps = {
@@ -1313,4 +1309,83 @@ describe('connectors.simplerest', function () {
1313
1309
  scope.persist(false)
1314
1310
  }).timeout(5000)
1315
1311
  })
1312
+ describe('flow', function () {
1313
+ it('should ignore matching message', async function () {
1314
+ const caps = {
1315
+ [Capabilities.CONTAINERMODE]: 'simplerest',
1316
+ [Capabilities.WAITFORBOTTIMEOUT]: 1000,
1317
+ [Capabilities.SIMPLEREST_URL]: 'https://mock.com/flowignore',
1318
+ [Capabilities.SIMPLEREST_RESPONSE_JSONPATH]: ['$.text'],
1319
+ [Capabilities.SIMPLEREST_CONTEXT_IGNORE_JSONPATH]: '$.ignore',
1320
+ [Capabilities.SIMPLEREST_CONTEXT_IGNORE_MATCH]: 'Y'
1321
+ }
1322
+ const scope = nock('https://mock.com')
1323
+ .get('/flowignore').reply(200, { text: 'ignore it', ignore: 'Y' })
1324
+
1325
+ const driver = new BotDriver(caps)
1326
+ const container = await driver.Build()
1327
+ await container.Start()
1328
+
1329
+ await container.UserSays({ text: 'hallo' })
1330
+ try {
1331
+ await container.WaitBotSays()
1332
+ assert.fail('expected response to be ignored')
1333
+ } catch (err) {
1334
+ assert.equal(err.message, 'Bot did not respond within 1s')
1335
+ }
1336
+ await container.Stop()
1337
+ await container.Clean()
1338
+ scope.persist(false)
1339
+ })
1340
+ it('should skip matching message', async function () {
1341
+ const caps = {
1342
+ [Capabilities.CONTAINERMODE]: 'simplerest',
1343
+ [Capabilities.SIMPLEREST_URL]: 'https://mock.com/flowskip',
1344
+ [Capabilities.SIMPLEREST_RESPONSE_JSONPATH]: ['$.text'],
1345
+ [Capabilities.SIMPLEREST_CONTEXT_SKIP_JSONPATH]: '$.skip',
1346
+ [Capabilities.SIMPLEREST_CONTEXT_SKIP_MATCH]: 'Y'
1347
+ }
1348
+ const scope = nock('https://mock.com')
1349
+ .get('/flowskip').reply(200, { text: 'skip it', skip: 'Y' })
1350
+ .get('/flowskip').reply(200, { text: 'not skip it', skip: 'N' })
1351
+
1352
+ const driver = new BotDriver(caps)
1353
+ const container = await driver.Build()
1354
+ await container.Start()
1355
+
1356
+ await container.UserSays({ text: 'hello' })
1357
+ const botMsg = await container.WaitBotSays()
1358
+ assert.equal(botMsg.messageText, 'not skip it')
1359
+
1360
+ await container.Stop()
1361
+ await container.Clean()
1362
+ scope.persist(false)
1363
+ })
1364
+ it('should continue on matching message', async function () {
1365
+ const caps = {
1366
+ [Capabilities.CONTAINERMODE]: 'simplerest',
1367
+ [Capabilities.SIMPLEREST_URL]: 'https://mock.com/flowcontinue',
1368
+ [Capabilities.SIMPLEREST_RESPONSE_JSONPATH]: ['$.text'],
1369
+ [Capabilities.SIMPLEREST_CONTEXT_CONTINUE_JSONPATH]: '$.continue',
1370
+ [Capabilities.SIMPLEREST_CONTEXT_CONTINUE_MATCH]: 'Y'
1371
+ }
1372
+ const scope = nock('https://mock.com')
1373
+ .get('/flowcontinue').reply(200, { text: 'continue it', continue: 'Y' })
1374
+ .get('/flowcontinue').reply(200, { text: 'not continue it', continue: 'N' })
1375
+
1376
+ const driver = new BotDriver(caps)
1377
+ const container = await driver.Build()
1378
+ await container.Start()
1379
+
1380
+ await container.UserSays({ text: 'hello' })
1381
+ const botMsg1 = await container.WaitBotSays()
1382
+ assert.equal(botMsg1.messageText, 'continue it')
1383
+ const botMsg2 = await container.WaitBotSays()
1384
+ assert.equal(botMsg2.messageText, 'not continue it')
1385
+
1386
+ await container.Stop()
1387
+ await container.Clean()
1388
+ scope.persist(false)
1389
+ })
1390
+ })
1316
1391
  })