mirror of https://github.com/hashicorp/consul
890 lines
17 KiB
Go
890 lines
17 KiB
Go
package structs
|
|
|
|
import (
|
|
"bytes"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/hashicorp/go-msgpack/codec"
|
|
"github.com/hashicorp/hcl"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// TestDecodeConfigEntry is the 'structs' mirror image of
|
|
// command/config/write/config_write_test.go:TestParseConfigEntry
|
|
func TestDecodeConfigEntry(t *testing.T) {
|
|
|
|
for _, tc := range []struct {
|
|
name string
|
|
camel string
|
|
snake string
|
|
expect ConfigEntry
|
|
expectErr string
|
|
}{
|
|
// TODO(rb): test json?
|
|
{
|
|
name: "proxy-defaults: extra fields or typo",
|
|
snake: `
|
|
kind = "proxy-defaults"
|
|
name = "main"
|
|
cornfig {
|
|
"foo" = 19
|
|
}
|
|
`,
|
|
camel: `
|
|
Kind = "proxy-defaults"
|
|
Name = "main"
|
|
Cornfig {
|
|
"foo" = 19
|
|
}
|
|
`,
|
|
expectErr: `invalid config key "cornfig"`,
|
|
},
|
|
{
|
|
name: "proxy-defaults",
|
|
snake: `
|
|
kind = "proxy-defaults"
|
|
name = "main"
|
|
config {
|
|
"foo" = 19
|
|
"bar" = "abc"
|
|
"moreconfig" {
|
|
"moar" = "config"
|
|
}
|
|
}
|
|
mesh_gateway {
|
|
mode = "remote"
|
|
}
|
|
`,
|
|
camel: `
|
|
Kind = "proxy-defaults"
|
|
Name = "main"
|
|
Config {
|
|
"foo" = 19
|
|
"bar" = "abc"
|
|
"moreconfig" {
|
|
"moar" = "config"
|
|
}
|
|
}
|
|
MeshGateway {
|
|
Mode = "remote"
|
|
}
|
|
`,
|
|
expect: &ProxyConfigEntry{
|
|
Kind: "proxy-defaults",
|
|
Name: "main",
|
|
Config: map[string]interface{}{
|
|
"foo": 19,
|
|
"bar": "abc",
|
|
"moreconfig": map[string]interface{}{
|
|
"moar": "config",
|
|
},
|
|
},
|
|
MeshGateway: MeshGatewayConfig{
|
|
Mode: MeshGatewayModeRemote,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "namespaces invalid top level",
|
|
snake: `
|
|
kind = "terminating-gateway"
|
|
name = "terminating-gateway"
|
|
namespace = "foo"
|
|
`,
|
|
camel: `
|
|
Kind = "terminating-gateway"
|
|
Name = "terminating-gateway"
|
|
Namespace = "foo"
|
|
`,
|
|
expectErr: `invalid config key "namespace", namespaces are a consul enterprise feature`,
|
|
},
|
|
{
|
|
name: "namespaces invalid deep",
|
|
snake: `
|
|
kind = "ingress-gateway"
|
|
name = "ingress-web"
|
|
listeners = [
|
|
{
|
|
port = 8080
|
|
protocol = "http"
|
|
services = [
|
|
{
|
|
name = "web"
|
|
hosts = ["test.example.com", "test2.example.com"]
|
|
namespace = "frontend"
|
|
},
|
|
]
|
|
}
|
|
]
|
|
`,
|
|
camel: `
|
|
Kind = "ingress-gateway"
|
|
Name = "ingress-web"
|
|
Namespace = "blah"
|
|
Listeners = [
|
|
{
|
|
Port = 8080
|
|
Protocol = "http"
|
|
Services = [
|
|
{
|
|
Name = "web"
|
|
Hosts = ["test.example.com", "test2.example.com"]
|
|
Namespace = "frontend"
|
|
},
|
|
]
|
|
},
|
|
]
|
|
`,
|
|
expectErr: `invalid config key "listeners[0].services[0].namespace", namespaces are a consul enterprise feature`,
|
|
},
|
|
{
|
|
name: "service-defaults",
|
|
snake: `
|
|
kind = "service-defaults"
|
|
name = "main"
|
|
protocol = "http"
|
|
external_sni = "abc-123"
|
|
mesh_gateway {
|
|
mode = "remote"
|
|
}
|
|
`,
|
|
camel: `
|
|
Kind = "service-defaults"
|
|
Name = "main"
|
|
Protocol = "http"
|
|
ExternalSNI = "abc-123"
|
|
MeshGateway {
|
|
Mode = "remote"
|
|
}
|
|
`,
|
|
expect: &ServiceConfigEntry{
|
|
Kind: "service-defaults",
|
|
Name: "main",
|
|
Protocol: "http",
|
|
ExternalSNI: "abc-123",
|
|
MeshGateway: MeshGatewayConfig{
|
|
Mode: MeshGatewayModeRemote,
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "service-router: kitchen sink",
|
|
snake: `
|
|
kind = "service-router"
|
|
name = "main"
|
|
routes = [
|
|
{
|
|
match {
|
|
http {
|
|
path_exact = "/foo"
|
|
header = [
|
|
{
|
|
name = "debug1"
|
|
present = true
|
|
},
|
|
{
|
|
name = "debug2"
|
|
present = false
|
|
invert = true
|
|
},
|
|
{
|
|
name = "debug3"
|
|
exact = "1"
|
|
},
|
|
{
|
|
name = "debug4"
|
|
prefix = "aaa"
|
|
},
|
|
{
|
|
name = "debug5"
|
|
suffix = "bbb"
|
|
},
|
|
{
|
|
name = "debug6"
|
|
regex = "a.*z"
|
|
},
|
|
]
|
|
}
|
|
}
|
|
destination {
|
|
service = "carrot"
|
|
service_subset = "kale"
|
|
namespace = "leek"
|
|
prefix_rewrite = "/alternate"
|
|
request_timeout = "99s"
|
|
num_retries = 12345
|
|
retry_on_connect_failure = true
|
|
retry_on_status_codes = [401, 209]
|
|
}
|
|
},
|
|
{
|
|
match {
|
|
http {
|
|
path_prefix = "/foo"
|
|
methods = [ "GET", "DELETE" ]
|
|
query_param = [
|
|
{
|
|
name = "hack1"
|
|
present = true
|
|
},
|
|
{
|
|
name = "hack2"
|
|
exact = "1"
|
|
},
|
|
{
|
|
name = "hack3"
|
|
regex = "a.*z"
|
|
},
|
|
]
|
|
}
|
|
}
|
|
},
|
|
{
|
|
match {
|
|
http {
|
|
path_regex = "/foo"
|
|
}
|
|
}
|
|
},
|
|
]
|
|
`,
|
|
camel: `
|
|
Kind = "service-router"
|
|
Name = "main"
|
|
Routes = [
|
|
{
|
|
Match {
|
|
HTTP {
|
|
PathExact = "/foo"
|
|
Header = [
|
|
{
|
|
Name = "debug1"
|
|
Present = true
|
|
},
|
|
{
|
|
Name = "debug2"
|
|
Present = false
|
|
Invert = true
|
|
},
|
|
{
|
|
Name = "debug3"
|
|
Exact = "1"
|
|
},
|
|
{
|
|
Name = "debug4"
|
|
Prefix = "aaa"
|
|
},
|
|
{
|
|
Name = "debug5"
|
|
Suffix = "bbb"
|
|
},
|
|
{
|
|
Name = "debug6"
|
|
Regex = "a.*z"
|
|
},
|
|
]
|
|
}
|
|
}
|
|
Destination {
|
|
Service = "carrot"
|
|
ServiceSubset = "kale"
|
|
Namespace = "leek"
|
|
PrefixRewrite = "/alternate"
|
|
RequestTimeout = "99s"
|
|
NumRetries = 12345
|
|
RetryOnConnectFailure = true
|
|
RetryOnStatusCodes = [401, 209]
|
|
}
|
|
},
|
|
{
|
|
Match {
|
|
HTTP {
|
|
PathPrefix = "/foo"
|
|
Methods = [ "GET", "DELETE" ]
|
|
QueryParam = [
|
|
{
|
|
Name = "hack1"
|
|
Present = true
|
|
},
|
|
{
|
|
Name = "hack2"
|
|
Exact = "1"
|
|
},
|
|
{
|
|
Name = "hack3"
|
|
Regex = "a.*z"
|
|
},
|
|
]
|
|
}
|
|
}
|
|
},
|
|
{
|
|
Match {
|
|
HTTP {
|
|
PathRegex = "/foo"
|
|
}
|
|
}
|
|
},
|
|
]
|
|
`,
|
|
expect: &ServiceRouterConfigEntry{
|
|
Kind: "service-router",
|
|
Name: "main",
|
|
Routes: []ServiceRoute{
|
|
{
|
|
Match: &ServiceRouteMatch{
|
|
HTTP: &ServiceRouteHTTPMatch{
|
|
PathExact: "/foo",
|
|
Header: []ServiceRouteHTTPMatchHeader{
|
|
{
|
|
Name: "debug1",
|
|
Present: true,
|
|
},
|
|
{
|
|
Name: "debug2",
|
|
Present: false,
|
|
Invert: true,
|
|
},
|
|
{
|
|
Name: "debug3",
|
|
Exact: "1",
|
|
},
|
|
{
|
|
Name: "debug4",
|
|
Prefix: "aaa",
|
|
},
|
|
{
|
|
Name: "debug5",
|
|
Suffix: "bbb",
|
|
},
|
|
{
|
|
Name: "debug6",
|
|
Regex: "a.*z",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
Destination: &ServiceRouteDestination{
|
|
Service: "carrot",
|
|
ServiceSubset: "kale",
|
|
Namespace: "leek",
|
|
PrefixRewrite: "/alternate",
|
|
RequestTimeout: 99 * time.Second,
|
|
NumRetries: 12345,
|
|
RetryOnConnectFailure: true,
|
|
RetryOnStatusCodes: []uint32{401, 209},
|
|
},
|
|
},
|
|
{
|
|
Match: &ServiceRouteMatch{
|
|
HTTP: &ServiceRouteHTTPMatch{
|
|
PathPrefix: "/foo",
|
|
Methods: []string{"GET", "DELETE"},
|
|
QueryParam: []ServiceRouteHTTPMatchQueryParam{
|
|
{
|
|
Name: "hack1",
|
|
Present: true,
|
|
},
|
|
{
|
|
Name: "hack2",
|
|
Exact: "1",
|
|
},
|
|
{
|
|
Name: "hack3",
|
|
Regex: "a.*z",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Match: &ServiceRouteMatch{
|
|
HTTP: &ServiceRouteHTTPMatch{
|
|
PathRegex: "/foo",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "service-splitter: kitchen sink",
|
|
snake: `
|
|
kind = "service-splitter"
|
|
name = "main"
|
|
splits = [
|
|
{
|
|
weight = 99.1
|
|
service_subset = "v1"
|
|
},
|
|
{
|
|
weight = 0.9
|
|
service = "other"
|
|
namespace = "alt"
|
|
},
|
|
]
|
|
`,
|
|
camel: `
|
|
Kind = "service-splitter"
|
|
Name = "main"
|
|
Splits = [
|
|
{
|
|
Weight = 99.1
|
|
ServiceSubset = "v1"
|
|
},
|
|
{
|
|
Weight = 0.9
|
|
Service = "other"
|
|
Namespace = "alt"
|
|
},
|
|
]
|
|
`,
|
|
expect: &ServiceSplitterConfigEntry{
|
|
Kind: ServiceSplitter,
|
|
Name: "main",
|
|
Splits: []ServiceSplit{
|
|
{
|
|
Weight: 99.1,
|
|
ServiceSubset: "v1",
|
|
},
|
|
{
|
|
Weight: 0.9,
|
|
Service: "other",
|
|
Namespace: "alt",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "service-resolver: subsets with failover",
|
|
snake: `
|
|
kind = "service-resolver"
|
|
name = "main"
|
|
default_subset = "v1"
|
|
connect_timeout = "15s"
|
|
subsets = {
|
|
"v1" = {
|
|
filter = "Service.Meta.version == v1"
|
|
},
|
|
"v2" = {
|
|
filter = "Service.Meta.version == v2"
|
|
only_passing = true
|
|
},
|
|
}
|
|
failover = {
|
|
"v2" = {
|
|
service = "failcopy"
|
|
service_subset = "sure"
|
|
namespace = "neighbor"
|
|
datacenters = ["dc5", "dc14"]
|
|
},
|
|
"*" = {
|
|
datacenters = ["dc7"]
|
|
}
|
|
}`,
|
|
camel: `
|
|
Kind = "service-resolver"
|
|
Name = "main"
|
|
DefaultSubset = "v1"
|
|
ConnectTimeout = "15s"
|
|
Subsets = {
|
|
"v1" = {
|
|
Filter = "Service.Meta.version == v1"
|
|
},
|
|
"v2" = {
|
|
Filter = "Service.Meta.version == v2"
|
|
OnlyPassing = true
|
|
},
|
|
}
|
|
Failover = {
|
|
"v2" = {
|
|
Service = "failcopy"
|
|
ServiceSubset = "sure"
|
|
Namespace = "neighbor"
|
|
Datacenters = ["dc5", "dc14"]
|
|
},
|
|
"*" = {
|
|
Datacenters = ["dc7"]
|
|
}
|
|
}`,
|
|
expect: &ServiceResolverConfigEntry{
|
|
Kind: "service-resolver",
|
|
Name: "main",
|
|
DefaultSubset: "v1",
|
|
ConnectTimeout: 15 * time.Second,
|
|
Subsets: map[string]ServiceResolverSubset{
|
|
"v1": {
|
|
Filter: "Service.Meta.version == v1",
|
|
},
|
|
"v2": {
|
|
Filter: "Service.Meta.version == v2",
|
|
OnlyPassing: true,
|
|
},
|
|
},
|
|
Failover: map[string]ServiceResolverFailover{
|
|
"v2": {
|
|
Service: "failcopy",
|
|
ServiceSubset: "sure",
|
|
Namespace: "neighbor",
|
|
Datacenters: []string{"dc5", "dc14"},
|
|
},
|
|
"*": {
|
|
Datacenters: []string{"dc7"},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "service-resolver: redirect",
|
|
snake: `
|
|
kind = "service-resolver"
|
|
name = "main"
|
|
redirect {
|
|
service = "other"
|
|
service_subset = "backup"
|
|
namespace = "alt"
|
|
datacenter = "dc9"
|
|
}
|
|
`,
|
|
camel: `
|
|
Kind = "service-resolver"
|
|
Name = "main"
|
|
Redirect {
|
|
Service = "other"
|
|
ServiceSubset = "backup"
|
|
Namespace = "alt"
|
|
Datacenter = "dc9"
|
|
}
|
|
`,
|
|
expect: &ServiceResolverConfigEntry{
|
|
Kind: "service-resolver",
|
|
Name: "main",
|
|
Redirect: &ServiceResolverRedirect{
|
|
Service: "other",
|
|
ServiceSubset: "backup",
|
|
Namespace: "alt",
|
|
Datacenter: "dc9",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "service-resolver: default",
|
|
snake: `
|
|
kind = "service-resolver"
|
|
name = "main"
|
|
`,
|
|
camel: `
|
|
Kind = "service-resolver"
|
|
Name = "main"
|
|
`,
|
|
expect: &ServiceResolverConfigEntry{
|
|
Kind: "service-resolver",
|
|
Name: "main",
|
|
},
|
|
},
|
|
{
|
|
name: "ingress-gateway: kitchen sink",
|
|
snake: `
|
|
kind = "ingress-gateway"
|
|
name = "ingress-web"
|
|
|
|
tls {
|
|
enabled = true
|
|
}
|
|
|
|
listeners = [
|
|
{
|
|
port = 8080
|
|
protocol = "http"
|
|
services = [
|
|
{
|
|
name = "web"
|
|
hosts = ["test.example.com", "test2.example.com"]
|
|
},
|
|
{
|
|
name = "db"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
port = 9999
|
|
protocol = "tcp"
|
|
services = [
|
|
{
|
|
name = "mysql"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
port = 2234
|
|
protocol = "tcp"
|
|
services = [
|
|
{
|
|
name = "postgres"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
`,
|
|
camel: `
|
|
Kind = "ingress-gateway"
|
|
Name = "ingress-web"
|
|
TLS {
|
|
Enabled = true
|
|
}
|
|
Listeners = [
|
|
{
|
|
Port = 8080
|
|
Protocol = "http"
|
|
Services = [
|
|
{
|
|
Name = "web"
|
|
Hosts = ["test.example.com", "test2.example.com"]
|
|
},
|
|
{
|
|
Name = "db"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
Port = 9999
|
|
Protocol = "tcp"
|
|
Services = [
|
|
{
|
|
Name = "mysql"
|
|
}
|
|
]
|
|
},
|
|
{
|
|
Port = 2234
|
|
Protocol = "tcp"
|
|
Services = [
|
|
{
|
|
Name = "postgres"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
`,
|
|
expect: &IngressGatewayConfigEntry{
|
|
Kind: "ingress-gateway",
|
|
Name: "ingress-web",
|
|
TLS: GatewayTLSConfig{
|
|
Enabled: true,
|
|
},
|
|
Listeners: []IngressListener{
|
|
{
|
|
Port: 8080,
|
|
Protocol: "http",
|
|
Services: []IngressService{
|
|
{
|
|
Name: "web",
|
|
Hosts: []string{"test.example.com", "test2.example.com"},
|
|
},
|
|
{
|
|
Name: "db",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Port: 9999,
|
|
Protocol: "tcp",
|
|
Services: []IngressService{
|
|
{
|
|
Name: "mysql",
|
|
},
|
|
},
|
|
},
|
|
{
|
|
Port: 2234,
|
|
Protocol: "tcp",
|
|
Services: []IngressService{
|
|
{
|
|
Name: "postgres",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
name: "terminating-gateway: kitchen sink",
|
|
snake: `
|
|
kind = "terminating-gateway"
|
|
name = "terminating-gw-west"
|
|
services = [
|
|
{
|
|
name = "payments",
|
|
ca_file = "/etc/payments/ca.pem",
|
|
cert_file = "/etc/payments/cert.pem",
|
|
key_file = "/etc/payments/tls.key",
|
|
sni = "mydomain",
|
|
},
|
|
{
|
|
name = "*",
|
|
ca_file = "/etc/all/ca.pem",
|
|
cert_file = "/etc/all/cert.pem",
|
|
key_file = "/etc/all/tls.key",
|
|
sni = "my-alt-domain",
|
|
},
|
|
]
|
|
`,
|
|
camel: `
|
|
Kind = "terminating-gateway"
|
|
Name = "terminating-gw-west"
|
|
Services = [
|
|
{
|
|
Name = "payments",
|
|
CAFile = "/etc/payments/ca.pem",
|
|
CertFile = "/etc/payments/cert.pem",
|
|
KeyFile = "/etc/payments/tls.key",
|
|
SNI = "mydomain",
|
|
},
|
|
{
|
|
Name = "*",
|
|
CAFile = "/etc/all/ca.pem",
|
|
CertFile = "/etc/all/cert.pem",
|
|
KeyFile = "/etc/all/tls.key",
|
|
SNI = "my-alt-domain",
|
|
},
|
|
]
|
|
`,
|
|
expect: &TerminatingGatewayConfigEntry{
|
|
Kind: "terminating-gateway",
|
|
Name: "terminating-gw-west",
|
|
Services: []LinkedService{
|
|
{
|
|
Name: "payments",
|
|
CAFile: "/etc/payments/ca.pem",
|
|
CertFile: "/etc/payments/cert.pem",
|
|
KeyFile: "/etc/payments/tls.key",
|
|
SNI: "mydomain",
|
|
},
|
|
{
|
|
Name: "*",
|
|
CAFile: "/etc/all/ca.pem",
|
|
CertFile: "/etc/all/cert.pem",
|
|
KeyFile: "/etc/all/tls.key",
|
|
SNI: "my-alt-domain",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
} {
|
|
tc := tc
|
|
|
|
testbody := func(t *testing.T, body string) {
|
|
t.Helper()
|
|
|
|
var raw map[string]interface{}
|
|
err := hcl.Decode(&raw, body)
|
|
require.NoError(t, err)
|
|
|
|
got, err := DecodeConfigEntry(raw)
|
|
if tc.expectErr != "" {
|
|
require.Nil(t, got)
|
|
require.Error(t, err)
|
|
requireContainsLower(t, err.Error(), tc.expectErr)
|
|
} else {
|
|
require.NoError(t, err)
|
|
require.Equal(t, tc.expect, got)
|
|
}
|
|
}
|
|
|
|
t.Run(tc.name+" (snake case)", func(t *testing.T) {
|
|
testbody(t, tc.snake)
|
|
})
|
|
t.Run(tc.name+" (camel case)", func(t *testing.T) {
|
|
testbody(t, tc.camel)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestServiceConfigResponse_MsgPack(t *testing.T) {
|
|
// TODO(banks) lib.MapWalker doesn't actually fix the map[interface{}] issue
|
|
// it claims to in docs yet. When it does uncomment those cases below.
|
|
a := ServiceConfigResponse{
|
|
ProxyConfig: map[string]interface{}{
|
|
"string": "foo",
|
|
// "map": map[string]interface{}{
|
|
// "baz": "bar",
|
|
// },
|
|
},
|
|
UpstreamConfigs: map[string]map[string]interface{}{
|
|
"a": {
|
|
"string": "aaaa",
|
|
// "map": map[string]interface{}{
|
|
// "baz": "aa",
|
|
// },
|
|
},
|
|
"b": {
|
|
"string": "bbbb",
|
|
// "map": map[string]interface{}{
|
|
// "baz": "bb",
|
|
// },
|
|
},
|
|
},
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
|
|
// Encode as msgPack using a regular handle i.e. NOT one with RawAsString
|
|
// since our RPC codec doesn't use that.
|
|
enc := codec.NewEncoder(&buf, MsgpackHandle)
|
|
require.NoError(t, enc.Encode(&a))
|
|
|
|
var b ServiceConfigResponse
|
|
|
|
dec := codec.NewDecoder(&buf, MsgpackHandle)
|
|
require.NoError(t, dec.Decode(&b))
|
|
|
|
require.Equal(t, a, b)
|
|
}
|
|
|
|
func TestConfigEntryResponseMarshalling(t *testing.T) {
|
|
|
|
cases := map[string]ConfigEntryResponse{
|
|
"nil entry": {},
|
|
"proxy-default entry": {
|
|
Entry: &ProxyConfigEntry{
|
|
Kind: ProxyDefaults,
|
|
Name: ProxyConfigGlobal,
|
|
Config: map[string]interface{}{
|
|
"foo": "bar",
|
|
},
|
|
},
|
|
},
|
|
"service-default entry": {
|
|
Entry: &ServiceConfigEntry{
|
|
Kind: ServiceDefaults,
|
|
Name: "foo",
|
|
Protocol: "tcp",
|
|
// Connect: ConnectConfiguration{SideCarProxy: true},
|
|
},
|
|
},
|
|
}
|
|
|
|
for name, tcase := range cases {
|
|
name := name
|
|
tcase := tcase
|
|
t.Run(name, func(t *testing.T) {
|
|
|
|
data, err := tcase.MarshalBinary()
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, data)
|
|
|
|
var resp ConfigEntryResponse
|
|
require.NoError(t, resp.UnmarshalBinary(data))
|
|
|
|
require.Equal(t, tcase, resp)
|
|
})
|
|
}
|
|
}
|
|
|
|
func requireContainsLower(t *testing.T, haystack, needle string) {
|
|
t.Helper()
|
|
require.Contains(t, strings.ToLower(haystack), strings.ToLower(needle))
|
|
}
|