consul/agent/http_decode_test.go

2551 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": {"aa", "aaa"},
"b": {"bb", "bbb", "bbbb"},
"d": {"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
// 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
}