diff --git a/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc b/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc index ec8f2932..f267612f 100644 --- a/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc +++ b/docs/src/docs/asciidoc/customizing-requests-and-responses.adoc @@ -108,12 +108,10 @@ You can also specify a different replacement if you wish. -[[customizing-requests-and-responses-preprocessors-remove-headers]] -==== Removing Headers +[[customizing-requests-and-responses-preprocessors-modify-headers]] +==== Modifying Headers -`removeHeaders` on `Preprocessors` removes any headers from the request or response where the name is equal to any of the given header names. - -`removeMatchingHeaders` on `Preprocessors` removes any headers from the request or response where the name matches any of the given regular expression patterns. +You can use `modifyHeaders` on `Preprocessors` to add, set, and remove request or response headers. diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessor.java deleted file mode 100644 index 01710e99..00000000 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessor.java +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright 2014-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.restdocs.operation.preprocess; - -import java.util.List; -import java.util.Map.Entry; - -import org.springframework.http.HttpHeaders; -import org.springframework.restdocs.operation.OperationRequest; -import org.springframework.restdocs.operation.OperationRequestFactory; -import org.springframework.restdocs.operation.OperationResponse; -import org.springframework.restdocs.operation.OperationResponseFactory; - -/** - * An {@link OperationPreprocessor} that removes headers. The headers to remove are - * provided as constructor arguments and can be either plain string or patterns to match - * against the headers found - * - * @author Andy Wilkinson - */ -class HeaderRemovingOperationPreprocessor implements OperationPreprocessor { - - private final OperationRequestFactory requestFactory = new OperationRequestFactory(); - - private final OperationResponseFactory responseFactory = new OperationResponseFactory(); - - private final HeaderFilter headerFilter; - - HeaderRemovingOperationPreprocessor(HeaderFilter headerFilter) { - this.headerFilter = headerFilter; - } - - @Override - public OperationResponse preprocess(OperationResponse response) { - return this.responseFactory.createFrom(response, removeHeaders(response.getHeaders())); - } - - @Override - public OperationRequest preprocess(OperationRequest request) { - return this.requestFactory.createFrom(request, removeHeaders(request.getHeaders())); - } - - private HttpHeaders removeHeaders(HttpHeaders originalHeaders) { - HttpHeaders processedHeaders = new HttpHeaders(); - for (Entry> header : originalHeaders.entrySet()) { - if (!this.headerFilter.excludeHeader(header.getKey())) { - processedHeaders.put(header.getKey(), header.getValue()); - } - } - return processedHeaders; - } - -} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeadersModifyingOperationPreprocessor.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeadersModifyingOperationPreprocessor.java new file mode 100644 index 00000000..72078847 --- /dev/null +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/HeadersModifyingOperationPreprocessor.java @@ -0,0 +1,218 @@ +/* + * Copyright 2014-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.restdocs.operation.preprocess; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.springframework.http.HttpHeaders; +import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationRequestFactory; +import org.springframework.restdocs.operation.OperationResponse; +import org.springframework.restdocs.operation.OperationResponseFactory; +import org.springframework.util.Assert; + +/** + * An {@link OperationPreprocessor} that modifies a request or response by adding, + * setting, or removing headers. + * + * @author Jihoon Cha + * @author Andy Wilkinson + * @since 3.0.0 + */ +public class HeadersModifyingOperationPreprocessor implements OperationPreprocessor { + + private final OperationRequestFactory requestFactory = new OperationRequestFactory(); + + private final OperationResponseFactory responseFactory = new OperationResponseFactory(); + + private final List modifications = new ArrayList<>(); + + @Override + public OperationRequest preprocess(OperationRequest request) { + return this.requestFactory.createFrom(request, preprocess(request.getHeaders())); + } + + @Override + public OperationResponse preprocess(OperationResponse response) { + return this.responseFactory.createFrom(response, preprocess(response.getHeaders())); + } + + private HttpHeaders preprocess(HttpHeaders headers) { + HttpHeaders modifiedHeaders = new HttpHeaders(); + modifiedHeaders.putAll(headers); + for (Modification modification : this.modifications) { + modification.applyTo(modifiedHeaders); + } + return modifiedHeaders; + } + + /** + * Adds a header with the given {@code name} and {@code value}. + * @param name the name + * @param value the value + * @return {@code this} + */ + public HeadersModifyingOperationPreprocessor add(String name, String value) { + this.modifications.add(new AddHeaderModification(name, value)); + return this; + } + + /** + * Sets the header with the given {@code name} to have the given {@code values}. + * @param name the name + * @param values the values + * @return {@code this} + */ + public HeadersModifyingOperationPreprocessor set(String name, String... values) { + Assert.notEmpty(values, "At least one value must be provided"); + this.modifications.add(new SetHeaderModification(name, Arrays.asList(values))); + return this; + } + + /** + * Removes the header with the given {@code name}. + * @param name the name of the parameter + * @return {@code this} + */ + public HeadersModifyingOperationPreprocessor remove(String name) { + this.modifications.add(new RemoveHeaderModification(name)); + return this; + } + + /** + * Removes the given {@code value} from the header with the given {@code name}. + * @param name the name + * @param value the value + * @return {@code this} + */ + public HeadersModifyingOperationPreprocessor remove(String name, String value) { + this.modifications.add(new RemoveValueHeaderModification(name, value)); + return this; + } + + /** + * Remove headers that match the given {@code namePattern} regular expression. + * @param namePattern the name pattern + * @return {@code this} + * @see Matcher#matches() + */ + public HeadersModifyingOperationPreprocessor removeMatching(String namePattern) { + this.modifications.add(new RemoveHeadersByNamePatternModification(Pattern.compile(namePattern))); + return this; + } + + private interface Modification { + + void applyTo(HttpHeaders headers); + + } + + private static final class AddHeaderModification implements Modification { + + private final String name; + + private final String value; + + private AddHeaderModification(String name, String value) { + this.name = name; + this.value = value; + } + + @Override + public void applyTo(HttpHeaders headers) { + headers.add(this.name, this.value); + } + + } + + private static final class SetHeaderModification implements Modification { + + private final String name; + + private final List values; + + private SetHeaderModification(String name, List values) { + this.name = name; + this.values = values; + } + + @Override + public void applyTo(HttpHeaders headers) { + headers.put(this.name, this.values); + } + + } + + private static final class RemoveHeaderModification implements Modification { + + private final String name; + + private RemoveHeaderModification(String name) { + this.name = name; + } + + @Override + public void applyTo(HttpHeaders headers) { + headers.remove(this.name); + } + + } + + private static final class RemoveValueHeaderModification implements Modification { + + private final String name; + + private final String value; + + private RemoveValueHeaderModification(String name, String value) { + this.name = name; + this.value = value; + } + + @Override + public void applyTo(HttpHeaders headers) { + List values = headers.get(this.name); + if (values != null) { + values.remove(this.value); + if (values.isEmpty()) { + headers.remove(this.name); + } + } + } + + } + + private static final class RemoveHeadersByNamePatternModification implements Modification { + + private final Pattern namePattern; + + private RemoveHeadersByNamePatternModification(Pattern namePattern) { + this.namePattern = namePattern; + } + + @Override + public void applyTo(HttpHeaders headers) { + headers.keySet().removeIf((name) -> this.namePattern.matcher(name).matches()); + } + + } + +} diff --git a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/Preprocessors.java b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/Preprocessors.java index 6be9107c..a70b2169 100644 --- a/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/Preprocessors.java +++ b/spring-restdocs-core/src/main/java/org/springframework/restdocs/operation/preprocess/Preprocessors.java @@ -31,6 +31,7 @@ import org.springframework.restdocs.operation.OperationResponse; * * @author Andy Wilkinson * @author Roland Huss + * @author Jihoon Cha */ public final class Preprocessors { @@ -73,10 +74,17 @@ public final class Preprocessors { * {@code headersToRemove}. * @param headerNames the header names * @return the preprocessor + * @deprecated since 3.0.0 in favor of {@link #modifyHeaders()} and + * {@link HeadersModifyingOperationPreprocessor#remove(String)} * @see String#equals(Object) */ + @Deprecated public static OperationPreprocessor removeHeaders(String... headerNames) { - return new HeaderRemovingOperationPreprocessor(new ExactMatchHeaderFilter(headerNames)); + HeadersModifyingOperationPreprocessor preprocessor = new HeadersModifyingOperationPreprocessor(); + for (String headerName : headerNames) { + preprocessor.remove(headerName); + } + return preprocessor; } /** @@ -85,10 +93,17 @@ public final class Preprocessors { * {@code headerNamePatterns} regular expressions. * @param headerNamePatterns the header name patterns * @return the preprocessor + * @deprecated since 3.0.0 in favor of {@link #modifyHeaders()} and + * {@link HeadersModifyingOperationPreprocessor#removeMatching(String)} * @see java.util.regex.Matcher#matches() */ + @Deprecated public static OperationPreprocessor removeMatchingHeaders(String... headerNamePatterns) { - return new HeaderRemovingOperationPreprocessor(new PatternMatchHeaderFilter(headerNamePatterns)); + HeadersModifyingOperationPreprocessor preprocessor = new HeadersModifyingOperationPreprocessor(); + for (String headerNamePattern : headerNamePatterns) { + preprocessor.removeMatching(headerNamePattern); + } + return preprocessor; } /** @@ -132,6 +147,16 @@ public final class Preprocessors { return new ParametersModifyingOperationPreprocessor(); } + /** + * Returns a {@code HeadersModifyingOperationPreprocessor} that can then be configured + * to modify the headers of the request. + * @return the preprocessor + * @since 3.0.0 + */ + public static HeadersModifyingOperationPreprocessor modifyHeaders() { + return new HeadersModifyingOperationPreprocessor(); + } + /** * Returns a {@code UriModifyingOperationPreprocessor} that will modify URIs in the * request or response by changing one or more of their host, scheme, and port. diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessorTests.java deleted file mode 100644 index 87f5e8c0..00000000 --- a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeaderRemovingOperationPreprocessorTests.java +++ /dev/null @@ -1,104 +0,0 @@ -/* - * Copyright 2014-2019 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package org.springframework.restdocs.operation.preprocess; - -import java.net.URI; -import java.util.Arrays; -import java.util.Collections; - -import org.junit.Test; - -import org.springframework.http.HttpHeaders; -import org.springframework.http.HttpMethod; -import org.springframework.http.HttpStatus; -import org.springframework.restdocs.operation.OperationRequest; -import org.springframework.restdocs.operation.OperationRequestFactory; -import org.springframework.restdocs.operation.OperationRequestPart; -import org.springframework.restdocs.operation.OperationResponse; -import org.springframework.restdocs.operation.OperationResponseFactory; -import org.springframework.restdocs.operation.Parameters; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link HeaderRemovingOperationPreprocessorTests}. - * - * @author Andy Wilkinson - * @author Roland Huss - */ -public class HeaderRemovingOperationPreprocessorTests { - - private final OperationRequestFactory requestFactory = new OperationRequestFactory(); - - private final OperationResponseFactory responseFactory = new OperationResponseFactory(); - - private final HeaderRemovingOperationPreprocessor preprocessor = new HeaderRemovingOperationPreprocessor( - new ExactMatchHeaderFilter("b")); - - @Test - public void modifyRequestHeaders() { - OperationRequest request = this.requestFactory.create(URI.create("http://localhost"), HttpMethod.GET, - new byte[0], getHttpHeaders(), new Parameters(), Collections.emptyList()); - OperationRequest preprocessed = this.preprocessor.preprocess(request); - assertThat(preprocessed.getHeaders().size()).isEqualTo(2); - assertThat(preprocessed.getHeaders()).containsEntry("a", Arrays.asList("alpha")); - assertThat(preprocessed.getHeaders()).containsEntry("Host", Arrays.asList("localhost")); - } - - @Test - public void modifyResponseHeaders() { - OperationResponse response = createResponse(); - OperationResponse preprocessed = this.preprocessor.preprocess(response); - assertThat(preprocessed.getHeaders().size()).isEqualTo(1); - assertThat(preprocessed.getHeaders()).containsEntry("a", Arrays.asList("alpha")); - } - - @Test - public void modifyWithPattern() { - OperationResponse response = createResponse("content-length", "1234"); - HeaderRemovingOperationPreprocessor processor = new HeaderRemovingOperationPreprocessor( - new PatternMatchHeaderFilter("co.*le(.)gth]")); - OperationResponse preprocessed = processor.preprocess(response); - assertThat(preprocessed.getHeaders().size()).isEqualTo(2); - assertThat(preprocessed.getHeaders()).containsEntry("a", Arrays.asList("alpha")); - assertThat(preprocessed.getHeaders()).containsEntry("b", Arrays.asList("bravo", "banana")); - } - - @Test - public void removeAllHeaders() { - HeaderRemovingOperationPreprocessor processor = new HeaderRemovingOperationPreprocessor( - new PatternMatchHeaderFilter(".*")); - OperationResponse preprocessed = processor.preprocess(createResponse()); - assertThat(preprocessed.getHeaders().size()).isEqualTo(0); - } - - private OperationResponse createResponse(String... extraHeaders) { - return this.responseFactory.create(HttpStatus.OK.value(), getHttpHeaders(extraHeaders), new byte[0]); - } - - private HttpHeaders getHttpHeaders(String... extraHeaders) { - HttpHeaders httpHeaders = new HttpHeaders(); - httpHeaders.add("a", "alpha"); - httpHeaders.add("b", "bravo"); - httpHeaders.add("b", "banana"); - for (int i = 0; i < extraHeaders.length; i += 2) { - httpHeaders.add(extraHeaders[i], extraHeaders[i + 1]); - } - return httpHeaders; - } - -} diff --git a/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeadersModifyingOperationPreprocessorTests.java b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeadersModifyingOperationPreprocessorTests.java new file mode 100644 index 00000000..ad55d141 --- /dev/null +++ b/spring-restdocs-core/src/test/java/org/springframework/restdocs/operation/preprocess/HeadersModifyingOperationPreprocessorTests.java @@ -0,0 +1,167 @@ +/* + * Copyright 2014-2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.restdocs.operation.preprocess; + +import java.net.URI; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.function.Consumer; + +import org.junit.Test; + +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.restdocs.operation.OperationRequest; +import org.springframework.restdocs.operation.OperationRequestFactory; +import org.springframework.restdocs.operation.OperationResponse; +import org.springframework.restdocs.operation.OperationResponseFactory; +import org.springframework.restdocs.operation.Parameters; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link HeadersModifyingOperationPreprocessor}. + * + * @author Jihoon Cha + * @author Andy Wilkinson + */ +public class HeadersModifyingOperationPreprocessorTests { + + private final HeadersModifyingOperationPreprocessor preprocessor = new HeadersModifyingOperationPreprocessor(); + + @Test + public void addNewHeader() { + this.preprocessor.add("a", "alpha"); + assertThat(this.preprocessor.preprocess(createRequest()).getHeaders()).containsEntry("a", + Arrays.asList("alpha")); + assertThat(this.preprocessor.preprocess(createResponse()).getHeaders()).containsEntry("a", + Arrays.asList("alpha")); + } + + @Test + public void addValueToExistingHeader() { + this.preprocessor.add("a", "alpha"); + assertThat(this.preprocessor.preprocess(createRequest((headers) -> headers.add("a", "apple"))).getHeaders()) + .containsEntry("a", Arrays.asList("apple", "alpha")); + assertThat(this.preprocessor.preprocess(createResponse((headers) -> headers.add("a", "apple"))).getHeaders()) + .containsEntry("a", Arrays.asList("apple", "alpha")); + } + + @Test + public void setNewHeader() { + this.preprocessor.set("a", "alpha", "avocado"); + assertThat(this.preprocessor.preprocess(createRequest()).getHeaders()).containsEntry("a", + Arrays.asList("alpha", "avocado")); + assertThat(this.preprocessor.preprocess(createResponse()).getHeaders()).containsEntry("a", + Arrays.asList("alpha", "avocado")); + } + + @Test + public void setExistingHeader() { + this.preprocessor.set("a", "alpha", "avocado"); + assertThat(this.preprocessor.preprocess(createRequest((headers) -> headers.add("a", "apple"))).getHeaders()) + .containsEntry("a", Arrays.asList("alpha", "avocado")); + assertThat(this.preprocessor.preprocess(createResponse((headers) -> headers.add("a", "apple"))).getHeaders()) + .containsEntry("a", Arrays.asList("alpha", "avocado")); + } + + @Test + public void removeNonExistentHeader() { + this.preprocessor.remove("a"); + assertThat(this.preprocessor.preprocess(createRequest()).getHeaders()).doesNotContainKey("a"); + assertThat(this.preprocessor.preprocess(createResponse()).getHeaders()).doesNotContainKey("a"); + } + + @Test + public void removeHeader() { + OperationPreprocessor preprocessor = this.preprocessor.remove("a"); + assertThat(preprocessor.preprocess(createRequest((headers) -> headers.add("a", "apple"))).getHeaders()) + .doesNotContainKey("a"); + assertThat(preprocessor.preprocess(createResponse((headers) -> headers.add("a", "apple"))).getHeaders()) + .doesNotContainKey("a"); + } + + @Test + public void removeHeaderValueForNonExistentHeader() { + this.preprocessor.remove("a", "apple"); + assertThat(this.preprocessor.preprocess(createRequest()).getHeaders()).doesNotContainKey("a"); + assertThat(this.preprocessor.preprocess(createResponse()).getHeaders()).doesNotContainKey("a"); + } + + @Test + public void removeHeaderValueWithMultipleValues() { + this.preprocessor.remove("a", "apple"); + assertThat(this.preprocessor + .preprocess(createRequest((headers) -> headers.addAll("a", List.of("apple", "alpha")))).getHeaders()) + .containsEntry("a", Arrays.asList("alpha")); + assertThat(this.preprocessor + .preprocess(createResponse((headers) -> headers.addAll("a", List.of("apple", "alpha")))).getHeaders()) + .containsEntry("a", Arrays.asList("alpha")); + } + + @Test + public void removeHeaderValueWithSingleValueRemovesEntryEntirely() { + this.preprocessor.remove("a", "apple"); + assertThat(this.preprocessor.preprocess(createRequest((headers) -> headers.add("a", "apple"))).getHeaders()) + .doesNotContainKey("a"); + assertThat(this.preprocessor.preprocess(createResponse((headers) -> headers.add("a", "apple"))).getHeaders()) + .doesNotContainKey("a"); + } + + @Test + public void removeHeadersByNamePattern() { + Consumer headersCustomizer = (headers) -> { + headers.add("apple", "apple"); + headers.add("alpha", "alpha"); + headers.add("avocado", "avocado"); + headers.add("bravo", "bravo"); + }; + this.preprocessor.removeMatching("^a.*"); + assertThat(this.preprocessor.preprocess(createRequest(headersCustomizer)).getHeaders()).containsOnlyKeys("Host", + "bravo"); + assertThat(this.preprocessor.preprocess(createResponse(headersCustomizer)).getHeaders()) + .containsOnlyKeys("bravo"); + } + + private OperationRequest createRequest() { + return createRequest(null); + } + + private OperationRequest createRequest(Consumer headersCustomizer) { + HttpHeaders headers = new HttpHeaders(); + if (headersCustomizer != null) { + headersCustomizer.accept(headers); + } + return new OperationRequestFactory().create(URI.create("http://localhost:8080"), HttpMethod.GET, new byte[0], + headers, new Parameters(), Collections.emptyList()); + } + + private OperationResponse createResponse() { + return createResponse(null); + } + + private OperationResponse createResponse(Consumer headersCustomizer) { + HttpHeaders headers = new HttpHeaders(); + if (headersCustomizer != null) { + headersCustomizer.accept(headers); + } + return new OperationResponseFactory().create(HttpStatus.OK.value(), headers, new byte[0]); + } + +}