From 8bd4ce8b424dae86d1e9fe90c15199f86c2dc618 Mon Sep 17 00:00:00 2001 From: DinoChiesa Date: Fri, 6 Dec 2024 23:57:35 +0000 Subject: [PATCH] feat: check for policies attached after RaiseFault --- README.md | 1 + lib/package/Endpoint.js | 2 +- lib/package/Flow.js | 4 +- ...8-unreachable-policies-after-raisefault.js | 175 ++++++++++++++++++ .../policies/AM-CleanResponseHeaders.xml | 11 ++ .../apiproxy/policies/AM-Extra-Header.xml | 7 + .../policies/AM-InjectProxyVersionHeader.xml | 7 + .../apiproxy/policies/AM-Response.xml | 9 + .../policies/JS-RemoveCopiedHeaders.xml | 13 ++ .../policies/RF-Error-Not-Enabled.xml | 9 + .../apiproxy/policies/RF-Error.xml | 9 + .../apiproxy/policies/RF-Invalid-Request.xml | 10 + .../apiproxy/proxies/endpoint1.xml | 98 ++++++++++ .../apiproxy/proxies/endpoint2.xml | 98 ++++++++++ .../apiproxy/unreached-policies.xml | 5 + .../sharedflowbundle/Shared-Flow-1.xml | 22 +++ .../sharedflowbundle/policies/AM-Header-1.xml | 7 + .../sharedflowbundle/policies/AM-Header-2.xml | 7 + .../policies/RF-Unsupported.xml | 10 + .../sharedflows/sf-default.xml | 13 ++ test/specs/ST008-test.js | 104 +++++++++++ 21 files changed, 619 insertions(+), 2 deletions(-) create mode 100644 lib/package/plugins/ST008-unreachable-policies-after-raisefault.js create mode 100644 test/fixtures/resources/ST008-unreached-policies-after-raisefault/apiproxy/policies/AM-CleanResponseHeaders.xml create mode 100644 test/fixtures/resources/ST008-unreached-policies-after-raisefault/apiproxy/policies/AM-Extra-Header.xml create mode 100644 test/fixtures/resources/ST008-unreached-policies-after-raisefault/apiproxy/policies/AM-InjectProxyVersionHeader.xml create mode 100644 test/fixtures/resources/ST008-unreached-policies-after-raisefault/apiproxy/policies/AM-Response.xml create mode 100644 test/fixtures/resources/ST008-unreached-policies-after-raisefault/apiproxy/policies/JS-RemoveCopiedHeaders.xml create mode 100644 test/fixtures/resources/ST008-unreached-policies-after-raisefault/apiproxy/policies/RF-Error-Not-Enabled.xml create mode 100644 test/fixtures/resources/ST008-unreached-policies-after-raisefault/apiproxy/policies/RF-Error.xml create mode 100644 test/fixtures/resources/ST008-unreached-policies-after-raisefault/apiproxy/policies/RF-Invalid-Request.xml create mode 100644 test/fixtures/resources/ST008-unreached-policies-after-raisefault/apiproxy/proxies/endpoint1.xml create mode 100644 test/fixtures/resources/ST008-unreached-policies-after-raisefault/apiproxy/proxies/endpoint2.xml create mode 100644 test/fixtures/resources/ST008-unreached-policies-after-raisefault/apiproxy/unreached-policies.xml create mode 100644 test/fixtures/resources/ST008-unreached-policies-after-raisefault/sharedflowbundle/Shared-Flow-1.xml create mode 100644 test/fixtures/resources/ST008-unreached-policies-after-raisefault/sharedflowbundle/policies/AM-Header-1.xml create mode 100644 test/fixtures/resources/ST008-unreached-policies-after-raisefault/sharedflowbundle/policies/AM-Header-2.xml create mode 100644 test/fixtures/resources/ST008-unreached-policies-after-raisefault/sharedflowbundle/policies/RF-Unsupported.xml create mode 100644 test/fixtures/resources/ST008-unreached-policies-after-raisefault/sharedflowbundle/sharedflows/sf-default.xml create mode 100644 test/specs/ST008-test.js diff --git a/README.md b/README.md index 1a461e3d..083f6557 100644 --- a/README.md +++ b/README.md @@ -354,6 +354,7 @@ This is the current list: |   |:white_check_mark:| ST005 | Extract Variables Step with FormParam | A check for message content should be performed before policy execution. | |   |:white_check_mark:| ST006 | JSON Threat Protection Step | A check for message content should be performed before policy execution. | |   |:white_check_mark:| ST007 | XML Threat Protection Step | A check for message content should be performed before policy execution. | +|   |:white_check_mark:| ST008 | Unreachable policies | Policies should not be attached after RaiseFault policies. | | Policy |   |   |   |   | |   |:white_check_mark:| PO006 | Policy Name & filename agreement | Policy name attribute should coincide with the policy filename. | |   |:white_check_mark:| PO007 | Policy Naming Conventions - type indication | It is recommended that the policy name use a prefix or follow a pattern that indicates the policy type. | diff --git a/lib/package/Endpoint.js b/lib/package/Endpoint.js index 7afe25f1..7ac13a68 100644 --- a/lib/package/Endpoint.js +++ b/lib/package/Endpoint.js @@ -250,7 +250,7 @@ Endpoint.prototype.getAllFlows = function () { } allFlows.push.apply(allFlows, this.getFlows()); - this.allFlows = allFlows; + this.allFlows = allFlows.filter(e => !!e); } debug("< getAllFlows()"); return this.allFlows; diff --git a/lib/package/Flow.js b/lib/package/Flow.js index fb7fd8fe..1f276751 100644 --- a/lib/package/Flow.js +++ b/lib/package/Flow.js @@ -105,15 +105,17 @@ Flow.prototype.getFlowResponse = function () { const n = xpath.select("./Response", this.element); this.flowResponse = n && n[0] && new FlowPhase(n[0], this); } - debug(`< getFlowResponse`); + debug(`< getFlowResponse ${this.flowResponse}`); return this.flowResponse; }; Flow.prototype.getSharedFlow = function () { + debug(`> getSharedFlow: ${this.getFlowName()}`); if (this.sharedflow == null) { const n = xpath.select("/SharedFlow", this.element); this.sharedflow = n && n[0] && new FlowPhase(n[0], this); } + debug(`< getSharedFlow (${this.sharedFlow})`); return this.sharedflow; }; diff --git a/lib/package/plugins/ST008-unreachable-policies-after-raisefault.js b/lib/package/plugins/ST008-unreachable-policies-after-raisefault.js new file mode 100644 index 00000000..7800150e --- /dev/null +++ b/lib/package/plugins/ST008-unreachable-policies-after-raisefault.js @@ -0,0 +1,175 @@ +/* + Copyright 2019-2024 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +const ruleId = require("../lintUtil.js").getRuleId(), + debug = require("debug")("apigeelint:" + ruleId), + util = require("util"), + xpath = require("xpath"); + +const plugin = { + ruleId, + name: "Unreachable Steps after RaiseFault", + message: "Steps in the same flow after a RaiseFault cannot be reached.", + fatal: false, + severity: 1, //1=warning, 2=error + nodeType: "Step", + enabled: true, +}; + +let bundle = null; + +class PolicyFlowChecker { + constructor(plugin, debug) { + debug(`PolicyFlowChecker ctor`); + this.plugin = plugin; + this.debug = debug; + } + + checkSequence(stepParent, entity) { + let flagged = false; + const allSteps = stepParent.getSteps(); + // get the array of indexes + const isUnconditionalRaiseFaults = allSteps.map((step) => { + if (step.getCondition()) return false; + let referredPolicy = bundle + .getPolicies() + .find((p) => p.getSteps().find((s) => s == step)); + if (!referredPolicy || referredPolicy.getType() !== "RaiseFault") + return false; + let enabled = referredPolicy.select("@enabled"); + enabled = enabled && enabled[0]; + if (!enabled || enabled == "true") return true; + return false; + }); + + // just look at the FIRST one, skip all subsequent unconditional RF + isUnconditionalRaiseFaults.find((found, ix) => { + if (found) { + const step = allSteps[ix]; + debug( + `found an attached, unconditional RaiseFault policy, line ${step.getElement().lineNumber}`, + ); + if (ix < allSteps.length - 1) { + debug(`not last in flow`); + allSteps.slice(ix + 1).forEach((unreachableStep) => { + flagged = true; + entity.addMessage({ + plugin: checker.plugin, + source: unreachableStep.getElement().toString(), + line: unreachableStep.getElement().lineNumber, + column: unreachableStep.getElement().columnNumber, + message: `Step ${unreachableStep.getName()} is attached after a RaiseFault, cannot be reached.`, + }); + }); + } + return true; + } + return false; + }); + return flagged; + } + + check(sequence) { + let flagged = false; + try { + let debug = this.debug; + let bundlePolicies = bundle.getPolicies(); + let checker = this; + let sequenceType = sequence.constructor.name; + debug(`thingType: ${sequenceType}`); + if (sequenceType == "Flow") { + const flow = sequence; + debug(`flow.name: ${flow.name}`); + + ["FlowRequest", "FlowResponse", "SharedFlow"].forEach((m) => { + const method = `get${m}`; + debug(`calling flow.${method}()`); + const flowPhase = flow[method](); + if (flowPhase) { + if (this.checkSequence(flowPhase, flow)) { + flagged = true; + } + } + }); + } else { + if (this.checkSequence(sequence, sequence.getParent())) { + flagged = true; + } + } + } catch (exc1) { + flagged = true; + sequence.addMessage({ + plugin: checker.plugin, + message: exc1.message + exc1.stack, + }); + } + + return flagged; + } +} + +const checker = new PolicyFlowChecker(plugin, debug); + +const checkEndpoint = function (endpoint, cb) { + let flagged = false; + try { + endpoint.getAllFlows().forEach((sequence, ix) => { + if (checker.check(sequence)) { + flagged = true; + } + }); + endpoint.getFaultRules().forEach((sequence) => { + if (checker.check(sequence)) { + flagged = true; + } + }); + if (endpoint.getDefaultFaultRule()) { + if (checker.check(endpoint.getDefaultFaultRule())) { + flagged = true; + } + } + } catch (exc1) { + endpoint.addMessage({ + plugin: checker.plugin, + message: exc1.message + " " + exc1.stack, + }); + } + + if (typeof cb == "function") { + cb(null, flagged); + } +}; + +const onProxyEndpoint = function (endpoint, cb) { + debug("onProxyEndpoint"); + checkEndpoint(endpoint, cb); +}; + +const onTargetEndpoint = function (endpoint, cb) { + debug("onTargetEndpoint"); + checkEndpoint(endpoint, cb); +}; + +const onBundle = function (b, cb) { + bundle = b; +}; + +module.exports = { + plugin, + onBundle, + onProxyEndpoint, + onTargetEndpoint, +}; diff --git a/test/fixtures/resources/ST008-unreached-policies-after-raisefault/apiproxy/policies/AM-CleanResponseHeaders.xml b/test/fixtures/resources/ST008-unreached-policies-after-raisefault/apiproxy/policies/AM-CleanResponseHeaders.xml new file mode 100644 index 00000000..dafd4ff9 --- /dev/null +++ b/test/fixtures/resources/ST008-unreached-policies-after-raisefault/apiproxy/policies/AM-CleanResponseHeaders.xml @@ -0,0 +1,11 @@ + + + + + + false + diff --git a/test/fixtures/resources/ST008-unreached-policies-after-raisefault/apiproxy/policies/AM-Extra-Header.xml b/test/fixtures/resources/ST008-unreached-policies-after-raisefault/apiproxy/policies/AM-Extra-Header.xml new file mode 100644 index 00000000..3c6b8279 --- /dev/null +++ b/test/fixtures/resources/ST008-unreached-policies-after-raisefault/apiproxy/policies/AM-Extra-Header.xml @@ -0,0 +1,7 @@ + + + +
{messageid}
+
+
+
diff --git a/test/fixtures/resources/ST008-unreached-policies-after-raisefault/apiproxy/policies/AM-InjectProxyVersionHeader.xml b/test/fixtures/resources/ST008-unreached-policies-after-raisefault/apiproxy/policies/AM-InjectProxyVersionHeader.xml new file mode 100644 index 00000000..b87c5ed0 --- /dev/null +++ b/test/fixtures/resources/ST008-unreached-policies-after-raisefault/apiproxy/policies/AM-InjectProxyVersionHeader.xml @@ -0,0 +1,7 @@ + + + +
{apiproxy.name} r{apiproxy.revision}
+
+
+
diff --git a/test/fixtures/resources/ST008-unreached-policies-after-raisefault/apiproxy/policies/AM-Response.xml b/test/fixtures/resources/ST008-unreached-policies-after-raisefault/apiproxy/policies/AM-Response.xml new file mode 100644 index 00000000..8f54f542 --- /dev/null +++ b/test/fixtures/resources/ST008-unreached-policies-after-raisefault/apiproxy/policies/AM-Response.xml @@ -0,0 +1,9 @@ + + + +request verb: {request.verb} + + 200 + OK + + diff --git a/test/fixtures/resources/ST008-unreached-policies-after-raisefault/apiproxy/policies/JS-RemoveCopiedHeaders.xml b/test/fixtures/resources/ST008-unreached-policies-after-raisefault/apiproxy/policies/JS-RemoveCopiedHeaders.xml new file mode 100644 index 00000000..736162de --- /dev/null +++ b/test/fixtures/resources/ST008-unreached-policies-after-raisefault/apiproxy/policies/JS-RemoveCopiedHeaders.xml @@ -0,0 +1,13 @@ + + + +var names = String(context.getVariable('request.headers.names')); +names = names.substring(1, names.length-1); +names.split(',').forEach(function(name) { + context.removeVariable('response.header.' + name.trim()); +}); + + diff --git a/test/fixtures/resources/ST008-unreached-policies-after-raisefault/apiproxy/policies/RF-Error-Not-Enabled.xml b/test/fixtures/resources/ST008-unreached-policies-after-raisefault/apiproxy/policies/RF-Error-Not-Enabled.xml new file mode 100644 index 00000000..ee720b26 --- /dev/null +++ b/test/fixtures/resources/ST008-unreached-policies-after-raisefault/apiproxy/policies/RF-Error-Not-Enabled.xml @@ -0,0 +1,9 @@ + + + + Error + 500 + + + true + diff --git a/test/fixtures/resources/ST008-unreached-policies-after-raisefault/apiproxy/policies/RF-Error.xml b/test/fixtures/resources/ST008-unreached-policies-after-raisefault/apiproxy/policies/RF-Error.xml new file mode 100644 index 00000000..5996f800 --- /dev/null +++ b/test/fixtures/resources/ST008-unreached-policies-after-raisefault/apiproxy/policies/RF-Error.xml @@ -0,0 +1,9 @@ + + + + Error + 500 + + + true + diff --git a/test/fixtures/resources/ST008-unreached-policies-after-raisefault/apiproxy/policies/RF-Invalid-Request.xml b/test/fixtures/resources/ST008-unreached-policies-after-raisefault/apiproxy/policies/RF-Invalid-Request.xml new file mode 100644 index 00000000..49a864a5 --- /dev/null +++ b/test/fixtures/resources/ST008-unreached-policies-after-raisefault/apiproxy/policies/RF-Invalid-Request.xml @@ -0,0 +1,10 @@ + + + + Bad Request + 400 + Bad Request + + + true + diff --git a/test/fixtures/resources/ST008-unreached-policies-after-raisefault/apiproxy/proxies/endpoint1.xml b/test/fixtures/resources/ST008-unreached-policies-after-raisefault/apiproxy/proxies/endpoint1.xml new file mode 100644 index 00000000..83641e26 --- /dev/null +++ b/test/fixtures/resources/ST008-unreached-policies-after-raisefault/apiproxy/proxies/endpoint1.xml @@ -0,0 +1,98 @@ + + + /unreached-policies + + + + + AM-InjectProxyVersionHeader + + true + + + + + + RF-Error + + + + AM-CleanResponseHeaders + + + AM-Extra-Header + + conditional_statement + + + + + + + + + + AM-CleanResponseHeaders + + + JS-RemoveCopiedHeaders + + + AM-InjectProxyVersionHeader + + + + + + + a contrived response + + + + + proxy.pathsuffix MatchesPath "/f1" + + + + possibly a contrived response, possibly a fault + + + request.header.accept = "*/*" + RF-Invalid-Request + + + + AM-Extra-Header + + + + proxy.pathsuffix MatchesPath "/f2" + + + + unknown request - raise a fault + + + RF-Invalid-Request + + + + AM-Extra-Header + + + + + + + + + + + AM-Response + request.verb != "OPTIONS" + + + + + + diff --git a/test/fixtures/resources/ST008-unreached-policies-after-raisefault/apiproxy/proxies/endpoint2.xml b/test/fixtures/resources/ST008-unreached-policies-after-raisefault/apiproxy/proxies/endpoint2.xml new file mode 100644 index 00000000..7912b5b5 --- /dev/null +++ b/test/fixtures/resources/ST008-unreached-policies-after-raisefault/apiproxy/proxies/endpoint2.xml @@ -0,0 +1,98 @@ + + + /unreached-policies + + + + + AM-InjectProxyVersionHeader + + true + + + + + + RF-Error-Not-Enabled + + + + AM-CleanResponseHeaders + + + AM-Extra-Header + + conditional_statement + + + + + + + + + + AM-CleanResponseHeaders + + + JS-RemoveCopiedHeaders + + + AM-InjectProxyVersionHeader + + + + + + + a contrived response + + + + + proxy.pathsuffix MatchesPath "/f1" + + + + possibly a contrived response, possibly a fault + + + request.header.accept = "*/*" + RF-Invalid-Request + + + + AM-Extra-Header + + + + proxy.pathsuffix MatchesPath "/f2" + + + + unknown request - raise a fault + + + RF-Invalid-Request + + + + AM-Extra-Header + + + + + + + + + + + AM-Response + request.verb != "OPTIONS" + + + + + + diff --git a/test/fixtures/resources/ST008-unreached-policies-after-raisefault/apiproxy/unreached-policies.xml b/test/fixtures/resources/ST008-unreached-policies-after-raisefault/apiproxy/unreached-policies.xml new file mode 100644 index 00000000..3e10127e --- /dev/null +++ b/test/fixtures/resources/ST008-unreached-policies-after-raisefault/apiproxy/unreached-policies.xml @@ -0,0 +1,5 @@ + + 1621009025657 + 1621009262854 + /unreached-policies + diff --git a/test/fixtures/resources/ST008-unreached-policies-after-raisefault/sharedflowbundle/Shared-Flow-1.xml b/test/fixtures/resources/ST008-unreached-policies-after-raisefault/sharedflowbundle/Shared-Flow-1.xml new file mode 100644 index 00000000..577126f3 --- /dev/null +++ b/test/fixtures/resources/ST008-unreached-policies-after-raisefault/sharedflowbundle/Shared-Flow-1.xml @@ -0,0 +1,22 @@ + + + + 1486431487439 + dchiesa@google.com + + Shared-Flow-1 + 1486431673737 + dchiesa@google.com + + JavaScript-1 + + + jsc://JavaScript-1.js + + + SharedFlow + false + + default + + diff --git a/test/fixtures/resources/ST008-unreached-policies-after-raisefault/sharedflowbundle/policies/AM-Header-1.xml b/test/fixtures/resources/ST008-unreached-policies-after-raisefault/sharedflowbundle/policies/AM-Header-1.xml new file mode 100644 index 00000000..3c36aba5 --- /dev/null +++ b/test/fixtures/resources/ST008-unreached-policies-after-raisefault/sharedflowbundle/policies/AM-Header-1.xml @@ -0,0 +1,7 @@ + + + +
{messageid}
+
+
+
diff --git a/test/fixtures/resources/ST008-unreached-policies-after-raisefault/sharedflowbundle/policies/AM-Header-2.xml b/test/fixtures/resources/ST008-unreached-policies-after-raisefault/sharedflowbundle/policies/AM-Header-2.xml new file mode 100644 index 00000000..e0f90fcc --- /dev/null +++ b/test/fixtures/resources/ST008-unreached-policies-after-raisefault/sharedflowbundle/policies/AM-Header-2.xml @@ -0,0 +1,7 @@ + + + +
{messageid}
+
+
+
diff --git a/test/fixtures/resources/ST008-unreached-policies-after-raisefault/sharedflowbundle/policies/RF-Unsupported.xml b/test/fixtures/resources/ST008-unreached-policies-after-raisefault/sharedflowbundle/policies/RF-Unsupported.xml new file mode 100644 index 00000000..f64a1408 --- /dev/null +++ b/test/fixtures/resources/ST008-unreached-policies-after-raisefault/sharedflowbundle/policies/RF-Unsupported.xml @@ -0,0 +1,10 @@ + + + + Bad Request + 400 + Bad Request + + + true + diff --git a/test/fixtures/resources/ST008-unreached-policies-after-raisefault/sharedflowbundle/sharedflows/sf-default.xml b/test/fixtures/resources/ST008-unreached-policies-after-raisefault/sharedflowbundle/sharedflows/sf-default.xml new file mode 100644 index 00000000..a3d3f318 --- /dev/null +++ b/test/fixtures/resources/ST008-unreached-policies-after-raisefault/sharedflowbundle/sharedflows/sf-default.xml @@ -0,0 +1,13 @@ + + + + AM-Header-1 + + + RF-Unsupported + + + AM-Header-2 + + + diff --git a/test/specs/ST008-test.js b/test/specs/ST008-test.js new file mode 100644 index 00000000..88d12959 --- /dev/null +++ b/test/specs/ST008-test.js @@ -0,0 +1,104 @@ +/* + Copyright 2019-2024 Google LLC + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +/* global describe, it */ + +const assert = require("assert"), + ruleId = 'ST008', + path = require("path"), + debug = require("debug")(`apigeelint:${ruleId}-test`), + util = require("util"), + bl = require("../../lib/package/bundleLinter.js"); + +const expectedMessageRe = + new RegExp("^Step [-A-Za-z0-9_]{2,} is attached after a RaiseFault,.+$"); + + +describe(`${ruleId} - unreachable policies`, () => { + function check(fileBasename, bundleType, expected) { + let configuration = { + debug: true, + source: { + type: "filesystem", + path: path.resolve(__dirname, `../fixtures/resources/ST008-unreached-policies-after-raisefault/${bundleType}`), + bundleType + }, + excluded: {}, + setExitCode: false, + output: () => {} // suppress output + }; + + bl.lint(configuration, (bundle) => { + let items = bundle.getReport(); + assert.ok(items); + assert.ok(items.length); + debug(util.format(items)); + items = items.filter( m => m.filePath.endsWith(fileBasename)); + debug(util.format(items)); + assert.equal(items.length, 1, `expected:1`); + items.forEach( item => + debug(util.format(item.messages))); + let st008Messages = items[0].messages.filter( m => m.ruleId == ruleId); + debug(util.format(st008Messages)); + assert.equal(st008Messages.length, expected.length); + + expected.forEach( (item, ix) => { + assert.equal(st008Messages[ix].line, item.line, `case(${ix}) line`); + assert.equal(st008Messages[ix].column, item.column, `case(${ix}) column`); + assert.equal(st008Messages[ix].severity, 1, `case(${ix}) severity`); + assert.ok(st008Messages[ix].message.match(expectedMessageRe), `case(${ix}) message ${st008Messages[ix].message}`); + }); + }); + } + + it('should find the expected warnings in an apiproxy', () => { + let expected = [ + { + line: 19, + column: 7 + }, + { + line: 22, + column: 7 + }, + { + line: 79, + column: 9 + } + ]; + + check('endpoint1.xml', 'apiproxy', expected); + }); + + it('should not find warnings when the RaiseFault policy is not enabled', () => { + let expected = [ + { + line: 79, + column: 9 + } + ]; + + check('endpoint2.xml', 'apiproxy', expected); + }); + + it('should find the expected warnings in a sharedflow', () => { + let expected = [ + { line: 9, column: 3 } + ]; + check('sf-default.xml', 'sharedflowbundle', expected); + }); + +});