]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-15918 making the new endpoint /api/push/sonarlint_events async
authorLukasz Jarocki <lukasz.jarocki@sonarsource.com>
Mon, 24 Jan 2022 09:01:00 +0000 (10:01 +0100)
committersonartech <sonartech@sonarsource.com>
Fri, 18 Feb 2022 15:48:03 +0000 (15:48 +0000)
23 files changed:
server/sonar-web/public/WEB-INF/web.xml
server/sonar-webserver-pushapi/build.gradle
server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/ServerPushAction.java
server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/sonarlint/SonarLintPushAction.java
server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/ServerPushWsTest.java
server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/sonarlint/SonarLintPushActionTest.java
server/sonar-webserver-pushapi/src/testFixtures/java/org/sonar/server/pushapi/DumbPushResponse.java [new file with mode: 0644]
server/sonar-webserver-pushapi/src/testFixtures/java/org/sonar/server/pushapi/TestPushRequest.java [new file with mode: 0644]
server/sonar-webserver-pushapi/src/testFixtures/java/org/sonar/server/pushapi/WsPushActionTester.java [new file with mode: 0644]
server/sonar-webserver-ws/build.gradle
server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/ServletRequest.java
server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/ServletResponse.java
server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/ServletRequestTest.java
server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/ServletResponseTest.java
server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/WebServiceEngineTest.java
server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/WsUtilsTest.java
server/sonar-webserver-ws/src/testFixtures/java/org/sonar/server/ws/DumbResponse.java
server/sonar-webserver-ws/src/testFixtures/java/org/sonar/server/ws/TestRequest.java
server/sonar-webserver-ws/src/testFixtures/java/org/sonar/server/ws/TestResponse.java
server/sonar-webserver-ws/src/testFixtures/java/org/sonar/server/ws/TestableResponse.java [new file with mode: 0644]
server/sonar-webserver-ws/src/testFixtures/java/org/sonar/server/ws/WsActionTester.java
server/sonar-webserver/src/main/java/org/sonar/server/app/TomcatAccessLog.java
sonar-testing-harness/build.gradle

index d2ba0126c87481a502ab0116649fea8c2077e9d9..da4795bfbeffbbca90d8d959b7f40cfde6cd6ea7 100644 (file)
@@ -2,23 +2,26 @@
 
 <web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xmlns="http://java.sun.com/xml/ns/javaee"
-  xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
-  id="SonarQube"
-  version="3.0"
-  metadata-complete="true">
+         xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
+         id="SonarQube"
+         version="3.0"
+         metadata-complete="true">
   <display-name>SonarQube</display-name>
 
   <filter>
     <filter-name>ServletFilters</filter-name>
     <filter-class>org.sonar.server.platform.web.MasterServletFilter</filter-class>
+    <async-supported>true</async-supported>
   </filter>
   <filter>
     <filter-name>UserSessionFilter</filter-name>
     <filter-class>org.sonar.server.platform.web.UserSessionFilter</filter-class>
+    <async-supported>true</async-supported>
   </filter>
   <filter>
     <filter-name>SetCharacterEncodingFilter</filter-name>
     <filter-class>org.apache.catalina.filters.SetCharacterEncodingFilter</filter-class>
+    <async-supported>true</async-supported>
     <init-param>
       <param-name>encoding</param-name>
       <param-value>UTF-8</param-value>
   <filter>
     <filter-name>SecurityFilter</filter-name>
     <filter-class>org.sonar.server.platform.web.SecurityServletFilter</filter-class>
+    <async-supported>true</async-supported>
   </filter>
   <filter>
     <filter-name>RootFilter</filter-name>
     <filter-class>org.sonar.server.platform.web.RootFilter</filter-class>
+    <async-supported>true</async-supported>
   </filter>
   <filter>
     <filter-name>RedirectFilter</filter-name>
     <filter-class>org.sonar.server.platform.web.RedirectFilter</filter-class>
+    <async-supported>true</async-supported>
   </filter>
   <filter>
     <filter-name>RequestUidFilter</filter-name>
     <filter-class>org.sonar.server.platform.web.RequestIdFilter</filter-class>
+    <async-supported>true</async-supported>
   </filter>
   <filter>
     <filter-name>WebPagesFilter</filter-name>
     <filter-class>org.sonar.server.platform.web.WebPagesFilter</filter-class>
+    <async-supported>true</async-supported>
   </filter>
   <filter>
     <filter-name>CacheControlFilter</filter-name>
     <filter-class>org.sonar.server.platform.web.CacheControlFilter</filter-class>
