Do not rely on magic numbers for parsing the URL Refactor PullReplicationFilter to use regular expressions when projectName is extracted Add unit tests for PullReplicationFilter to cover main functionality Bug: Issue 345363 Change-Id: I5c762df2d79a7b06aaed49c09d5f47f2d4b19dda
diff --git a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilter.java b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilter.java index 0a9b266..d2bca2b 100644 --- a/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilter.java +++ b/src/main/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilter.java
@@ -25,7 +25,6 @@ import static javax.servlet.http.HttpServletResponse.SC_OK; import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED; -import com.google.common.base.Splitter; import com.google.common.flogger.FluentLogger; import com.google.gerrit.extensions.annotations.PluginName; import com.google.gerrit.extensions.api.projects.HeadInput; @@ -38,7 +37,6 @@ import com.google.gerrit.extensions.restapi.RestApiException; import com.google.gerrit.extensions.restapi.TopLevelResource; import com.google.gerrit.extensions.restapi.UnprocessableEntityException; -import com.google.gerrit.extensions.restapi.Url; import com.google.gerrit.httpd.AllRequestFilter; import com.google.gerrit.httpd.restapi.RestApiServlet; import com.google.gerrit.json.OutputFormat; @@ -61,9 +59,11 @@ import java.io.EOFException; import java.io.IOException; import java.io.PrintWriter; -import java.util.ArrayList; -import java.util.List; import java.util.Map; +import java.util.NoSuchElementException; +import java.util.Optional; +import java.util.regex.Matcher; +import java.util.regex.Pattern; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.ServletRequest; @@ -74,6 +74,10 @@ public class PullReplicationFilter extends AllRequestFilter { private static final FluentLogger logger = FluentLogger.forEnclosingClass(); + private static final Pattern projectNameInGerritUrl = Pattern.compile(".*/projects/([^/]+)/.*"); + private static final Pattern projectNameInitProjectUrl = + Pattern.compile(".*/init-project/([^/]+.git)"); + private FetchAction fetchAction; private ApplyObjectAction applyObjectAction; private ApplyObjectsAction applyObjectsAction; @@ -180,6 +184,9 @@ } catch (InitProjectException | ResourceNotFoundException e) { RestApiServlet.replyError( httpRequest, httpResponse, SC_INTERNAL_SERVER_ERROR, e.getMessage(), e.caching(), e); + } catch (NoSuchElementException e) { + RestApiServlet.replyError( + httpRequest, httpResponse, SC_BAD_REQUEST, "Project name not present in the url", e); } catch (Exception e) { throw new ServletException(e); } @@ -188,8 +195,8 @@ private void doInitProject(HttpServletRequest httpRequest, HttpServletResponse httpResponse) throws RestApiException, IOException, PermissionBackendException { - String path = httpRequest.getRequestURI(); - String projectName = Url.decode(path.substring(path.lastIndexOf('/') + 1)); + IdString id = getInitProjectName(httpRequest).get(); + String projectName = id.get(); if (projectInitializationAction.initProject(projectName)) { setResponse( httpResponse, HttpServletResponse.SC_CREATED, "Project " + projectName + " initialized"); @@ -202,7 +209,7 @@ private Response<Map<String, Object>> doApplyObject(HttpServletRequest httpRequest) throws RestApiException, IOException, PermissionBackendException { RevisionInput input = readJson(httpRequest, TypeLiteral.get(RevisionInput.class)); - IdString id = getProjectName(httpRequest); + IdString id = getProjectName(httpRequest).get(); ProjectResource projectResource = projectsCollection.parse(TopLevelResource.INSTANCE, id); return (Response<Map<String, Object>>) applyObjectAction.apply(projectResource, input); @@ -212,7 +219,7 @@ private Response<Map<String, Object>> doApplyObjects(HttpServletRequest httpRequest) throws RestApiException, IOException, PermissionBackendException { RevisionsInput input = readJson(httpRequest, TypeLiteral.get(RevisionsInput.class)); - IdString id = getProjectName(httpRequest); + IdString id = getProjectName(httpRequest).get(); ProjectResource projectResource = projectsCollection.parse(TopLevelResource.INSTANCE, id); return (Response<Map<String, Object>>) applyObjectsAction.apply(projectResource, input); @@ -221,16 +228,16 @@ @SuppressWarnings("unchecked") private Response<String> doUpdateHEAD(HttpServletRequest httpRequest) throws Exception { HeadInput input = readJson(httpRequest, TypeLiteral.get(HeadInput.class)); - ProjectResource projectResource = - projectsCollection.parse(TopLevelResource.INSTANCE, getProjectName(httpRequest)); + IdString id = getProjectName(httpRequest).get(); + ProjectResource projectResource = projectsCollection.parse(TopLevelResource.INSTANCE, id); return (Response<String>) updateHEADAction.apply(projectResource, input); } @SuppressWarnings("unchecked") private Response<String> doDeleteProject(HttpServletRequest httpRequest) throws Exception { - ProjectResource projectResource = - projectsCollection.parse(TopLevelResource.INSTANCE, getProjectName(httpRequest)); + IdString id = getProjectName(httpRequest).get(); + ProjectResource projectResource = projectsCollection.parse(TopLevelResource.INSTANCE, id); return (Response<String>) projectDeletionAction.apply(projectResource, new ProjectDeletionAction.DeleteInput()); } @@ -239,7 +246,7 @@ private Response<Map<String, Object>> doFetch(HttpServletRequest httpRequest) throws IOException, RestApiException, PermissionBackendException { Input input = readJson(httpRequest, TypeLiteral.get(Input.class)); - IdString id = getProjectName(httpRequest); + IdString id = getProjectName(httpRequest).get(); ProjectResource projectResource = projectsCollection.parse(TopLevelResource.INSTANCE, id); return (Response<Map<String, Object>>) fetchAction.apply(projectResource, input); @@ -296,42 +303,48 @@ * @param req * @return project name */ - private IdString getProjectName(HttpServletRequest req) { - String path = req.getRequestURI(); + private Optional<IdString> getInitProjectName(HttpServletRequest req) { + return extractProjectName(req, projectNameInitProjectUrl); + } - List<IdString> out = new ArrayList<>(); - for (String p : Splitter.on('/').split(path)) { - out.add(IdString.fromUrl(p)); + private Optional<IdString> getProjectName(HttpServletRequest req) { + return extractProjectName(req, projectNameInGerritUrl); + } + + private Optional<IdString> extractProjectName(HttpServletRequest req, Pattern urlPattern) { + String path = req.getRequestURI(); + Matcher projectGroupMatcher = urlPattern.matcher(path); + + if (projectGroupMatcher.find()) { + return Optional.of(IdString.fromUrl(projectGroupMatcher.group(1))); } - if (!out.isEmpty() && out.get(out.size() - 1).isEmpty()) { - out.remove(out.size() - 1); - } - return out.get(3); + + return Optional.empty(); } private boolean isApplyObjectAction(HttpServletRequest httpRequest) { - return httpRequest.getRequestURI().endsWith("pull-replication~apply-object"); + return httpRequest.getRequestURI().endsWith(String.format("/%s~apply-object", pluginName)); } private boolean isApplyObjectsAction(HttpServletRequest httpRequest) { - return httpRequest.getRequestURI().endsWith("pull-replication~apply-objects"); + return httpRequest.getRequestURI().endsWith(String.format("/%s~apply-objects", pluginName)); } private boolean isFetchAction(HttpServletRequest httpRequest) { - return httpRequest.getRequestURI().endsWith("pull-replication~fetch"); + return httpRequest.getRequestURI().endsWith(String.format("/%s~fetch", pluginName)); } private boolean isInitProjectAction(HttpServletRequest httpRequest) { - return httpRequest.getRequestURI().contains("pull-replication/init-project/"); + return httpRequest.getRequestURI().contains(String.format("/%s/init-project/", pluginName)); } private boolean isUpdateHEADAction(HttpServletRequest httpRequest) { - return httpRequest.getRequestURI().matches("(/a)?/projects/[^/]+/HEAD") + return httpRequest.getRequestURI().matches(".*/projects/[^/]+/HEAD") && "PUT".equals(httpRequest.getMethod()); } private boolean isDeleteProjectAction(HttpServletRequest httpRequest) { - return httpRequest.getRequestURI().endsWith(String.format("%s~delete-project", pluginName)) + return httpRequest.getRequestURI().endsWith(String.format("/%s~delete-project", pluginName)) && "DELETE".equals(httpRequest.getMethod()); } }
diff --git a/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilterTest.java b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilterTest.java new file mode 100644 index 0000000..da2d4a5 --- /dev/null +++ b/src/test/java/com/googlesource/gerrit/plugins/replication/pull/api/PullReplicationFilterTest.java
@@ -0,0 +1,358 @@ +package com.googlesource.gerrit.plugins.replication.pull.api; + +import static com.google.common.net.HttpHeaders.ACCEPT; +import static com.google.gerrit.httpd.restapi.RestApiServlet.SC_UNPROCESSABLE_ENTITY; +import static javax.servlet.http.HttpServletResponse.SC_CONFLICT; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.mockito.internal.verification.VerificationModeFactory.atLeastOnce; +import static org.mockito.internal.verification.VerificationModeFactory.times; + +import com.google.common.net.MediaType; +import com.google.gerrit.extensions.restapi.*; +import com.google.gerrit.server.CurrentUser; +import com.google.gerrit.server.project.ProjectResource; +import com.google.gerrit.server.restapi.project.ProjectsCollection; +import com.google.inject.Provider; +import java.io.*; +import java.nio.charset.StandardCharsets; +import javax.servlet.FilterChain; +import javax.servlet.ServletOutputStream; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class PullReplicationFilterTest { + + @Mock HttpServletRequest request; + @Mock HttpServletResponse response; + @Mock FilterChain filterChain; + @Mock private FetchAction fetchAction; + @Mock private ApplyObjectAction applyObjectAction; + @Mock private ApplyObjectsAction applyObjectsAction; + @Mock private ProjectInitializationAction projectInitializationAction; + @Mock private UpdateHeadAction updateHEADAction; + @Mock private ProjectDeletionAction projectDeletionAction; + @Mock private ProjectsCollection projectsCollection; + @Mock private CurrentUser currentUser; + @Mock private Provider<CurrentUser> userProvider; + @Mock private ProjectResource projectResource; + @Mock private ServletOutputStream outputStream; + @Mock private PrintWriter printWriter; + private final String PLUGIN_NAME = "pull-replication"; + private final String PROJECT_NAME = "some-project"; + private final String PROJECT_NAME_GIT = "some-project.git"; + private final String FETCH_URI = + String.format("any-prefix/projects/%s/%s~fetch", PROJECT_NAME, PLUGIN_NAME); + private final String APPLY_OBJECT_URI = + String.format("any-prefix/projects/%s/%s~apply-object", PROJECT_NAME, PLUGIN_NAME); + private final String APPLY_OBJECTS_URI = + String.format("any-prefix/projects/%s/%s~apply-objects", PROJECT_NAME, PLUGIN_NAME); + private final String HEAD_URI = String.format("any-prefix/projects/%s/HEAD", PROJECT_NAME); + private final String DELETE_PROJECT_URI = + String.format("any-prefix/projects/%s/%s~delete-project", PROJECT_NAME, PLUGIN_NAME); + private final String INIT_PROJECT_URI = + String.format("any-prefix/%s/init-project/%s", PLUGIN_NAME, PROJECT_NAME_GIT); + + private final Response OK_RESPONSE = Response.ok(); + + private PullReplicationFilter createPullReplicationFilter() { + return new PullReplicationFilter( + fetchAction, + applyObjectAction, + applyObjectsAction, + projectInitializationAction, + updateHEADAction, + projectDeletionAction, + projectsCollection, + userProvider, + PLUGIN_NAME); + } + + private void defineBehaviours(byte[] payload, String uri) throws Exception { + when(request.getRequestURI()).thenReturn(uri); + when(userProvider.get()).thenReturn(currentUser); + when(currentUser.isIdentifiedUser()).thenReturn(true); + InputStream is = new ByteArrayInputStream(payload); + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(is)); + when(request.getReader()).thenReturn(bufferedReader); + when(projectsCollection.parse(TopLevelResource.INSTANCE, IdString.fromDecoded(PROJECT_NAME))) + .thenReturn(projectResource); + when(response.getWriter()).thenReturn(printWriter); + } + + private void verifyBehaviours() throws Exception { + verify(request, atLeastOnce()).getRequestURI(); + verify(userProvider).get(); + verify(currentUser).isIdentifiedUser(); + verify(request).getReader(); + verify(projectsCollection).parse(TopLevelResource.INSTANCE, IdString.fromDecoded(PROJECT_NAME)); + verify(response).getWriter(); + verify(response).setContentType("application/json"); + verify(response).setStatus(HttpServletResponse.SC_OK); + } + + @Test + public void shouldFilterFetchAction() throws Exception { + byte[] payloadFetch = + ("{" + + "\"label\":\"Replication\", " + + "\"ref_name\": \"refs/heads/master\", " + + "\"async\":false" + + "}") + .getBytes(StandardCharsets.UTF_8); + + defineBehaviours(payloadFetch, FETCH_URI); + when(fetchAction.apply(any(), any())).thenReturn(OK_RESPONSE); + + PullReplicationFilter pullReplicationFilter = createPullReplicationFilter(); + pullReplicationFilter.doFilter(request, response, filterChain); + + verifyBehaviours(); + verify(fetchAction).apply(eq(projectResource), any()); + } + + @Test + public void shouldFilterApplyObjectAction() throws Exception { + + byte[] payloadApplyObject = + ("{\"label\":\"Replication\",\"ref_name\":\"refs/heads/master\"," + + "\"revision_data\":{" + + "\"commit_object\":{\"type\":1,\"content\":\"some-content\"}," + + "\"tree_object\":{\"type\":2,\"content\":\"some-content\"}," + + "\"blobs\":[]}" + + "}") + .getBytes(StandardCharsets.UTF_8); + + defineBehaviours(payloadApplyObject, APPLY_OBJECT_URI); + + when(applyObjectAction.apply(any(), any())).thenReturn(OK_RESPONSE); + + PullReplicationFilter pullReplicationFilter = createPullReplicationFilter(); + pullReplicationFilter.doFilter(request, response, filterChain); + + verifyBehaviours(); + verify(applyObjectAction).apply(eq(projectResource), any()); + } + + @Test + public void shouldFilterApplyObjectsAction() throws Exception { + + byte[] payloadApplyObjects = + ("{\"label\":\"Replication\",\"ref_name\":\"refs/heads/master\"," + + "\"revisions_data\":[{" + + "\"commit_object\":{\"type\":1,\"content\":\"some-content\"}," + + "\"tree_object\":{\"type\":2,\"content\":\"some-content\"}," + + "\"blobs\":[]}]}") + .getBytes(StandardCharsets.UTF_8); + + defineBehaviours(payloadApplyObjects, APPLY_OBJECTS_URI); + + when(applyObjectsAction.apply(any(), any())).thenReturn(OK_RESPONSE); + + PullReplicationFilter pullReplicationFilter = createPullReplicationFilter(); + pullReplicationFilter.doFilter(request, response, filterChain); + + verifyBehaviours(); + verify(applyObjectsAction).apply(eq(projectResource), any()); + } + + @Test + public void shouldFilterProjectInitializationAction() throws Exception { + + when(request.getRequestURI()).thenReturn(INIT_PROJECT_URI); + when(request.getHeader(ACCEPT)).thenReturn(MediaType.PLAIN_TEXT_UTF_8.toString()); + when(userProvider.get()).thenReturn(currentUser); + when(currentUser.isIdentifiedUser()).thenReturn(true); + when(projectInitializationAction.initProject(PROJECT_NAME_GIT)).thenReturn(true); + when(response.getWriter()).thenReturn(printWriter); + + final PullReplicationFilter pullReplicationFilter = createPullReplicationFilter(); + pullReplicationFilter.doFilter(request, response, filterChain); + + verify(request, times(5)).getRequestURI(); + verify(userProvider).get(); + verify(currentUser).isIdentifiedUser(); + verify(projectInitializationAction).initProject(eq(PROJECT_NAME_GIT)); + verify(response).getWriter(); + } + + @Test + public void shouldFilterUpdateHEADAction() throws Exception { + + byte[] payloadUpdateHead = "{\"ref\":\"some-ref\"}".getBytes(StandardCharsets.UTF_8); + defineBehaviours(payloadUpdateHead, HEAD_URI); + when(request.getMethod()).thenReturn("PUT"); + when(updateHEADAction.apply(any(), any())).thenReturn(OK_RESPONSE); + + final PullReplicationFilter pullReplicationFilter = createPullReplicationFilter(); + pullReplicationFilter.doFilter(request, response, filterChain); + + verifyBehaviours(); + verify(updateHEADAction).apply(eq(projectResource), any()); + } + + @Test + public void shouldFilterProjectDeletionAction() throws Exception { + when(request.getRequestURI()).thenReturn(DELETE_PROJECT_URI); + when(request.getMethod()).thenReturn("DELETE"); + when(userProvider.get()).thenReturn(currentUser); + when(currentUser.isIdentifiedUser()).thenReturn(true); + when(projectsCollection.parse(TopLevelResource.INSTANCE, IdString.fromDecoded(PROJECT_NAME))) + .thenReturn(projectResource); + when(projectDeletionAction.apply(any(), any())).thenReturn(OK_RESPONSE); + when(response.getWriter()).thenReturn(printWriter); + + final PullReplicationFilter pullReplicationFilter = createPullReplicationFilter(); + pullReplicationFilter.doFilter(request, response, filterChain); + + verify(request, times(7)).getRequestURI(); + verify(userProvider).get(); + verify(currentUser).isIdentifiedUser(); + verify(projectsCollection).parse(TopLevelResource.INSTANCE, IdString.fromDecoded(PROJECT_NAME)); + verify(projectDeletionAction).apply(eq(projectResource), any()); + verify(response).getWriter(); + verify(response).setContentType("application/json"); + verify(response).setStatus(OK_RESPONSE.statusCode()); + } + + @Test + public void shouldGoNextInChainWhenUriDoesNotMatch() throws Exception { + when(request.getRequestURI()).thenReturn("any-url"); + final PullReplicationFilter pullReplicationFilter = createPullReplicationFilter(); + pullReplicationFilter.doFilter(request, response, filterChain); + verify(filterChain).doFilter(request, response); + } + + @Test + public void shouldBe404WhenJsonIsMalformed() throws Exception { + byte[] payloadMalformedJson = "some-json-malformed".getBytes(StandardCharsets.UTF_8); + InputStream is = new ByteArrayInputStream(payloadMalformedJson); + BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(is)); + when(request.getRequestURI()).thenReturn(FETCH_URI); + when(request.getReader()).thenReturn(bufferedReader); + when(userProvider.get()).thenReturn(currentUser); + when(currentUser.isIdentifiedUser()).thenReturn(true); + when(response.getOutputStream()).thenReturn(outputStream); + + PullReplicationFilter pullReplicationFilter = createPullReplicationFilter(); + pullReplicationFilter.doFilter(request, response, filterChain); + + verify(response).setStatus(HttpServletResponse.SC_BAD_REQUEST); + } + + @Test + public void shouldBe500WhenProjectCannotBeInitiated() throws Exception { + when(request.getRequestURI()).thenReturn(INIT_PROJECT_URI); + when(request.getHeader(ACCEPT)).thenReturn(MediaType.PLAIN_TEXT_UTF_8.toString()); + when(userProvider.get()).thenReturn(currentUser); + when(currentUser.isIdentifiedUser()).thenReturn(true); + when(projectInitializationAction.initProject(PROJECT_NAME_GIT)).thenReturn(false); + when(response.getOutputStream()).thenReturn(outputStream); + + final PullReplicationFilter pullReplicationFilter = createPullReplicationFilter(); + pullReplicationFilter.doFilter(request, response, filterChain); + + verify(response).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + + @Test + public void shouldBe500WhenResourceNotFound() throws Exception { + when(request.getRequestURI()).thenReturn(DELETE_PROJECT_URI); + when(request.getMethod()).thenReturn("DELETE"); + when(userProvider.get()).thenReturn(currentUser); + when(currentUser.isIdentifiedUser()).thenReturn(true); + when(projectsCollection.parse(TopLevelResource.INSTANCE, IdString.fromDecoded(PROJECT_NAME))) + .thenReturn(projectResource); + when(projectDeletionAction.apply(any(), any())) + .thenThrow(new ResourceNotFoundException("resource not found")); + when(response.getOutputStream()).thenReturn(outputStream); + + final PullReplicationFilter pullReplicationFilter = createPullReplicationFilter(); + pullReplicationFilter.doFilter(request, response, filterChain); + + verify(response).setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR); + } + + @Test + public void shouldBe403WhenUserIsNotAuthorised() throws Exception { + byte[] payloadFetchAction = + ("{" + + "\"label\":\"Replication\", " + + "\"ref_name\": \"refs/heads/master\", " + + "\"async\":false" + + "}") + .getBytes(StandardCharsets.UTF_8); + + defineBehaviours(payloadFetchAction, FETCH_URI); + when(fetchAction.apply(any(), any())) + .thenThrow(new AuthException("The user is not authorised")); + when(response.getOutputStream()).thenReturn(outputStream); + + PullReplicationFilter pullReplicationFilter = createPullReplicationFilter(); + pullReplicationFilter.doFilter(request, response, filterChain); + + verify(response).setStatus(HttpServletResponse.SC_FORBIDDEN); + } + + @Test + public void shouldBe422WhenEntityCannotBeProcessed() throws Exception { + byte[] payloadFetchAction = + ("{" + + "\"label\":\"Replication\", " + + "\"ref_name\": \"refs/heads/master\", " + + "\"async\":false" + + "}") + .getBytes(StandardCharsets.UTF_8); + + defineBehaviours(payloadFetchAction, FETCH_URI); + when(fetchAction.apply(any(), any())) + .thenThrow(new UnprocessableEntityException("Entity cannot be processed")); + when(response.getOutputStream()).thenReturn(outputStream); + + PullReplicationFilter pullReplicationFilter = createPullReplicationFilter(); + pullReplicationFilter.doFilter(request, response, filterChain); + + verify(response).setStatus(SC_UNPROCESSABLE_ENTITY); + } + + @Test + public void shouldBe409WhenThereIsResourceConflict() throws Exception { + when(request.getRequestURI()).thenReturn(DELETE_PROJECT_URI); + when(request.getMethod()).thenReturn("DELETE"); + when(userProvider.get()).thenReturn(currentUser); + when(currentUser.isIdentifiedUser()).thenReturn(true); + when(projectsCollection.parse(TopLevelResource.INSTANCE, IdString.fromDecoded(PROJECT_NAME))) + .thenReturn(projectResource); + + when(projectDeletionAction.apply(any(), any())) + .thenThrow(new ResourceConflictException("Resource conflict")); + when(response.getOutputStream()).thenReturn(outputStream); + + PullReplicationFilter pullReplicationFilter = createPullReplicationFilter(); + pullReplicationFilter.doFilter(request, response, filterChain); + + verify(response).setStatus(SC_CONFLICT); + } + + @Test + public void shouldBe400WhenProjectNameIsNotPresentInURL() throws Exception { + when(request.getRequestURI()) + .thenReturn(String.format("any-prefix/projects/%s~delete-project", PLUGIN_NAME)); + when(request.getMethod()).thenReturn("DELETE"); + when(userProvider.get()).thenReturn(currentUser); + when(currentUser.isIdentifiedUser()).thenReturn(true); + when(response.getOutputStream()).thenReturn(outputStream); + + PullReplicationFilter pullReplicationFilter = createPullReplicationFilter(); + pullReplicationFilter.doFilter(request, response, filterChain); + + verify(response).setStatus(HttpServletResponse.SC_BAD_REQUEST); + } +}