]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-4741 HTTPS mode
authorSimon Brandhof <simon.brandhof@gmail.com>
Sun, 13 Oct 2013 23:00:12 +0000 (01:00 +0200)
committerSimon Brandhof <simon.brandhof@gmail.com>
Sun, 13 Oct 2013 23:00:12 +0000 (01:00 +0200)
sonar-application/pom.xml
sonar-application/src/main/assembly/conf/sonar.properties
sonar-application/src/main/java/org/sonar/application/Connectors.java
sonar-application/src/main/java/org/sonar/application/EmbeddedTomcat.java
sonar-application/src/main/java/org/sonar/application/Logging.java
sonar-application/src/main/java/org/sonar/application/Props.java
sonar-application/src/main/java/org/sonar/application/Webapp.java
sonar-application/src/test/java/org/sonar/application/ConnectorsTest.java
sonar-application/src/test/java/org/sonar/application/LoggingTest.java
sonar-application/src/test/java/org/sonar/application/PropsTest.java
sonar-application/src/test/java/org/sonar/application/WebappTest.java

index a55c24d7c998332866254bd82d2d90e0e4cdded8..5a63c20575e4ba19793136ecd1ab3d91edf9cf7c 100644 (file)
       <artifactId>http-request</artifactId>
       <scope>test</scope>
     </dependency>
+    <dependency>
+      <groupId>com.google.guava</groupId>
+      <artifactId>guava</artifactId>
+      <scope>test</scope>
+    </dependency>
+
   </dependencies>
 
   <build>
index 3b4fc79cd0e4c87a9da8b621771468b1a6f96f53..1ef1d031316dca88145c31e4021872d5f383a873 100644 (file)
 
 
 
-#--------------------------------------------------------------------------------------------------
-# WEB SERVER
-
-# Binding address
-#sonar.web.host=0.0.0.0
-
-# TCP port for incoming HTTP connections
-#sonar.web.port=9000
-
-# Web context must start with slash (/)
-#sonar.web.context=/
-
-# The maximum number of connections that the server will accept and process at any given time.
-# When this number has been reached, the server will not accept any more connections until
-# the number of connections falls below this value. The operating system may still accept connections
-# based on the sonar.web.connections.acceptCount property. The default value is 50.
-#sonar.web.connections.maxThreads=50
-
-# The minimum number of threads always kept running. If not specified, the default of 5 is used.
-#sonar.web.connections.minThreads=5
-
-# The maximum queue length for incoming connection requests when all possible request processing
-# threads are in use. Any requests received when the queue is full will be refused.
-# The default value is 25.
-#sonar.web.connections.acceptCount=25
-
-
-
 #--------------------------------------------------------------------------------------------------
 # DATABASE
 #
@@ -111,6 +83,70 @@ sonar.jdbc.timeBetweenEvictionRunsMillis=30000
 
 
 
