0

I'm trying to mock a failure of an outbound gateway in a unit test like this:

MessageHandler mockCognitiveAnalyze =
        mockMessageHandler().handleNextAndReply(m -> ResponseEntity.status(HttpStatus.UNAUTHORIZED).build());
this.mockIntegrationContext.substituteMessageHandlerFor("cognitiveServicesReadEndpoint", mockCognitiveAnalyze);

What I would expect is that when the cognitiveServicesReadEndpoint is invoked, it won't succeed and the flow should be finished (or in my case, a retry advice is applied so it should be repeated). But what happens is, no exception or error will be thrown and the next handle is invoked. The flow looks like this:

.handle(Http.outboundGateway(cognitiveServicesUri + "/vision/v3.0/read/analyze")
                .mappedRequestHeaders("Ocp-Apim-Subscription-Key")
                .mappedResponseHeaders("Operation-Location"),
        c -> c.advice(this.advices.retryAdvice())
                .id("cognitiveServicesReadEndpoint"))
.transform(p -> "")
.handle(Http.outboundGateway(h -> h.getHeaders().get("Operation-Location"))
        .mappedRequestHeaders("Ocp-Apim-Subscription-Key")
        .httpMethod(HttpMethod.GET)
        .expectedResponseType(String.class), this.advices.ocrSpec())

Any idea how can I setup the mock handler to properly throw an exception?

Side note: .transform(p -> "") is needed for proper handling of the headers, see Spring Integration HTTP Outbound Gateway header not forwarder on a consecutive request

UPDATE1

This is how the handler looks at the moment:

.handle(Http.outboundGateway(h -> String.format("%s%s", uri, h.getHeaders()
        .get(CustomHeaders.DOCUMENT_ID.name())))
        .httpMethod(HttpMethod.GET)
        .expectedResponseType(byte[].class), c -> c.advice(this.advices.retryAdvice())
        .id("endpoint1"))
