/* * Copyright (C) 2015 * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.cleverbus.component.externalcall; import static org.hamcrest.CoreMatchers.equalTo; import static org.hamcrest.CoreMatchers.instanceOf; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertThat; import static org.junit.Assert.fail; import static org.junit.Assume.assumeThat; import static org.junit.Assume.assumeTrue; import java.io.IOException; import java.util.Arrays; import java.util.HashMap; import java.util.LinkedList; import java.util.Map; import java.util.Queue; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import javax.annotation.Nullable; import org.cleverbus.api.asynch.AsynchConstants; import org.cleverbus.api.entity.ExternalCall; import org.cleverbus.api.entity.ExternalCallStateEnum; import org.cleverbus.api.entity.Message; import org.cleverbus.api.exception.IntegrationException; import org.cleverbus.api.exception.InternalErrorEnum; import org.cleverbus.api.exception.LockFailureException; import org.cleverbus.api.extcall.ExtCallComponentParams; import org.cleverbus.common.log.Log; import org.cleverbus.component.AbstractComponentsDbTest; import org.cleverbus.core.common.asynch.AsynchMessageRoute; import org.cleverbus.core.common.dao.ExternalCallDao; import org.cleverbus.test.ActiveRoutes; import org.cleverbus.test.ExternalSystemTestEnum; import org.cleverbus.test.ServiceTestEnum; import org.apache.camel.EndpointInject; import org.apache.camel.Exchange; import org.apache.camel.Processor; import org.apache.camel.Produce; import org.apache.camel.ProducerTemplate; import org.apache.camel.component.mock.MockEndpoint; import org.apache.commons.lang.exception.ExceptionUtils; import org.junit.Before; import org.junit.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; /** * Test suite for {@link ExternalCallComponent}. */ @ActiveRoutes(classes = {AsynchMessageRoute.class}) public class ExternalCallComponentTest extends AbstractComponentsDbTest { private static final String REQUEST_XML = " <cus:setCustomerRequest xmlns=\"http://cleverbus.org/ws/Customer-v1\"" + " xmlns:cus=\"http://cleverbus.org/ws/CustomerService-v1\">" + " <cus:customer>" + " <externalCustomerID>12</externalCustomerID>" + " <customerNo>23</customerNo>" + " <customerTypeID>2</customerTypeID>" + " <lastName>Juza</lastName>" + " <firstName>Petr</firstName>" + " </cus:customer>" + " </cus:setCustomerRequest>"; @Autowired private ExternalCallDao externalCallDao; @Produce private ProducerTemplate producer; @EndpointInject(uri = "mock:test") private MockEndpoint mockEndpoint; private Long extCallId; @Value("${asynch.externalCall.skipUriPattern}") private String skipUriPattern; @Before public void resetExtCallId() { extCallId = null; } @Test public void testExternalCallOK() throws Exception { final Message msg = messages(1)[0]; // mock response and in-process asserts mockEndpoint.whenAnyExchangeReceived( recordCallIdAndAnswer("external call reply body", "mock:test", "ok123456")); // send message mockEndpoint.expectedMessageCount(1); String reply = requestViaExternalCall(msg, "mock:test", "ok123456", "external call original body"); mockEndpoint.assertIsSatisfied(); // verify result assertEquals("external call reply body", reply); // check the call is now in DB as OK and with the correct timestamp assertExtCallStateInDB(extCallId, ExternalCallStateEnum.OK, msg); } @Test public void testExternalCallObjectEntityIdOK() throws Exception { final Message msg = messages(1)[0]; // mock response and in-process asserts mockEndpoint.whenAnyExchangeReceived(recordCallIdAndAnswer("external call reply body", "mock:test", "CRM_" + msg.getCorrelationId() + "_[CustomEntityId]:123654")); // send message mockEndpoint.expectedMessageCount(1); String reply = requestViaExternalCall(msg, ExternalCallKeyType.MESSAGE, "mock:test", new CustomEntityId("123654"), "external call original body"); mockEndpoint.assertIsSatisfied(); // verify result assertEquals("external call reply body", reply); // check the call is now in DB as OK and with the correct timestamp assertExtCallStateInDB(extCallId, ExternalCallStateEnum.OK, msg); } @Test public void testExternalCallFailedIOException() throws Exception { Message msg = messages(1)[0]; // simulate failure mockEndpoint.whenAnyExchangeReceived(recordCallIdAndThrow( new IOException("test exception to simulate failure"), "mock:test", "fail123456")); // send message mockEndpoint.expectedMessageCount(1); try { requestViaExternalCall(msg, "mock:test", "fail123456", "external call original body"); fail("Should've gotten an exception by now"); } catch (Exception exc) { // verify failure assertThat(exc, is(instanceOf(IOException.class))); } mockEndpoint.assertIsSatisfied(); // check the call is now in DB as FAILED and with the correct timestamp assertExtCallStateInDB(extCallId, ExternalCallStateEnum.FAILED, msg); } @Test public void testExternalCallFailedIntegrationException() throws Exception { Message msg = messages(1)[0]; // simulate failure mockEndpoint.whenAnyExchangeReceived(recordCallIdAndThrow( new IntegrationException(InternalErrorEnum.E100, "test exception to simulate failure"), "mock:test", "fail123456")); // send message mockEndpoint.expectedMessageCount(1); try { requestViaExternalCall(msg, "mock:test", "fail123456", "external call original body"); fail("Should've gotten an exception by now"); } catch (Exception exc) { // verify failure assertThat(exc, is(instanceOf(IntegrationException.class))); } mockEndpoint.assertIsSatisfied(); // check the call is now in DB as FAILED and with the correct timestamp assertExtCallStateInDB(extCallId, ExternalCallStateEnum.FAILED, msg); } @Test public void testExternalCallDuplicate() throws Exception { Message msg = messages(1)[0]; mockEndpoint.whenAnyExchangeReceived(recordCallIdAndAnswer("external call reply body", "mock:test", "dupe123456")); // send first message mockEndpoint.expectedMessageCount(1); String reply = requestViaExternalCall(msg, "mock:test", "dupe123456", "external call original body"); mockEndpoint.assertIsSatisfied(); // verify result assertEquals("external call reply body", reply); // 1st reply is as expected // check the call is now in DB as OK and with the correct timestamp ExternalCallStateEnum state = ExternalCallStateEnum.OK; Long callId = extCallId; assertExtCallStateInDB(callId, state, msg); // send a duplicate message reply = requestViaExternalCall(msg, "mock:test", "dupe123456", "external call original body"); mockEndpoint.assertIsSatisfied(); assertEquals("external call original body", reply); // 2nd returned original body unchanged -- no call, no reply // check the call is in DB completely unchanged assertExtCallStateInDB(extCallId, ExternalCallStateEnum.OK, msg); } @Test public void testExternalCallDuplicateFail() throws Exception { Message msg = messages(1)[0]; mockEndpoint.whenExchangeReceived(1, recordCallIdAndThrow( new IOException("Simulated Failure for Testing Duplicate of Failure"), "mock:test", "dupeFail123456")); mockEndpoint.whenExchangeReceived(2, recordCallIdAndAnswer("external call reply body", "mock:test", "dupeFail123456")); // send first message mockEndpoint.expectedMessageCount(1); try { requestViaExternalCall(msg, "mock:test", "dupeFail123456", "external call original body"); // verify result fail("Should've failed due to exception by now"); } catch (Exception exc) { // verify failure assertThat(exc, is(instanceOf(IOException.class))); } mockEndpoint.assertIsSatisfied(); // check the call is now in DB as OK and with the correct timestamp assertExtCallStateInDB(extCallId, ExternalCallStateEnum.FAILED, msg); // send a duplicate message mockEndpoint.expectedMessageCount(2); String reply = requestViaExternalCall(msg, "mock:test", "dupeFail123456", "external call original body"); mockEndpoint.assertIsSatisfied(); assertEquals("external call reply body", reply); // 2nd returned reply, since it was called again after 1st fail // check the call is in DB now as OK assertExtCallStateInDB(extCallId, ExternalCallStateEnum.OK, msg); } @Test public void testExternalCallNewer() throws Exception { Message[] msg = messages(2); assumeTrue("For this test the 2nd message must be older", msg[0].getMsgTimestamp().compareTo(msg[1].getMsgTimestamp()) < 0); mockEndpoint.whenAnyExchangeReceived( recordCallIdAndAnswer("external call reply body", "mock:test", "twiceKey")); // send 1st message mockEndpoint.expectedMessageCount(1); String reply = requestViaExternalCall(msg[0], "mock:test", "twiceKey", "external call original body"); mockEndpoint.assertIsSatisfied(); // verify result assertEquals("external call reply body", reply); // 1st reply is as expected // check the call is now in DB as OK and with the correct timestamp assertExtCallStateInDB(extCallId, ExternalCallStateEnum.OK, msg[0]); // send 2nd message mockEndpoint.expectedMessageCount(2); reply = requestViaExternalCall(msg[1], "mock:test", "twiceKey", "external call original body 2"); mockEndpoint.assertIsSatisfied(); // verify the call worked, since the 2nd message is newer assertEquals("external call reply body", reply); // 2nd reply is as expected too // check the call is in DB with the new timestamp assertExtCallStateInDB(extCallId, ExternalCallStateEnum.OK, msg[1]); } @Test public void testExternalCallOlder() throws Exception { Message[] msg = messages(2); assumeTrue("For this test the 2nd message must be older", msg[0].getMsgTimestamp().compareTo(msg[1].getMsgTimestamp()) <= 0); mockEndpoint.whenAnyExchangeReceived(recordCallIdAndAnswer("external call reply body", "mock:test", "older123456")); // send 2nd message - reverse order mockEndpoint.expectedMessageCount(1); String reply = requestViaExternalCall(msg[1], "mock:test", "older123456", "external call original body"); mockEndpoint.assertIsSatisfied(); // verify result assertEquals("external call reply body", reply); // 1st reply is as expected // check the call is now in DB as OK and with the correct msg1 NEWER timestamp assertExtCallStateInDB(extCallId, ExternalCallStateEnum.OK, msg[1]); // send 1st message - reverse order reply = requestViaExternalCall(msg[0], "mock:test", "older123456", "external call original body 2"); mockEndpoint.assertIsSatisfied(); // verify the 2nd call was skipped, since the 2nd message is older assertEquals("external call original body 2", reply); // check the call is now in DB as OK, still with msg1 NEWER timestamp assertExtCallStateInDB(extCallId, ExternalCallStateEnum.OK, msg[1]); } @Test public void testExternalCallOlderFail() throws Exception { Message[] msg = messages(2); assumeTrue("For this test the 2nd message must be newer", msg[0].getMsgTimestamp().compareTo(msg[1].getMsgTimestamp()) <= 0); mockEndpoint.whenExchangeReceived(1, recordCallIdAndThrow(new IOException("Simulated External Call Failure"), "mock:test", "olderFail123456")); mockEndpoint.whenExchangeReceived(2, recordCallIdAndAnswer("external call reply body", "mock:test", "olderFail123456")); // send 2nd message - reverse order mockEndpoint.expectedMessageCount(1); try { requestViaExternalCall(msg[1], "mock:test", "olderFail123456", "external call original body"); fail("Should've failed due to an exception by now"); } catch (Exception exc) { // verify failure assertThat(exc, is(instanceOf(IOException.class))); } mockEndpoint.assertIsSatisfied(); // check the call is now in DB as FAILED and with the correct msg1 NEWER timestamp assertExtCallStateInDB(extCallId, ExternalCallStateEnum.FAILED, msg[1]); // send 1st message - reverse order String reply = requestViaExternalCall(msg[0], "mock:test", "olderFail123456", "external call original body 2"); mockEndpoint.assertIsSatisfied(); // verify the 2nd call was NOT made, since the 1st failed call is newer assertEquals("external call original body 2", reply); // check the call remains unchanged assertExtCallStateInDB(extCallId, ExternalCallStateEnum.FAILED, msg[1]); } @Test public void testExternalCallOlderFailBetween() throws Exception { Message[] msg = messages(3); assumeTrue("For this test the 2nd message must be newer than 1st", msg[0].getMsgTimestamp().compareTo(msg[1].getMsgTimestamp()) <= 0); assumeTrue("For this test the 3nd message must be newest - newer than 1st and 2nd", msg[1].getMsgTimestamp().compareTo(msg[2].getMsgTimestamp()) <= 0); mockEndpoint.whenExchangeReceived(1, recordCallIdAndThrow(new IOException("Simulated External Call Failure"), "mock:test", "olderFailBetween123456")); mockEndpoint.whenExchangeReceived(2, recordCallIdAndAnswer("external call reply body", "mock:test", "olderFailBetween123456")); // send 2nd message to fail, but trigger external call update mockEndpoint.expectedMessageCount(1); try { requestViaExternalCall(msg[1], "mock:test", "olderFailBetween123456", "external call original body"); fail("Should've failed due to an exception by now"); } catch (Exception exc) { // verify failure assertThat(exc, is(instanceOf(IOException.class))); } mockEndpoint.assertIsSatisfied(); // check the call is now in DB as FAILED and with the correct msg1 NEWER timestamp assertExtCallStateInDB(extCallId, ExternalCallStateEnum.FAILED, msg[1]); // send 1st (oldest) message - should be ignored mockEndpoint.expectedMessageCount(1); String reply = requestViaExternalCall(msg[0], "mock:test", "olderFailBetween123456", "external call original body 2"); mockEndpoint.assertIsSatisfied(); // verify the 2nd call was NOT made, since the 1st failed call is newer assertEquals("external call original body 2", reply); // check the call remains unchanged assertExtCallStateInDB(extCallId, ExternalCallStateEnum.FAILED, msg[1]); // send 3rd (latest) message - should be allowed mockEndpoint.expectedMessageCount(2); reply = requestViaExternalCall(msg[2], "mock:test", "olderFailBetween123456", "external call original body 3"); mockEndpoint.assertIsSatisfied(); // verify the 3rd call was made, since it's the newest call assertEquals("external call reply body", reply); // check the call remains unchanged assertExtCallStateInDB(extCallId, ExternalCallStateEnum.OK, msg[2]); } @Test public void testExternalCallSkipUriPattern() throws Exception { // for this test the following must be set in application0.cfg: // asynch.externalCall.skipUriPattern = mock:(//)?ignoreTestEndpointUri.* assumeThat(skipUriPattern, equalTo("mock:(//)?ignoreTestEndpointUri.*")); final Message msg = messages(1)[0]; MockEndpoint mockIgnore = getCamelContext().getEndpoint("mock://ignoreTestEndpointUri_something", MockEndpoint.class); mockIgnore.expectedMessageCount(0); // send message String reply = requestViaExternalCall(msg, "mock:ignoreTestEndpointUri_something", "skip123456", "external call original body"); mockIgnore.assertIsSatisfied(); // verify result assertEquals("external call original body", reply); } @Test(timeout = 60000) public void testExternalCallLockFailure() throws Exception { final int messageCount = 21; // how many messages total should be sent final int batchSize = 3; // how many messages should be sent together with verifications after each batch final int responseDelay = 10; // delay before mock responds, in millis boolean lockFailureEncountered = false; // test is pointless if it was never encountered Message[] messages = messages(messageCount); // set up mock to reply and to verify that the external call is reused mockEndpoint.whenAnyExchangeReceived(recordCallIdAndAnswer("external call reply body", responseDelay, "mock:test", "concurrentKey")); for (int batchStart = 0; batchStart < messages.length; batchStart += batchSize) { Message[] batch = Arrays.copyOfRange(messages, batchStart, Math.min(messages.length, batchStart + batchSize)); lockFailureEncountered |= sendAndVerifyBatch(batch); } assumeTrue("This test is pointless if lock failure is never encountered", lockFailureEncountered); } private boolean sendAndVerifyBatch(Message[] messages) throws Exception { boolean lockFailureEncountered = false; HashMap<Message, Future<String>> replies = new HashMap<Message, Future<String>>(); // send messages that have no reply, resend messages that have LockFailureException instead of a reply // verify results and re-send failures - test has timeout set because this is potentially endless Queue<Message> unverifiedMessages = new LinkedList<Message>(Arrays.asList(messages)); while (!unverifiedMessages.isEmpty()) { Message message = unverifiedMessages.poll(); boolean replyAvailable = replies.containsKey(message); if (replyAvailable) { Future<String> reply = replies.get(message); try { reply.get(); // this will throw an exception if it occurred during processing } catch (Exception exc) { if (ExceptionUtils.indexOfType(exc, LockFailureException.class) != -1) { // expected cause - this test verifies that this scenario happens and is handled properly lockFailureEncountered = true; replyAvailable = false; // mark reply unavailable to resend the original message } else { // fail by rethrowing Log.error("Unexpected failure for message {} --", message, exc); throw exc; } } } if (!replyAvailable) { unverifiedMessages.add(message); // mark message as still unverified replies.put(message, requestViaExternalCallAsync(message, "mock:test", "concurrentKey", "external call original body")); } } // check the call is now in DB as OK and with the correct LAST msg timestamp assertExtCallStateInDB(extCallId, ExternalCallStateEnum.OK, messages[messages.length - 1]); return lockFailureEncountered; } private Future<String> requestViaExternalCallAsync(final Message msg, final String targetURI, final Object key, final String body) throws Exception { return getStringBodyFuture(producer.asyncSend("extcall:custom:" + targetURI, prepareExternalCallProcessor(msg, key, body))); } private String requestViaExternalCall(final Message msg, final String targetURI, final Object key, final String body) throws Exception { return requestViaExternalCall(msg, ExternalCallKeyType.CUSTOM, targetURI, key, body); } private String requestViaExternalCall( final Message msg, @Nullable final ExternalCallKeyType externalCallKeyType, final String targetURI, final Object key, final String body) throws Exception { ExternalCallKeyType keyType = (externalCallKeyType == null) ? ExternalCallKeyType.CUSTOM : externalCallKeyType; Exchange reply = producer.request("extcall:" + keyType.toString() + ":" + targetURI, prepareExternalCallProcessor(msg, key, body)); Exception exc = reply.getProperty(Exchange.EXCEPTION_CAUGHT, Exception.class); if (exc != null) { throw exc; } exc = reply.getException(); if (exc != null) { throw exc; } return reply.hasOut() ? reply.getOut().getBody(String.class) : reply.getIn().getBody(String.class); } private Processor prepareExternalCallProcessor(final Message msg, final Object key, final String body) { return new Processor() { @Override public void process(Exchange exchange) throws Exception { exchange.getIn().setHeaders(headers(msg)); exchange.getIn().setBody(body); exchange.setProperty(ExtCallComponentParams.EXTERNAL_CALL_KEY, key); } }; } /** * Returns a processor that answers with the specified answer body. * The received exchange is verified to have {@link Message} in {@link AsynchConstants#MSG_HEADER}. * <p/> * It also records the external call ID into the instance field {@link #extCallId} * or verifies the new one matches the old one, if one is already recorded. * * @param answerBody the body to set for the exchange IN message * @param operationName * @param entityId * @return processor, e.g. for use with mocks */ private Processor recordCallIdAndAnswer(final Object answerBody, String operationName, String entityId) { return recordCallIdAndAnswer(answerBody, 0, operationName, entityId); } /** * Same as {@link #recordCallIdAndAnswer(Object, String, String)}, but throws an exception instead. * * @param answerException the exception to throw * @param operationName * @param entityId * @return processor, e.g. for use with mocks */ private Processor recordCallIdAndThrow(final Exception answerException, final String operationName, final String entityId) { return new Processor() { @Override public void process(Exchange exchange) throws Exception { verifyAndRecordCallId(exchange, operationName, entityId); throw answerException; } }; } /** * Same as {@link #recordCallIdAndAnswer(Object, String, String)}, but waits for the specified delay before answering. * * @param answerBody the body to set for the exchange IN message * @param responseDelay delay in milliseconds to wait before answering * @param operationName * @param entityId * @return processor, e.g. for use with mocks */ private Processor recordCallIdAndAnswer(final Object answerBody, final int responseDelay, final String operationName, final String entityId) { return new Processor() { @Override public void process(Exchange exchange) throws Exception { verifyAndRecordCallId(exchange, operationName, entityId); //simulate delay, then respond Thread.sleep(responseDelay); exchange.getIn().setBody(answerBody); } }; } private void verifyAndRecordCallId(Exchange exchange, String operationName, String entityId) { Message msg = exchange.getIn().getHeader(AsynchConstants.MSG_HEADER, Message.class); ExternalCall extCall = externalCallDao.getExternalCall(operationName, entityId); Log.info("Processing ExternalCall={} for Message={}", extCall, msg); assertNotNull(extCall); assertEquals(extCall.getState(), ExternalCallStateEnum.PROCESSING); // check it's also in the DB in the correct state assertExtCallStateInDB(extCall.getId(), ExternalCallStateEnum.PROCESSING, msg); Long newExtCallId = extCall.getId(); if (extCallId == null) { extCallId = newExtCallId; } else { assertEquals(extCallId, newExtCallId); } } /** Verifies there's an ExternalCall instance in DB with the specified ID, state and msgId + msgTimestamp. */ private void assertExtCallStateInDB(Long callId, ExternalCallStateEnum state, Message message) { ExternalCall extCall = em.find(ExternalCall.class, callId); assertNotNull(String.format("ExternalCall with ID [%s] doesn't exist in the DB", callId), extCall); assertEquals(String.format("ExternalCall [%s]%ndoesn't have the expected state [%s]", extCall, state), state, extCall.getState()); assertEquals(String.format("ExternalCall [%s]%ndoesn't reference the expected message [%s]", extCall, message), message.getMsgId(), extCall.getMsgId()); assertEquals( String.format( "ExternalCall msgTimestamp [%s] doesn't match expected msgTimestamp [%s] of message [%s]", extCall.getMsgTimestamp(), message.getMsgTimestamp(), message), message.getMsgTimestamp().getTime(), extCall.getMsgTimestamp().getTime()); } private Message[] messages(final int messageCount) throws Exception { return createAndSaveMessages(messageCount, ExternalSystemTestEnum.CRM, ServiceTestEnum.CUSTOMER, "setCustomer", REQUEST_XML); } private Map<String, Object> headers(Message msg) { Map<String, Object> headers = new HashMap<String, Object>(); headers.put(AsynchConstants.MSG_HEADER, msg); headers.put(AsynchConstants.ASYNCH_MSG_HEADER, true); return headers; } private Future<String> getStringBodyFuture(final Future<Exchange> reply) { return new Future<String>() { @Override public boolean cancel(boolean mayInterruptIfRunning) { return reply.cancel(mayInterruptIfRunning); } @Override public boolean isCancelled() { return reply.isCancelled(); } @Override public boolean isDone() { return reply.isDone(); } @Override public String get() throws InterruptedException, ExecutionException { return getReplyString(reply.get()); } @Override public String get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException { return getReplyString(reply.get(timeout, unit)); } private String getReplyString(Exchange exchange) throws InterruptedException, ExecutionException { throwExceptionOptionally(exchange.getProperty(Exchange.EXCEPTION_CAUGHT, Exception.class)); throwExceptionOptionally(exchange.getException()); return exchange.getOut().getBody(String.class); } private void throwExceptionOptionally(Exception exc) throws InterruptedException, ExecutionException { if (exc != null) { if (exc instanceof InterruptedException) { throw (InterruptedException) exc; } else if (exc instanceof ExecutionException) { throw (ExecutionException) exc; } else { throw new ExecutionException(exc); } } } }; } private class CustomEntityId { private String entityId; public CustomEntityId(String entityId) { this.entityId = entityId; } @Override public String toString() { return "[CustomEntityId]:" + entityId; } } }