+#--------------------------------------------------------------------------------------------------
+# WEB SERVER
+
+# Binding address
+#sonar.web.host=0.0.0.0
+
+# Web context. When set, it must start with forward slash (for example /sonarqube).
+# The default value is root context (empty value).
+#sonar.web.context=
+
+# TCP port for incoming HTTP connections
+#sonar.web.http.enable=true
+#sonar.web.port=9000
+
+# TCP port for incoming HTTPS connections. Disabled by default.
+#sonar.web.https.enable=false
+#sonar.web.https.port=9443
+
+# HTTPS - the alias used to for the server certificate in the keystore.
+# If not specified the first key read in the keystore is used.
+#sonar.web.https.keyAlias=
+
+# HTTPS - the password used to access the server certificate from the
+# specified keystore file. The default value is "changeit".
+#sonar.web.https.keyPass=changeit
+
+# HTTPS - the pathname of the keystore file where is stored the server certificate.
+# By default, the pathname is the file ".keystore" in the user home.
+# If keystoreType doesn't need a file use empty value.
+#sonar.web.https.keystoreFile=
+
+# HTTPS - the password used to access the specified keystore file. The default
+# value is the value of sonar.web.https.keyPass.
+#sonar.web.https.keystorePass=
+
+# HTTPS - the type of keystore file to be used for the server certificate.
+# The default value is JKS (Java KeyStore).
+#sonar.web.https.keystoreType=JKS
+
+# HTTPS - the name of the keystore provider to be used for the server certificate.
+# If not specified, the list of registered providers is traversed in preference order
+# and the first provider that supports the keystore type is used (see sonar.web.https.keystoreType).
+#sonar.web.https.keystoreProvider=
+
+# The maximum number of connections that the server will accept and process at any given time.
+# When this number has been reached, the server will not accept any more connections until
+# the number of connections falls below this value. The operating system may still accept connections
+# based on the sonar.web.connections.acceptCount property. The default value is 50 for each
+# enabled connector.
+#sonar.web.http.maxThreads=50
+#sonar.web.https.maxThreads=50
+
+# The minimum number of threads always kept running. If not specified, the default of 5 is used.
+#sonar.web.http.minThreads=5
+#sonar.web.https.minThreads=5
+
+# The maximum queue length for incoming connection requests when all possible request processing
+# threads are in use. Any requests received when the queue is full will be refused.
+# The default value is 25 for each enabled connector.
+#sonar.web.http.acceptCount=25
+#sonar.web.https.acceptCount=25
+
+
+
 #--------------------------------------------------------------------------------------------------
 # UPDATE CENTER
 
index 9dac5526d6ed61a29928aaed53f3407dd9d94de5..77609d1820d412312b3538503c0d341e04beece0 100644 (file)
@@ -21,42 +21,99 @@ package org.sonar.application;
 
 import org.apache.catalina.connector.Connector;
 import org.apache.catalina.startup.Tomcat;
+import org.slf4j.LoggerFactory;
 
