]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-10340 Add ETags for badges
authorEric Hartmann <hartmann.eric@gmail.com>
Wed, 6 Jun 2018 06:43:42 +0000 (08:43 +0200)
committerSonarTech <sonartech@sonarsource.com>
Thu, 7 Jun 2018 18:20:47 +0000 (20:20 +0200)
server/sonar-server/src/main/java/org/sonar/server/badge/ws/ETagUtils.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/badge/ws/MeasureAction.java
server/sonar-server/src/main/java/org/sonar/server/badge/ws/QualityGateAction.java
server/sonar-server/src/test/java/org/sonar/server/badge/ws/ETagUtilsTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/badge/ws/MeasureActionTest.java
server/sonar-server/src/test/java/org/sonar/server/badge/ws/QualityGateActionTest.java
server/sonar-server/src/test/java/org/sonar/server/ws/TestRequest.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 (file)
index 0000000..4f98f50
--- /dev/null
@@ -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 <a href="https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function#FNV-1a_hash"></a>
+   */
+  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 <a href="https://en.wikipedia.org/wiki/HTTP_ETag"></a>
+   * <a href="https://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.11"></a>
+   */
+  static String getETag(String output) {
+    return "W/" + hash(output.getBytes(Charset.forName("UTF-8")));
+  }
+}
index 5a2f9406a6bb26e9d73b2de1a70d686890aa11ed..462fcd9b90c8b8a51a57192eec7222a0b7f2c1a8 100644 (file)
@@ -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<String> 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;
   }
 
+
 }
index dad88fd643f759782284f9a61522d96c3038c3bc..4c71ae1b528f77551c63fd59040fef2468c5bf62 100644 (file)
 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<String> 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 (file)
index 0000000..36df418
--- /dev/null
@@ -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));
+  }
+}
index 652675893f845b02a6aab151c814d61cec00e56b..31c2d4e277d59e85da6591ca14b95ab94ccf8742 100644 (file)
@@ -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(
       "<text", expectedLabel + "</text>",
       "<text", expectedValue + "</text>",
       "rect fill=\"" + expectedColorValue.getValue() + "\"");
   }
 
-  private void checkError(String svg, String expectedError) {
-    assertThat(svg).contains("<text", ">" + expectedError + "</text>");
+  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("<text", ">" + expectedError + "</text>");
   }
 
+  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()));
   }
index 52cedacbf863fa624221601d14e36139b35ea005..39b0b9e54cfc29dfeb329ff69996eb45000adb97 100644 (file)
  */
 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("<text", ">" + expectedError + "</text>");
+  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("<text", ">" + expectedError + "</text>");
   }
 
-  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("<!-- SONARQUBE QUALITY GATE PASS -->");
+        assertThat(response.getInput()).contains("<!-- SONARQUBE QUALITY GATE PASS -->");
         break;
       case WARN:
-        assertThat(response).contains("<!-- SONARQUBE QUALITY GATE WARN -->");
+        assertThat(response.getInput()).contains("<!-- SONARQUBE QUALITY GATE WARN -->");
         break;
       case ERROR:
-        assertThat(response).contains("<!-- SONARQUBE QUALITY GATE FAIL -->");
+        assertThat(response.getInput()).contains("<!-- SONARQUBE QUALITY GATE FAIL -->");
         break;
     }
   }
index 64bc11f2f19b336da3dd4df37d3e09d0f3614d39..b81e9c488e5fe3a43b526cfb29462b82661f5cdc 100644 (file)
@@ -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<String, String> getHeaders() {
+    return ImmutableMap.copyOf(headers);
+  }
+
   @Override
   public Optional<String> 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;
   }