Skip to content

Commit

Permalink
PLUGINAPI-42 Add unit tests and mutualize UrlPattern
Browse files Browse the repository at this point in the history
  • Loading branch information
ericg138 committed Apr 18, 2023
1 parent 8c10020 commit 1212873
Show file tree
Hide file tree
Showing 7 changed files with 257 additions and 380 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@
*/
public interface HttpRequest {


/**
* Returns the port number to which the request was sent.
*/
Expand Down
177 changes: 1 addition & 176 deletions plugin-api/src/main/java/org/sonar/api/web/ServletFilter.java
Original file line number Diff line number Diff line change
Expand Up @@ -19,25 +19,12 @@
*/
package org.sonar.api.web;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.function.Predicate;
import java.util.stream.Collectors;
import org.sonar.api.ExtensionPoint;
import org.sonar.api.server.ServerSide;

import static java.util.Arrays.asList;
import static java.util.Collections.unmodifiableList;
import static org.apache.commons.lang.StringUtils.substringBeforeLast;
import static org.sonar.api.utils.Preconditions.checkArgument;

/**
* {@code @deprecated} since 9.16. Use {@link org.sonar.api.web.HttpFilter} instead.
*
* @since 3.1
*/
@ServerSide
Expand All @@ -51,166 +38,4 @@ public abstract class ServletFilter implements javax.servlet.Filter {
public UrlPattern doGetPattern() {
return UrlPattern.builder().build();
}

public static final class UrlPattern {

private static final String MATCH_ALL = "/*";

private final List<String> inclusions;
private final List<String> exclusions;
private final Predicate<String>[] inclusionPredicates;
private final Predicate<String>[] exclusionPredicates;

private UrlPattern(Builder builder) {
this.inclusions = unmodifiableList(new ArrayList<>(builder.inclusions));
this.exclusions = unmodifiableList(new ArrayList<>(builder.exclusions));
if (builder.inclusionPredicates.isEmpty()) {
// because Stream#anyMatch() returns false if stream is empty
this.inclusionPredicates = new Predicate[]{s -> true};
} else {
this.inclusionPredicates = builder.inclusionPredicates.stream().toArray(Predicate[]::new);
}
this.exclusionPredicates = builder.exclusionPredicates.stream().toArray(Predicate[]::new);
}

public boolean matches(String path) {
return !Arrays.stream(exclusionPredicates).anyMatch(pattern -> pattern.test(path)) &&
Arrays.stream(inclusionPredicates).anyMatch(pattern -> pattern.test(path));
}

/**
* @since 6.0
*/
public Collection<String> getInclusions() {
return inclusions;
}

/**
* @since 6.0
*/
public Collection<String> getExclusions() {
return exclusions;
}

public String label() {
return "UrlPattern{" +
"inclusions=[" + convertPatternsToString(inclusions) + "]" +
", exclusions=[" + convertPatternsToString(exclusions) + "]" +
'}';
}

private static String convertPatternsToString(List<String> input) {
StringBuilder output = new StringBuilder();
if (input.isEmpty()) {
return "";
}
if (input.size() == 1) {
return output.append(input.get(0)).toString();
}
return output.append(input.get(0)).append(", ...").toString();
}

/**
* Defines only a single inclusion pattern. This is a shortcut for {@code builder().includes(inclusionPattern).build()}.
*/
public static UrlPattern create(String inclusionPattern) {
return builder().includes(inclusionPattern).build();
}

/**
* @since 6.0
*/
public static Builder builder() {
return new Builder();
}

/**
* @since 6.0
*/
public static class Builder {
private static final String WILDCARD_CHAR = "*";
private static final Collection<String> STATIC_RESOURCES = unmodifiableList(asList(
"*.css", "*.css.map", "*.ico", "*.png", "*.jpg", "*.jpeg", "*.gif", "*.svg", "*.js", "*.js.map", "*.pdf", "/json/*", "*.woff2",
"/static/*", "/robots.txt", "/favicon.ico", "/apple-touch-icon*", "/mstile*"));

private final Set<String> inclusions = new LinkedHashSet<>();
private final Set<String> exclusions = new LinkedHashSet<>();
private final Set<Predicate<String>> inclusionPredicates = new HashSet<>();
private final Set<Predicate<String>> exclusionPredicates = new HashSet<>();

private Builder() {
}

public static Collection<String> staticResourcePatterns() {
return STATIC_RESOURCES;
}

/**
* Add inclusion patterns. Supported formats are:
* <ul>
* <li>path prefixed by / and ended by * or /*, for example "/api/foo/*", to match all paths "/api/foo" and "api/api/foo/something/else"</li>
* <li>path prefixed by / and ended by .*, for example "/api/foo.*", to match exact path "/api/foo" with any suffix like "/api/foo.protobuf"</li>
* <li>path prefixed by *, for example "*\/foo", to match all paths "/api/foo" and "something/else/foo"</li>
* <li>path with leading slash and no wildcard, for example "/api/foo", to match exact path "/api/foo"</li>
* </ul>
*/
public Builder includes(String... includePatterns) {
return includes(asList(includePatterns));
}

/**
* Add exclusion patterns. See format described in {@link #includes(String...)}
*/
public Builder includes(Collection<String> includePatterns) {
this.inclusions.addAll(includePatterns);
this.inclusionPredicates.addAll(includePatterns.stream()
.filter(pattern -> !MATCH_ALL.equals(pattern))
.map(Builder::compile)
.collect(Collectors.toList()));
return this;
}

public Builder excludes(String... excludePatterns) {
return excludes(asList(excludePatterns));
}

public Builder excludes(Collection<String> excludePatterns) {
this.exclusions.addAll(excludePatterns);
this.exclusionPredicates.addAll(excludePatterns.stream()
.map(Builder::compile)
.collect(Collectors.toList()));
return this;
}

public UrlPattern build() {
return new UrlPattern(this);
}

private static Predicate<String> compile(String pattern) {
int countStars = pattern.length() - pattern.replace(WILDCARD_CHAR, "").length();
if (countStars == 0) {
checkArgument(pattern.startsWith("/"), "URL pattern must start with slash '/': %s", pattern);
return url -> url.equals(pattern);
}
checkArgument(countStars == 1, "URL pattern accepts only zero or one wildcard character '*': %s", pattern);
if (pattern.charAt(0) == '/') {
checkArgument(pattern.endsWith(WILDCARD_CHAR), "URL pattern must end with wildcard character '*': %s", pattern);
if (pattern.endsWith("/*")) {
String path = pattern.substring(0, pattern.length() - "/*".length());
return url -> url.startsWith(path);
}
if (pattern.endsWith(".*")) {
String path = pattern.substring(0, pattern.length() - ".*".length());
return url -> substringBeforeLast(url, ".").equals(path);
}
String path = pattern.substring(0, pattern.length() - "*".length());
return url -> url.startsWith(path);
}
checkArgument(pattern.startsWith(WILDCARD_CHAR), "URL pattern must start with wildcard character '*': %s", pattern);
// remove the leading *
String path = pattern.substring(1);
return url -> url.endsWith(path);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Sonar Plugin API
* Copyright (C) 2009-2023 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.api.server.http;

import jakarta.servlet.http.HttpServletRequest;
import java.util.Enumeration;
import org.junit.Test;

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class JakartaHttpRequestTest {

@Test
public void initRequest() {
HttpServletRequest requestMock = mock(HttpServletRequest.class);
when(requestMock.getServerPort()).thenReturn(80);
when(requestMock.isSecure()).thenReturn(true);
when(requestMock.getScheme()).thenReturn("https");
when(requestMock.getServerName()).thenReturn("hostname");
when(requestMock.getRequestURL()).thenReturn(new StringBuffer("https://hostname:80/path"));
when(requestMock.getRequestURI()).thenReturn("/path");
when(requestMock.getQueryString()).thenReturn("param1=value1");
when(requestMock.getContextPath()).thenReturn("/path");
when(requestMock.getMethod()).thenReturn("POST");
when(requestMock.getParameter("param1")).thenReturn("value1");
when(requestMock.getParameterValues("param1")).thenReturn(new String[]{"value1"});
when(requestMock.getHeader("header1")).thenReturn("hvalue1");
Enumeration<String> headers = mock(Enumeration.class);
when(requestMock.getHeaders("header1")).thenReturn(headers);

JakartaHttpRequest request = new JakartaHttpRequest(requestMock);

assertThat(request.getRawRequest()).isSameAs(requestMock);
assertThat(request.getServerPort()).isEqualTo(80);
assertThat(request.isSecure()).isTrue();
assertThat(request.getScheme()).isEqualTo("https");
assertThat(request.getServerName()).isEqualTo("hostname");
assertThat(request.getRequestURL()).isEqualTo("https://hostname:80/path");
assertThat(request.getRequestURI()).isEqualTo("/path");
assertThat(request.getQueryString()).isEqualTo("param1=value1");
assertThat(request.getContextPath()).isEqualTo("/path");
assertThat(request.getMethod()).isEqualTo("POST");
assertThat(request.getParameter("param1")).isEqualTo("value1");
assertThat(request.getParameterValues("param1")).containsExactly("value1");
assertThat(request.getHeader("header1")).isEqualTo("hvalue1");
assertThat(request.getHeaders("header1")).isEqualTo(headers);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Sonar Plugin API
* Copyright (C) 2009-2023 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.api.server.http;

import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.List;
import org.junit.Test;

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

public class JakartaHttpResponseTest {

@Test
public void initResponse() throws IOException {
HttpServletResponse responseMock = mock(HttpServletResponse.class);
when(responseMock.getHeader("h1")).thenReturn("hvalue1");
when(responseMock.getHeaders("h1")).thenReturn(List.of("hvalue1"));
when(responseMock.getStatus()).thenReturn(200);
PrintWriter writer = mock(PrintWriter.class);
when(responseMock.getWriter()).thenReturn(writer);

JakartaHttpResponse response = new JakartaHttpResponse(responseMock);

assertThat(response.getRawResponse()).isSameAs(responseMock);
assertThat(response.getHeader("h1")).isEqualTo("hvalue1");
assertThat(response.getHeaders("h1")).asList().containsExactly("hvalue1");
assertThat(response.getStatus()).isEqualTo(200);
assertThat(response.getWriter()).isEqualTo(writer);

response.addHeader("h2", "hvalue2");
response.setStatus(201);
response.setContentType("text/plain");
response.sendRedirect("http://redirect");
verify(responseMock).addHeader("h2", "hvalue2");
verify(responseMock).setStatus(201);
verify(responseMock).setContentType("text/plain");
verify(responseMock).sendRedirect("http://redirect");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* Sonar Plugin API
* Copyright (C) 2009-2023 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.api.server.http;

import java.util.Enumeration;
import javax.servlet.http.HttpServletRequest;
import org.junit.Test;

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;

public class JavaxHttpRequestTest {

@Test
public void initRequest() {
HttpServletRequest requestMock = mock(HttpServletRequest.class);
when(requestMock.getServerPort()).thenReturn(80);
when(requestMock.isSecure()).thenReturn(true);
when(requestMock.getScheme()).thenReturn("https");
when(requestMock.getServerName()).thenReturn("hostname");
when(requestMock.getRequestURL()).thenReturn(new StringBuffer("https://hostname:80/path"));
when(requestMock.getRequestURI()).thenReturn("/path");
when(requestMock.getQueryString()).thenReturn("param1=value1");
when(requestMock.getContextPath()).thenReturn("/path");
when(requestMock.getMethod()).thenReturn("POST");
when(requestMock.getParameter("param1")).thenReturn("value1");
when(requestMock.getParameterValues("param1")).thenReturn(new String[]{"value1"});
when(requestMock.getHeader("header1")).thenReturn("hvalue1");
Enumeration<String> headers = mock(Enumeration.class);
when(requestMock.getHeaders("header1")).thenReturn(headers);

JavaxHttpRequest request = new JavaxHttpRequest(requestMock);

assertThat(request.getRawRequest()).isSameAs(requestMock);
assertThat(request.getServerPort()).isEqualTo(80);
assertThat(request.isSecure()).isTrue();
assertThat(request.getScheme()).isEqualTo("https");
assertThat(request.getServerName()).isEqualTo("hostname");
assertThat(request.getRequestURL()).isEqualTo("https://hostname:80/path");
assertThat(request.getRequestURI()).isEqualTo("/path");
assertThat(request.getQueryString()).isEqualTo("param1=value1");
assertThat(request.getContextPath()).isEqualTo("/path");
assertThat(request.getMethod()).isEqualTo("POST");
assertThat(request.getParameter("param1")).isEqualTo("value1");
assertThat(request.getParameterValues("param1")).containsExactly("value1");
assertThat(request.getHeader("header1")).isEqualTo("hvalue1");
assertThat(request.getHeaders("header1")).isEqualTo(headers);
}
}
Loading

0 comments on commit 1212873

Please sign in to comment.