diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index b9190893f..0304c1a49 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -2,6 +2,8 @@ #### 7.3.2-SNAPSHOT +* PR [#270](https://github.com/azagniotov/stubby4j/pull/270) - Added `additive` proxy strategy behavior that sets additional headers to the request (https://github.com/azagniotov) + #### 7.3.1 * PR [#264](https://github.com/azagniotov/stubby4j/pull/264) - Multiple proxy configurations support (https://github.com/azagniotov) diff --git a/docs/REQUEST_PROXYING.md b/docs/REQUEST_PROXYING.md index c559243ba..a6a2d876c 100644 --- a/docs/REQUEST_PROXYING.md +++ b/docs/REQUEST_PROXYING.md @@ -122,20 +122,41 @@ This can be anything describing your proxy configuration. #### strategy (`required`) -Describes how the request to-be-proxied should be proxied. Currently only `as-is` strategy is supported, which means that request will be proxied without any changes/modifications before being sent to the proxy service. In the future enhancements to the request proxying functionality, more strategies will be supported. +Describes how the request to-be-proxied should be proxied. Currently only the following strategy values are supported: +* `as-is`: no changes/modifications will be applied to the request before proxying it +* `additive`: additive changes will be applied to the request before proxying it. The additive changes currently supported are setting of additional HTTP headers using the `headers` property on the `proxy-config` object. + +In the future enhancements to the request proxying functionality, more strategies will be supported. #### properties (`required`) -Describes the properties of the proxy, e.g.: endpoint, headers, etc. Currently only `endpoint` property is supported. In the future enhancements to the request proxying functionality, more properties will be supported. +A map of key/value pairs describing the properties of the proxy, e.g.: endpoint, etc. Currently only `endpoint` property is supported. In the future enhancements to the request proxying functionality, more properties will be supported. + +##### endpoint (`required`) -#### endpoint (`required`) +Must be defined under the `properties` property. Describes the target service endpoint where the request will be proxied to. This should be a protocol scheme + fully qualified domain name (i.e.: `FQDN`) without any URI paths: -Describes the target service endpoint where the request will be proxied to. This should be a protocol scheme + fully qualified domain name (i.e.: `FQDN`) without any URI paths: * Correct: `https://jsonplaceholder.typicode.com` * Incorrect: `jsonplaceholder.typicode.com` * Incorrect: `https://jsonplaceholder.typicode.com/` * Incorrect: `https://jsonplaceholder.typicode.com/posts/1` +#### headers (`optional`) + +A map of key/value pairs describing an HTTP header name and its value. The `headers` property can be used when `strategy` property is set to `additive`. The headers will be added to the request being proxied in an additive manner, i.e.: they will not replace the headers already set on the request. + +```yaml +- proxy-config: + uuid: some-other-unique-name + strategy: additive + properties: + endpoint: https://jsonplaceholder.typicode.com + headers: + content-type: application/json+special + x-custom-header: something/unique + x-custom-header-2: another/thing +``` + ## Application of proxy config at runtime Request proxying happens when there is at least one `proxy config` object defined in the YAML config and an incoming HTTP request did not match any of the declared `stubby4j`'s stubs. diff --git a/src/functional-test/java/io/github/azagniotov/stubby4j/ProxyConfigWithStubsTest.java b/src/functional-test/java/io/github/azagniotov/stubby4j/ProxyConfigWithStubsTest.java index 1d5cb9903..541a4ef32 100644 --- a/src/functional-test/java/io/github/azagniotov/stubby4j/ProxyConfigWithStubsTest.java +++ b/src/functional-test/java/io/github/azagniotov/stubby4j/ProxyConfigWithStubsTest.java @@ -140,7 +140,7 @@ public void shouldReturnProxiedResponseUsingSpecificProxyConfig_WhenStubsWereNot // The 'null' overrides the default value "gzip", also I had to .disableContentCompression() on WEB_CLIENT httpHeaders.setAcceptEncoding(null); - // The 'some-unique-name' is actually set in include-proxy-config.yaml + // The 'some-unique-name' is actually set in 'proxy-config' object defined in resources/yaml/include-proxy-config.yaml httpHeaders.set(HEADER_X_STUBBY_PROXY_CONFIG, "some-unique-name"); request.setHeaders(httpHeaders); @@ -175,7 +175,9 @@ public void should_UpdateStubbedProxyConfig_WithJsonRequest_ByValidUuid() throws assertThat(getResponseContent).isEqualTo( "- proxy-config:\n" + " uuid: some-unique-name\n" + - " strategy: custom\n" + + " strategy: additive\n" + + " headers:\n" + + " x-original-stubby4j-custom-header: custom/value\n" + " properties:\n" + " endpoint: https://jsonplaceholder.typicode.com"); @@ -208,7 +210,9 @@ public void should_UpdateStubbedProxyConfig_WithJsonRequest_ByValidUuid() throws assertThat(getResponseContent).isEqualTo( "- proxy-config:\n" + " uuid: some-unique-name\n" + - " strategy: custom\n" + + " strategy: additive\n" + + " headers:\n" + + " x-custom-header: custom/value\n" + " properties:\n" + " endpoint: https://UPDATED.com"); } diff --git a/src/functional-test/resources/json/request/json_payload_11.json b/src/functional-test/resources/json/request/json_payload_11.json index c776bbefc..5350e6849 100644 --- a/src/functional-test/resources/json/request/json_payload_11.json +++ b/src/functional-test/resources/json/request/json_payload_11.json @@ -2,7 +2,10 @@ { "proxy-config": { "uuid": "some-unique-name", - "strategy": "custom", + "strategy": "additive", + "headers": { + "x-custom-header": "custom/value" + }, "properties": { "endpoint": "https://UPDATED.com" } diff --git a/src/functional-test/resources/yaml/include-proxy-config.yaml b/src/functional-test/resources/yaml/include-proxy-config.yaml index 4f8b4ff71..dd81ba02a 100644 --- a/src/functional-test/resources/yaml/include-proxy-config.yaml +++ b/src/functional-test/resources/yaml/include-proxy-config.yaml @@ -6,20 +6,22 @@ - proxy-config: uuid: some-unique-name - strategy: custom + strategy: additive + headers: + x-original-stubby4j-custom-header: custom/value properties: endpoint: https://jsonplaceholder.typicode.com - proxy-config: description: description-2 uuid: some-unique-name-two - strategy: custom + strategy: additive properties: endpoint: https://google.com - proxy-config: description: description-3 uuid: some-unique-name-three - strategy: custom + strategy: additive properties: endpoint: https://google.com diff --git a/src/integration-test/java/io/github/azagniotov/stubby4j/yaml/YamlParserTest.java b/src/integration-test/java/io/github/azagniotov/stubby4j/yaml/YamlParserTest.java index 1ead77e37..2334389ef 100644 --- a/src/integration-test/java/io/github/azagniotov/stubby4j/yaml/YamlParserTest.java +++ b/src/integration-test/java/io/github/azagniotov/stubby4j/yaml/YamlParserTest.java @@ -967,7 +967,7 @@ public void shouldUnmarshall_toProxyConfigs() throws Exception { final StubProxyConfig customStubProxyConfig = proxyConfigs.get("some-unique-name"); assertThat(customStubProxyConfig.getUUID()).isEqualTo("some-unique-name"); - assertThat(customStubProxyConfig.getStrategy()).isEqualTo(StubProxyStrategy.CUSTOM); + assertThat(customStubProxyConfig.getStrategy()).isEqualTo(StubProxyStrategy.ADDITIVE); assertThat(customStubProxyConfig.getProperties().size()).isEqualTo(1); assertThat(customStubProxyConfig.getProperties().get("endpoint")).isEqualTo("https://jsonplaceholder.typicode.com"); @@ -985,7 +985,7 @@ public void shouldUnmarshall_toProxyConfigs() throws Exception { "- proxy-config:\n" + " description: woah! this is a unique proxy-config\n" + " uuid: some-unique-name\n" + - " strategy: custom\n" + + " strategy: additive\n" + " properties:\n" + " endpoint: https://jsonplaceholder.typicode.com\n"); } @@ -1014,7 +1014,7 @@ public void shouldUnmarshall_toProxyConfigsWithStubs() throws Exception { final StubProxyConfig customStubProxyConfig = proxyConfigs.get("some-unique-name"); assertThat(customStubProxyConfig.getUUID()).isEqualTo("some-unique-name"); - assertThat(customStubProxyConfig.getStrategy()).isEqualTo(StubProxyStrategy.CUSTOM); + assertThat(customStubProxyConfig.getStrategy()).isEqualTo(StubProxyStrategy.ADDITIVE); assertThat(customStubProxyConfig.getProperties().size()).isEqualTo(1); assertThat(customStubProxyConfig.getProperties().get("endpoint")).isEqualTo("https://jsonplaceholder.typicode.com"); @@ -1027,7 +1027,7 @@ public void shouldUnmarshall_toProxyConfigsWithStubs() throws Exception { assertThat(customStubProxyConfig.getProxyConfigAsYAML()).isEqualTo( "- proxy-config:\n" + " uuid: some-unique-name\n" + - " strategy: custom\n" + + " strategy: additive\n" + " properties:\n" + " endpoint: https://jsonplaceholder.typicode.com\n"); } diff --git a/src/integration-test/resources/yaml/proxy-config-duplicate-uuid.yaml b/src/integration-test/resources/yaml/proxy-config-duplicate-uuid.yaml index 9b68edc0b..cf74af99f 100644 --- a/src/integration-test/resources/yaml/proxy-config-duplicate-uuid.yaml +++ b/src/integration-test/resources/yaml/proxy-config-duplicate-uuid.yaml @@ -6,6 +6,6 @@ - proxy-config: uuid: some-unique-name - strategy: custom + strategy: additive properties: endpoint: https://jsonplaceholder.typicode.com diff --git a/src/integration-test/resources/yaml/proxy-config-valid-config-with-stubs.yaml b/src/integration-test/resources/yaml/proxy-config-valid-config-with-stubs.yaml index c42514072..883857fd7 100644 --- a/src/integration-test/resources/yaml/proxy-config-valid-config-with-stubs.yaml +++ b/src/integration-test/resources/yaml/proxy-config-valid-config-with-stubs.yaml @@ -5,7 +5,7 @@ - proxy-config: uuid: some-unique-name - strategy: custom + strategy: additive properties: endpoint: https://jsonplaceholder.typicode.com diff --git a/src/integration-test/resources/yaml/proxy-config-valid-config.yaml b/src/integration-test/resources/yaml/proxy-config-valid-config.yaml index c495b0655..1f8397dfc 100644 --- a/src/integration-test/resources/yaml/proxy-config-valid-config.yaml +++ b/src/integration-test/resources/yaml/proxy-config-valid-config.yaml @@ -10,6 +10,6 @@ - proxy-config: description: woah! this is a unique proxy-config uuid: some-unique-name - strategy: custom + strategy: additive properties: endpoint: https://jsonplaceholder.typicode.com diff --git a/src/integration-test/resources/yaml/proxy-config-without-default-config.yaml b/src/integration-test/resources/yaml/proxy-config-without-default-config.yaml index 4c8be8c1e..b0c81ffda 100644 --- a/src/integration-test/resources/yaml/proxy-config-without-default-config.yaml +++ b/src/integration-test/resources/yaml/proxy-config-without-default-config.yaml @@ -6,6 +6,6 @@ - proxy-config: uuid: another-unique-name - strategy: custom + strategy: additive properties: endpoint: https://jsonplaceholder.typicode.com diff --git a/src/main/java/io/github/azagniotov/stubby4j/stubs/StubProxyConfig.java b/src/main/java/io/github/azagniotov/stubby4j/stubs/StubProxyConfig.java index 3f257f72f..2a472925f 100644 --- a/src/main/java/io/github/azagniotov/stubby4j/stubs/StubProxyConfig.java +++ b/src/main/java/io/github/azagniotov/stubby4j/stubs/StubProxyConfig.java @@ -50,6 +50,10 @@ public StubProxyStrategy getStrategy() { return strategy; } + public boolean isAdditiveStrategy() { + return strategy == StubProxyStrategy.ADDITIVE; + } + public final Map getHeaders() { return new HashMap<>(headers); } @@ -102,12 +106,7 @@ public static final class Builder extends AbstractBuilder { public Builder() { super(); - this.description = null; - this.uuid = DEFAULT_UUID; - this.strategy = null; - this.headers = new LinkedHashMap<>(); - this.properties = new LinkedHashMap<>(); - this.proxyConfigAsYAML = null; + reset(); } public Builder withDescription(final String description) { @@ -170,15 +169,20 @@ public StubProxyConfig build() { properties, proxyConfigAsYAML); + reset(); + + this.fieldNameAndValues.clear(); + + return stubProxyConfig; + } + + private void reset() { this.description = null; this.uuid = DEFAULT_UUID; - this.strategy = null; + this.strategy = StubProxyStrategy.AS_IS; this.headers = new LinkedHashMap<>(); this.properties = new LinkedHashMap<>(); this.proxyConfigAsYAML = null; - this.fieldNameAndValues.clear(); - - return stubProxyConfig; } } } diff --git a/src/main/java/io/github/azagniotov/stubby4j/stubs/StubProxyStrategy.java b/src/main/java/io/github/azagniotov/stubby4j/stubs/StubProxyStrategy.java index 199bc2393..3084a17df 100644 --- a/src/main/java/io/github/azagniotov/stubby4j/stubs/StubProxyStrategy.java +++ b/src/main/java/io/github/azagniotov/stubby4j/stubs/StubProxyStrategy.java @@ -8,8 +8,21 @@ import static io.github.azagniotov.stubby4j.utils.StringUtils.toLower; public enum StubProxyStrategy { + /** + * No changes/modifications will be applied to the request before proxying it. + *

