From b4e149e1b5beca3c2bdb9b996678277c540bde23 Mon Sep 17 00:00:00 2001 From: Casey Waldren Date: Fri, 15 Nov 2024 10:35:13 -0800 Subject: [PATCH] feat: add streaming mode http proxy test (#258) Adds a single test for streaming with HTTP proxy configuration (client-side). Ideally, the next bit of work will be.. - to support verifying that events and polling also go through the proxy. - extending to server SDKs (should be just adding to the `server_side_stream_all.go`, but requires an SDK to test it) The purpose of this test is to check that an SDK can make a request via a configured HTTP Proxy. In this mode, the SDK sends requests to the proxy, and the request body contains the intended URL that the proxy should connect to on behalf of the SDK. Future work might be to support other proxy modes, like HTTPS proxy (where the connection between SDK and proxy is encrypted) or SOCKS4/5. --- docs/service_spec.md | 10 +++++++ sdktests/client_side_stream_all.go | 2 ++ sdktests/common_tests_base.go | 9 ++++++ sdktests/common_tests_stream_request.go | 38 +++++++++++++++++++++++++ servicedef/sdk_config.go | 5 ++++ servicedef/service_params.go | 4 +++ 6 files changed, 68 insertions(+) diff --git a/docs/service_spec.md b/docs/service_spec.md index d09aee35..259179c3 100644 --- a/docs/service_spec.md +++ b/docs/service_spec.md @@ -230,6 +230,14 @@ This means the SDK supports setting the wrapper name and version and includes th When this capability is set a `wrapper` configuration will be included with a subset of tests. + +### Capability `"http-proxy"` + +This indicates the SDK is capable of configuring an HTTP proxy for its network requests. + +All requests should be sent to the proxy. This is generally implemented in an SDK via standard networking +library capabilities, such as setting an environment variable (like `http_proxy`) or a configuration option. + ### Stop test service: `DELETE /` The test harness sends this request at the end of a test run if you have specified `--stop-service-at-end` on the [command line](./running.md). The test service should simply quit. This is a convenience so CI scripts can simply start the test service in the background and assume it will be stopped for them. @@ -291,6 +299,8 @@ A `POST` request indicates that the test harness wants to start an instance of t * `wrapper` (object, optional): If specified contains wrapper configuration. * `name`: The name of the wrapper. * `version`: The version of the wrapper. + * `proxy` (object, optional): If specified contains proxy configuration. + * `httpProxy` (string, optional): An HTTP proxy, of the form `http://host:port`. The response to a valid request is any HTTP `2xx` status, with a `Location` header whose value is the URL of the test service resource representing this SDK client instance (that is, the one that would be used for "Close client" or "Send command" as described below). diff --git a/sdktests/client_side_stream_all.go b/sdktests/client_side_stream_all.go index 26798bbb..04d9fc59 100644 --- a/sdktests/client_side_stream_all.go +++ b/sdktests/client_side_stream_all.go @@ -22,6 +22,8 @@ func doClientSideStreamRequestTest(t *ldtest.T) { streamTests := NewCommonStreamingTests(t, "doClientSideStreamRequestTest", WithCredential(envIDOrMobileKey)) + streamTests.RequestViaHTTPProxy(t) + streamTests.RequestMethodAndHeaders(t, envIDOrMobileKey) requestPathMatcher := func(method flagRequestMethod) m.Matcher { diff --git a/sdktests/common_tests_base.go b/sdktests/common_tests_base.go index ee791e5e..b1482fe5 100644 --- a/sdktests/common_tests_base.go +++ b/sdktests/common_tests_base.go @@ -233,3 +233,12 @@ func (c commonTestsBase) sendArbitraryEvent(t *ldtest.T, client *SDKClient) { } client.SendCustomEvent(t, params) } + +func (c commonTestsBase) withHTTPProxy(url string) SDKConfigurer { + return helpers.ConfigOptionFunc[servicedef.SDKConfigParams](func(configOut *servicedef.SDKConfigParams) error { + configOut.Proxy = o.Some(servicedef.SDKConfigProxyParams{ + HTTPProxy: o.Some(url), + }) + return nil + }) +} diff --git a/sdktests/common_tests_stream_request.go b/sdktests/common_tests_stream_request.go index 7fcd5f9a..13b64894 100644 --- a/sdktests/common_tests_stream_request.go +++ b/sdktests/common_tests_stream_request.go @@ -2,6 +2,7 @@ package sdktests import ( "fmt" + "net/url" "strings" "time" @@ -182,3 +183,40 @@ func (c CommonStreamingTests) RequestContextProperties(t *ldtest.T, getPath stri } }) } + +func (c CommonStreamingTests) RequestViaHTTPProxy(t *ldtest.T) { + t.RequireCapability(servicedef.CapabilityHTTPProxy) + t.Run("http proxy", func(t *ldtest.T) { + dataSource, configurers := c.setupDataSources(t, nil) + + // The idea here is that we'll configure the SDK's service endpoints with an arbitrary host, but with the + // correct path that the test harness expects (like /endpoints/1). Then, we'll inject the actual test harness's + // endpoint via the HTTP Proxy configuration. + // + // The SDK should therefore: + // 1. Open a socket to the test harness's host and port + // 2. Send an HTTP request that has the arbitrary host and the correct path + // + // If the SDK didn't support proxying, then it would attempt to connect to the arbitrary host and + // the harness should fail the connection assertion. + streamURI := strings.Replace(dataSource.Endpoint().BaseURL(), "localhost", "not.valid.local", 1) + + u, err := url.Parse(dataSource.Endpoint().BaseURL()) + if err != nil { + t.Errorf("unexpected error parsing URL: %s", err) + t.FailNow() + } + u.Path = "" + + _ = NewSDKClient(t, c.baseSDKConfigurationPlus( + append(configurers, + WithStreamingConfig(servicedef.SDKConfigStreamingParams{ + BaseURI: streamURI, + }), + c.withHTTPProxy(u.String()), + )...)...) + + _, err = dataSource.Endpoint().AwaitConnection(time.Second) + assert.NoError(t, err) + }) +} diff --git a/servicedef/sdk_config.go b/servicedef/sdk_config.go index 0dc2f2b4..20bd0a38 100644 --- a/servicedef/sdk_config.go +++ b/servicedef/sdk_config.go @@ -17,6 +17,7 @@ type SDKConfigParams struct { InitCanFail bool `json:"initCanFail,omitempty"` ServiceEndpoints o.Maybe[SDKConfigServiceEndpointsParams] `json:"serviceEndpoints,omitempty"` TLS o.Maybe[SDKConfigTLSParams] `json:"tls,omitempty"` + Proxy o.Maybe[SDKConfigProxyParams] `json:"proxy,omitempty"` Streaming o.Maybe[SDKConfigStreamingParams] `json:"streaming,omitempty"` Polling o.Maybe[SDKConfigPollingParams] `json:"polling,omitempty"` Events o.Maybe[SDKConfigEventParams] `json:"events,omitempty"` @@ -33,6 +34,10 @@ type SDKConfigTLSParams struct { CustomCAFile string `json:"customCAFile,omitempty"` } +type SDKConfigProxyParams struct { + HTTPProxy o.Maybe[string] `json:"httpProxy,omitempty"` +} + type SDKConfigServiceEndpointsParams struct { Streaming string `json:"streaming,omitempty"` Polling string `json:"polling,omitempty"` diff --git a/servicedef/service_params.go b/servicedef/service_params.go index e04825eb..ae365f45 100644 --- a/servicedef/service_params.go +++ b/servicedef/service_params.go @@ -64,6 +64,10 @@ const ( // CapabilityWrapper indicates that the SDK supports setting wrapper name and version and including them in request // headers. CapabilityWrapper = "wrapper" + + // CapabilityHTTPProxy indicates that the SDK supports setting an HTTP proxy, through which the SDK will + // make all requests. + CapabilityHTTPProxy = "http-proxy" ) type StatusRep struct {