What are the best practices / recommendations around building complex functionality in karate-netty?
We have a requirement to write a relatively complex mock service to avoid hitting our integration partner in lower regions. We have about 8 endpoints comprising search, lookup, and additional information for later stages of our checkout funnel. We need to do things like for id = 7, return this payload, but for id = 8, return that payload. karate-netty supports all of this very nicely! However, an additional requirement we have is to closely mirror error behavior - eg missing auth header, wrong shape of payload, etc. With this sort of conditional/exception handling, we struggled to find a paradigm within karate-netty that worked for us.
To explain what I mean, imagine an endpoint which requires an auth header, and valid request.foo, and valid request.bar. You might write something like this:
Scenario: pathMatches('ourEndpoint') && methodIs('post') && !headerContains('Auth', 'secret')
* call read('403.feature')
Scenario: pathMatches('ourEndpoint') && methodIs('post') && !request.foo
* call read('422.feature') { missing: 'foo' }
Scenario: pathMatches('ourEndpoint') && methodIs('post') && !request.bar
* call read('422.feature') { missing: 'bar' }
Scenario: pathMatches('ourEndpoint') && methodIs('post') && headerContains('Auth', 'secret') && request.foo && request.bar
* call read('200.feature')
This is fine for small examples like this, but the problem with this approach for complicated endpoints doing a lot of validation is that you have to specify all of your conditions twice - once in the error case and once in the 200 case. This is not so optimal when you have 10 different scenarios for an endpoint - your 200 condition in particular becomes extremely unwieldy. It would be nice if there was something like karate.abort() that instead of aborting a Scenario instead aborted an entire feature - karate.abortFeature() if you will. Then you would only have to specify your error conditions once and simplify the condition for the 200 case to simply handle every request to that endpoint that hasn't errored out before, eg:
Scenario: pathMatches('ourEndpoint') && methodIs('post') && !headerContains('Auth', 'secret')
* call read('403.feature')
* karate.abortFeature()
Scenario: pathMatches('ourEndpoint') && methodIs('post') && !request.foo
* call read('422.feature') { missing: 'foo' }
* karate.abortFeature()
Scenario: pathMatches('ourEndpoint') && methodIs('post') && !request.bar
* call read('422.feature') { missing: 'bar' }
* karate.abortFeature()
Scenario: pathMatches('ourEndpoint') && methodIs('post')
* call read('200.feature')
It would also be nice if there were some way to support filtering at multiple levels; then we could have per-endpoint feature files like so:
Feature: pathMatches('ourEndpoint') && methodIs('post')
Scenario: !headerContains('Auth', 'secret')
* call read('403.feature')
* karate.abortFeature()
Scenario: !request.foo
* call read('422.feature') { missing: 'foo' }
* karate.abortFeature()
Scenario: !request.bar
* call read('422.feature') { missing: 'bar' }
* karate.abortFeature()
Scenario:
* call read('200.feature')
Lacking the above, we went with pulling all the conditionals and exception flows in to a single scenario, something like this:
* def abortWithResponse =
"""
function(responseStatus, response) {
karate.set('response', response);
karate.set('responseStatus', responseStatus);
karate.abort();
}
"""
Scenario: pathMatches('ourEndpoint') && methodIs('post')
* if (!headerContains('Auth', 'secret')) abortWithResponse(403, read('403response.json'));
* if (!request.foo) abortWithResponse(422, build422Response({missing: ['foo']));
* if (!request.bar) abortWithResponse(422, build422Response({missing: ['bar']));
* call read('200.feature')
This works, but feels rather inelegant and hard to read. Is there a different pattern that anyone is using to accomplish these ends? If not, would adding karate.abortFeature() be a useful addition?
Thanks in advance!