bbyars / mountebank

Over the wire test doubles
http://www.mbtest.org
MIT License
2k stars 266 forks source link

Allow composing multiple matches to one response #463

Open demus-nine opened 5 years ago

demus-nine commented 5 years ago

Assume you have set up predicates that each matches one of a list of numbers (fx, PINs). Assume the expected response contains a block of xml or json for each matched result, but only for the matched values. As I understand it, the first matching predicate wins, so the only way I see to do this is to create mocks with a predicates in the right order that matches all the expected combinations or with javascript.

I suggest to add a factility that allows all predicates to be evaluated and their respective outputs to be concatenated with a header and footer block.

bbyars commented 5 years ago

Thanks for the suggestion, but I'm struggling to imagine what that would look like. Do you have an example imposter JSON in mind you can use to explain the idea a bit more? -Brandon

On Mon, Oct 7, 2019 at 3:54 AM Daniel Demus notifications@github.com wrote:

Assume you have set up predicates that match one of a list of numbers (fx, PINs). Assume the expected response contains a block of xml or json for each matched result, but only for each matched value. As I understand it, the first matching predicate wins, sot the only way I see to do this is to create mocks with a predicates in the right order that matches all the expected combinations or with javascript.

I suggest to add a factility that allows all predicates to be evaluated and their respective outputs to be concatenated with a header and footer block.

— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/bbyars/mountebank/issues/463?email_source=notifications&email_token=AAARFP7GFZ4LQBJVS3JO7XDQNL2L5A5CNFSM4I6BSAL2YY3PNVWWK3TUL52HS4DFUVEXG43VMWVGG33NNVSW45C7NFSM4HP77WFA, or mute the thread https://github.com/notifications/unsubscribe-auth/AAARFP5WADTOSDPNA7ZXRJTQNL2L5ANCNFSM4I6BSALQ .

demus-nine commented 4 years ago

Let's say your request body is something like

req
  - searchid: 1111
  - searchid: 3333
  - searchid: 4444
/req

I am not using json or xml to avoid being refered to the xpath and jsonpath predicates. The predicate is

"predicates": [
        {
          "or": [
            { "contains": { "- searchid: 1111" } },
            { "contains": { "- searchid: 2222" } }
            { "contains": { "- searchid: 3333" } }
          ]
        }
]

Let's say the return value is supposed to be xml:

<response>
   <somemetadata>1245</somemetadata>
   <results>
      <result>
        <name>Dummy<name/>
        <id>1111</id>
      </result>
      <result>
        <name>Dummy<name/>
        <id>3333</id>
      </result>
   <results>
</response>

Notice that the result element needs to be repeated for every match substituting part of the matched text. So there is a header, some variable number of repeating blocks and a footer. I couldn't find any easy way to implement this other than implementing it in javascript, and I think this isn't an unusual pattern. I would suggest something like:

"responses": {
   "header": "<response>\n   <somemetadata>1245</somemetadata>\n   <results>",
   "footer": "   <results>\n</response>",
   "eachMatch": {
     "body": "      <result>\n        <name>Dummy<name/>\n        <id>${ID}</id>\n      </result>",
     "copy": {
       "from": "match",
       "into": "${ID}",
       "method": "regex",
       "selector": "<id>(\d+)</id>"
     }
}

The "from" could also just default to the text matched by a given predicate. You could also implement something like this for xpath or jsonpath predicates that match multiple nodes.

demus-nine commented 4 years ago

I can send you the javascript implementation that produces the required result, if that would help.

bbyars commented 4 years ago

Sure!

On Wed, Mar 18, 2020 at 3:35 AM Daniel Demus notifications@github.com wrote:

I can send you the javascript implementation that produces the required result, if that would help.

— You are receiving this because you commented. Reply to this email directly, view it on GitHub https://github.com/bbyars/mountebank/issues/463#issuecomment-600491636, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAARFP5PD2SU6UGQTAN4ADDRICBWHANCNFSM4I6BSALQ .

demus-nine commented 4 years ago

Sorry, I was distracted. Anyway here is an example of what I ended up doing in cucumber/groovy to generate a chunk of json, that could be sent to mountebank, so it could have a chunk of javascript, that could generate a chunk of XML with the correct aggregate response, when the input was a set of search values that contained some matches and some non-matches:

void fakeConditionalCSRPResponse(Map<String, SomeMockData> validValues) {
    List<String> responseBlocks = []
    validValues.eachWithIndex { String validValue, SomeMockData someMockData, Integer index ->
        responseBlocks << "\"${validValue}\": ${JsonOutput.toJson(Responses.blockOfValidResponse(replacementValue1, replacementValue2, replacementValue3))}".toString()
    }

    String injection = """function (request, state, logger) {
    const info = (logger || {}).info || (() => {});
    const xpath = require('xpath');
    const DOMParser = require('xmldom').DOMParser;
    const parser = new DOMParser({
        errorHandler: (level, message) => {
        const warn = (logger || {}).warn || (() => {});
        warn("%s (source: %s)", message, JSON.stringify(request.body));
        }
    });

    const validInputValues = new Set(${validValues.keySet().inspect()});
    const valuesToFind = xpath.select("//*[local-name(.)='ElementToMatchValueOf']", parser.parseFromString(request.body)).map(node => node.textContent);

    const bodyMap = {
        ${responseBlocks.join(',\n    ')}
    };
    let outputForNotFound = "";
    let outputForFound = "";
    for (let foundValue of valuesToFind) {
        if (validInputValues.has(cpr)) {
        outputForFound += bodyMap[cpr];
        } else {
        outputForNotFound += '${
                StringEscapeUtils.escapeJavaScript('''
                        <ns1:Elem1>
                            <ns1:Elem2>
                                <ns1:Elem3>11</ns1:Elem3>
                                <ns1:Text>Fejl i PNR</ns1:Text>
                            </ns1:Elem2>
                        </ns1:Elem1>''')}'
        }
    }

    const output = "${StringEscapeUtils.escapeJavaScript(
    """<soapenv:Envelope xmlns:soapenv="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <soapenv:Body>
        <ns1:WebServiceReponse xmlns="http://fake.io">
        <ns1:Ugh>
            <ns1:Yech>
            <ns1:TransactionId>4251934a-08f4-4d43-8527-2e9578400340.1</ns1:TransactionId>
            <ns1:ServiceId>Service1</ns1:ServiceId>
            <ns1:TransaktionsTime>2015-09-03T14:04:18</ns1:TransaktionsTime>""")}" + outputForNotFound + "${StringEscapeUtils.escapeJavaScript("""
            </ns1:Yech>
        </ns1:Ugh>
        <ns1:ResultList>""")}" + outputForFound + "${StringEscapeUtils.escapeJavaScript("""
        </ns1:ResultList>
        </ns1:WebServiceReponse>
    </soapenv:Body>
    </soapenv:Envelope>""")}";

    info('Result ' + output);
    return {
        headers: { Connection: "close", "Content-Type": "text/xml; charset=UTF-8" },
        statusCode: 200,
        body: output
    };

    }
    """

        String imposterJson =
"""{
    "protocol": "http",
    "port": $MOUNTEBANK_PORT,
    "name": "Example",
    "stubs":
    [
        {
            "responses":
            [
                {
                    "inject": ${JsonOutput.toJson(injection)}
                }
            ]
        }
    ]
}
"""
        return createImposterFromJson(imposterJson)
    }

I hope you read groovy.

konflic commented 3 years ago

Hi, sorry, is it working? Or going to be merged someday?