]> source.dussan.org Git - sonarqube.git/commitdiff
SONAR-8100 add DefaultOrganization to Pico container in CE and Web
authorSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Tue, 27 Sep 2016 15:32:37 +0000 (17:32 +0200)
committerSébastien Lesaint <sebastien.lesaint@sonarsource.com>
Wed, 28 Sep 2016 13:26:29 +0000 (15:26 +0200)
includes a ThreadLocal cache to avoid multiple calls to DB for same information

16 files changed:
server/sonar-ce/src/main/java/org/sonar/ce/container/ComputeEngineContainerImpl.java
server/sonar-ce/src/test/java/org/sonar/ce/container/ComputeEngineContainerImplTest.java
server/sonar-server/src/main/java/org/sonar/ce/organization/DefaultOrganizationLoader.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/ce/organization/package-info.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/computation/task/projectanalysis/container/ProjectAnalysisTaskContainerPopulator.java
server/sonar-server/src/main/java/org/sonar/server/organization/DefaultOrganization.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/organization/DefaultOrganizationCache.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/organization/DefaultOrganizationProvider.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/organization/DefaultOrganizationProviderImpl.java [new file with mode: 0644]
server/sonar-server/src/main/java/org/sonar/server/platform/platformlevel/PlatformLevel3.java
server/sonar-server/src/main/java/org/sonar/server/user/UserSessionFilter.java
server/sonar-server/src/test/java/org/sonar/ce/organization/DefaultOrganizationLoaderTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/organization/DefaultOrganizationProviderImplTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/organization/DefaultOrganizationProviderRule.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/organization/DefaultOrganizationTest.java [new file with mode: 0644]
server/sonar-server/src/test/java/org/sonar/server/user/UserSessionFilterTest.java

index 9796bfcd4dd9bdc6ec79c5559f7872f15735360b..a09c4cb5589793de0c0ae03e913651cae5e30e20 100644 (file)
@@ -95,6 +95,7 @@ import org.sonar.server.notification.NotificationCenter;
 import org.sonar.server.notification.NotificationService;
 import org.sonar.server.notification.email.AlertsEmailTemplate;
 import org.sonar.server.notification.email.EmailNotificationChannel;
+import org.sonar.server.organization.DefaultOrganizationProviderImpl;
 import org.sonar.server.platform.DatabaseServerCompatibility;
 import org.sonar.server.platform.DefaultServerUpgradeStatus;
 import org.sonar.server.platform.ServerFileSystemImpl;
@@ -270,7 +271,8 @@ public class ComputeEngineContainerImpl implements ComputeEngineContainer {
       new StartupMetadataProvider(),
       ServerIdManager.class,
       UriReader.class,
-      ServerImpl.class
+      ServerImpl.class,
+      DefaultOrganizationProviderImpl.class
     };
   }
 
