open-consul/agent/http_decode_test.go
Daniel Nephin 7bc3d09857 Remove deadcode
This UnmarshalJSON was never called. The decode function is passed a map[string]interface
so it has no way of knowing that this function exists.

Tested by adding a panic to this function and watching the tests pass.

I attempted to use this Unmarshal function by passing in the type, however the tests
showed that it does not work. The test was failing to parse the request.

If the performance of this endpoint is indeed critical we can solve the problem by adding
all the fields to the request struct and handling the normalziation without a custom Unmarshal.
2020-04-22 16:48:28 -04:00

2553 lines
72 KiB
Go

package agent
// This file contains tests for JSON unmarshaling.
// These tests were originally written as regression tests to capture existing decoding behavior
// when we moved from mapstructure to encoding/json as a JSON decoder.
// See https://github.com/hashicorp/consul/pull/6624.
//
// Most likely, if you are adding new tests, you will only need to check your struct
// for the special values in 'translateValueTestCases' (time.Durations, etc).
// You can easily copy the structure of an existing test such as
// 'TestDecodeACLPolicyWrite'.
//
// There are two main categories of tests in this file:
//
// 1. translateValueTestCase: test decoding of special values such as:
// - time.Duration
// - api.ReadableDuration
// - time.Time
// - Hash []byte
//
// 2. translateKeyTestCase: test decoding with alias keys such as "FooBar" => "foo_bar" (see lib.TranslateKeys)
// For these test cases, one must write an 'equalityFn' which takes an output interface{} (struct, usually)
// as well as 'want' interface{} value, and returns an error if the test
// condition failed, or nil if it passed.
//
// There are some test cases which are easily generalizable, and have been pulled
// out of the scope of a single test so that many tests may use them.
// These include the durationTestCases, hashTestCases etc (value) as well as
// some common field alias translations, such as translateScriptArgsTCs for
// CheckTypes.
//
import (
"bytes"
"fmt"
"strings"
"testing"
"time"
"github.com/hashicorp/consul/agent/structs"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/consul/types"
)
// =======================================================
// TranslateValues:
// =======================================================
type translateValueTestCase struct {
desc string
timestamps *timestampTC
durations *durationTC
hashes *hashTC
wantErr bool
}
type timestampTC struct {
in string
want time.Time
}
type durationTC struct {
in string
want time.Duration
}
type hashTC struct {
in string
want []byte
}
var durationTestCases = append(positiveDurationTCs, negativeDurationTCs...)
var translateValueTestCases = append(append(
timestampTestCases,
durationTestCases...),
hashTestCases...)
var hashTestCases = []translateValueTestCase{
{
desc: "hashes base64 encoded",
hashes: &hashTC{
in: `"c29tZXRoaW5nIHdpY2tlZCB0aGlzIHdheSBjb21lcw=="`,
want: []byte("c29tZXRoaW5nIHdpY2tlZCB0aGlzIHdheSBjb21lcw=="),
},
},
{
desc: "hashes not-base64 encoded",
hashes: &hashTC{
in: `"something wicked this way comes"`,
want: []byte("something wicked this way comes"),
},
},
{
desc: "hashes empty string",
hashes: &hashTC{
in: `""`,
want: []byte{},
},
},
{
desc: "hashes null",
hashes: &hashTC{
in: `null`,
want: []byte{},
},
},
{
desc: "hashes numeric value",
hashes: &hashTC{
in: `100`,
},
wantErr: true,
},
}
var timestampTestCases = []translateValueTestCase{
{
desc: "timestamps correctly RFC3339 formatted",
timestamps: &timestampTC{
in: `"2020-01-02T15:04:05Z"`,
want: time.Date(2020, 01, 02, 15, 4, 5, 0, time.UTC),
},
},
{
desc: "timestamps incorrectly formatted (RFC822)",
timestamps: &timestampTC{
in: `"02 Jan 21 15:04"`,
},
wantErr: true,
},
{
desc: "timestamps incorrectly formatted (RFC850)",
timestamps: &timestampTC{
in: `"Monday, 02-Jan-20 15:04:05"`,
},
wantErr: true,
},
{
desc: "timestamps empty string",
timestamps: &timestampTC{
in: `""`,
},
wantErr: true,
},
{
desc: "timestamps null",
timestamps: &timestampTC{
in: `null`,
want: time.Time{},
},
},
}
var positiveDurationTCs = []translateValueTestCase{
{
desc: "durations correctly formatted",
durations: &durationTC{
in: `"2h0m15s"`,
want: (2*time.Hour + 15*time.Second),
},
},
{
desc: "durations small, correctly formatted",
durations: &durationTC{
in: `"50ms"`,
want: (50 * time.Millisecond),
},
},
{
desc: "durations incorrectly formatted",
durations: &durationTC{
in: `"x2h0m0s"`,
},
wantErr: true,
},
{
desc: "durations empty string",
durations: &durationTC{
in: `""`,
},
wantErr: true,
},
{
desc: "durations string without quotes",
durations: &durationTC{
in: `2h5m`,
},
wantErr: true,
},
{
desc: "durations numeric",
durations: &durationTC{
in: `2000`,
want: time.Duration(2000),
},
},
}
// Separate these negative value test cases out from others b/c some
// cases do not handle negative values correctly. This way some tests
// can write their own testCases for negative values.
var negativeDurationTCs = []translateValueTestCase{
{
desc: "durations negative",
durations: &durationTC{
in: `"-50ms"`,
want: -50 * time.Millisecond,
},
},
{
desc: "durations numeric and negative",
durations: &durationTC{
in: `-2000`,
want: time.Duration(-2000),
},
},
}
var checkTypeHeaderTestCases = []struct {
desc string
in string
want map[string][]string
wantErr bool
}{
{
desc: "filled in map",
in: `{"a": ["aa", "aaa"], "b": ["bb", "bbb", "bbbb"], "c": [], "d": ["dd"]}`,
want: map[string][]string{
"a": []string{"aa", "aaa"},
"b": []string{"bb", "bbb", "bbbb"},
"d": []string{"dd"},
},
},
{
desc: "empty map",
in: `{}`,
want: map[string][]string{},
},
{
desc: "empty map",
in: `null`,
want: map[string][]string{},
},
{
desc: "malformatted map",
in: `{"a": "aa"}`,
wantErr: true,
},
{
desc: "not a map (slice)",
in: `["a", "b"]`,
wantErr: true,
},
{
desc: "not a map (int)",
in: `1`,
wantErr: true,
},
}
// =======================================================
// TranslateKeys:
// =======================================================
type translateKeyTestCase struct {
jsonFmtStr string
desc string
in []interface{}
want interface{}
equalityFn func(outStruct, wantVal interface{}) error
}
// FixupCheckType's Translate Keys:
// lib.TranslateKeys(rawMap, map[string]string{
// "args": "ScriptArgs",
// "script_args": "ScriptArgs",
// "deregister_critical_service_after": "DeregisterCriticalServiceAfter",
// "docker_container_id": "DockerContainerID",
// "tls_skip_verify": "TLSSkipVerify",
// "service_id": "ServiceID",
var translateCheckTypeTCs = [][]translateKeyTestCase{
translateScriptArgsTCs,
translateDeregisterTCs,
translateDockerTCs,
translateTLSTCs,
translateServiceIDTCs,
}
// ScriptArgs: []string
func scriptArgsEqFn(out interface{}, want interface{}) error {
var got []string
switch v := out.(type) {
case structs.CheckDefinition:
got = v.ScriptArgs
case *structs.CheckDefinition:
got = v.ScriptArgs
case structs.CheckType:
got = v.ScriptArgs
case *structs.CheckType:
got = v.ScriptArgs
case structs.HealthCheckDefinition:
got = v.ScriptArgs
case *structs.HealthCheckDefinition:
got = v.ScriptArgs
default:
panic(fmt.Sprintf("unexpected type %T", out))
}
wantSlice := want.([]string)
if len(got) != len(wantSlice) {
return fmt.Errorf("ScriptArgs: expected %v, got %v", wantSlice, got)
}
for i := range got {
if got[i] != wantSlice[i] {
return fmt.Errorf("ScriptArgs: [i=%d] expected %v, got %v", i, wantSlice, got)
}
}
return nil
}
var scriptFields = []string{
`"ScriptArgs": %s`,
`"args": %s`,
`"script_args": %s`,
}
var translateScriptArgsTCs = []translateKeyTestCase{
{
desc: "scriptArgs: all set",
in: []interface{}{`["1"]`, `["2"]`, `["3"]`},
want: []string{"1"},
jsonFmtStr: "{" + strings.Join(scriptFields, ",") + "}",
equalityFn: scriptArgsEqFn,
},
{
desc: "scriptArgs: first and second set",
in: []interface{}{`["1"]`, `["2"]`},
want: []string{"1"},
jsonFmtStr: "{" + scriptFields[0] + "," + scriptFields[1] + "}",
equalityFn: scriptArgsEqFn,
},
{
desc: "scriptArgs: first and third set",
in: []interface{}{`["1"]`, `["3"]`},
want: []string{"1"},
jsonFmtStr: "{" + scriptFields[0] + "," + scriptFields[2] + "}",
equalityFn: scriptArgsEqFn,
},
{
desc: "scriptArgs: second and third set",
in: []interface{}{`["2"]`, `["3"]`},
want: []string{"2"},
jsonFmtStr: "{" + scriptFields[1] + "," + scriptFields[2] + "}",
equalityFn: scriptArgsEqFn,
},
{
desc: "scriptArgs: first set",
in: []interface{}{`["1"]`},
want: []string{"1"},
jsonFmtStr: "{" + scriptFields[0] + "}",
equalityFn: scriptArgsEqFn,
},
{
desc: "scriptArgs: second set",
in: []interface{}{`["2"]`},
want: []string{"2"},
jsonFmtStr: "{" + scriptFields[1] + "}",
equalityFn: scriptArgsEqFn,
},
{
desc: "scriptArgs: third set",
in: []interface{}{`["3"]`},
want: []string{"3"},
jsonFmtStr: "{" + scriptFields[2] + "}",
equalityFn: scriptArgsEqFn,
},
{
desc: "scriptArgs: none set",
in: []interface{}{},
want: []string{},
jsonFmtStr: "{}",
equalityFn: scriptArgsEqFn,
},
}
func deregisterEqFn(out interface{}, want interface{}) error {
var got interface{}
switch v := out.(type) {
case structs.CheckDefinition:
got = v.DeregisterCriticalServiceAfter
case *structs.CheckDefinition:
got = v.DeregisterCriticalServiceAfter
case structs.CheckType:
got = v.DeregisterCriticalServiceAfter
case *structs.CheckType:
got = v.DeregisterCriticalServiceAfter
case structs.HealthCheckDefinition:
got = v.DeregisterCriticalServiceAfter
case *structs.HealthCheckDefinition:
got = v.DeregisterCriticalServiceAfter
default:
panic(fmt.Sprintf("unexpected type %T", out))
}
if got != want {
return fmt.Errorf("expected DeregisterCriticalServiceAfter to be %s, got %s", want, got)
}
return nil
}
var deregisterFields = []string{
`"DeregisterCriticalServiceAfter": %s`,
`"deregister_critical_service_after": %s`,
}
var translateDeregisterTCs = []translateKeyTestCase{
{
desc: "deregister: both set",
in: []interface{}{`"2h0m"`, `"3h0m"`},
want: 2 * time.Hour,
jsonFmtStr: "{" + strings.Join(deregisterFields, ",") + "}",
equalityFn: deregisterEqFn,
},
{
desc: "deregister: first set",
in: []interface{}{`"2h0m"`},
want: 2 * time.Hour,
jsonFmtStr: "{" + deregisterFields[0] + "}",
equalityFn: deregisterEqFn,
},
{
desc: "deregister: second set",
in: []interface{}{`"3h0m"`},
want: 3 * time.Hour,
jsonFmtStr: "{" + deregisterFields[1] + "}",
equalityFn: deregisterEqFn,
},
{
desc: "deregister: neither set",
in: []interface{}{},
want: time.Duration(0),
jsonFmtStr: "{}",
equalityFn: deregisterEqFn,
},
}
// DockerContainerID: string
func dockerEqFn(out interface{}, want interface{}) error {
var got interface{}
switch v := out.(type) {
case structs.CheckDefinition:
got = v.DockerContainerID
case *structs.CheckDefinition:
got = v.DockerContainerID
case structs.CheckType:
got = v.DockerContainerID
case *structs.CheckType:
got = v.DockerContainerID
case structs.HealthCheckDefinition:
got = v.DockerContainerID
case *structs.HealthCheckDefinition:
got = v.DockerContainerID
default:
panic(fmt.Sprintf("unexpected type %T", out))
}
if got != want {
return fmt.Errorf("expected DockerContainerID to be %s, got %s", want, got)
}
return nil
}
var dockerFields = []string{`"DockerContainerID": %s`, `"docker_container_id": %s`}
var translateDockerTCs = []translateKeyTestCase{
{
desc: "dockerContainerID: both set",
in: []interface{}{`"id-1"`, `"id-2"`},
want: "id-1",
jsonFmtStr: "{" + strings.Join(dockerFields, ",") + "}",
equalityFn: dockerEqFn,
},
{
desc: "dockerContainerID: first set",
in: []interface{}{`"id-1"`},
want: "id-1",
jsonFmtStr: "{" + dockerFields[0] + "}",
equalityFn: dockerEqFn,
},
{
desc: "dockerContainerID: second set",
in: []interface{}{`"id-2"`},
want: "id-2",
jsonFmtStr: "{" + dockerFields[1] + "}",
equalityFn: dockerEqFn,
},
{
desc: "dockerContainerID: neither set",
in: []interface{}{},
want: "", // zero value
jsonFmtStr: "{}",
equalityFn: dockerEqFn,
},
}
// TLSSkipVerify: bool
func tlsEqFn(out interface{}, want interface{}) error {
var got interface{}
switch v := out.(type) {
case structs.CheckDefinition:
got = v.TLSSkipVerify
case *structs.CheckDefinition:
got = v.TLSSkipVerify
case structs.CheckType:
got = v.TLSSkipVerify
case *structs.CheckType:
got = v.TLSSkipVerify
case structs.HealthCheckDefinition:
got = v.TLSSkipVerify
case *structs.HealthCheckDefinition:
got = v.TLSSkipVerify
default:
panic(fmt.Sprintf("unexpected type %T", out))
}
if got != want {
return fmt.Errorf("expected TLSSkipVerify to be %v, got %v", want, got)
}
return nil
}
var tlsFields = []string{`"TLSSkipVerify": %s`, `"tls_skip_verify": %s`}
var translateTLSTCs = []translateKeyTestCase{
{
desc: "tlsSkipVerify: both set",
in: []interface{}{`true`, `false`},
want: true,
jsonFmtStr: "{" + strings.Join(tlsFields, ",") + "}",
equalityFn: tlsEqFn,
},
{
desc: "tlsSkipVerify: first set",
in: []interface{}{`true`},
want: true,
jsonFmtStr: "{" + tlsFields[0] + "}",
equalityFn: tlsEqFn,
},
{
desc: "tlsSkipVerify: second set",
in: []interface{}{`true`},
want: true,
jsonFmtStr: "{" + tlsFields[1] + "}",
equalityFn: tlsEqFn,
},
{
desc: "tlsSkipVerify: neither set",
in: []interface{}{},
want: false, // zero value
jsonFmtStr: "{}",
equalityFn: tlsEqFn,
},
}
// ServiceID: string
func serviceIDEqFn(out interface{}, want interface{}) error {
var got interface{}
switch v := out.(type) {
case structs.CheckDefinition:
got = v.ServiceID
case *structs.CheckDefinition:
got = v.ServiceID
case structs.CheckType:
return nil // CheckType does not have a ServiceID field
case *structs.CheckType:
return nil // CheckType does not have a ServiceID field
case structs.HealthCheckDefinition:
return nil // HealthCheckDefinition does not have a ServiceID field
case *structs.HealthCheckDefinition:
return nil // HealthCheckDefinition does not have a ServiceID field
default:
panic(fmt.Sprintf("unexpected type %T", out))
}
if got != want {
return fmt.Errorf("expected ServiceID to be %s, got %s", want, got)
}
return nil
}
var serviceIDFields = []string{`"ServiceID": %s`, `"service_id": %s`}
var translateServiceIDTCs = []translateKeyTestCase{
{
desc: "serviceID: both set",
in: []interface{}{`"id-1"`, `"id-2"`},
want: "id-1",
jsonFmtStr: "{" + strings.Join(serviceIDFields, ",") + "}",
equalityFn: serviceIDEqFn,
},
{
desc: "serviceID: first set",
in: []interface{}{`"id-1"`},
want: "id-1",
jsonFmtStr: "{" + serviceIDFields[0] + "}",
equalityFn: serviceIDEqFn,
},
{
desc: "serviceID: second set",
in: []interface{}{`"id-2"`},
want: "id-2",
jsonFmtStr: "{" + serviceIDFields[1] + "}",
equalityFn: serviceIDEqFn,
},
{
desc: "serviceID: neither set",
in: []interface{}{},
want: "", // zero value
jsonFmtStr: "{}",
equalityFn: serviceIDEqFn,
},
}
// ACLPolicySetRequest:
// Policy structs.ACLPolicy
// ID string
// Name string
// Description string
// Rules string
// Syntax acl.SyntaxVersion
// Datacenters []string
// Hash []uint8
// RaftIndex structs.RaftIndex
// CreateIndex uint64
// ModifyIndex uint64
// Datacenter string
// WriteRequest structs.WriteRequest
// Token string
func TestDecodeACLPolicyWrite(t *testing.T) {
for _, tc := range hashTestCases {
t.Run(tc.desc, func(t *testing.T) {
jsonStr := fmt.Sprintf(`{
"Hash": %s
}`, tc.hashes.in)
body := bytes.NewBuffer([]byte(jsonStr))
var out structs.ACLPolicy
err := decodeBody(body, &out)
if err != nil && !tc.wantErr {
t.Fatal(err)
}
if err == nil && tc.wantErr {
t.Fatal("expected error, got nil")
}
if !bytes.Equal(out.Hash, tc.hashes.want) {
t.Fatalf("expected hash to be %s, got %s", tc.hashes.want, out.Hash)
}
})
}
}
// ACLTokenSetRequest:
// ACLToken structs.ACLToken
// AccessorID string
// SecretID string
// Description string
// Policies []structs.ACLTokenPolicyLink
// ID string
// Name string
// Roles []structs.ACLTokenRoleLink
// ID string
// Name string
// ServiceIdentities []*structs.ACLServiceIdentity
// ServiceName string
// Datacenters []string
// Type string
// Rules string
// Local bool
// AuthMethod string
// ExpirationTime *time.Time
// ExpirationTTL time.Duration
// CreateTime time.Time
// Hash []uint8
// RaftIndex structs.RaftIndex
// CreateIndex uint64
// ModifyIndex uint64
// Create bool
// Datacenter string
// WriteRequest structs.WriteRequest
// Token string
func TestDecodeACLToken(t *testing.T) {
for _, tc := range translateValueTestCases {
t.Run(tc.desc, func(t *testing.T) {
// set up request body
var expTime, expTTL, createTime, hash = "null", "null", "null", "null"
if tc.hashes != nil {
hash = tc.hashes.in
}
if tc.timestamps != nil {
expTime = tc.timestamps.in
createTime = tc.timestamps.in
}
if tc.durations != nil {
expTTL = tc.durations.in
}
bodyBytes := []byte(fmt.Sprintf(`{
"ExpirationTime": %s,
"ExpirationTTL": %s,
"CreateTime": %s,
"Hash": %s
}`, expTime, expTTL, createTime, hash))
body := bytes.NewBuffer(bodyBytes)
// decode body
var out structs.ACLToken
err := decodeBody(body, &out)
if err != nil && !tc.wantErr {
t.Fatal(err)
}
if err == nil && tc.wantErr {
t.Fatal("expected error, got nil")
}
// are we testing hashes in this test case?
if tc.hashes != nil {
if !bytes.Equal(out.Hash, tc.hashes.want) {
t.Fatalf("expected hash to be %s, got %s", tc.hashes.want, out.Hash)
}
}
// are we testing durations?
if tc.durations != nil {
if out.ExpirationTTL != tc.durations.want {
t.Fatalf("expected expirationTTL to be %s, got %s", tc.durations.want, out.ExpirationTTL)
}
}
// are we testing timestamps?
if tc.timestamps != nil {
if out.ExpirationTime != nil {
if !out.ExpirationTime.Equal(tc.timestamps.want) {
t.Fatalf("expected expirationTime to be %s, got %s", tc.timestamps.want, out.ExpirationTime)
}
} else {
if !tc.timestamps.want.IsZero() {
t.Fatalf("expected empty expirationTime, got %v", out.ExpirationTime)
}
}
if !out.CreateTime.Equal(tc.timestamps.want) {
t.Fatalf("expected createTime to be %s, got %s", tc.timestamps.want, out.CreateTime)
}
}
})
}
}
// ACLRoleSetRequest:
// Role structs.ACLRole
// ID string
// Name string
// Description string
// Policies []structs.ACLRolePolicyLink
// ID string
// Name string
// ServiceIdentities []*structs.ACLServiceIdentity
// ServiceName string
// Datacenters []string
// Hash []uint8
// RaftIndex structs.RaftIndex
// CreateIndex uint64
// ModifyIndex uint64
// Datacenter string
// WriteRequest structs.WriteRequest
// Token string
func TestDecodeACLRoleWrite(t *testing.T) {
for _, tc := range hashTestCases {
t.Run(tc.desc, func(t *testing.T) {
jsonStr := fmt.Sprintf(`{
"Hash": %s
}`, tc.hashes.in)
body := bytes.NewBuffer([]byte(jsonStr))
var out structs.ACLRole
err := decodeBody(body, &out)
if err == nil && tc.wantErr {
t.Fatal("expected error, got nil")
}
if err != nil && !tc.wantErr {
t.Fatalf("expected no error, got: %v", err)
}
if !bytes.Equal(out.Hash, tc.hashes.want) {
t.Fatalf("expected hash to be %s, got %s", tc.hashes.want, out.Hash)
}
})
}
}
// CheckDefinition:
// ID types.CheckID
// Name string
// Notes string
// ServiceID string
// Token string
// Status string
// ScriptArgs []string
// HTTP string
// Header map[string][]string
// Method string
// TCP string
// Interval time.Duration
// DockerContainerID string
// Shell string
// GRPC string
// GRPCUseTLS bool
// TLSSkipVerify bool
// AliasNode string
// AliasService string
// Timeout time.Duration
// TTL time.Duration
// DeregisterCriticalServiceAfter time.Duration
// OutputMaxSize int
// ==========
// decodeCB == FixupCheckType
func TestDecodeAgentRegisterCheck(t *testing.T) {
// Durations: Interval, Timeout, TTL, DeregisterCriticalServiceAfter
for _, tc := range durationTestCases {
t.Run(tc.desc, func(t *testing.T) {
// set up request body
jsonStr := fmt.Sprintf(`{
"Interval": %[1]s,
"Timeout": %[1]s,
"TTL": %[1]s,
"DeregisterCriticalServiceAfter": %[1]s
}`, tc.durations.in)
body := bytes.NewBuffer([]byte(jsonStr))
var out structs.CheckDefinition
err := decodeBody(body, &out)
if err == nil && tc.wantErr {
t.Fatal("expected err, got nil")
}
if err != nil && !tc.wantErr {
t.Fatalf("expected nil error, got %v", err)
}
err = checkTypeDurationTest(out, tc.durations.want, "")
if err != nil {
t.Fatal(err)
}
})
}
for _, tc := range checkTypeHeaderTestCases {
t.Run(tc.desc, func(t *testing.T) {
// set up request body
jsonStr := fmt.Sprintf(`{"Header": %s}`, tc.in)
body := bytes.NewBuffer([]byte(jsonStr))
var out structs.CheckDefinition
err := decodeBody(body, &out)
if err == nil && tc.wantErr {
t.Fatal("expected err, got nil")
}
if err != nil && !tc.wantErr {
t.Fatalf("expected nil error, got %v", err)
}
if err := checkTypeHeaderTest(out, tc.want, ""); err != nil {
t.Fatal(err)
}
})
}
for _, tcs := range translateCheckTypeTCs {
for _, tc := range tcs {
t.Run(tc.desc, func(t *testing.T) {
jsonStr := fmt.Sprintf(tc.jsonFmtStr, tc.in...)
body := bytes.NewBuffer([]byte(jsonStr))
var out structs.CheckDefinition
err := decodeBody(body, &out)
if err != nil {
t.Fatal(err)
}
if err := tc.equalityFn(out, tc.want); err != nil {
t.Fatal(err)
}
})
}
}
}
// ServiceDefinition:
// Kind structs.ServiceKind
// ID string
// Name string
// Tags []string
// Address string
// TaggedAddresses map[string]structs.ServiceAddress
// Address string
// Port int
// Meta map[string]string
// Port int
// Check structs.CheckType
// CheckID types.CheckID
// Name string
// Status string
// Notes string
// ScriptArgs []string
// HTTP string
// Header map[string][]string
// Method string
// TCP string
// Interval time.Duration
// AliasNode string
// AliasService string
// DockerContainerID string
// Shell string
// GRPC string
// GRPCUseTLS bool
// TLSSkipVerify bool
// Timeout time.Duration
// TTL time.Duration
// ProxyHTTP string
// ProxyGRPC string
// DeregisterCriticalServiceAfter time.Duration
// OutputMaxSize int
// Checks structs.CheckTypes
// Weights *structs.Weights
// Passing int
// Warning int
// Token string
// EnableTagOverride bool
// Proxy *structs.ConnectProxyConfig
// DestinationServiceName string
// DestinationServiceID string
// LocalServiceAddress string
// LocalServicePort int
// Config map[string]interface {}
// Upstreams structs.Upstreams
// DestinationType string
// DestinationNamespace string
// DestinationName string
// Datacenter string
// LocalBindAddress string
// LocalBindPort int
// Config map[string]interface {}
// MeshGateway structs.MeshGatewayConfig
// Mode structs.MeshGatewayMode
// MeshGateway structs.MeshGatewayConfig
// Expose structs.ExposeConfig
// Checks bool
// Paths []structs.ExposePath
// ListenerPort int
// Path string
// LocalPathPort int
// Protocol string
// ParsedFromCheck bool
// Connect *structs.ServiceConnect
// Native bool
// SidecarService *structs.ServiceDefinition
func TestDecodeAgentRegisterService(t *testing.T) {
// key translation tests:
// decodeCB fields:
// --------------------
// "enable_tag_override": "EnableTagOverride",
// // Proxy Upstreams
// "destination_name": "DestinationName",
// "destination_type": "DestinationType",
// "destination_namespace": "DestinationNamespace",
// "local_bind_port": "LocalBindPort",
// "local_bind_address": "LocalBindAddress",
// // Proxy Config
// "destination_service_name": "DestinationServiceName",
// "destination_service_id": "DestinationServiceID",
// "local_service_port": "LocalServicePort",
// "local_service_address": "LocalServiceAddress",
// // SidecarService
// "sidecar_service": "SidecarService",
// // Expose Config
// "local_path_port": "LocalPathPort",
// "listener_port": "ListenerPort",
// "tagged_addresses": "TaggedAddresses",
// EnableTagOverride: bool
enableTagOverrideEqFn := func(out interface{}, want interface{}) error {
got := out.(structs.ServiceDefinition).EnableTagOverride
if got != want {
return fmt.Errorf("expected EnableTagOverride to be %v, got %v", want, got)
}
return nil
}
var enableTagOverrideFields = []string{
`"EnableTagOverride": %s`,
`"enable_tag_override": %s`,
}
var translateEnableTagOverrideTCs = []translateKeyTestCase{
{
desc: "translateEnableTagTCs: both set",
in: []interface{}{`true`, `false`},
want: true,
jsonFmtStr: "{" + strings.Join(enableTagOverrideFields, ",") + "}",
equalityFn: enableTagOverrideEqFn,
},
{
desc: "translateEnableTagTCs: first set",
in: []interface{}{`true`},
want: true,
jsonFmtStr: "{" + enableTagOverrideFields[0] + "}",
equalityFn: enableTagOverrideEqFn,
},
{
desc: "translateEnableTagTCs: second set",
in: []interface{}{`true`},
want: true,
jsonFmtStr: "{" + enableTagOverrideFields[1] + "}",
equalityFn: enableTagOverrideEqFn,
},
{
desc: "translateEnableTagTCs: neither set",
in: []interface{}{},
want: false, // zero value
jsonFmtStr: "{}",
equalityFn: enableTagOverrideEqFn,
},
}
// DestinationName: string (Proxy.Upstreams)
destinationNameEqFn := func(out interface{}, want interface{}) error {
got := out.(structs.ServiceDefinition).Proxy.Upstreams[0].DestinationName
if got != want {
return fmt.Errorf("expected DestinationName to be %s, got %s", want, got)
}
return nil
}
var destinationNameFields = []string{
`"DestinationName": %s`,
`"destination_name": %s`,
}
var translateDestinationNameTCs = []translateKeyTestCase{
{
desc: "DestinationName: both set",
in: []interface{}{`"a"`, `"b"`},
want: "a",
jsonFmtStr: `{"Proxy": {"Upstreams": [{` + strings.Join(destinationNameFields, ",") + `}]}}`,
equalityFn: destinationNameEqFn,
},
{
desc: "DestinationName: first set",
in: []interface{}{`"a"`},
want: "a",
jsonFmtStr: `{"Proxy": {"Upstreams": [{` + destinationNameFields[0] + `}]}}`,
equalityFn: destinationNameEqFn,
},
{
desc: "DestinationName: second set",
in: []interface{}{`"b"`},
want: "b",
jsonFmtStr: `{"Proxy": {"Upstreams": [{` + destinationNameFields[1] + `}]}}`,
equalityFn: destinationNameEqFn,
},
{
desc: "DestinationName: neither set",
in: []interface{}{},
want: "", // zero value
jsonFmtStr: `{"Proxy": {"Upstreams": [{}]}}`,
equalityFn: destinationNameEqFn,
},
}
// DestinationType: string (Proxy.Upstreams)
destinationTypeEqFn := func(out interface{}, want interface{}) error {
got := out.(structs.ServiceDefinition).Proxy.Upstreams[0].DestinationType
if got != want {
return fmt.Errorf("expected DestinationType to be %s, got %s", want, got)
}
return nil
}
var destinationTypeFields = []string{
`"DestinationType": %s`,
`"destination_type": %s`,
}
var translateDestinationTypeTCs = []translateKeyTestCase{
{
desc: "DestinationType: both set",
in: []interface{}{`"a"`, `"b"`},
want: "a",
jsonFmtStr: `{"Proxy": {"Upstreams": [{` + strings.Join(destinationTypeFields, ",") + `}]}}`,
equalityFn: destinationTypeEqFn,
},
{
desc: "DestinationType: first set",
in: []interface{}{`"a"`},
want: "a",
jsonFmtStr: `{"Proxy": {"Upstreams": [{` + destinationTypeFields[0] + `}]}}`,
equalityFn: destinationTypeEqFn,
},
{
desc: "DestinationType: second set",
in: []interface{}{`"b"`},
want: "b",
jsonFmtStr: `{"Proxy": {"Upstreams": [{` + destinationTypeFields[1] + `}]}}`,
equalityFn: destinationTypeEqFn,
},
{
desc: "DestinationType: neither set",
in: []interface{}{},
want: "", // zero value
jsonFmtStr: `{"Proxy": {"Upstreams": [{}]}}`,
equalityFn: destinationTypeEqFn,
},
}
// DestinationNamespace: string (Proxy.Upstreams)
destinationNamespaceEqFn := func(out interface{}, want interface{}) error {
got := out.(structs.ServiceDefinition).Proxy.Upstreams[0].DestinationNamespace
if got != want {
return fmt.Errorf("expected DestinationNamespace to be %s, got %s", want, got)
}
return nil
}
var destinationNamespaceFields = []string{
`"DestinationNamespace": %s`,
`"destination_namespace": %s`,
}
var translateDestinationNamespaceTCs = []translateKeyTestCase{
{
desc: "DestinationNamespace: both set",
in: []interface{}{`"a"`, `"b"`},
want: "a",
jsonFmtStr: `{"Proxy": {"Upstreams": [{` + strings.Join(destinationNamespaceFields, ",") + `}]}}`,
equalityFn: destinationNamespaceEqFn,
},
{
desc: "DestinationNamespace: first set",
in: []interface{}{`"a"`},
want: "a",
jsonFmtStr: `{"Proxy": {"Upstreams": [{` + destinationNamespaceFields[0] + `}]}}`,
equalityFn: destinationNamespaceEqFn,
},
{
desc: "DestinationNamespace: second set",
in: []interface{}{`"b"`},
want: "b",
jsonFmtStr: `{"Proxy": {"Upstreams": [{` + destinationNamespaceFields[1] + `}]}}`,
equalityFn: destinationNamespaceEqFn,
},
{
desc: "DestinationNamespace: neither set",
in: []interface{}{},
want: "", // zero value
jsonFmtStr: `{"Proxy": {"Upstreams": [{}]}}`,
equalityFn: destinationNamespaceEqFn,
},
}
// LocalBindPort: int (Proxy.Upstreams)
localBindPortEqFn := func(out interface{}, want interface{}) error {
got := out.(structs.ServiceDefinition).Proxy.Upstreams[0].LocalBindPort
if got != want {
return fmt.Errorf("expected LocalBindPort to be %v, got %v", want, got)
}
return nil
}
var localBindPortFields = []string{
`"LocalBindPort": %s`,
`"local_bind_port": %s`,
}
var translateLocalBindPortTCs = []translateKeyTestCase{
{
desc: "LocalBindPort: both set",
in: []interface{}{`1`, `2`},
want: 1,
jsonFmtStr: `{"Proxy": {"Upstreams": [{` + strings.Join(localBindPortFields, ",") + `}]}}`,
equalityFn: localBindPortEqFn,
},
{
desc: "LocalBindPort: first set",
in: []interface{}{`1`},
want: 1,
jsonFmtStr: `{"Proxy": {"Upstreams": [{` + localBindPortFields[0] + `}]}}`,
equalityFn: localBindPortEqFn,
},
{
desc: "LocalBindPort: second set",
in: []interface{}{`2`},
want: 2,
jsonFmtStr: `{"Proxy": {"Upstreams": [{` + localBindPortFields[1] + `}]}}`,
equalityFn: localBindPortEqFn,
},
{
desc: "LocalBindPort: neither set",
in: []interface{}{},
want: 0, // zero value
jsonFmtStr: `{"Proxy": {"Upstreams": [{}]}}`,
equalityFn: localBindPortEqFn,
},
}
// LocalBindAddress: string (Proxy.Upstreams)
localBindAddressEqFn := func(out interface{}, want interface{}) error {
got := out.(structs.ServiceDefinition).Proxy.Upstreams[0].LocalBindAddress
if got != want {
return fmt.Errorf("expected LocalBindAddress to be %s, got %s", want, got)
}
return nil
}
var localBindAddressFields = []string{
`"LocalBindAddress": %s`,
`"local_bind_address": %s`,
}
var translateLocalBindAddressTCs = []translateKeyTestCase{
{
desc: "LocalBindAddress: both set",
in: []interface{}{`"one"`, `"two"`},
want: "one",
jsonFmtStr: `{"Proxy": {"Upstreams": [{` + strings.Join(localBindAddressFields, ",") + `}]}}`,
equalityFn: localBindAddressEqFn,
},
{
desc: "LocalBindAddress: first set",
in: []interface{}{`"one"`},
want: "one",
jsonFmtStr: `{"Proxy": {"Upstreams": [{` + localBindAddressFields[0] + `}]}}`,
equalityFn: localBindAddressEqFn,
},
{
desc: "LocalBindAddress: second set",
in: []interface{}{`"two"`},
want: "two",
jsonFmtStr: `{"Proxy": {"Upstreams": [{` + localBindAddressFields[1] + `}]}}`,
equalityFn: localBindAddressEqFn,
},
{
desc: "LocalBindAddress: neither set",
in: []interface{}{},
want: "", // zero value
jsonFmtStr: `{"Proxy": {"Upstreams": [{}]}}`,
equalityFn: localBindAddressEqFn,
},
}
// DestinationServiceName: string (Proxy)
destinationServiceNameEqFn := func(out interface{}, want interface{}) error {
got := out.(structs.ServiceDefinition).Proxy.DestinationServiceName
if got != want {
return fmt.Errorf("expected DestinationServiceName to be %s, got %s", want, got)
}
return nil
}
var destinationServiceNameFields = []string{
`"DestinationServiceName": %s`,
`"destination_service_name": %s`,
}
var translateDestinationServiceNameTCs = []translateKeyTestCase{
{
desc: "DestinationServiceName: both set",
in: []interface{}{`"one"`, `"two"`},
want: "one",
jsonFmtStr: `{"Proxy": {` + strings.Join(destinationServiceNameFields, ",") + `}}`,
equalityFn: destinationServiceNameEqFn,
},
{
desc: "DestinationServiceName: first set",
in: []interface{}{`"one"`},
want: "one",
jsonFmtStr: `{"Proxy": {` + destinationServiceNameFields[0] + `}}`,
equalityFn: destinationServiceNameEqFn,
},
{
desc: "DestinationServiceName: second set",
in: []interface{}{`"two"`},
want: "two",
jsonFmtStr: `{"Proxy": {` + destinationServiceNameFields[1] + `}}`,
equalityFn: destinationServiceNameEqFn,
},
{
desc: "DestinationServiceName: neither set",
in: []interface{}{},
want: "", // zero value
jsonFmtStr: `{"Proxy": {` + `}}`,
equalityFn: destinationServiceNameEqFn,
},
}
// DestinationServiceID: string (Proxy)
destinationServiceIDEqFn := func(out interface{}, want interface{}) error {
got := out.(structs.ServiceDefinition).Proxy.DestinationServiceID
if got != want {
return fmt.Errorf("expected DestinationServiceID to be %s, got %s", want, got)
}
return nil
}
var destinationServiceIDFields = []string{
`"DestinationServiceID": %s`,
`"destination_service_id": %s`,
}
var translateDestinationServiceIDTCs = []translateKeyTestCase{
{
desc: "DestinationServiceID: both set",
in: []interface{}{`"one"`, `"two"`},
want: "one",
jsonFmtStr: `{"Proxy": {` + strings.Join(destinationServiceIDFields, ",") + `}}`,
equalityFn: destinationServiceIDEqFn,
},
{
desc: "DestinationServiceID: first set",
in: []interface{}{`"one"`},
want: "one",
jsonFmtStr: `{"Proxy": {` + destinationServiceIDFields[0] + `}}`,
equalityFn: destinationServiceIDEqFn,
},
{
desc: "DestinationServiceID: second set",
in: []interface{}{`"two"`},
want: "two",
jsonFmtStr: `{"Proxy": {` + destinationServiceIDFields[1] + `}}`,
equalityFn: destinationServiceIDEqFn,
},
{
desc: "DestinationServiceID: neither set",
in: []interface{}{},
want: "", // zero value
jsonFmtStr: `{"Proxy": {}}`,
equalityFn: destinationServiceIDEqFn,
},
}
// LocalServicePort: int (Proxy)
localServicePortEqFn := func(out interface{}, want interface{}) error {
got := out.(structs.ServiceDefinition).Proxy.LocalServicePort
if got != want {
return fmt.Errorf("expected LocalServicePort to be %v, got %v", want, got)
}
return nil
}
var localServicePortFields = []string{
`"LocalServicePort": %s`,
`"local_service_port": %s`,
}
var translateLocalServicePortTCs = []translateKeyTestCase{
{
desc: "LocalServicePort: both set",
in: []interface{}{`1`, `2`},
want: 1,
jsonFmtStr: `{"Proxy": {` + strings.Join(localServicePortFields, ",") + `}}`,
equalityFn: localServicePortEqFn,
},
{
desc: "LocalServicePort: first set",
in: []interface{}{`1`},
want: 1,
jsonFmtStr: `{"Proxy": {` + localServicePortFields[0] + `}}`,
equalityFn: localServicePortEqFn,
},
{
desc: "LocalServicePort: second set",
in: []interface{}{`2`},
want: 2,
jsonFmtStr: `{"Proxy": {` + localServicePortFields[1] + `}}`,
equalityFn: localServicePortEqFn,
},
{
desc: "LocalServicePort: neither set",
in: []interface{}{},
want: 0, // zero value
jsonFmtStr: `{"Proxy": {}}`,
equalityFn: localServicePortEqFn,
},
}
// LocalServiceAddress: string (Proxy)
localServiceAddressEqFn := func(out interface{}, want interface{}) error {
got := out.(structs.ServiceDefinition).Proxy.LocalServiceAddress
if got != want {
return fmt.Errorf("expected LocalServiceAddress to be %s, got %s", want, got)
}
return nil
}
var localServiceAddressFields = []string{
`"LocalServiceAddress": %s`,
`"local_service_address": %s`,
}
var translateLocalServiceAddressTCs = []translateKeyTestCase{
{
desc: "LocalServiceAddress: both set",
in: []interface{}{`"one"`, `"two"`},
want: "one",
jsonFmtStr: `{"Proxy": {` + strings.Join(localServiceAddressFields, ",") + `}}`,
equalityFn: localServiceAddressEqFn,
},
{
desc: "LocalServiceAddress: first set",
in: []interface{}{`"one"`},
want: "one",
jsonFmtStr: `{"Proxy": {` + localServiceAddressFields[0] + `}}`,
equalityFn: localServiceAddressEqFn,
},
{
desc: "LocalServiceAddress: second set",
in: []interface{}{`"two"`},
want: "two",
jsonFmtStr: `{"Proxy": {` + localServiceAddressFields[1] + `}}`,
equalityFn: localServiceAddressEqFn,
},
{
desc: "LocalServiceAddress: neither set",
in: []interface{}{},
want: "", // zero value
jsonFmtStr: `{"Proxy": {}}`,
equalityFn: localServiceAddressEqFn,
},
}
// SidecarService: ServiceDefinition (Connect)
sidecarServiceEqFn := func(out interface{}, want interface{}) error {
scService := out.(structs.ServiceDefinition).Connect.SidecarService
if scService == nil {
if want != "" {
return fmt.Errorf("expected SidecarService with Name '%s', got nil service", want)
}
return nil
}
if scService.Name != want {
return fmt.Errorf("expected SidecarService with Name '%s', got Name=%s", want, scService.Name)
}
return nil
}
var sidecarServiceFields = []string{
`"SidecarService": %s`,
`"sidecar_service": %s`,
}
var translateSidecarServiceTCs = []translateKeyTestCase{
{
desc: "SidecarService: both set",
in: []interface{}{`{"Name": "one"}`, `{"Name": "two"}`},
want: "one",
jsonFmtStr: `{"Connect": {` + strings.Join(sidecarServiceFields, ",") + `}}`,
equalityFn: sidecarServiceEqFn,
},
{
desc: "SidecarService: first set",
in: []interface{}{`{"Name": "one"}`},
want: "one",
jsonFmtStr: `{"Connect": {` + sidecarServiceFields[0] + `}}`,
equalityFn: sidecarServiceEqFn,
},
{
desc: "SidecarService: second set",
in: []interface{}{`{"Name": "two"}`},
want: "two",
jsonFmtStr: `{"Connect": {` + sidecarServiceFields[1] + `}}`,
equalityFn: sidecarServiceEqFn,
},
{
desc: "SidecarService: neither set",
in: []interface{}{},
want: "", // zero value
jsonFmtStr: `{"Connect": {}}`,
equalityFn: sidecarServiceEqFn,
},
}
// LocalPathPort: int (Proxy.Expose.Paths)
localPathPortEqFn := func(out interface{}, want interface{}) error {
got := out.(structs.ServiceDefinition).Proxy.Expose.Paths[0].LocalPathPort
if got != want {
return fmt.Errorf("expected LocalPathPort to be %v, got %v", want, got)
}
return nil
}
var localPathPortFields = []string{
`"LocalPathPort": %s`,
`"local_path_port": %s`,
}
var translateLocalPathPortTCs = []translateKeyTestCase{
{
desc: "LocalPathPort: both set",
in: []interface{}{`1`, `2`},
want: 1,
jsonFmtStr: `{"Proxy": {"Expose": {"Paths": [{` + strings.Join(localPathPortFields, ",") + `}]}}}`,
equalityFn: localPathPortEqFn,
},
{
desc: "LocalPathPort: first set",
in: []interface{}{`1`},
want: 1,
jsonFmtStr: `{"Proxy": {"Expose": {"Paths": [{` + localPathPortFields[0] + `}]}}}`,
equalityFn: localPathPortEqFn,
},
{
desc: "LocalPathPort: second set",
in: []interface{}{`2`},
want: 2,
jsonFmtStr: `{"Proxy": {"Expose": {"Paths": [{` + localPathPortFields[1] + `}]}}}`,
equalityFn: localPathPortEqFn,
},
{
desc: "LocalPathPort: neither set",
in: []interface{}{},
want: 0, // zero value
jsonFmtStr: `{"Proxy": {"Expose": {"Paths": [{}]}}}`,
equalityFn: localPathPortEqFn,
},
}
// ListenerPort: int (Proxy.Expose.Paths)
listenerPortEqFn := func(out interface{}, want interface{}) error {
got := out.(structs.ServiceDefinition).Proxy.Expose.Paths[0].ListenerPort
if got != want {
return fmt.Errorf("expected ListenerPort to be %v, got %v", want, got)
}
return nil
}
var listenerPortFields = []string{
`"ListenerPort": %s`,
`"listener_port": %s`,
}
var translateListenerPortTCs = []translateKeyTestCase{
{
desc: "ListenerPort: both set",
in: []interface{}{`1`, `2`},
want: 1,
jsonFmtStr: `{"Proxy": {"Expose": {"Paths": [{` + strings.Join(listenerPortFields, ",") + `}]}}}`,
equalityFn: listenerPortEqFn,
},
{
desc: "ListenerPort: first set",
in: []interface{}{`1`},
want: 1,
jsonFmtStr: `{"Proxy": {"Expose": {"Paths": [{` + listenerPortFields[0] + `}]}}}`,
equalityFn: listenerPortEqFn,
},
{
desc: "ListenerPort: second set",
in: []interface{}{`2`},
want: 2,
jsonFmtStr: `{"Proxy": {"Expose": {"Paths": [{` + listenerPortFields[1] + `}]}}}`,
equalityFn: listenerPortEqFn,
},
{
desc: "ListenerPort: neither set",
in: []interface{}{},
want: 0, // zero value
jsonFmtStr: `{"Proxy": {"Expose": {"Paths": [{}]}}}`,
equalityFn: listenerPortEqFn,
},
}
// TaggedAddresses: map[string]structs.ServiceAddress
taggedAddressesEqFn := func(out interface{}, want interface{}) error {
tgdAddresses := out.(structs.ServiceDefinition).TaggedAddresses
if tgdAddresses == nil {
if want != "" {
return fmt.Errorf("expected TaggedAddresses at key='key' to have Address='%s', got nil TaggedAddress", want)
}
return nil
}
if tgdAddresses["key"].Address != want {
return fmt.Errorf("expected TaggedAddresses at key='key' to have Address '%v', got Address=%v", want, tgdAddresses)
}
return nil
}
var taggedAddressesFields = []string{
`"TaggedAddresses": %s`,
`"tagged_addresses": %s`,
}
var translateTaggedAddressesTCs = []translateKeyTestCase{
{
desc: "TaggedAddresses: both set",
in: []interface{}{`{"key": {"Address": "1"}}`, `{"key": {"Address": "2"}}`},
want: "1",
jsonFmtStr: `{` + strings.Join(taggedAddressesFields, ",") + `}`,
equalityFn: taggedAddressesEqFn,
},
{
desc: "TaggedAddresses: first set",
in: []interface{}{`{"key": {"Address": "1"}}`},
want: "1",
jsonFmtStr: `{` + taggedAddressesFields[0] + `}`,
equalityFn: taggedAddressesEqFn,
},
{
desc: "TaggedAddresses: second set",
in: []interface{}{`{"key": {"Address": "2"}}`},
want: "2",
jsonFmtStr: `{` + taggedAddressesFields[1] + `}`,
equalityFn: taggedAddressesEqFn,
},
{
desc: "TaggedAddresses: neither set",
in: []interface{}{},
want: "", // zero value
jsonFmtStr: `{}`,
equalityFn: taggedAddressesEqFn,
},
}
// lib.TranslateKeys keys pasted here again to check against:
// ---------------------------------------
// "enable_tag_override": "EnableTagOverride",
// // Proxy Upstreams
// "destination_name": "DestinationName",
// "destination_type": "DestinationType",
// "destination_namespace": "DestinationNamespace",
// "local_bind_port": "LocalBindPort",
// "local_bind_address": "LocalBindAddress",
// // Proxy Config
// "destination_service_name": "DestinationServiceName",
// "destination_service_id": "DestinationServiceID",
// "local_service_port": "LocalServicePort",
// "local_service_address": "LocalServiceAddress",
// // SidecarService
// "sidecar_service": "SidecarService",
// // Expose Config
// "local_path_port": "LocalPathPort",
// "listener_port": "ListenerPort",
// "tagged_addresses": "TaggedAddresses",
var translateFieldTCs = [][]translateKeyTestCase{
translateEnableTagOverrideTCs,
translateDestinationNameTCs,
translateDestinationTypeTCs,
translateDestinationNamespaceTCs,
translateLocalBindPortTCs,
translateLocalBindAddressTCs,
translateDestinationServiceNameTCs,
translateDestinationServiceIDTCs,
translateLocalServicePortTCs,
translateLocalServiceAddressTCs,
translateSidecarServiceTCs,
translateLocalPathPortTCs,
translateListenerPortTCs,
translateTaggedAddressesTCs,
}
for _, tcGroup := range translateFieldTCs {
for _, tc := range tcGroup {
t.Run(tc.desc, func(t *testing.T) {
checkJSONStr := fmt.Sprintf(tc.jsonFmtStr, tc.in...)
body := bytes.NewBuffer([]byte(checkJSONStr))
var out structs.ServiceDefinition
err := decodeBody(body, &out)
if err != nil {
t.Fatal(err)
}
if err := tc.equalityFn(out, tc.want); err != nil {
t.Fatal(err)
}
})
}
}
// ======================================================
for _, tc := range durationTestCases {
t.Run(tc.desc, func(t *testing.T) {
// set up request body
jsonStr := fmt.Sprintf(`{
"Check": {
"Interval": %[1]s,
"Timeout": %[1]s,
"TTL": %[1]s,
"DeregisterCriticalServiceAfter": %[1]s
},
"Checks": [
{
"Interval": %[1]s,
"Timeout": %[1]s,
"TTL": %[1]s,
"DeregisterCriticalServiceAfter": %[1]s
}
]
}`, tc.durations.in)
body := bytes.NewBuffer([]byte(jsonStr))
var out structs.ServiceDefinition
err := decodeBody(body, &out)
if err == nil && tc.wantErr {
t.Fatal("expected err, got nil")
}
if err != nil && !tc.wantErr {
t.Fatalf("expected nil error, got %v", err)
}
err = checkTypeDurationTest(out.Check, tc.durations.want, "")
if err != nil {
t.Fatal(err)
}
if out.Checks == nil {
if tc.durations.want != 0 {
t.Fatalf("Checks is nil, expected duration values to be %v", tc.durations.want)
}
return
}
err = checkTypeDurationTest(out.Checks[0], tc.durations.want, "[i=0]")
if err != nil {
t.Fatal(err)
}
})
}
for _, tc := range checkTypeHeaderTestCases {
t.Run(tc.desc, func(t *testing.T) {
// set up request body
checkJSONStr := fmt.Sprintf(`{"Header": %s}`, tc.in)
jsonStr := fmt.Sprintf(`{
"Check": %[1]s,
"Checks": [%[1]s]
}`, checkJSONStr)
body := bytes.NewBuffer([]byte(jsonStr))
var out structs.ServiceDefinition
err := decodeBody(body, &out)
if err == nil && tc.wantErr {
t.Fatal("expected err, got nil")
}
if err != nil && !tc.wantErr {
t.Fatalf("expected nil error, got %v", err)
}
if err := checkTypeHeaderTest(out.Check, tc.want, "Check"); err != nil {
t.Fatal(err)
}
if out.Checks == nil {
if tc.want != nil {
t.Fatalf("Checks is nil, expected Header to be %v", tc.want)
}
return
}
if err := checkTypeHeaderTest(out.Checks[0], tc.want, "Checks[0]"); err != nil {
t.Fatal(err)
}
})
}
for _, tcs := range translateCheckTypeTCs {
for _, tc := range tcs {
t.Run(tc.desc, func(t *testing.T) {
checkJSONStr := fmt.Sprintf(tc.jsonFmtStr, tc.in...)
jsonStr := fmt.Sprintf(`{
"Check": %[1]s,
"Checks": [%[1]s]
}`, checkJSONStr)
body := bytes.NewBuffer([]byte(jsonStr))
var out structs.ServiceDefinition
err := decodeBody(body, &out)
if err != nil {
t.Fatal(err)
}
if err := tc.equalityFn(out.Check, tc.want); err != nil {
t.Fatal(err)
}
if err := tc.equalityFn(out.Checks[0], tc.want); err != nil {
t.Fatal(err)
}
})
}
}
}
// RegisterRequest:
// Datacenter string
// ID types.NodeID
// Node string
// Address string
// TaggedAddresses map[string]string
// NodeMeta map[string]string
// Service *structs.NodeService
// Kind structs.ServiceKind
// ID string
// Service string
// Tags []string
// Address string
// TaggedAddresses map[string]structs.ServiceAddress
// Address string
// Port int
// Meta map[string]string
// Port int
// Weights *structs.Weights
// Passing int
// Warning int
// EnableTagOverride bool
// Proxy structs.ConnectProxyConfig
// DestinationServiceName string
// DestinationServiceID string
// LocalServiceAddress string
// LocalServicePort int
// Config map[string]interface {}
// Upstreams structs.Upstreams
// DestinationType string
// DestinationNamespace string
// DestinationName string
// Datacenter string
// LocalBindAddress string
// LocalBindPort int
// Config map[string]interface {}
// MeshGateway structs.MeshGatewayConfig
// Mode structs.MeshGatewayMode
// MeshGateway structs.MeshGatewayConfig
// Expose structs.ExposeConfig
// Checks bool
// Paths []structs.ExposePath
// ListenerPort int
// Path string
// LocalPathPort int
// Protocol string
// ParsedFromCheck bool
// Connect structs.ServiceConnect
// Native bool
// SidecarService *structs.ServiceDefinition
// Kind structs.ServiceKind
// ID string
// Name string
// Tags []string
// Address string
// TaggedAddresses map[string]structs.ServiceAddress
// Meta map[string]string
// Port int
// Check structs.CheckType
// CheckID types.CheckID
// Name string
// Status string
// Notes string
// ScriptArgs []string
// HTTP string
// Header map[string][]string
// Method string
// TCP string
// Interval time.Duration
// AliasNode string
// AliasService string
// DockerContainerID string
// Shell string
// GRPC string
// GRPCUseTLS bool
// TLSSkipVerify bool
// Timeout time.Duration
// TTL time.Duration
// ProxyHTTP string
// ProxyGRPC string
// DeregisterCriticalServiceAfter time.Duration
// OutputMaxSize int
// Checks structs.CheckTypes
// Weights *structs.Weights
// Token string
// EnableTagOverride bool
// Proxy *structs.ConnectProxyConfig
// Connect *structs.ServiceConnect
// LocallyRegisteredAsSidecar bool
// RaftIndex structs.RaftIndex
// CreateIndex uint64
// ModifyIndex uint64
// Check *structs.HealthCheck
// Node string
// CheckID types.CheckID
// Name string
// Status string
// Notes string
// Output string
// ServiceID string
// ServiceName string
// ServiceTags []string
// Definition structs.HealthCheckDefinition
// HTTP string
// TLSSkipVerify bool
// Header map[string][]string
// Method string
// TCP string
// Interval time.Duration
// OutputMaxSize uint
// Timeout time.Duration
// DeregisterCriticalServiceAfter time.Duration
// ScriptArgs []string
// DockerContainerID string
// Shell string
// GRPC string
// GRPCUseTLS bool
// AliasNode string
// AliasService string
// TTL time.Duration
// RaftIndex structs.RaftIndex
// Checks structs.HealthChecks
// SkipNodeUpdate bool
// WriteRequest structs.WriteRequest
// Token string
func TestDecodeCatalogRegister(t *testing.T) {
for _, tc := range durationTestCases {
t.Run(tc.desc, func(t *testing.T) {
// set up request body
jsonStr := fmt.Sprintf(`{
"Service": {
"Connect": {
"SidecarService": {
"Check": {
"Interval": %[1]s,
"Timeout": %[1]s,
"TTL": %[1]s,
"DeregisterCriticalServiceAfter": %[1]s
}
}
}
},
"Check": {
"Definition": {
"Interval": %[1]s,
"Timeout": %[1]s,
"TTL": %[1]s,
"DeregisterCriticalServiceAfter": %[1]s
}
}
}`, tc.durations.in)
body := bytes.NewBuffer([]byte(jsonStr))
var out structs.RegisterRequest
err := decodeBody(body, &out)
if err == nil && tc.wantErr {
t.Fatal("expected err, got nil")
}
if err != nil && !tc.wantErr {
t.Fatalf("expected nil error, got %v", err)
}
if err != nil && tc.wantErr {
return // no point continuing
}
// Service and Check will be nil if tc.wantErr == true && err != nil.
// We don't want to panic upon trying to follow a nil pointer, so we
// check these on a higher level here.
if out.Service == nil && tc.durations.want != 0 {
t.Fatalf("Service is nil, expected duration values to be %v", tc.durations.want)
}
if out.Check == nil && tc.durations.want != 0 {
t.Fatalf("Check is nil, expected duration values to be %v", tc.durations.want)
}
if out.Service == nil && out.Check == nil {
return
}
// Carry on checking nested fields
err = checkTypeDurationTest(out.Service.Connect.SidecarService.Check, tc.durations.want, "Service.Connect.SidecarService.Check")
if err != nil {
t.Fatal(err)
}
err = checkTypeDurationTest(out.Check.Definition, tc.durations.want, "Check.Definition")
if err != nil {
t.Fatal(err)
}
})
}
}
// IntentionRequest:
// Datacenter string
// Op structs.IntentionOp
// Intention *structs.Intention
// ID string
// Description string
// SourceNS string
// SourceName string
// DestinationNS string
// DestinationName string
// SourceType structs.IntentionSourceType
// Action structs.IntentionAction
// DefaultAddr string
// DefaultPort int
// Meta map[string]string
// Precedence int
// CreatedAt time.Time mapstructure:'-'
// UpdatedAt time.Time mapstructure:'-'
// Hash []uint8
// RaftIndex structs.RaftIndex
// CreateIndex uint64
// ModifyIndex uint64
// WriteRequest structs.WriteRequest
// Token string
func TestDecodeIntentionCreate(t *testing.T) {
for _, tc := range append(hashTestCases, timestampTestCases...) {
t.Run(tc.desc, func(t *testing.T) {
// set up request body
var createdAt, updatedAt, hash = "null", "null", "null"
if tc.hashes != nil {
hash = tc.hashes.in
}
if tc.timestamps != nil {
createdAt = tc.timestamps.in
updatedAt = tc.timestamps.in
}
bodyBytes := []byte(fmt.Sprintf(`{
"CreatedAt": %s,
"UpdatedAt": %s,
"Hash": %s
}`, createdAt, updatedAt, hash))
body := bytes.NewBuffer(bodyBytes)
// decode body
var out structs.Intention
err := decodeBody(body, &out)
if tc.hashes != nil {
// We should only check tc.wantErr for hashes in this case.
//
// This is because our CreatedAt and UpdatedAt timestamps have
// `mapstructure:"-"` tags, so these fields values should always be 0,
// and not return an error upon decoding (because they are to be ignored
// all together).
if err != nil && !tc.wantErr {
t.Fatal(err)
}
if err == nil && tc.wantErr {
t.Fatal("expected error, got nil")
}
} else if err != nil {
t.Fatal(err)
}
// are we testing hashes in this test case?
if tc.hashes != nil {
if !bytes.Equal(out.Hash, tc.hashes.want) {
t.Fatalf("expected hash to be %s, got %s", tc.hashes.want, out.Hash)
}
}
// are we testing timestamps?
if tc.timestamps != nil {
// CreatedAt and UpdatedAt should never be encoded/decoded, so we check
// that the timestamps are 0 here instead of tc.timestamps.want.
if !out.CreatedAt.IsZero() {
t.Fatalf("expected CreatedAt to be zero value, got %s", out.CreatedAt)
}
if !out.UpdatedAt.IsZero() {
t.Fatalf("expected UpdatedAt to be zero value, got %s", out.UpdatedAt)
}
}
})
}
}
// AutopilotConfiguration:
// CleanupDeadServers bool
// LastContactThreshold *api.ReadableDuration
// MaxTrailingLogs uint64
// ServerStabilizationTime *api.ReadableDuration
// RedundancyZoneTag string
// DisableUpgradeMigration bool
// UpgradeVersionTag string
// CreateIndex uint64
// ModifyIndex uint64
func TestDecodeOperatorAutopilotConfiguration(t *testing.T) {
for _, tc := range durationTestCases {
t.Run(tc.desc, func(t *testing.T) {
// set up request body
jsonStr := fmt.Sprintf(`{
"LastContactThreshold": %[1]s,
"ServerStabilizationTime": %[1]s
}`, tc.durations.in)
body := bytes.NewBuffer([]byte(jsonStr))
var out api.AutopilotConfiguration
err := decodeBody(body, &out)
if err == nil && tc.wantErr {
t.Fatal("expected err, got nil")
}
if err != nil && !tc.wantErr {
t.Fatalf("expected nil error, got %v", err)
}
if out.LastContactThreshold == nil {
if tc.durations.want != 0 {
t.Fatalf("expected LastContactThreshold to be %v, got nil.", tc.durations.want)
}
} else if *out.LastContactThreshold != api.ReadableDuration(tc.durations.want) {
t.Fatalf("expected LastContactThreshold to be %s, got %s", tc.durations.want, out.LastContactThreshold)
}
if out.ServerStabilizationTime == nil {
if tc.durations.want != 0 {
t.Fatalf("expected ServerStabilizationTime to be %v, got nil.", tc.durations.want)
}
} else if *out.ServerStabilizationTime != api.ReadableDuration(tc.durations.want) {
t.Fatalf("expected ServerStabilizationTime to be %s, got %s", tc.durations.want, out.ServerStabilizationTime)
}
})
}
}
// SessionRequest:
// Datacenter string
// Op structs.SessionOp
// Session structs.Session
// ID string
// Name string
// Node string
// Checks []types.CheckID
// LockDelay time.Duration
// Behavior structs.SessionBehavior
// TTL string
// RaftIndex structs.RaftIndex
// CreateIndex uint64
// ModifyIndex uint64
// WriteRequest structs.WriteRequest
// Token string
func TestDecodeSessionCreate(t *testing.T) {
// outSession var is shared among test cases b/c of the
// nature/signature of the FixupChecks callback.
var outSession structs.Session
// lockDelayMinThreshold = 1000
sessionDurationTCs := append(positiveDurationTCs,
translateValueTestCase{
desc: "duration small, numeric (< lockDelayMinThreshold)",
durations: &durationTC{
in: `20`,
want: (20 * time.Second),
},
},
translateValueTestCase{
desc: "duration string, no unit",
durations: &durationTC{
in: `"20"`,
},
wantErr: true,
},
translateValueTestCase{
desc: "duration small, string, already duration",
durations: &durationTC{
in: `"20ns"`, // ns ignored
want: (20 * time.Second),
},
},
translateValueTestCase{
desc: "duration small, numeric, negative",
durations: &durationTC{
in: `-5`,
want: -5 * time.Second,
},
},
)
for _, tc := range sessionDurationTCs {
t.Run(tc.desc, func(t *testing.T) {
// outSession var is shared among test cases b/c of the
// nature/signature of the FixupChecks callback.
// Wipe it clean before each test case.
outSession = structs.Session{}
// set up request body
jsonStr := fmt.Sprintf(`{
"LockDelay": %s
}`, tc.durations.in)
body := bytes.NewBuffer([]byte(jsonStr))
// outSession var is shared among test cases
err := decodeBody(body, &outSession)
if err == nil && tc.wantErr {
t.Fatal("expected err, got nil")
}
if err != nil && !tc.wantErr {
t.Fatalf("expected nil error, got %v", err)
}
if outSession.LockDelay != tc.durations.want {
t.Fatalf("expected LockDelay to be %v, got %v", tc.durations.want, outSession.LockDelay)
}
})
}
checkIDTestCases := []struct {
desc string
in string
want []types.CheckID
wantErr bool
}{
{
desc: "many check ids",
in: `["one", "two", "three"]`,
want: []types.CheckID{"one", "two", "three"},
},
{
desc: "one check ids",
in: `["foo"]`,
want: []types.CheckID{"foo"},
},
{
desc: "empty check id slice",
in: `[]`,
want: []types.CheckID{},
},
{
desc: "null check ids",
in: `null`,
want: []types.CheckID{},
},
{
desc: "empty value check ids",
in: ``,
wantErr: true,
},
{
desc: "malformatted check ids (string)",
in: `"one"`,
wantErr: true,
},
}
for _, tc := range checkIDTestCases {
t.Run(tc.desc, func(t *testing.T) {
// outSession var is shared among test cases b/c of the
// nature/signature of the FixupChecks callback.
// Wipe it clean before each test case.
outSession = structs.Session{}
// set up request body
jsonStr := fmt.Sprintf(`{
"Checks": %s
}`, tc.in)
body := bytes.NewBuffer([]byte(jsonStr))
err := decodeBody(body, &outSession)
if err == nil && tc.wantErr {
t.Fatal("expected err, got nil")
}
if err != nil && !tc.wantErr {
t.Fatalf("expected nil error, got %v", err)
}
if len(outSession.Checks) != len(tc.want) {
t.Fatalf("expected Checks to be %v, got %v", tc.want, outSession.Checks)
}
for i := range outSession.Checks {
if outSession.Checks[i] != tc.want[i] {
t.Fatalf("expected Checks to be %v, got %v", tc.want, outSession.Checks)
}
}
})
}
}
// TxnOps:
// KV *api.KVTxnOp
// Verb api.KVOp
// Key string
// Value []uint8
// Flags uint64
// Index uint64
// Session string
// Node *api.NodeTxnOp
// Verb api.NodeOp
// Node api.Node
// ID string
// Node string
// Address string
// Datacenter string
// TaggedAddresses map[string]string
// Meta map[string]string
// CreateIndex uint64
// ModifyIndex uint64
// Service *api.ServiceTxnOp
// Verb api.ServiceOp
// Node string
// Service api.AgentService
// Kind api.ServiceKind
// ID string
// Service string
// Tags []string
// Meta map[string]string
// Port int
// Address string
// TaggedAddresses map[string]api.ServiceAddress
// Address string
// Port int
// Weights api.AgentWeights
// Passing int
// Warning int
// EnableTagOverride bool
// CreateIndex uint64
// ModifyIndex uint64
// ContentHash string
// Proxy *api.AgentServiceConnectProxyConfig
// DestinationServiceName string
// DestinationServiceID string
// LocalServiceAddress string
// LocalServicePort int
// Config map[string]interface {}
// Upstreams []api.Upstream
// DestinationType api.UpstreamDestType
// DestinationNamespace string
// DestinationName string
// Datacenter string
// LocalBindAddress string
// LocalBindPort int
// Config map[string]interface {}
// MeshGateway api.MeshGatewayConfig
// Mode api.MeshGatewayMode
// MeshGateway api.MeshGatewayConfig
// Expose api.ExposeConfig
// Checks bool
// Paths []api.ExposePath
// ListenerPort int
// Path string
// LocalPathPort int
// Protocol string
// ParsedFromCheck bool
// Connect *api.AgentServiceConnect
// Native bool
// SidecarService *api.AgentServiceRegistration
// Kind api.ServiceKind
// ID string
// Name string
// Tags []string
// Port int
// Address string
// TaggedAddresses map[string]api.ServiceAddress
// EnableTagOverride bool
// Meta map[string]string
// Weights *api.AgentWeights
// Check *api.AgentServiceCheck
// CheckID string
// Name string
// Args []string
// DockerContainerID string
// Shell string
// Interval string
// Timeout string
// TTL string
// HTTP string
// Header map[string][]string
// Method string
// TCP string
// Status string
// Notes string
// TLSSkipVerify bool
// GRPC string
// GRPCUseTLS bool
// AliasNode string
// AliasService string
// DeregisterCriticalServiceAfter string
// Checks api.AgentServiceChecks
// Proxy *api.AgentServiceConnectProxyConfig
// Connect *api.AgentServiceConnect
// Check *api.CheckTxnOp
// Verb api.CheckOp
// Check api.HealthCheck
// Node string
// CheckID string
// Name string
// Status string
// Notes string
// Output string
// ServiceID string
// ServiceName string
// ServiceTags []string
// Definition api.HealthCheckDefinition
// HTTP string
// Header map[string][]string
// Method string
// Body string
// TLSSkipVerify bool
// TCP string
// IntervalDuration time.Duration
// TimeoutDuration time.Duration
// DeregisterCriticalServiceAfterDuration time.Duration
// Interval api.ReadableDuration
// Timeout api.ReadableDuration
// DeregisterCriticalServiceAfter api.ReadableDuration
// CreateIndex uint64
// ModifyIndex uint64
func TestDecodeTxnConvertOps(t *testing.T) {
for _, tc := range durationTestCases {
t.Run(tc.desc, func(t *testing.T) {
// set up request body
jsonStr := fmt.Sprintf(`[{
"Check": {
"Check": {
"Definition": {
"IntervalDuration": %[1]s,
"TimeoutDuration": %[1]s,
"DeregisterCriticalServiceAfterDuration": %[1]s,
"Interval": %[1]s,
"Timeout": %[1]s,
"DeregisterCriticalServiceAfter": %[1]s
}
}
}
}]`, tc.durations.in)
body := bytes.NewBuffer([]byte(jsonStr))
var out api.TxnOps
err := decodeBody(body, &out)
if err == nil && tc.wantErr {
t.Fatal("expected err, got nil")
}
if err != nil && !tc.wantErr {
t.Fatalf("expected nil error, got %v", err)
}
// Check will be nil if we want an error and got one (tc.wantErr == true && err != nil).
// We don't want to panic dereferencing a nil pointer, so we
// check this on a higher level here.
if out == nil || out[0] == nil {
if tc.durations.want != 0 {
t.Fatalf("Check is nil, expected duration values to be %v", tc.durations.want)
}
return
}
outCheck := out[0].Check.Check.Definition
if outCheck.IntervalDuration != tc.durations.want {
t.Fatalf("expected IntervalDuration to be %v, got %v", tc.durations.want, outCheck.IntervalDuration)
}
if outCheck.TimeoutDuration != tc.durations.want {
t.Fatalf("expected TimeoutDuration to be %v, got %v", tc.durations.want, outCheck.TimeoutDuration)
}
if outCheck.DeregisterCriticalServiceAfterDuration != tc.durations.want {
t.Fatalf("expected DeregisterCriticalServiceAfterDuration to be %v, got %v", tc.durations.want, outCheck.DeregisterCriticalServiceAfterDuration)
}
if outCheck.Interval != api.ReadableDuration(tc.durations.want) {
t.Fatalf("expected Interval to be %v, got %v", tc.durations.want, outCheck.Interval)
}
if outCheck.Timeout != api.ReadableDuration(tc.durations.want) {
t.Fatalf("expected Timeout to be %v, got %v", tc.durations.want, outCheck.Timeout)
}
if outCheck.DeregisterCriticalServiceAfter != api.ReadableDuration(tc.durations.want) {
t.Fatalf("expected DeregisterCriticalServiceAfter to be %v, got %v", tc.durations.want, outCheck.DeregisterCriticalServiceAfter)
}
})
}
}
// =========================================
// Helper funcs:
// =========================================
// checkTypeDurationTest is a helper func to test durations in CheckTYpe or CheckDefiniton
// (to reduce repetetive typing).
func checkTypeDurationTest(check interface{}, want time.Duration, prefix string) error {
// check for pointers first
switch v := check.(type) {
case *structs.CheckType:
check = *v
case *structs.CheckDefinition:
check = *v
case *structs.HealthCheckDefinition:
check = *v
}
var interval, timeout, ttl, deregister time.Duration
switch v := check.(type) {
case structs.CheckType:
interval = v.Interval
timeout = v.Timeout
ttl = v.TTL
deregister = v.DeregisterCriticalServiceAfter
case structs.CheckDefinition:
interval = v.Interval
timeout = v.Timeout
ttl = v.TTL
deregister = v.DeregisterCriticalServiceAfter
case structs.HealthCheckDefinition:
interval = v.Interval
timeout = v.Timeout
ttl = v.TTL
deregister = v.DeregisterCriticalServiceAfter
default:
panic(fmt.Sprintf("unexpected type %T", check))
}
if interval != want {
return fmt.Errorf("%s expected Check.Interval to be %s, got %s", prefix, want, interval)
}
if timeout != want {
return fmt.Errorf("%s expected Check.Timeout to be %s, got %s", prefix, want, timeout)
}
if ttl != want {
return fmt.Errorf("%s expected Check.TTL to be %s, got %s", prefix, want, ttl)
}
if deregister != want {
return fmt.Errorf("%s expected Check.DeregisterCriticalServiceAfter to be %s, got %s", prefix, want, deregister)
}
return nil
}
// checkTypeDurationTest is a helper func to test the Header map in a CheckType or CheckDefiniton
// (to reduce repetetive typing).
func checkTypeHeaderTest(check interface{}, want map[string][]string, prefix string) error {
var header map[string][]string
switch v := check.(type) {
case structs.CheckType:
header = v.Header
case *structs.CheckType:
header = v.Header
case structs.CheckDefinition:
header = v.Header
case *structs.CheckDefinition:
header = v.Header
}
for wantk, wantvs := range want {
if len(header[wantk]) != len(wantvs) {
return fmt.Errorf("expected Header to be %v, got %v", want, header)
}
for i, wantv := range wantvs {
if header[wantk][i] != wantv {
return fmt.Errorf("expected Header to be %v, got %v", want, header)
}
}
}
return nil
}