mirror of https://github.com/hashicorp/consul
1157 lines
35 KiB
Go
1157 lines
35 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package propertyoverride
|
|
|
|
import (
|
|
"fmt"
|
|
"google.golang.org/protobuf/types/known/anypb"
|
|
"testing"
|
|
|
|
envoy_cluster_v3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
|
|
corev3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
|
|
envoy_endpoint_v3 "github.com/envoyproxy/go-control-plane/envoy/config/endpoint/v3"
|
|
envoy_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3"
|
|
envoy_route_v3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
|
|
"github.com/google/go-cmp/cmp"
|
|
"github.com/stretchr/testify/require"
|
|
"google.golang.org/protobuf/proto"
|
|
"google.golang.org/protobuf/testing/protocmp"
|
|
_struct "google.golang.org/protobuf/types/known/structpb"
|
|
"google.golang.org/protobuf/types/known/wrapperspb"
|
|
)
|
|
|
|
func TestPatchStruct(t *testing.T) {
|
|
makePatch := func(o Op, p string, v any) Patch {
|
|
return Patch{
|
|
Op: o,
|
|
Path: p,
|
|
Value: v,
|
|
}
|
|
}
|
|
makeAddPatch := func(p string, v any) Patch {
|
|
return makePatch(OpAdd, p, v)
|
|
}
|
|
makeRemovePatch := func(p string) Patch {
|
|
return makePatch(OpRemove, p, nil)
|
|
}
|
|
expectFieldsListErr := func(resourceName string, truncatedFieldsWarning bool) func(t *testing.T, err error) {
|
|
return func(t *testing.T, err error) {
|
|
require.Contains(t, err.Error(), fmt.Sprintf("available %s fields:", resourceName))
|
|
if truncatedFieldsWarning {
|
|
require.Contains(t, err.Error(), "First 10 fields for this message included, configure with `Debug = true` to print all.")
|
|
} else {
|
|
require.NotContains(t, err.Error(), "First 10 fields for this message included, configure with `Debug = true` to print all.")
|
|
}
|
|
}
|
|
}
|
|
type args struct {
|
|
k proto.Message
|
|
patches []Patch
|
|
debug bool
|
|
}
|
|
type testCase struct {
|
|
args args
|
|
expected proto.Message
|
|
ok bool
|
|
errMsg string
|
|
errFunc func(*testing.T, error)
|
|
}
|
|
|
|
// Simplify test case construction for variants of potential input types
|
|
uint32VariantTestCase := func(i any) testCase {
|
|
return testCase{
|
|
args: args{
|
|
k: &envoy_endpoint_v3.Endpoint{
|
|
HealthCheckConfig: &envoy_endpoint_v3.Endpoint_HealthCheckConfig{
|
|
PortValue: 3000,
|
|
},
|
|
},
|
|
patches: []Patch{makeAddPatch(
|
|
"/health_check_config/port_value",
|
|
i,
|
|
)},
|
|
},
|
|
expected: &envoy_endpoint_v3.Endpoint{
|
|
HealthCheckConfig: &envoy_endpoint_v3.Endpoint_HealthCheckConfig{
|
|
PortValue: 1234,
|
|
},
|
|
},
|
|
ok: true,
|
|
}
|
|
}
|
|
uint32WrapperVariantTestCase := func(i any) testCase {
|
|
return testCase{
|
|
args: args{
|
|
k: &envoy_cluster_v3.Cluster{
|
|
OutlierDetection: &envoy_cluster_v3.OutlierDetection{
|
|
EnforcingConsecutive_5Xx: wrapperspb.UInt32(9999),
|
|
},
|
|
},
|
|
patches: []Patch{makeAddPatch(
|
|
"/outlier_detection/enforcing_consecutive_5xx",
|
|
i,
|
|
)},
|
|
},
|
|
expected: &envoy_cluster_v3.Cluster{
|
|
OutlierDetection: &envoy_cluster_v3.OutlierDetection{
|
|
EnforcingConsecutive_5Xx: wrapperspb.UInt32(1234),
|
|
},
|
|
},
|
|
ok: true,
|
|
}
|
|
}
|
|
uint64WrapperVariantTestCase := func(i any) testCase {
|
|
return testCase{
|
|
args: args{
|
|
k: &envoy_cluster_v3.Cluster{
|
|
LbConfig: &envoy_cluster_v3.Cluster_RingHashLbConfig_{
|
|
RingHashLbConfig: &envoy_cluster_v3.Cluster_RingHashLbConfig{
|
|
MaximumRingSize: wrapperspb.UInt64(999999999),
|
|
},
|
|
},
|
|
},
|
|
patches: []Patch{makeAddPatch(
|
|
"/ring_hash_lb_config/maximum_ring_size",
|
|
i,
|
|
)},
|
|
},
|
|
expected: &envoy_cluster_v3.Cluster{
|
|
LbConfig: &envoy_cluster_v3.Cluster_RingHashLbConfig_{
|
|
RingHashLbConfig: &envoy_cluster_v3.Cluster_RingHashLbConfig{
|
|
MaximumRingSize: wrapperspb.UInt64(12345678),
|
|
},
|
|
},
|
|
},
|
|
ok: true,
|
|
}
|
|
}
|
|
doubleVariantTestCase := func(i any) testCase {
|
|
return testCase{
|
|
args: args{
|
|
k: &envoy_cluster_v3.Cluster{
|
|
LbConfig: &envoy_cluster_v3.Cluster_LeastRequestLbConfig_{
|
|
LeastRequestLbConfig: &envoy_cluster_v3.Cluster_LeastRequestLbConfig{
|
|
ActiveRequestBias: &corev3.RuntimeDouble{
|
|
DefaultValue: 1.0,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
patches: []Patch{makeAddPatch(
|
|
"/least_request_lb_config/active_request_bias/default_value",
|
|
i,
|
|
)},
|
|
},
|
|
expected: &envoy_cluster_v3.Cluster{
|
|
LbConfig: &envoy_cluster_v3.Cluster_LeastRequestLbConfig_{
|
|
LeastRequestLbConfig: &envoy_cluster_v3.Cluster_LeastRequestLbConfig{
|
|
ActiveRequestBias: &corev3.RuntimeDouble{
|
|
DefaultValue: 1.5,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
ok: true,
|
|
}
|
|
}
|
|
doubleWrapperVariantTestCase := func(i any) testCase {
|
|
return testCase{
|
|
args: args{
|
|
k: &envoy_cluster_v3.Cluster{
|
|
PreconnectPolicy: &envoy_cluster_v3.Cluster_PreconnectPolicy{
|
|
PerUpstreamPreconnectRatio: wrapperspb.Double(1.0),
|
|
},
|
|
},
|
|
patches: []Patch{makeAddPatch(
|
|
"/preconnect_policy/per_upstream_preconnect_ratio",
|
|
i,
|
|
)},
|
|
},
|
|
expected: &envoy_cluster_v3.Cluster{
|
|
PreconnectPolicy: &envoy_cluster_v3.Cluster_PreconnectPolicy{
|
|
PerUpstreamPreconnectRatio: wrapperspb.Double(1.5),
|
|
},
|
|
},
|
|
ok: true,
|
|
}
|
|
}
|
|
enumByNumberVariantTestCase := func(i any) testCase {
|
|
return testCase{
|
|
args: args{
|
|
k: &envoy_cluster_v3.Cluster{
|
|
LbConfig: &envoy_cluster_v3.Cluster_RingHashLbConfig_{
|
|
RingHashLbConfig: &envoy_cluster_v3.Cluster_RingHashLbConfig{
|
|
HashFunction: envoy_cluster_v3.Cluster_RingHashLbConfig_XX_HASH,
|
|
},
|
|
},
|
|
},
|
|
patches: []Patch{makeAddPatch(
|
|
"/ring_hash_lb_config/hash_function",
|
|
i,
|
|
)},
|
|
},
|
|
expected: &envoy_cluster_v3.Cluster{
|
|
LbConfig: &envoy_cluster_v3.Cluster_RingHashLbConfig_{
|
|
RingHashLbConfig: &envoy_cluster_v3.Cluster_RingHashLbConfig{
|
|
HashFunction: envoy_cluster_v3.Cluster_RingHashLbConfig_MURMUR_HASH_2,
|
|
},
|
|
},
|
|
},
|
|
ok: true,
|
|
}
|
|
}
|
|
repeatedIntVariantTestCase := func(i any) testCase {
|
|
return testCase{
|
|
args: args{
|
|
k: &envoy_route_v3.RetryPolicy{
|
|
RetriableStatusCodes: []uint32{429, 502},
|
|
},
|
|
patches: []Patch{makeAddPatch(
|
|
"/retriable_status_codes",
|
|
i,
|
|
)},
|
|
},
|
|
expected: &envoy_route_v3.RetryPolicy{
|
|
RetriableStatusCodes: []uint32{503, 504},
|
|
},
|
|
ok: true,
|
|
}
|
|
}
|
|
|
|
cases := map[string]testCase{
|
|
// Some variants of target types are covered in conversion code but missing
|
|
// from this table due to lacking available examples in Envoy v3 protos. An
|
|
// improvement could be a home-rolled proto with every possible target type.
|
|
"add single field: int->uint32": uint32VariantTestCase(int(1234)),
|
|
"add single field: int32->uint32": uint32VariantTestCase(int32(1234)),
|
|
"add single field: int64->uint32": uint32VariantTestCase(int64(1234)),
|
|
"add single field: uint->uint32": uint32VariantTestCase(uint(1234)),
|
|
"add single field: uint32->uint32": uint32VariantTestCase(uint32(1234)),
|
|
"add single field: uint64->uint32": uint32VariantTestCase(uint64(1234)),
|
|
"add single field: float32->uint32": uint32VariantTestCase(float32(1234.0)),
|
|
"add single field: float64->uint32": uint32VariantTestCase(float64(1234.0)),
|
|
"add single field: int->uint32 wrapper": uint32WrapperVariantTestCase(int(1234)),
|
|
"add single field: int32->uint32 wrapper": uint32WrapperVariantTestCase(int32(1234)),
|
|
"add single field: int64->uint32 wrapper": uint32WrapperVariantTestCase(int64(1234)),
|
|
"add single field: uint->uint32 wrapper": uint32WrapperVariantTestCase(uint(1234)),
|
|
"add single field: uint32->uint32 wrapper": uint32WrapperVariantTestCase(uint32(1234)),
|
|
"add single field: uint64->uint32 wrapper": uint32WrapperVariantTestCase(uint64(1234)),
|
|
"add single field: float32->uint32 wrapper": uint32WrapperVariantTestCase(float32(1234.0)),
|
|
"add single field: float64->uint32 wrapper": uint32WrapperVariantTestCase(float64(1234.0)),
|
|
"add single field: int->uint64 wrapper": uint64WrapperVariantTestCase(int(12345678)),
|
|
"add single field: int32->uint64 wrapper": uint64WrapperVariantTestCase(int32(12345678)),
|
|
"add single field: int64->uint64 wrapper": uint64WrapperVariantTestCase(int64(12345678)),
|
|
"add single field: uint->uint64 wrapper": uint64WrapperVariantTestCase(uint(12345678)),
|
|
"add single field: uint32->uint64 wrapper": uint64WrapperVariantTestCase(uint32(12345678)),
|
|
"add single field: uint64->uint64 wrapper": uint64WrapperVariantTestCase(uint64(12345678)),
|
|
"add single field: float32->uint64 wrapper": uint64WrapperVariantTestCase(float32(12345678.0)),
|
|
"add single field: float64->uint64 wrapper": uint64WrapperVariantTestCase(float64(12345678.0)),
|
|
"add single field: float32->double": doubleVariantTestCase(float32(1.5)),
|
|
"add single field: float64->double": doubleVariantTestCase(float64(1.5)),
|
|
"add single field: float32->double wrapper": doubleWrapperVariantTestCase(float32(1.5)),
|
|
"add single field: float64->double wrapper": doubleWrapperVariantTestCase(float64(1.5)),
|
|
"add single field: bool": {
|
|
args: args{
|
|
k: &envoy_cluster_v3.Cluster{
|
|
RespectDnsTtl: false,
|
|
},
|
|
patches: []Patch{makeAddPatch(
|
|
"/respect_dns_ttl",
|
|
true,
|
|
)},
|
|
},
|
|
expected: &envoy_cluster_v3.Cluster{
|
|
RespectDnsTtl: true,
|
|
},
|
|
ok: true,
|
|
},
|
|
"add single field: bool wrapper": {
|
|
args: args{
|
|
k: &envoy_listener_v3.Listener{
|
|
UseOriginalDst: wrapperspb.Bool(false),
|
|
},
|
|
patches: []Patch{makeAddPatch(
|
|
"/use_original_dst",
|
|
true,
|
|
)},
|
|
},
|
|
expected: &envoy_listener_v3.Listener{
|
|
UseOriginalDst: wrapperspb.Bool(true),
|
|
},
|
|
ok: true,
|
|
},
|
|
"add single field: string": {
|
|
args: args{
|
|
k: &envoy_cluster_v3.Cluster{
|
|
AltStatName: "foo",
|
|
},
|
|
patches: []Patch{makeAddPatch(
|
|
"/alt_stat_name",
|
|
"bar",
|
|
)},
|
|
},
|
|
expected: &envoy_cluster_v3.Cluster{
|
|
AltStatName: "bar",
|
|
},
|
|
ok: true,
|
|
},
|
|
"add single field: enum by name": {
|
|
args: args{
|
|
k: &envoy_cluster_v3.Cluster{
|
|
LbConfig: &envoy_cluster_v3.Cluster_RingHashLbConfig_{
|
|
RingHashLbConfig: &envoy_cluster_v3.Cluster_RingHashLbConfig{
|
|
HashFunction: envoy_cluster_v3.Cluster_RingHashLbConfig_XX_HASH,
|
|
},
|
|
},
|
|
},
|
|
patches: []Patch{makeAddPatch(
|
|
"/ring_hash_lb_config/hash_function",
|
|
"MURMUR_HASH_2",
|
|
)},
|
|
},
|
|
expected: &envoy_cluster_v3.Cluster{
|
|
LbConfig: &envoy_cluster_v3.Cluster_RingHashLbConfig_{
|
|
RingHashLbConfig: &envoy_cluster_v3.Cluster_RingHashLbConfig{
|
|
HashFunction: envoy_cluster_v3.Cluster_RingHashLbConfig_MURMUR_HASH_2,
|
|
},
|
|
},
|
|
},
|
|
ok: true,
|
|
},
|
|
"add single field: enum by number int": enumByNumberVariantTestCase(int(1)),
|
|
"add single field: enum by number int32": enumByNumberVariantTestCase(int32(1)),
|
|
"add single field: enum by number int64": enumByNumberVariantTestCase(int64(1)),
|
|
"add single field: enum by number uint": enumByNumberVariantTestCase(uint(1)),
|
|
"add single field: enum by number uint32": enumByNumberVariantTestCase(uint32(1)),
|
|
"add single field: enum by number uint64": enumByNumberVariantTestCase(uint64(1)),
|
|
"add single field previously unmodified": {
|
|
args: args{
|
|
k: &envoy_cluster_v3.Cluster{},
|
|
patches: []Patch{makeAddPatch(
|
|
"/alt_stat_name",
|
|
"bar",
|
|
)},
|
|
},
|
|
expected: &envoy_cluster_v3.Cluster{
|
|
AltStatName: "bar",
|
|
},
|
|
ok: true,
|
|
},
|
|
"add single field deeply nested": {
|
|
args: args{
|
|
k: &envoy_cluster_v3.Cluster{
|
|
UpstreamConnectionOptions: &envoy_cluster_v3.UpstreamConnectionOptions{
|
|
TcpKeepalive: &corev3.TcpKeepalive{
|
|
KeepaliveProbes: wrapperspb.UInt32(2),
|
|
},
|
|
},
|
|
},
|
|
patches: []Patch{makeAddPatch(
|
|
"/upstream_connection_options/tcp_keepalive/keepalive_probes",
|
|
5,
|
|
)},
|
|
},
|
|
expected: &envoy_cluster_v3.Cluster{
|
|
UpstreamConnectionOptions: &envoy_cluster_v3.UpstreamConnectionOptions{
|
|
TcpKeepalive: &corev3.TcpKeepalive{
|
|
KeepaliveProbes: wrapperspb.UInt32(5),
|
|
},
|
|
},
|
|
},
|
|
ok: true,
|
|
},
|
|
"add single field deeply nested with intermediate unset field": {
|
|
args: args{
|
|
k: &envoy_cluster_v3.Cluster{
|
|
// Explicitly set to nil just in case defaults change.
|
|
UpstreamConnectionOptions: nil,
|
|
},
|
|
patches: []Patch{makeAddPatch(
|
|
"/upstream_connection_options/tcp_keepalive/keepalive_probes",
|
|
1234,
|
|
)},
|
|
},
|
|
expected: &envoy_cluster_v3.Cluster{
|
|
UpstreamConnectionOptions: &envoy_cluster_v3.UpstreamConnectionOptions{
|
|
TcpKeepalive: &corev3.TcpKeepalive{
|
|
KeepaliveProbes: wrapperspb.UInt32(1234),
|
|
},
|
|
},
|
|
},
|
|
ok: true,
|
|
},
|
|
"add repeated field: int->uint32": repeatedIntVariantTestCase([]int{503, 504}),
|
|
"add repeated field: int32->uint32": repeatedIntVariantTestCase([]int32{503, 504}),
|
|
"add repeated field: int64->uint32": repeatedIntVariantTestCase([]int64{503, 504}),
|
|
"add repeated field: uint->uint32": repeatedIntVariantTestCase([]uint{503, 504}),
|
|
"add repeated field: uint32->uint32": repeatedIntVariantTestCase([]uint32{503, 504}),
|
|
"add repeated field: uint64->uint32": repeatedIntVariantTestCase([]uint64{503, 504}),
|
|
"add repeated field: float32->uint32": repeatedIntVariantTestCase([]float32{503.0, 504.0}),
|
|
"add repeated field: float64->uint32": repeatedIntVariantTestCase([]float64{503.0, 504.0}),
|
|
"add repeated field: string": {
|
|
args: args{
|
|
k: &envoy_route_v3.RouteConfiguration{},
|
|
patches: []Patch{makeAddPatch(
|
|
"/internal_only_headers",
|
|
[]string{"X-Custom-Header1", "X-Custom-Header-2"},
|
|
)},
|
|
},
|
|
expected: &envoy_route_v3.RouteConfiguration{
|
|
InternalOnlyHeaders: []string{"X-Custom-Header1", "X-Custom-Header-2"},
|
|
},
|
|
ok: true,
|
|
},
|
|
"add repeated field: enum by name": {
|
|
args: args{
|
|
k: &corev3.HealthStatusSet{
|
|
Statuses: []corev3.HealthStatus{corev3.HealthStatus_DRAINING},
|
|
},
|
|
patches: []Patch{makeAddPatch(
|
|
"/statuses",
|
|
[]string{"HEALTHY", "UNHEALTHY"},
|
|
)},
|
|
},
|
|
expected: &corev3.HealthStatusSet{
|
|
Statuses: []corev3.HealthStatus{corev3.HealthStatus_HEALTHY, corev3.HealthStatus_UNHEALTHY},
|
|
},
|
|
ok: true,
|
|
},
|
|
"add repeated field: enum by number": {
|
|
args: args{
|
|
k: &corev3.HealthStatusSet{
|
|
Statuses: []corev3.HealthStatus{corev3.HealthStatus_DRAINING},
|
|
},
|
|
patches: []Patch{makeAddPatch(
|
|
"/statuses",
|
|
[]int{1, 2},
|
|
)},
|
|
},
|
|
expected: &corev3.HealthStatusSet{
|
|
Statuses: []corev3.HealthStatus{corev3.HealthStatus_HEALTHY, corev3.HealthStatus_UNHEALTHY},
|
|
},
|
|
ok: true,
|
|
},
|
|
"add message field: empty": {
|
|
args: args{
|
|
k: &envoy_listener_v3.Listener{},
|
|
patches: []Patch{makeAddPatch(
|
|
"/connection_balance_config/exact_balance",
|
|
map[string]any{},
|
|
)},
|
|
},
|
|
expected: &envoy_listener_v3.Listener{
|
|
ConnectionBalanceConfig: &envoy_listener_v3.Listener_ConnectionBalanceConfig{
|
|
BalanceType: &envoy_listener_v3.Listener_ConnectionBalanceConfig_ExactBalance_{},
|
|
},
|
|
},
|
|
ok: true,
|
|
},
|
|
"add message field: multiple fields": {
|
|
args: args{
|
|
k: &envoy_cluster_v3.Cluster{
|
|
OutlierDetection: &envoy_cluster_v3.OutlierDetection{
|
|
EnforcingConsecutive_5Xx: wrapperspb.UInt32(9999),
|
|
FailurePercentageThreshold: wrapperspb.UInt32(9999),
|
|
},
|
|
},
|
|
patches: []Patch{makeAddPatch(
|
|
"/outlier_detection",
|
|
map[string]any{
|
|
"enforcing_consecutive_5xx": 1234,
|
|
"failure_percentage_request_volume": 2345,
|
|
},
|
|
)},
|
|
},
|
|
expected: &envoy_cluster_v3.Cluster{
|
|
OutlierDetection: &envoy_cluster_v3.OutlierDetection{
|
|
EnforcingConsecutive_5Xx: wrapperspb.UInt32(1234),
|
|
FailurePercentageRequestVolume: wrapperspb.UInt32(2345),
|
|
},
|
|
},
|
|
ok: true,
|
|
},
|
|
"add multiple single field patches merge with existing object": {
|
|
args: args{
|
|
k: &envoy_cluster_v3.Cluster{
|
|
OutlierDetection: &envoy_cluster_v3.OutlierDetection{
|
|
EnforcingConsecutive_5Xx: wrapperspb.UInt32(9999),
|
|
FailurePercentageThreshold: wrapperspb.UInt32(9999),
|
|
},
|
|
},
|
|
patches: []Patch{
|
|
makeAddPatch(
|
|
"/outlier_detection/enforcing_consecutive_5xx",
|
|
1234,
|
|
),
|
|
makeAddPatch(
|
|
"/outlier_detection/failure_percentage_request_volume",
|
|
2345,
|
|
),
|
|
},
|
|
},
|
|
expected: &envoy_cluster_v3.Cluster{
|
|
OutlierDetection: &envoy_cluster_v3.OutlierDetection{
|
|
EnforcingConsecutive_5Xx: wrapperspb.UInt32(1234),
|
|
FailurePercentageRequestVolume: wrapperspb.UInt32(2345), // Previously unspecified field set
|
|
FailurePercentageThreshold: wrapperspb.UInt32(9999), // Existing unmodified field retained
|
|
},
|
|
},
|
|
ok: true,
|
|
},
|
|
"remove single field: scalar wrapper": {
|
|
args: args{
|
|
k: &envoy_cluster_v3.Cluster{
|
|
OutlierDetection: &envoy_cluster_v3.OutlierDetection{
|
|
EnforcingConsecutive_5Xx: wrapperspb.UInt32(9999),
|
|
},
|
|
},
|
|
patches: []Patch{makeRemovePatch(
|
|
"/outlier_detection/enforcing_consecutive_5xx",
|
|
)},
|
|
},
|
|
expected: &envoy_cluster_v3.Cluster{
|
|
OutlierDetection: &envoy_cluster_v3.OutlierDetection{},
|
|
},
|
|
ok: true,
|
|
},
|
|
"remove single field: string (reset to empty)": {
|
|
args: args{
|
|
k: &envoy_cluster_v3.Cluster{
|
|
AltStatName: "foo",
|
|
},
|
|
patches: []Patch{makeRemovePatch(
|
|
"/alt_stat_name",
|
|
)},
|
|
},
|
|
expected: &envoy_cluster_v3.Cluster{},
|
|
ok: true,
|
|
},
|
|
"remove single field: bool (reset to false)": {
|
|
args: args{
|
|
k: &envoy_cluster_v3.Cluster{
|
|
RespectDnsTtl: true,
|
|
},
|
|
patches: []Patch{makeRemovePatch(
|
|
"/respect_dns_ttl",
|
|
)},
|
|
},
|
|
expected: &envoy_cluster_v3.Cluster{
|
|
RespectDnsTtl: false,
|
|
},
|
|
ok: true,
|
|
},
|
|
"remove single field: enum (reset to default)": {
|
|
args: args{
|
|
k: &envoy_cluster_v3.Cluster{
|
|
LbConfig: &envoy_cluster_v3.Cluster_RingHashLbConfig_{
|
|
RingHashLbConfig: &envoy_cluster_v3.Cluster_RingHashLbConfig{
|
|
HashFunction: envoy_cluster_v3.Cluster_RingHashLbConfig_MURMUR_HASH_2,
|
|
},
|
|
},
|
|
},
|
|
patches: []Patch{makeRemovePatch(
|
|
"/ring_hash_lb_config/hash_function",
|
|
)},
|
|
},
|
|
expected: &envoy_cluster_v3.Cluster{
|
|
LbConfig: &envoy_cluster_v3.Cluster_RingHashLbConfig_{
|
|
RingHashLbConfig: &envoy_cluster_v3.Cluster_RingHashLbConfig{
|
|
HashFunction: envoy_cluster_v3.Cluster_RingHashLbConfig_XX_HASH,
|
|
},
|
|
},
|
|
},
|
|
ok: true,
|
|
},
|
|
"remove single field: message": {
|
|
args: args{
|
|
k: &envoy_cluster_v3.Cluster{
|
|
OutlierDetection: &envoy_cluster_v3.OutlierDetection{
|
|
EnforcingConsecutive_5Xx: wrapperspb.UInt32(9999),
|
|
FailurePercentageThreshold: wrapperspb.UInt32(9999),
|
|
},
|
|
},
|
|
patches: []Patch{makeRemovePatch(
|
|
"/outlier_detection",
|
|
)},
|
|
},
|
|
expected: &envoy_cluster_v3.Cluster{},
|
|
ok: true,
|
|
},
|
|
"remove single field: map": {
|
|
args: args{
|
|
k: &envoy_cluster_v3.Cluster{
|
|
Metadata: &corev3.Metadata{
|
|
FilterMetadata: map[string]*_struct.Struct{
|
|
"foo": nil,
|
|
},
|
|
},
|
|
},
|
|
patches: []Patch{makeRemovePatch(
|
|
"/metadata/filter_metadata",
|
|
)},
|
|
},
|
|
expected: &envoy_cluster_v3.Cluster{
|
|
Metadata: &corev3.Metadata{},
|
|
},
|
|
ok: true,
|
|
},
|
|
"remove single field: Any": {
|
|
args: args{
|
|
k: &envoy_cluster_v3.Cluster{
|
|
ClusterDiscoveryType: &envoy_cluster_v3.Cluster_ClusterType{
|
|
ClusterType: &envoy_cluster_v3.Cluster_CustomClusterType{
|
|
TypedConfig: &anypb.Any{
|
|
TypeUrl: "foo",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
patches: []Patch{
|
|
makeRemovePatch(
|
|
"/cluster_type/typed_config",
|
|
),
|
|
},
|
|
},
|
|
// Invalid actual config, but used as an example of removing Any field directly
|
|
expected: &envoy_cluster_v3.Cluster{
|
|
ClusterDiscoveryType: &envoy_cluster_v3.Cluster_ClusterType{
|
|
ClusterType: &envoy_cluster_v3.Cluster_CustomClusterType{},
|
|
},
|
|
},
|
|
ok: true,
|
|
},
|
|
"remove single field deeply nested": {
|
|
args: args{
|
|
k: &envoy_cluster_v3.Cluster{
|
|
UpstreamConnectionOptions: &envoy_cluster_v3.UpstreamConnectionOptions{
|
|
TcpKeepalive: &corev3.TcpKeepalive{
|
|
KeepaliveProbes: wrapperspb.UInt32(9999),
|
|
},
|
|
},
|
|
},
|
|
patches: []Patch{makeRemovePatch(
|
|
"/upstream_connection_options/tcp_keepalive/keepalive_probes",
|
|
)},
|
|
},
|
|
expected: &envoy_cluster_v3.Cluster{
|
|
UpstreamConnectionOptions: &envoy_cluster_v3.UpstreamConnectionOptions{
|
|
TcpKeepalive: &corev3.TcpKeepalive{},
|
|
},
|
|
},
|
|
ok: true,
|
|
},
|
|
"remove repeated field: message": {
|
|
args: args{
|
|
k: &envoy_cluster_v3.Cluster{
|
|
Filters: []*envoy_cluster_v3.Filter{
|
|
{
|
|
Name: "foo",
|
|
},
|
|
},
|
|
},
|
|
patches: []Patch{makeRemovePatch(
|
|
"/filters",
|
|
)},
|
|
},
|
|
expected: &envoy_cluster_v3.Cluster{},
|
|
ok: true,
|
|
},
|
|
"remove multiple single field patches merge with existing object": {
|
|
args: args{
|
|
k: &envoy_cluster_v3.Cluster{
|
|
OutlierDetection: &envoy_cluster_v3.OutlierDetection{
|
|
EnforcingConsecutive_5Xx: wrapperspb.UInt32(9999),
|
|
FailurePercentageThreshold: wrapperspb.UInt32(9999),
|
|
},
|
|
},
|
|
patches: []Patch{
|
|
makeRemovePatch(
|
|
"/outlier_detection/enforcing_consecutive_5xx",
|
|
),
|
|
makeRemovePatch(
|
|
"/outlier_detection/failure_percentage_request_volume", // No-op removal
|
|
),
|
|
},
|
|
},
|
|
expected: &envoy_cluster_v3.Cluster{
|
|
OutlierDetection: &envoy_cluster_v3.OutlierDetection{
|
|
FailurePercentageThreshold: wrapperspb.UInt32(9999), // Existing unmodified field retained
|
|
},
|
|
},
|
|
ok: true,
|
|
},
|
|
"remove does not instantiate intermediate fields that are unset": {
|
|
args: args{
|
|
k: &envoy_cluster_v3.Cluster{
|
|
// Explicitly set to nil just in case defaults change.
|
|
UpstreamConnectionOptions: nil,
|
|
},
|
|
patches: []Patch{makeRemovePatch(
|
|
"/upstream_connection_options/tcp_keepalive/keepalive_probes",
|
|
)},
|
|
},
|
|
expected: &envoy_cluster_v3.Cluster{},
|
|
ok: true,
|
|
},
|
|
"add and remove multiple single field patches merge with existing object": {
|
|
args: args{
|
|
k: &envoy_cluster_v3.Cluster{
|
|
OutlierDetection: &envoy_cluster_v3.OutlierDetection{
|
|
FailurePercentageRequestVolume: wrapperspb.UInt32(9999),
|
|
FailurePercentageThreshold: wrapperspb.UInt32(9999),
|
|
},
|
|
},
|
|
patches: []Patch{
|
|
makeAddPatch(
|
|
"/outlier_detection/enforcing_consecutive_5xx",
|
|
1234,
|
|
),
|
|
makeRemovePatch(
|
|
"/outlier_detection/failure_percentage_request_volume",
|
|
),
|
|
},
|
|
},
|
|
expected: &envoy_cluster_v3.Cluster{
|
|
OutlierDetection: &envoy_cluster_v3.OutlierDetection{
|
|
EnforcingConsecutive_5Xx: wrapperspb.UInt32(1234), // New field added
|
|
FailurePercentageThreshold: wrapperspb.UInt32(9999), // Existing unmodified field retained
|
|
},
|
|
},
|
|
ok: true,
|
|
},
|
|
"add then remove respects order of operations": {
|
|
args: args{
|
|
k: &envoy_cluster_v3.Cluster{},
|
|
patches: []Patch{
|
|
makeAddPatch(
|
|
"/outlier_detection/enforcing_consecutive_5xx",
|
|
1234,
|
|
),
|
|
makeRemovePatch(
|
|
"/outlier_detection",
|
|
),
|
|
},
|
|
},
|
|
expected: &envoy_cluster_v3.Cluster{},
|
|
ok: true,
|
|
},
|
|
"remove then add respects order of operations": {
|
|
args: args{
|
|
k: &envoy_cluster_v3.Cluster{
|
|
OutlierDetection: &envoy_cluster_v3.OutlierDetection{
|
|
FailurePercentageRequestVolume: wrapperspb.UInt32(9999),
|
|
},
|
|
},
|
|
patches: []Patch{
|
|
makeRemovePatch(
|
|
"/outlier_detection",
|
|
),
|
|
makeAddPatch(
|
|
"/outlier_detection/enforcing_consecutive_5xx",
|
|
1234,
|
|
),
|
|
},
|
|
},
|
|
expected: &envoy_cluster_v3.Cluster{
|
|
OutlierDetection: &envoy_cluster_v3.OutlierDetection{
|
|
EnforcingConsecutive_5Xx: wrapperspb.UInt32(1234), // New field added
|
|
// Previous field removed by remove op
|
|
},
|
|
},
|
|
ok: true,
|
|
},
|
|
"add invalid value: scalar->scalar type mismatch": {
|
|
args: args{
|
|
k: &envoy_cluster_v3.Cluster{
|
|
OutlierDetection: &envoy_cluster_v3.OutlierDetection{
|
|
EnforcingConsecutive_5Xx: wrapperspb.UInt32(9999),
|
|
},
|
|
},
|
|
patches: []Patch{makeAddPatch(
|
|
"/outlier_detection/enforcing_consecutive_5xx",
|
|
"NaN",
|
|
)},
|
|
},
|
|
ok: false,
|
|
errMsg: "patch value type string could not be applied to target field type 'google.protobuf.UInt32Value'",
|
|
},
|
|
"add invalid value: non-scalar->scalar type mismatch": {
|
|
args: args{
|
|
k: &envoy_cluster_v3.Cluster{
|
|
OutlierDetection: &envoy_cluster_v3.OutlierDetection{
|
|
EnforcingConsecutive_5Xx: wrapperspb.UInt32(9999),
|
|
},
|
|
},
|
|
patches: []Patch{makeAddPatch(
|
|
"/respect_dns_ttl",
|
|
[]string{"bad", "value"},
|
|
)},
|
|
},
|
|
ok: false,
|
|
errMsg: "patch value type []string could not be applied to target field type 'bool'",
|
|
},
|
|
"add invalid value: scalar->enum type mismatch": {
|
|
args: args{
|
|
k: &envoy_cluster_v3.Cluster{},
|
|
patches: []Patch{makeAddPatch(
|
|
"/ring_hash_lb_config/hash_function",
|
|
1.5,
|
|
)},
|
|
},
|
|
ok: false,
|
|
errMsg: "patch value type float64 could not be applied to target field type 'enum'",
|
|
},
|
|
"add invalid value: nil scalar": {
|
|
args: args{
|
|
k: &envoy_cluster_v3.Cluster{
|
|
RespectDnsTtl: false,
|
|
},
|
|
patches: []Patch{makeAddPatch(
|
|
"/respect_dns_ttl",
|
|
nil,
|
|
)},
|
|
},
|
|
ok: false,
|
|
errMsg: "non-nil Value is required; use an empty map to reset all fields on a message or the 'remove' op to unset fields",
|
|
},
|
|
"add invalid value: nil wrapper": {
|
|
args: args{
|
|
k: &envoy_cluster_v3.Cluster{
|
|
OutlierDetection: &envoy_cluster_v3.OutlierDetection{
|
|
EnforcingConsecutive_5Xx: wrapperspb.UInt32(9999),
|
|
},
|
|
},
|
|
patches: []Patch{makeAddPatch(
|
|
"/outlier_detection/enforcing_consecutive_5xx",
|
|
nil,
|
|
)},
|
|
},
|
|
ok: false,
|
|
errMsg: "non-nil Value is required; use an empty map to reset all fields on a message or the 'remove' op to unset fields",
|
|
},
|
|
"add invalid value: nil message": {
|
|
|
|
args: args{
|
|
k: &envoy_cluster_v3.Cluster{
|
|
OutlierDetection: &envoy_cluster_v3.OutlierDetection{
|
|
EnforcingConsecutive_5Xx: wrapperspb.UInt32(9999),
|
|
FailurePercentageThreshold: wrapperspb.UInt32(9999),
|
|
},
|
|
},
|
|
patches: []Patch{makeAddPatch(
|
|
"/outlier_detection",
|
|
nil,
|
|
)},
|
|
},
|
|
ok: false,
|
|
errMsg: "non-nil Value is required; use an empty map to reset all fields on a message or the 'remove' op to unset fields",
|
|
},
|
|
"add invalid value: mixed type scalar": {
|
|
args: args{
|
|
k: &envoy_route_v3.RouteConfiguration{},
|
|
patches: []Patch{makeAddPatch(
|
|
"/internal_only_headers",
|
|
[]any{"X-Custom-Header1", 123},
|
|
)},
|
|
},
|
|
expected: &envoy_route_v3.RouteConfiguration{
|
|
InternalOnlyHeaders: []string{"X-Custom-Header1", "X-Custom-Header-2"},
|
|
},
|
|
ok: false,
|
|
errMsg: "patch value type []interface {} could not be applied to target field type 'repeated string'",
|
|
},
|
|
"add unsupported target: message with non-scalar fields": {
|
|
args: args{
|
|
k: &envoy_cluster_v3.Cluster{},
|
|
patches: []Patch{makeAddPatch(
|
|
"/dns_failure_refresh_rate",
|
|
map[string]any{
|
|
"base_interval": map[string]any{},
|
|
},
|
|
)},
|
|
},
|
|
ok: false,
|
|
errMsg: "unsupported target field type 'google.protobuf.Duration'",
|
|
},
|
|
"add unsupported target: map field": {
|
|
args: args{
|
|
k: &envoy_cluster_v3.Cluster{},
|
|
patches: []Patch{makeAddPatch(
|
|
"/metadata/filter_metadata",
|
|
map[string]any{
|
|
"foo": "bar",
|
|
},
|
|
)},
|
|
},
|
|
ok: false,
|
|
errMsg: "unsupported target field type 'map'",
|
|
},
|
|
"add unsupported target: non-message field via map": {
|
|
args: args{
|
|
k: &envoy_cluster_v3.Cluster{},
|
|
patches: []Patch{
|
|
makeAddPatch(
|
|
"/name",
|
|
map[string]any{
|
|
"cluster_refresh_rate": "5s",
|
|
"cluster_refresh_timeout": "3s",
|
|
"redirect_refresh_interval": "5s",
|
|
"redirect_refresh_threshold": 5,
|
|
},
|
|
),
|
|
},
|
|
},
|
|
ok: false,
|
|
errMsg: "non-message field type 'string' cannot be set via a map",
|
|
},
|
|
"add unsupported target: non-message parent field via single value": {
|
|
args: args{
|
|
k: &envoy_cluster_v3.Cluster{},
|
|
patches: []Patch{
|
|
makeAddPatch(
|
|
"/name/foo",
|
|
"bar",
|
|
),
|
|
},
|
|
},
|
|
ok: false,
|
|
errMsg: "path contains member of non-message field 'name' (type 'string'); this type does not support child fields",
|
|
},
|
|
"add unsupported target: non-message parent field via map": {
|
|
args: args{
|
|
k: &envoy_cluster_v3.Cluster{},
|
|
patches: []Patch{
|
|
makeAddPatch(
|
|
"/name/foo",
|
|
map[string]any{
|
|
"cluster_refresh_rate": "5s",
|
|
"cluster_refresh_timeout": "3s",
|
|
"redirect_refresh_interval": "5s",
|
|
"redirect_refresh_threshold": 5,
|
|
},
|
|
),
|
|
},
|
|
},
|
|
ok: false,
|
|
errMsg: "path contains member of non-message field 'name' (type 'string'); this type does not support child fields",
|
|
},
|
|
"add unsupported target: Any field": {
|
|
args: args{
|
|
k: &envoy_cluster_v3.Cluster{},
|
|
patches: []Patch{
|
|
makeAddPatch(
|
|
// Purposefully use a wrong-but-reasonable field name to ensure special error is returned
|
|
"/cluster_type/typed_config/@type",
|
|
"foo",
|
|
),
|
|
},
|
|
},
|
|
ok: false,
|
|
errMsg: "variant-type message fields (google.protobuf.Any) are not supported",
|
|
},
|
|
"add unsupported target: repeated message": {
|
|
args: args{
|
|
k: &envoy_cluster_v3.Cluster{},
|
|
patches: []Patch{makeAddPatch(
|
|
"/filters",
|
|
[]any{}, // We don't need a value in this slice to test behavior
|
|
)},
|
|
},
|
|
ok: false,
|
|
errMsg: "unsupported target field type 'repeated envoy.config.cluster.v3.Filter'",
|
|
},
|
|
"empty path": {
|
|
args: args{
|
|
k: &envoy_cluster_v3.Cluster{},
|
|
patches: []Patch{makeAddPatch(
|
|
"",
|
|
"ignored",
|
|
)},
|
|
},
|
|
ok: false,
|
|
errMsg: "non-empty, non-root Path is required",
|
|
errFunc: expectFieldsListErr("envoy.config.cluster.v3.Cluster", true),
|
|
},
|
|
"empty path debug mode": {
|
|
args: args{
|
|
k: &envoy_cluster_v3.Cluster{},
|
|
patches: []Patch{makeAddPatch(
|
|
"",
|
|
"ignored",
|
|
)},
|
|
debug: true,
|
|
},
|
|
ok: false,
|
|
errMsg: "non-empty, non-root Path is required",
|
|
errFunc: expectFieldsListErr("envoy.config.cluster.v3.Cluster", false),
|
|
},
|
|
"root path": {
|
|
args: args{
|
|
k: &envoy_cluster_v3.Cluster{},
|
|
patches: []Patch{makeAddPatch(
|
|
"/",
|
|
"ignored",
|
|
)},
|
|
},
|
|
ok: false,
|
|
errMsg: "non-empty, non-root Path is required",
|
|
errFunc: expectFieldsListErr("envoy.config.cluster.v3.Cluster", true),
|
|
},
|
|
"root path debug mode": {
|
|
args: args{
|
|
k: &envoy_cluster_v3.Cluster{},
|
|
patches: []Patch{makeAddPatch(
|
|
"/",
|
|
"ignored",
|
|
)},
|
|
debug: true,
|
|
},
|
|
ok: false,
|
|
errMsg: "non-empty, non-root Path is required",
|
|
errFunc: expectFieldsListErr("envoy.config.cluster.v3.Cluster", false),
|
|
},
|
|
"invalid path: add unknown field": {
|
|
args: args{
|
|
k: &envoy_cluster_v3.Cluster{},
|
|
patches: []Patch{makeAddPatch(
|
|
"/outlier_detection/foo",
|
|
"ignored",
|
|
)},
|
|
},
|
|
ok: false,
|
|
errMsg: "no match for field 'foo'!",
|
|
errFunc: expectFieldsListErr("envoy.config.cluster.v3.OutlierDetection", true),
|
|
},
|
|
"invalid path: remove unknown field": {
|
|
args: args{
|
|
k: &envoy_cluster_v3.Cluster{},
|
|
patches: []Patch{makeRemovePatch(
|
|
"/outlier_detection/foo",
|
|
)},
|
|
},
|
|
ok: false,
|
|
errMsg: "no match for field 'foo'!",
|
|
errFunc: expectFieldsListErr("envoy.config.cluster.v3.OutlierDetection", true),
|
|
},
|
|
"invalid path: unknown field debug mode": {
|
|
args: args{
|
|
k: &envoy_cluster_v3.Cluster{},
|
|
patches: []Patch{makeAddPatch(
|
|
"/outlier_detection/foo",
|
|
"ignored",
|
|
)},
|
|
debug: true,
|
|
},
|
|
ok: false,
|
|
errMsg: "no match for field 'foo'!",
|
|
errFunc: expectFieldsListErr("envoy.config.cluster.v3.OutlierDetection", false),
|
|
},
|
|
"error field list includes first 10 fields when not in debug mode": {
|
|
args: args{
|
|
k: &envoy_cluster_v3.Cluster{},
|
|
patches: []Patch{makeAddPatch(
|
|
"",
|
|
"ignored",
|
|
)},
|
|
},
|
|
ok: false,
|
|
errMsg: "transport_socket_matches\nname\nalt_stat_name\ntype\ncluster_type\neds_cluster_config\nconnect_timeout\nper_connection_buffer_limit_bytes\nlb_policy\nload_assignment",
|
|
errFunc: expectFieldsListErr("envoy.config.cluster.v3.Cluster", true),
|
|
},
|
|
"error field list includes all fields when in debug mode": {
|
|
args: args{
|
|
k: &envoy_cluster_v3.Cluster{},
|
|
patches: []Patch{makeAddPatch(
|
|
"",
|
|
"ignored",
|
|
)},
|
|
debug: true,
|
|
},
|
|
ok: false,
|
|
errMsg: "transport_socket_matches\nname\nalt_stat_name\ntype\ncluster_type\neds_cluster_config\nconnect_timeout\nper_connection_buffer_limit_bytes\nlb_policy\nload_assignment\nhealth_checks\nmax_requests_per_connection\ncircuit_breakers\nupstream_http_protocol_options\ncommon_http_protocol_options\nhttp_protocol_options\nhttp2_protocol_options\ntyped_extension_protocol_options\ndns_refresh_rate\ndns_failure_refresh_rate\nrespect_dns_ttl\ndns_lookup_family\ndns_resolvers\nuse_tcp_for_dns_lookups\ndns_resolution_config\ntyped_dns_resolver_config\nwait_for_warm_on_init\noutlier_detection\ncleanup_interval\nupstream_bind_config\nlb_subset_config\nring_hash_lb_config\nmaglev_lb_config\noriginal_dst_lb_config\nleast_request_lb_config\nround_robin_lb_config\ncommon_lb_config\ntransport_socket\nmetadata\nprotocol_selection\nupstream_connection_options\nclose_connections_on_host_health_failure\nignore_health_on_host_removal\nfilters\nload_balancing_policy\nlrs_server\ntrack_timeout_budgets\nupstream_config\ntrack_cluster_stats\npreconnect_policy\nconnection_pool_per_downstream_connection",
|
|
errFunc: expectFieldsListErr("envoy.config.cluster.v3.Cluster", false),
|
|
},
|
|
"error field list warns about first 10 fields only when > 10 available when not in debug mode": {
|
|
args: args{
|
|
k: &envoy_cluster_v3.Cluster{},
|
|
patches: []Patch{makeAddPatch(
|
|
"/upstream_connection_options/tcp_keepalive/foo",
|
|
"ignored",
|
|
)},
|
|
debug: false,
|
|
},
|
|
ok: false,
|
|
errMsg: "keepalive_probes\nkeepalive_time\nkeepalive_interval",
|
|
errFunc: expectFieldsListErr("envoy.config.core.v3.TcpKeepalive", false),
|
|
},
|
|
"invalid path: empty path element": {
|
|
args: args{
|
|
k: &envoy_cluster_v3.Cluster{},
|
|
patches: []Patch{makeAddPatch(
|
|
"/outlier_detection//",
|
|
"ignored",
|
|
)},
|
|
},
|
|
ok: false,
|
|
errMsg: "empty field name in path",
|
|
},
|
|
"invalid path: repeated field member": {
|
|
args: args{
|
|
k: &envoy_listener_v3.Listener{},
|
|
patches: []Patch{makeRemovePatch(
|
|
"/filter_chains/0/transport_socket_connect_timeout",
|
|
)},
|
|
},
|
|
ok: false,
|
|
errMsg: "path contains member of repeated field 'filter_chains'; repeated field member access is not supported",
|
|
},
|
|
"invalid path: map field member": {
|
|
args: args{
|
|
k: &envoy_cluster_v3.Cluster{
|
|
Metadata: &corev3.Metadata{
|
|
FilterMetadata: map[string]*_struct.Struct{
|
|
"foo": nil,
|
|
},
|
|
},
|
|
},
|
|
patches: []Patch{makeRemovePatch(
|
|
"/metadata/filter_metadata/foo",
|
|
)},
|
|
},
|
|
ok: false,
|
|
errMsg: "path contains member of map field 'filter_metadata'; map field member access is not supported",
|
|
},
|
|
}
|
|
|
|
copyMessage := func(m proto.Message) proto.Message { return m }
|
|
for n, tc := range cases {
|
|
t.Run(n, func(t *testing.T) {
|
|
// Copy k so that we can compare before and after
|
|
copyOfK := copyMessage(tc.args.k)
|
|
var err error
|
|
for _, p := range tc.args.patches {
|
|
// Repeatedly patch value, replacing with the new version each time
|
|
copyOfK, err = PatchStruct(copyOfK, p, tc.args.debug)
|
|
if err != nil {
|
|
break // Break on the first error
|
|
}
|
|
}
|
|
if tc.ok {
|
|
require.NoError(t, err)
|
|
if diff := cmp.Diff(tc.expected, copyOfK, protocmp.Transform()); diff != "" {
|
|
t.Errorf("unexpected difference:\n%v", diff)
|
|
}
|
|
} else {
|
|
require.Error(t, err)
|
|
if tc.errMsg != "" {
|
|
require.ErrorContains(t, err, tc.errMsg)
|
|
}
|
|
if tc.errFunc != nil {
|
|
tc.errFunc(t, err)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|