-class Connectors {
+import javax.annotation.Nullable;
 
-  static final String PROPERTY_SHUTDOWN_TOKEN = "sonar.web.shutdown.token";
-  static final String PROPERTY_SHUTDOWN_PORT = "sonar.web.shutdown.port";
-  static final String PROPERTY_MIN_THREADS = "sonar.web.connections.minThreads";
-  static final String PROPERTY_MAX_THREADS = "sonar.web.connections.maxThreads";
-  static final String PROPERTY_ACCEPT_COUNT = "sonar.web.connections.acceptCount";
+class Connectors {
 
   static void configure(Tomcat tomcat, Props props) {
     tomcat.getServer().setAddress(props.of("sonar.web.host", "0.0.0.0"));
     configureShutdown(tomcat, props);
+    configureConnectors(tomcat, props);
+  }
 
-    Connector connector = new Connector("HTTP/1.1");
-    connector.setPort(props.intOf("sonar.web.port", 9000));
-    connector.setURIEncoding("UTF-8");
-    configurePool(props, connector);
-    configureCompression(connector);
-    tomcat.setConnector(connector);
-    tomcat.getService().addConnector(connector);
+  private static void configureConnectors(Tomcat tomcat, Props props) {
+    Connector http = newHttpConnector(props);
+    Connector https = newHttpsConnector(props);
+
+    if (http == null) {
+      if (https == null) {
+        throw new IllegalStateException("Both HTTP and HTTPS connectors are disabled");
+      }
+      // Enable only HTTPS
+      tomcat.setConnector(https);
+      tomcat.getService().addConnector(https);
+    } else {
+      // Both HTTP and HTTPS are enabled
+      tomcat.setConnector(http);
+      tomcat.getService().addConnector(http);
+      if (https != null) {
+        tomcat.getService().addConnector(https);
+      }
+    }
   }
 
   private static void configureShutdown(Tomcat tomcat, Props props) {
-    String shutdownToken = props.of(PROPERTY_SHUTDOWN_TOKEN);
-    Integer shutdownPort = props.intOf(PROPERTY_SHUTDOWN_PORT);
+    String shutdownToken = props.of("sonar.web.shutdown.token");
+    Integer shutdownPort = props.intOf("sonar.web.shutdown.port");
     if (shutdownToken != null && !"".equals(shutdownToken) && shutdownPort != null) {
       tomcat.getServer().setPort(shutdownPort);
       tomcat.getServer().setShutdown(shutdownToken);
+      info("Shutdown command is enabled on port " + shutdownPort);
+    }
+  }
+
+  @Nullable
+  private static Connector newHttpConnector(Props props) {
+    Connector connector = null;
+    if (props.booleanOf("sonar.web.http.enable", true)) {
+      connector = newConnector(props, "http");
+      // Not named "sonar.web.http.port" to keep backward-compatibility
+      int port = props.intOf("sonar.web.port", 9000);
+      connector.setPort(port);
+      info("HTTP connector is enabled on port " + port);
+    }
+    return connector;
+  }
+
+  @Nullable
+  private static Connector newHttpsConnector(Props props) {
+    Connector connector = null;
+    if (props.booleanOf("sonar.web.https.enable")) {
+      connector = newConnector(props, "https");
+      int port = props.intOf("sonar.web.https.port", 9443);
+      connector.setPort(port);
+      connector.setSecure(true);
+      connector.setScheme("https");
+      setConnectorAttribute(connector, "keyAlias", props.of("sonar.web.https.keyAlias"));
+      String keyPassword = props.of("sonar.web.https.keyPass", "changeit");
+      setConnectorAttribute(connector, "keyPass", keyPassword);
+      setConnectorAttribute(connector, "keystorePass", props.of("sonar.web.https.keystorePass", keyPassword));
+      setConnectorAttribute(connector, "keystoreFile", props.of("sonar.web.https.keystoreFile"));
+      setConnectorAttribute(connector, "keystoreType", props.of("sonar.web.https.keystoreType", "JKS"));
+      setConnectorAttribute(connector, "keystoreProvider", props.of("sonar.web.https.keystoreProvider"));
+      setConnectorAttribute(connector, "clientAuth", false);
+      setConnectorAttribute(connector, "sslProtocol", "TLS");
+      setConnectorAttribute(connector, "SSLEnabled", true);
+      info("HTTPS connector is enabled on port " + port);
     }
+    return connector;
+  }
+
+  private static Connector newConnector(Props props, String scheme) {
+    Connector connector = new Connector("HTTP/1.1");
+    connector.setURIEncoding("UTF-8");
+    configurePool(props, connector, scheme);
+    configureCompression(connector);
+    return connector;
   }
 
-  private static void configurePool(Props props, Connector connector) {
+  private static void configurePool(Props props, Connector connector, String scheme) {
     connector.setProperty("acceptorThreadCount", String.valueOf(2));
-    connector.setProperty("minSpareThreads", String.valueOf(props.intOf(PROPERTY_MIN_THREADS, 5)));
-    connector.setProperty("maxThreads", String.valueOf(props.intOf(PROPERTY_MAX_THREADS, 50)));
-    connector.setProperty("acceptCount", String.valueOf(props.intOf(PROPERTY_ACCEPT_COUNT, 25)));
+    connector.setProperty("minSpareThreads", String.valueOf(props.intOf("sonar.web." + scheme + ".minThreads", 5)));
+    connector.setProperty("maxThreads", String.valueOf(props.intOf("sonar.web." + scheme + ".maxThreads", 50)));
+    connector.setProperty("acceptCount", String.valueOf(props.intOf("sonar.web." + scheme + ".acceptCount", 25)));
   }
 
   private static void configureCompression(Connector connector) {
@@ -64,4 +121,14 @@ class Connectors {
     connector.setProperty("compressionMinSize", "1024");
     connector.setProperty("compressableMimeType", "text/html,text/xml,text/plain,text/css,application/json,application/javascript");
   }
+
+  private static void setConnectorAttribute(Connector c, String key, @Nullable Object value) {
+    if (value != null) {
+      c.setAttribute(key, value);
+    }
+  }
+
+  private static void info(String message) {
+    LoggerFactory.getLogger(Connectors.class).info(message);
+  }
 }
index 4838e3d2202a2110c3fac25586105d8ff2b370fc..b92783982d3a792ebdf07dc03071a79d9d6cd1df 100644 (file)
  */
 package org.sonar.application;
 
+import org.apache.catalina.LifecycleEvent;
 import org.apache.catalina.LifecycleException;
+import org.apache.catalina.LifecycleListener;
+import org.apache.catalina.connector.Connector;
 import org.apache.catalina.startup.Tomcat;
 import org.apache.commons.io.FileUtils;
 
@@ -67,7 +70,6 @@ class EmbeddedTomcat {
     Logging.configure(tomcat, env);
     Connectors.configure(tomcat, props);
     Webapp.configure(tomcat, env, props);
-
     tomcat.start();
     addShutdownHook();
     tomcat.getServer().await();
@@ -117,6 +119,10 @@ class EmbeddedTomcat {
   }
 
   int port() {
-    return tomcat.getService().findConnectors()[0].getLocalPort();
+    Connector[] connectors = tomcat.getService().findConnectors();
+    if (connectors.length > 0) {
+      return connectors[0].getLocalPort();
+    }
+    return -1;
   }
 }
index 29d8d097698eb6f7529e96da6ba81cb5c28bc768..ae5a5a4d6b3e8b569a309f9c99eb836ca6e43874 100644 (file)
 package org.sonar.application;
 
 import ch.qos.logback.access.tomcat.LogbackValve;
+import org.apache.catalina.LifecycleEvent;
+import org.apache.catalina.LifecycleListener;
 import org.apache.catalina.startup.Tomcat;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 import org.slf4j.bridge.SLF4JBridgeHandler;
 
 import java.io.File;
@@ -38,6 +42,8 @@ class Logging {
 
   static void configure(Tomcat tomcat, Env env) {
     tomcat.setSilent(false);
+    tomcat.getService().addLifecycleListener(new StartupLogger(LoggerFactory.getLogger(Logging.class)));
+
     LogbackValve valve = new LogbackValve();
     valve.setQuiet(true);
     File confFile = env.file(CONF_PATH);
@@ -47,4 +53,20 @@ class Logging {
     valve.setFilename(confFile.getAbsolutePath());
     tomcat.getHost().getPipeline().addValve(valve);
   }
+
+  static class StartupLogger implements LifecycleListener {
+    private Logger logger;
+
+    StartupLogger(Logger logger) {
+      this.logger = logger;
+    }
+
+    @Override
+    public void lifecycleEvent(LifecycleEvent event) {
+      if ("after_start".equals(event.getType())) {
+        logger.info("Web server is up");
+      }
+    }
+  }
+
 }
index f720553c18a008f8855e06bb1d4388d9c917f7cb..4e0bb6c3d20bd1e77609e3a72e80185ccc807648 100644 (file)
@@ -29,7 +29,7 @@ import java.io.IOException;
 import java.util.Properties;
 
 /**
- * TODO support env substitution
+ * TODO support env substitution and encryption
  */
 class Props {
   private final Properties props;
@@ -52,6 +52,11 @@ class Props {
     return s != null && Boolean.parseBoolean(s);
   }
 
+  boolean booleanOf(String key, boolean defaultValue) {
+    String s = of(key);
+    return s != null ? Boolean.parseBoolean(s) : defaultValue;
+  }
+
   Integer intOf(String key) {
     String s = of(key);
     if (s != null && !"".equals(s)) {
index aa85ea7d1b55c207d6ac5a0118efaf95aa7a353f..89f84c3603a3e6e2b0aa87cfb7d4c59b00a304c5 100644 (file)
@@ -28,7 +28,7 @@ class Webapp {
   private static final String RAILS_ENV = "rails.env";
 
   static void configure(Tomcat tomcat, Env env, Props props) {
-    String ctx = props.of("sonar.web.context", "/");
+    String ctx = getContext(props);
     try {
       Context context = tomcat.addWebapp(ctx, env.file("web").getAbsolutePath());
       context.setConfigFile(env.file("web/META-INF/context.xml").toURI().toURL());
@@ -40,6 +40,14 @@ class Webapp {
     }
   }
 
+  static String getContext(Props props) {
+    String context = props.of("sonar.web.context", "");
+    if (!"".equals(context) && !context.startsWith("/")) {
+      throw new IllegalStateException("Value of sonar.web.context must start with a forward slash: " + context);
+    }
+    return context;
+  }
+
   static void configureRailsMode(Props props, Context context) {
     if (props.booleanOf("sonar.web.dev")) {
       context.addParameter(RAILS_ENV, "development");
index b40b79b32f0af2723a19bb658dfd314a58c171e3..c1594b2c46af99974f8172480365de1ed36ffc39 100644 (file)
  */
 package org.sonar.application;
 
+import com.google.common.collect.ImmutableMap;
 import org.apache.catalina.connector.Connector;
 import org.apache.catalina.startup.Tomcat;
 import org.junit.Test;
 import org.mockito.ArgumentMatcher;
 import org.mockito.Mockito;
 
+import java.util.Map;
 import java.util.Properties;
 
+import static org.fest.assertions.Assertions.assertThat;
+import static org.fest.assertions.Fail.fail;
 import static org.mockito.Matchers.anyInt;
 import static org.mockito.Matchers.anyString;
 import static org.mockito.Matchers.argThat;
-import static org.mockito.Mockito.mock;
-import static org.mockito.Mockito.never;
-import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.*;
 
 public class ConnectorsTest {
+
+  Tomcat tomcat = mock(Tomcat.class, Mockito.RETURNS_DEEP_STUBS);
+
+  //---- connectors
+
   @Test
-  public void enable_shutdown_port() throws Exception {
+  public void configure_thread_pool() throws Exception {
     Properties p = new Properties();
-    p.setProperty(Connectors.PROPERTY_SHUTDOWN_PORT, "9010");
-    p.setProperty(Connectors.PROPERTY_SHUTDOWN_TOKEN, "SHUTDOWN");
+    p.setProperty("sonar.web.http.minThreads", "2");
+    p.setProperty("sonar.web.http.maxThreads", "30");
+    p.setProperty("sonar.web.http.acceptCount", "20");
     Props props = new Props(p);
 
-    Tomcat tomcat = mock(Tomcat.class, Mockito.RETURNS_DEEP_STUBS);
     Connectors.configure(tomcat, props);
 
-    verify(tomcat.getServer()).setPort(9010);
-    verify(tomcat.getServer()).setShutdown("SHUTDOWN");
+    verify(tomcat).setConnector(argThat(new PropertiesMatcher(
+      ImmutableMap.<String, Object>of("minSpareThreads", 2, "maxThreads", 30, "acceptCount", 20)
+    )));
   }
 
   @Test
-  public void disable_shutdown_port_by_default() throws Exception {
+  public void configure_default_thread_pool() throws Exception {
     Props props = new Props(new Properties());
 
-    Tomcat tomcat = mock(Tomcat.class, Mockito.RETURNS_DEEP_STUBS);
     Connectors.configure(tomcat, props);
 
-    verify(tomcat.getServer(), never()).setPort(anyInt());
-    verify(tomcat.getServer(), never()).setShutdown(anyString());
+    verify(tomcat).setConnector(argThat(new PropertiesMatcher(
+      ImmutableMap.<String, Object>of("minSpareThreads", 5, "maxThreads", 50, "acceptCount", 25)
+    )));
   }
 
   @Test
-  public void disable_shutdown_port_if_missing_token() throws Exception {
+  public void fail_if_both_http_and_https_are_disabled() {
     Properties p = new Properties();
-    // only the port, but not the token
-    p.setProperty(Connectors.PROPERTY_SHUTDOWN_PORT, "9010");
+    p.setProperty("sonar.web.http.enable", "false");
+    p.setProperty("sonar.web.https.enable", "false");
     Props props = new Props(p);
 
-    Tomcat tomcat = mock(Tomcat.class, Mockito.RETURNS_DEEP_STUBS);
-    Connectors.configure(tomcat, props);
-
-    verify(tomcat.getServer(), never()).setPort(anyInt());
-    verify(tomcat.getServer(), never()).setShutdown(anyString());
+    try {
+      Connectors.configure(tomcat, props);
+      fail();
+    } catch (IllegalStateException e) {
+      assertThat(e.getMessage()).isEqualTo("Both HTTP and HTTPS connectors are disabled");
+    }
   }
 
   @Test
-  public void configure_thread_pool() throws Exception {
+  public void only_https_is_enabled() {
     Properties p = new Properties();
-    p.setProperty(Connectors.PROPERTY_MIN_THREADS, "2");
-    p.setProperty(Connectors.PROPERTY_MAX_THREADS, "30");
-    p.setProperty(Connectors.PROPERTY_ACCEPT_COUNT, "20");
+    p.setProperty("sonar.web.http.enable", "false");
+    p.setProperty("sonar.web.https.enable", "true");
     Props props = new Props(p);
 
-    Tomcat tomcat = mock(Tomcat.class, Mockito.RETURNS_DEEP_STUBS);
     Connectors.configure(tomcat, props);
 
     verify(tomcat).setConnector(argThat(new ArgumentMatcher<Connector>() {
       @Override
       public boolean matches(Object o) {
         Connector c = (Connector)o;
-        return (Integer)c.getProperty("minSpareThreads") == 2 &&
-          (Integer) c.getProperty("maxThreads") == 30 &&
-          (Integer) c.getProperty("acceptCount") == 20;
+        return c.getScheme().equals("https") && c.getPort()==9443;
       }
     }));
   }
 
   @Test
-  public void configure_default_thread_pool() throws Exception {
-    Props props = new Props(new Properties());
+  public void both_http_and_https_are_enabled() {
+    Properties p = new Properties();
+    p.setProperty("sonar.web.http.enable", "true");
+    p.setProperty("sonar.web.https.enable", "true");
+    Props props = new Props(p);
 
-    Tomcat tomcat = mock(Tomcat.class, Mockito.RETURNS_DEEP_STUBS);
     Connectors.configure(tomcat, props);
 
-    verify(tomcat).setConnector(argThat(new ArgumentMatcher<Connector>() {
+    verify(tomcat.getService()).addConnector(argThat(new ArgumentMatcher<Connector>() {
       @Override
       public boolean matches(Object o) {
         Connector c = (Connector)o;
-        return (Integer)c.getProperty("minSpareThreads") == 5 &&
-          (Integer) c.getProperty("maxThreads") == 50 &&
-          (Integer) c.getProperty("acceptCount") == 25;
+        return c.getScheme().equals("http") && c.getPort()==9000;
       }
     }));
+    verify(tomcat.getService()).addConnector(argThat(new ArgumentMatcher<Connector>() {
+      @Override
+      public boolean matches(Object o) {
+        Connector c = (Connector)o;
+        return c.getScheme().equals("https") && c.getPort()==9443;
+      }
+    }));
+  }
+
+
+  //---- shutdown port
+
+  @Test
+  public void enable_shutdown_port() throws Exception {
+    Properties p = new Properties();
+    p.setProperty("sonar.web.shutdown.port", "9010");
+    p.setProperty("sonar.web.shutdown.token", "SHUTDOWN");
+    Props props = new Props(p);
+
+    Connectors.configure(tomcat, props);
+
+    verify(tomcat.getServer()).setPort(9010);
+    verify(tomcat.getServer()).setShutdown("SHUTDOWN");
+  }
+
+  @Test
+  public void disable_shutdown_port_by_default() throws Exception {
+    Props props = new Props(new Properties());
+
+    Connectors.configure(tomcat, props);
+
+    verify(tomcat.getServer(), never()).setPort(anyInt());
+    verify(tomcat.getServer(), never()).setShutdown(anyString());
+  }
+
+  @Test
+  public void disable_shutdown_port_if_missing_token() throws Exception {
+    Properties p = new Properties();
+    // only the port, but not the token
+    p.setProperty("sonar.web.shutdown.port", "9010");
+    Props props = new Props(p);
+
+    Connectors.configure(tomcat, props);
+
+    verify(tomcat.getServer(), never()).setPort(anyInt());
+    verify(tomcat.getServer(), never()).setShutdown(anyString());
+  }
+
+  private static class PropertiesMatcher extends ArgumentMatcher<Connector> {
+    private final Map<String, Object> expected;
+
+    PropertiesMatcher(Map<String, Object> expected) {
+      this.expected = expected;
+    }
+
+    public boolean matches(Object o) {
+      Connector c = (Connector) o;
+      for (Map.Entry<String, Object> entry : expected.entrySet()) {
+        if (!entry.getValue().equals(c.getProperty(entry.getKey()))) {
+          return false;
+        }
+      }
+      return true;
+    }
   }
 }
index ca5f6ef3c517259ca2ef655b76dc11da3a6546ca..39808394b887221847e9f424debd2db656a97921 100644 (file)
 package org.sonar.application;
 
 import ch.qos.logback.access.tomcat.LogbackValve;
+import org.apache.catalina.Lifecycle;
+import org.apache.catalina.LifecycleEvent;
 import org.apache.catalina.Valve;
 import org.apache.catalina.startup.Tomcat;
 import org.junit.Test;
 import org.mockito.ArgumentMatcher;
 import org.mockito.Mockito;
+import org.slf4j.Logger;
 
 import java.io.File;
 
@@ -65,4 +68,18 @@ public class LoggingTest {
       assertThat(e).hasMessage("File is missing: " + confFile.getAbsolutePath());
     }
   }
+
+  @Test
+  public void log_when_started() {
+    Logger logger = mock(Logger.class);
+    Logging.StartupLogger listener = new Logging.StartupLogger(logger);
+
+    LifecycleEvent event = new LifecycleEvent(mock(Lifecycle.class), "before_init", null);
+    listener.lifecycleEvent(event);
+    verifyZeroInteractions(logger);
+
+    event = new LifecycleEvent(mock(Lifecycle.class), "after_start", null);
+    listener.lifecycleEvent(event);
+    verify(logger).info("Web server is up");
+  }
 }
index f6c1f2ece0165ad034ec2c7a16c356a8e3aa9f78..20ca51e516107248c48e70b99a8845c330628626 100644 (file)
@@ -84,6 +84,19 @@ public class PropsTest {
     assertThat(props.booleanOf("unknown")).isFalse();
   }
 
+  @Test
+  public void booleanOf_default_value() throws Exception {
+    Properties p = new Properties();
+    p.setProperty("foo", "true");
+    p.setProperty("bar", "false");
+    Props props = new Props(p);
+
+    assertThat(props.booleanOf("unset", false)).isFalse();
+    assertThat(props.booleanOf("unset", true)).isTrue();
+    assertThat(props.booleanOf("foo", false)).isTrue();
+    assertThat(props.booleanOf("bar", true)).isFalse();
+  }
+
   @Test
   public void load_file_and_system_properties() throws Exception {
     Env env = mock(Env.class);
index 0e0dd904a1c4e6f72dcdd22e3d72eff79ca671d0..aa1ba82eee8a045fb817c793d50c331bd892eb6a 100644 (file)
@@ -26,6 +26,7 @@ import org.junit.Test;
 import org.junit.rules.TemporaryFolder;
 
 import java.io.File;
+import java.util.Properties;
 
 import static org.fest.assertions.Assertions.assertThat;
 import static org.fest.assertions.Fail.fail;
@@ -46,7 +47,7 @@ public class WebappTest {
     when(tomcat.addContext("", webDir.getAbsolutePath())).thenThrow(new NullPointerException());
 
     try {
-      Webapp.configure(tomcat, env, mock(Props.class));
+      Webapp.configure(tomcat, env, new Props(new Properties()));
       fail();
     } catch (IllegalStateException e) {
       assertThat(e).hasMessage("Fail to configure webapp");
@@ -76,4 +77,23 @@ public class WebappTest {
     verify(context).addParameter("jruby.max.runtimes", "1");
     verify(context).addParameter("rails.env", "production");
   }
+
+  @Test
+  public void context_must_start_with_slash() throws Exception {
+    Properties p = new Properties();
+    p.setProperty("sonar.web.context", "foo");
+
+    try {
+      Webapp.getContext(new Props(p));
+      fail();
+    } catch (IllegalStateException e) {
+      assertThat(e.getMessage()).isEqualTo("Value of sonar.web.context must start with a forward slash: foo");
+    }
+  }
+
+  @Test
+  public void default_context_is_root() throws Exception {
+    String context = Webapp.getContext(new Props(new Properties()));
+    assertThat(context).isEqualTo("");
+  }
 }