+    <async-supported>true</async-supported>
   </filter>
 
   <!-- order of execution is important -->
     <url-pattern>/*</url-pattern>
   </filter-mapping>
 
+  <servlet>
+    <servlet-name>default-servlet</servlet-name>
+    <servlet-class>
+      org.apache.catalina.servlets.DefaultServlet
+    </servlet-class>
+    <load-on-startup>1</load-on-startup>
+    <async-supported>true</async-supported>
+  </servlet>
+
+  <servlet-mapping>
+      <servlet-name>default</servlet-name>
+      <url-pattern>/</url-pattern>
+  </servlet-mapping>
+
   <servlet>
     <servlet-name>static</servlet-name>
     <servlet-class>org.sonar.server.platform.web.StaticResourcesServlet</servlet-class>
index 963886381b715e6dfc991d7bf8107e09b90b3993..a2c94714bd02bc09a3e327f24f060d0bc3211b4f 100644 (file)
@@ -5,12 +5,15 @@ sonarqube {
 }
 dependencies {
 
+    compile 'javax.servlet:javax.servlet-api'
     compile project(':server:sonar-webserver-auth')
     compile project(':server:sonar-webserver-ws')
 
     testCompile 'junit:junit'
     testCompile 'org.assertj:assertj-core'
     testCompile 'org.mockito:mockito-core'
-
     testCompile testFixtures(project(':server:sonar-webserver-ws'))
+
+    testFixturesApi project(':sonar-testing-harness')
+    testFixturesCompileOnly testFixtures(project(':server:sonar-webserver-ws'))
 }
index 405145b77acb407a4f4dffc58e9e26655193438d..fb613de127ae4129ab7fbc27e11ff03162e56bc6 100644 (file)
  */
 package org.sonar.server.pushapi;
 
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.util.Map;
+import javax.servlet.http.HttpServletResponse;
+import org.sonar.server.ws.ServletRequest;
+import org.sonar.server.ws.ServletResponse;
 import org.sonar.server.ws.WsAction;
 
