outofcoffee / imposter

Scriptable, multipurpose mock server. Run standalone mock servers, or embed mocks within your tests.
https://imposter.sh
Other
374 stars 61 forks source link

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

Open hilsonp opened 1 year ago

hilsonp commented 1 year ago

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:

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.

outofcoffee commented 1 year ago

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!

hilsonp commented 1 year ago

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"