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 }