-public interface ServerPushAction extends WsAction {
-  //marker interface
+public abstract class ServerPushAction implements WsAction {
+
+  protected boolean isServerSideEventsRequest(ServletRequest request) {
+    Map<String, String> headers = request.getHeaders();
+    String accept = headers.get("accept");
+    if (accept != null) {
+      return accept.contains("text/event-stream");
+    }
+    return false;
+  }
+
+  protected void setHeadersForResponse(ServletResponse response) throws IOException {
+    response.stream().setStatus(HttpServletResponse.SC_OK);
+    response.stream().setCharacterEncoding(StandardCharsets.UTF_8.name());
+    response.stream().setMediaType("text/event-stream");
+    // By adding this header, and not closing the connection,
+    // we disable HTTP chunking, and we can use write()+flush()
+    // to send data in the text/event-stream protocol
+    response.setHeader("Connection", "close");
+    response.stream().flushBuffer();
+  }
+
 
 }
index 3c73eefd492a122b388cd87e78a9accac9fc4821..eb72c1c4e81df4b3f6df05a6fd26903fce780500 100644 (file)
  */
 package org.sonar.server.pushapi.sonarlint;
 
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import javax.servlet.AsyncContext;
+import javax.servlet.http.HttpServletResponse;
 import org.sonar.api.server.ws.Request;
 import org.sonar.api.server.ws.Response;
 import org.sonar.api.server.ws.WebService;
 import org.sonar.api.utils.log.Logger;
 import org.sonar.api.utils.log.Loggers;
 import org.sonar.server.pushapi.ServerPushAction;
+import org.sonar.server.ws.ServletRequest;
+import org.sonar.server.ws.ServletResponse;
 
-public class SonarLintPushAction implements ServerPushAction {
+public class SonarLintPushAction extends ServerPushAction {
 
   private static final Logger LOGGER = Loggers.get(SonarLintPushAction.class);
 
@@ -56,14 +62,29 @@ public class SonarLintPushAction implements ServerPushAction {
   }
 
   @Override
-  public void handle(Request request, Response response) {
+  public void handle(Request request, Response response) throws IOException {
+    ServletRequest servletRequest = (ServletRequest) request;
+    ServletResponse servletResponse = (ServletResponse) response;
+
     String projectKeys = request.getParam(PROJECT_PARAM_KEY).getValue();
     String languages = request.getParam(LANGUAGE_PARAM_KEY).getValue();
 
-    //to remove later
+    // to remove later
     LOGGER.debug(projectKeys != null ? projectKeys : "");
     LOGGER.debug(languages != null ? languages : "");
 
-    response.noContent();
+    AsyncContext asyncContext = servletRequest.startAsync();
+    asyncContext.setTimeout(0);
+
+    if (!isServerSideEventsRequest(servletRequest)) {
+      servletResponse.stream().setStatus(HttpServletResponse.SC_NOT_ACCEPTABLE);
+      return;
+    }
+
+    setHeadersForResponse(servletResponse);
+
+    //test response to remove later
+    response.stream().output().write("Hello world".getBytes(StandardCharsets.UTF_8));
+    response.stream().output().flush();
   }
 }
index 184044a2ea433503ac7fda64be96059ee915a9d3..3b5d46670758222486c84c7ba9d6abb8bdae65af 100644 (file)
@@ -46,7 +46,7 @@ public class ServerPushWsTest {
     assertThat(controller.actions()).isNotEmpty();
   }
 
-  private static class DummyServerPushAction implements ServerPushAction {
+  private static class DummyServerPushAction extends ServerPushAction {
 
     @Override
     public void define(WebService.NewController context) {
index 87a7203a40ab2cde098696d84668398549878612..f6e9f5c7187ca99273dc953a8c83b1c69c2e60e8 100644 (file)
@@ -21,9 +21,9 @@ package org.sonar.server.pushapi.sonarlint;
 
 import org.junit.Test;
 import org.sonar.api.server.ws.WebService;
-import org.sonar.server.ws.TestRequest;
+import org.sonar.server.pushapi.TestPushRequest;
+import org.sonar.server.pushapi.WsPushActionTester;
 import org.sonar.server.ws.TestResponse;
-import org.sonar.server.ws.WsActionTester;
 
 import static org.assertj.core.api.Assertions.assertThat;
 import static org.assertj.core.api.Assertions.assertThatThrownBy;
@@ -31,7 +31,7 @@ import static org.assertj.core.api.Assertions.tuple;
 
 public class SonarLintPushActionTest {
 
-  private final WsActionTester ws = new WsActionTester(new SonarLintPushAction());
+  private final WsPushActionTester ws = new WsPushActionTester(new SonarLintPushAction());
 
   @Test
   public void defineTest() {
@@ -45,18 +45,31 @@ public class SonarLintPushActionTest {
   }
 
   @Test
-  public void handle_returnsNoResponseWhenParamsProvided() {
-    TestResponse response = ws.newRequest()
+  public void handle_returnsNoResponseWhenParamsAndHeadersProvided() {
+    TestResponse response = ws.newPushRequest()
       .setParam("projectKeys", "project1,project2")
       .setParam("languages", "java")
+      .setHeader("accept", "text/event-stream")
       .execute();
 
-    assertThat(response.getStatus()).isEqualTo(204);
+    assertThat(response.getInput()).isEqualTo("Hello world");
+  }
+
+  @Test
+  public void handle_whenAcceptHeaderNotProvided_statusCode406() {
+    TestResponse testResponse = ws.newPushRequest().
+       setParam("projectKeys", "project1,project2")
+      .setParam("languages", "java")
+      .execute();
+
+    assertThat(testResponse.getStatus()).isEqualTo(406);
   }
 
   @Test
   public void handle_whenParamsNotProvided_throwException() {
-    TestRequest testRequest = ws.newRequest();
+    TestPushRequest testRequest =  ws.newPushRequest()
+      .setHeader("accept", "text/event-stream");
+
     assertThatThrownBy(testRequest::execute)
       .isInstanceOf(IllegalArgumentException.class)
       .hasMessage("The 'projectKeys' parameter is missing");
diff --git a/server/sonar-webserver-pushapi/src/testFixtures/java/org/sonar/server/pushapi/DumbPushResponse.java b/server/sonar-webserver-pushapi/src/testFixtures/java/org/sonar/server/pushapi/DumbPushResponse.java
new file mode 100644 (file)
index 0000000..d8bd464
--- /dev/null
@@ -0,0 +1,139 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.pushapi;
+
+import com.google.common.base.Throwables;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.net.HttpURLConnection;
+import java.nio.charset.StandardCharsets;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import javax.annotation.CheckForNull;
+import javax.servlet.http.HttpServletResponse;
+import org.apache.commons.io.IOUtils;
+import org.sonar.api.server.ws.Response;
+import org.sonar.api.utils.text.JsonWriter;
+import org.sonar.api.utils.text.XmlWriter;
+import org.sonar.server.ws.ServletResponse;
+import org.sonar.server.ws.TestableResponse;
+
+import static org.mockito.Mockito.mock;
+
+public class DumbPushResponse extends ServletResponse implements TestableResponse {
+
+  public DumbPushResponse() {
+    super(mock(HttpServletResponse.class));
+  }
+
+  private DumbPushResponse.InMemoryStream stream;
+
+  private final ByteArrayOutputStream output = new ByteArrayOutputStream();
+
+  private Map<String, String> headers = new HashMap<>();
+
+  public class InMemoryStream extends ServletStream {
+    private String mediaType;
+
+    private int status = 200;
+
+    public InMemoryStream() {
+      super(mock(HttpServletResponse.class));
+    }
+
+    @Override
+    public ServletStream setMediaType(String s) {
+      this.mediaType = s;
+      return this;
+    }
+
+    @Override
+    public ServletStream setStatus(int i) {
+      this.status = i;
+      return this;
+    }
+
+    @Override
+    public OutputStream output() {
+      return output;
+    }
+  }
+
+  @Override
+  public JsonWriter newJsonWriter() {
+    return JsonWriter.of(new OutputStreamWriter(output, StandardCharsets.UTF_8));
+  }
+
+  @Override
+  public XmlWriter newXmlWriter() {
+    return XmlWriter.of(new OutputStreamWriter(output, StandardCharsets.UTF_8));
+  }
+
+  @Override
+  public ServletStream stream() {
+    if (stream == null) {
+      stream = new DumbPushResponse.InMemoryStream();
+    }
+    return stream;
+  }
+
+  @Override
+  public Response noContent() {
+    stream().setStatus(HttpURLConnection.HTTP_NO_CONTENT);
+    IOUtils.closeQuietly(output);
+    return this;
+  }
+
+  @CheckForNull
+  public String mediaType() {
+    return ((DumbPushResponse.InMemoryStream) stream()).mediaType;
+  }
+
+  public int status() {
+    return ((InMemoryStream) stream()).status;
+  }
+
+  @Override
+  public Response setHeader(String name, String value) {
+    headers.put(name, value);
+    return this;
+  }
+
+  public Collection<String> getHeaderNames() {
+    return headers.keySet();
+  }
+
+  @CheckForNull
+  public String getHeader(String name) {
+    return headers.get(name);
+  }
+
+  public byte[] getFlushedOutput() {
+    try {
+      output.flush();
+      return output.toByteArray();
+    } catch (IOException e) {
+      throw Throwables.propagate(e);
+    }
+  }
+}
diff --git a/server/sonar-webserver-pushapi/src/testFixtures/java/org/sonar/server/pushapi/TestPushRequest.java b/server/sonar-webserver-pushapi/src/testFixtures/java/org/sonar/server/pushapi/TestPushRequest.java
new file mode 100644 (file)
index 0000000..ace34b5
--- /dev/null
@@ -0,0 +1,98 @@
+package org.sonar.server.pushapi;/*
+ * SonarQube
+ * Copyright (C) 2009-2021 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import com.google.common.base.Throwables;
+import java.util.Map;
+import java.util.Optional;
+import javax.servlet.AsyncContext;
+import org.sonar.server.ws.ServletRequest;
+import org.sonar.server.ws.TestRequest;
+import org.sonar.server.ws.TestResponse;
+
+import static org.mockito.Mockito.mock;
+
+public class TestPushRequest extends ServletRequest {
+
+  private TestRequest testRequest = new TestRequest();
+
+  public TestPushRequest() {
+    super(null);
+  }
+
+  @Override
+  public AsyncContext startAsync() {
+    return mock(AsyncContext.class);
+  }
+
+  @Override
+  public String method() {
+    return testRequest.method();
+  }
+
+  @Override
+  public boolean hasParam(String key) {
+    return testRequest.hasParam(key);
+  }
+
+  @Override
+  public Map<String, String[]> getParams() {
+    return testRequest.getParams();
+  }
+
+  @Override
+  public String readParam(String key) {
+    return testRequest.readParam(key);
+  }
+
+  @Override
+  public String getMediaType() {
+    return testRequest.getMediaType();
+  }
+
+  public TestPushRequest setParam(String key, String value) {
+    testRequest.setParam(key, value);
+    return this;
+  }
+
+  @Override
+  public Map<String, String> getHeaders() {
+    return testRequest.getHeaders();
+  }
+
+  @Override
+  public Optional<String> header(String name) {
+    return testRequest.header(name);
+  }
+
+  public TestPushRequest setHeader(String name, String value) {
+    testRequest.setHeader(name, value);
+    return this;
+  }
+
+  public TestResponse execute() {
+    try {
+      DumbPushResponse response = new DumbPushResponse();
+      action().handler().handle(this, response);
+      return new TestResponse(response);
+    } catch (Exception e) {
+      throw Throwables.propagate(e);
+    }
+  }
+}
diff --git a/server/sonar-webserver-pushapi/src/testFixtures/java/org/sonar/server/pushapi/WsPushActionTester.java b/server/sonar-webserver-pushapi/src/testFixtures/java/org/sonar/server/pushapi/WsPushActionTester.java
new file mode 100644 (file)
index 0000000..cb52c1c
--- /dev/null
@@ -0,0 +1,34 @@
+package org.sonar.server.pushapi;/*
+ * SonarQube
+ * Copyright (C) 2009-2021 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+
+import org.sonar.server.ws.WsAction;
+import org.sonar.server.ws.WsActionTester;
+
+public class WsPushActionTester extends WsActionTester {
+  public WsPushActionTester(WsAction wsAction) {
+    super(wsAction);
+  }
+
+  public TestPushRequest newPushRequest() {
+    TestPushRequest request = new TestPushRequest();
+    request.setAction(action);
+    return request;
+  }
+}
index 33d0249b7f1eef1440733d23e6d59ff1104b5332..3fa8fed9bb6767cac589d929ade51be3a1709cb9 100644 (file)
@@ -20,6 +20,8 @@ dependencies {
   compileOnly 'javax.servlet:javax.servlet-api'
   compileOnly 'org.apache.tomcat.embed:tomcat-embed-core'
 
+  testCompile 'com.tngtech.java:junit-dataprovider'
+  testCompile 'junit:junit'
   testCompile 'com.google.code.findbugs:jsr305'
   testCompile 'javax.servlet:javax.servlet-api'
   testCompile 'org.apache.tomcat.embed:tomcat-embed-core'
index 89226f6d9a97930d88e4d1f58fb177272676bc71..cfc18bb29698d283d015559d8164f30085573d60 100644 (file)
@@ -30,6 +30,7 @@ import java.util.List;
 import java.util.Map;
 import java.util.Optional;
 import javax.annotation.CheckForNull;
+import javax.servlet.AsyncContext;
 import javax.servlet.http.HttpServletRequest;
 import org.sonar.api.impl.ws.PartImpl;
 import org.sonar.api.impl.ws.ValidatingRequest;
@@ -122,6 +123,10 @@ public class ServletRequest extends ValidatingRequest {
     }
   }
 
+  public AsyncContext startAsync() {
+    return source.startAsync();
+  }
+
   private boolean isMultipartContent() {
     String contentType = source.getContentType();
     return contentType != null && contentType.toLowerCase(ENGLISH).startsWith(MULTIPART);
index f02e380142a76c450a5712225e8669e3aa25ee4d..8d294b4552e51773767f0aa1a35747648367175d 100644 (file)
@@ -80,6 +80,16 @@ public class ServletResponse implements Response {
       response.reset();
       return this;
     }
+
+    public ServletStream flushBuffer() throws IOException {
+      response.flushBuffer();
+      return this;
+    }
+
+    public ServletStream setCharacterEncoding(String charset) {
+      response.setCharacterEncoding(charset);
+      return this;
+    }
   }
 
   @Override
index 79f57aed7b882ff016ee97f39e72c93ca23f5b1c..159be0e20eb6cc58a39755f39f598b2ea072b08b 100644 (file)
@@ -210,4 +210,11 @@ public class ServletRequestTest {
     assertThat(underTest.getReader()).isEqualTo(reader);
   }
 
+  @Test
+  public void startAsync() {
+    underTest.startAsync();
+
+    verify(source).startAsync();
+  }
+
 }
index c5099cd677c2258b9d57152d73968e153254208f..19cbc9f93f824d6ca09f215cd27dd4915b7a2651 100644 (file)
@@ -19,6 +19,8 @@
  */
 package org.sonar.server.ws;
 
+import java.io.IOException;
+import java.nio.charset.Charset;
 import javax.servlet.ServletOutputStream;
 import javax.servlet.http.HttpServletResponse;
 import org.junit.Before;
@@ -83,6 +85,20 @@ public class ServletResponseTest {
     verify(response).setStatus(404);
   }
 
+  @Test
+  public void setCharacterEncoding_encodingIsSet() {
+    underTest.stream().setCharacterEncoding("UTF-8");
+
+    verify(response).setCharacterEncoding("UTF-8");
+  }
+
+  @Test
+  public void flushBuffer_bufferIsFlushed() throws IOException {
+    underTest.stream().flushBuffer();
+
+    verify(response).flushBuffer();
+  }
+
   @Test
   public void test_output() {
     assertThat(underTest.stream().output()).isEqualTo(output);
index 6e4e694d67b11630f36817073410f73ded1e55ea..e138a6bc316311f8c43d1e4dcf1e3fb984ee395a 100644 (file)
  */
 package org.sonar.server.ws;
 
+import com.tngtech.java.junit.dataprovider.DataProvider;
+import com.tngtech.java.junit.dataprovider.DataProviderRunner;
+import com.tngtech.java.junit.dataprovider.UseDataProvider;
 import java.util.function.Consumer;
 import javax.servlet.http.HttpServletResponse;
 import org.apache.catalina.connector.ClientAbortException;
 import org.junit.Rule;
 import org.junit.Test;
+import org.junit.runner.RunWith;
 import org.mockito.Mockito;
 import org.sonar.api.server.ws.Request;
 import org.sonar.api.server.ws.RequestHandler;
@@ -45,6 +49,7 @@ import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
 import static org.mockito.Mockito.when;
 
+@RunWith(DataProviderRunner.class)
 public class WebServiceEngineTest {
 
   @Rule
@@ -68,37 +73,27 @@ public class WebServiceEngineTest {
     }
   }
 
-  @Test
-  public void ws_returns_successful_response() {
-    Request request = new TestRequest().setPath("/api/ping");
-
-    DumbResponse response = run(request, newPingWs(a -> {
-    }));
-
-    assertThat(response.stream().outputAsString()).isEqualTo("pong");
-    assertThat(response.stream().status()).isEqualTo(200);
+  @DataProvider
+  public static Object[][] responseData() {
+    return new Object[][] {
+      {"/api/ping", "pong", 200},
+      {"api/ping", "pong", 200},
+      {"api/ping.json", "pong", 200},
+      {"xxx/ping", "{\"errors\":[{\"msg\":\"Unknown url : xxx/ping\"}]}", 404},
+      {"api/xxx", "{\"errors\":[{\"msg\":\"Unknown url : api/xxx\"}]}", 404}
+    };
   }
 
   @Test
-  public void accept_path_that_does_not_start_with_slash() {
-    Request request = new TestRequest().setPath("api/ping");
+  @UseDataProvider("responseData")
+  public void ws_returns_successful_response(String path, String output, int statusCode) {
+    Request request = new TestRequest().setPath(path);
 
     DumbResponse response = run(request, newPingWs(a -> {
     }));
 
-    assertThat(response.stream().outputAsString()).isEqualTo("pong");
-    assertThat(response.stream().status()).isEqualTo(200);
-  }
-
-  @Test
-  public void request_path_can_contain_valid_media_type() {
-    Request request = new TestRequest().setPath("api/ping.json");
-
-    DumbResponse response = run(request, newPingWs(a -> {
-    }));
-
-    assertThat(response.stream().outputAsString()).isEqualTo("pong");
-    assertThat(response.stream().status()).isEqualTo(200);
+    assertThat(response.stream().outputAsString()).isEqualTo(output);
+    assertThat(response.status()).isEqualTo(statusCode);
   }
 
   @Test
@@ -108,8 +103,8 @@ public class WebServiceEngineTest {
     DumbResponse response = run(request, newPingWs(a -> {
     }));
 
-    assertThat(response.stream().status()).isEqualTo(400);
-    assertThat(response.stream().mediaType()).isEqualTo(MediaTypes.JSON);
+    assertThat(response.status()).isEqualTo(400);
+    assertThat(response.mediaType()).isEqualTo(MediaTypes.JSON);
     assertThat(response.stream().outputAsString()).isEqualTo("{\"errors\":[{\"msg\":\"Unknown action extension: bat\"}]}");
   }
 
@@ -121,29 +116,7 @@ public class WebServiceEngineTest {
     DumbResponse response = run(request, newWs("api/foo", a -> a.setHandler(handler)));
 
     assertThat(response.stream().outputAsString()).isEmpty();
-    assertThat(response.stream().status()).isEqualTo(204);
-  }
-
-  @Test
-  public void return_404_if_controller_does_not_exist() {
-    Request request = new TestRequest().setPath("xxx/ping");
-
-    DumbResponse response = run(request, newPingWs(a -> {
-    }));
-
-    assertThat(response.stream().outputAsString()).isEqualTo("{\"errors\":[{\"msg\":\"Unknown url : xxx/ping\"}]}");
-    assertThat(response.stream().status()).isEqualTo(404);
-  }
-
-  @Test
-  public void return_404_if_action_does_not_exist() {
-    Request request = new TestRequest().setPath("api/xxx");
-
-    DumbResponse response = run(request, newPingWs(a -> {
-    }));
-
-    assertThat(response.stream().outputAsString()).isEqualTo("{\"errors\":[{\"msg\":\"Unknown url : api/xxx\"}]}");
-    assertThat(response.stream().status()).isEqualTo(404);
+    assertThat(response.status()).isEqualTo(204);
   }
 
   @Test
@@ -153,7 +126,7 @@ public class WebServiceEngineTest {
     DumbResponse response = run(request, newWs("api/foo", a -> a.setPost(true)));
 
     assertThat(response.stream().outputAsString()).isEqualTo("{\"errors\":[{\"msg\":\"HTTP method POST is required\"}]}");
-    assertThat(response.stream().status()).isEqualTo(405);
+    assertThat(response.status()).isEqualTo(405);
   }
 
   @Test
@@ -164,7 +137,7 @@ public class WebServiceEngineTest {
     }));
 
     assertThat(response.stream().outputAsString()).isEqualTo("pong");
-    assertThat(response.stream().status()).isEqualTo(200);
+    assertThat(response.status()).isEqualTo(200);
   }
 
   @Test
@@ -175,7 +148,7 @@ public class WebServiceEngineTest {
     }));
 
     assertThat(response.stream().outputAsString()).isEqualTo("{\"errors\":[{\"msg\":\"HTTP method PUT is not allowed\"}]}");
-    assertThat(response.stream().status()).isEqualTo(405);
+    assertThat(response.status()).isEqualTo(405);
   }
 
   @Test
@@ -186,7 +159,7 @@ public class WebServiceEngineTest {
     }));
 
     assertThat(response.stream().outputAsString()).isEqualTo("{\"errors\":[{\"msg\":\"HTTP method DELETE is not allowed\"}]}");
-    assertThat(response.stream().status()).isEqualTo(405);
+    assertThat(response.status()).isEqualTo(405);
   }
 
   @Test
@@ -196,7 +169,7 @@ public class WebServiceEngineTest {
     DumbResponse response = run(request, newPingWs(a -> a.setPost(true)));
 
     assertThat(response.stream().outputAsString()).isEqualTo("pong");
-    assertThat(response.stream().status()).isEqualTo(200);
+    assertThat(response.status()).isEqualTo(200);
   }
 
   @Test
@@ -206,7 +179,7 @@ public class WebServiceEngineTest {
     DumbResponse response = run(request, newWs("api/foo", a -> a.setHandler((req, resp) -> request.param("unknown"))));
 
     assertThat(response.stream().outputAsString()).isEqualTo("{\"errors\":[{\"msg\":\"BUG - parameter \\u0027unknown\\u0027 is undefined for action \\u0027foo\\u0027\"}]}");
-    assertThat(response.stream().status()).isEqualTo(400);
+    assertThat(response.status()).isEqualTo(400);
   }
 
   @Test
@@ -219,7 +192,7 @@ public class WebServiceEngineTest {
     }));
 
     assertThat(response.stream().outputAsString()).isEqualTo("{\"errors\":[{\"msg\":\"The \\u0027bar\\u0027 parameter is missing\"}]}");
-    assertThat(response.stream().status()).isEqualTo(400);
+    assertThat(response.status()).isEqualTo(400);
   }
 
   @Test
@@ -233,7 +206,7 @@ public class WebServiceEngineTest {
     }));
 
     assertThat(response.stream().outputAsString()).isEqualTo("{\"errors\":[{\"msg\":\"The \\u0027bar\\u0027 parameter is missing\"}]}");
