diff --git a/test/server/BUILD b/test/server/BUILD index 0ea078851..71ae178ab 100644 --- a/test/server/BUILD +++ b/test/server/BUILD @@ -1,6 +1,7 @@ load( "@envoy//bazel:envoy_build_system.bzl", "envoy_cc_test", + "envoy_cc_test_library", "envoy_package", ) @@ -8,6 +9,17 @@ licenses(["notice"]) # Apache 2 envoy_package() +envoy_cc_test_library( + name = "http_filter_integration_test_base_lib", + srcs = ["http_filter_integration_test_base.cc"], + hdrs = ["http_filter_integration_test_base.h"], + repository = "@envoy", + deps = [ + "//source/server:well_known_headers_lib", + "@envoy//test/integration:http_integration_lib", + ], +) + envoy_cc_test( name = "http_test_server_filter_integration_test", srcs = ["http_test_server_filter_integration_test.cc"], @@ -25,9 +37,9 @@ envoy_cc_test( srcs = ["http_dynamic_delay_filter_integration_test.cc"], repository = "@envoy", deps = [ + ":http_filter_integration_test_base_lib", "//source/server:http_dynamic_delay_filter_config", "@envoy//source/common/api:api_lib_with_external_headers", - "@envoy//test/integration:http_integration_lib", ], ) @@ -36,10 +48,10 @@ envoy_cc_test( srcs = ["http_time_tracking_filter_integration_test.cc"], repository = "@envoy", deps = [ + ":http_filter_integration_test_base_lib", "//source/server:http_time_tracking_filter_config", "@envoy//include/envoy/upstream:cluster_manager_interface_with_external_headers", "@envoy//source/common/api:api_lib_with_external_headers", - "@envoy//test/integration:http_integration_lib", "@envoy//test/test_common:simulated_time_system_lib", ], ) diff --git a/test/server/http_filter_integration_test_base.cc b/test/server/http_filter_integration_test_base.cc new file mode 100644 index 000000000..d204133ad --- /dev/null +++ b/test/server/http_filter_integration_test_base.cc @@ -0,0 +1,63 @@ +#include "test/server/http_filter_integration_test_base.h" + +#include "server/well_known_headers.h" + +#include "gtest/gtest.h" + +namespace Nighthawk { + +HttpFilterIntegrationTestBase::HttpFilterIntegrationTestBase( + Envoy::Network::Address::IpVersion ip_version) + : HttpIntegrationTest(Envoy::Http::CodecClient::Type::HTTP1, ip_version), + request_headers_({{":method", "GET"}, {":path", "/"}, {":authority", "host"}}) {} + +void HttpFilterIntegrationTestBase::initializeFilterConfiguration(absl::string_view config) { + config_helper_.addFilter(std::string(config)); + HttpIntegrationTest::initialize(); +} + +void HttpFilterIntegrationTestBase::setRequestLevelConfiguration( + absl::string_view request_level_config) { + setRequestHeader(Server::TestServer::HeaderNames::get().TestServerConfig, request_level_config); +} + +void HttpFilterIntegrationTestBase::switchToPostWithEntityBody() { + setRequestHeader(Envoy::Http::Headers::get().Method, + Envoy::Http::Headers::get().MethodValues.Post); +} + +void HttpFilterIntegrationTestBase::setRequestHeader( + const Envoy::Http::LowerCaseString& header_name, absl::string_view header_value) { + request_headers_.setCopy(header_name, header_value); +} + +Envoy::IntegrationStreamDecoderPtr +HttpFilterIntegrationTestBase::getResponse(ResponseOrigin expected_origin) { + cleanupUpstreamAndDownstream(); + codec_client_ = makeHttpConnection(lookupPort("http")); + Envoy::IntegrationStreamDecoderPtr response; + const bool is_post = request_headers_.Method()->value().getStringView() == + Envoy::Http::Headers::get().MethodValues.Post; + // Upon observing a POST request method, we inject a content body, as promised in the header file. + // This is useful, because emitting an entity body will hit distinct code in extensions. Hence we + // facilitate that. + const uint64_t request_body_size = is_post ? 1024 : 0; + // An extension can either act as an origin and synthesize a reply, or delegate that + // responsibility to an upstream. This behavior may change from request to request. For example, + // an extension is designed to transform input from an upstream, may start acting as an origin on + // misconfiguration. + if (expected_origin == ResponseOrigin::UPSTREAM) { + response = sendRequestAndWaitForResponse(request_headers_, request_body_size, + default_response_headers_, /*response_body_size*/ 0); + } else { + if (is_post) { + response = codec_client_->makeRequestWithBody(request_headers_, request_body_size); + } else { + response = codec_client_->makeHeaderOnlyRequest(request_headers_); + } + response->waitForEndStream(); + } + return response; +} + +} // namespace Nighthawk diff --git a/test/server/http_filter_integration_test_base.h b/test/server/http_filter_integration_test_base.h new file mode 100644 index 000000000..8027753ec --- /dev/null +++ b/test/server/http_filter_integration_test_base.h @@ -0,0 +1,85 @@ +#pragma once + +#include + +#include "external/envoy/test/integration/http_integration.h" + +namespace Nighthawk { + +/** + * Base class with shared functionality for testing Nighthawk test server http filter extensions. + * The class is stateful, and not safe to use from multiple threads. + */ +class HttpFilterIntegrationTestBase : public Envoy::HttpIntegrationTest { +protected: + /** + * Indicate the expected response origin. A test failure will occur upon mismatch. + */ + enum class ResponseOrigin { + /** + * The upstream will supply the response, and not the extension under test. + */ + UPSTREAM, + /** + * The extension under test will suplly a response, and no upstream will be set up to do that. + */ + EXTENSION + }; + /** + * Construct a new HttpFilterIntegrationTestBase instance. + * + * @param ip_version Specify the ip version that the integration test server will use to listen + * for connections. + */ + HttpFilterIntegrationTestBase(Envoy::Network::Address::IpVersion ip_version); + + /** + * We don't override SetUp(): tests using this fixture must call initializeFilterConfiguration() + * instead. This is to avoid imposing the need to create a fixture per filter configuration. + * + * @param filter_configuration Pass configuration for the filter under test. Will be handed off to + * Envoy::HttpIntegrationTest::config_helper_.addFilter. + */ + void initializeFilterConfiguration(absl::string_view filter_configuration); + + /** + * Make getResponse send request-level configuration. Test server extensions read that + * configuration and merge it with their static configuration to determine a final effective + * configuration. See TestServerConfig in well_known_headers.h for the up to date header name. + * + * @param request_level_config Configuration to be delivered by request-header in future calls to + * getResponse(). For example: "{response_body_size:1024}". + */ + void setRequestLevelConfiguration(absl::string_view request_level_config); + + /** + * Switch getResponse() to use the POST request method with an entity body. + * Doing so will make tests hit a different code paths in extensions. + */ + void switchToPostWithEntityBody(); + + /** + * Set a request header value. Overwrites any existing value. + * + * @param header_name Name of the request header to set. + * @param header_value Value to set for the request header. + */ + void setRequestHeader(const Envoy::Http::LowerCaseString& header_name, + absl::string_view header_value); + + /** + * Fetch a response, according to the options specified by the class methods. By default, + * simulates a GET request with minimal headers. + * @param expected_origin Indicate which component will be expected to reply: the extension or + * a fake upstream. Will cause a test failure upon mismatch. Can be used to verify that an + * extension short circuits and directly responds when expected. + * @return Envoy::IntegrationStreamDecoderPtr Pointer to the integration stream decoder, which can + * be used to inspect the response. + */ + Envoy::IntegrationStreamDecoderPtr getResponse(ResponseOrigin expected_origin); + +private: + Envoy::Http::TestRequestHeaderMapImpl request_headers_; +}; + +} // namespace Nighthawk