/*
* 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.jetty;
import com.betfair.cougar.api.export.Protocol;
import com.betfair.cougar.api.security.IdentityTokenResolver;
import com.betfair.cougar.core.api.OperationBindingDescriptor;
import com.betfair.cougar.core.api.ServiceVersion;
import com.betfair.cougar.core.impl.transports.TransportRegistryImpl;
import com.betfair.cougar.transport.api.TransportCommandProcessorFactory;
import com.betfair.cougar.transport.api.protocol.ProtocolBinding;
import com.betfair.cougar.transport.api.protocol.ProtocolBindingRegistry;
import com.betfair.cougar.transport.api.protocol.http.HttpCommandProcessor;
import com.betfair.cougar.transport.api.protocol.http.HttpServiceBindingDescriptor;
import com.betfair.cougar.transport.api.protocol.http.jsonrpc.JsonRpcOperationBindingDescriptor;
import com.betfair.cougar.transport.api.protocol.http.rescript.RescriptOperationBindingDescriptor;
import com.betfair.cougar.transport.api.protocol.http.rescript.RescriptParamBindingDescriptor;
import com.betfair.cougar.transport.impl.protocol.http.DefaultGeoLocationDeserializer;
import com.betfair.cougar.transport.jetty.jmx.JettyEndpoints;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.Mockito;
import org.springframework.core.io.FileSystemResource;
import org.springframework.util.StopWatch;
import javax.management.MBeanServer;
import javax.management.ObjectInstance;
import javax.management.ObjectName;
import java.io.File;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadMXBean;
import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import static org.junit.Assert.*;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.*;
public class JettyHttpTransportTest {
private static final String OPERATION_URI = "/myOperationUri";
private static final String RESCRIPT_SERVICE_CONTEXT_PATH = "/RESCRIPT_SERVICE_CONTEXT_PATH/";
private static final String JSONRPC_SERVICE_CONTEXT_PATH = "/JSON_RPC_SERVICE_CONTEXT_PATH";
public static final String SEP = System.getProperty("file.separator");
private MBeanServer mbeanServer;
private HttpServiceBindingDescriptor rescriptServiceBindingDescriptor;
private HttpServiceBindingDescriptor jsonRpcBindingDescriptor;
private HttpCommandProcessor commandProc;
private TransportCommandProcessorFactory<HttpCommandProcessor> factory;
private static final int TEST_HTTP_PORT = 19478;
@BeforeClass
public static void logQuash() {
//Jetty uses log4j internally, so we need to shut that up
org.apache.log4j.Logger.getRootLogger().addAppender(new org.apache.log4j.varia.NullAppender());
}
private JettyHttpTransport populateTransport() throws Exception {
JettyHttpTransport transport = new JettyHttpTransport();
populateTransport(transport);
return transport;
}
@Before
public void init() {
rescriptServiceBindingDescriptor = Mockito.mock(HttpServiceBindingDescriptor.class);
when(rescriptServiceBindingDescriptor.getServiceContextPath()).thenReturn(RESCRIPT_SERVICE_CONTEXT_PATH);
when(rescriptServiceBindingDescriptor.getServiceProtocol()).thenReturn(Protocol.RESCRIPT);
when(rescriptServiceBindingDescriptor.getServiceVersion()).thenReturn(new ServiceVersion("v3.0"));
when(rescriptServiceBindingDescriptor.getOperationBindings()).thenReturn(new OperationBindingDescriptor[]{
new RescriptOperationBindingDescriptor(null, OPERATION_URI, null, Collections.<RescriptParamBindingDescriptor>emptyList())
});
jsonRpcBindingDescriptor = Mockito.mock(HttpServiceBindingDescriptor.class);
when(jsonRpcBindingDescriptor.getServiceContextPath()).thenReturn(JSONRPC_SERVICE_CONTEXT_PATH);
when(jsonRpcBindingDescriptor.getServiceProtocol()).thenReturn(Protocol.JSON_RPC);
when(jsonRpcBindingDescriptor.getServiceVersion()).thenReturn(new ServiceVersion("v3.0"));
when(jsonRpcBindingDescriptor.getOperationBindings()).thenReturn(new OperationBindingDescriptor[]{
new JsonRpcOperationBindingDescriptor(null)
});
commandProc = Mockito.mock(HttpCommandProcessor.class);
factory = Mockito.mock(TransportCommandProcessorFactory.class);
when(factory.getCommandProcessor(Protocol.RESCRIPT)).thenReturn(commandProc);
when(factory.getCommandProcessor(Protocol.JSON_RPC)).thenReturn(commandProc);
}
private void populateTransport(JettyHttpTransport transport) throws Exception {
mbeanServer = mock(MBeanServer.class);
// mocking this method because otherwise Jetty gets it's knickers in a twist (and I'm not even trying to test
// this, hence why i don't care how often this is called, and hence why i'm not checking other calls to mbeanServer)
ObjectInstance mockedInstance = mock(ObjectInstance.class);
when(mbeanServer.registerMBean(any(Object.class), any(ObjectName.class))).thenReturn(mockedInstance);
transport.getServerWrapper().setMbeanServer(mbeanServer);
transport.setTransportRegistry(new TransportRegistryImpl());
transport.setHtmlContextPath("/wsdl");
transport.setHtmlRegex("/wsdl/[^/]+\\\\.wsdl");
transport.setHtmlMediaType("text/xml");
transport.setWsdlContextPath("/static-html");
transport.setWsdlRegex("/static-html/.+\\\\.html");
transport.setWsdlMediaType("text/html");
transport.setGeoLocationDeserializer(new DefaultGeoLocationDeserializer());
transport.setUuidHeader("X-UUid");
transport.getServerWrapper().setMinThreads(5);
// jetty now frustratingly checks we have enough max threads on startup
transport.getServerWrapper().setMaxThreads(33);
// default is 9001, but going to use something odd so we don't clash if people have something running
transport.getServerWrapper().setHttpPort(TEST_HTTP_PORT);
transport.getServerWrapper().setHttpReuseAddress(false);
transport.getServerWrapper().setHttpMaxIdle(30000);
transport.getServerWrapper().setHttpsPort(-1);
transport.getServerWrapper().setHttpsReuseAddress(false);
transport.getServerWrapper().setHttpsMaxIdle(30000);
transport.getServerWrapper().setHttpsWantClientAuth(false);
transport.getServerWrapper().setHttpsNeedClientAuth(false);
transport.getServerWrapper().setHttpsKeystore(new FileSystemResource("MUST_BE_OVERRIDDEN"));
transport.getServerWrapper().setHttpsKeystoreType("MUST_BE_OVERRIDDEN");
transport.getServerWrapper().setHttpsKeyPassword("MUST_BE_OVERRIDDEN");
transport.getServerWrapper().setHttpsTruststore(new FileSystemResource("MUST_BE_OVERRIDDEN"));
transport.getServerWrapper().setHttpsTruststoreType("MUST_BE_OVERRIDDEN");
transport.getServerWrapper().setHttpsTrustPassword("MUST_BE_OVERRIDDEN");
}
private void populateTransportWithCORSEnabled(JettyHttpTransport transport) throws Exception {
populateTransport(transport);
transport.setCorsEnabled(true);
}
@Test
public void testNotify() {
//We need to test that for a particular service we end up with
//1. An appropriately populated handlerSpecificationMap
//2. the appropriate command processor is informed of the serviceDefinition
ProtocolBinding pb = new ProtocolBinding(null, null, Protocol.RESCRIPT);
Set<ProtocolBinding> bindingSet = new HashSet<ProtocolBinding>();
bindingSet.add(pb);
ProtocolBindingRegistry bindingReg = Mockito.mock(ProtocolBindingRegistry.class);
when(bindingReg.getProtocolBindings()).thenReturn(bindingSet);
JettyHttpTransport transport = new JettyHttpTransport();
transport.setProtocolBindingRegistry(bindingReg);
transport.setCommandProcessorFactory(factory);
assertTrue("Should be no entry in the handlerSpecMap", transport.getHandlerSpecificationMap().isEmpty());
transport.registerHandler(rescriptServiceBindingDescriptor);
assertTrue("should be one entry in the handlerSpecMap", transport.getHandlerSpecificationMap().size() == 1);
//verify that the commandproc was notified with the appropriate binding descriptor
verify(commandProc).bind(eq(rescriptServiceBindingDescriptor));
}
@Test
public void testNotifyWithIdentityTokenResolvers() {
IdentityTokenResolver resolver = Mockito.mock(IdentityTokenResolver.class);
ProtocolBinding pb = new ProtocolBinding(null, resolver, Protocol.RESCRIPT);
Set<ProtocolBinding> bindingSet = new HashSet<ProtocolBinding>();
bindingSet.add(pb);
ProtocolBindingRegistry bindingReg = Mockito.mock(ProtocolBindingRegistry.class);
when(bindingReg.getProtocolBindings()).thenReturn(bindingSet);
JettyHttpTransport transport = new JettyHttpTransport();
transport.setProtocolBindingRegistry(bindingReg);
transport.setCommandProcessorFactory(factory);
transport.registerHandler(rescriptServiceBindingDescriptor);
JettyHandlerSpecification spec = transport.getHandlerSpecificationMap().values().iterator().next();
assertNotNull("Jetty handler spec should not be null", spec);
assertTrue("There should be one identityTokenResolver plugged in here", spec.getVersionToIdentityTokenResolverMap().size() == 1);
}
@Test
public void testCORSEnabledAddNewHandlerToContextHandlerCollection() throws Exception {
ProtocolBinding pb = new ProtocolBinding("/", null, Protocol.RESCRIPT);
ProtocolBinding pb2 = new ProtocolBinding("/api", null, Protocol.RESCRIPT);
ProtocolBinding pb3 = new ProtocolBinding("/", null, Protocol.JSON_RPC);
Set<ProtocolBinding> bindingSet = new HashSet<ProtocolBinding>();
bindingSet.add(pb);
bindingSet.add(pb2);
bindingSet.add(pb3);
ProtocolBindingRegistry bindingReg = Mockito.mock(ProtocolBindingRegistry.class);
when(bindingReg.getProtocolBindings()).thenReturn(bindingSet);
JettyHttpTransport transport = new JettyHttpTransport();
populateTransport(transport);
transport.setProtocolBindingRegistry(bindingReg);
transport.setCommandProcessorFactory(factory);
JettyEndpoints endpoints = Mockito.mock(JettyEndpoints.class);
transport.setJettyEndPoints(endpoints);
transport.notify(rescriptServiceBindingDescriptor);
transport.notify(jsonRpcBindingDescriptor);
transport.initialiseStaticJettyConfig();
populateTransportWithCORSEnabled(transport);
transport.onCougarStart();
assertEquals(3, transport.getHandlerCollection().getChildHandlersByClass(CrossOriginHandler.class).length);
// Instruct Jetty not to wait for Handlers to terminate
transport.getServerWrapper().getJettyServer().setStopTimeout(0);
transport.stop();
}
@Test
public void testCORSDisabledDoesNotAddHandlersToContextHandlerCollection() throws Exception {
ProtocolBinding pb = new ProtocolBinding("/", null, Protocol.RESCRIPT);
ProtocolBinding pb2 = new ProtocolBinding("/api", null, Protocol.RESCRIPT);
ProtocolBinding pb3 = new ProtocolBinding("/", null, Protocol.JSON_RPC);
Set<ProtocolBinding> bindingSet = new HashSet<ProtocolBinding>();
bindingSet.add(pb);
bindingSet.add(pb2);
bindingSet.add(pb3);
ProtocolBindingRegistry bindingReg = Mockito.mock(ProtocolBindingRegistry.class);
when(bindingReg.getProtocolBindings()).thenReturn(bindingSet);
JettyHttpTransport transport = new JettyHttpTransport();
populateTransport(transport);
transport.setProtocolBindingRegistry(bindingReg);
transport.setCommandProcessorFactory(factory);
JettyEndpoints endpoints = Mockito.mock(JettyEndpoints.class);
transport.setJettyEndPoints(endpoints);
transport.notify(rescriptServiceBindingDescriptor);
transport.notify(jsonRpcBindingDescriptor);
transport.initialiseStaticJettyConfig();
populateTransport(transport);
transport.onCougarStart();
assertEquals(0, transport.getHandlerCollection().getChildHandlersByClass(CrossOriginHandler.class).length);
// Instruct Jetty not to wait for Handlers to terminate
transport.getServerWrapper().getJettyServer().setStopTimeout(0);
transport.stop();
}
@Test
public void testEndpointListConstruction() throws Exception {
ProtocolBinding pb = new ProtocolBinding("/", null, Protocol.RESCRIPT);
ProtocolBinding pb2 = new ProtocolBinding("/api", null, Protocol.RESCRIPT);
ProtocolBinding pb3 = new ProtocolBinding("/", null, Protocol.JSON_RPC);
Set<ProtocolBinding> bindingSet = new HashSet<ProtocolBinding>();
bindingSet.add(pb);
bindingSet.add(pb2);
bindingSet.add(pb3);
ProtocolBindingRegistry bindingReg = Mockito.mock(ProtocolBindingRegistry.class);
when(bindingReg.getProtocolBindings()).thenReturn(bindingSet);
JettyHttpTransport transport = new JettyHttpTransport();
populateTransport(transport);
transport.setProtocolBindingRegistry(bindingReg);
transport.setCommandProcessorFactory(factory);
JettyEndpoints endpoints = Mockito.mock(JettyEndpoints.class);
ArgumentCaptor<List> captor = ArgumentCaptor.forClass(List.class);
transport.setJettyEndPoints(endpoints);
transport.notify(rescriptServiceBindingDescriptor);
transport.notify(jsonRpcBindingDescriptor);
transport.initialiseStaticJettyConfig();
transport.onCougarStart();
verify(endpoints).setEndPoints(captor.capture());
List<String> endpointValues = captor.getValue();
assertEquals("endpoints length incorrect", 3, endpointValues.size());
HttpServiceBindingDescriptor bindingDescriptor;
for (ProtocolBinding b : bindingSet) {
if (b.getProtocol() == Protocol.RESCRIPT) {
bindingDescriptor = rescriptServiceBindingDescriptor;
} else {
bindingDescriptor = jsonRpcBindingDescriptor;
}
String endpoint = b.getContextRoot() + bindingDescriptor.getServiceContextPath();
if (b.getProtocol() != Protocol.JSON_RPC) {
endpoint = endpoint + "v" + bindingDescriptor.getServiceVersion().getMajor() + OPERATION_URI;
} else {
endpoint = endpoint + "/";
}
boolean found = false;
for (int i = 0; i < endpointValues.size() && !found; i++) {
found = endpointValues.get(i).endsWith(endpoint);
}
assertTrue("endpoint was not found in list", found);
}
}
@Test
public void testSimpleStartStop() throws Exception {
// don't set any overrides
JettyHttpTransport transport = populateTransport();
// initialise
transport.start();
transport.stop();
}
@Test
public void testCantStartWithNoConnectors() throws Exception {
try {
JettyHttpTransport transport = populateTransport();
transport.getServerWrapper().setHttpPort(-1);
transport.getServerWrapper().setHttpsPort(-1);
transport.initialiseStaticJettyConfig();
fail("Transport shouldn't be able to start without connectors");
} catch (IllegalStateException ise) {
// expected
}
}
private String getServerKeystorePath() throws IOException {
String userDir = new File(System.getProperty("user.dir")).getCanonicalPath();
final String subDir = SEP + "cougar-framework" + SEP + "jetty-transport";
if (userDir.endsWith(subDir)) {
userDir = userDir.substring(0, userDir.indexOf(subDir));
}
final String cert = SEP + "src" + SEP + "test" + SEP + "resources" + SEP + "cougar_server_cert.jks";
return new File(userDir, subDir + cert).getCanonicalPath();
}
private String getServerTruststorePath() throws IOException {
String userDir = new File(System.getProperty("user.dir")).getCanonicalPath();
final String subDir = SEP + "cougar-framework" + SEP + "jetty-transport";
if (userDir.endsWith(subDir)) {
userDir = userDir.substring(0, userDir.indexOf(subDir));
}
final String cert = SEP + "src" + SEP + "test" + SEP + "resources" + SEP + "cougar_client_ca.jks";
return new File(userDir, subDir + cert).getCanonicalPath();
}
@Test
public void testOnlyHttpsConnectorCreated() throws Exception {
DummyTransport transport = new DummyTransport();
populateTransport(transport);
transport.getServerWrapper().setHttpPort(-1);
transport.getServerWrapper().setHttpsPort(9443);
transport.getServerWrapper().setHttpsKeystore(new FileSystemResource(getServerKeystorePath()));
transport.getServerWrapper().setHttpsKeyPassword("password");
transport.getServerWrapper().setHttpsKeystoreType("JKS");
transport.initialiseStaticJettyConfig();
assertFalse("Http transport should NOT have been created", transport.isHttpConnectorCreated());
assertTrue("Https transport should have been created", transport.isHttpsConnectorCreated());
}
@Test
public void testHttpsConnectorWithClientAuth() throws Exception {
DummyTransport transport = new DummyTransport();
populateTransport(transport);
transport.getServerWrapper().setHttpPort(-1);
transport.getServerWrapper().setHttpsPort(9443);
transport.getServerWrapper().setHttpsKeystore(new FileSystemResource(getServerKeystorePath()));
transport.getServerWrapper().setHttpsKeyPassword("password");
transport.getServerWrapper().setHttpsKeystoreType("JKS");
transport.getServerWrapper().setHttpsTruststore(new FileSystemResource(getServerTruststorePath()));
transport.getServerWrapper().setHttpsTrustPassword("password");
transport.getServerWrapper().setHttpsTruststoreType("JKS");
transport.getServerWrapper().setHttpsNeedClientAuth(true);
transport.initialiseStaticJettyConfig();
assertFalse("Http transport should NOT have been created", transport.isHttpConnectorCreated());
assertTrue("Https transport should have been created", transport.isHttpsConnectorCreated());
}
@Test
public void testBothConnectorsCreated() throws Exception {
DummyTransport transport = new DummyTransport();
populateTransport(transport);
transport.getServerWrapper().setHttpPort(TEST_HTTP_PORT);
transport.getServerWrapper().setHttpsPort(9443);
transport.getServerWrapper().setHttpsKeystore(new FileSystemResource(getServerKeystorePath()));
transport.getServerWrapper().setHttpsKeyPassword("password");
transport.getServerWrapper().setHttpsKeystoreType("JKS");
transport.getServerWrapper().setHttpsTruststore(new FileSystemResource(getServerTruststorePath()));
transport.getServerWrapper().setHttpsTrustPassword("password");
transport.getServerWrapper().setHttpsTruststoreType("JKS");
transport.initialiseStaticJettyConfig();
assertTrue("Http transport should have been created", transport.isHttpConnectorCreated());
assertTrue("Https transport should have been created", transport.isHttpsConnectorCreated());
}
@Test
public void testThreadCounts() throws Exception {
JettyHttpTransport transport = populateTransport();
ProtocolBindingRegistry bindingReg = Mockito.mock(ProtocolBindingRegistry.class);
when(bindingReg.getProtocolBindings()).thenReturn(new HashSet<ProtocolBinding>());
transport.setProtocolBindingRegistry(bindingReg);
JettyEndpoints endpoints = Mockito.mock(JettyEndpoints.class);
transport.setJettyEndPoints(endpoints);
transport.getServerWrapper().setHttpAcceptors(2);
transport.getServerWrapper().setHttpSelectors(4);
transport.getServerWrapper().setMaxThreads(8);
transport.getServerWrapper().setMinThreads(8);
transport.initialiseStaticJettyConfig();
transport.getServerWrapper().setThreadPoolName("testThreadCounts");
transport.onCougarStart();
long startTime = System.currentTimeMillis();
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
try {
while (true) {
try {
int acceptors = 0;
int selectors = 0;
int poolThreads = 0;
for (long l : threadMXBean.getAllThreadIds()) {
String threadName = threadMXBean.getThreadInfo(l).getThreadName();
if (threadName.matches("^testThreadCounts.*")) poolThreads++;
if (threadName.matches("^testThreadCounts.*acceptor.*")) acceptors++;
if (threadName.matches("^testThreadCounts.*selector.*")) selectors++;
}
assertEquals("Total thread count is not correct", 8, poolThreads);
assertEquals("Acceptor thread count is not correct", 2, acceptors);
// Jetty 9 threads don't become selectors automatically
// assertEquals("Selector thread count is not correct", 4, selectors);
break;
} catch (AssertionError e) {
if (System.currentTimeMillis() - startTime > 10 * 1000) {
throw e;
} else {
// give threads some time to take runnables
Thread.sleep(1000);
}
}
}
} finally {
transport.getServerWrapper().stop();
}
}
private class DummyTransport extends JettyHttpTransport {
public boolean isHttpConnectorCreated() {
return getServerWrapper().isHttpConnectorCreated();
}
public boolean isHttpsConnectorCreated() {
return getServerWrapper().isHttpsConnectorCreated();
}
}
}