-    assertThat(response.stream().status()).isEqualTo(400);
+    assertThat(response.status()).isEqualTo(400);
   }
 
   @Test
@@ -246,7 +219,7 @@ public class WebServiceEngineTest {
     }));
 
     assertThat(response.stream().outputAsString()).isEqualTo("hello");
-    assertThat(response.stream().status()).isEqualTo(200);
+    assertThat(response.status()).isEqualTo(200);
   }
 
   @Test
@@ -259,7 +232,7 @@ public class WebServiceEngineTest {
     }));
 
     assertThat(response.stream().outputAsString()).isEqualTo("bar");
-    assertThat(response.stream().status()).isEqualTo(200);
+    assertThat(response.status()).isEqualTo(200);
   }
 
   @Test
@@ -272,7 +245,7 @@ public class WebServiceEngineTest {
     }));
 
     assertThat(response.stream().outputAsString()).isEqualTo("json");
-    assertThat(response.stream().status()).isEqualTo(200);
+    assertThat(response.status()).isEqualTo(200);
   }
 
   @Test
@@ -285,7 +258,7 @@ public class WebServiceEngineTest {
     }));
 
     assertThat(response.stream().outputAsString()).isEqualTo("{\"errors\":[{\"msg\":\"Value of parameter \\u0027format\\u0027 (yml) must be one of: [json, xml]\"}]}");
