From b5566d14514a36eec77e4fc21133167653e53965 Mon Sep 17 00:00:00 2001 From: Duarte Meneses Date: Wed, 30 Jun 2021 13:47:02 -0400 Subject: [PATCH] SONAR-15118 Reading DevOps response header should be case-insensitive --- .../GithubApplicationHttpClientImpl.java | 2 +- .../alm/client/gitlab/GitlabHttpClient.java | 8 ++-- .../GithubApplicationHttpClientImplTest.java | 11 ++++++ .../client/gitlab/GitlabHttpClientTest.java | 37 +++++++++++++++++++ .../java/org/sonar/auth/OAuthRestClient.java | 10 +++-- .../org/sonar/auth/OAuthRestClientTest.java | 14 +++++++ 6 files changed, 75 insertions(+), 7 deletions(-) diff --git a/server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/GithubApplicationHttpClientImpl.java b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/GithubApplicationHttpClientImpl.java index 0f56ffc5c52..71b654655dd 100644 --- a/server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/GithubApplicationHttpClientImpl.java +++ b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/github/GithubApplicationHttpClientImpl.java @@ -203,7 +203,7 @@ public class GithubApplicationHttpClientImpl implements GithubApplicationHttpCli @CheckForNull private static String readNextEndPoint(okhttp3.Response response) { - String links = response.header("link"); + String links = response.headers().get("link"); if (links == null || links.isEmpty() || !links.contains("rel=\"next\"")) { return null; } diff --git a/server/sonar-alm-client/src/main/java/org/sonar/alm/client/gitlab/GitlabHttpClient.java b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/gitlab/GitlabHttpClient.java index ffe6e647de6..4932e2db87e 100644 --- a/server/sonar-alm-client/src/main/java/org/sonar/alm/client/gitlab/GitlabHttpClient.java +++ b/server/sonar-alm-client/src/main/java/org/sonar/alm/client/gitlab/GitlabHttpClient.java @@ -29,6 +29,7 @@ import java.util.Arrays; import java.util.List; import java.util.Optional; import javax.annotation.Nullable; +import okhttp3.Headers; import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.RequestBody; @@ -302,11 +303,12 @@ public class GitlabHttpClient { .build(); try (Response response = client.newCall(request).execute()) { + Headers headers = response.headers(); checkResponseIsSuccessful(response, "Could not get projects from GitLab instance"); List projectList = Project.parseJsonArray(response.body().string()); - int returnedPageNumber = parseAndGetIntegerHeader(response.header("X-Page")); - int returnedPageSize = parseAndGetIntegerHeader(response.header("X-Per-Page")); - int totalProjects = parseAndGetIntegerHeader(response.header("X-Total")); + int returnedPageNumber = parseAndGetIntegerHeader(headers.get("X-Page")); + int returnedPageSize = parseAndGetIntegerHeader(headers.get("X-Per-Page")); + int totalProjects = parseAndGetIntegerHeader(headers.get("X-Total")); return new ProjectList(projectList, returnedPageNumber, returnedPageSize, totalProjects); } catch (JsonSyntaxException e) { throw new IllegalArgumentException("Could not parse GitLab answer to search projects. Got a non-json payload as result."); diff --git a/server/sonar-alm-client/src/test/java/org/sonar/alm/client/github/GithubApplicationHttpClientImplTest.java b/server/sonar-alm-client/src/test/java/org/sonar/alm/client/github/GithubApplicationHttpClientImplTest.java index 5922ececdb2..8bbf5da3a70 100644 --- a/server/sonar-alm-client/src/test/java/org/sonar/alm/client/github/GithubApplicationHttpClientImplTest.java +++ b/server/sonar-alm-client/src/test/java/org/sonar/alm/client/github/GithubApplicationHttpClientImplTest.java @@ -162,6 +162,17 @@ public class GithubApplicationHttpClientImplTest { assertThat(response.getNextEndPoint()).contains("https://api.github.com/installation/repositories?per_page=5&page=2"); } + @Test + public void get_returns_endPoint_when_link_header_has_next_rel_different_case() throws IOException { + String linkHeader = "; rel=\"next\""; + server.enqueue(new MockResponse().setBody(randomBody) + .setHeader("Link", linkHeader)); + + GetResponse response = underTest.get(appUrl, accessToken, randomEndPoint); + + assertThat(response.getNextEndPoint()).contains("https://api.github.com/installation/repositories?per_page=5&page=2"); + } + @DataProvider public static Object[][] linkHeadersWithNextRel() { String expected = "https://api.github.com/installation/repositories?per_page=5&page=2"; diff --git a/server/sonar-alm-client/src/test/java/org/sonar/alm/client/gitlab/GitlabHttpClientTest.java b/server/sonar-alm-client/src/test/java/org/sonar/alm/client/gitlab/GitlabHttpClientTest.java index 283371f2cb4..229f26f6ebe 100644 --- a/server/sonar-alm-client/src/test/java/org/sonar/alm/client/gitlab/GitlabHttpClientTest.java +++ b/server/sonar-alm-client/src/test/java/org/sonar/alm/client/gitlab/GitlabHttpClientTest.java @@ -243,6 +243,43 @@ public class GitlabHttpClientTest { assertThat(projectGitlabRequest.getMethod()).isEqualTo("GET"); } + @Test + public void search_projects_with_case_insensitive_pagination_headers() throws InterruptedException { + MockResponse projects1 = new MockResponse() + .setResponseCode(200) + .setBody("[\n" + + " {\n" + + " \"id\": 1,\n" + + " \"name\": \"SonarQube example 1\",\n" + + " \"name_with_namespace\": \"SonarSource / SonarQube / SonarQube example 1\",\n" + + " \"path\": \"sonarqube-example-1\",\n" + + " \"path_with_namespace\": \"sonarsource/sonarqube/sonarqube-example-1\",\n" + + " \"web_url\": \"https://example.gitlab.com/sonarsource/sonarqube/sonarqube-example-1\"\n" + + " }" + + "]"); + projects1.addHeader("x-page", 1); + projects1.addHeader("x-Per-page", 1); + projects1.addHeader("X-Total", 2); + server.enqueue(projects1); + + ProjectList projectList = underTest.searchProjects(gitlabUrl, "pat", "example", 1, 10); + + assertThat(projectList.getPageNumber()).isEqualTo(1); + assertThat(projectList.getPageSize()).isEqualTo(1); + assertThat(projectList.getTotal()).isEqualTo(2); + + assertThat(projectList.getProjects()).hasSize(1); + assertThat(projectList.getProjects()).extracting( + Project::getId, Project::getName, Project::getNameWithNamespace, Project::getPath, Project::getPathWithNamespace, Project::getWebUrl).containsExactly( + tuple(1L, "SonarQube example 1", "SonarSource / SonarQube / SonarQube example 1", "sonarqube-example-1", "sonarsource/sonarqube/sonarqube-example-1", + "https://example.gitlab.com/sonarsource/sonarqube/sonarqube-example-1")); + + RecordedRequest projectGitlabRequest = server.takeRequest(10, TimeUnit.SECONDS); + String gitlabUrlCall = projectGitlabRequest.getRequestUrl().toString(); + assertThat(gitlabUrlCall).isEqualTo(server.url("") + "projects?archived=false&simple=true&membership=true&order_by=name&sort=asc&search=example&page=1&per_page=10"); + assertThat(projectGitlabRequest.getMethod()).isEqualTo("GET"); + } + @Test public void search_projects_projectName_param_should_be_encoded() throws InterruptedException { MockResponse projects = new MockResponse() diff --git a/server/sonar-auth-common/src/main/java/org/sonar/auth/OAuthRestClient.java b/server/sonar-auth-common/src/main/java/org/sonar/auth/OAuthRestClient.java index 8a95fba3c95..c21795d5aef 100644 --- a/server/sonar-auth-common/src/main/java/org/sonar/auth/OAuthRestClient.java +++ b/server/sonar-auth-common/src/main/java/org/sonar/auth/OAuthRestClient.java @@ -27,6 +27,7 @@ import com.github.scribejava.core.oauth.OAuth20Service; import java.io.IOException; import java.util.ArrayList; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.concurrent.ExecutionException; import java.util.function.Function; @@ -81,11 +82,14 @@ public class OAuthRestClient { } private static Optional readNextEndPoint(Response response) { - String link = response.getHeader("Link"); - if (link == null || link.isEmpty() || !link.contains("rel=\"next\"")) { + Optional link = response.getHeaders().entrySet().stream() + .filter(e -> "Link".equalsIgnoreCase(e.getKey())) + .map(Map.Entry::getValue) + .findAny(); + if (link.isEmpty() || link.get().isEmpty() || !link.get().contains("rel=\"next\"")) { return Optional.empty(); } - Matcher nextLinkMatcher = NEXT_LINK_PATTERN.matcher(link); + Matcher nextLinkMatcher = NEXT_LINK_PATTERN.matcher(link.get()); if (!nextLinkMatcher.find()) { return Optional.empty(); } diff --git a/server/sonar-auth-common/src/test/java/org/sonar/auth/OAuthRestClientTest.java b/server/sonar-auth-common/src/test/java/org/sonar/auth/OAuthRestClientTest.java index 60be82738b0..3f29fd2597d 100644 --- a/server/sonar-auth-common/src/test/java/org/sonar/auth/OAuthRestClientTest.java +++ b/server/sonar-auth-common/src/test/java/org/sonar/auth/OAuthRestClientTest.java @@ -97,6 +97,20 @@ public class OAuthRestClientTest { assertThat(response).contains("A", "B"); } + @Test + public void execute_paginated_request_case_insensitive_headers() { + mockWebServer.enqueue(new MockResponse() + .setHeader("link", "<" + serverUrl + "/test?per_page=100&page=2>; rel=\"next\", <" + serverUrl + "/test?per_page=100&page=2>; rel=\"last\"") + .setBody("A")); + mockWebServer.enqueue(new MockResponse() + .setHeader("link", "<" + serverUrl + "/test?per_page=100&page=1>; rel=\"prev\", <" + serverUrl + "/test?per_page=100&page=1>; rel=\"first\"") + .setBody("B")); + + List response = executePaginatedRequest(serverUrl + "/test", oAuth20Service, auth2AccessToken, Arrays::asList); + + assertThat(response).contains("A", "B"); + } + @Test public void fail_to_executed_paginated_request() { mockWebServer.enqueue(new MockResponse() -- 2.39.5