imposter icon indicating copy to clipboard operation
imposter copied to clipboard

soap "light" mode -- or -- xpath without checking value

Open hilsonp opened this issue 2 years ago • 2 comments
trafficstars

Hello,

I olmost nailed it and we finish our implementation for SOAP mocks.

I really like the way imposter can generate a mock from openapi or wsdl contracts. This is absolutely clever !

Nevertheless, our use case is that we mock apis and web services of providers. The contract quality is too different from one provider to another hence we never base our mocks on the provider contract. Instead we exclusively make imposter respond based on files (json/xml) that we maintain, groovy, templates or a mix of everything ;-) The most important being that we do NOT use their contract other than for documentation purposes.

This being said:

REST

For REST mocks, it is easy, we use the rest plugin (and not the OpenAPI plugin) and fully qualify the resources matching as well as the responses in the imposter-config.yaml.

SOAP

For SOAP thought, the soap plugin requires the contract which forces us to copy/maintain a version of the provider contract. This is something I wanted to avoid. Then, I though I would mock a SOAP as any HTTP service, matching the operation with an xpath. Instead of giving the contract, I only need to declare system.xmlNamespaces

Here is an example of soap message I need to match:

<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/" xmlns:op="http://example.com/services/imposterTest/messages/v1">
   <env:Header/>
   <env:Body>
      <op:operationOne>
         <truc>much</truc>
      </op:operationOne>
   </env:Body>
</env:Envelope>

The way I send it is: curl -v --request POST --header "Content-Type: text/xml;charset=UTF-8" --data '<env:Envelope xmlns:env="http://schemas.xmlsoap.org/soap/envelope/" xmlns:op="http://example.com/services/imposterTest/messages/v1"><env:Header/><env:Body> <op:operationOne><truc>much</truc></op:operationOne></env:Body></env:Envelope>' http://localhost:8080/

Here is an imposter-config.yaml that matches it (although it does not let me achieve my goal yet).

plugin: rest 
#basePath: /imposter/test/v1 # Put here what the basePath found in the provider contrat as: soap:address location

resources:
  - method: POST
    path: "/"
    requestBody:
      xPath: "/env:Envelope/env:Body/op:operationOne/truc"
      value: "much"
    response:
      statusCode: 202
      content: "Yahoo we got it"

system:
  xmlNamespaces:
    env: "http://schemas.xmlsoap.org/soap/envelope/"
    op:  "http://example.com/services/imposterTest/messages/v1"

The log is the following and I received the 202 with "Yahoo we got it":

C:\Users\ME\Documents\imposter>imposter.exe up prov-soa-csesd-imposterTest-v1\mock
time="2023-06-15T23:45:44+02:00" level=debug msg="engine binary '3.23.0' already present"
Picked up JAVA_TOOL_OPTIONS: "-Dvertx.cacheDirBase=C:\Temp\vertx-cache"
WARNING: sun.reflect.Reflection.getCallerClass is not supported. This will impact performance.
23:45:50 INFO  i.g.i.Imposter - Starting mock engine 3.23.0
23:45:50 DEBUG i.g.i.c.u.ConfigUtil - Loading configuration file: ConfigReference(file=C:\Users\ME\Documents\imposter\prov-soa-csesd-imposterTest-v1\mock\imposter-config.yaml, configRoot=C:\Users\ME\Documents\imposter\prov-soa-csesd-imposterTest-v1\mock)
23:45:52 DEBUG i.g.i.p.PluginManager - Loaded 7 plugin(s): [js-detector, store-detector, meta-detector, js-nashorn-standalone, store-inmem, config-detector, rest]
23:45:53 DEBUG i.g.i.p.r.RestPluginImpl - Adding handler: POST -> /
23:45:53 INFO  i.g.i.Imposter - Mock engine up and running on http://localhost:8080
time="2023-06-15T23:45:53+02:00" level=info msg="watching for changes to: C:\\Users\\ME\\Documents\\imposter\\prov-soa-csesd-imposterTest-v1\\mock"
23:50:12 DEBUG i.g.i.h.AbstractResourceMatcher - Matched resource config for POST http://localhost:8080/
23:50:12 INFO  i.g.i.p.r.RestPluginImpl - Handling object request for: POST http://localhost:8080/
23:50:12 INFO  i.g.i.s.ResponseServiceImpl - Serving response data (15 bytes) for POST http://localhost:8080/ with status code 202

I do not want to match /env:Envelope/env:Body/op:operationOne/truc but only check that /env:Envelope/env:Body/op:operationOne exists.

Then I tried:

plugin: rest 

resources:
  - method: POST
    path: "/"
    requestBody:
      xPath: "string(boolean(/env:Envelope/env:Body/op:operationOne))"
      value: "true"
    response:
      statusCode: 202
      content: "Yahoo we got it"

system:
  xmlNamespaces:
    env: "http://schemas.xmlsoap.org/soap/envelope/"
    op:  "http://example.com/services/imposterTest/messages/v1"