-    assertThat(response.stream().status()).isEqualTo(400);
+    assertThat(response.status()).isEqualTo(400);
   }
 
   @Test
@@ -295,8 +268,8 @@ public class WebServiceEngineTest {
     DumbResponse response = run(request, newFailWs());
 
     assertThat(response.stream().outputAsString()).isEqualTo("{\"errors\":[{\"msg\":\"An error has occurred. Please contact your administrator\"}]}");
-    assertThat(response.stream().status()).isEqualTo(500);
-    assertThat(response.stream().mediaType()).isEqualTo(MediaTypes.JSON);
+    assertThat(response.status()).isEqualTo(500);
+    assertThat(response.mediaType()).isEqualTo(MediaTypes.JSON);
     assertThat(logTester.logs(LoggerLevel.ERROR)).filteredOn(l -> l.contains("Fail to process request api/foo")).isNotEmpty();
   }
 
@@ -310,8 +283,8 @@ public class WebServiceEngineTest {
 
     assertThat(response.stream().outputAsString()).isEqualTo(
       "{\"errors\":[{\"msg\":\"Bad request !\"}]}");
-    assertThat(response.stream().status()).isEqualTo(400);
-    assertThat(response.stream().mediaType()).isEqualTo(MediaTypes.JSON);
+    assertThat(response.status()).isEqualTo(400);
+    assertThat(response.mediaType()).isEqualTo(MediaTypes.JSON);
     assertThat(logTester.logs(LoggerLevel.ERROR)).isEmpty();
   }
 
