diff --git a/CODEOWNERS b/CODEOWNERS index 23e833dfa840..aed7c1bc37d6 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -329,7 +329,7 @@ extensions/filters/http/oauth2 @derekargueta @mattklein123 # String matching extensions /*/extensions/string_matcher/ @ggreenway @cpakulski # Header mutation -/*/extensions/filters/http/header_mutation @wbpcode @htuch @soulxu +/*/extensions/filters/http/header_mutation @wbpcode @htuch @soulxu @derekargueta # Health checkers /*/extensions/health_checkers/grpc @zuercher @botengyao /*/extensions/health_checkers/http @zuercher @botengyao diff --git a/api/envoy/extensions/filters/http/header_mutation/v3/BUILD b/api/envoy/extensions/filters/http/header_mutation/v3/BUILD index 876a007c83cf..412f3ebb8dde 100644 --- a/api/envoy/extensions/filters/http/header_mutation/v3/BUILD +++ b/api/envoy/extensions/filters/http/header_mutation/v3/BUILD @@ -7,6 +7,7 @@ licenses(["notice"]) # Apache 2 api_proto_package( deps = [ "//envoy/config/common/mutation_rules/v3:pkg", + "//envoy/config/core/v3:pkg", "@com_github_cncf_xds//udpa/annotations:pkg", ], ) diff --git a/api/envoy/extensions/filters/http/header_mutation/v3/header_mutation.proto b/api/envoy/extensions/filters/http/header_mutation/v3/header_mutation.proto index db267d2c591f..131a5bc6af86 100644 --- a/api/envoy/extensions/filters/http/header_mutation/v3/header_mutation.proto +++ b/api/envoy/extensions/filters/http/header_mutation/v3/header_mutation.proto @@ -3,6 +3,7 @@ syntax = "proto3"; package envoy.extensions.filters.http.header_mutation.v3; import "envoy/config/common/mutation_rules/v3/mutation_rules.proto"; +import "envoy/config/core/v3/base.proto"; import "udpa/annotations/status.proto"; @@ -19,6 +20,9 @@ message Mutations { // The request mutations are applied before the request is forwarded to the upstream cluster. repeated config.common.mutation_rules.v3.HeaderMutation request_mutations = 1; + // The query parameter mutations are applied after request mutations before the request is forwarded to the upstream cluster. + repeated config.core.v3.KeyValueMutation query_parameter_mutations = 3; + // The response mutations are applied before the response is sent to the downstream client. repeated config.common.mutation_rules.v3.HeaderMutation response_mutations = 2; } diff --git a/changelogs/current.yaml b/changelogs/current.yaml index 4f616f951122..9d30d40fce5c 100644 --- a/changelogs/current.yaml +++ b/changelogs/current.yaml @@ -93,5 +93,9 @@ new_features: ` to allow overriding TLS certificate selection behavior. An extension can select certificate base on the incoming SNI, in both sync and async mode. +- area: http + change: | + Add query parameter capabilities to :ref:` Header Mutation Filter + ` for adding/removing query parameters on a request. deprecated: diff --git a/docs/root/configuration/http/http_filters/header_mutation_filter.rst b/docs/root/configuration/http/http_filters/header_mutation_filter.rst index 69f1fd47bf90..9671672b6800 100644 --- a/docs/root/configuration/http/http_filters/header_mutation_filter.rst +++ b/docs/root/configuration/http/http_filters/header_mutation_filter.rst @@ -6,7 +6,7 @@ Header Mutation * This filter should be configured with the type URL ``type.googleapis.com/envoy.extensions.filters.http.header_mutation.v3.HeaderMutation``. * :ref:`v3 API reference ` -This is a filter that can be used to add, remove, append, or update HTTP headers. It can be added in any position in the filter chain +This is a filter that can be used to add, remove, append, or update HTTP headers and query parameters. It can be added in any position in the filter chain and used as downstream or upstream HTTP filter. The filter can be configured to apply the header mutations to the request, response, or both. @@ -23,3 +23,52 @@ In addition, this filter can be used as upstream HTTP filter and mutate the requ Please note that as an encoder filter, this filter follows the standard rules of when it will execute in situations such as local replies - response headers will not be unconditionally added in cases where the filter would be bypassed. + + +Example configurations +---------------------- + +The following configuration will transform requests from `/some/path?remove-me=value` into `/some/path?param=new-value`. + +.. code-block:: yaml + + http_filters: + - name: envoy.filters.http.header_mutation + typed_config: + "@type": type.googleapis.com/envoy.extensions.filters.http.header_mutation.v3.HeaderMutation + query_parameters_to_add: + - append_action: APPEND_IF_EXISTS_OR_ADD + query_parameter: + key: param + value: new-value + query_parameters_to_remove: + - remove-me + + - name: envoy.filters.http.router + +The following configuration will add a query parameter only on requests that match `/foobar`. + +.. code-block:: yaml + + route_config: + virtual_hosts: + - name: service + domains: ["*"] + routes: + - match: { path: /foobar } + route: { cluster: service1 } + typed_per_filter_config: + envoy.filters.http.header_mutation: + "@type": type.googleapis.com/envoy.extensions.filters.http.header_mutation.v3.HeaderMutation + query_parameters_to_add: + - append_action: APPEND_IF_EXISTS_OR_ADD + query_parameter: + key: param + value: new-value + - match: { path: / } + route: { cluster: service2 } + + http_filters: + - name: envoy.filters.http.header_mutation + - name: envoy.filters.http.router + diff --git a/source/common/router/config_impl.h b/source/common/router/config_impl.h index 2a6bfc723532..5b91d53ba94b 100644 --- a/source/common/router/config_impl.h +++ b/source/common/router/config_impl.h @@ -643,7 +643,7 @@ class InternalRedirectPolicyImpl : public InternalRedirectPolicy { using DefaultInternalRedirectPolicy = ConstSingleton; /** - * Base implementation for all route entries.q + * Base implementation for all route entries. */ class RouteEntryImplBase : public RouteEntryAndRoute, public Matchable, diff --git a/source/common/router/header_parser.h b/source/common/router/header_parser.h index 6d7ff3fa4842..867730fe99b7 100644 --- a/source/common/router/header_parser.h +++ b/source/common/router/header_parser.h @@ -97,7 +97,7 @@ class HeaderParser : public Http::HeaderEvaluator { /** * Helper methods to evaluate methods without explicitly passing request and response headers. - * The method will try to fetch request headers from steam_info. Response headers will always be + * The method will try to fetch request headers from stream_info. Response headers will always be * empty. */ void evaluateHeaders(Http::HeaderMap& headers, const StreamInfo::StreamInfo& stream_info) const { diff --git a/source/extensions/filters/http/header_mutation/BUILD b/source/extensions/filters/http/header_mutation/BUILD index 48f12a052502..488a30855067 100644 --- a/source/extensions/filters/http/header_mutation/BUILD +++ b/source/extensions/filters/http/header_mutation/BUILD @@ -14,6 +14,7 @@ envoy_cc_library( srcs = ["header_mutation.cc"], hdrs = ["header_mutation.h"], deps = [ + ":query_params_evaluator_lib", "//envoy/server:filter_config_interface", "//source/common/config:utility_lib", "//source/common/http:header_map_lib", @@ -36,3 +37,19 @@ envoy_cc_extension( "@envoy_api//envoy/extensions/filters/http/header_mutation/v3:pkg_cc_proto", ], ) + +envoy_cc_library( + name = "query_params_evaluator_lib", + srcs = ["query_params_evaluator.cc"], + hdrs = ["query_params_evaluator.h"], + deps = [ + "//envoy/formatter:substitution_formatter_interface", + "//envoy/http:header_map_interface", + "//envoy/http:query_params_interface", + "//source/common/formatter:substitution_formatter_lib", + "//source/common/http:utility_lib", + "//source/common/protobuf:utility_lib", + "@envoy_api//envoy/config/core/v3:pkg_cc_proto", + "@envoy_api//envoy/extensions/filters/http/header_mutation/v3:pkg_cc_proto", + ], +) diff --git a/source/extensions/filters/http/header_mutation/header_mutation.cc b/source/extensions/filters/http/header_mutation/header_mutation.cc index e61635a58079..b8bd71e5a75d 100644 --- a/source/extensions/filters/http/header_mutation/header_mutation.cc +++ b/source/extensions/filters/http/header_mutation/header_mutation.cc @@ -12,10 +12,11 @@ namespace Extensions { namespace HttpFilters { namespace HeaderMutation { -void Mutations::mutateRequestHeaders(Http::HeaderMap& headers, +void Mutations::mutateRequestHeaders(Http::RequestHeaderMap& headers, const Formatter::HttpFormatterContext& ctx, const StreamInfo::StreamInfo& stream_info) const { request_mutations_->evaluateHeaders(headers, ctx, stream_info); + query_params_evaluator_->evaluateQueryParams(headers, stream_info); } void Mutations::mutateResponseHeaders(Http::HeaderMap& headers, diff --git a/source/extensions/filters/http/header_mutation/header_mutation.h b/source/extensions/filters/http/header_mutation/header_mutation.h index 6701a4b219d1..dd5b0d2e513e 100644 --- a/source/extensions/filters/http/header_mutation/header_mutation.h +++ b/source/extensions/filters/http/header_mutation/header_mutation.h @@ -10,6 +10,7 @@ #include "source/common/common/logger.h" #include "source/common/http/header_mutation.h" #include "source/extensions/filters/http/common/pass_through_filter.h" +#include "source/extensions/filters/http/header_mutation/query_params_evaluator.h" #include "absl/strings/string_view.h" @@ -32,9 +33,12 @@ class Mutations { HeaderMutations::create(config.request_mutations()), std::unique_ptr)), response_mutations_( THROW_OR_RETURN_VALUE(HeaderMutations::create(config.response_mutations()), - std::unique_ptr)) {} + std::unique_ptr)), + query_params_evaluator_( + std::make_unique(config.query_parameter_mutations())) {} - void mutateRequestHeaders(Http::HeaderMap& headers, const Formatter::HttpFormatterContext& ctx, + void mutateRequestHeaders(Http::RequestHeaderMap& headers, + const Formatter::HttpFormatterContext& ctx, const StreamInfo::StreamInfo& stream_info) const; void mutateResponseHeaders(Http::HeaderMap& headers, const Formatter::HttpFormatterContext& ctx, const StreamInfo::StreamInfo& stream_info) const; @@ -42,6 +46,7 @@ class Mutations { private: const std::unique_ptr request_mutations_; const std::unique_ptr response_mutations_; + QueryParamsEvaluatorPtr query_params_evaluator_; }; class PerRouteHeaderMutation : public Router::RouteSpecificFilterConfig { diff --git a/source/extensions/filters/http/header_mutation/query_params_evaluator.cc b/source/extensions/filters/http/header_mutation/query_params_evaluator.cc new file mode 100644 index 000000000000..21f6184905b5 --- /dev/null +++ b/source/extensions/filters/http/header_mutation/query_params_evaluator.cc @@ -0,0 +1,76 @@ +#include "source/extensions/filters/http/header_mutation/query_params_evaluator.h" + +#include +#include +#include + +#include "envoy/http/query_params.h" +#include "envoy/stream_info/stream_info.h" + +#include "source/common/formatter/substitution_formatter.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace HeaderMutation { + +using Http::Utility::QueryParamsMulti; + +QueryParamsEvaluator::QueryParamsEvaluator( + const Protobuf::RepeatedPtrField& query_param_mutations) + : formatter_(std::make_unique("", true)) { + + for (const auto& query_param : query_param_mutations) { + mutations_.emplace_back(query_param); + } +} + +void QueryParamsEvaluator::evaluateQueryParams(Http::RequestHeaderMap& headers, + const StreamInfo::StreamInfo& stream_info) const { + if (mutations_.empty()) { + return; + } + + absl::string_view path = headers.getPathValue(); + QueryParamsMulti query_params = QueryParamsMulti::parseAndDecodeQueryString(path); + + Formatter::HttpFormatterContext ctx{&headers}; + for (const auto& mutation : mutations_) { + if (!mutation.remove().empty()) { + query_params.remove(mutation.remove()); + } else { + const auto value_option = mutation.append(); + const auto key = value_option.entry().key(); + const auto value = value_option.entry().value(); + const auto formatter = std::make_unique(value, true); + switch (AppendAction(value_option.action())) { + case AppendAction::AppendIfExistsOrAdd: + query_params.add(key, formatter->formatWithContext(ctx, stream_info)); + break; + case AppendAction::AddIfAbsent: + if (!query_params.getFirstValue(key).has_value()) { + query_params.add(key, formatter->formatWithContext(ctx, stream_info)); + } + break; + case AppendAction::OverwriteIfExistsOrAdd: + query_params.overwrite(key, value); + break; + case AppendAction::OverwriteIfExists: + if (query_params.getFirstValue(key).has_value()) { + query_params.overwrite(key, value); + } + break; + default: + PANIC("unreachable"); + } + } + } + + const auto new_path = query_params.replaceQueryString(headers.Path()->value()); + headers.setPath(new_path); +} + +} // namespace HeaderMutation +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/source/extensions/filters/http/header_mutation/query_params_evaluator.h b/source/extensions/filters/http/header_mutation/query_params_evaluator.h new file mode 100644 index 000000000000..0bee9461792f --- /dev/null +++ b/source/extensions/filters/http/header_mutation/query_params_evaluator.h @@ -0,0 +1,61 @@ +#pragma once + +#include +#include +#include + +#include "envoy/config/core/v3/base.pb.h" +#include "envoy/extensions/filters/http/header_mutation/v3/header_mutation.pb.h" +#include "envoy/formatter/substitution_formatter.h" +#include "envoy/http/header_map.h" +#include "envoy/http/query_params.h" +#include "envoy/stream_info/stream_info.h" + +#include "source/common/protobuf/protobuf.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace HeaderMutation { + +using KeyValueAppendProto = envoy::config::core::v3::KeyValueAppend; +using KeyValueAppendActionProto = envoy::config::core::v3::KeyValueAppend_KeyValueAppendAction; +using KeyValueMutationProto = envoy::config::core::v3::KeyValueMutation; + +enum class AppendAction { + AppendIfExistsOrAdd = envoy::config::core::v3::KeyValueAppend::APPEND_IF_EXISTS_OR_ADD, + AddIfAbsent = envoy::config::core::v3::KeyValueAppend::ADD_IF_ABSENT, + OverwriteIfExistsOrAdd = envoy::config::core::v3::KeyValueAppend::OVERWRITE_IF_EXISTS_OR_ADD, + OverwriteIfExists = envoy::config::core::v3::KeyValueAppend::OVERWRITE_IF_EXISTS, +}; + +class QueryParamsEvaluator; +using QueryParamsEvaluatorPtr = std::unique_ptr; + +class QueryParamsEvaluator { +public: + QueryParamsEvaluator( + const Protobuf::RepeatedPtrField& query_param_mutations); + + /** + * Processes headers first through query parameter removals then through query parameter + * additions. Header is modified in-place. + * @param headers supplies the request headers. + * @param stream_info used by the substitution formatter. Can be retrieved via + * decoder_callbacks_.streamInfo(); + */ + void evaluateQueryParams(Http::RequestHeaderMap& headers, + const StreamInfo::StreamInfo& stream_info) const; + +protected: + QueryParamsEvaluator() = default; + +private: + std::vector mutations_; + Formatter::FormatterPtr formatter_; +}; + +} // namespace HeaderMutation +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy diff --git a/test/extensions/filters/http/header_mutation/BUILD b/test/extensions/filters/http/header_mutation/BUILD index 0565dff752ed..ba3c3f21256d 100644 --- a/test/extensions/filters/http/header_mutation/BUILD +++ b/test/extensions/filters/http/header_mutation/BUILD @@ -54,3 +54,14 @@ envoy_extension_cc_test( "@com_google_absl//absl/strings:str_format", ], ) + +envoy_extension_cc_test( + name = "query_params_evaluator_test", + srcs = ["query_params_evaluator_test.cc"], + extension_names = ["envoy.filters.http.header_mutation"], + deps = [ + "//source/common/router:string_accessor_lib", + "//source/extensions/filters/http/header_mutation:query_params_evaluator_lib", + "//test/mocks/stream_info:stream_info_mocks", + ], +) diff --git a/test/extensions/filters/http/header_mutation/config_test.cc b/test/extensions/filters/http/header_mutation/config_test.cc index 38210e1929bb..ba636e8b5d6c 100644 --- a/test/extensions/filters/http/header_mutation/config_test.cc +++ b/test/extensions/filters/http/header_mutation/config_test.cc @@ -40,6 +40,13 @@ TEST(FactoryTest, FactoryTest) { key: "flag-header" value: "%REQ(ANOTHER-FLAG-HEADER)%" append_action: APPEND_IF_EXISTS_OR_ADD + query_parameter_mutations: + - remove: remove-me + - append: + action: APPEND_IF_EXISTS_OR_ADD + entry: + key: foo + value: bar )EOF"; PerRouteProtoConfig per_route_proto_config; diff --git a/test/extensions/filters/http/header_mutation/header_mutation_test.cc b/test/extensions/filters/http/header_mutation/header_mutation_test.cc index 3b5ede576915..b9cdb0fbf7b4 100644 --- a/test/extensions/filters/http/header_mutation/header_mutation_test.cc +++ b/test/extensions/filters/http/header_mutation/header_mutation_test.cc @@ -80,6 +80,13 @@ TEST(HeaderMutationFilterTest, HeaderMutationFilterTest) { key: "flag-header-6" value: "flag-header-6-value" append_action: "OVERWRITE_IF_EXISTS" + query_parameter_mutations: + - remove: route-remove-me + - append: + action: APPEND_IF_EXISTS_OR_ADD + entry: + key: route-param + value: cm91dGUtdmFsdWU= # 'route-value' b64-encoded )EOF"; const std::string config_yaml = R"EOF( @@ -92,6 +99,13 @@ TEST(HeaderMutationFilterTest, HeaderMutationFilterTest) { append_action: "ADD_IF_ABSENT" response_mutations: - remove: "global-flag-header" + query_parameter_mutations: + - remove: global-remove-me + - append: + action: APPEND_IF_EXISTS_OR_ADD + entry: + key: global-param + value: Z2xvYmFsLXZhbHVl # 'global-value' b64-encoded )EOF"; PerRouteProtoConfig per_route_proto_config; @@ -128,7 +142,7 @@ TEST(HeaderMutationFilterTest, HeaderMutationFilterTest) { {"flag-header-4", "flag-header-4-value-old"}, {"flag-header-6", "flag-header-6-value-old"}, {":method", "GET"}, - {":path", "/"}, + {":path", "/?global-remove-me=true&route-remove-me=true"}, {":scheme", "http"}, {":authority", "host"}}; @@ -148,6 +162,7 @@ TEST(HeaderMutationFilterTest, HeaderMutationFilterTest) { EXPECT_FALSE(headers.has("flag-header-5")); // 'flag-header-6' was present and should be overwritten. EXPECT_EQ("flag-header-6-value", headers.get_("flag-header-6")); + EXPECT_EQ("/?global-param=global-value&route-param=route-value", headers.get_(":path")); } // Case where the decodeHeaders() is not called and the encodeHeaders() is called. diff --git a/test/extensions/filters/http/header_mutation/query_params_evaluator_test.cc b/test/extensions/filters/http/header_mutation/query_params_evaluator_test.cc new file mode 100644 index 000000000000..3da161e93d11 --- /dev/null +++ b/test/extensions/filters/http/header_mutation/query_params_evaluator_test.cc @@ -0,0 +1,174 @@ +#include +#include + +#include "source/common/router/string_accessor_impl.h" +#include "source/extensions/filters/http/header_mutation/query_params_evaluator.h" + +#include "test/mocks/stream_info/mocks.h" +#include "test/test_common/utility.h" + +#include "gmock/gmock.h" +#include "gtest/gtest.h" + +namespace Envoy { +namespace Extensions { +namespace HttpFilters { +namespace HeaderMutation { + +class QueryParamsEvaluatorTest : public testing::Test { +public: + std::string evaluateWithPath(const std::string& path) { + auto paramsEvaluator = std::make_unique(mutations_); + Http::TestRequestHeaderMapImpl request_headers{{":path", path}}; + paramsEvaluator->evaluateQueryParams(request_headers, stream_info_); + return std::string(request_headers.getPathValue()); + } + + void addParamToAdd(absl::string_view key, absl::string_view value, AppendAction append_action) { + auto* qp = envoy::config::core::v3::KeyValue::default_instance().New(); + qp->set_key(key); + qp->set_value(value); + + auto* vo = KeyValueAppendProto::default_instance().New(); + vo->set_action(static_cast(append_action)); + vo->set_allocated_entry(qp); + + auto* mutation = mutations_.Add(); + mutation->set_allocated_append(vo); + } + + void addParamToRemove(absl::string_view key) { + auto* mutation = mutations_.Add(); + mutation->set_remove(key); + } + + void setFilterData(absl::string_view key, absl::string_view value) { + stream_info_.filter_state_->setData(key, std::make_unique(value), + StreamInfo::FilterState::StateType::ReadOnly, + StreamInfo::FilterState::LifeSpan::FilterChain); + } + + Protobuf::RepeatedPtrField mutations_; + testing::NiceMock stream_info_; +}; + +TEST_F(QueryParamsEvaluatorTest, EmptyConfigEvaluator) { + const auto old_path = "/path?this=should&stay=the&exact=same"; + const auto new_path = evaluateWithPath(old_path); + EXPECT_EQ("/path?this=should&stay=the&exact=same", new_path); +} + +TEST_F(QueryParamsEvaluatorTest, AppendIfExistsOrAddWhenAbsent) { + addParamToAdd("foo", "value_new", AppendAction::AppendIfExistsOrAdd); + + const auto old_path = "/path"; + const auto new_path = evaluateWithPath(old_path); + EXPECT_EQ("/path?foo=value_new", new_path); +} + +TEST_F(QueryParamsEvaluatorTest, AppendIfExistsOrAddWhenPresent) { + addParamToAdd("foo", "value_new", AppendAction::AppendIfExistsOrAdd); + + const auto old_path = "/path?foo=value_old"; + const auto new_path = evaluateWithPath(old_path); + EXPECT_EQ("/path?foo=value_old&foo=value_new", new_path); +} + +TEST_F(QueryParamsEvaluatorTest, AddIfAbsentWhenAbsent) { + addParamToAdd("foo", "value", AppendAction::AddIfAbsent); + + const auto old_path = "/path?"; + const auto new_path = evaluateWithPath(old_path); + EXPECT_EQ("/path?foo=value", new_path); +} + +TEST_F(QueryParamsEvaluatorTest, AddIfAbsentWhenPresent) { + addParamToAdd("foo", "value_new", AppendAction::AddIfAbsent); + + const auto old_path = "/path?foo=value_old"; + const auto new_path = evaluateWithPath(old_path); + EXPECT_EQ("/path?foo=value_old", new_path); +} + +TEST_F(QueryParamsEvaluatorTest, OverWriteIfExistsOrAddWhenAbsent) { + addParamToAdd("foo", "value_new", AppendAction::OverwriteIfExistsOrAdd); + + const auto old_path = "/path"; + const auto new_path = evaluateWithPath(old_path); + EXPECT_EQ("/path?foo=value_new", new_path); +} + +TEST_F(QueryParamsEvaluatorTest, OverWriteIfExistsOrAddWhenPresent) { + addParamToAdd("foo", "value_new", AppendAction::OverwriteIfExistsOrAdd); + + const auto old_path = "/path?foo=value_old"; + const auto new_path = evaluateWithPath(old_path); + EXPECT_EQ("/path?foo=value_new", new_path); +} + +TEST_F(QueryParamsEvaluatorTest, OverWriteIfExistsWhenAbsent) { + addParamToAdd("foo", "value_new", AppendAction::OverwriteIfExists); + + const auto old_path = "/path"; + const auto new_path = evaluateWithPath(old_path); + EXPECT_EQ("/path", new_path); +} + +TEST_F(QueryParamsEvaluatorTest, OverWriteIfExistsWhenPresent) { + addParamToAdd("foo", "value_new", AppendAction::OverwriteIfExists); + + const auto old_path = "/path?foo=value_old"; + const auto new_path = evaluateWithPath(old_path); + EXPECT_EQ("/path?foo=value_new", new_path); +} + +TEST_F(QueryParamsEvaluatorTest, ChainMultipleParams) { + addParamToAdd("foo", "value_1", AppendAction::AppendIfExistsOrAdd); + addParamToAdd("foo", "value_2", AppendAction::AppendIfExistsOrAdd); + + const auto old_path = "/path?bar=123"; + const auto new_path = evaluateWithPath(old_path); + EXPECT_EQ("/path?bar=123&foo=value_1&foo=value_2", new_path); +} + +TEST_F(QueryParamsEvaluatorTest, RemoveMultipleParams) { + addParamToRemove("foo"); + + const auto old_path = "/path?foo=value_1&foo=value_2&bar=123"; + const auto new_path = evaluateWithPath(old_path); + EXPECT_EQ("/path?bar=123", new_path); +} + +TEST_F(QueryParamsEvaluatorTest, AddEmptyValue) { + addParamToAdd("foo", "", AppendAction::AppendIfExistsOrAdd); + + const auto old_path = "/path?bar=123"; + const auto new_path = evaluateWithPath(old_path); + EXPECT_EQ("/path?bar=123&foo=", new_path); +} + +TEST_F(QueryParamsEvaluatorTest, CommandSubstitution) { + addParamToAdd("start_time", "%START_TIME(%s)%", AppendAction::AppendIfExistsOrAdd); + setFilterData("variable", "substituted-value"); + + const SystemTime start_time(std::chrono::seconds(1522796769)); + EXPECT_CALL(stream_info_, startTime()).WillRepeatedly(testing::Return(start_time)); + + const auto old_path = "/path"; + const auto new_path = evaluateWithPath(old_path); + EXPECT_EQ("/path?start_time=1522796769", new_path); +} + +TEST_F(QueryParamsEvaluatorTest, CommandSubstitutionFilterState) { + addParamToAdd("foo", "%FILTER_STATE(variable)%", AppendAction::AppendIfExistsOrAdd); + setFilterData("variable", "substituted-value"); + + const auto old_path = "/path?bar=123"; + const auto new_path = evaluateWithPath(old_path); + EXPECT_EQ("/path?bar=123&foo=\"substituted-value\"", new_path); +} + +} // namespace HeaderMutation +} // namespace HttpFilters +} // namespace Extensions +} // namespace Envoy