diff options
author | Lukasz Jarocki <lukasz.jarocki@sonarsource.com> | 2022-01-24 10:01:00 +0100 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2022-02-18 15:48:03 +0000 |
commit | a9e3735302b847752a6abd60397f91fb9b91e4b7 (patch) | |
tree | 4ce1976e6eec8d77f5da6f3a7a17a700ee125d96 /server | |
parent | dd5c24bc96a10a3b81bd1c94d40b936ba451cd90 (diff) | |
download | sonarqube-a9e3735302b847752a6abd60397f91fb9b91e4b7.tar.gz sonarqube-a9e3735302b847752a6abd60397f91fb9b91e4b7.zip |
SONAR-15918 making the new endpoint /api/push/sonarlint_events async
Diffstat (limited to 'server')
22 files changed, 521 insertions, 115 deletions
diff --git a/server/sonar-web/public/WEB-INF/web.xml b/server/sonar-web/public/WEB-INF/web.xml index d2ba0126c87..da4795bfbef 100644 --- a/server/sonar-web/public/WEB-INF/web.xml +++ b/server/sonar-web/public/WEB-INF/web.xml @@ -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> @@ -27,26 +30,32 @@ <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 --> @@ -88,6 +97,20 @@ </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> </servlet> diff --git a/server/sonar-webserver-pushapi/build.gradle b/server/sonar-webserver-pushapi/build.gradle index 963886381b7..a2c94714bd0 100644 --- a/server/sonar-webserver-pushapi/build.gradle +++ b/server/sonar-webserver-pushapi/build.gradle @@ -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')) } diff --git a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/ServerPushAction.java b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/ServerPushAction.java index 405145b77ac..fb613de127a 100644 --- a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/ServerPushAction.java +++ b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/ServerPushAction.java @@ -19,9 +19,35 @@ */ 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(); + } + } diff --git a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/sonarlint/SonarLintPushAction.java b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/sonarlint/SonarLintPushAction.java index 3c73eefd492..eb72c1c4e81 100644 --- a/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/sonarlint/SonarLintPushAction.java +++ b/server/sonar-webserver-pushapi/src/main/java/org/sonar/server/pushapi/sonarlint/SonarLintPushAction.java @@ -19,14 +19,20 @@ */ 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(); } } diff --git a/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/ServerPushWsTest.java b/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/ServerPushWsTest.java index 184044a2ea4..3b5d4667075 100644 --- a/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/ServerPushWsTest.java +++ b/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/ServerPushWsTest.java @@ -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) { diff --git a/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/sonarlint/SonarLintPushActionTest.java b/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/sonarlint/SonarLintPushActionTest.java index 87a7203a40a..f6e9f5c7187 100644 --- a/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/sonarlint/SonarLintPushActionTest.java +++ b/server/sonar-webserver-pushapi/src/test/java/org/sonar/server/pushapi/sonarlint/SonarLintPushActionTest.java @@ -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 index 00000000000..d8bd4641f0a --- /dev/null +++ b/server/sonar-webserver-pushapi/src/testFixtures/java/org/sonar/server/pushapi/DumbPushResponse.java @@ -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 index 00000000000..ace34b5db09 --- /dev/null +++ b/server/sonar-webserver-pushapi/src/testFixtures/java/org/sonar/server/pushapi/TestPushRequest.java @@ -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 index 00000000000..cb52c1cff32 --- /dev/null +++ b/server/sonar-webserver-pushapi/src/testFixtures/java/org/sonar/server/pushapi/WsPushActionTester.java @@ -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; + } +} diff --git a/server/sonar-webserver-ws/build.gradle b/server/sonar-webserver-ws/build.gradle index 33d0249b7f1..3fa8fed9bb6 100644 --- a/server/sonar-webserver-ws/build.gradle +++ b/server/sonar-webserver-ws/build.gradle @@ -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' diff --git a/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/ServletRequest.java b/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/ServletRequest.java index 89226f6d9a9..cfc18bb2969 100644 --- a/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/ServletRequest.java +++ b/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/ServletRequest.java @@ -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); diff --git a/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/ServletResponse.java b/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/ServletResponse.java index f02e380142a..8d294b4552e 100644 --- a/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/ServletResponse.java +++ b/server/sonar-webserver-ws/src/main/java/org/sonar/server/ws/ServletResponse.java @@ -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 diff --git a/server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/ServletRequestTest.java b/server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/ServletRequestTest.java index 79f57aed7b8..159be0e20eb 100644 --- a/server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/ServletRequestTest.java +++ b/server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/ServletRequestTest.java @@ -210,4 +210,11 @@ public class ServletRequestTest { assertThat(underTest.getReader()).isEqualTo(reader); } + @Test + public void startAsync() { + underTest.startAsync(); + + verify(source).startAsync(); + } + } diff --git a/server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/ServletResponseTest.java b/server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/ServletResponseTest.java index c5099cd677c..19cbc9f93f8 100644 --- a/server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/ServletResponseTest.java +++ b/server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/ServletResponseTest.java @@ -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; @@ -84,6 +86,20 @@ public class ServletResponseTest { } @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); } diff --git a/server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/WebServiceEngineTest.java b/server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/WebServiceEngineTest.java index 6e4e694d67b..e138a6bc316 100644 --- a/server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/WebServiceEngineTest.java +++ b/server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/WebServiceEngineTest.java @@ -19,11 +19,15 @@ */ 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 diff --git a/server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/WsUtilsTest.java b/server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/WsUtilsTest.java index 71a4cbcc1c3..79bb015322c 100644 --- a/server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/WsUtilsTest.java +++ b/server/sonar-webserver-ws/src/test/java/org/sonar/server/ws/WsUtilsTest.java @@ -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"); } diff --git a/server/sonar-webserver-ws/src/testFixtures/java/org/sonar/server/ws/DumbResponse.java b/server/sonar-webserver-ws/src/testFixtures/java/org/sonar/server/ws/DumbResponse.java index 9b91d24949b..adebb870aba 100644 --- a/server/sonar-webserver-ws/src/testFixtures/java/org/sonar/server/ws/DumbResponse.java +++ b/server/sonar-webserver-ws/src/testFixtures/java/org/sonar/server/ws/DumbResponse.java @@ -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); diff --git a/server/sonar-webserver-ws/src/testFixtures/java/org/sonar/server/ws/TestRequest.java b/server/sonar-webserver-ws/src/testFixtures/java/org/sonar/server/ws/TestRequest.java index dc083b685ad..d37f7a1e00f 100644 --- a/server/sonar-webserver-ws/src/testFixtures/java/org/sonar/server/ws/TestRequest.java +++ b/server/sonar-webserver-ws/src/testFixtures/java/org/sonar/server/ws/TestRequest.java @@ -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); } diff --git a/server/sonar-webserver-ws/src/testFixtures/java/org/sonar/server/ws/TestResponse.java b/server/sonar-webserver-ws/src/testFixtures/java/org/sonar/server/ws/TestResponse.java index c2511c22310..6fa82315e90 100644 --- a/server/sonar-webserver-ws/src/testFixtures/java/org/sonar/server/ws/TestResponse.java +++ b/server/sonar-webserver-ws/src/testFixtures/java/org/sonar/server/ws/TestResponse.java @@ -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 index 00000000000..056c9e68019 --- /dev/null +++ b/server/sonar-webserver-ws/src/testFixtures/java/org/sonar/server/ws/TestableResponse.java @@ -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(); +} diff --git a/server/sonar-webserver-ws/src/testFixtures/java/org/sonar/server/ws/WsActionTester.java b/server/sonar-webserver-ws/src/testFixtures/java/org/sonar/server/ws/WsActionTester.java index dbe31b268e4..863a5aaec1a 100644 --- a/server/sonar-webserver-ws/src/testFixtures/java/org/sonar/server/ws/WsActionTester.java +++ b/server/sonar-webserver-ws/src/testFixtures/java/org/sonar/server/ws/WsActionTester.java @@ -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(); diff --git a/server/sonar-webserver/src/main/java/org/sonar/server/app/TomcatAccessLog.java b/server/sonar-webserver/src/main/java/org/sonar/server/app/TomcatAccessLog.java index bd39d2cde7b..81a6b0a1b8e 100644 --- a/server/sonar-webserver/src/main/java/org/sonar/server/app/TomcatAccessLog.java +++ b/server/sonar-webserver/src/main/java/org/sonar/server/app/TomcatAccessLog.java @@ -54,6 +54,7 @@ class TomcatAccessLog { appender.setEncoder(fileEncoder); appender.start(); valve.addAppender(appender); + valve.setAsyncSupported(true); tomcat.getHost().getPipeline().addValve(valve); } } |