But the reply is a 200 (maybe same as https://github.com/outofcoffee/imposter/issues/395) with no content:

< HTTP/1.1 200 OK
< X-Imposter-Request: d09170ef-f084-4daf-aeac-8dd4b6a01085
< Server: imposter
< content-length: 0
<

and the log shows:

23:56:16 DEBUG i.g.i.p.r.RestPluginImpl - Adding handler: POST -> /
23:56:16 INFO  i.g.i.Imposter - Mock engine up and running on http://localhost:8080
23:56:37 INFO  i.g.i.p.r.RestPluginImpl - Handling object request for: POST http://localhost:8080/
23:56:37 WARN  i.g.i.s.ResponseServiceImpl - Response file and data are blank for [d09170ef-f084-4daf-aeac-8dd4b6a01085] POST http://localhost:8080/
23:56:37 DEBUG i.g.i.s.ResponseServiceImpl - Returning empty response for [d09170ef-f084-4daf-aeac-8dd4b6a01085] POST http://localhost:8080/

I also tried just giving the xPath and not giving a value (as I only want to check that the operationOne element exists):

plugin: rest 

resources:
  - method: POST
    path: "/"
    requestBody:
      xPath: "/env:Envelope/env:Body/op:operationOne"
    response:
      statusCode: 202
      content: "Yahoo we got it"

system:
  xmlNamespaces:
    env: "http://schemas.xmlsoap.org/soap/envelope/"
    op:  "http://example.com/services/imposterTest/messages/v1"

The same 200 with empty response happen.

Ultimately

The first (minimalist) thing I tried was this I removed the basePath because it seemed to introduce some strange behaviour) This also failed because it would only allow GET (and not POST)

plugin: rest 
basePath: /imposter/test/v1 # Put here what the basePath found in the provider contrat as: soap:address location

resources:
  - requestBody:
      xPath: "/env:Envelope/env:Body/op:operationOne"
    response:
      statusCode: 202
      content: "Yahoo we got it"

system:
  xmlNamespaces:
    env: "http://schemas.xmlsoap.org/soap/envelope/"
    op:  "http://example.com/services/imposterTest/messages/v1"

The dream

Since, as far as I know, the SOAP operation is always the first element under the /envelope/body, I would love being able to:

  • not provide the wsdl,
  • provide the namespaces for the envelope and the operation
  • declare resources using 'operation' and the name of the SOAP operation
  • be able to declare a basePath
  • ?? not having imposter listening by default on GET / ?? (or maybe it is to allow giving the status or the help html ?)

Like this:

plugin: soaplight
basePath: /imposter/test/v1 # Put here the basePath found in the provider contrat as: soap:address location
xmlNamespaces:
  env: "http://schemas.xmlsoap.org/soap/envelope/"
  op:  "http://example.com/services/imposterTest/messages/v1"

resources:
  - operation: operationOne
    response:
      statusCode: 202
      content: "Yahoo we got it"

Hoping you can find a working equivalent to "string(boolean(/env:Envelope/env:Body/op:operationOne))". Hoping you the soaplight is inspiring ;-)

Best regards.

hilsonp avatar Jun 15 '23 22:06 hilsonp

Hi @hilsonp, thank you for describing this.

One thing to try is to set the operator in the XPath matcher, for example:

resources:
  - method: POST
    path: "/something"
    requestBody:
      xPath: "/env:Envelope/env:Body/op:operationOne"
      operator: Exists
    response:
      statusCode: 202
      content: "Yahoo we got it"

More details added here: https://docs.imposter.sh/request_matching/#body-match-operators

Edit: fixed name of property - thanks!

outofcoffee avatar Jun 17 '23 12:06 outofcoffee

Hello @outofcoffee

Thank you. It works ! Note: in your last comment, you made a typo: It is operator and not operation ;-)

Here is what we will use imposter to expose SOAP mocks without needing to store the contract (unless you make a 'light' mode for the 'soap' plugin ;-) )

plugin: rest 
basePath: /imposter/test/v1 # Put here what the basePath found in the provider contrat as: soap:address location
system:
  xmlNamespaces:
    soap: "http://schemas.xmlsoap.org/soap/envelope/"
    operation:  "http://example.com/services/imposterTest/messages/v1"

resources:
  - method: POST
    requestBody:
      xPath: "/soap:Envelope/soap:Body/operation:operationOne"
      operator: Exists
    response:
      statusCode: 202
      content: "Yahoo we got it"

I'm sure you got my point but just in case, the same mock may be declared in a soap "light" mode (by specifying a wsdlNamespaces instead of wsdlFile) with this minimalist and clean config:

plugin: soap 
basePath: /imposter/test/v1
wsdlNamespaces:
    soap: "http://schemas.xmlsoap.org/soap/envelope/"
    operation:  "http://example.com/services/imposterTest/messages/v1"

resources:
  - operation: operationOne
    response:
      statusCode: 202
      content: "Yahoo we got it"

hilsonp avatar Jun 18 '23 09:06 hilsonp