This Self Testing Toolkit implemented "Rules Engine" by taking advantage of Json Rules Engine.
"Json Rules Engine" is a powerful, lightweight rules engine. Rules are composed of simple json structures, making them human readable and easy to persist.
Rules are composed of two components: conditions and events. Conditions are a set of requirements that must be met to trigger the rule's event.
You can refer https://www.npmjs.com/package/json-rules-engine for further information on JRE.
The rules in self testing toolkit are implemented at two levels.
- One for validating the incoming request and send error callback if the validation rule matches.
- Another for generating callbacks if the callback rule matches.
In both cases the conditions syntax is same as per the Json Rules Engine, where as the event types will be different for the two levels.
The rules file is a json file with an array of individual rules.
A typical validation rule file syntax is like below.
[
{
"ruleId": 1,
"description": "If the transfer amount is equal to 2 USD, send a fixed error callback",
"conditions": {
"all": [
{
"fact": "path",
"operator": "equal",
"value": "/transfers"
},
{
"fact": "method",
"operator": "equal",
"value": "post"
},
{
"fact": "body",
"operator": "equal",
"value": "2",
"path": "amount.amount"
}
]
},
"event": {
"type": "FIXED_ERROR_CALLBACK",
"params": {
"path": "/transfers/{$request.body.transferId}/error",
"method": "put",
"headers": {},
"delay": 100,
"body": {
"errorInformation": {
"errorCode": "5001",
"errorDescription": "The amount is less than the required amount"
}
}
}
}
}
]
Conditions can be defined with either "all" or "any" type of arrays. In each array there are objects containing three basic properties "fact", "operator" and "value".
-
Fact - facts are the variable names that can be one of the following
-
path - The request path
Example: /transfers, /parties/MSISDN/9876543210 ... etc
-
method - The http method of the request in lowercase
Example: get, post ... etc
-
body - The request body object. The properties of the body can be specified using a condition helper called "path".
Example: amount.amount from the body '{ "amount": { "amount": 20, "type": "USD" } }' can be compared with the following condition
{ "fact": "body", "operator": "equal", "value": "100", "path": "amount.amount" } -
pathParams - By using this the path parameter values from the request can be compared.
Example: Type & ID of pathParams from the get request /parties/{Type}/{ID}
-
queryParams - By using this the query parameter values from the request can be compared.
Example: 'state' of queryParams from the get request /settlementWindows?state=OPEN
-
operationPath - This is used for matching the path syntax from open API file
Example: operationPath contains the value /parties/{Type}/{ID} instead of /parties/MSISDN/9876543210 which can actually be found in the fact 'path'.
-
-
Operator - The
operatorcompares the value returned by thefactto what is stored in thevalueproperty. If the result is truthy, the condition passes. There are number of operators available to compare the values-
String and Numeric operators:
equal- fact must equal valuenotEqual- fact must not equal valuethese operators use strict equality (===) and inequality (!==)
-
Numeric operators:
lessThan- fact must be less than valuelessThanInclusive- fact must be less than or equal to valuegreaterThan- fact must be greater than valuegreaterThanInclusive- fact must be greater than or equal to value -
String Numeric operators:
(These operators can be used if the type fo the property is string, but we want to parse it to integer and compare)
numericEqual- fact must equal valuenumericNotEqual- fact must not equal valuenumericLessThan- fact must be less than valuenumericLessThanInclusive- fact must be less than or equal to valuenumericGreaterThan- fact must be greater than valuenumericGreaterThanInclusive- fact must be greater than or equal to value -
Array operators:
in- fact must be included in value (an array)notIn- fact must not be included in value (an array)contains- fact (an array) must include valuedoesNotContain- fact (an array) must not include value
-
-
Value - The actual value to compare
Following are the examples of conditions
"all": [
{
"fact": "path",
"operator": "equal",
"value": "/transfers"
},
{
"fact": "method",
"operator": "equal",
"value": "post"
}
]
"any": [
{
"fact": "body",
"path": "amount.type",
"operator": "equal",
"value": "GBP"
},
{
"fact": "body",
"path": "amount.type",
"operator": "equal",
"value": "USD"
}
]
And event can be defined with "type" and "params" properties.
There are different type of events based on the rules engine level.
- Validation Rules Engine
- FIXED_ERROR_CALLBACK
- MOCK_ERROR_CALLBACK
- NO_CALLBACK
- Callbacks Rules Engine
- FIXED_CALLBACK
- MOCK_CALLBACK
- NO_CALLBACK
- Synchronous Response Rules Engine
- FIXED_RESPONSE
- MOCK_RESPONSE
For FIXED_ERROR_CALLBACK & FIXED_CALLBACK, we need to supply every detail for sending callback like path, method, headers & body Let's look at an example of an event type FIXED_ERROR_CALLBACK
"event": {
"type": "FIXED_ERROR_CALLBACK",
"params": {
"path": "/parties/{$request.params.Type}/{$request.params.ID}/error",
"method": "put",
"headers": {},
"delay": 100,
"body": {
"errorInformation": {
"errorCode": "5001",
"errorDescription": "The party {$request.params.ID} is not found"
}
}
}
}
You can observe the configurable parameters '{$request.params.Type}' and '{$request.params.ID}'. These values will be replaced with the values from the request in run time.
The following is the list of configurable parameters that you can define anywhere in path, headers & body values.
- $request.params - Parameters in the request path
- $request.body - Request body
- $request.path - Entire request path
- $request.query - Query parameters of the request
- $request.headers - Http headers of the request
- $config.<param_name> - Configuration parameter value from environment or local.env file
- $session.negotiatedContentType - Negotiated content type. Can be sent to callbacks and error callbacks
- $session.uniqueId - UniqueID for the request session
Advanced parameters for developers:
There are some advanced parameters type $function is implemented for advanced users / developers. The syntax is like below
- $function.sample.getSampleText (For development purpose)
The above parameter calls the function 'getSampleText' in 'sample.js' file located at 'lib/mocking/custom-functions' folder. An argument will be passed to this function from which the developer can get the request parameters to process like 'arg1.request.body.quoteId', 'arg1.request.headers.date' ...etc
Please be noted that these javascript files can not be change in runtime and need compilation for the changes.
For MOCK_ERROR_CALLBACK & MOCK_CALLBACK, we don't need to supply the details like path, method, headers & body. The toolkit will generate the callback based on the openAPI definition file.
Let's look at an example of an event type MOCK_ERROR_CALLBACK
"event": {
"type": "MOCK_ERROR_CALLBACK",
"params": {
}
}
That's it, the self testing toolkit can be able to generate a mock callback based on the open API file with random values.
Also you can define the params like in fixed callbacks to override a particular value.
See the following example in which the generated values of the firstName & name will be replaced accordingly
"event": {
"type": "MOCK_CALLBACK",
"params": {
"body": {
"party": {
"personalInfo": {
"complexName": {
"firstName": "A fixed value",
}
}
"name": "The party with phone number ${request.params.ID}"
}
}
}
}
For NO_CALLBACK, there are some test cases where the requirements dictate that there is no follow up callback.
"event": {
"type": "NO_CALLBACK",
}
These event types are used for defining events for synchronous response rules.
These are same like callback event types, but the only difference is we need to provide the statusCode for FIXED_RESPONSE type. MOCK_RESPONSE automatically picks up the success response code from api specification file.
Let's look at an example of an event type FIXED_RESPONSE
"event": {
"type": "FIXED_RESPONSE",
"params": {
"body": [
{
"createdDate": "2020-02-10",
"id": 123,
"state": "{$request.query.state}",
"reason": "string",
"changedDate": "2020-02-10"
}
],
"statusCode": "200"
}
}
The 'priority' property in a rule dictates when rule should be run, relative to other rules. Higher priority rules are run before lower priority rules. Rules with the same priority are run in parallel. Priority must be a positive, non-zero integer.
Example:
Consider the following example, observe the priority values. So rule which compares the amount value will be executed first and then the other rule will be executed. So if the amount is equal to 50, then we will get FIXED_CALLBACK event in first place, and in other cases we get MOCK_CALLBACK.
[
{
"ruleId": 1,
"priority": 2,
"description": "If the transfer amount is equal to 50 USD, send a fixed callback",
"conditions": {
"all": [
{
"fact": "path",
"operator": "equal",
"value": "/transfers"
},
{
"fact": "method",
"operator": "equal",
"value": "post"
},
{
"fact": "body",
"operator": "equal",
"value": "50",
"path": "amount.amount"
}
]
},
"event": {
"type": "FIXED_CALLBACK",
"params": {
...Some fixed callback
}
}
},
{
"ruleId": 2,
"priority": 1,
"description": "Send a mock callback for all the remaining transfer requests",
"conditions": {
"all": [
{
"fact": "path",
"operator": "equal",
"value": "/transfers"
},
{
"fact": "method",
"operator": "equal",
"value": "post"
}
]
},
"event": {
"type": "MOCK_CALLBACK",
"params": {
}
}
},