/*
* Copyright 2014, The Sporting Exchange Limited
* Copyright 2015, Simon Matić Langford
*
* 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;
import com.betfair.cougar.api.DehydratedExecutionContext;
import com.betfair.cougar.api.ExecutionContext;
import com.betfair.cougar.api.RequestUUID;
import com.betfair.cougar.api.ResponseCode;
import com.betfair.cougar.api.export.Protocol;
import com.betfair.cougar.api.fault.CougarApplicationException;
import com.betfair.cougar.api.security.IdentityTokenResolver;
import com.betfair.cougar.core.api.OperationBindingDescriptor;
import com.betfair.cougar.core.api.RequestTimer;
import com.betfair.cougar.core.api.ServiceBindingDescriptor;
import com.betfair.cougar.core.api.ServiceVersion;
import com.betfair.cougar.core.api.ev.*;
import com.betfair.cougar.core.api.exception.CougarException;
import com.betfair.cougar.core.api.exception.CougarServiceException;
import com.betfair.cougar.core.api.exception.PanicInTheCougar;
import com.betfair.cougar.core.api.exception.ServerFaultCode;
import com.betfair.cougar.core.api.tracing.Tracer;
import com.betfair.cougar.core.api.transcription.Parameter;
import com.betfair.cougar.core.api.transcription.ParameterType;
import com.betfair.cougar.core.impl.DefaultTimeConstraints;
import com.betfair.cougar.logging.CougarLoggingUtils;
import com.betfair.cougar.transport.api.*;
import com.betfair.cougar.transport.api.protocol.http.HttpCommand;
import com.betfair.cougar.transport.impl.CommandValidatorRegistry;
import com.betfair.cougar.transport.impl.protocol.http.rescript.RescriptOperationBindingTest;
import com.betfair.cougar.util.RequestUUIDImpl;
import com.betfair.cougar.util.UUIDGeneratorImpl;
import org.custommonkey.xmlunit.XMLTestCase;
import org.hamcrest.BaseMatcher;
import org.hamcrest.Description;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.InOrder;
import javax.servlet.ReadListener;
import javax.servlet.ServletInputStream;
import javax.servlet.ServletOutputStream;
import javax.servlet.WriteListener;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.Executor;
import static org.junit.Assert.*;
import static org.mockito.Matchers.any;
import static org.mockito.Mockito.*;
public abstract class AbstractHttpCommandProcessorTest<CredentialContainer> {
private static final String SERVICE_PATH = "/myservice/v1.0";
protected static final OperationKey firstOpKey = new OperationKey(
new ServiceVersion(2, 1), "HTTPTest", "FirstTestOp");
protected static final Parameter[] firstOpParams = new Parameter[] { new Parameter(
"FirstOpFirstParam", ParameterType.create(String.class, null),
false) };
protected static final ParameterType firstOpReturn = ParameterType.create(String.class,
null);
protected static final OperationKey mapOpKey = new OperationKey(
new ServiceVersion(2, 1), "HTTPTest", "MapTestOp");
protected static final Parameter[] mapOpParams = new Parameter[] { new Parameter(
"MapOpFirstParam", ParameterType.create(HashMap.class, Integer.class, Double.class),
false) };
protected static final ParameterType mapOpReturn = ParameterType.create(HashMap.class, Integer.class, Double.class);
protected static final OperationKey listOpKey = new OperationKey(
new ServiceVersion(2, 1), "HTTPTest", "ListTestOp");
protected static final Parameter[] listOpParams = new Parameter[] { new Parameter(
"ListOpFirstParam", ParameterType.create(List.class, Date.class),
false) };
protected static final ParameterType listOpReturn = ParameterType.create(List.class, Date.class);
protected static final OperationKey invalidOpKey = new OperationKey(
new ServiceVersion(2, 1), "HTTPTest", "InvalidTestOp");
protected static final Parameter[] invalidOpParams = new Parameter[] { new Parameter(
"InvalidOpFirstParam", ParameterType.create(TestEnum.class, null),
false) };
protected static final ParameterType invalidOpReturn = ParameterType.create(TestEnum.class, null);
protected static final OperationKey voidReturnOpKey = new OperationKey( new ServiceVersion(2, 1), "HTTPTest", "VoidReturnTestOp");
protected static final Parameter[] voidReturnOpParams = new Parameter[] { new Parameter("VoidReturnOpFirstParam", ParameterType.create(TestEnum.class, null), true)};
protected DehydratedExecutionContext context;
public static enum TestEnum {
TEST1
// ,TEST2
// ,UNRECOGNIZED_VALUE
}
protected XMLTestCase xmlTestCase = new XMLTestCase();
protected HttpServletRequest request;
protected HttpServletResponse response;
protected TestServletOutputStream testOut;
protected List<String[]> faultMessages;
protected TestEV ev;
protected RequestLogger logger;
protected Tracer tracer;
protected CommandValidatorRegistry<HttpCommand> validatorRegistry = new CommandValidatorRegistry<>();
protected DehydratedExecutionContextResolution contextResolution;
protected LocalCommandProcessor commandProcessor;
protected Protocol protocol;
@BeforeClass
public static void suppressLogging() {
CougarLoggingUtils.suppressAllRootLoggerOutput();
}
@Before
public void init() throws Exception {
TimeZone.setDefault(TimeZone.getTimeZone("UTC"));
RequestUUIDImpl.setGenerator(new UUIDGeneratorImpl());
logger = mock(RequestLogger.class);
tracer = mock(Tracer.class);
contextResolution = mock(DehydratedExecutionContextResolution.class);
context = mock(DehydratedExecutionContext.class);
when(contextResolution.resolveExecutionContext(eq(getProtocol()),any(HttpCommand.class),isCredentialContainer())).thenReturn(context);
RequestUUID uuid = new RequestUUIDImpl();
when(context.getRequestUUID()).thenReturn(uuid);
request = mock(HttpServletRequest.class);
when(request.getContextPath()).thenReturn(SERVICE_PATH);
when(request.getHeaderNames()).thenReturn(RescriptOperationBindingTest.enumerator(new ArrayList<String>().iterator()));
response = mock(HttpServletResponse.class);
testOut = new TestServletOutputStream();
when(response.getOutputStream()).thenReturn(testOut);
ev = new TestEV();
ev.registerOperation(null, new SimpleOperationDefinition(firstOpKey, firstOpParams,firstOpReturn), null, null, 0);
ev.registerOperation(null, new SimpleOperationDefinition(mapOpKey, mapOpParams, mapOpReturn), null, null, 0);
ev.registerOperation(null, new SimpleOperationDefinition(listOpKey, listOpParams, listOpReturn), null, null, 0);
ev.registerOperation(null, new SimpleOperationDefinition(invalidOpKey, invalidOpParams, invalidOpReturn), null, null, 0);
ev.registerOperation(null, new SimpleOperationDefinition(voidReturnOpKey, voidReturnOpParams, null), null, null, 0);
commandProcessor = new LocalCommandProcessor();
init(commandProcessor);
}
protected abstract CredentialContainer isCredentialContainer();
protected abstract Protocol getProtocol();
protected void verifyTracerCalls(OperationKey expected) {
final ArgumentCaptor<RequestUUID> captor = ArgumentCaptor.forClass(RequestUUID.class);
final ArgumentCaptor<OperationKey> opKeyCaptor = ArgumentCaptor.forClass(OperationKey.class);
if (expected != null) {
InOrder inOrder = inOrder(tracer);
inOrder.verify(tracer).start(captor.capture(), opKeyCaptor.capture());
inOrder.verify(tracer).end(argThat(new BaseMatcher<RequestUUID>() {
@Override
public boolean matches(Object o) {
return o.equals(captor.getValue());
}
@Override
public void describeTo(Description description) {
}
}));
}
}
@Test(expected = PanicInTheCougar.class)
public void testMultipleServiceBindSameVersion() {
final ServiceVersion sv = new ServiceVersion("v3.2");
final String serviceName = "testServiceName";
ServiceBindingDescriptor sbd = new ServiceBindingDescriptor() {
@Override
public OperationBindingDescriptor[] getOperationBindings() {
return new OperationBindingDescriptor[0];
}
@Override
public ServiceVersion getServiceVersion() {
return sv;
}
@Override
public String getServiceName() {
return serviceName;
}
@Override
public Protocol getServiceProtocol() {
return getProtocol();
}
};
commandProcessor.bind(sbd);
commandProcessor.bind(sbd);
}
@Test(expected = PanicInTheCougar.class)
public void testMultipleServiceBindingSameMajorVersion() {
final String serviceName = "testServiceName";
commandProcessor.bind(new ServiceBindingDescriptor() {
@Override
public OperationBindingDescriptor[] getOperationBindings() {
return new OperationBindingDescriptor[0]; //To change body of implemented methods use File | Settings | File Templates.
}
@Override
public ServiceVersion getServiceVersion() {
return new ServiceVersion("v3.2");
}
@Override
public String getServiceName() {
return serviceName;
}
@Override
public Protocol getServiceProtocol() {
return null;
}
});
commandProcessor.bind(new ServiceBindingDescriptor() {
@Override
public OperationBindingDescriptor[] getOperationBindings() {
return new OperationBindingDescriptor[0]; //To change body of implemented methods use File | Settings | File Templates.
}
@Override
public ServiceVersion getServiceVersion() {
return new ServiceVersion("v3.3");
}
@Override
public String getServiceName() {
return serviceName;
}
@Override
public Protocol getServiceProtocol() {
return null;
}
});
}
@Test
public void testMultipleServiceBindingDifferentMajorVersion() {
final String serviceName = "testServiceName";
commandProcessor.bind(new ServiceBindingDescriptor() {
@Override
public OperationBindingDescriptor[] getOperationBindings() {
return new OperationBindingDescriptor[0]; //To change body of implemented methods use File | Settings | File Templates.
}
@Override
public ServiceVersion getServiceVersion() {
return new ServiceVersion("v1.2");
}
@Override
public String getServiceName() {
return serviceName;
}
@Override
public Protocol getServiceProtocol() {
return null;
}
});
commandProcessor.bind(new ServiceBindingDescriptor() {
@Override
public OperationBindingDescriptor[] getOperationBindings() {
return new OperationBindingDescriptor[0]; //To change body of implemented methods use File | Settings | File Templates.
}
@Override
public ServiceVersion getServiceVersion() {
return new ServiceVersion("v3.3");
}
@Override
public String getServiceName() {
return serviceName;
}
@Override
public Protocol getServiceProtocol() {
return null;
}
});
int count=0;
for (ServiceBindingDescriptor ignored : commandProcessor.getServiceBindingDescriptors()) {
count++;
}
assertEquals(2, count);
}
@Test
public void testMultipleServiceBindingDifferentService() {
commandProcessor.bind(new ServiceBindingDescriptor() {
@Override
public OperationBindingDescriptor[] getOperationBindings() {
return new OperationBindingDescriptor[0]; //To change body of implemented methods use File | Settings | File Templates.
}
@Override
public ServiceVersion getServiceVersion() {
return new ServiceVersion("v1.2");
}
@Override
public String getServiceName() {
return "service1";
}
@Override
public Protocol getServiceProtocol() {
return null;
}
});
commandProcessor.bind(new ServiceBindingDescriptor() {
@Override
public OperationBindingDescriptor[] getOperationBindings() {
return new OperationBindingDescriptor[0]; //To change body of implemented methods use File | Settings | File Templates.
}
@Override
public ServiceVersion getServiceVersion() {
return new ServiceVersion("v3.3");
}
@Override
public String getServiceName() {
return "service2";
}
@Override
public Protocol getServiceProtocol() {
return null;
}
});
int count=0;
for (ServiceBindingDescriptor ignored : commandProcessor.getServiceBindingDescriptors()) {
count++;
}
assertEquals(2, count);
}
@Test
public void testUriStrip() {
String[][] textMatrix = {
{ "/service/v1.2", "/service/v1"},
{ "/service/V1.2", "/service/V1"},
{ "/service/v1.0/foo", "/service/v1/foo" },
{ "/service/v1/foo", "/service/v1/foo" },
{ "/service/v20.3/foo", "/service/v20/foo"},
{ "/v1/foo", "/v1/foo"},
{ "/v20.3/foo", "/v20/foo"},
{ "/service/v1.3/foo?action=add&sky=blue", "/service/v1/foo?action=add&sky=blue"}
};
for (String[] pair : textMatrix) {
assertEquals(pair[1], commandProcessor.stripMinorVersionFromUri(pair[0]));
}
}
@Test
public void testCallsValidators() throws Exception {
HttpCommand command = new TestHttpCommand(null, null);
//noinspection unchecked
CommandValidator<HttpCommand> validator = mock(CommandValidator.class);
validatorRegistry.addValidator(validator);
commandProcessor.process(command);
assertFalse(commandProcessor.errorCalled());
verify(validator).validate(any(HttpCommand.class));
}
@Test
public void testStopsOnValidatorFail() throws Exception {
HttpCommand command = new TestHttpCommand(null, null);
CommandValidator<HttpCommand> validator = new CommandValidator<HttpCommand>() {
@Override
public void validate(HttpCommand command) throws CougarException {
throw new CougarServiceException(ServerFaultCode.SecurityException, "wibble");
}
};
validatorRegistry.addValidator(validator);
commandProcessor.process(command);
assertTrue(commandProcessor.errorCalled());
}
protected void init(AbstractHttpCommandProcessor commandProcessor) throws Exception {
//noinspection NullableProblems
commandProcessor.setExecutor(new Executor() {
@Override
public void execute(Runnable runnable) {
runnable.run();
}
});
commandProcessor.setExecutionVenue(ev);
commandProcessor.setRequestLogger(logger);
commandProcessor.setValidatorRegistry(validatorRegistry);
commandProcessor.setHardFailEnumDeserialisation(true);
commandProcessor.setTracer(tracer);
}
protected class TestEV implements ExecutionVenue {
private ExecutionObserver observer;
private Object[] args;
private HashMap<OperationKey, OperationDefinition> map = new HashMap<>();
private int invokedCount = 0;
public Object[] getArgs() {
return args;
}
public ExecutionObserver getObserver() {
return observer;
}
public int getInvokedCount() {
return invokedCount;
}
@Override
public void execute(ExecutionContext ctx, OperationKey key,
Object[] args, ExecutionObserver observer, TimeConstraints clientExpiryTime) {
invokedCount++;
this.args = args;
this.observer = observer;
}
@Override
public void execute(final ExecutionContext ctx, final OperationKey key, final Object[] args, final ExecutionObserver observer, final Executor executor, final TimeConstraints clientExpiryTime) {
executor.execute(new Runnable() {
@Override
public void run() {
execute(ctx, key, args, observer, clientExpiryTime);
}
});
}
@Override
public void registerOperation(String namespace, OperationDefinition def, Executable executable, ExecutionTimingRecorder recorder, long max) {
map.put(def.getOperationKey(), def);
}
@Override
public OperationDefinition getOperationDefinition(OperationKey key) {
return map.get(key);
}
@Override
public Set<OperationKey> getOperationKeys() {
return map.keySet();
}
@Override
public void setPostProcessors(
List<ExecutionPostProcessor> preProcessorList) {
}
@Override
public void setPreProcessors(
List<ExecutionPreProcessor> preProcessorList) {
}
}
protected class TestServletInputStream extends ServletInputStream {
private String input;
private int pos = 0;
public TestServletInputStream(String input) {
this.input = input;
}
@Override
public int read() throws IOException {
if (pos < input.length()) {
return input.charAt(pos++);
}
return -1;
}
@Override
public boolean isFinished() {
return (pos >= input.length());
}
@Override
public boolean isReady() {
return (pos < input.length());
}
@Override
public void setReadListener(ReadListener readListener) {
}
};
protected class TestServletOutputStream extends ServletOutputStream {
private StringBuffer output = new StringBuffer();
@Override
public void write(int character) throws IOException {
output.append((char) character);
}
public String getOutput() {
return output.toString();
}
@Override
public boolean isReady() {
return true;
}
@Override
public void setWriteListener(WriteListener writeListener) {
}
}
protected HttpCommand createCommand(IdentityTokenResolver identityTokenResolver, Protocol protocol) {
return new TestHttpCommand(identityTokenResolver, protocol);
}
protected class TestHttpCommand implements HttpCommand {
private CommandStatus commandStatus = CommandStatus.InProgress;
private RequestTimer timer = new RequestTimer();
private IdentityTokenResolver identityTokenResolver;
private String pathInfo = "/test";
private Protocol protocol;
public TestHttpCommand(IdentityTokenResolver identityTokenResolver, Protocol protocol) {
this.identityTokenResolver = identityTokenResolver;
this.protocol = protocol;
}
@Override
public HttpServletRequest getRequest() {
return request;
}
@Override
public HttpServletResponse getResponse() {
return response;
}
@Override
public IdentityTokenResolver<?, ?, ?> getIdentityTokenResolver() {
return this.identityTokenResolver;
}
@Override
public CommandStatus getStatus() {
return commandStatus;
}
@Override
public void onComplete() {
commandStatus = CommandStatus.Complete;
}
@Override
public RequestTimer getTimer() {
return timer;
}
@Override
public String getFullPath() {
return "/foo"+getOperationPath();
}
@Override
public String getOperationPath() {
if (protocol == Protocol.SOAP) {
return SERVICE_PATH;
} else {
return SERVICE_PATH+pathInfo;
}
}
public void setPathInfo(String pathInfo) {
this.pathInfo = pathInfo;
}
};
protected static class TestApplicationException extends CougarApplicationException {
private final List<String[]> faultMessages;
public TestApplicationException(ResponseCode code, String message, List<String[]> faultMessages) {
super(code, message);
this.faultMessages = faultMessages;
}
@Override
public List<String[]> getApplicationFaultMessages() {
return faultMessages;
}
@Override
public String getApplicationFaultNamespace() {
return null;
}
}
private class LocalCommandProcessor extends AbstractHttpCommandProcessor<Void> {
private boolean errorCalled;
private LocalCommandProcessor() {
super(Protocol.RESCRIPT,contextResolution,"X-RequestTimeout");
}
@Override
protected CommandResolver<HttpCommand> createCommandResolver(final HttpCommand command, Tracer tracer) {
return new CommandResolver<HttpCommand>() {
@Override
public DehydratedExecutionContext resolveExecutionContext() {
return context;
}
@Override
public List<ExecutionCommand> resolveExecutionCommands() {
List commands = Arrays.asList(new ExecutionCommand() {
@Override
public OperationKey getOperationKey() {
return null;
}
@Override
public Object[] getArgs() {
return new Object[0];
}
@Override
public void onResult(ExecutionResult executionResult) {
}
@Override
public TimeConstraints getTimeConstraints() {
return DefaultTimeConstraints.NO_CONSTRAINTS;
}
});
//noinspection unchecked
return commands;
}
};
}
@Override
protected void writeErrorResponse(HttpCommand command, DehydratedExecutionContext context, CougarException e, boolean traceStarted) {
errorCalled = true;
}
@Override
public void onCougarStart() {
}
private boolean errorCalled() {
return errorCalled;
}
}
}