@@ -328,8 +301,8 @@ public class WebServiceEngineTest {
       + "{\"msg\":\"two\"},"
       + "{\"msg\":\"three\"}"
       + "]}");
-    assertThat(response.stream().status()).isEqualTo(400);
-    assertThat(response.stream().mediaType()).isEqualTo(MediaTypes.JSON);
+    assertThat(response.status()).isEqualTo(400);
+    assertThat(response.mediaType()).isEqualTo(MediaTypes.JSON);
     assertThat(logTester.logs(LoggerLevel.ERROR)).isEmpty();
   }
 
@@ -343,8 +316,8 @@ public class WebServiceEngineTest {
 
     assertThat(response.stream().outputAsString()).isEqualTo(
         "{\"scope\":\"PROJECT\",\"errors\":[{\"msg\":\"Bad request !\"}]}");
-    assertThat(response.stream().status()).isEqualTo(400);
-    assertThat(response.stream().mediaType()).isEqualTo(MediaTypes.JSON);
+    assertThat(response.status()).isEqualTo(400);
+    assertThat(response.mediaType()).isEqualTo(MediaTypes.JSON);
     assertThat(logTester.logs(LoggerLevel.ERROR)).isEmpty();
   }
 
@@ -357,8 +330,8 @@ public class WebServiceEngineTest {
     })));
 
     assertThat(response.stream().outputAsString()).isEqualTo("{\"errors\":[{\"msg\":\"this should not fail %s\"}]}");