+ * See: https://github.com/azagniotov/stubby4j/blob/master/docs/REQUEST_PROXYING.md#supported-yaml-properties + */ AS_IS("as-is"), - CUSTOM("custom"); + + /** + * Additive changes will be applied to the request before proxying it. The additive changes + * currently supported are setting of additional HTTP headers using the `headers` property on the + * `proxy-config` object. + *

+ * See: https://github.com/azagniotov/stubby4j/blob/master/docs/REQUEST_PROXYING.md#supported-yaml-properties + */ + ADDITIVE("additive"); private static final Map PROPERTY_NAME_TO_ENUM_MEMBER; diff --git a/src/main/java/io/github/azagniotov/stubby4j/stubs/StubRepository.java b/src/main/java/io/github/azagniotov/stubby4j/stubs/StubRepository.java index f642dfa60..4496fc2d2 100644 --- a/src/main/java/io/github/azagniotov/stubby4j/stubs/StubRepository.java +++ b/src/main/java/io/github/azagniotov/stubby4j/stubs/StubRepository.java @@ -225,6 +225,9 @@ private StubResponse proxyRequest(final StubHttpLifecycle incomingHttpLifecycle) try { incomingRequest.getHeaders().put(HEADER_X_STUBBY_PROXY_REQUEST, proxyRoundTripUuid); + + handleIfAdditiveProxyStrategy(incomingRequest, proxyConfig); + final StubbyResponse stubbyResponse = stubbyHttpTransport.httpRequestFromStub(incomingRequest, proxyEndpoint); for (Map.Entry> entry : stubbyResponse.headers().entrySet()) { final String headerName = ObjectUtils.isNull(entry.getKey()) ? "null" : entry.getKey(); @@ -253,6 +256,16 @@ private StubResponse proxyRequest(final StubHttpLifecycle incomingHttpLifecycle) } } + private void handleIfAdditiveProxyStrategy(final StubRequest incomingRequest, final StubProxyConfig proxyConfig) { + if (proxyConfig.isAdditiveStrategy()) { + if (proxyConfig.hasHeaders()) { + for (final Map.Entry headerEntry : proxyConfig.getHeaders().entrySet()) { + incomingRequest.getHeaders().put(headerEntry.getKey(), headerEntry.getValue()); + } + } + } + } + private void recordResponse(StubHttpLifecycle incomingRequest, StubHttpLifecycle matchedStub, StubResponse matchedStubResponse) { final String recordingSource = String.format("%s%s", matchedStubResponse.getBody(), incomingRequest.getUrl()); try { diff --git a/src/test/java/io/github/azagniotov/stubby4j/stubs/StubProxyConfigBuilderTest.java b/src/test/java/io/github/azagniotov/stubby4j/stubs/StubProxyConfigBuilderTest.java index fd522fa01..e585faec7 100644 --- a/src/test/java/io/github/azagniotov/stubby4j/stubs/StubProxyConfigBuilderTest.java +++ b/src/test/java/io/github/azagniotov/stubby4j/stubs/StubProxyConfigBuilderTest.java @@ -28,6 +28,22 @@ public void cleanup() throws Exception { RegexParser.REGEX_PATTERN_CACHE.clear(); } + @Test + public void stubbedProxyConfigDefaultStrategyNotAdditive() throws Exception { + + final StubProxyConfig stubProxyConfig = builder.build(); + assertThat(stubProxyConfig.isAdditiveStrategy()).isFalse(); + assertThat(stubProxyConfig.getStrategy()).isEqualTo(StubProxyStrategy.AS_IS); + } + + @Test + public void stubbedProxyConfigStrategyAdditive() throws Exception { + + final StubProxyConfig stubProxyConfig = builder.withStrategy(StubProxyStrategy.ADDITIVE.toString()).build(); + assertThat(stubProxyConfig.isAdditiveStrategy()).isTrue(); + assertThat(stubProxyConfig.getStrategy()).isEqualTo(StubProxyStrategy.ADDITIVE); + } + @Test public void stubbedProxyConfigHasNoHeaders() throws Exception { @@ -173,7 +189,7 @@ public void stubbedProxyConfigNotEqualsAssertingConfig() throws Exception { final StubProxyConfig assertingStubProxyConfig = builder .withUuid("unique") - .withStrategy("custom") + .withStrategy("additive") .withPropertyEndpoint("http://google.com") .build(); diff --git a/src/test/java/io/github/azagniotov/stubby4j/stubs/StubRepositoryTest.java b/src/test/java/io/github/azagniotov/stubby4j/stubs/StubRepositoryTest.java index b5b34e03b..871831988 100644 --- a/src/test/java/io/github/azagniotov/stubby4j/stubs/StubRepositoryTest.java +++ b/src/test/java/io/github/azagniotov/stubby4j/stubs/StubRepositoryTest.java @@ -1137,6 +1137,97 @@ public void shouldApplyDefaultProxyConfigToHttpTransport_WhenResponseIsProxiable assertThat(proxiedResponse.getHeaders().get(HEADER_X_STUBBY_PROXY_RESPONSE)).isEqualTo(proxyRequestUuid); } + @Test + public void shouldApplyProxyConfigAdditiveStrategyHeadersToHttpTransport_WhenResponseIsProxiable() throws Exception { + + final StubHttpLifecycle httpLifecycle = new StubHttpLifecycle.Builder() + .withUUID("uuid") + .withRequest(new StubRequest.Builder().withUrl("/some/uri/path/1").withMethod("GET").build()) + .withResponse(new StubResponse.Builder().build()) + .build(); + + final StubProxyConfig stubProxyConfig = new StubProxyConfig.Builder() + .withStrategy("additive") + .withHeader("x-custom-header", "something/unique") + .withHeader("x-custom-header-2", "another/thing") + .withPropertyEndpoint("https://jsonplaceholder.typicode.com") + .build(); + + final YamlParseResultSet yamlParseResultSet = new YamlParseResultSet(new LinkedList() {{ + add(httpLifecycle); + }}, new HashMap() {{ + put(httpLifecycle.getUUID(), httpLifecycle); + }}, new HashMap() {{ + put(stubProxyConfig.getUUID(), stubProxyConfig); + }}); + + spyStubRepository.resetStubsCache(yamlParseResultSet); + + when(mockStubbyHttpTransport.httpRequestFromStub(any(StubRequest.class), anyString())).thenReturn(new StubbyResponse(201, "OK!", new HashMap<>())); + + final StubRequest incomingRequest = + requestBuilder + .withUrl("/post/1") + .withMethodGet() + .withHeader("content-type", Common.HEADER_APPLICATION_JSON) + .build(); + + doReturn(incomingRequest).when(spyStubRepository).toStubRequest(any(HttpServletRequest.class)); + spyStubRepository.search(mockHttpServletRequest); + + verify(mockStubbyHttpTransport, times(1)).httpRequestFromStub(stubRequestCaptor.capture(), anyString()); + + // The 'content-type', HEADER_X_STUBBY_PROXY_REQUEST and two additive headers + assertThat(stubRequestCaptor.getValue().getHeaders().size()).isEqualTo(4); + assertThat(stubRequestCaptor.getValue().getHeaders().containsKey(HEADER_X_STUBBY_PROXY_REQUEST)).isTrue(); + assertThat(stubRequestCaptor.getValue().getHeaders().containsKey("x-custom-header")).isTrue(); + assertThat(stubRequestCaptor.getValue().getHeaders().containsKey("x-custom-header-2")).isTrue(); + } + + @Test + public void shouldNotApplyProxyConfigAdditiveStrategyEmptyHeadersToHttpTransport_WhenResponseIsProxiable() throws Exception { + + final StubHttpLifecycle httpLifecycle = new StubHttpLifecycle.Builder() + .withUUID("uuid") + .withRequest(new StubRequest.Builder().withUrl("/some/uri/path/1").withMethod("GET").build()) + .withResponse(new StubResponse.Builder().build()) + .build(); + + final StubProxyConfig stubProxyConfig = new StubProxyConfig.Builder() + .withStrategy("additive") + .withPropertyEndpoint("https://jsonplaceholder.typicode.com") + .build(); + + final YamlParseResultSet yamlParseResultSet = new YamlParseResultSet(new LinkedList() {{ + add(httpLifecycle); + }}, new HashMap() {{ + put(httpLifecycle.getUUID(), httpLifecycle); + }}, new HashMap() {{ + put(stubProxyConfig.getUUID(), stubProxyConfig); + }}); + + spyStubRepository.resetStubsCache(yamlParseResultSet); + + when(mockStubbyHttpTransport.httpRequestFromStub(any(StubRequest.class), anyString())).thenReturn(new StubbyResponse(201, "OK!", new HashMap<>())); + + final StubRequest incomingRequest = + requestBuilder + .withUrl("/post/1") + .withMethodGet() + .withHeader("content-type", Common.HEADER_APPLICATION_JSON) + .build(); + + doReturn(incomingRequest).when(spyStubRepository).toStubRequest(any(HttpServletRequest.class)); + spyStubRepository.search(mockHttpServletRequest); + + verify(mockStubbyHttpTransport, times(1)).httpRequestFromStub(stubRequestCaptor.capture(), anyString()); + + // The 'content-type' header and the HEADER_X_STUBBY_PROXY_REQUEST only + assertThat(stubRequestCaptor.getValue().getHeaders().size()).isEqualTo(2); + assertThat(stubRequestCaptor.getValue().getHeaders().containsKey(HEADER_X_STUBBY_PROXY_REQUEST)).isTrue(); + assertThat(stubRequestCaptor.getValue().getHeaders().containsKey("content-type")).isTrue(); + } + @Test public void shouldApplyProxyConfigByProxyConfigUuidHeaderToHttpTransport_WhenResponseIsProxiable() throws Exception {