.wireTap(sf -> sf.enrichHeaders(h -> h.header("ocp-apim-subscription-key", computerVisionApiKey))
        .handle(Http.outboundGateway(cognitiveServicesUri + "/vision/v3.0/read/analyze")
                        .mappedRequestHeaders("Ocp-Apim-Subscription-Key")
                        .mappedResponseHeaders("Operation-Location"),
                c -> c.advice(this.advices.retryAdvice())
                        .id("cognitiveServicesReadEndpoint"))

And the testing code:

MessageHandler mockCognitiveAnalyze = mockMessageHandler().handleNext(m -> {
    throw new HttpClientErrorException(HttpStatus.UNAUTHORIZED);
});
this.mockIntegrationContext.substituteMessageHandlerFor("cognitiveServicesReadEndpoint",
        mockCognitiveAnalyze);
Blink
  • 1,408
  • 13
  • 21

2 Answers2

2

The MockMessageHandler.handleNextAndReply() provides a Function which is invoked in the handleMessageInternal(). Since there is no interaction with the Resttemplate (you fully substitute it), your HttpStatus.UNAUTHORIZED is not translated into an exception.

You probably should simulate a RestTemplate logic on the matter in that your handleNextAndReply() and throw respective exception instead.

The RestTemplate has the logic like this:

protected void handleResponse(URI url, HttpMethod method, ClientHttpResponse response) throws IOException {
    ResponseErrorHandler errorHandler = getErrorHandler();
    boolean hasError = errorHandler.hasError(response);
    if (logger.isDebugEnabled()) {
        try {
            int code = response.getRawStatusCode();
            HttpStatus status = HttpStatus.resolve(code);
            logger.debug("Response " + (status != null ? status : code));
        }
        catch (IOException ex) {
            // ignore
        }
    }
    if (hasError) {
        errorHandler.handleError(url, method, response);
    }
}

So, you need to borrow and idea from the errorHandler which is a DefaultResponseErrorHandler and can throw respective exception from the provided ClientHttpResponse. Or you just can go ahead and throw an HttpClientErrorException.Unauthorized directly.

UPDATE

Something like this:

MockMessageHandler mockMessageHandler = mockMessageHandler();
mockMessageHandler.handleNext(message -> { 
         throw new HttpClientErrorException.create(...);
});

this.mockIntegrationContext.substituteMessageHandlerFor("httpGateway", mockMessageHandler);
Artem Bilan
  • 113,505
  • 11
  • 91
  • 118
  • I ended up using the mockserver... I wasn't sure how to exactly implement what you suggested – Blink Sep 25 '20 at 11:59
  • Ok my below answer still doesn't work completely... it seems that the mockserver has to be injected somehow differently into Spring Integration as it currently doesn't intercept the API calls... Could you please elaborate on your suggestion with an example? Or maybe you know why the mockserver isn't working properly? – Blink Sep 25 '20 at 12:19
  • What is that `restTemplate`? Do you inject it into those `Http.outboundGateway()`? How do you expect mock server will intercept `RestTemplate` operations if you don't use that `restTemplate`? – Artem Bilan Sep 25 '20 at 13:25
  • And also see an UPDATE in my answer. – Artem Bilan Sep 25 '20 at 13:28
  • I'm autowiring the resttemplate and providing it to the mock server. I thought that way it can do some magic and intercept it as the outbound gateway uses a resttemplate as well – Blink Sep 26 '20 at 19:16
  • Did you post your update in the end? I can't find it – Blink Sep 26 '20 at 19:18
  • I added sample for `handleNextAndReply()` impl into my answer. What I meant about `restTemplate` injection is exactly about that `Http.outboundGateway()`, not your test environment. If you don't inject `RestTemplate` into a gateway, the one is created internally and, of course, you can't autowire it into a test class for further mocking. You need to be explicit and use the same `RestTemplate` not only in the test class, but in the gateway the behavior of which you would like to mock. – Artem Bilan Sep 28 '20 at 15:35
  • I get it now. Actually I've tried that but as I was getting compilation errors I went the other direction which wasn't so smart ;) The think was you can't throw the Unauthorized as it's a private constructor... So just do that instead throw new HttpClientErrorException(HttpStatus.UNAUTHORIZED) – Blink Sep 29 '20 at 09:14
  • Ok after running my test with the "throw new" within handleNextAndReply, the test will fail exactly at the line where the exception is thrown so it's not throwing the exception in the integration flow but already within the execution of the test... I've tried wrapping it within try/catch but it didn't change anything... Is it correct behaviour or did I miss something important? – Blink Sep 29 '20 at 09:26
  • You probably don't use a `substituteMessageHandlerFor()` properly. See what I've changed in my UPDATE in the answer. – Artem Bilan Sep 29 '20 at 15:06
  • Thanks Artem but it looks to me exactly the same as I have... I've posted the code in my question as UPDATE1. Could you please take a look? Have I missed anything? :( Is it because of the wiretap? – Blink Sep 29 '20 at 15:24
  • No, not related. Probably you have something else in your flow which you don't show. Or your `this.advices.retryAdvice()` just doesn't retry that exception and of course it is thrown back to the caller - your test method. – Artem Bilan Sep 29 '20 at 15:36
  • I have removed the retryAdvice and still the same problem. I have exactly the same test but instead of throwing exception I'm just returning some message and everything works as expected. There is really nothing more in the flow except what you see. I think I'm giving up... – Blink Sep 30 '20 at 08:02
  • May be you can share a minimal project on GH to let me to take a look? – Artem Bilan Sep 30 '20 at 14:14
  • Sure, here it is: https://github.com/nadworny/spring-integration-tests – Blink Oct 01 '20 at 09:38
  • OK. My bad: the feature you want to achieve doesn't work (yet) - we just don't copy advices into a `MockMessageHandler`. Feel free to raise a GH issue and we will consider what could be possible in the Testing Framework to fulfill the gap! – Artem Bilan Oct 01 '20 at 14:55
  • As a work around I'd suggest just to send message from the `handleNext()` to the `recoveryChannel`. Not sure though what you do with that `errorChannel`. You flow is event-driver and only you can initiate its action, so `errorChannel` is not involved anywhere in your flow. – Artem Bilan Oct 01 '20 at 14:57
  • Thanks I'll try that. I'm using error channel to set a status of processed document to "failed" whenever an error occurs. – Blink Oct 01 '20 at 18:43
1

This is a final solution:

MessageHandler mockCognitiveAnalyze = mockMessageHandler().handleNextAndReply(m -> {
    throw new HttpClientErrorException(HttpStatus.UNAUTHORIZED);
});
Blink
  • 1,408
  • 13
  • 21