-    assertThat(response.stream().status()).isEqualTo(400);
-    assertThat(response.stream().mediaType()).isEqualTo(MediaTypes.JSON);
+    assertThat(response.status()).isEqualTo(400);
+    assertThat(response.mediaType()).isEqualTo(MediaTypes.JSON);
   }
 
   @Test
index 71a4cbcc1c344a24d665087990ed405a3f95ccc7..79bb015322c89c39ab8b65ef9c97a325d0aa7b5e 100644 (file)
@@ -43,7 +43,7 @@ public class WsUtilsTest {
     Issues.Issue msg = Issues.Issue.newBuilder().setKey("I1").build();
     WsUtils.writeProtobuf(msg, request, response);
 
-    assertThat(response.stream().mediaType()).isEqualTo(MediaTypes.JSON);
+    assertThat(response.mediaType()).isEqualTo(MediaTypes.JSON);
     assertThat(response.outputAsString())
       .startsWith("{")
       .contains("\"key\":\"I1\"")
@@ -59,7 +59,7 @@ public class WsUtilsTest {
     Issues.Issue msg = Issues.Issue.newBuilder().setKey("I1").build();
     WsUtils.writeProtobuf(msg, request, response);
 
-    assertThat(response.stream().mediaType()).isEqualTo(MediaTypes.PROTOBUF);
+    assertThat(response.mediaType()).isEqualTo(MediaTypes.PROTOBUF);
     assertThat(Issues.Issue.parseFrom(response.getFlushedOutput()).getKey()).isEqualTo("I1");
   }
 
index 9b91d24949b5498ffb5351432dfa11af30ec6ecf..adebb870aba7aaba26cdde17e3c1eb7c2cd01558 100644 (file)
@@ -35,7 +35,7 @@ import org.sonar.api.server.ws.Response;
 import org.sonar.api.utils.text.JsonWriter;
 import org.sonar.api.utils.text.XmlWriter;
 
-public class DumbResponse implements Response {
+public class DumbResponse implements Response, TestableResponse {
   private InMemoryStream stream;
 
   private final ByteArrayOutputStream output = new ByteArrayOutputStream();
@@ -47,15 +47,6 @@ public class DumbResponse implements Response {
 
     private int status = 200;
 
-    @CheckForNull
-    public String mediaType() {
-      return mediaType;
-    }
-
-    public int status() {
-      return status;
-    }
-
     @Override
     public Response.Stream setMediaType(String s) {
       this.mediaType = s;
@@ -107,6 +98,15 @@ public class DumbResponse implements Response {
     return new String(output.toByteArray(), StandardCharsets.UTF_8);
   }
 
+  @CheckForNull
+  public String mediaType() {
+    return stream().mediaType;
+  }
+
+  public int status() {
+    return stream().status;
+  }
+
   @Override
   public Response setHeader(String name, String value) {
     headers.put(name, value);
index dc083b685adaf82e613d339558f09d95cd09469a..d37f7a1e00f05a96932410006245edae53f2819f 100644 (file)
@@ -73,17 +73,17 @@ public class TestRequest extends ValidatingRequest {
   }
 
   @Override
-  protected String readParam(String key) {
+  public String readParam(String key) {
     return params.get(key);
   }
 
   @Override
-  protected List<String> readMultiParam(String key) {
+  public List<String> readMultiParam(String key) {
     return multiParams.get(key);
   }
 
   @Override
-  protected InputStream readInputStreamParam(String key) {
+  public InputStream readInputStreamParam(String key) {
     String value = readParam(key);
     if (value == null) {
       return null;
@@ -92,7 +92,7 @@ public class TestRequest extends ValidatingRequest {
   }
 
   @Override
-  protected Part readPart(String key) {
+  public Part readPart(String key) {
     return parts.get(key);
   }
 
index c2511c2231000f76b06803b1739d42b79016318f..6fa82315e9032af0c48f637ac3a02a98ef022591 100644 (file)
@@ -33,14 +33,14 @@ import static org.assertj.core.api.Assertions.assertThat;
 
 public class TestResponse {
 
-  private final DumbResponse dumbResponse;
+  private final TestableResponse testableResponse;
 
-  public TestResponse(DumbResponse dumbResponse) {
-    this.dumbResponse = dumbResponse;
+  public TestResponse(TestableResponse dumbResponse) {
+    this.testableResponse = dumbResponse;
   }
 
   public InputStream getInputStream() {
-    return new ByteArrayInputStream(dumbResponse.getFlushedOutput());
+    return new ByteArrayInputStream(testableResponse.getFlushedOutput());
   }
 
   public <T extends GeneratedMessageV3> T getInputObject(Class<T> protobufClass) {
@@ -55,20 +55,20 @@ public class TestResponse {
   }
 
   public String getInput() {
-    return new String(dumbResponse.getFlushedOutput(), StandardCharsets.UTF_8);
+    return new String(testableResponse.getFlushedOutput(), StandardCharsets.UTF_8);
   }
 
   public String getMediaType() {
-    return dumbResponse.stream().mediaType();
+    return testableResponse.mediaType();
   }
 
   public int getStatus() {
-    return dumbResponse.stream().status();
+    return testableResponse.status();
   }
 
   @CheckForNull
   public String getHeader(String headerKey) {
-    return dumbResponse.getHeader(headerKey);
+    return testableResponse.getHeader(headerKey);
   }
 
   public void assertJson(String expectedJson) {
diff --git a/server/sonar-webserver-ws/src/testFixtures/java/org/sonar/server/ws/TestableResponse.java b/server/sonar-webserver-ws/src/testFixtures/java/org/sonar/server/ws/TestableResponse.java
new file mode 100644 (file)
index 0000000..056c9e6
--- /dev/null
@@ -0,0 +1,35 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2021 SonarSource SA
+ * mailto:info AT sonarsource DOT com
+ *
+ * This program is free software; you can redistribute it and/or
+ * modify it under the terms of the GNU Lesser General Public
+ * License as published by the Free Software Foundation; either
+ * version 3 of the License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this program; if not, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
+ */
+package org.sonar.server.ws;
+
+import org.sonar.api.server.ws.Response;
+
+public interface TestableResponse {
+
+  byte[] getFlushedOutput();
+
+  Response.Stream stream();
+
+  String getHeader(String headerKey);
+
+  int status();
+
+  String mediaType();
+}
index dbe31b268e4614a3369b9dad482082785dd18658..863a5aaec1ac10303e6f75858ba57688356ba321 100644 (file)
@@ -25,7 +25,7 @@ import org.sonar.api.server.ws.WebService;
 public class WsActionTester {
 
   public static final String CONTROLLER_KEY = "test";
-  private final WebService.Action action;
+  protected final WebService.Action action;
 
   public WsActionTester(WsAction wsAction) {
     WebService.Context context = new WebService.Context();
index bd39d2cde7bb418a3e75f942f6ff1db1fe86ff9f..81a6b0a1b8eca4609ce99169a1c524a55859c4be 100644 (file)
@@ -54,6 +54,7 @@ class TomcatAccessLog {
       appender.setEncoder(fileEncoder);
       appender.start();
       valve.addAppender(appender);
+      valve.setAsyncSupported(true);
       tomcat.getHost().getPipeline().addValve(valve);
     }
   }
index 80da028df67fa514fa059f5485ceb2a1a7d0d36e..d2e4c7b6ab7b4aca148fb831365b9ee3b4b246ee 100644 (file)
@@ -17,6 +17,7 @@ dependencies {
   compile 'org.assertj:assertj-core'
   compile 'org.hamcrest:hamcrest-core'
   compile 'org.jsoup:jsoup'
+  compile 'org.mockito:mockito-core'
 
   compileOnly 'com.google.code.findbugs:jsr305'
 }