diff --git a/agent/agent_endpoint.go b/agent/agent_endpoint.go index 6486ad1c1d..1124411c2b 100644 --- a/agent/agent_endpoint.go +++ b/agent/agent_endpoint.go @@ -757,75 +757,8 @@ func (s *HTTPServer) AgentHealthServiceByName(resp http.ResponseWriter, req *htt func (s *HTTPServer) AgentRegisterService(resp http.ResponseWriter, req *http.Request) (interface{}, error) { var args structs.ServiceDefinition // Fixup the type decode of TTL or Interval if a check if provided. - decodeCB := func(raw interface{}) error { - rawMap, ok := raw.(map[string]interface{}) - if !ok { - return nil - } - // see https://github.com/hashicorp/consul/pull/3557 why we need this - // and why we should get rid of it. - lib.TranslateKeys(rawMap, map[string]string{ - "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", - - // DON'T Recurse into these opaque config maps or we might mangle user's - // keys. Note empty canonical is a special sentinel to prevent recursion. - "Meta": "", - - "tagged_addresses": "TaggedAddresses", - - // upstreams is an array but this prevents recursion into config field of - // any item in the array. - "Proxy.Config": "", - "Proxy.Upstreams.Config": "", - "Connect.Proxy.Config": "", - "Connect.Proxy.Upstreams.Config": "", - - // Same exceptions as above, but for a nested sidecar_service note we use - // the canonical form SidecarService since that is translated by the time - // the lookup here happens. - "Connect.SidecarService.Meta": "", - "Connect.SidecarService.Proxy.Config": "", - "Connect.SidecarService.Proxy.Upstreams.config": "", - }) - - for k, v := range rawMap { - switch strings.ToLower(k) { - case "check": - if err := FixupCheckType(v); err != nil { - return err - } - case "checks": - chkTypes, ok := v.([]interface{}) - if !ok { - continue - } - for _, chkType := range chkTypes { - if err := FixupCheckType(chkType); err != nil { - return err - } - } - } - } - return nil - } - if err := decodeBody(req, &args, decodeCB); err != nil { + if err := decodeBody(req, &args, registerServiceDecodeCB); err != nil { resp.WriteHeader(http.StatusBadRequest) fmt.Fprintf(resp, "Request decode failed: %v", err) return nil, nil @@ -961,6 +894,76 @@ func (s *HTTPServer) AgentRegisterService(resp http.ResponseWriter, req *http.Re return nil, nil } +// registerServiceDecodeCB is used in AgentRegisterService for request body decoding +func registerServiceDecodeCB(raw interface{}) error { + rawMap, ok := raw.(map[string]interface{}) + if !ok { + return nil + } + + // see https://github.com/hashicorp/consul/pull/3557 why we need this + // and why we should get rid of it. + lib.TranslateKeys(rawMap, map[string]string{ + "enable_tag_override": "EnableTagOverride", + // Proxy Upstreams + "destination_name": "DestinationName", // string + "destination_type": "DestinationType", // string + "destination_namespace": "DestinationNamespace", // string + "local_bind_port": "LocalBindPort", // int + "local_bind_address": "LocalBindAddress", // string + // Proxy Config + "destination_service_name": "DestinationServiceName", // string (Proxy.) + "destination_service_id": "DestinationServiceID", // string + "local_service_port": "LocalServicePort", // int + "local_service_address": "LocalServiceAddress", // string + // SidecarService + "sidecar_service": "SidecarService", // ServiceDefinition (Connect.) + // Expose Config + "local_path_port": "LocalPathPort", // int (Proxy.Expose.Paths.) + "listener_port": "ListenerPort", // int + + // DON'T Recurse into these opaque config maps or we might mangle user's + // keys. Note empty canonical is a special sentinel to prevent recursion. + "Meta": "", + + "tagged_addresses": "TaggedAddresses", // map[string]structs.ServiceAddress{Address string; Port int} + + // upstreams is an array but this prevents recursion into config field of + // any item in the array. + "Proxy.Config": "", + "Proxy.Upstreams.Config": "", + "Connect.Proxy.Config": "", + "Connect.Proxy.Upstreams.Config": "", + + // Same exceptions as above, but for a nested sidecar_service note we use + // the canonical form SidecarService since that is translated by the time + // the lookup here happens. + "Connect.SidecarService.Meta": "", + "Connect.SidecarService.Proxy.Config": "", + "Connect.SidecarService.Proxy.Upstreams.config": "", + }) + + for k, v := range rawMap { + switch strings.ToLower(k) { + case "check": + if err := FixupCheckType(v); err != nil { + return err + } + case "checks": + chkTypes, ok := v.([]interface{}) + if !ok { + continue + } + for _, chkType := range chkTypes { + if err := FixupCheckType(chkType); err != nil { + return err + } + } + } + } + return nil +} + func (s *HTTPServer) AgentDeregisterService(resp http.ResponseWriter, req *http.Request) (interface{}, error) { serviceID := strings.TrimPrefix(req.URL.Path, "/v1/agent/service/deregister/") diff --git a/agent/http_decode_test.go b/agent/http_decode_test.go new file mode 100644 index 0000000000..dbfc7e4523 --- /dev/null +++ b/agent/http_decode_test.go @@ -0,0 +1,3322 @@ +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" + "net/http" + "net/http/httptest" + "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: ×tampTC{ + in: `"2020-01-02T15:04:05Z"`, + want: time.Date(2020, 01, 02, 15, 4, 5, 0, time.UTC), + }, + }, + { + desc: "timestamps incorrectly formatted (RFC822)", + timestamps: ×tampTC{ + in: `"02 Jan 21 15:04"`, + }, + wantErr: true, + }, + { + desc: "timestamps incorrectly formatted (RFC850)", + timestamps: ×tampTC{ + in: `"Monday, 02-Jan-20 15:04:05"`, + }, + wantErr: true, + }, + { + desc: "timestamps empty string", + timestamps: ×tampTC{ + in: `""`, + }, + wantErr: true, + }, + { + desc: "timestamps null", + timestamps: ×tampTC{ + 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, + }, +} + +// ================================== +// $GOPATH/github.com/hashicorp/consul/agent/acl_endpoint.go: +// 327 s.parseToken(req, &args.Token) +// 328 +// 329: if err := decodeBody(req, &args.Policy, fixTimeAndHashFields); err != nil { +// 330 return nil, BadRequestError{Reason: fmt.Sprintf("Policy decoding failed: %v", err)} +// 331 } +// ================================== + +// 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)) + req := httptest.NewRequest("POST", "http://foo.com", body) + + var out structs.ACLPolicy + err := decodeBody(req, &out, fixTimeAndHashFields) + 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) + } + }) + + } +} + +// ================================== +// $GOPATH/github.com/hashicorp/consul/agent/acl_endpoint.go: +// 511 s.parseToken(req, &args.Token) +// 512 +// 513: if err := decodeBody(req, &args.ACLToken, fixTimeAndHashFields); err != nil { +// 514 return nil, BadRequestError{Reason: fmt.Sprintf("Token decoding failed: %v", err)} +// 515 } +// ================================== + +// 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)) + + // set up request + body := bytes.NewBuffer(bodyBytes) + req := httptest.NewRequest("POST", "http://foo.com", body) + + // decode body + var out structs.ACLToken + err := decodeBody(req, &out, fixTimeAndHashFields) + 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) + } + } + }) + + } +} + +// ================================== +// $GOPATH/github.com/hashicorp/consul/agent/acl_endpoint.go: +// 555 } +// 556 +// 557: if err := decodeBody(req, &args.ACLToken, fixTimeAndHashFields); err != nil && err.Error() != "EOF" { +// 558 return nil, BadRequestError{Reason: fmt.Sprintf("Token decoding failed: %v", err)} +// 559 } +// ================================== +func TestDecodeACLTokenClone(t *testing.T) { + t.Skip("COVERED BY ABOVE (same structs.ACLTokenSetRequest).") +} + +// ================================== +// $GOPATH/github.com/hashicorp/consul/agent/acl_endpoint.go: +// 689 s.parseToken(req, &args.Token) +// 690 +// 691: if err := decodeBody(req, &args.Role, fixTimeAndHashFields); err != nil { +// 692 return nil, BadRequestError{Reason: fmt.Sprintf("Role decoding failed: %v", err)} +// 693 } +// ================================== + +// 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)) + req := httptest.NewRequest("POST", "http://foo.com", body) + + var out structs.ACLRole + err := decodeBody(req, &out, fixTimeAndHashFields) + 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) + } + }) + + } +} + +// ================================== +// $GOPATH/github.com/hashicorp/consul/agent/acl_endpoint.go: +// 822 s.parseToken(req, &args.Token) +// 823 +// 824: if err := decodeBody(req, &args.BindingRule, fixTimeAndHashFields); err != nil { +// 825 return nil, BadRequestError{Reason: fmt.Sprintf("BindingRule decoding failed: %v", err)} +// 826 } +// ================================== +// +// ACLBindingRuleSetRequest: +// BindingRule structs.ACLBindingRule +// ID string +// Description string +// AuthMethod string +// Selector string +// BindType string +// BindName string +// RaftIndex structs.RaftIndex +// CreateIndex uint64 +// ModifyIndex uint64 +// Datacenter string +// WriteRequest structs.WriteRequest +// Token string +func TestDecodeACLBindingRuleWrite(t *testing.T) { + t.Skip("DONE. no special fields to parse; fixTimeAndHashFields: no time or hash fields.") +} + +// ================================== +// $GOPATH/github.com/hashicorp/consul/agent/acl_endpoint.go: +// 954 s.parseToken(req, &args.Token) +// 955 +// 956: if err := decodeBody(req, &args.AuthMethod, fixTimeAndHashFields); err != nil { +// 957 return nil, BadRequestError{Reason: fmt.Sprintf("AuthMethod decoding failed: %v", err)} +// 958 } +// ================================== +// ACLAuthMethodSetRequest: +// AuthMethod structs.ACLAuthMethod +// Name string +// Type string +// Description string +// Config map[string]interface {} +// RaftIndex structs.RaftIndex +// CreateIndex uint64 +// ModifyIndex uint64 +// Datacenter string +// WriteRequest structs.WriteRequest +// Token string +func TestDecodeACLAuthMethodWrite(t *testing.T) { + t.Skip("DONE. no special fields to parse; fixTimeAndHashFields: no time or hash fields.") +} + +// ================================== +// $GOPATH/github.com/hashicorp/consul/agent/acl_endpoint.go: +// 1000 s.parseDC(req, &args.Datacenter) +// 1001 +// 1002: if err := decodeBody(req, &args.Auth, nil); err != nil { +// 1003 return nil, BadRequestError{Reason: fmt.Sprintf("Failed to decode request body:: %v", err)} +// 1004 } +// ================================== +// ACLLoginRequest: +// Auth *structs.ACLLoginParams +// AuthMethod string +// BearerToken string +// Meta map[string]string +// Datacenter string +// WriteRequest structs.WriteRequest +// Token string +func TestDecodeACLLogin(t *testing.T) { + t.Skip("DONE. no special fields to parse; no decodeBody callback used.") +} + +// ================================== +// $GOPATH/github.com/hashicorp/consul/agent/acl_endpoint_legacy.go: +// 66 // Handle optional request body +// 67 if req.ContentLength > 0 { +// 68: if err := decodeBody(req, &args.ACL, nil); err != nil { +// 69 resp.WriteHeader(http.StatusBadRequest) +// 70 fmt.Fprintf(resp, "Request decode failed: %v", err) +// ================================== +// +// ACLRequest: +// Datacenter string +// Op structs.ACLOp +// ACL structs.ACL +// ID string +// Name string +// Type string +// Rules string +// RaftIndex structs.RaftIndex +// CreateIndex uint64 +// ModifyIndex uint64 +// WriteRequest structs.WriteRequest +// Token string +func TestDecodeACLUpdate(t *testing.T) { + t.Skip("DONE. no special fields to parse; no decodeBody callback used.") +} + +// ================================== +// $GOPATH/github.com/hashicorp/consul/agent/agent_endpoint.go: +// 461 return FixupCheckType(raw) +// 462 } +// 463: if err := decodeBody(req, &args, decodeCB); err != nil { +// 464 resp.WriteHeader(http.StatusBadRequest) +// 465 fmt.Fprintf(resp, "Request decode failed: %v", err) +// ================================== +// 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)) + req := httptest.NewRequest("POST", "http://foo.com", body) + + var out structs.CheckDefinition + err := decodeBody(req, &out, FixupCheckType) + 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) + } + }) + } + // decodeCB: + // - Header field + // - translate keys + + 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)) + req := httptest.NewRequest("POST", "http://foo.com", body) + + var out structs.CheckDefinition + err := decodeBody(req, &out, FixupCheckType) + 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)) + req := httptest.NewRequest("POST", "http://foo.com", body) + + var out structs.CheckDefinition + err := decodeBody(req, &out, FixupCheckType) + if err != nil { + t.Fatal(err) + } + + if err := tc.equalityFn(out, tc.want); err != nil { + t.Fatal(err) + } + }) + } + } + +} + +// ================================== +// $GOPATH/github.com/hashicorp/consul/agent/agent_endpoint.go: +// 603 func (s *HTTPServer) AgentCheckUpdate(resp http.ResponseWriter, req *http.Request) (interface{}, error) { +// 604 var update checkUpdate +// 605: if err := decodeBody(req, &update, nil); err != nil { +// 606 resp.WriteHeader(http.StatusBadRequest) +// 607 fmt.Fprintf(resp, "Request decode failed: %v", err) +// ================================== +// type checkUpdate struct { +// Status string +// Output string +// } +func TestDecodeAgentCheckUpdate(t *testing.T) { + t.Skip("DONE. no special fields to parse; no decodeBody callback used.") +} + +// ================================== +// $GOPATH/github.com/hashicorp/consul/agent/agent_endpoint.go: +// 822 return nil +// 823 } +// 824: if err := decodeBody(req, &args, decodeCB); err != nil { +// 825 resp.WriteHeader(http.StatusBadRequest) +// 826 fmt.Fprintf(resp, "Request decode failed: %v", err) +// ================================== +// +// decodeCB: +// ----------- +// 1. lib.TranslateKeys() +// 2. FixupCheckType +// a. lib.TranslateKeys() +// b. parseDuration() +// c. parseHeaderMap() +// +// +// Type fields: +// ----------- +// 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) { + var callback = registerServiceDecodeCB + + // 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": [{` + destinationNamespaceFields[1] + `}]}}`, + equalityFn: destinationNamespaceEqFn, + }, + { + desc: "DestinationNamespace: first set", + in: []interface{}{`"a"`}, + want: "a", + jsonFmtStr: `{"Proxy": {"Upstreams": [{` + destinationNamespaceFields[1] + `}]}}`, + 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)) + req := httptest.NewRequest("POST", "http://foo.com", body) + + var out structs.ServiceDefinition + err := decodeBody(req, &out, callback) + 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)) + req := httptest.NewRequest("POST", "http://foo.com", body) + + var out structs.ServiceDefinition + err := decodeBody(req, &out, callback) + 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)) + req := httptest.NewRequest("POST", "http://foo.com", body) + + var out structs.ServiceDefinition + err := decodeBody(req, &out, callback) + 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)) + req := httptest.NewRequest("POST", "http://foo.com", body) + + var out structs.ServiceDefinition + err := decodeBody(req, &out, callback) + 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) + } + }) + } + } + +} + +// ================================== +// $GOPATH/github.com/hashicorp/consul/agent/agent_endpoint.go: +// 1173 // fields to this later if needed. +// 1174 var args api.AgentToken +// 1175: if err := decodeBody(req, &args, nil); err != nil { +// 1176 resp.WriteHeader(http.StatusBadRequest) +// 1177 fmt.Fprintf(resp, "Request decode failed: %v", err) +// ================================== +// AgentToken: +// Token string +func TestDecodeAgentToken(t *testing.T) { + t.Skip("DONE. no special fields to parse; no decodeBody callback used.") +} + +// ================================== +// $GOPATH/github.com/hashicorp/consul/agent/agent_endpoint.go: +// 1332 // Decode the request from the request body +// 1333 var authReq structs.ConnectAuthorizeRequest +// 1334: if err := decodeBody(req, &authReq, nil); err != nil { +// 1335 return nil, BadRequestError{fmt.Sprintf("Request decode failed: %v", err)} +// 1336 } +// ================================== +// ConnectAuthorizeRequest: +// Target string +// ClientCertURI string +// ClientCertSerial string +func TestDecodeAgentConnectAuthorize(t *testing.T) { + t.Skip("DONE. no special fields to parse; no decodeBody callback used.") +} + +// ================================== +// $GOPATH/github.com/hashicorp/consul/agent/catalog_endpoint.go: +// 18 +// 19 var args structs.RegisterRequest +// 20: if err := decodeBody(req, &args, durations.FixupDurations); err != nil { +// 21 resp.WriteHeader(http.StatusBadRequest) +// 22 fmt.Fprintf(resp, "Request decode failed: %v", 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)) + req := httptest.NewRequest("POST", "http://foo.com", body) + + var out structs.RegisterRequest + err := decodeBody(req, &out, durations.FixupDurations) + if err == nil && tc.wantErr { + t.Fatal("expected err, got nil") + } + if err != nil && !tc.wantErr { + t.Fatalf("expected nil error, got %v", err) + } + + // 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) + } + }) + } +} + +// ================================== +// $GOPATH/github.com/hashicorp/consul/agent/catalog_endpoint.go: +// 47 +// 48 var args structs.DeregisterRequest +// 49: if err := decodeBody(req, &args, nil); err != nil { +// 50 resp.WriteHeader(http.StatusBadRequest) +// 51 fmt.Fprintf(resp, "Request decode failed: %v", err) +// ================================== +// DeregisterRequest: +// Datacenter string +// Node string +// ServiceID string +// CheckID types.CheckID +// WriteRequest structs.WriteRequest +// Token string +func TestDecodeCatalogDeregister(t *testing.T) { + t.Skip("DONE. no special fields to parse; no decodeBody callback used.") +} + +// ================================== +// $GOPATH/github.com/hashicorp/consul/agent/config_endpoint.go: +// 104 +// 105 var raw map[string]interface{} +// 106: if err := decodeBody(req, &raw, nil); err != nil { +// 107 return nil, BadRequestError{Reason: fmt.Sprintf("Request decoding failed: %v", err)} +// 108 } +// ================================== +func TestDecodeConfigApply(t *testing.T) { + // TODO $$ + t.Skip("Leave this fn as-is? Decoding code should probably be the same for all config parsing.") + +} + +// ================================== +// $GOPATH/github.com/hashicorp/consul/agent/connect_ca_endpoint.go: +// 63 s.parseDC(req, &args.Datacenter) +// 64 s.parseToken(req, &args.Token) +// 65: if err := decodeBody(req, &args.Config, nil); err != nil { +// 66 resp.WriteHeader(http.StatusBadRequest) +// 67 fmt.Fprintf(resp, "Request decode failed: %v", err) +// ================================== +// CARequest: +// Config *structs.CAConfiguration +// ClusterID string +// Provider string +// Config map[string]interface {} +// RaftIndex structs.RaftIndex +func TestDecodeConnectCAConfigurationSet(t *testing.T) { + t.Skip("DONE. no special fields to parse; no decodeBody callback used.") +} + +// ================================== +// $GOPATH/github.com/hashicorp/consul/agent/coordinate_endpoint.go: +// 151 +// 152 args := structs.CoordinateUpdateRequest{} +// 153: if err := decodeBody(req, &args, nil); err != nil { +// 154 resp.WriteHeader(http.StatusBadRequest) +// 155 fmt.Fprintf(resp, "Request decode failed: %v", err) +// ================================== +// CoordinateUpdateRequest: +// Datacenter string +// Node string +// Segment string +// Coord *coordinate.Coordinate +// Vec []float64 +// Error float64 +// Adjustment float64 +// Height float64 +// WriteRequest structs.WriteRequest +// Token string +func TestDecodeCoordinateUpdate(t *testing.T) { + t.Skip("DONE. no special fields to parse; no decodeBody callback used.") +} + +// ================================== +// $GOPATH/github.com/hashicorp/consul/agent/discovery_chain_endpoint.go: +// 29 if req.Method == "POST" { +// 30 var raw map[string]interface{} +// 31: if err := decodeBody(req, &raw, nil); err != nil { +// 32 return nil, BadRequestError{Reason: fmt.Sprintf("Request decoding failed: %v", err)} +// 33 } +// ================================== +// discoveryChainReadRequest: +// OverrideMeshGateway structs.MeshGatewayConfig +// Mode structs.MeshGatewayMode // string +// OverrideProtocol string +// OverrideConnectTimeout time.Duration +func TestDecodeDiscoveryChainRead(t *testing.T) { + // Special Beast! + + // This decodeBody call is a special beast, in that it decodes with decodeBody + // into a map[string]interface{} and runs subsequent decoding logic outside of + // the call. + + // decode code copied from agent/discovery_chain_endpoint.go + fullDecodeFn := func(req *http.Request, v *discoveryChainReadRequest) error { + var raw map[string]interface{} + if err := decodeBody(req, &raw, nil); err != nil { + return fmt.Errorf("Request decoding failed: %v", err) + } + + apiReq, err := decodeDiscoveryChainReadRequest(raw) + if err != nil { + return fmt.Errorf("Request decoding failed: %v", err) + } + + *v = *apiReq + + return nil + } + + // It doesn't seem as though mapstructure does weakly typed durations. + var weaklyTypedDurationTCs = []translateValueTestCase{ + { + desc: "positive string integer (weakly typed)", + durations: &durationTC{ + in: `"2000"`, + }, + wantErr: true, + }, + { + desc: "negative string integer (weakly typed)", + durations: &durationTC{ + in: `"-50"`, + }, + wantErr: true, + }, + } + + for _, tc := range append(durationTestCases, weaklyTypedDurationTCs...) { + t.Run(tc.desc, func(t *testing.T) { + // set up request body + jsonStr := fmt.Sprintf(`{ + "OverrideConnectTimeout": %s + }`, tc.durations.in) + body := bytes.NewBuffer([]byte(jsonStr)) + req := httptest.NewRequest("POST", "http://foo.com", body) + + var out discoveryChainReadRequest + // fullDecodeFn is declared above in this test. + err := fullDecodeFn(req, &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.OverrideConnectTimeout != tc.durations.want { + t.Fatalf("expected OverrideConnectTimeout to be %s, got %s", tc.durations.want, out.OverrideConnectTimeout) + } + }) + } + + // Other possibly weakly-typed inputs.. + var weaklyTypedStringTCs = []struct { + desc string + in, want string + wantErr bool + }{ + { + desc: "positive integer for string field (weakly typed)", + in: `200`, + want: "200", + }, + { + desc: "negative integer for string field (weakly typed)", + in: `-200`, + want: "-200", + }, + { + desc: "bool for string field (weakly typed)", + in: `true`, + want: "1", + }, + { + desc: "float for string field (weakly typed)", + in: `1.2223`, + want: "1.2223", + }, + { + desc: "map for string field (weakly typed)", + in: `{}`, + wantErr: true, + }, + { + desc: "slice for string field (weakly typed)", + in: `[]`, + want: "", + }, + } + + for _, tc := range weaklyTypedStringTCs { + t.Run(tc.desc, func(t *testing.T) { + // set up request body + jsonStr := fmt.Sprintf(`{ + "OverrideProtocol": %[1]s, + "OverrideMeshGateway": {"Mode": %[1]s} + }`, tc.in) + body := bytes.NewBuffer([]byte(jsonStr)) + req := httptest.NewRequest("POST", "http://foo.com", body) + + var out discoveryChainReadRequest + // fullDecodeFn is declared above in this test. + err := fullDecodeFn(req, &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.OverrideProtocol != tc.want { + t.Fatalf("expected OverrideProtocol to be %s, got %s", tc.want, out.OverrideProtocol) + } + if out.OverrideMeshGateway.Mode != structs.MeshGatewayMode(tc.want) { + t.Fatalf("expected OverrideMeshGateway.Mode to be %s, got %s", tc.want, out.OverrideMeshGateway.Mode) + } + }) + } + + // translate field tcs + + overrideMeshGatewayFields := []string{ + `"OverrideMeshGateway": {"Mode": %s}`, + `"override_mesh_gateway": {"Mode": %s}`, + } + + overrideMeshGatewayEqFn := func(out interface{}, want interface{}) error { + got := out.(discoveryChainReadRequest).OverrideMeshGateway.Mode + if got != structs.MeshGatewayMode(want.(string)) { + return fmt.Errorf("expected OverrideMeshGateway to be %s, got %s", want, got) + } + return nil + } + + var translateOverrideMeshGatewayTCs = []translateKeyTestCase{ + { + desc: "OverrideMeshGateway: both set", + in: []interface{}{`"one"`, `"two"`}, + want: "one", + jsonFmtStr: `{` + strings.Join(overrideMeshGatewayFields, ",") + `}`, + equalityFn: overrideMeshGatewayEqFn, + }, + { + desc: "OverrideMeshGateway: first set", + in: []interface{}{`"one"`}, + want: "one", + jsonFmtStr: `{` + overrideMeshGatewayFields[0] + `}`, + equalityFn: overrideMeshGatewayEqFn, + }, + { + desc: "OverrideMeshGateway: second set", + in: []interface{}{`"two"`}, + want: "two", + jsonFmtStr: `{` + overrideMeshGatewayFields[1] + `}`, + equalityFn: overrideMeshGatewayEqFn, + }, + { + desc: "OverrideMeshGateway: neither set", + in: []interface{}{}, + want: "", // zero value + jsonFmtStr: `{}`, + equalityFn: overrideMeshGatewayEqFn, + }, + } + + overrideProtocolFields := []string{ + `"OverrideProtocol": %s`, + `"override_protocol": %s`, + } + + overrideProtocolEqFn := func(out interface{}, want interface{}) error { + got := out.(discoveryChainReadRequest).OverrideProtocol + if got != want { + return fmt.Errorf("expected OverrideProtocol to be %s, got %s", want, got) + } + return nil + } + + var translateOverrideProtocolTCs = []translateKeyTestCase{ + { + desc: "OverrideProtocol: both set", + in: []interface{}{`"one"`, `"two"`}, + want: "one", + jsonFmtStr: `{` + strings.Join(overrideProtocolFields, ",") + `}`, + equalityFn: overrideProtocolEqFn, + }, + { + desc: "OverrideProtocol: first set", + in: []interface{}{`"one"`}, + want: "one", + jsonFmtStr: `{` + overrideProtocolFields[0] + `}`, + equalityFn: overrideProtocolEqFn, + }, + { + desc: "OverrideProtocol: second set", + in: []interface{}{`"two"`}, + want: "two", + jsonFmtStr: `{` + overrideProtocolFields[1] + `}`, + equalityFn: overrideProtocolEqFn, + }, + { + desc: "OverrideProtocol: neither set", + in: []interface{}{}, + want: "", // zero value + jsonFmtStr: `{}`, + equalityFn: overrideProtocolEqFn, + }, + } + + overrideConnectTimeoutFields := []string{ + `"OverrideConnectTimeout": %s`, + `"override_connect_timeout": %s`, + } + + overrideConnectTimeoutEqFn := func(out interface{}, want interface{}) error { + got := out.(discoveryChainReadRequest).OverrideConnectTimeout + if got != want { + return fmt.Errorf("expected OverrideConnectTimeout to be %s, got %s", want, got) + } + return nil + } + + var translateOverrideConnectTimeoutTCs = []translateKeyTestCase{ + { + desc: "OverrideConnectTimeout: both set", + in: []interface{}{`"2h0m"`, `"3h0m"`}, + want: 2 * time.Hour, + jsonFmtStr: "{" + strings.Join(overrideConnectTimeoutFields, ",") + "}", + equalityFn: overrideConnectTimeoutEqFn, + }, + { + desc: "OverrideConnectTimeout: first set", + in: []interface{}{`"2h0m"`}, + want: 2 * time.Hour, + jsonFmtStr: "{" + overrideConnectTimeoutFields[0] + "}", + equalityFn: overrideConnectTimeoutEqFn, + }, + { + desc: "OverrideConnectTimeout: second set", + in: []interface{}{`"3h0m"`}, + want: 3 * time.Hour, + jsonFmtStr: "{" + overrideConnectTimeoutFields[1] + "}", + equalityFn: overrideConnectTimeoutEqFn, + }, + { + desc: "OverrideConnectTimeout: neither set", + in: []interface{}{}, + want: time.Duration(0), + jsonFmtStr: "{}", + equalityFn: overrideConnectTimeoutEqFn, + }, + } + + // from decodeDiscoveryChainReadRequest: + // + // lib.TranslateKeys(raw, map[string]string{ + // "override_mesh_gateway": "overridemeshgateway", + // "override_protocol": "overrideprotocol", + // "override_connect_timeout": "overrideconnecttimeout", + // }) + + translateFieldTCs := [][]translateKeyTestCase{ + translateOverrideMeshGatewayTCs, + translateOverrideProtocolTCs, + translateOverrideConnectTimeoutTCs, + } + + for _, tcGroup := range translateFieldTCs { + for _, tc := range tcGroup { + t.Run(tc.desc, func(t *testing.T) { + jsonStr := fmt.Sprintf(tc.jsonFmtStr, tc.in...) + body := bytes.NewBuffer([]byte(jsonStr)) + req := httptest.NewRequest("POST", "http://foo.com", body) + + var out discoveryChainReadRequest + // fullDecodeFn is declared above in this test. + err := fullDecodeFn(req, &out) + if err != nil { + t.Fatal(err) + } + + if err := tc.equalityFn(out, tc.want); err != nil { + t.Fatal(err) + } + }) + } + } + +} + +// ================================== +// $GOPATH/github.com/hashicorp/consul/agent/intentions_endpoint.go: +// 66 s.parseDC(req, &args.Datacenter) +// 67 s.parseToken(req, &args.Token) +// 68: if err := decodeBody(req, &args.Intention, fixHashField); err != nil { +// 69 return nil, fmt.Errorf("Failed to decode request body: %s", err) +// 70 } +// ================================== +// 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)) + + // set up request + body := bytes.NewBuffer(bodyBytes) + req := httptest.NewRequest("POST", "http://foo.com", body) + + // decode body + var out structs.Intention + err := decodeBody(req, &out, fixHashField) + 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) + } + } + }) + + } +} + +// ================================== +// $GOPATH/github.com/hashicorp/consul/agent/intentions_endpoint.go: +// 259 s.parseDC(req, &args.Datacenter) +// 260 s.parseToken(req, &args.Token) +// 261: if err := decodeBody(req, &args.Intention, fixHashField); err != nil { +// 262 return nil, BadRequestError{Reason: fmt.Sprintf("Request decode failed: %v", err)} +// 263 } +// ================================== +func TestDecodeIntentionSpecificUpdate(t *testing.T) { + t.Skip("DONE. COVERED BY ABOVE (same structs.Intention)") +} + +// ================================== +// $GOPATH/github.com/hashicorp/consul/agent/operator_endpoint.go: +// 77 var args keyringArgs +// 78 if req.Method == "POST" || req.Method == "PUT" || req.Method == "DELETE" { +// 79: if err := decodeBody(req, &args, nil); err != nil { +// 80 return nil, BadRequestError{Reason: fmt.Sprintf("Request decode failed: %v", err)} +// 81 } +// ================================== +// type keyringArgs struct { +// Key string +// Token string +// RelayFactor uint8 +// LocalOnly bool // ?local-only; only used for GET requests +// } +func TestDecodeOperatorKeyringEndpoint(t *testing.T) { + t.Skip("DONE. no special fields to parse; no decodeBody callback used.") +} + +// ================================== +// $GOPATH/github.com/hashicorp/consul/agent/operator_endpoint.go: +// 219 var conf api.AutopilotConfiguration +// 220 durations := NewDurationFixer("lastcontactthreshold", "serverstabilizationtime") +// 221: if err := decodeBody(req, &conf, durations.FixupDurations); err != nil { +// 222 return nil, BadRequestError{Reason: fmt.Sprintf("Error parsing autopilot config: %v", err)} +// 223 } +// ================================== +// 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)) + req := httptest.NewRequest("POST", "http://foo.com", body) + + var out api.AutopilotConfiguration + err := decodeBody(req, &out, durations.FixupDurations) + 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) + } + }) + } +} + +// ================================== +// $GOPATH/github.com/hashicorp/consul/agent/prepared_query_endpoint.go: +// 24 s.parseDC(req, &args.Datacenter) +// 25 s.parseToken(req, &args.Token) +// 26: if err := decodeBody(req, &args.Query, nil); err != nil { +// 27 resp.WriteHeader(http.StatusBadRequest) +// 28 fmt.Fprintf(resp, "Request decode failed: %v", err) +// ================================== +// PreparedQueryRequest: +// Datacenter string +// Op structs.PreparedQueryOp +// Query *structs.PreparedQuery +// ID string +// Name string +// Session string +// Token string +// Template structs.QueryTemplateOptions +// Type string +// Regexp string +// RemoveEmptyTags bool +// Service structs.ServiceQuery +// Service string +// Failover structs.QueryDatacenterOptions +// NearestN int +// Datacenters []string +// OnlyPassing bool +// IgnoreCheckIDs []types.CheckID +// Near string +// Tags []string +// NodeMeta map[string]string +// ServiceMeta map[string]string +// Connect bool +// DNS structs.QueryDNSOptions +// TTL string +// RaftIndex structs.RaftIndex +// CreateIndex uint64 +// ModifyIndex uint64 +// WriteRequest structs.WriteRequest +// Token string +func TestDecodePreparedQueryGeneral_Create(t *testing.T) { + t.Skip("DONE. no special fields to parse; no decodeBody callback used.") +} + +// ================================== +// $GOPATH/github.com/hashicorp/consul/agent/prepared_query_endpoint.go: +// 254 s.parseToken(req, &args.Token) +// 255 if req.ContentLength > 0 { +// 256: if err := decodeBody(req, &args.Query, nil); err != nil { +// 257 resp.WriteHeader(http.StatusBadRequest) +// 258 fmt.Fprintf(resp, "Request decode failed: %v", err) +// ================================== +func TestDecodePreparedQueryGeneral_Update(t *testing.T) { + t.Skip("DONE. COVERED BY ABOVE (same structs.PreparedQuery)") +} + +// ================================== +// $GOPATH/github.com/hashicorp/consul/agent/session_endpoint.go: +// 54 return nil +// 55 } +// 56: if err := decodeBody(req, &args.Session, fixup); err != nil { +// 57 resp.WriteHeader(http.StatusBadRequest) +// 58 fmt.Fprintf(resp, "Request decode failed: %v", err) +// ================================== +// 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 + + // copied from agent/session_endpoint.go + fixupCB := func(raw interface{}) error { + if err := FixupLockDelay(raw); err != nil { + return err + } + if err := FixupChecks(raw, &outSession); err != nil { + return err + } + return nil + } + + // 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, + }, + }, + // // Test cases that illicit bad behavior; Don't run them. + // translateValueTestCase{ + // desc: "durations large, numeric and negative", + // durations: &durationTC{ + // in: `-2000`, + // want: time.Duration(-2000), + // }, + // // --- FAIL: TestDecodeSessionCreate/durations_large,_numeric_and_negative (0.00s) + // // http_decode_test.go:2665: expected LockDelay to be -2µs, got -33m20s + // }, + // translateValueTestCase{ + // desc: "durations string, negative", + // durations: &durationTC{ + // in: `"-50ms"`, + // want: -50 * time.Millisecond, + // }, + // // --- FAIL: TestDecodeSessionCreate/durations_string,_negative (0.00s) + // // http_decode_test.go:2665: expected LockDelay to be -50ms, got -13888h53m20s + // }, + ) + + 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)) + req := httptest.NewRequest("POST", "http://foo.com", body) + + // outSession var is shared among test cases + err := decodeBody(req, &outSession, fixupCB) + 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)) + req := httptest.NewRequest("POST", "http://foo.com", body) + + // outSession var is shared among test cases + err := decodeBody(req, &outSession, fixupCB) + 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) + } + } + }) + } + +} + +// ================================== +// $GOPATH/github.com/hashicorp/consul/agent/txn_endpoint.go: +// 116 // associate the error with a given operation. +// 117 var ops api.TxnOps +// 118: if err := decodeBody(req, &ops, fixupTxnOps); err != nil { +// 119 resp.WriteHeader(http.StatusBadRequest) +// 120 fmt.Fprintf(resp, "Failed to parse body: %v", err) +// ================================== +// 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 +// 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)) + req := httptest.NewRequest("POST", "http://foo.com", body) + + var out api.TxnOps + err := decodeBody(req, &out, fixupTxnOps) + 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) + } + }) + } +} + +// ======================================= +// Benchmarks: +// ================================== +// $GOPATH/github.com/hashicorp/consul/agent/http.go: +// 574 +// 575 // decodeBody is used to decode a JSON request body +// 576: func decodeBody(req *http.Request, out interface{}, cb func(interface{}) error) error { +// 577 // This generally only happens in tests since real HTTP requests set +// 578 // a non-nil body with no content. We guard against it anyways to prevent +// ================================== +func BenchmarkDecodeBody(b *testing.B) { + b.Skip() // TODO: benchmark +} + +// ========================================= +// 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 +}