/* * Copyright 2014, The Sporting Exchange Limited * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.betfair.cougar.transport.impl.protocol.http.soap; import com.betfair.cougar.api.ExecutionContext; import com.betfair.cougar.api.ResponseCode; import com.betfair.cougar.api.export.Protocol; import com.betfair.cougar.api.security.*; import com.betfair.cougar.core.api.OperationBindingDescriptor; import com.betfair.cougar.core.api.ServiceVersion; import com.betfair.cougar.core.api.ev.ExecutionResult; import com.betfair.cougar.core.api.ev.TimeConstraints; import com.betfair.cougar.core.api.exception.CougarServiceException; import com.betfair.cougar.core.api.exception.ServerFaultCode; import com.betfair.cougar.core.api.fault.FaultController; import com.betfair.cougar.marshalling.impl.databinding.xml.JdkEmbeddedXercesSchemaValidationFailureParser; import com.betfair.cougar.transport.api.CommandResolver; import com.betfair.cougar.transport.api.ExecutionCommand; import com.betfair.cougar.transport.api.TransportCommand; import com.betfair.cougar.transport.api.TransportCommand.CommandStatus; import com.betfair.cougar.transport.api.protocol.http.HttpCommand; import com.betfair.cougar.transport.api.protocol.http.soap.SoapIdentityTokenResolver; import com.betfair.cougar.transport.api.protocol.http.soap.SoapOperationBindingDescriptor; import com.betfair.cougar.transport.api.protocol.http.soap.SoapServiceBindingDescriptor; import com.betfair.cougar.transport.impl.protocol.http.AbstractHttpCommandProcessorTest; import com.betfair.cougar.transport.impl.protocol.http.ContentTypeNormaliser; import junit.framework.AssertionFailedError; import org.apache.axiom.om.OMAttribute; import org.apache.axiom.om.OMElement; import org.apache.axiom.soap.SOAPBody; import org.apache.axiom.soap.SOAPHeader; import org.apache.axiom.soap.impl.builder.StAXSOAPModelBuilder; import org.joda.time.format.DateTimeFormatter; import org.joda.time.format.ISODateTimeFormat; import org.junit.Before; import org.junit.Test; import org.mockito.ArgumentMatcher; import javax.servlet.ServletInputStream; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.ws.rs.core.MediaType; import javax.xml.XMLConstants; import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLStreamReader; import javax.xml.transform.stream.StreamSource; import javax.xml.validation.SchemaFactory; import java.io.IOException; import java.security.cert.X509Certificate; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import static org.junit.Assert.*; import static org.junit.Assert.assertEquals; import static org.mockito.Matchers.any; import static org.mockito.Mockito.*; public class SoapTransportCommandProcessorTest extends AbstractHttpCommandProcessorTest<OMElement> { public static final String AZ = "Azerbaijan"; private static final String soapEnvStart = "<?xml version='1.0' encoding='utf-8'?><soapenv:Envelope xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\">"; private static final String soapEnvFinish = "</soapenv:Envelope>"; private static final String soapHeaderStart = "<soapenv:Header>"; private static final String soapHeaderFinish = "</soapenv:Header>"; private static final String soapBodyStart = "<soapenv:Body>"; private static final String soapBodyFinish = "</soapenv:Body>"; private static final String soapFaultStart = "<soapenv:Fault>"; private static final String soapFaultFinish = "</soapenv:Fault>"; private static final String nullSoapBody = "<soapenv:Body/>"; private static final String firstOpIn = "<FirstTestOpRequest xmlns=\"http://www.betfair.com/soaptest\"><FirstOpFirstParam>hello</FirstOpFirstParam></FirstTestOpRequest>"; private static final String firstOpInDuplicate = "<FirstTestOpRequest xmlns=\"http://www.betfair.com/soaptest\"><FirstOpFirstParam>hello</FirstOpFirstParam><FirstOpFirstParam>goodbye</FirstOpFirstParam></FirstTestOpRequest>"; private static final String firstOpOut = "<FirstTestOpResponse xmlns=\"http://www.betfair.com/soaptest\"><response>goodbye</response></FirstTestOpResponse>"; private static final String firstOpError = "<faultcode>soapenv:Client</faultcode><faultstring>TestError-123</faultstring>"; private static final String firstOpErrorDetail = "<detail><sptst:TestApplicationException xmlns:sptst=\"http://www.betfair.com/soaptest\"><sptst:TheError>The Error Detail</sptst:TheError></sptst:TestApplicationException></detail>"; private static final String mapOpIn = "<MapOpRequest xmlns=\"http://www.betfair.com/soaptest\"><MapOpFirstParam><entry key=\"1\"><Double>1.0</Double></entry><entry key=\"2\"><Double>2.2</Double></entry><entry key=\"3\"/></MapOpFirstParam></MapOpRequest>"; private static final String mapOpOut = "<MapOpResponse xmlns=\"http://www.betfair.com/soaptest\"><response><entry key=\"1\"><Double>1.0</Double></entry><entry key=\"2\"/><entry key=\"3\"><Double>3.3</Double></entry></response></MapOpResponse>"; private static final String listOpIn = "<ListOpRequest xmlns=\"http://www.betfair.com/soaptest\"><ListOpFirstParam><Date>248556211-09-30T12:12:53.297+01:00</Date><Date>248556211-09-30T12:12:53.297Z</Date></ListOpFirstParam></ListOpRequest>"; private static final String listOpOut = "<ListOpResponse xmlns=\"http://www.betfair.com/soaptest\"><response><Date>248556211-09-30T11:12:53.297Z</Date><Date>248556211-09-30T12:12:53.297Z</Date></response></ListOpResponse>"; private static final String invalidOpIn = "<InvalidOpRequest xmlns=\"http://www.betfair.com/soaptest\"><InvalidOpFirstParam>INVALID</InvalidOpFirstParam></InvalidOpRequest>"; private static final String invalidOpError = "<faultcode>soapenv:Client</faultcode><faultstring>DSC-0044</faultstring><detail />"; private static final String invalidCredentialsError = "<faultcode>soapenv:Client</faultcode><faultstring>DSC-0015</faultstring><detail />"; private static final String voidResponseOpIn = "<VoidResponseRequest xmlns=\"http://www.betfair.com/soaptest\"><VoidReturnOpFirstParam>TEST1</VoidReturnOpFirstParam></VoidResponseRequest>"; private static final String duplicateRequestParamIn = "<VoidResponseRequest xmlns=\"http://www.betfair.com/soaptest\"><VoidReturnOpFirstParam>TEST1</VoidReturnOpFirstParam><VoidReturnOpFirstParam>TEST2</VoidReturnOpFirstParam></VoidResponseRequest>"; private static final String invalidVoidResponseOpIn = "<VoidResponseRequest xmlns=\"http://www.betfair.com/soaptest\"><VoidReturnOpFirstParam>INVALID</VoidReturnOpFirstParam></VoidResponseRequest>"; private static final String invalidVoidResponseOpOut = "<faultcode>soapenv:Client</faultcode><faultstring>DSC-0044</faultstring><detail />"; private static final String externalEntityIn = "<!DOCTYPE foo [<!ELEMENT foo ANY ><!ENTITY xxe1 SYSTEM \"file:///etc/shadow\" >]> "; private static final String externalEntityInWithBody = externalEntityIn+"<VoidResponseRequest xmlns=\"http://www.betfair.com/soaptest\"><VoidReturnOpFirstParam>&foo;</VoidReturnOpFirstParam>"; private static final String invalidInputFault = "<faultcode>soapenv:Client</faultcode><faultstring>DSC-0044</faultstring><detail />"; private static final OperationBindingDescriptor[] operationBindings = new OperationBindingDescriptor[] { new SoapOperationBindingDescriptor(firstOpKey, "FirstTestOpRequest", "FirstTestOpResponse"), new SoapOperationBindingDescriptor(mapOpKey, "MapOpRequest", "MapOpResponse"), new SoapOperationBindingDescriptor(listOpKey, "ListOpRequest", "ListOpResponse"), new SoapOperationBindingDescriptor(invalidOpKey, "InvalidOpRequest", "InvalidOpResponse"), new SoapOperationBindingDescriptor(voidReturnOpKey, "VoidResponseRequest", null)}; private static final SoapServiceBindingDescriptor serviceBinding = new SoapServiceBindingDescriptor() { private ServiceVersion serviceVersion = new ServiceVersion("v1.92"); @Override public String getNamespacePrefix() { return "sptst"; } @Override public String getNamespaceURI() { return "http://www.betfair.com/soaptest"; } @Override public String getServiceContextPath() { return "/myservice/"; } @Override public Protocol getServiceProtocol() { return Protocol.SOAP; } @Override public OperationBindingDescriptor[] getOperationBindings() { return operationBindings; } @Override public ServiceVersion getServiceVersion() { return serviceVersion; } @Override public String getServiceName() { return "TestService"; } @Override public String getSchemaPath() { return "xsd/foo.xsd"; } }; private SoapTransportCommandProcessor soapCommandProcessor; private SoapIdentityTokenResolver identityTokenResolver; private HttpCommand command; @Before public void init() throws Exception { super.init(); soapCommandProcessor = new SoapTransportCommandProcessor(contextResolution, "X-RequestTimeout", new JdkEmbeddedXercesSchemaValidationFailureParser()); init(soapCommandProcessor); ContentTypeNormaliser ctn = mock(ContentTypeNormaliser.class); when(ctn.getNormalisedResponseMediaType(any(HttpServletRequest.class))).thenReturn(MediaType.APPLICATION_XML_TYPE); soapCommandProcessor.setContentTypeNormaliser(ctn); identityTokenResolver = mock(SoapIdentityTokenResolver.class); when(identityTokenResolver.resolve(any(SOAPHeader.class), any(X509Certificate[].class))).thenReturn(new ArrayList<IdentityToken>()); soapCommandProcessor.setValidatorRegistry(validatorRegistry); soapCommandProcessor.bind(serviceBinding); soapCommandProcessor.onCougarStart(); command=super.createCommand(identityTokenResolver, Protocol.SOAP); } @Override protected OMElement isCredentialContainer() { return any(OMElement.class); } @Override protected Protocol getProtocol() { return Protocol.SOAP; } /** * Basic test with string parameters * @throws Exception */ @Test public void testProcess() throws Exception { // Set up the input when(request.getInputStream()).thenReturn( new TestServletInputStream(buildSoapMessage(null, firstOpIn, null, null))); when(request.getScheme()).thenReturn("http"); // Resolve the input command soapCommandProcessor.process(command); assertEquals(1, ev.getInvokedCount()); // Assert that we resolved the expected arguments Object[] args = ev.getArgs(); assertNotNull(args); assertEquals(1, args.length); assertEquals("hello", args[0]); // Assert that the expected result is sent assertNotNull(ev.getObserver()); ev.getObserver().onResult(new ExecutionResult("goodbye")); assertEquals(CommandStatus.Complete, command.getStatus()); assertSoapyEquals(buildSoapMessage(null, firstOpOut, null, null), testOut.getOutput()); verify(response).setContentType(MediaType.TEXT_XML); verify(logger).logAccess(eq(command), isA(ExecutionContext.class), anyLong(), anyLong(), any(MediaType.class), any(MediaType.class), any(ResponseCode.class)); verifyTracerCalls(firstOpKey); } /** * Basic test with string parameters * @throws Exception */ @Test public void testProcessRetrieveCredentials() throws Exception { // Set up the input when(request.getInputStream()).thenReturn(new TestServletInputStream(buildSoapMessage(null, firstOpIn, null, null))); when(request.getScheme()).thenReturn("http"); // Resolve the input command soapCommandProcessor.process(command); assertEquals(1, ev.getInvokedCount()); // Assert that we resolved the expected arguments Object[] args = ev.getArgs(); assertNotNull(args); assertEquals(1, args.length); assertEquals("hello", args[0]); // Assert that the expected result is sent assertNotNull(ev.getObserver()); ev.getObserver().onResult(new ExecutionResult("goodbye")); assertEquals(CommandStatus.Complete, command.getStatus()); assertSoapyEquals(buildSoapMessage(null, firstOpOut, null, null), testOut.getOutput()); verify(response).setContentType(MediaType.TEXT_XML); verify(logger).logAccess(eq(command), isA(ExecutionContext.class), anyLong(), anyLong(), any(MediaType.class), any(MediaType.class), any(ResponseCode.class)); verifyTracerCalls(firstOpKey); } /** * Test for a SOAP RPC call with a void return * @throws Exception */ @Test public void testVoid() throws Exception { // Set up the input when(request.getInputStream()).thenReturn( new TestServletInputStream(buildSoapMessage(null, voidResponseOpIn, null, null))); when(request.getScheme()).thenReturn("http"); // Resolve the input command soapCommandProcessor.process(command); assertEquals(1, ev.getInvokedCount()); // Assert that we resolved the expected arguments Object[] args = ev.getArgs(); assertNotNull(args); assertEquals(1, args.length); assertEquals(TestEnum.TEST1, args[0]); // Assert that the expected result is sent assertNotNull(ev.getObserver()); ev.getObserver().onResult(new ExecutionResult()); assertEquals(CommandStatus.Complete, command.getStatus()); assertSoapyEquals(soapEnvStart+nullSoapBody+soapEnvFinish, testOut.getOutput()); verify(response).setContentType(MediaType.TEXT_XML); verify(logger).logAccess(eq(command), isA(ExecutionContext.class), anyLong(), anyLong(), any(MediaType.class), any(MediaType.class), any(ResponseCode.class)); verifyTracerCalls(voidReturnOpKey); } /** * Tests a void RPC call that throws an exception - we should still get a correctly formed * SOAP error body back * @throws Exception */ @Test public void testProcess_voidWithException() throws Exception { when(request.getInputStream()).thenReturn( new TestServletInputStream(buildSoapMessage(null, invalidVoidResponseOpIn, null, null))); when(request.getScheme()).thenReturn("http"); // Resolve the input command soapCommandProcessor.process(command); assertEquals(CommandStatus.Complete, command.getStatus()); assertSoapyEquals(buildSoapMessage(null, null, invalidVoidResponseOpOut, null), testOut.getOutput()); verify(response).setContentType(MediaType.TEXT_XML); verify(response).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); verify(logger).logAccess(eq(command), isA(ExecutionContext.class), anyLong(), anyLong(), any(MediaType.class), any(MediaType.class), any(ResponseCode.class)); verifyTracerCalls(null); } /** * @throws Exception */ @Test public void testDuplicateRequestParam() throws Exception { // Set up the input when(request.getInputStream()).thenReturn( new TestServletInputStream(buildSoapMessage(null, duplicateRequestParamIn, null, null))); when(request.getScheme()).thenReturn("http"); // Resolve the input command soapCommandProcessor.process(command); assertEquals(1, ev.getInvokedCount()); // Assert that we resolved the expected arguments Object[] args = ev.getArgs(); assertNotNull(args); assertEquals(1, args.length); assertEquals(TestEnum.TEST1, args[0]); // Assert that the expected result is sent assertNotNull(ev.getObserver()); ev.getObserver().onResult(new ExecutionResult()); assertEquals(CommandStatus.Complete, command.getStatus()); assertSoapyEquals(soapEnvStart+nullSoapBody+soapEnvFinish, testOut.getOutput()); verify(response).setContentType(MediaType.TEXT_XML); verify(logger).logAccess(eq(command), isA(ExecutionContext.class), anyLong(), anyLong(), any(MediaType.class), any(MediaType.class), any(ResponseCode.class)); verifyTracerCalls(voidReturnOpKey); } /** * Tests exceptions * @throws Exception */ @Test public void testProcess_OnException() throws Exception { // Set up the input when(request.getInputStream()).thenReturn( new TestServletInputStream(buildSoapMessage(null, firstOpIn, null, null))); when(request.getScheme()).thenReturn("http"); faultMessages = new ArrayList<String[]>(); faultMessages.add(new String[] {"TheError", "The Error Detail"}); // Resolve the input command soapCommandProcessor.process(command); assertEquals(1, ev.getInvokedCount()); assertNotNull(ev.getObserver()); // Assert that the expected exception is sent ev.getObserver().onResult(new ExecutionResult(new CougarServiceException( ServerFaultCode.ServiceCheckedException, "Error in App", new TestApplicationException(ResponseCode.Forbidden, "TestError-123",faultMessages)))); assertEquals(CommandStatus.Complete, command.getStatus()); assertSoapyEquals(buildSoapMessage(null, null, firstOpError, firstOpErrorDetail), testOut.getOutput()); verify(response).setContentType(MediaType.TEXT_XML); verify(response).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); verify(logger).logAccess(eq(command), isA(ExecutionContext.class), anyLong(), anyLong(), any(MediaType.class), any(MediaType.class), any(ResponseCode.class)); verifyTracerCalls(firstOpKey); } /** * Tests the detailed fault reporting for the exception * @throws Exception */ @Test public void testDetailedFaultReporting() throws Exception { FaultController.getInstance().setDetailedFaults(true); try { // Set up the input when(request.getInputStream()).thenReturn( new TestServletInputStream(buildSoapMessage(null, firstOpIn, null, null))); when(request.getScheme()).thenReturn("http"); faultMessages = new ArrayList<String[]>(); faultMessages.add(new String[] {"TheError", "The Error Detail"}); // Resolve the input command soapCommandProcessor.process(command); // Assert that the expected exception is sent TestApplicationException tae = new TestApplicationException(ResponseCode.Forbidden, "TestError-123", faultMessages); ev.getObserver().onResult(new ExecutionResult(new CougarServiceException( ServerFaultCode.ServiceCheckedException, "Error in App", tae))); assertEquals(CommandStatus.Complete, command.getStatus()); String message = testOut.getOutput(); int traceStartPos = message.indexOf("<trace>"); int traceEndPos = message.indexOf("</trace>"); int messageStartPos = message.indexOf("<message>"); int messageEndPos = message.indexOf("</message>"); assertTrue(traceStartPos != -1); assertTrue(traceEndPos != -1); assertTrue(messageStartPos != -1); assertTrue(messageEndPos != -1); assertTrue(traceEndPos > (traceStartPos+7)); assertTrue(messageEndPos > (messageStartPos+9)); verify(logger).logAccess(eq(command), isA(ExecutionContext.class), anyLong(), anyLong(), any(MediaType.class), any(MediaType.class), any(ResponseCode.class)); verifyTracerCalls(firstOpKey); } finally { FaultController.getInstance().setDetailedFaults(false); } } /** * Tests a client error response is sent for invalid input * @throws Exception */ @Test public void testProcess_InvalidInput() throws Exception { when(request.getInputStream()).thenReturn( new TestServletInputStream(buildSoapMessage(null, invalidOpIn, null, null))); when(request.getScheme()).thenReturn("http"); // Resolve the input command soapCommandProcessor.process(command); assertEquals(CommandStatus.Complete, command.getStatus()); assertSoapyEquals(buildSoapMessage(null, null, invalidOpError, null), testOut.getOutput()); verify(response).setContentType(MediaType.TEXT_XML); verify(response).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); verify(logger).logAccess(eq(command), isA(ExecutionContext.class), anyLong(), anyLong(), any(MediaType.class), any(MediaType.class), any(ResponseCode.class)); verifyTracerCalls(null); } /** * Tests Map, Integer and Double parameters in and out * @throws Exception */ @Test public void testProcess_MapIntDouble() throws Exception { // Set up the input when(request.getInputStream()).thenReturn( new TestServletInputStream(buildSoapMessage(null, mapOpIn, null, null))); when(request.getScheme()).thenReturn("http"); // Resolve the input command soapCommandProcessor.process(command); assertEquals(1, ev.getInvokedCount()); // Assert that we resolved the expected arguments Object[] args = ev.getArgs(); assertNotNull(args); assertEquals(1, args.length); Map<Integer, Double> requestMap = (Map<Integer, Double>)args[0]; assertEquals(3, requestMap.size()); Double requestEntry = requestMap.get(1); assertNotNull(requestEntry); assertEquals(1.0, requestEntry, 0.01); requestEntry = requestMap.get(2); assertNotNull(requestEntry); assertEquals(2.2, requestEntry, 0.01); requestEntry = requestMap.get(3); assertNull(requestEntry); // Assert that the expected result is sent assertNotNull(ev.getObserver()); Map<Integer, Double> response = new HashMap<Integer, Double>(); response.put(1, 1.0); response.put(3, 3.3); response.put(2, null); ev.getObserver().onResult(new ExecutionResult(response)); assertEquals(CommandStatus.Complete, command.getStatus()); assertSoapyEquals(buildSoapMessage(null, mapOpOut, null, null), testOut.getOutput()); verify(logger).logAccess(eq(command), isA(ExecutionContext.class), anyLong(), anyLong(), any(MediaType.class), any(MediaType.class), any(ResponseCode.class)); verifyTracerCalls(mapOpKey); } /** * Tests List and Date parameters in and out * @throws Exception */ @Test public void testProcess_ListDate() throws Exception { // Set up the input when(request.getInputStream()).thenReturn( new TestServletInputStream(buildSoapMessage(null, listOpIn, null, null))); when(request.getScheme()).thenReturn("http"); // Resolve the input command soapCommandProcessor.process(command); assertEquals(1, ev.getInvokedCount()); DateTimeFormatter xmlFormat = ISODateTimeFormat.dateTimeParser(); // Assert that we resolved the expected arguments Object[] args = ev.getArgs(); assertNotNull(args); assertEquals(1, args.length); List<Date> listArg = (List<Date>)args[0]; assertEquals(2, listArg.size()); assertEquals(xmlFormat.parseDateTime("248556211-09-30T12:12:53.297+01:00").toDate(), listArg.get(0)); assertEquals(xmlFormat.parseDateTime("248556211-09-30T12:12:53.297Z").toDate(), listArg.get(1)); // Assert that the expected result is sent assertNotNull(ev.getObserver()); List<Date> response = new ArrayList<Date>(); response.add(xmlFormat.parseDateTime("248556211-09-30T12:12:53.297+01:00").toDate()); response.add(xmlFormat.parseDateTime("248556211-09-30T12:12:53.297Z").toDate()); ev.getObserver().onResult(new ExecutionResult(response)); assertEquals(CommandStatus.Complete, command.getStatus()); assertSoapyEquals(buildSoapMessage(null, listOpOut, null, null), testOut.getOutput()); verify(logger).logAccess(eq(command), isA(ExecutionContext.class), anyLong(), anyLong(), any(MediaType.class), any(MediaType.class), any(ResponseCode.class)); verifyTracerCalls(listOpKey); } /** * US53541 * Test for attempted XML Entity Injection * @throws Exception */ @Test public void xmlEntityInjection() throws Exception { when(request.getInputStream()).thenReturn( new TestServletInputStream(buildSoapMessage(null, externalEntityIn, null, null))); when(request.getScheme()).thenReturn("http"); // Resolve the input command soapCommandProcessor.process(command); assertEquals(CommandStatus.Complete, command.getStatus()); assertSoapyEquals(buildSoapMessage(null, null, invalidInputFault, null), testOut.getOutput()); verify(response).setContentType(MediaType.TEXT_XML); verify(response).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); verify(logger).logAccess(eq(command), any(ExecutionContext.class), anyLong(), anyLong(), any(MediaType.class), any(MediaType.class), any(ResponseCode.class)); //verifyTracerCalls(); // todo: #81: put this back } /** * US53541 * Test for attempted XML Entity Injection * @throws Exception */ @Test public void xmlEntityInjectionWithBody() throws Exception { when(request.getInputStream()).thenReturn( new TestServletInputStream(buildSoapMessage(null, externalEntityInWithBody, null, null))); when(request.getScheme()).thenReturn("http"); // Resolve the input command soapCommandProcessor.process(command); assertEquals(CommandStatus.Complete, command.getStatus()); assertSoapyEquals(buildSoapMessage(null, null, invalidInputFault, null), testOut.getOutput()); verify(response).setContentType(MediaType.TEXT_XML); verify(response).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); verify(logger).logAccess(eq(command), any(ExecutionContext.class), anyLong(), anyLong(), any(MediaType.class), any(MediaType.class), any(ResponseCode.class)); //verifyTracerCalls(); // todo: #81: put this back } @Test public void validationDisabled() throws Exception { when(request.getInputStream()).thenReturn( new TestServletInputStream(buildSoapMessage(null, firstOpInDuplicate, null, null))); when(request.getScheme()).thenReturn("http"); soapCommandProcessor.setSchemaValidationEnabled(false); soapCommandProcessor.process(command); ev.getObserver().onResult(new ExecutionResult("goodbye")); assertEquals(CommandStatus.Complete, command.getStatus()); // note we don't check we got the right result // cougar is just non-deterministic if you have validation disabled assertSoapyEquals(buildSoapMessage(null, firstOpOut, null, null), testOut.getOutput()); verify(response).setContentType(MediaType.TEXT_XML); verify(logger).logAccess(eq(command), isA(ExecutionContext.class), anyLong(), anyLong(), any(MediaType.class), any(MediaType.class), any(ResponseCode.class)); verifyTracerCalls(firstOpKey); } @Test public void validationEnabledValidInput() throws Exception { // verify the schema is actually valid before we continue SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); schemaFactory.newSchema(new StreamSource(getClass().getClassLoader().getResourceAsStream("xsd/foo.xsd"))); soapCommandProcessor.setSchemaValidationEnabled(true); testProcess(); } @Test public void validationEnabledInvalidInput() throws Exception { // verify the schema is actually valid before we continue SchemaFactory schemaFactory = SchemaFactory.newInstance(XMLConstants.W3C_XML_SCHEMA_NS_URI); schemaFactory.newSchema(new StreamSource(getClass().getClassLoader().getResourceAsStream("xsd/foo.xsd"))); when(request.getInputStream()).thenReturn( new TestServletInputStream(buildSoapMessage(null, firstOpInDuplicate, null, null))); when(request.getScheme()).thenReturn("http"); soapCommandProcessor.setSchemaValidationEnabled(true); soapCommandProcessor.process(command); assertEquals(CommandStatus.Complete, command.getStatus()); assertSoapyEquals(buildSoapMessage(null, null, invalidInputFault, null), testOut.getOutput()); verify(response).setContentType(MediaType.TEXT_XML); verify(response).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); verify(logger).logAccess(eq(command), any(ExecutionContext.class), anyLong(), anyLong(), any(MediaType.class), any(MediaType.class), any(ResponseCode.class)); //verifyTracerCalls(); // todo: #81: put this back } /** * Tests a client error response is sent for invalid input * @throws Exception */ @Test public void testProcess_IOException() throws Exception { when(request.getScheme()).thenReturn("http"); ServletInputStream is = mock(ServletInputStream.class); when(request.getInputStream()).thenReturn(is); when(is.read()).thenThrow(new IOException("i/o error")); when(is.read((byte[])any())).thenThrow(new IOException("i/o error")); when(is.read((byte[])any(),anyInt(),anyInt())).thenThrow(new IOException("i/o error")); // Resolve the input command soapCommandProcessor.process(command); assertEquals(CommandStatus.Complete, command.getStatus()); assertSoapyEquals(buildSoapMessage(null, null, invalidOpError, null), testOut.getOutput()); verify(response).setContentType(MediaType.TEXT_XML); verify(response).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); verify(logger).logAccess(eq(command), isA(ExecutionContext.class), anyLong(), anyLong(), any(MediaType.class), any(MediaType.class), any(ResponseCode.class)); //verifyTracerCalls(); // todo: #81: put this back } /** * Tests a client error response is sent for invalid input * @throws Exception */ @Test public void testProcess_TooMuchData() throws Exception { when(request.getInputStream()).thenReturn( new TestServletInputStream(buildSoapMessage(null, firstOpIn, null, null))); when(request.getScheme()).thenReturn("http"); soapCommandProcessor.setMaxPostBodyLength(10L); // Resolve the input command soapCommandProcessor.process(command); assertEquals(CommandStatus.Complete, command.getStatus()); assertSoapyEquals(buildSoapMessage(null, null, invalidOpError, null), testOut.getOutput()); verify(response).setContentType(MediaType.TEXT_XML); verify(response).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); verify(logger).logAccess(eq(command), isA(ExecutionContext.class), anyLong(), anyLong(), any(MediaType.class), any(MediaType.class), any(ResponseCode.class)); //verifyTracerCalls(); // todo: #81: put this back } @Test public void createCommandResolver_NoTimeout() throws IOException { // Set up the input when(request.getInputStream()).thenReturn( new TestServletInputStream(buildSoapMessage(null, firstOpIn, null, null))); when(request.getScheme()).thenReturn("http"); // resolve the command CommandResolver<HttpCommand> cr = soapCommandProcessor.createCommandResolver(command, tracer); Iterable<ExecutionCommand> executionCommands = cr.resolveExecutionCommands(); // check the output ExecutionCommand executionCommand = executionCommands.iterator().next(); TimeConstraints constraints = executionCommand.getTimeConstraints(); assertNull(constraints.getExpiryTime()); } @Test public void createCommandResolver_WithTimeout() throws IOException { // Set up the input when(request.getInputStream()).thenReturn( new TestServletInputStream(buildSoapMessage(null, firstOpIn, null, null))); when(request.getScheme()).thenReturn("http"); // resolve the command when(request.getHeader("X-RequestTimeout")).thenReturn("10000"); when(context.getRequestTime()).thenReturn(new Date()); CommandResolver<HttpCommand> cr = soapCommandProcessor.createCommandResolver(command, tracer); Iterable<ExecutionCommand> executionCommands = cr.resolveExecutionCommands(); // check the output ExecutionCommand executionCommand = executionCommands.iterator().next(); TimeConstraints constraints = executionCommand.getTimeConstraints(); assertNotNull(constraints.getExpiryTime()); } @Test public void createCommandResolver_WithTimeoutAndOldRequestTime() throws IOException { // Set up the input when(request.getInputStream()).thenReturn( new TestServletInputStream(buildSoapMessage(null, firstOpIn, null, null))); when(request.getScheme()).thenReturn("http"); // resolve the command when(request.getHeader("X-RequestTimeout")).thenReturn("10000"); when(context.getRequestTime()).thenReturn(new Date(System.currentTimeMillis() - 10001)); CommandResolver<HttpCommand> cr = soapCommandProcessor.createCommandResolver(command, tracer); Iterable<ExecutionCommand> executionCommands = cr.resolveExecutionCommands(); // check the output ExecutionCommand executionCommand = executionCommands.iterator().next(); TimeConstraints constraints = executionCommand.getTimeConstraints(); assertTrue(constraints.getExpiryTime() < System.currentTimeMillis()); } /** * DE5417 * @throws Exception */ @Test public void testCredentialsNotFirstHeaderElement() throws Exception { // Set up the input when(request.getInputStream()).thenReturn( new AbstractHttpCommandProcessorTest.TestServletInputStream("<soapenv:Envelope xmlns:a=\"http://www.w3.org/2005/08/addressing\" xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:sec=\"http://www.betfair.com/security/\" xmlns:usac=\"http://www.betfair.com/servicetypes/v1/USAccount/\">\n" + " <soapenv:Header>\n" + " <a:Action>retrieveAccount</a:Action>\n" + " <sec:Credentials>\n" + " <sec:X-Application>1001</sec:X-Application>\n" + " <sec:X-Authentication>vpIt3Zu38ZWppNBKYnbX7Uhno9zOwAydXvIwknGEdTc=</sec:X-Authentication>\n" + " </sec:Credentials>\n" + " \n" + "\n" + " <a:MessageID>urn:uuid:2c4364e6-81e2-4ade-a507-620fd4d75b06</a:MessageID>\n" + "\n" + " <a:ReplyTo><a:Address>http://www.w3.org/2005/08/addressing/anonymous</a:Address></a:ReplyTo>\n" + "\n" + " <VsDebuggerCausalityData xmlns=\"http://schemas.microsoft.com/vstudio/diagnostics/servicemodelsink\">uIDPozjxUVEEdSJJhzx3knx8ugoAAAAAi/9uTcBBVUWS2bFa1TmSWXHtuzkndd9Gj4MOMliiOqcACQAA</VsDebuggerCausalityData>\n" + "\n" + " <a:To soapenv:mustUnderstand=\"1\">http://localhost/SomeService/v1.0</a:To>\n" + " </soapenv:Header>\n" + " <soapenv:Body>\n" + firstOpIn + " </soapenv:Body>\n" + "</soapenv:Envelope>")); when(request.getScheme()).thenReturn("http"); ArgumentMatcher<OMElement> credsMatcher = new ArgumentMatcher<OMElement>() { @Override public boolean matches(Object argument) { if (!(argument instanceof OMElement)) { return false; } OMElement element = (OMElement) argument; List<IdentityToken> allTokensFound = new ArrayList<IdentityToken>(); Iterator it = element.getChildElements(); while (it.hasNext()) { OMElement next = (OMElement) it.next(); allTokensFound.add(new IdentityToken(next.getLocalName(), next.getText().trim())); } if (!allTokensFound.remove(new IdentityToken("X-Application","1001"))) { return false; } if (!allTokensFound.remove(new IdentityToken("X-Authentication","vpIt3Zu38ZWppNBKYnbX7Uhno9zOwAydXvIwknGEdTc="))) { return false; } return allTokensFound.isEmpty(); } }; // Resolve the input command soapCommandProcessor.process(command); assertEquals(1, ev.getInvokedCount()); // Assert that we resolved the expected arguments Object[] args = ev.getArgs(); assertNotNull(args); assertEquals(1, args.length); assertEquals("hello", args[0]); // Assert that the expected result is sent assertNotNull(ev.getObserver()); ev.getObserver().onResult(new ExecutionResult("goodbye")); assertEquals(TransportCommand.CommandStatus.Complete, command.getStatus()); assertSoapyEquals(buildSoapMessage(null, firstOpOut, null, null), testOut.getOutput()); verify(response).setContentType(MediaType.TEXT_XML); verify(logger).logAccess(eq(command), isA(ExecutionContext.class), anyLong(), anyLong(), any(MediaType.class), any(MediaType.class), any(ResponseCode.class)); verifyTracerCalls(firstOpKey); } private String buildSoapMessage(String header, String body, String fault, String faultDetail) { StringBuffer result = new StringBuffer(soapEnvStart); if (header != null) { result.append(soapHeaderStart + header + soapHeaderFinish); } if (body != null || fault != null) { result.append(soapBodyStart); if (body != null) { result.append(body); } if (fault != null) { result.append(soapFaultStart + fault); if (faultDetail != null) { result.append(faultDetail); } result.append(soapFaultFinish); } result.append(soapBodyFinish); } result.append(soapEnvFinish); return result.toString(); } private void assertSoapyEquals(String expected, String actual) throws Exception{ XMLStreamReader expectedParser = XMLInputFactory.newInstance() .createXMLStreamReader( new TestServletInputStream(expected)); StAXSOAPModelBuilder expectedBuilder = new StAXSOAPModelBuilder(expectedParser); XMLStreamReader actualParser = XMLInputFactory.newInstance() .createXMLStreamReader( new TestServletInputStream(expected)); StAXSOAPModelBuilder actualBuilder = new StAXSOAPModelBuilder(actualParser); assertHeaders(expectedBuilder.getSOAPEnvelope().getHeader(), actualBuilder.getSOAPEnvelope().getHeader()); assertBody(expectedBuilder.getSOAPEnvelope().getBody(), actualBuilder.getSOAPEnvelope().getBody()); try { xmlTestCase.assertXMLEqual(expected, actual); } catch (AssertionFailedError afe) { StringBuilder sb = new StringBuilder(); sb.append("Expected:\n" + expected + "\n"); sb.append("Actual:\n"+actual+"\n"); sb.append(afe.getMessage()); throw new AssertionFailedError(sb.toString()); } } private void assertHeaders(SOAPHeader expectedHeader, SOAPHeader actualHeader) { if (expectedHeader == null) { assertNull(actualHeader); } else { assertElement(expectedHeader, actualHeader); } } private void assertBody(SOAPBody expectedBody, SOAPBody actualBody) { if (expectedBody == null) { assertNull(actualBody); } else { assertElement(expectedBody, actualBody); } } private void assertElement(OMElement expectedElement, OMElement actualElement) { Iterator expectedIt = expectedElement.getChildElements(); Iterator actualIt = actualElement.getChildElements(); assertEquals(expectedElement.getLocalName(), actualElement.getLocalName()); if (expectedElement.getNamespace() == null) { assertNull(actualElement.getNamespace()); } else { assertEquals(expectedElement.getNamespace().getNamespaceURI(), actualElement.getNamespace().getNamespaceURI()); } assertEquals(expectedElement.getText(), actualElement.getText()); assertEquals(expectedElement.getType(), actualElement.getType()); assertAttributes(expectedElement, actualElement); while (expectedIt.hasNext()) { assertTrue(actualIt.hasNext()); OMElement expectedChildElement = (OMElement)expectedIt.next(); OMElement actualChildElement = (OMElement)actualIt.next(); assertElement(expectedChildElement, actualChildElement); } assertFalse(actualIt.hasNext()); } private void assertAttributes(OMElement expectedElement, OMElement actualElement) { Iterator expectedIt = expectedElement.getAllAttributes(); Iterator actualIt = actualElement.getAllAttributes(); while (expectedIt.hasNext()) { assertTrue(actualIt.hasNext()); OMAttribute expectedAttribute = (OMAttribute)expectedIt.next(); OMAttribute actualAttribute = (OMAttribute)actualIt.next(); assertEquals(expectedAttribute.getAttributeType(), actualAttribute.getAttributeType()); assertEquals(expectedAttribute.getAttributeValue(), actualAttribute.getAttributeValue()); if (expectedAttribute.getNamespace() == null) { assertNull(actualAttribute.getNamespace()); } else { assertEquals(expectedAttribute.getNamespace().getNamespaceURI(), actualAttribute.getNamespace().getNamespaceURI()); } } assertFalse(actualIt.hasNext()); } }