/*
* 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.rescript;
import com.betfair.cougar.api.ExecutionContext;
import com.betfair.cougar.api.ResponseCode;
import com.betfair.cougar.api.export.Protocol;
import com.betfair.cougar.api.fault.FaultCode;
import com.betfair.cougar.api.security.IdentityToken;
import com.betfair.cougar.api.security.InvalidCredentialsException;
import com.betfair.cougar.core.api.OperationBindingDescriptor;
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.CougarServiceException;
import com.betfair.cougar.core.api.exception.CougarValidationException;
import com.betfair.cougar.core.api.exception.PanicInTheCougar;
import com.betfair.cougar.core.api.exception.ServerFaultCode;
import com.betfair.cougar.core.api.fault.CougarFault;
import com.betfair.cougar.core.api.fault.Fault;
import com.betfair.cougar.core.api.transcription.Parameter;
import com.betfair.cougar.marshalling.api.databinding.DataBindingFactory;
import com.betfair.cougar.marshalling.api.databinding.FaultMarshaller;
import com.betfair.cougar.marshalling.api.databinding.Marshaller;
import com.betfair.cougar.marshalling.api.databinding.UnMarshaller;
import com.betfair.cougar.marshalling.impl.databinding.DataBindingManager;
import com.betfair.cougar.marshalling.impl.databinding.DataBindingMap;
import com.betfair.cougar.transport.api.CommandResolver;
import com.betfair.cougar.transport.api.DehydratedExecutionContextResolution;
import com.betfair.cougar.transport.api.ExecutionCommand;
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.HttpServiceBindingDescriptor;
import com.betfair.cougar.transport.api.protocol.http.rescript.*;
import com.betfair.cougar.transport.api.protocol.http.rescript.RescriptParamBindingDescriptor.ParamSource;
import com.betfair.cougar.transport.impl.protocol.http.AbstractHttpCommandProcessorTest;
import com.betfair.cougar.transport.impl.protocol.http.ContentTypeNormaliser;
import com.betfair.cougar.util.RequestUUIDImpl;
import com.betfair.cougar.util.UUIDGeneratorImpl;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.ArgumentMatcher;
import org.mockito.InOrder;
import org.mockito.Mockito;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.ws.rs.core.MediaType;
import java.io.IOException;
import java.io.OutputStream;
import java.security.cert.X509Certificate;
import java.util.*;
import static org.junit.Assert.*;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.argThat;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.anyLong;
import static org.mockito.Mockito.*;
/**
* Unit test for @see RescriptTransportCommandProcessor
*
*/
public class RescriptTransportCommandProcessorTest extends AbstractHttpCommandProcessorTest<Void> {
private OperationBindingDescriptor[] operationBindings;
private ServiceBindingDescriptor serviceBinding = new HttpServiceBindingDescriptor() {
private ServiceVersion serviceVersion = new ServiceVersion("v1.2");
@Override
public String getServiceContextPath() {
return "/myservice/";
}
@Override
public OperationBindingDescriptor[] getOperationBindings() {
return operationBindings;
}
@Override
public Protocol getServiceProtocol() {
return Protocol.RESCRIPT;
}
@Override
public String getServiceName() {
return "RescriptTestService";
}
@Override
public ServiceVersion getServiceVersion() {
return serviceVersion;
}
};
private RescriptTransportCommandProcessor rescriptCommandProcessor;
private Marshaller marshaller;
private UnMarshaller unmarshaller;
private FaultMarshaller faultMarshaller;
private ContentTypeNormaliser ctn;
private RescriptIdentityTokenResolver credentialResolver;
private TestHttpCommand command;
@BeforeClass
public static void setupStatic() {
RequestUUIDImpl.setGenerator(new UUIDGeneratorImpl());
}
@Before
public void init() throws Exception {
super.init();
List<RescriptParamBindingDescriptor> firstOpParamBindings = new ArrayList<RescriptParamBindingDescriptor>();
firstOpParamBindings.add(new RescriptParamBindingDescriptor("FirstOpFirstParam", ParamSource.QUERY));
List<RescriptParamBindingDescriptor> mapOpParamBindings = new ArrayList<RescriptParamBindingDescriptor>();
mapOpParamBindings.add(new RescriptParamBindingDescriptor("MapOpFirstParam", ParamSource.BODY));
List<RescriptParamBindingDescriptor> listOpParamBindings = new ArrayList<RescriptParamBindingDescriptor>();
listOpParamBindings.add(new RescriptParamBindingDescriptor("ListOpFirstParam", ParamSource.BODY));
List<RescriptParamBindingDescriptor> invalidOpParamBindings = new ArrayList<RescriptParamBindingDescriptor>();
invalidOpParamBindings.add(new RescriptParamBindingDescriptor("InvalidOpFirstParam", ParamSource.QUERY));
List<RescriptParamBindingDescriptor> voidReturnOpParamBindings = new ArrayList<RescriptParamBindingDescriptor>();
voidReturnOpParamBindings.add(new RescriptParamBindingDescriptor("VoidReturnOpFirstParam", ParamSource.QUERY));
operationBindings = new OperationBindingDescriptor[] {
new RescriptOperationBindingDescriptor(firstOpKey, "/FirstTestOp", "GET", firstOpParamBindings, TestResponse.class),
new RescriptOperationBindingDescriptor(mapOpKey, "/MapOp", "POST", mapOpParamBindings, TestResponse.class, TestBody.class),
new RescriptOperationBindingDescriptor(listOpKey, "/ListOp", "GET", listOpParamBindings, TestResponse.class, TestBody.class),
new RescriptOperationBindingDescriptor(invalidOpKey, "/InvalidOp", "GET", invalidOpParamBindings, TestResponse.class),
new RescriptOperationBindingDescriptor(voidReturnOpKey, "/VoidReturnOp", "GET", voidReturnOpParamBindings, null)};
rescriptCommandProcessor = new RescriptTransportCommandProcessor(contextResolution,"X-RequestTimeout");
init(rescriptCommandProcessor);
ctn = mock(ContentTypeNormaliser.class);
when(ctn.getNormalisedRequestMediaType(any(HttpServletRequest.class))).thenReturn(MediaType.APPLICATION_XML_TYPE);
when(ctn.getNormalisedResponseMediaType(any(HttpServletRequest.class))).thenReturn(MediaType.APPLICATION_XML_TYPE);
when(ctn.getNormalisedEncoding(any(HttpServletRequest.class))).thenReturn("utf-8");
rescriptCommandProcessor.setContentTypeNormaliser(ctn);
credentialResolver = mock(RescriptIdentityTokenResolver.class);
when(credentialResolver.resolve(any(HttpServletRequest.class), any(X509Certificate[].class))).thenReturn(new ArrayList<IdentityToken>());
rescriptCommandProcessor.setValidatorRegistry(validatorRegistry);
command = new TestHttpCommand(credentialResolver, Protocol.RESCRIPT);
DataBindingFactory dbf = mock(DataBindingFactory.class);
marshaller = mock(Marshaller.class);
unmarshaller = mock(UnMarshaller.class);
faultMarshaller = mock(FaultMarshaller.class);
when(dbf.getMarshaller()).thenReturn(marshaller);
when(dbf.getUnMarshaller()).thenReturn(unmarshaller);
when(dbf.getFaultMarshaller()).thenReturn(faultMarshaller);
DataBindingMap dbm = new DataBindingMap();
dbm.setFactory(dbf);
dbm.setPreferredContentType(MediaType.APPLICATION_XML);
HashSet<String> contentTypes = new HashSet<String>();
contentTypes.add(MediaType.APPLICATION_XML);
dbm.setContentTypes(contentTypes);
DataBindingManager.getInstance().addBindingMap(dbm);
rescriptCommandProcessor.bind(serviceBinding);
rescriptCommandProcessor.onCougarStart();
}
@Override
protected Void isCredentialContainer() {
return (Void) isNull();
}
@Override
protected Protocol getProtocol() {
return Protocol.RESCRIPT;
}
/**
* Basic test with string parameters
* @throws Exception
*/
@Test
public void testProcess() throws Exception {
// Set up the input
command.setPathInfo("/FirstTestOp");
when(request.getParameter("FirstOpFirstParam")).thenReturn("hello");
when(request.getScheme()).thenReturn("http");
// Resolve the input command
rescriptCommandProcessor.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());
verify(response).setContentType(MediaType.APPLICATION_XML);
InOrder inorder = inOrder(marshaller, logger);
inorder.verify(marshaller).marshall(any(OutputStream.class), argThat(matchesResponse("goodbye")), eq("utf-8"), eq(false));
inorder.verify(logger).logAccess(eq(command), any(ExecutionContext.class), anyLong(), anyLong(),
any(MediaType.class), any(MediaType.class), any(ResponseCode.class));
verifyTracerCalls(firstOpKey);
}
/**
* Tests exceptions
* @throws Exception
*/
@Test
public void testProcess_OnException() throws Exception {
// Set up the input
command.setPathInfo("/FirstTestOp");
when(request.getParameter("FirstOpFirstParam")).thenReturn("hello");
when(request.getScheme()).thenReturn("http");
// Resolve the input command
rescriptCommandProcessor.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());
verify(response).setContentType(MediaType.APPLICATION_XML);
ArgumentCaptor<Fault> faultCaptor = ArgumentCaptor.forClass(Fault.class);
InOrder inorder = inOrder(faultMarshaller, logger);
inorder.verify(faultMarshaller).marshallFault(any(OutputStream.class), faultCaptor.capture(), eq("utf-8"));
inorder.verify(logger).logAccess(eq(command), any(ExecutionContext.class), anyLong(), anyLong(),
any(MediaType.class), any(MediaType.class), any(ResponseCode.class));
assertNotNull(faultCaptor.getValue());
verify(response).setStatus(HttpServletResponse.SC_FORBIDDEN);
assertEquals("TestError-123", faultCaptor.getValue().getErrorCode());
assertEquals(FaultCode.Client, faultCaptor.getValue().getFaultCode());
verifyTracerCalls(firstOpKey);
}
/**
* Tests a client error response is sent for invalid input
* @throws Exception
*/
@Test
public void testProcess_InvalidInput() throws Exception {
// Set up the input
command.setPathInfo("/InvalidOp");
when(request.getParameter("InvalidOpFirstParam")).thenReturn("INVALID");
when(request.getScheme()).thenReturn("http");
// Resolve the input command
rescriptCommandProcessor.process(command);
assertEquals(CommandStatus.Complete, command.getStatus());
verify(response).setContentType(MediaType.APPLICATION_XML);
verify(response).setStatus(HttpServletResponse.SC_BAD_REQUEST);
InOrder inorder = inOrder(faultMarshaller, logger);
inorder.verify(faultMarshaller).marshallFault(any(OutputStream.class), any(CougarFault.class), eq("utf-8"));
inorder.verify(logger).logAccess(eq(command), any(ExecutionContext.class), anyLong(), anyLong(),
any(MediaType.class), any(MediaType.class), any(ResponseCode.class));
verifyTracerCalls(null);
}
@Test
public void testProcess_InvalidContentType() throws Exception {
// Set up the input
command.setPathInfo("/InvalidOp");
when(request.getParameter("InvalidOpFirstParam")).thenReturn(TestEnum.TEST1.toString());
when(request.getScheme()).thenReturn("http");
// Resolve the input command
rescriptCommandProcessor.process(command);
assertEquals(1, ev.getInvokedCount());
assertNotNull(ev.getObserver());
//Now get ready to send a response, but the response type is invalid
when(ctn.getNormalisedResponseMediaType(any(HttpServletRequest.class))).thenThrow(
new CougarValidationException(ServerFaultCode.AcceptTypeNotValid, ""));
ev.getObserver().onResult(new ExecutionResult("something"));
//Verify we get the correct response status
assertEquals(CommandStatus.Complete, command.getStatus());
verify(response).setContentType(MediaType.APPLICATION_XML);
verify(response).setStatus(HttpServletResponse.SC_NOT_ACCEPTABLE);
//And verify that the error is marshalled then logged in that order
InOrder inorder = inOrder(faultMarshaller, logger);
inorder.verify(faultMarshaller).marshallFault(any(OutputStream.class), any(CougarFault.class), eq("utf-8"));
inorder.verify(logger).logAccess(eq(command), any(ExecutionContext.class), anyLong(), anyLong(),
any(MediaType.class), any(MediaType.class), any(ResponseCode.class));
verifyTracerCalls(invalidOpKey);
}
@Test
public void testProcess_void() throws InvalidCredentialsException {
// Set up the input
command.setPathInfo("/VoidReturnOp");
when(request.getParameter("VoidReturnOpFirstParam")).thenReturn(TestEnum.TEST1.toString());
when(request.getScheme()).thenReturn("http");
// Resolve the input command
rescriptCommandProcessor.process(command);
assertNotNull(ev.getObserver());
ev.getObserver().onResult(new ExecutionResult());
verify(response).setStatus(HttpServletResponse.SC_OK);
verifyTracerCalls(voidReturnOpKey);
}
@Test
public void testProcess_voidWithException() throws IOException, InvalidCredentialsException {
// Set up the input
command.setPathInfo("/VoidReturnOp");
when(request.getParameter("VoidReturnOpFirstParam")).thenReturn("INVALID");
when(request.getScheme()).thenReturn("http");
//Resolve / run this command
rescriptCommandProcessor.process(command);
assertEquals(CommandStatus.Complete, command.getStatus());
verify(response).setContentType(MediaType.APPLICATION_XML);
verify(response).setStatus(HttpServletResponse.SC_BAD_REQUEST);
InOrder inorder = inOrder(faultMarshaller, logger);
inorder.verify(faultMarshaller).marshallFault(any(OutputStream.class), any(CougarFault.class), eq("utf-8"));
inorder.verify(logger).logAccess(eq(command), any(ExecutionContext.class), anyLong(), anyLong(),
any(MediaType.class), any(MediaType.class), any(ResponseCode.class));
verifyTracerCalls(null);
}
@Test(expected=PanicInTheCougar.class)
public void testBindOperation() {
//Ensure that we don't have more than one operation bound with the same uri path
OperationDefinition op = Mockito.mock(OperationDefinition.class);
when(op.getParameters()).thenReturn(new Parameter[0]);
ExecutionVenue ev = Mockito.mock(ExecutionVenue.class);
OperationKey key = Mockito.mock(OperationKey.class);
when(ev.getOperationDefinition(key)).thenReturn(op);
HttpServiceBindingDescriptor serviceDescriptor = new HttpServiceBindingDescriptor() {
private ServiceVersion serviceVersion = new ServiceVersion("v42.1");
@Override
public String getServiceContextPath() {
return "/I/v1.0";
}
@Override
public OperationBindingDescriptor[] getOperationBindings() {
return new OperationBindingDescriptor[0];
}
@Override
public Protocol getServiceProtocol() {
return null;
}
@Override
public String getServiceName() {
return "AnotherServiceName";
}
@Override
public ServiceVersion getServiceVersion() {
return serviceVersion;
}
};
RescriptOperationBindingDescriptor op1 = new RescriptOperationBindingDescriptor(key, "url1", "GET", Collections.<RescriptParamBindingDescriptor>emptyList(), null);
RescriptOperationBindingDescriptor op2 = new RescriptOperationBindingDescriptor(key, "url2", "POST", Collections.<RescriptParamBindingDescriptor>emptyList(), null);
RescriptTransportCommandProcessor sut = new RescriptTransportCommandProcessor(contextResolution, "X-RequestTimeout");
sut.setExecutionVenue(ev);
sut.bindOperation(serviceDescriptor, op1);
sut.bindOperation(serviceDescriptor, op2);
sut.bindOperation(serviceDescriptor, op1);
fail("Duplicate url binding forbidden, an exception should have been thrown");
}
@Test
public void testSameOperationDifferentContextPaths() {
// Ensure that allow more two of the same operation if the are different
OperationDefinition op = Mockito.mock(OperationDefinition.class);
when(op.getParameters()).thenReturn(new Parameter[0]);
ExecutionVenue ev = Mockito.mock(ExecutionVenue.class);
OperationKey key = Mockito.mock(OperationKey.class);
when(ev.getOperationDefinition(key)).thenReturn(op);
HttpServiceBindingDescriptor serviceDescriptorV1 = new HttpServiceBindingDescriptor() {
@Override
public String getServiceContextPath() {
return "/I/v1.0";
}
@Override
public OperationBindingDescriptor[] getOperationBindings() {
return new OperationBindingDescriptor[0];
}
@Override
public Protocol getServiceProtocol() {
return null;
}
@Override
public ServiceVersion getServiceVersion() {
return new ServiceVersion(1,0);
}
@Override
public String getServiceName() {
return "Wibble";
}
};
HttpServiceBindingDescriptor serviceDescriptorV2 = new HttpServiceBindingDescriptor() {
@Override
public String getServiceContextPath() {
return "/I/v2.0";
}
@Override
public OperationBindingDescriptor[] getOperationBindings() {
return new OperationBindingDescriptor[0];
}
@Override
public Protocol getServiceProtocol() {
return null;
}
@Override
public ServiceVersion getServiceVersion() {
return new ServiceVersion(2,0);
}
@Override
public String getServiceName() {
return "Wibble";
}
};
RescriptOperationBindingDescriptor op1 = new RescriptOperationBindingDescriptor(key, "url1", "GET", Collections.<RescriptParamBindingDescriptor>emptyList(), null);
RescriptTransportCommandProcessor sut = new RescriptTransportCommandProcessor(contextResolution, null);
sut.setExecutionVenue(ev);
sut.bindOperation(serviceDescriptorV1, op1);
sut.bindOperation(serviceDescriptorV2, op1);
}
@Test
public void createCommandResolver_NoTimeout() {
// Set up the input
command.setPathInfo("/FirstTestOp");
when(request.getParameter("FirstOpFirstParam")).thenReturn("hello");
when(request.getScheme()).thenReturn("http");
// resolve the command
CommandResolver<HttpCommand> cr = rescriptCommandProcessor.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() {
// Set up the input
command.setPathInfo("/FirstTestOp");
when(request.getParameter("FirstOpFirstParam")).thenReturn("hello");
when(request.getScheme()).thenReturn("http");
// resolve the command
when(request.getHeader("X-RequestTimeout")).thenReturn("10000");
when(context.getRequestTime()).thenReturn(new Date());
CommandResolver<HttpCommand> cr = rescriptCommandProcessor.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() {
// Set up the input
command.setPathInfo("/FirstTestOp");
when(request.getParameter("FirstOpFirstParam")).thenReturn("hello");
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 = rescriptCommandProcessor.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());
}
private ArgumentMatcher<RescriptResponse> matchesResponse(final Object responseValue) {
return new ArgumentMatcher<RescriptResponse>() {
@Override
public boolean matches(Object argument) {
assertTrue(argument instanceof RescriptResponse);
assertEquals(responseValue, ((RescriptResponse)argument).getResult());
return true;
}
};
}
public static class TestResponse implements RescriptResponse {
private Object result;
@Override
public Object getResult() {
return result;
}
@Override
public void setResult(Object result) {
this.result = result;
}
}
public static class TestBody implements RescriptBody {
private HashMap<String, Object> map = new HashMap<String, Object>();
@Override
public Object getValue(String name) {
return map.get(name);
}
public void put(String name, Object value) {
map.put(name, value);
}
}
}