From b9cd4425d5c2ab63672b8c18bb2812451a685beb Mon Sep 17 00:00:00 2001 From: Eric Hartmann Date: Wed, 6 Jun 2018 08:43:42 +0200 Subject: [PATCH] SONAR-10340 Add ETags for badges --- .../org/sonar/server/badge/ws/ETagUtils.java | 58 +++++++ .../sonar/server/badge/ws/MeasureAction.java | 23 ++- .../server/badge/ws/QualityGateAction.java | 22 ++- .../sonar/server/badge/ws/ETagUtilsTest.java | 39 +++++ .../server/badge/ws/MeasureActionTest.java | 137 +++++++++++----- .../badge/ws/QualityGateActionTest.java | 153 +++++++++++++----- .../java/org/sonar/server/ws/TestRequest.java | 8 +- 7 files changed, 353 insertions(+), 87 deletions(-) create mode 100644 server/sonar-server/src/main/java/org/sonar/server/badge/ws/ETagUtils.java create mode 100644 server/sonar-server/src/test/java/org/sonar/server/badge/ws/ETagUtilsTest.java diff --git a/server/sonar-server/src/main/java/org/sonar/server/badge/ws/ETagUtils.java b/server/sonar-server/src/main/java/org/sonar/server/badge/ws/ETagUtils.java new file mode 100644 index 00000000000..4f98f50c7bf --- /dev/null +++ b/server/sonar-server/src/main/java/org/sonar/server/badge/ws/ETagUtils.java @@ -0,0 +1,58 @@ +/* + * SonarQube + * Copyright (C) 2009-2018 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.badge.ws; + +import java.nio.charset.Charset; + +public class ETagUtils { + // Format for Expires Header + static final String RFC1123_DATE = "EEE, dd MMM yyyy HH:mm:ss zzz"; + + private static final long FNV1_INIT = 0xcbf29ce484222325L; + private static final long FNV1_PRIME = 0x100000001b3L; + + private ETagUtils() { + // Utility class no instantiation allowed + } + + /** + * hash method of a String independant of the JVM + * FNV-1a hash method @see + */ + private static long hash(byte[] input) { + long hash = FNV1_INIT; + for (byte b : input) { + hash ^= b & 0xff; + hash *= FNV1_PRIME; + } + return hash; + } + + /** + * Calculate the ETag of the badge + * + * @see + * + */ + static String getETag(String output) { + return "W/" + hash(output.getBytes(Charset.forName("UTF-8"))); + } +} diff --git a/server/sonar-server/src/main/java/org/sonar/server/badge/ws/MeasureAction.java b/server/sonar-server/src/main/java/org/sonar/server/badge/ws/MeasureAction.java index 5a2f9406a6b..462fcd9b90c 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/badge/ws/MeasureAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/badge/ws/MeasureAction.java @@ -21,8 +21,12 @@ package org.sonar.server.badge.ws; import com.google.common.collect.ImmutableMap; import com.google.common.io.Resources; +import java.text.SimpleDateFormat; +import java.util.Date; import java.util.EnumMap; +import java.util.Locale; import java.util.Map; +import java.util.Optional; import java.util.function.Function; import org.sonar.api.server.ws.Request; import org.sonar.api.server.ws.Response; @@ -54,10 +58,12 @@ import static org.sonar.api.measures.CoreMetrics.SQALE_RATING_KEY; import static org.sonar.api.measures.CoreMetrics.TECHNICAL_DEBT_KEY; import static org.sonar.api.measures.CoreMetrics.VULNERABILITIES_KEY; import static org.sonar.api.measures.Metric.Level; -import static org.sonar.api.measures.Metric.ValueType; import static org.sonar.api.measures.Metric.Level.ERROR; import static org.sonar.api.measures.Metric.Level.OK; import static org.sonar.api.measures.Metric.Level.WARN; +import static org.sonar.api.measures.Metric.ValueType; +import static org.sonar.server.badge.ws.ETagUtils.RFC1123_DATE; +import static org.sonar.server.badge.ws.ETagUtils.getETag; import static org.sonar.server.badge.ws.SvgFormatter.formatDuration; import static org.sonar.server.badge.ws.SvgFormatter.formatNumeric; import static org.sonar.server.badge.ws.SvgFormatter.formatPercent; @@ -131,6 +137,7 @@ public class MeasureAction implements ProjectBadgesWsAction { @Override public void handle(Request request, Response response) throws Exception { + response.setHeader("Cache-Control", "no-cache"); response.stream().setMediaType(SVG); String metricKey = request.mandatoryParam(PARAM_METRIC); try (DbSession dbSession = dbClient.openSession(false)) { @@ -138,8 +145,19 @@ public class MeasureAction implements ProjectBadgesWsAction { MetricDto metric = dbClient.metricDao().selectByKey(dbSession, metricKey); checkState(metric != null && metric.isEnabled(), "Metric '%s' hasn't been found", metricKey); LiveMeasureDto measure = getMeasure(dbSession, project, metricKey); - write(generateSvg(metric, measure), response.stream().output(), UTF_8); + String result = generateSvg(metric, measure); + String eTag = getETag(result); + Optional requestedETag = request.header("If-None-Match"); + if (requestedETag.filter(eTag::equals).isPresent()) { + response.stream().setStatus(304); + return; + } + response.setHeader("ETag", eTag); + write(result, response.stream().output(), UTF_8); } catch (ProjectBadgesException | ForbiddenException | NotFoundException e) { + // There is an issue, so do not return any ETag but make this response expire now + SimpleDateFormat sdf = new SimpleDateFormat(RFC1123_DATE, Locale.US); + response.setHeader("Expires", sdf.format(new Date())); write(svgGenerator.generateError(e.getMessage()), response.stream().output(), UTF_8); } } @@ -187,4 +205,5 @@ public class MeasureAction implements ProjectBadgesWsAction { return value; } + } diff --git a/server/sonar-server/src/main/java/org/sonar/server/badge/ws/QualityGateAction.java b/server/sonar-server/src/main/java/org/sonar/server/badge/ws/QualityGateAction.java index dad88fd643f..4c71ae1b528 100644 --- a/server/sonar-server/src/main/java/org/sonar/server/badge/ws/QualityGateAction.java +++ b/server/sonar-server/src/main/java/org/sonar/server/badge/ws/QualityGateAction.java @@ -20,6 +20,10 @@ package org.sonar.server.badge.ws; import com.google.common.io.Resources; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; +import java.util.Optional; import org.sonar.api.measures.Metric.Level; import org.sonar.api.server.ws.Request; import org.sonar.api.server.ws.Response; @@ -35,9 +39,11 @@ import org.sonar.server.exceptions.NotFoundException; import static java.nio.charset.StandardCharsets.UTF_8; import static org.apache.commons.io.IOUtils.write; import static org.sonar.api.measures.CoreMetrics.ALERT_STATUS_KEY; +import static org.sonar.server.badge.ws.ETagUtils.RFC1123_DATE; +import static org.sonar.server.badge.ws.ETagUtils.getETag; import static org.sonarqube.ws.MediaTypes.SVG; -public class QualityGateAction implements ProjectBadgesWsAction { +public class QualityGateAction implements ProjectBadgesWsAction { private final DbClient dbClient; private final ProjectBadgesSupport support; @@ -62,12 +68,24 @@ public class QualityGateAction implements ProjectBadgesWsAction { @Override public void handle(Request request, Response response) throws Exception { + response.setHeader("Cache-Control", "no-cache"); response.stream().setMediaType(SVG); try (DbSession dbSession = dbClient.openSession(false)) { ComponentDto project = support.getComponent(dbSession, request); Level qualityGateStatus = getQualityGate(dbSession, project); - write(svgGenerator.generateQualityGate(qualityGateStatus), response.stream().output(), UTF_8); + String result = svgGenerator.generateQualityGate(qualityGateStatus); + String eTag = getETag(result); + Optional requestedETag = request.header("If-None-Match"); + if (requestedETag.filter(eTag::equals).isPresent()) { + response.stream().setStatus(304); + return; + } + response.setHeader("ETag", eTag); + write(result, response.stream().output(), UTF_8); } catch (ProjectBadgesException | ForbiddenException | NotFoundException e) { + // There is an issue, so do not return any ETag but make this response expire now + SimpleDateFormat sdf = new SimpleDateFormat(RFC1123_DATE, Locale.US); + response.setHeader("Expires", sdf.format(new Date())); write(svgGenerator.generateError(e.getMessage()), response.stream().output(), UTF_8); } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/badge/ws/ETagUtilsTest.java b/server/sonar-server/src/test/java/org/sonar/server/badge/ws/ETagUtilsTest.java new file mode 100644 index 00000000000..36df418a7d8 --- /dev/null +++ b/server/sonar-server/src/test/java/org/sonar/server/badge/ws/ETagUtilsTest.java @@ -0,0 +1,39 @@ +package org.sonar.server.badge.ws;/* + * SonarQube + * Copyright (C) 2009-2018 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.junit.Test; + +import static org.apache.commons.lang.RandomStringUtils.randomAlphanumeric; +import static org.assertj.core.api.Assertions.assertThat; + + +public class ETagUtilsTest { + + @Test + public void getETag_should_start_with_W_SLASH() { + assertThat(ETagUtils.getETag(randomAlphanumeric(15))).startsWith("W/"); + } + + @Test + public void getETag_should_return_same_value_for_same_input() { + String input = randomAlphanumeric(200); + assertThat(ETagUtils.getETag(input)).isEqualTo(ETagUtils.getETag(input)); + } +} diff --git a/server/sonar-server/src/test/java/org/sonar/server/badge/ws/MeasureActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/badge/ws/MeasureActionTest.java index 652675893f8..31c2d4e277d 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/badge/ws/MeasureActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/badge/ws/MeasureActionTest.java @@ -22,6 +22,10 @@ package org.sonar.server.badge.ws; import com.tngtech.java.junit.dataprovider.DataProvider; import com.tngtech.java.junit.dataprovider.DataProviderRunner; import com.tngtech.java.junit.dataprovider.UseDataProvider; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; @@ -42,6 +46,7 @@ import org.sonar.server.badge.ws.SvgGenerator.Color; import org.sonar.server.component.ComponentFinder; import org.sonar.server.computation.task.projectanalysis.qualitymodel.Rating; import org.sonar.server.tester.UserSessionRule; +import org.sonar.server.ws.TestResponse; import org.sonar.server.ws.WsActionTester; import static org.assertj.core.api.Assertions.assertThat; @@ -91,12 +96,15 @@ public class MeasureActionTest { MetricDto metric = db.measures().insertMetric(m -> m.setKey(BUGS_KEY).setValueType(INT.name())); db.measures().insertLiveMeasure(project, metric, m -> m.setValue(10_000d)); - String response = ws.newRequest() + TestResponse response = ws.newRequest() .setParam("project", project.getKey()) .setParam("metric", metric.getKey()) - .execute().getInput(); + .execute(); checkSvg(response, "bugs", "10k", DEFAULT); + + // Second call with If-None-Match must return 304 + checkWithIfNoneMatchHeader(project, metric, response); } @Test @@ -106,12 +114,15 @@ public class MeasureActionTest { MetricDto metric = db.measures().insertMetric(m -> m.setKey(COVERAGE_KEY).setValueType(PERCENT.name())); db.measures().insertLiveMeasure(project, metric, m -> m.setValue(12.345d)); - String response = ws.newRequest() + TestResponse response = ws.newRequest() .setParam("project", project.getKey()) .setParam("metric", metric.getKey()) - .execute().getInput(); + .execute(); checkSvg(response, "coverage", "12.3%", DEFAULT); + + // Second call with If-None-Match must return 304 + checkWithIfNoneMatchHeader(project, metric, response); } @Test @@ -121,12 +132,15 @@ public class MeasureActionTest { MetricDto metric = db.measures().insertMetric(m -> m.setKey(TECHNICAL_DEBT_KEY).setValueType(WORK_DUR.name())); db.measures().insertLiveMeasure(project, metric, m -> m.setValue(10_000d)); - String response = ws.newRequest() + TestResponse response = ws.newRequest() .setParam("project", project.getKey()) .setParam("metric", metric.getKey()) - .execute().getInput(); + .execute(); checkSvg(response, "technical debt", "21d", DEFAULT); + + // Second call with If-None-Match must return 304 + checkWithIfNoneMatchHeader(project, metric, response); } @DataProvider @@ -148,12 +162,15 @@ public class MeasureActionTest { MetricDto metric = db.measures().insertMetric(m -> m.setKey(SQALE_RATING_KEY).setValueType(RATING.name())); db.measures().insertLiveMeasure(project, metric, m -> m.setValue((double) rating.getIndex()).setData(rating.name())); - String response = ws.newRequest() + TestResponse response = ws.newRequest() .setParam("project", project.getKey()) .setParam("metric", metric.getKey()) - .execute().getInput(); + .execute(); checkSvg(response, "maintainability", rating.name(), color); + + // Second call with If-None-Match must return 304 + checkWithIfNoneMatchHeader(project, metric, response); } @DataProvider @@ -173,12 +190,15 @@ public class MeasureActionTest { MetricDto metric = createQualityGateMetric(); db.measures().insertLiveMeasure(project, metric, m -> m.setData(status.name())); - String response = ws.newRequest() + TestResponse response = ws.newRequest() .setParam("project", project.getKey()) .setParam("metric", metric.getKey()) - .execute().getInput(); + .execute(); checkSvg(response, "quality gate", expectedValue, expectedColor); + + // Second call with If-None-Match must return 304 + checkWithIfNoneMatchHeader(project, metric, response); } @Test @@ -190,13 +210,24 @@ public class MeasureActionTest { ComponentDto longBranch = db.components().insertProjectBranch(project, b -> b.setBranchType(LONG)); db.measures().insertLiveMeasure(longBranch, metric, m -> m.setValue(10_000d)); - String response = ws.newRequest() + TestResponse response = ws.newRequest() .setParam("project", longBranch.getKey()) .setParam("branch", longBranch.getBranch()) .setParam("metric", metric.getKey()) - .execute().getInput(); + .execute(); checkSvg(response, "bugs", "10k", DEFAULT); + + // Second call with If-None-Match must return 304 + response = ws.newRequest() + .setHeader("If-None-Match", response.getHeader("ETag")) + .setParam("project", longBranch.getKey()) + .setParam("branch", longBranch.getBranch()) + .setParam("metric", metric.getKey()) + .execute(); + + assertThat(response.getInput()).isEmpty(); + assertThat(response.getStatus()).isEqualTo(304); } @Test @@ -207,126 +238,129 @@ public class MeasureActionTest { MetricDto metric = db.measures().insertMetric(m -> m.setKey(BUGS_KEY).setValueType(INT.name())); db.measures().insertLiveMeasure(application, metric, m -> m.setValue(10_000d)); - String response = ws.newRequest() + TestResponse response = ws.newRequest() .setParam("project", application.getKey()) .setParam("metric", metric.getKey()) - .execute().getInput(); + .execute(); checkSvg(response, "bugs", "10k", DEFAULT); + + // Second call with If-None-Match must return 304 + checkWithIfNoneMatchHeader(application, metric, response); } @Test - public void return_error_if_project_does_not_exist() { + public void return_error_if_project_does_not_exist() throws ParseException { MetricDto metric = db.measures().insertMetric(m -> m.setKey(BUGS_KEY)); - String response = ws.newRequest() + TestResponse response = ws.newRequest() .setParam("project", "unknown") .setParam("metric", metric.getKey()) - .execute().getInput(); + .execute(); checkError(response, "Project has not been found"); } @Test - public void return_error_if_branch_does_not_exist() { + public void return_error_if_branch_does_not_exist() throws ParseException { ComponentDto project = db.components().insertMainBranch(); ComponentDto branch = db.components().insertProjectBranch(project, b -> b.setBranchType(BranchType.LONG)); userSession.addProjectPermission(USER, project); MetricDto metric = db.measures().insertMetric(m -> m.setKey(BUGS_KEY)); - String response = ws.newRequest() + TestResponse response = ws.newRequest() .setParam("project", branch.getKey()) .setParam("branch", "unknown") .setParam("metric", metric.getKey()) - .execute().getInput(); + .execute(); checkError(response, "Project has not been found"); } @Test - public void return_error_if_measure_not_found() { + public void return_error_if_measure_not_found() throws ParseException { ComponentDto project = db.components().insertPublicProject(); userSession.registerComponents(project); MetricDto metric = db.measures().insertMetric(m -> m.setKey(BUGS_KEY)); - String response = ws.newRequest() + TestResponse response = ws.newRequest() .setParam("project", project.getKey()) .setParam("metric", metric.getKey()) - .execute().getInput(); + .execute(); checkError(response, "Measure has not been found"); } @Test - public void return_error_on_directory() { + public void return_error_on_directory() throws ParseException { ComponentDto project = db.components().insertPublicProject(); ComponentDto directory = db.components().insertComponent(ComponentTesting.newDirectory(project, "path")); userSession.registerComponents(project); MetricDto metric = db.measures().insertMetric(m -> m.setKey(BUGS_KEY).setValueType(INT.name())); - String response = ws.newRequest() + TestResponse response = ws.newRequest() .setParam("project", directory.getKey()) .setParam("metric", metric.getKey()) - .execute().getInput(); + .execute(); checkError(response, "Project is invalid"); } @Test - public void return_error_on_short_living_branch() { + public void return_error_on_short_living_branch() throws ParseException { ComponentDto project = db.components().insertMainBranch(); ComponentDto shortBranch = db.components().insertProjectBranch(project, b -> b.setBranchType(SHORT)); UserDto user = db.users().insertUser(); userSession.logIn(user).addProjectPermission(USER, project); MetricDto metric = db.measures().insertMetric(m -> m.setKey(BUGS_KEY).setValueType(INT.name())); - String response = ws.newRequest() + TestResponse response = ws.newRequest() .setParam("project", shortBranch.getKey()) .setParam("branch", shortBranch.getBranch()) .setParam("metric", metric.getKey()) - .execute().getInput(); + .execute(); checkError(response, "Project is invalid"); } @Test - public void return_error_on_private_project() { + public void return_error_on_private_project() throws ParseException { ComponentDto project = db.components().insertPrivateProject(); UserDto user = db.users().insertUser(); userSession.logIn(user).addProjectPermission(USER, project); MetricDto metric = db.measures().insertMetric(m -> m.setKey(BUGS_KEY).setValueType(INT.name())); - String response = ws.newRequest() + TestResponse response = ws.newRequest() .setParam("project", project.getKey()) .setParam("metric", metric.getKey()) - .execute().getInput(); + .execute(); checkError(response, "Project is invalid"); } @Test - public void return_error_on_provisioned_project() { + public void return_error_on_provisioned_project() throws ParseException { ComponentDto project = db.components().insertPublicProject(); userSession.registerComponents(project); MetricDto metric = db.measures().insertMetric(m -> m.setKey(BUGS_KEY).setValueType(INT.name())); - String response = ws.newRequest() + TestResponse response = ws.newRequest() .setParam("project", project.getKey()) .setParam("metric", metric.getKey()) - .execute().getInput(); + .execute(); checkError(response, "Measure has not been found"); } @Test - public void return_error_if_unauthorized() { + public void return_error_if_unauthorized() throws ParseException { ComponentDto project = db.components().insertPublicProject(); MetricDto metric = db.measures().insertMetric(m -> m.setKey(BUGS_KEY)); - String response = ws.newRequest() + TestResponse response = ws.newRequest() .setParam("project", project.getKey()) .setParam("metric", metric.getKey()) - .execute().getInput(); + .execute(); checkError(response, "Insufficient privileges"); } @@ -394,17 +428,36 @@ public class MeasureActionTest { tuple("metric", true)); } - private void checkSvg(String svg, String expectedLabel, String expectedValue, Color expectedColorValue) { - assertThat(svg).contains( + private void checkSvg(TestResponse response, String expectedLabel, String expectedValue, Color expectedColorValue) { + assertThat(response.getHeader("ETag")).startsWith("W/"); + assertThat(response.getHeader("Cache-Control")).contains("no-cache"); + assertThat(response.getHeader("Expires")).isNull(); + + assertThat(response.getInput()).contains( "", "", "rect fill=\"" + expectedColorValue.getValue() + "\""); } - private void checkError(String svg, String expectedError) { - assertThat(svg).contains("" + expectedError + ""); + private void checkError(TestResponse response, String expectedError) throws ParseException { + SimpleDateFormat expiresDateFormat = new SimpleDateFormat(ETagUtils.RFC1123_DATE, Locale.US); + assertThat(response.getHeader("Cache-Control")).contains("no-cache"); + assertThat(response.getHeader("Expires")).isNotNull(); + assertThat(response.getHeader("ETag")).isNull(); + assertThat(expiresDateFormat.parse(response.getHeader("Expires"))).isBefore(new Date()); + assertThat(response.getInput()).contains("" + expectedError + ""); } + private void checkWithIfNoneMatchHeader(ComponentDto application, MetricDto metric, TestResponse response) { + TestResponse newResponse = ws.newRequest() + .setHeader("If-None-Match", response.getHeader("ETag")) + .setParam("project", application.getKey()) + .setParam("metric", metric.getKey()) + .execute(); + + assertThat(newResponse.getInput()).isEmpty(); + assertThat(newResponse.getStatus()).isEqualTo(304); + } private MetricDto createQualityGateMetric() { return db.measures().insertMetric(m -> m.setKey(CoreMetrics.ALERT_STATUS_KEY).setValueType(LEVEL.name())); } diff --git a/server/sonar-server/src/test/java/org/sonar/server/badge/ws/QualityGateActionTest.java b/server/sonar-server/src/test/java/org/sonar/server/badge/ws/QualityGateActionTest.java index 52cedacbf86..39b0b9e54cf 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/badge/ws/QualityGateActionTest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/badge/ws/QualityGateActionTest.java @@ -19,6 +19,11 @@ */ package org.sonar.server.badge.ws; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Date; +import java.util.Locale; import org.junit.Rule; import org.junit.Test; import org.junit.rules.ExpectedException; @@ -29,11 +34,13 @@ import org.sonar.api.server.ws.WebService.Param; import org.sonar.db.DbTester; import org.sonar.db.component.ComponentDto; import org.sonar.db.component.ComponentTesting; +import org.sonar.db.measure.LiveMeasureDto; import org.sonar.db.metric.MetricDto; import org.sonar.db.organization.OrganizationDto; import org.sonar.db.user.UserDto; import org.sonar.server.component.ComponentFinder; import org.sonar.server.tester.UserSessionRule; +import org.sonar.server.ws.TestResponse; import org.sonar.server.ws.WsActionTester; import static org.assertj.core.api.Assertions.assertThat; @@ -70,9 +77,9 @@ public class QualityGateActionTest { MetricDto metric = createQualityGateMetric(); db.measures().insertLiveMeasure(project, metric, m -> m.setData(OK.name())); - String response = ws.newRequest() + TestResponse response = ws.newRequest() .setParam("project", project.getKey()) - .execute().getInput(); + .execute(); checkResponse(response, OK); } @@ -84,9 +91,9 @@ public class QualityGateActionTest { MetricDto metric = createQualityGateMetric(); db.measures().insertLiveMeasure(project, metric, m -> m.setData(WARN.name())); - String response = ws.newRequest() + TestResponse response = ws.newRequest() .setParam("project", project.getKey()) - .execute().getInput(); + .execute(); checkResponse(response, WARN); } @@ -98,13 +105,71 @@ public class QualityGateActionTest { MetricDto metric = createQualityGateMetric(); db.measures().insertLiveMeasure(project, metric, m -> m.setData(ERROR.name())); - String response = ws.newRequest() + TestResponse response = ws.newRequest() .setParam("project", project.getKey()) - .execute().getInput(); + .execute(); checkResponse(response, ERROR); } + @Test + public void etag_should_be_different_if_quality_gate_is_different() { + ComponentDto project = db.components().insertPublicProject(); + userSession.registerComponents(project); + MetricDto metric = createQualityGateMetric(); + LiveMeasureDto liveMeasure = db.measures().insertLiveMeasure(project, metric, m -> m.setData(OK.name())); + + TestResponse response = ws.newRequest() + .setParam("project", project.getKey()) + .execute(); + String eTagOK = response.getHeader("ETag"); + + liveMeasure.setData(WARN.name()); + db.getDbClient().liveMeasureDao().insertOrUpdate(db.getSession(), liveMeasure, null); + db.commit(); + + response = ws.newRequest() + .setParam("project", project.getKey()) + .execute(); + + String eTagWARN = response.getHeader("ETag"); + + liveMeasure.setData(ERROR.name()); + db.getDbClient().liveMeasureDao().insertOrUpdate(db.getSession(), liveMeasure, null); + db.commit(); + + response = ws.newRequest() + .setParam("project", project.getKey()) + .execute(); + + String eTagERROR = response.getHeader("ETag"); + + assertThat(Arrays.asList(eTagOK, eTagWARN, eTagERROR)) + .doesNotContainNull() + .doesNotHaveDuplicates(); + } + + @Test + public void when_IfNoneMatch_match_etag_http_304_must_be_send() { + ComponentDto project = db.components().insertPublicProject(); + userSession.registerComponents(project); + MetricDto metric = createQualityGateMetric(); + db.measures().insertLiveMeasure(project, metric, m -> m.setData(OK.name())); + + TestResponse response = ws.newRequest() + .setParam("project", project.getKey()) + .execute(); + String eTag = response.getHeader("ETag"); + + response = ws.newRequest() + .setParam("project", project.getKey()) + .setHeader("If-None-Match", eTag) + .execute(); + + assertThat(response.getInput()).isEmpty(); + assertThat(response.getStatus()).isEqualTo(304); + } + @Test public void quality_gate_on_long_living_branch() { ComponentDto project = db.components().insertMainBranch(p -> p.setPrivate(false)); @@ -114,10 +179,10 @@ public class QualityGateActionTest { ComponentDto longBranch = db.components().insertProjectBranch(project, b -> b.setBranchType(LONG)); db.measures().insertLiveMeasure(longBranch, metric, m -> m.setData(WARN.name())); - String response = ws.newRequest() + TestResponse response = ws.newRequest() .setParam("project", longBranch.getKey()) .setParam("branch", longBranch.getBranch()) - .execute().getInput(); + .execute(); checkResponse(response, WARN); } @@ -130,111 +195,111 @@ public class QualityGateActionTest { MetricDto metric = createQualityGateMetric(); db.measures().insertLiveMeasure(application, metric, m -> m.setData(WARN.name())); - String response = ws.newRequest() + TestResponse response = ws.newRequest() .setParam("project", application.getKey()) - .execute().getInput(); + .execute(); checkResponse(response, WARN); } @Test - public void return_error_on_directory() { + public void return_error_on_directory() throws ParseException { ComponentDto project = db.components().insertPublicProject(); ComponentDto directory = db.components().insertComponent(ComponentTesting.newDirectory(project, "path")); userSession.registerComponents(project); - String response = ws.newRequest() + TestResponse response = ws.newRequest() .setParam("project", directory.getKey()) - .execute().getInput(); + .execute(); checkError(response, "Project is invalid"); } @Test - public void return_error_on_short_living_branch() { + public void return_error_on_short_living_branch() throws ParseException { ComponentDto project = db.components().insertMainBranch(p -> p.setPrivate(false)); userSession.registerComponents(project); ComponentDto shortBranch = db.components().insertProjectBranch(project, b -> b.setBranchType(SHORT)); - String response = ws.newRequest() + TestResponse response = ws.newRequest() .setParam("project", shortBranch.getKey()) .setParam("branch", shortBranch.getBranch()) - .execute().getInput(); + .execute(); checkError(response, "Project is invalid"); } @Test - public void return_error_on_private_project() { + public void return_error_on_private_project() throws ParseException { ComponentDto project = db.components().insertPrivateProject(); UserDto user = db.users().insertUser(); userSession.logIn(user).addProjectPermission(USER, project); - String response = ws.newRequest() + TestResponse response = ws.newRequest() .setParam("project", project.getKey()) - .execute().getInput(); + .execute(); checkError(response, "Project is invalid"); } @Test - public void return_error_on_provisioned_project() { + public void return_error_on_provisioned_project() throws ParseException { ComponentDto project = db.components().insertPublicProject(); userSession.registerComponents(project); - String response = ws.newRequest() + TestResponse response = ws.newRequest() .setParam("project", project.getKey()) - .execute().getInput(); + .execute(); checkError(response, "Quality gate has not been found"); } @Test - public void return_error_on_not_existing_project() { - String response = ws.newRequest() + public void return_error_on_not_existing_project() throws ParseException { + TestResponse response = ws.newRequest() .setParam("project", "unknown") - .execute().getInput(); + .execute(); checkError(response, "Project has not been found"); } @Test - public void return_error_on_not_existing_branch() { + public void return_error_on_not_existing_branch() throws ParseException { ComponentDto project = db.components().insertMainBranch(p -> p.setPrivate(false)); userSession.registerComponents(project); ComponentDto branch = db.components().insertProjectBranch(project, b -> b.setBranchType(LONG)); - String response = ws.newRequest() + TestResponse response = ws.newRequest() .setParam("project", branch.getKey()) .setParam("branch", "unknown") - .execute().getInput(); + .execute(); checkError(response, "Project has not been found"); } @Test - public void return_error_if_measure_not_found() { + public void return_error_if_measure_not_found() throws ParseException { ComponentDto project = db.components().insertPublicProject(); userSession.registerComponents(project); - String response = ws.newRequest() + TestResponse response = ws.newRequest() .setParam("project", project.getKey()) - .execute().getInput(); + .execute(); checkError(response, "Quality gate has not been found"); } @Test - public void return_error_if_measure_value_is_null() { + public void return_error_if_measure_value_is_null() throws ParseException { ComponentDto project = db.components().insertPublicProject(); userSession.registerComponents(project); MetricDto metric = createQualityGateMetric(); db.measures().insertLiveMeasure(project, metric, m -> m.setValue(null).setData((String) null)); - String response = ws.newRequest() + TestResponse response = ws.newRequest() .setParam("project", project.getKey()) .setParam("metric", metric.getKey()) - .execute().getInput(); + .execute(); checkError(response, "Quality gate has not been found"); } @@ -274,20 +339,28 @@ public class QualityGateActionTest { return db.measures().insertMetric(m -> m.setKey(ALERT_STATUS_KEY).setValueType(LEVEL.name())); } - private void checkError(String svg, String expectedError) { - assertThat(svg).contains("" + expectedError + ""); + private void checkError(TestResponse response, String expectedError) throws ParseException { + SimpleDateFormat expiresDateFormat = new SimpleDateFormat(ETagUtils.RFC1123_DATE, Locale.US); + assertThat(response.getHeader("Cache-Control")).contains("no-cache"); + assertThat(response.getHeader("Expires")).isNotNull(); + assertThat(response.getHeader("ETag")).isNull(); + assertThat(expiresDateFormat.parse(response.getHeader("Expires"))).isBefore(new Date()); + assertThat(response.getInput()).contains("" + expectedError + ""); } - private void checkResponse(String response, Level status) { + private void checkResponse(TestResponse response, Level status) { + assertThat(response.getHeader("ETag")).startsWith("W/"); + assertThat(response.getHeader("Cache-Control")).contains("no-cache"); + assertThat(response.getHeader("Expires")).isNull(); switch (status) { case OK: - assertThat(response).contains(""); + assertThat(response.getInput()).contains(""); break; case WARN: - assertThat(response).contains(""); + assertThat(response.getInput()).contains(""); break; case ERROR: - assertThat(response).contains(""); + assertThat(response.getInput()).contains(""); break; } } diff --git a/server/sonar-server/src/test/java/org/sonar/server/ws/TestRequest.java b/server/sonar-server/src/test/java/org/sonar/server/ws/TestRequest.java index 64bc11f2f19..b81e9c488e5 100644 --- a/server/sonar-server/src/test/java/org/sonar/server/ws/TestRequest.java +++ b/server/sonar-server/src/test/java/org/sonar/server/ws/TestRequest.java @@ -21,6 +21,7 @@ package org.sonar.server.ws; import com.google.common.base.Throwables; import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.ListMultimap; import com.google.common.collect.Maps; import com.google.protobuf.GeneratedMessageV3; @@ -161,13 +162,18 @@ public class TestRequest extends ValidatingRequest { return this; } + @Override + public Map getHeaders() { + return ImmutableMap.copyOf(headers); + } + @Override public Optional header(String name) { return Optional.ofNullable(headers.get(name)); } public TestRequest setHeader(String name, String value) { - this.headers.put(requireNonNull(name), requireNonNull(value)); + headers.put(requireNonNull(name), requireNonNull(value)); return this; } -- 2.39.5