Skip to content

Commit

Permalink
Added ability to look for proxy-config by UUID header value
Browse files Browse the repository at this point in the history
  • Loading branch information
azagniotov committed Mar 23, 2021
1 parent 523db3f commit 9ae87dd
Show file tree
Hide file tree
Showing 7 changed files with 367 additions and 30 deletions.
39 changes: 28 additions & 11 deletions docs/REQUEST_PROXYING.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,13 @@

As of `v7.3.0` of `stubby4j`, a new feature is available that enables you to configure a proxy/intercept where requests are proxied to another service (i.e.: a real, live service), when such requests did not match any of the configured stubs.

On this page you will learn how to add a proxy configuration described in YAML to an existing stub `request`/`response` YAML configuration that you created as part of [Endpoint configuration HOWTO](../README.md#endpoint-configuration-howto). Keep on reading to understand how to configure a proxy.
On this page you will learn how to add a proxy configuration described in YAML to an existing stub `request`/`response` YAML configuration that you created as part of [Endpoint configuration HOWTO](../README.md#endpoint-configuration-howto). Currently, it is possible to add multiple proxy configurations for requests that don't match any of the `stubby4j`'s stubs.

Currently, it is possible to configure only `one` proxy configuration that serves as a catch-all for all requests that don't match any of the `stubby4j`'s stubs. In the future enhancements to the request proxying functionality, multiple proxy configurations will be supported.
In order to enable request proxying behavior, you need to add at least one proxy configuration to your YAML config, the default proxy config (denoted with `default proxy config` hereafter). The `default proxy config` serves as a catch-all for requests that don't match any of the `stubby4j`'s stubs.

In addition to the `default proxy config`, you can add additional proxy configurations. You can then control at runtime using an HTTP header `x-stubby4j-proxy-config-uuid` set on the HTTP request to `stubby4j` which proxy configuration should be applied if your request was not matched to any of the stubs.

Keep on reading to understand how to add proxy configurations to your `stubby4j` YAML config.

### Table of contents

Expand All @@ -21,18 +25,24 @@ Currently, it is possible to configure only `one` proxy configuration that serve

This section explains the usage, intent and behavior of each YAML property on the proxy configuration object. In `stubby4j` YAML config, the proxy configuration are denoted using the `proxy-config` key.

The following is a fully-populated `proxy-config` configuration:
The following is a fully-populated multiple `proxy-config` configuration:

```yaml
- proxy-config:
uuid: default
description: this is a default proxy config that serves as a catch-all for non-matched requests
strategy: as-is
properties:
endpoint: https://jsonplaceholder.typicode.com

- proxy-config:
uuid: some-very-unique-key
description: this is a non-default proxy config which hits Google
strategy: as-is
properties:
endpoint: https://google.com
```
YAML proxy configuartion can be in the same YAML config as the stubs, i,e.: it is totaly OK to mix `request`/`response` ([Endpoint configuration HOWTO](../README.md#endpoint-configuration-howto)) and `proxy-config` in the same file. For example, the following is a totally valid YAML configuration:
YAML proxy configuartion can be in the same YAML config as the stubs, i,e.: it is totally OK to mix configs for `request`/`response` ([Endpoint configuration HOWTO](../README.md#endpoint-configuration-howto)) & `proxy-config` in the same file. For example, the following is a totally valid YAML configuration:
```yaml
- proxy-config:
uuid: default
Expand Down Expand Up @@ -72,22 +82,25 @@ includes:

### Supported YAML properties

#### uuid (`optional`)
#### uuid

##### Property is `optional` when

Currently, it is possible to configure only one proxy configuration that serves as a catch-all for all requests that don't match any of the stubby4j's stubs. This catch-all proxy is basically a default proxy config (denoted with `default proxy config` hereafter).
Defining only a single `proxy-config` object in your YAML configuration. That single `proxy-config` object serves as a catch-all for all requests that don't match any of the stubby4j's stubs, it is the `default proxy config`.

When creating a `proxy-config` definition without an explicit `uuid` property, an `uuid` property will be configured internally with a value `default` (i.e.: `uuid: default`. If you do choose to explicitly define the `uuid` property, do not set it to anything other than value `default`.
When creating a `proxy-config` definition without an explicit `uuid` property, an `uuid` property will be configured internally with a value `default` (i.e.: `uuid: default`. You can however, explicitly define the `uuid` property even for the only `proxy-config` defined, but do not set it to anything other than value `default`.

Please note, at this stage, explicitly setting `uuid` property to anything but value `default` is reserved for future enhancements to the request proxying functionality.
##### Property is `required` when

Defining multiple `proxy-config` objects in your YAML configuration. The `uuid` property must have unique values across all defined `proxy-config` objects. Please keep in mind that you always have defined a `default proxy config` (i.e.: with `uuid: default` or without `uuid` at all).

#### description (`optional`)

This can be anything describing your proxy configuration.

#### strategy (`required`)

Describes how the request to-be-proxied should be proxied. Currently only `as-is` startegy 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 `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.

#### properties (`required`)

Expand All @@ -103,7 +116,11 @@ Describes the target service endpoint where the request will be proxied to. This

## Application of proxy config at runtime

Request proxying happens when there is a `default proxy config` defined in the YAML config and the incoming HTTP request did not match any of the stubby4j's stubs.
Request proxying happens when there is at least one `proxy config` object defined in the YAML config and the incoming HTTP request did not match any of the stubby4j's stubs.

First, `stubby4j` will check if the HTTP header `x-stubby4j-proxy-config-uuid` has been set on the incoming request. If header is set, then the header value will be used to apply the respective proxy configuration to the request. Please note: the header value must be one of the configured unique `uuid` values in your `proxy-config` objects.

If the aforementioned header is not set or it is set but there is no matching `proxy-config` for the provided `uuid` value, then the `default proxy config` will be used as a fallback. Please note: you `do not` need to pass in the `x-stubby4j-proxy-config-uuid` header if you have only `default proxy-config` in your YAML configuration.

## Proxied request & response tracking

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
import java.net.URL;

import static com.google.common.truth.Truth.assertThat;
import static io.github.azagniotov.stubby4j.common.Common.HEADER_X_STUBBY_PROXY_CONFIG;
import static io.github.azagniotov.stubby4j.common.Common.HEADER_X_STUBBY_PROXY_RESPONSE;
import static io.github.azagniotov.stubby4j.utils.FileUtils.BR;

Expand Down Expand Up @@ -92,7 +93,7 @@ public void should_ReturnCompleteYAMLConfig_WhenSuccessfulGetMade_ToAdminPortalR

@Test
@PotentiallyFlaky("This test sending the request over the wire to https://jsonplaceholder.typicode.com")
public void shouldReturnProxiedRequestResponse_WhenStubsWereNotMatched() throws Exception {
public void shouldReturnProxiedResponseUsingDefaultProxyConfig_WhenStubsWereNotMatched() throws Exception {

// https://jsonplaceholder.typicode.com/todos/1
final String targetUriPath = "/todos/1";
Expand Down Expand Up @@ -123,6 +124,41 @@ public void shouldReturnProxiedRequestResponse_WhenStubsWereNotMatched() throws
"}");
}

@Test
@PotentiallyFlaky("This test sending the request over the wire to https://jsonplaceholder.typicode.com")
public void shouldReturnProxiedResponseUsingSpecificProxyConfig_WhenStubsWereNotMatched() throws Exception {

// https://jsonplaceholder.typicode.com/todos/1
final String targetUriPath = "/todos/1";

// Stub with URL '/todos/1' does not exist, so the request will be proxied
final String requestUrl = String.format("%s%s", STUBS_URL, targetUriPath);
final HttpRequest request = HttpUtils.constructHttpRequest(HttpMethods.GET, requestUrl);

final HttpHeaders httpHeaders = new HttpHeaders();
// I had to set this header to avoid "Not in GZIP format java.util.zip.ZipException: Not in GZIP format" error:
// 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
httpHeaders.set(HEADER_X_STUBBY_PROXY_CONFIG, "some-unique-name");
request.setHeaders(httpHeaders);

final HttpResponse response = request.execute();
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK_200);

assertThat(response.getHeaders().containsKey(HEADER_X_STUBBY_PROXY_RESPONSE)).isTrue();

final String responseContent = response.parseAsString().trim();
assertThat(responseContent).isEqualTo(
"{" + BR +
" \"userId\": 1," + BR +
" \"id\": 1," + BR +
" \"title\": \"delectus aut autem\"," + BR +
" \"completed\": false" + BR +
"}");
}

@Test
public void should_UpdateStubbedProxyConfig_WithJsonRequest_ByValidUuid() throws Exception {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
import java.util.concurrent.CompletableFuture;

import static com.google.common.truth.Truth.assertThat;
import static io.github.azagniotov.stubby4j.common.Common.HEADER_X_STUBBY_PROXY_CONFIG;
import static io.github.azagniotov.stubby4j.stubs.StubbableAuthorizationType.BASIC;
import static io.github.azagniotov.stubby4j.stubs.StubbableAuthorizationType.BEARER;
import static io.github.azagniotov.stubby4j.stubs.StubbableAuthorizationType.CUSTOM;
Expand Down Expand Up @@ -1123,7 +1124,7 @@ public void shouldThrowWhenDefaultProxyConfigMissing() throws Exception {

@Test
@PotentiallyFlaky("This test sending the request over the wire to https://jsonplaceholder.typicode.com")
public void shouldReturnProxiedRequestResponse_WhenStubsWereNotMatched() throws Exception {
public void shouldReturnProxiedResponseUsingDefaultProxyConfig_WhenStubsWereNotMatched() throws Exception {

// https://jsonplaceholder.typicode.com/todos/1
final String targetUriPath = "/todos/1";
Expand All @@ -1140,8 +1141,7 @@ public void shouldReturnProxiedRequestResponse_WhenStubsWereNotMatched() throws
.newStubbedProxyConfig()
.withProxyStrategyAsIs()
.withPropertyEndpoint("https://jsonplaceholder.typicode.com")
.toString()
.trim();
.build();

loadYamlToDataStore(stubsYaml + BR + BR + proxyConfigYaml);

Expand All @@ -1164,6 +1164,111 @@ public void shouldReturnProxiedRequestResponse_WhenStubsWereNotMatched() throws
"}");
}

@Test
@PotentiallyFlaky("This test sending the request over the wire to https://jsonplaceholder.typicode.com")
public void shouldReturnProxiedResponseUsingSpecificProxyConfig_WhenStubsWereNotMatched() throws Exception {

// https://jsonplaceholder.typicode.com/todos/1
final String targetUriPath = "/todos/1";

final String stubsYaml = YAML_BUILDER.newStubbedRequest()
.withMethodGet()
.withUrl("/a/totally/different/endpoint/stubbed")
.newStubbedResponse()
.withStatus("200")
.withLiteralBody("This is a response for todo 1")
.build();

final String headerProxyConfigUuid = "very-unique-proxy-config";
final String specificProxyConfigYaml = YAML_BUILDER
.newStubbedProxyConfig()
.withUuid(headerProxyConfigUuid)
.withProxyStrategyAsIs()
.withPropertyEndpoint("https://jsonplaceholder.typicode.com")
.build();

final String defaultProxyConfigYaml = YAML_BUILDER
.newStubbedProxyConfig()
.withProxyStrategyAsIs()
.withPropertyEndpoint("https://google.com")
.build();

loadYamlToDataStore(stubsYaml + BR + BR + specificProxyConfigYaml+ BR + BR + defaultProxyConfigYaml);

// Setting HEADER_X_STUBBY_PROXY_CONFIG with existing value in proxyConfigs map will select
// a proxy config by the value of the header at runtime, even if the default proxy config is defined
final StubRequest assertingRequest =
requestBuilder
.withUrl(targetUriPath)
.withHeader(HEADER_X_STUBBY_PROXY_CONFIG, headerProxyConfigUuid)
.withMethodGet().build();

final StubResponse foundStubResponse = setUpStubSearchBehavior(assertingRequest);

assertThat(Code.OK).isEqualTo(foundStubResponse.getHttpStatusCode());
assertThat(foundStubResponse.getHeaders().isEmpty()).isFalse();

assertThat(foundStubResponse.getBody()).isEqualTo(
"{" + BR +
" \"userId\": 1," + BR +
" \"id\": 1," + BR +
" \"title\": \"delectus aut autem\"," + BR +
" \"completed\": false" + BR +
"}");
}

@Test
@PotentiallyFlaky("This test sending the request over the wire to https://jsonplaceholder.typicode.com")
public void shouldReturnProxiedResponseFallingBackOnDefaultProxyConfig_WhenStubsWereNotMatched() throws Exception {

// https://jsonplaceholder.typicode.com/todos/1
final String targetUriPath = "/todos/1";

final String stubsYaml = YAML_BUILDER.newStubbedRequest()
.withMethodGet()
.withUrl("/a/totally/different/endpoint/stubbed")
.newStubbedResponse()
.withStatus("200")
.withLiteralBody("This is a response for todo 1")
.build();

final String headerProxyConfigUuid = "very-unique-proxy-config";
final String specificProxyConfigYaml = YAML_BUILDER
.newStubbedProxyConfig()
.withUuid(headerProxyConfigUuid)
.withProxyStrategyAsIs()
.withPropertyEndpoint("https://google.com")
.build();

final String defaultProxyConfigYaml = YAML_BUILDER
.newStubbedProxyConfig()
.withProxyStrategyAsIs()
.withPropertyEndpoint("https://jsonplaceholder.typicode.com")
.build();

loadYamlToDataStore(stubsYaml + BR + BR + specificProxyConfigYaml+ BR + BR + defaultProxyConfigYaml);

// Setting HEADER_X_STUBBY_PROXY_CONFIG with WRONG value will select default proxy config at runtime
final StubRequest assertingRequest =
requestBuilder
.withUrl(targetUriPath)
.withHeader(HEADER_X_STUBBY_PROXY_CONFIG, "WRONGHeaderProxyConfigUuid")
.withMethodGet().build();

final StubResponse foundStubResponse = setUpStubSearchBehavior(assertingRequest);

assertThat(Code.OK).isEqualTo(foundStubResponse.getHttpStatusCode());
assertThat(foundStubResponse.getHeaders().isEmpty()).isFalse();

assertThat(foundStubResponse.getBody()).isEqualTo(
"{" + BR +
" \"userId\": 1," + BR +
" \"id\": 1," + BR +
" \"title\": \"delectus aut autem\"," + BR +
" \"completed\": false" + BR +
"}");
}

private void loadYamlToDataStore(final String yaml) throws Exception {
spyStubRepository.resetStubsCache(new YamlParser().parse(".", yaml));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ public final class Common {
public static final String HEADER_APPLICATION_JSON = "application/json";
public static final String HEADER_APPLICATION_XML = "application/xml";
public static final String HEADER_X_STUBBY_RESOURCE_ID = "x-stubby-resource-id";
public static final String HEADER_X_STUBBY_PROXY_CONFIG = "x-stubby4j-proxy-config-uuid";
public static final String HEADER_X_STUBBY_PROXY_REQUEST = "x-stubby4j-proxy-request-uuid";
public static final String HEADER_X_STUBBY_PROXY_RESPONSE = "x-stubby4j-proxy-response-uuid";
public static final String HEADER_X_STUBBY_HTTP_ERROR_REAL_REASON = "x-stubby4j-http-error-real-reason";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
import java.util.concurrent.ExecutionException;
import java.util.concurrent.atomic.AtomicLong;

import static io.github.azagniotov.stubby4j.common.Common.HEADER_X_STUBBY_PROXY_CONFIG;
import static io.github.azagniotov.stubby4j.common.Common.HEADER_X_STUBBY_PROXY_REQUEST;
import static io.github.azagniotov.stubby4j.common.Common.HEADER_X_STUBBY_PROXY_RESPONSE;
import static io.github.azagniotov.stubby4j.stubs.StubResponse.notFoundResponse;
Expand Down Expand Up @@ -203,8 +204,20 @@ private StubResponse proxyRequest(final StubHttpLifecycle incomingHttpLifecycle)

// The catch-all will always be there if we have proxy configs, otherwise the YAML loading throws
final StubProxyConfig catchAllProxyConfig = proxyConfigs.get(StubProxyConfig.Builder.DEFAULT_UUID);

final StubRequest incomingRequest = incomingHttpLifecycle.getRequest();
final String proxyEndpoint = String.format("%s%s", catchAllProxyConfig.getPropertyEndpoint(), incomingHttpLifecycle.getUrl());
final String proxyConfigUuidHeader = incomingRequest
.getHeaders()
.getOrDefault(HEADER_X_STUBBY_PROXY_CONFIG, StubProxyConfig.Builder.DEFAULT_UUID);

if (!proxyConfigs.containsKey(proxyConfigUuidHeader)) {
final String warning = String.format("Could not find proxy config by UUID using header value '%s', falling back to 'default'", proxyConfigUuidHeader);
ANSITerminal.warn(warning);
LOGGER.warn(warning);
}

final StubProxyConfig proxyConfig = proxyConfigs.getOrDefault(proxyConfigUuidHeader, catchAllProxyConfig);
final String proxyEndpoint = String.format("%s%s", proxyConfig.getPropertyEndpoint(), incomingHttpLifecycle.getUrl());

final String proxyRoundTripUuid = UUID.randomUUID().toString();
final Map<String, String> proxyResponseFlatHeaders = new HashMap<>();
Expand Down
Loading

0 comments on commit 9ae87dd

Please sign in to comment.