index b027d5e4caea86269261e5c9877702f400f1eada..a3a33b4f636a4ea4c1e1386b2ae0e7fc7409af55 100644 (file)
@@ -97,7 +97,7 @@ public class ComputeEngineContainerImplTest {
     );
     assertThat(picoContainer.getParent().getComponentAdapters()).hasSize(
       CONTAINER_ITSELF
-        + 4 // level 3
+        + 5 // level 3
     );
     assertThat(picoContainer.getParent().getParent().getComponentAdapters()).hasSize(
       CONTAINER_ITSELF
diff --git a/server/sonar-server/src/main/java/org/sonar/ce/organization/DefaultOrganizationLoader.java b/server/sonar-server/src/main/java/org/sonar/ce/organization/DefaultOrganizationLoader.java
new file mode 100644 (file)
index 0000000..8af0fff
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.ce.organization;
+
+import org.picocontainer.Startable;
+import org.sonar.api.ce.ComputeEngineSide;
+import org.sonar.server.computation.task.container.EagerStart;
+import org.sonar.server.organization.DefaultOrganizationCache;
+
+@EagerStart
+@ComputeEngineSide
+public class DefaultOrganizationLoader implements Startable {
+  private final DefaultOrganizationCache defaultOrganizationCache;
+
+  public DefaultOrganizationLoader(DefaultOrganizationCache defaultOrganizationCache) {
+    this.defaultOrganizationCache = defaultOrganizationCache;
+  }
+
+  @Override
+  public void start() {
+    defaultOrganizationCache.load();
+  }
+
+  @Override
+  public void stop() {
+    defaultOrganizationCache.unload();
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/ce/organization/package-info.java b/server/sonar-server/src/main/java/org/sonar/ce/organization/package-info.java
new file mode 100644 (file)
index 0000000..48eb284
--- /dev/null
@@ -0,0 +1,23 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.
+ */
+@ParametersAreNonnullByDefault
+package org.sonar.ce.organization;
+
+import javax.annotation.ParametersAreNonnullByDefault;
index 5f455576977fb697da42c4d2509d5a3fbae4c5ee..78eeeed0b9eb6d456b8c1f6c73d1da548c285955 100644 (file)
@@ -22,19 +22,21 @@ package org.sonar.server.computation.task.projectanalysis.container;
 import java.util.Arrays;
 import java.util.List;
 import javax.annotation.Nullable;
+import org.sonar.ce.organization.DefaultOrganizationLoader;
 import org.sonar.ce.queue.CeTask;
 import org.sonar.ce.settings.SettingsLoader;
 import org.sonar.core.issue.tracking.Tracker;
 import org.sonar.core.platform.ContainerPopulator;
 import org.sonar.plugin.ce.ReportAnalysisComponentProvider;
+import org.sonar.server.computation.task.container.TaskContainer;
 import org.sonar.server.computation.task.projectanalysis.analysis.AnalysisMetadataHolderImpl;
+import org.sonar.server.computation.task.projectanalysis.api.posttask.PostProjectAnalysisTasksExecutor;
 import org.sonar.server.computation.task.projectanalysis.batch.BatchReportDirectoryHolderImpl;
 import org.sonar.server.computation.task.projectanalysis.batch.BatchReportReaderImpl;
 import org.sonar.server.computation.task.projectanalysis.component.DbIdsRepositoryImpl;
 import org.sonar.server.computation.task.projectanalysis.component.DisabledComponentsHolderImpl;
 import org.sonar.server.computation.task.projectanalysis.component.SettingsRepositoryImpl;
 import org.sonar.server.computation.task.projectanalysis.component.TreeRootHolderImpl;
-import org.sonar.server.computation.task.container.TaskContainer;
 import org.sonar.server.computation.task.projectanalysis.duplication.CrossProjectDuplicationStatusHolderImpl;
 import org.sonar.server.computation.task.projectanalysis.duplication.DuplicationRepositoryImpl;
 import org.sonar.server.computation.task.projectanalysis.duplication.IntegrateCrossProjectDuplications;
@@ -84,7 +86,6 @@ import org.sonar.server.computation.task.projectanalysis.measure.MeasureReposito
 import org.sonar.server.computation.task.projectanalysis.measure.MeasureToMeasureDto;
 import org.sonar.server.computation.task.projectanalysis.metric.MetricModule;
 import org.sonar.server.computation.task.projectanalysis.period.PeriodsHolderImpl;
-import org.sonar.server.computation.task.projectanalysis.api.posttask.PostProjectAnalysisTasksExecutor;
 import org.sonar.server.computation.task.projectanalysis.qualitygate.EvaluationResultTextConverterImpl;
 import org.sonar.server.computation.task.projectanalysis.qualitygate.QualityGateHolderImpl;
 import org.sonar.server.computation.task.projectanalysis.qualitygate.QualityGateServiceImpl;
@@ -97,9 +98,9 @@ import org.sonar.server.computation.task.projectanalysis.scm.ScmInfoRepositoryIm
 import org.sonar.server.computation.task.projectanalysis.source.LastCommitVisitor;
 import org.sonar.server.computation.task.projectanalysis.source.SourceHashRepositoryImpl;
 import org.sonar.server.computation.task.projectanalysis.source.SourceLinesRepositoryImpl;
+import org.sonar.server.computation.task.projectanalysis.step.ReportComputationSteps;
 import org.sonar.server.computation.task.step.ComputationStepExecutor;
 import org.sonar.server.computation.task.step.ComputationSteps;
-import org.sonar.server.computation.task.projectanalysis.step.ReportComputationSteps;
 import org.sonar.server.computation.taskprocessor.MutableTaskResultHolderImpl;
 import org.sonar.server.view.index.ViewIndex;
 
@@ -118,6 +119,7 @@ public final class ProjectAnalysisTaskContainerPopulator implements ContainerPop
   public void populateContainer(TaskContainer container) {
     ComputationSteps steps = new ReportComputationSteps(container);
     container.add(SettingsLoader.class);
+    container.add(DefaultOrganizationLoader.class);
     container.add(task);
     container.add(steps);
     container.addSingletons(componentClasses());
diff --git a/server/sonar-server/src/main/java/org/sonar/server/organization/DefaultOrganization.java b/server/sonar-server/src/main/java/org/sonar/server/organization/DefaultOrganization.java
new file mode 100644 (file)
index 0000000..566f5ae
--- /dev/null
@@ -0,0 +1,110 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.organization;
+
+import static java.util.Objects.requireNonNull;
+
+public class DefaultOrganization {
+  private final String uuid;
+  private final String key;
+  private final String name;
+  private final long createdAt;
+  private final long updatedAt;
+
+  private DefaultOrganization(Builder builder) {
+    this.uuid = requireNonNull(builder.uuid, "uuid can't be null");
+    this.key = requireNonNull(builder.key, "key can't be null");
+    this.name = requireNonNull(builder.name, "name can't be null");
+    this.createdAt = requireNonNull(builder.createdAt, "createdAt can't be null");
+    this.updatedAt = requireNonNull(builder.updatedAt, "updatedAt can't be null");
+  }
+
+  public static Builder newBuilder() {
+    return new Builder();
+  }
+
+  public String getUuid() {
+    return uuid;
+  }
+
+  public String getKey() {
+    return key;
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public long getCreatedAt() {
+    return createdAt;
+  }
+
+  public long getUpdatedAt() {
+    return updatedAt;
+  }
+
+  @Override
+  public String toString() {
+    return "DefaultOrganization{" +
+      "uuid='" + uuid + '\'' +
+      ", key='" + key + '\'' +
+      ", name='" + name + '\'' +
+      ", createdAt=" + createdAt +
+      ", updatedAt=" + updatedAt +
+      '}';
+  }
+
+  public static final class Builder {
+    private String uuid;
+    private String key;
+    private String name;
+    private Long createdAt;
+    private Long updatedAt;
+
+    public Builder setUuid(String uuid) {
+      this.uuid = uuid;
+      return this;
+    }
+
+    public Builder setKey(String key) {
+      this.key = key;
+      return this;
+    }
+
+    public Builder setName(String name) {
+      this.name = name;
+      return this;
+    }
+
+    public Builder setCreatedAt(long createdAt) {
+      this.createdAt = createdAt;
+      return this;
+    }
+
+    public Builder setUpdatedAt(long updatedAt) {
+      this.updatedAt = updatedAt;
+      return this;
+    }
+
+    public DefaultOrganization build() {
+      return new DefaultOrganization(this);
+    }
+  }
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/organization/DefaultOrganizationCache.java b/server/sonar-server/src/main/java/org/sonar/server/organization/DefaultOrganizationCache.java
new file mode 100644 (file)
index 0000000..0f017d8
--- /dev/null
@@ -0,0 +1,33 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.organization;
+
+public interface DefaultOrganizationCache {
+
+  /**
+   * Loads {@link DefaultOrganization} in cache.
+   */
+  void load();
+
+  /**
+   * Unloads {@link DefaultOrganization} from cache.
+   */
+  void unload();
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/organization/DefaultOrganizationProvider.java b/server/sonar-server/src/main/java/org/sonar/server/organization/DefaultOrganizationProvider.java
new file mode 100644 (file)
index 0000000..3073809
--- /dev/null
@@ -0,0 +1,28 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.organization;
+
+public interface DefaultOrganizationProvider {
+  /**
+   * @throws IllegalStateException if there is no default organization
+   */
+  DefaultOrganization get();
+
+}
diff --git a/server/sonar-server/src/main/java/org/sonar/server/organization/DefaultOrganizationProviderImpl.java b/server/sonar-server/src/main/java/org/sonar/server/organization/DefaultOrganizationProviderImpl.java
new file mode 100644 (file)
index 0000000..80f7c72
--- /dev/null
@@ -0,0 +1,97 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.organization;
+
+import java.util.Optional;
+import java.util.function.Supplier;
+import javax.annotation.CheckForNull;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.organization.OrganizationDto;
+import org.sonar.server.property.InternalProperties;
+
+import static com.google.common.base.Preconditions.checkState;
+
+public class DefaultOrganizationProviderImpl implements DefaultOrganizationProvider, DefaultOrganizationCache {
+  private static final ThreadLocal<Cache> CACHE = new ThreadLocal<>();
+
+  private final DbClient dbClient;
+
+  public DefaultOrganizationProviderImpl(DbClient dbClient) {
+    this.dbClient = dbClient;
+  }
+
+  @Override
+  public DefaultOrganization get() {
+    Cache cache = CACHE.get();
+    if (cache != null) {
+      return cache.get(() -> getDefaultOrganization(dbClient));
+    }
+
+    return getDefaultOrganization(dbClient);
+  }
+
+  private static DefaultOrganization getDefaultOrganization(DbClient dbClient) {
+    try (DbSession dbSession = dbClient.openSession(false)) {
+      Optional<String> uuid = dbClient.internalPropertiesDao().selectByKey(dbSession, InternalProperties.DEFAULT_ORGANIZATION);
+      checkState(uuid.isPresent() && !uuid.get().isEmpty(), "No Default organization uuid configured");
+      Optional<OrganizationDto> dto = dbClient.organizationDao().selectByUuid(dbSession, uuid.get());
+      checkState(dto.isPresent(), "Default organization with uuid '%s' does not exist", uuid.get());
+      return toDefaultOrganization(dto.get());
+    }
+  }
+
+  private static DefaultOrganization toDefaultOrganization(OrganizationDto organizationDto) {
+    return DefaultOrganization.newBuilder()
+      .setUuid(organizationDto.getUuid())
+      .setKey(organizationDto.getKey())
+      .setName(organizationDto.getName())
+      .setCreatedAt(organizationDto.getCreatedAt())
+      .setUpdatedAt(organizationDto.getUpdatedAt())
+      .build();
+  }
+
+  @Override
+  public void load() {
+    checkState(
+      CACHE.get() == null,
+      "load called twice for thread '%s' or state wasn't cleared last time it was used",
+      Thread.currentThread().getName());
+    CACHE.set(new Cache());
+  }
+
+  @Override
+  public void unload() {
+    CACHE.remove();
+  }
+
+  private static final class Cache {
+    @CheckForNull
+    private DefaultOrganization defaultOrganization;
+
+    public DefaultOrganization get(Supplier<DefaultOrganization> supplier) {
+      if (defaultOrganization == null) {
+        defaultOrganization = supplier.get();
+      }
+      return defaultOrganization;
+    }
+
+  }
+}
index 0d46bb9eaf9867fd72bc0285cc167f72a0b2f29e..88447785412c42923d6b08529682efc45ee7fa0b 100644 (file)
@@ -21,6 +21,7 @@ package org.sonar.server.platform.platformlevel;
 
 import org.sonar.api.utils.UriReader;
 import org.sonar.core.util.DefaultHttpDownloader;
+import org.sonar.server.organization.DefaultOrganizationProviderImpl;
 import org.sonar.server.platform.ServerIdGenerator;
 import org.sonar.server.platform.ServerIdLoader;
 import org.sonar.server.platform.ServerIdManager;
@@ -47,6 +48,7 @@ public class PlatformLevel3 extends PlatformLevel {
       ServerIdLoader.class,
       ServerIdGenerator.class,
       LogServerId.class,
-      DefaultHttpDownloader.class);
+      DefaultHttpDownloader.class,
+      DefaultOrganizationProviderImpl.class);
   }
 }
index 535641746e9854f819b6369782f50ba0e19c3ed5..73ca6d740d4be64d90ad31449957306e9968d910 100644 (file)
@@ -21,6 +21,7 @@ package org.sonar.server.user;
 
 import com.google.common.annotations.VisibleForTesting;
 import java.io.IOException;
+import javax.annotation.Nullable;
 import javax.servlet.Filter;
 import javax.servlet.FilterChain;
 import javax.servlet.FilterConfig;
@@ -30,11 +31,11 @@ import javax.servlet.ServletResponse;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import org.sonar.server.authentication.UserSessionInitializer;
+import org.sonar.server.organization.DefaultOrganizationCache;
 import org.sonar.server.platform.Platform;
 import org.sonar.server.setting.ThreadLocalSettings;
 
 public class UserSessionFilter implements Filter {
-
   private final Platform platform;
 
   public UserSessionFilter() {
@@ -52,18 +53,32 @@ public class UserSessionFilter implements Filter {
     HttpServletResponse response = (HttpServletResponse) servletResponse;
 
     ThreadLocalSettings settings = platform.getContainer().getComponentByType(ThreadLocalSettings.class);
+    DefaultOrganizationCache defaultOrganizationCache = platform.getContainer().getComponentByType(DefaultOrganizationCache.class);
     UserSessionInitializer userSessionInitializer = platform.getContainer().getComponentByType(UserSessionInitializer.class);
 
-    settings.load();
+    defaultOrganizationCache.load();
+    try {
+      settings.load();
+      try {
+        doFilter(request, response, chain, userSessionInitializer);
+      } finally {
+        settings.unload();
+      }
+    } finally {
+      defaultOrganizationCache.unload();
+    }
+  }
+
+  private static void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain,
+    @Nullable UserSessionInitializer userSessionInitializer) throws IOException, ServletException {
     try {
       if (userSessionInitializer == null || userSessionInitializer.initUserSession(request, response)) {
-        chain.doFilter(servletRequest, servletResponse);
+        chain.doFilter(request, response);
       }
     } finally {
       if (userSessionInitializer != null) {
         userSessionInitializer.removeUserSession();
       }
-      settings.unload();
     }
   }
 
diff --git a/server/sonar-server/src/test/java/org/sonar/ce/organization/DefaultOrganizationLoaderTest.java b/server/sonar-server/src/test/java/org/sonar/ce/organization/DefaultOrganizationLoaderTest.java
new file mode 100644 (file)
index 0000000..2d01e4c
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.ce.organization;
+
+import org.junit.Test;
+import org.sonar.server.organization.DefaultOrganizationCache;
+
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.verifyNoMoreInteractions;
+
+public class DefaultOrganizationLoaderTest {
+  private DefaultOrganizationCache defaultOrganizationCache = mock(DefaultOrganizationCache.class);
+  private DefaultOrganizationLoader underTest = new DefaultOrganizationLoader(defaultOrganizationCache);
+
+  @Test
+  public void start_calls_cache_load_method() {
+    underTest.start();
+
+    verify(defaultOrganizationCache).load();
+    verifyNoMoreInteractions(defaultOrganizationCache);
+  }
+
+  @Test
+  public void stop_calls_cache_unload_method() {
+    underTest.stop();
+
+    verify(defaultOrganizationCache).unload();
+    verifyNoMoreInteractions(defaultOrganizationCache);
+  }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/organization/DefaultOrganizationProviderImplTest.java b/server/sonar-server/src/test/java/org/sonar/server/organization/DefaultOrganizationProviderImplTest.java
new file mode 100644 (file)
index 0000000..ff71285
--- /dev/null
@@ -0,0 +1,185 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.organization;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+import org.sonar.api.utils.System2;
+import org.sonar.db.DbClient;
+import org.sonar.db.DbSession;
+import org.sonar.db.DbTester;
+import org.sonar.db.organization.OrganizationDto;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.fail;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+import static org.sonar.server.property.InternalProperties.DEFAULT_ORGANIZATION;
+
+public class DefaultOrganizationProviderImplTest {
+  private static final OrganizationDto ORGANIZATION_DTO_1 = new OrganizationDto()
+    .setUuid("uuid1")
+    .setName("the name of 1")
+    .setKey("the key 1");
+  private static final long DATE_1 = 1_999_888L;
+
+  private System2 system2 = mock(System2.class);
+
+  @Rule
+  public DbTester dbTester = DbTester.create(system2);
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  private DbClient dbClient = dbTester.getDbClient();
+  private DbSession dbSession = dbTester.getSession();
+
+  private DefaultOrganizationProviderImpl underTest = new DefaultOrganizationProviderImpl(dbClient);
+
+  @Test
+  public void get_fails_with_ISE_if_default_organization_internal_property_does_not_exist() {
+    expectISENoDefaultOrganizationUuid();
+
+    underTest.get();
+  }
+
+  @Test
+  public void get_fails_with_ISE_if_default_organization_internal_property_is_empty() {
+    dbClient.internalPropertiesDao().saveAsEmpty(dbSession, DEFAULT_ORGANIZATION);
+    dbSession.commit();
+
+    expectISENoDefaultOrganizationUuid();
+
+    underTest.get();
+  }
+
+  private void expectISENoDefaultOrganizationUuid() {
+    expectedException.expect(IllegalStateException.class);
+    expectedException.expectMessage("No Default organization uuid configured");
+  }
+
+  @Test
+  public void get_fails_with_ISE_if_default_organization_does_not_exist() {
+    dbClient.internalPropertiesDao().save(dbSession, DEFAULT_ORGANIZATION, "bla");
+    dbSession.commit();
+
+    expectedException.expect(IllegalStateException.class);
+    expectedException.expectMessage("Default organization with uuid 'bla' does not exist");
+
+    underTest.get();
+  }
+
+  @Test
+  public void get_returns_DefaultOrganization_populated_from_DB() {
+    insertOrganization(ORGANIZATION_DTO_1, DATE_1);
+    dbClient.internalPropertiesDao().save(dbSession, DEFAULT_ORGANIZATION, ORGANIZATION_DTO_1.getUuid());
+    dbSession.commit();
+
+    DefaultOrganization defaultOrganization = underTest.get();
+    assertThat(defaultOrganization.getUuid()).isEqualTo(ORGANIZATION_DTO_1.getUuid());
+    assertThat(defaultOrganization.getKey()).isEqualTo(ORGANIZATION_DTO_1.getKey());
+    assertThat(defaultOrganization.getName()).isEqualTo(ORGANIZATION_DTO_1.getName());
+    assertThat(defaultOrganization.getCreatedAt()).isEqualTo(DATE_1);
+    assertThat(defaultOrganization.getUpdatedAt()).isEqualTo(DATE_1);
+  }
+
+  @Test
+  public void get_returns_new_DefaultOrganization_with_each_call_when_cache_is_not_loaded() {
+    insertOrganization(ORGANIZATION_DTO_1, DATE_1);
+    dbClient.internalPropertiesDao().save(dbSession, DEFAULT_ORGANIZATION, ORGANIZATION_DTO_1.getUuid());
+    dbSession.commit();
+
+    assertThat(underTest.get()).isNotSameAs(underTest.get());
+  }
+
+  @Test
+  public void unload_does_not_fail_if_load_has_not_been_called() {
+    underTest.unload();
+  }
+
+  @Test
+  public void load_fails_with_ISE_when_called_twice_without_unload_in_between() {
+    underTest.load();
+
+    try {
+      underTest.load();
+      fail("A IllegalStateException should have been raised");
+    } catch (IllegalStateException e) {
+      assertThat(e).hasMessage("load called twice for thread '" + Thread.currentThread().getName() + "' or state wasn't cleared last time it was used");
+    } finally {
+      underTest.unload();
+    }
+  }
+
+  @Test
+  public void load_and_unload_cache_DefaultOrganization_object_by_thread() throws InterruptedException {
+    insertOrganization(ORGANIZATION_DTO_1, DATE_1);
+    dbClient.internalPropertiesDao().save(dbSession, DEFAULT_ORGANIZATION, ORGANIZATION_DTO_1.getUuid());
+    dbSession.commit();
+
+    try {
+      underTest.load();
+
+      DefaultOrganization cachedForThread1 = underTest.get();
+      assertThat(cachedForThread1).isSameAs(underTest.get());
+
+      Thread otherThread = new Thread(() -> {
+        try {
+          underTest.load();
+
+          assertThat(underTest.get())
+              .isNotSameAs(cachedForThread1)
+              .isSameAs(underTest.get());
+        } finally {
+          underTest.unload();
+        }
+      });
+      otherThread.start();
+      otherThread.join();
+    } finally {
+      underTest.unload();
+    }
+  }
+
+  @Test
+  public void get_returns_new_instance_for_each_call_once_unload_has_been_called() {
+    insertOrganization(ORGANIZATION_DTO_1, DATE_1);
+    dbClient.internalPropertiesDao().save(dbSession, DEFAULT_ORGANIZATION, ORGANIZATION_DTO_1.getUuid());
+    dbSession.commit();
+
+    try {
+      underTest.load();
+      DefaultOrganization cached = underTest.get();
+      assertThat(cached).isSameAs(underTest.get());
+
+      underTest.unload();
+      assertThat(underTest.get()).isNotSameAs(underTest.get()).isNotSameAs(cached);
+    } finally {
+      // fail safe
+      underTest.unload();
+    }
+  }
+
+  private void insertOrganization(OrganizationDto dto, long createdAt) {
+    when(system2.now()).thenReturn(createdAt);
+    dbClient.organizationDao().insert(dbSession, dto);
+    dbSession.commit();
+  }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/organization/DefaultOrganizationProviderRule.java b/server/sonar-server/src/test/java/org/sonar/server/organization/DefaultOrganizationProviderRule.java
new file mode 100644 (file)
index 0000000..7f07855
--- /dev/null
@@ -0,0 +1,70 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.organization;
+
+import com.google.common.base.Preconditions;
+import org.junit.rules.ExternalResource;
+import org.sonar.core.util.UuidFactoryImpl;
+
+public class DefaultOrganizationProviderRule extends ExternalResource implements DefaultOrganizationProvider {
+  private DefaultOrganization defaultOrganization;
+
+  private DefaultOrganizationProviderRule(DefaultOrganization defaultOrganization) {
+    this.defaultOrganization = defaultOrganization;
+  }
+
+  /**
+   * <p>
+   * This method is meant to be statically imported.
+   * </p>
+   */
+  public static DefaultOrganizationProviderRule someDefaultOrganization() {
+    String uuid = UuidFactoryImpl.INSTANCE.create();
+    return new DefaultOrganizationProviderRule(DefaultOrganization.newBuilder()
+      .setUuid(uuid)
+      .setName("Default organization " + uuid)
+      .setKey(uuid + "_key")
+      .setCreatedAt(uuid.hashCode())
+      .setUpdatedAt(uuid.hashCode())
+      .build());
+  }
+
+  /**
+   * <p>
+   * This method is meant to be statically imported.
+   * </p>
+   */
+  public static DefaultOrganizationProviderRule defaultOrganizationWithName(String name) {
+    String uuid = UuidFactoryImpl.INSTANCE.create();
+    return new DefaultOrganizationProviderRule(DefaultOrganization.newBuilder()
+      .setUuid(uuid)
+      .setName(name)
+      .setKey(uuid + "_key")
+      .setCreatedAt(uuid.hashCode())
+      .setUpdatedAt(uuid.hashCode())
+      .build());
+  }
+
+  @Override
+  public DefaultOrganization get() {
+    Preconditions.checkState(defaultOrganization != null, "No default organization is set");
+    return defaultOrganization;
+  }
+}
diff --git a/server/sonar-server/src/test/java/org/sonar/server/organization/DefaultOrganizationTest.java b/server/sonar-server/src/test/java/org/sonar/server/organization/DefaultOrganizationTest.java
new file mode 100644 (file)
index 0000000..90f825d
--- /dev/null
@@ -0,0 +1,116 @@
+/*
+ * SonarQube
+ * Copyright (C) 2009-2016 SonarSource SA
+ * mailto:contact 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.organization;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+public class DefaultOrganizationTest {
+  private static final long DATE_2 = 2_000_000L;
+  private static final long DATE_1 = 1_000_000L;
+
+  @Rule
+  public ExpectedException expectedException = ExpectedException.none();
+
+  private DefaultOrganization.Builder populatedBuilder = new DefaultOrganization.Builder()
+    .setUuid("uuid")
+    .setKey("key")
+    .setName("name")
+    .setCreatedAt(DATE_1)
+    .setUpdatedAt(DATE_2);
+
+  @Test
+  public void build_fails_if_uuid_is_null() {
+    populatedBuilder.setUuid(null);
+
+    expectedException.expect(NullPointerException.class);
+    expectedException.expectMessage("uuid can't be null");
+
+    populatedBuilder.build();
+  }
+
+  @Test
+  public void build_fails_if_key_is_null() {
+    populatedBuilder.setKey(null);
+
+    expectedException.expect(NullPointerException.class);
+    expectedException.expectMessage("key can't be null");
+
+    populatedBuilder.build();
+  }
+
+  @Test
+  public void build_fails_if_name_is_null() {
+    populatedBuilder.setName(null);
+
+    expectedException.expect(NullPointerException.class);
+    expectedException.expectMessage("name can't be null");
+
+    populatedBuilder.build();
+  }
+
+  @Test
+  public void build_fails_if_createdAt_not_set() {
+    DefaultOrganization.Builder underTest = new DefaultOrganization.Builder()
+      .setUuid("uuid")
+      .setKey("key")
+      .setName("name")
+      .setUpdatedAt(DATE_2);
+
+    expectedException.expect(NullPointerException.class);
+    expectedException.expectMessage("createdAt can't be null");
+
+    underTest.build();
+  }
+
+  @Test
+  public void build_fails_if_updatedAt_not_set() {
+    DefaultOrganization.Builder underTest = new DefaultOrganization.Builder()
+      .setUuid("uuid")
+      .setKey("key")
+      .setName("name")
+      .setCreatedAt(DATE_1);
+
+    expectedException.expect(NullPointerException.class);
+    expectedException.expectMessage("updatedAt can't be null");
+
+    underTest.build();
+  }
+
+  @Test
+  public void verify_toString() {
+    assertThat(populatedBuilder.build().toString())
+      .isEqualTo("DefaultOrganization{uuid='uuid', key='key', name='name', createdAt=1000000, updatedAt=2000000}");
+  }
+
+  @Test
+  public void verify_getters() {
+    DefaultOrganization underTest = populatedBuilder.build();
+
+    assertThat(underTest.getUuid()).isEqualTo("uuid");
+    assertThat(underTest.getKey()).isEqualTo("key");
+    assertThat(underTest.getName()).isEqualTo("name");
+    assertThat(underTest.getCreatedAt()).isEqualTo(DATE_1);
+    assertThat(underTest.getUpdatedAt()).isEqualTo(DATE_2);
+  }
+}
index 6e6810a596a9c7b3fc979b0640c7608d4f54269e..3768bfc388bf163dbb9b93e7625891012b6b772b 100644 (file)
@@ -27,11 +27,17 @@ import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 import org.junit.Before;
 import org.junit.Test;
+import org.mockito.InOrder;
 import org.mockito.Mockito;
 import org.sonar.server.authentication.UserSessionInitializer;
+import org.sonar.server.organization.DefaultOrganizationCache;
 import org.sonar.server.platform.Platform;
 import org.sonar.server.setting.ThreadLocalSettings;
 
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.fail;
+import static org.mockito.Mockito.doThrow;
+import static org.mockito.Mockito.inOrder;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.never;
 import static org.mockito.Mockito.verify;
@@ -46,17 +52,18 @@ public class UserSessionFilterTest {
   private HttpServletResponse response = mock(HttpServletResponse.class);
   private FilterChain chain = mock(FilterChain.class);
   private ThreadLocalSettings settings = mock(ThreadLocalSettings.class);
+  private DefaultOrganizationCache defaultOrganizationCache = mock(DefaultOrganizationCache.class);
   private UserSessionFilter underTest = new UserSessionFilter(platform);
 
   @Before
   public void setUp() {
     when(platform.getContainer().getComponentByType(ThreadLocalSettings.class)).thenReturn(settings);
+    when(platform.getContainer().getComponentByType(DefaultOrganizationCache.class)).thenReturn(defaultOrganizationCache);
   }
 
   @Test
   public void cleanup_user_session_after_request_handling() throws IOException, ServletException {
-    when(platform.getContainer().getComponentByType(UserSessionInitializer.class)).thenReturn(userSessionInitializer);
-    when(userSessionInitializer.initUserSession(request, response)).thenReturn(true);
+    mockUserSessionInitializer(true);
 
     underTest.doFilter(request, response, chain);
 
@@ -66,8 +73,7 @@ public class UserSessionFilterTest {
 
   @Test
   public void stop_when_user_session_return_false() throws Exception {
-    when(platform.getContainer().getComponentByType(UserSessionInitializer.class)).thenReturn(userSessionInitializer);
-    when(userSessionInitializer.initUserSession(request, response)).thenReturn(false);
+    mockUserSessionInitializer(false);
 
     underTest.doFilter(request, response, chain);
 
@@ -77,7 +83,7 @@ public class UserSessionFilterTest {
 
   @Test
   public void does_nothing_when_not_initialized() throws Exception {
-    when(platform.getContainer().getComponentByType(UserSessionInitializer.class)).thenReturn(null);
+    mockNoUserSessionInitializer();
 
     underTest.doFilter(request, response, chain);
 
@@ -85,6 +91,118 @@ public class UserSessionFilterTest {
     verifyZeroInteractions(userSessionInitializer);
   }
 
+  @Test
+  public void doFilter_loads_and_unloads_settings() throws Exception {
+    mockNoUserSessionInitializer();
+
+    underTest.doFilter(request, response, chain);
+
+    InOrder inOrder = inOrder(settings);
+    inOrder.verify(settings).load();
+    inOrder.verify(settings).unload();
+    inOrder.verifyNoMoreInteractions();
+  }
+
+  @Test
+  public void doFilter_unloads_Settings_even_if_chain_throws_exception() throws Exception {
+    mockNoUserSessionInitializer();
+    RuntimeException thrown = mockChainDoFilterError();
+
+    try {
+      underTest.doFilter(request, response, chain);
+      fail("A RuntimeException should have been thrown");
+    } catch (RuntimeException e) {
+      assertThat(e).isSameAs(thrown);
+      verify(settings).unload();
+    }
+  }
+
+  @Test
+  public void doFilter_unloads_Settings_even_if_DefaultOrganizationCache_unload_fails() throws Exception {
+    mockNoUserSessionInitializer();
+    RuntimeException thrown = new RuntimeException("Faking DefaultOrganizationCache.unload failing");
+    doThrow(thrown)
+        .when(defaultOrganizationCache)
+        .unload();
+
+    try {
+      underTest.doFilter(request, response, chain);
+      fail("A RuntimeException should have been thrown");
+    } catch (RuntimeException e) {
+      assertThat(e).isSameAs(thrown);
+      verify(settings).unload();
+    }
+  }
+
+  @Test
+  public void doFilter_unloads_Settings_even_if_UserSessionInitializer_removeUserSession_fails() throws Exception {
+    RuntimeException thrown = mockUserSessionInitializerRemoveUserSessionFailing();
+
+    try {
+      underTest.doFilter(request, response, chain);
+      fail("A RuntimeException should have been thrown");
+    } catch (RuntimeException e) {
+      assertThat(e).isSameAs(thrown);
+      verify(settings).unload();
+    }
+  }
+
+  @Test
+  public void doFilter_loads_and_unloads_DefaultOrganizationCache() throws Exception {
+    mockNoUserSessionInitializer();
+
+    underTest.doFilter(request, response, chain);
+
+    InOrder inOrder = inOrder(defaultOrganizationCache);
+    inOrder.verify(defaultOrganizationCache).load();
+    inOrder.verify(defaultOrganizationCache).unload();
+    inOrder.verifyNoMoreInteractions();
+  }
+
+  @Test
+  public void doFilter_unloads_DefaultOrganizationCache_even_if_chain_throws_exception() throws Exception {
+    mockNoUserSessionInitializer();
+    RuntimeException thrown = mockChainDoFilterError();
+
+    try {
+      underTest.doFilter(request, response, chain);
+      fail("A RuntimeException should have been thrown");
+    } catch (RuntimeException e) {
+      assertThat(e).isSameAs(thrown);
+      verify(defaultOrganizationCache).unload();
+    }
+  }
+
+  @Test
+  public void doFilter_unloads_DefaultOrganizationCache_even_if_Settings_unload_fails() throws Exception {
+    mockNoUserSessionInitializer();
+    RuntimeException thrown = new RuntimeException("Faking Settings.unload failing");
+    doThrow(thrown)
+        .when(settings)
+        .unload();
+
+    try {
+      underTest.doFilter(request, response, chain);
+      fail("A RuntimeException should have been thrown");
+    } catch (RuntimeException e) {
+      assertThat(e).isSameAs(thrown);
+      verify(defaultOrganizationCache).unload();
+    }
+  }
+
+  @Test
+  public void doFilter_unloads_DefaultOrganizationCache_even_if_UserSessionInitializer_removeUserSession_fails() throws Exception {
+    RuntimeException thrown = mockUserSessionInitializerRemoveUserSessionFailing();
+
+    try {
+      underTest.doFilter(request, response, chain);
+      fail("A RuntimeException should have been thrown");
+    } catch (RuntimeException e) {
+      assertThat(e).isSameAs(thrown);
+      verify(defaultOrganizationCache).unload();
+    }
+  }
+
   @Test
   public void just_for_fun_and_coverage() throws ServletException {
     UserSessionFilter filter = new UserSessionFilter();
@@ -92,4 +210,30 @@ public class UserSessionFilterTest {
     filter.destroy();
     // do not fail
   }
+
+  private void mockNoUserSessionInitializer() {
+    when(platform.getContainer().getComponentByType(UserSessionInitializer.class)).thenReturn(null);
+  }
+
+  private void mockUserSessionInitializer(boolean value) {
+    when(platform.getContainer().getComponentByType(UserSessionInitializer.class)).thenReturn(userSessionInitializer);
+    when(userSessionInitializer.initUserSession(request, response)).thenReturn(value);
+  }
+
+  private RuntimeException mockUserSessionInitializerRemoveUserSessionFailing() {
+    when(platform.getContainer().getComponentByType(UserSessionInitializer.class)).thenReturn(userSessionInitializer);
+    RuntimeException thrown = new RuntimeException("Faking UserSessionInitializer.removeUserSession failing");
+    doThrow(thrown)
+        .when(userSessionInitializer)
+        .removeUserSession();
+    return thrown;
+  }
+
+  private RuntimeException mockChainDoFilterError() throws IOException, ServletException {
+    RuntimeException thrown = new RuntimeException("Faking chain.doFilter failing");
+    doThrow(thrown)
+        .when(chain)
+        .doFilter(request, response);
+    return thrown;
+  }
 }