Repository: avenga/couper
Pull request #461 (2022-03-30 07:10:15)

Object keys uniqueness check
Keys in an object type attribute value are only handled case-insenstively if reasonable (e.g. the represent HTTP methods or header field values).

---
<details>
    <summary>Reviewer checklist</summary>
    <ul>
        <li>Read PR description: a summary about the changes is required</li>
        <li>Changelog updated</li>
        <li>Documentation: docs/{Reference, Cli, ...}, Docker and cli help/usage</li>
        <li>Pulled branch, manually tested</li>
        <li>Verified requirements are met</li>
        <li>Reviewed the code</li>
        <li>Reviewed the related tests</li>
    </ul>
</details>



Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1bf47dca..cb6657c4 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -8,6 +8,9 @@ Unreleased changes are available as `avenga/couper:edge` container.
   * Couper [reads and merges configuration files](./docs/CLI.md#global-options) from a given directory ([#437](https://github.com/avenga/couper/pull/437))
     * provided via `-d` command-line flag or `COUPER_FILE_DIRECTORY` environment variable
 
+* **Fixed**
+  * Keys in object type attribute values are only handled case-insensitively if reasonable (e.g. they represent HTTP methods or header field values) ([#461](https://github.com/avenga/couper/pull/461))
+
 ---
 
 ## [1.8.0](https://github.com/avenga/couper/releases/tag/v1.8.0)
diff --git a/config/configload/schema.go b/config/configload/schema.go
index e361c420..57de9b3d 100644
--- a/config/configload/schema.go
+++ b/config/configload/schema.go
@@ -264,13 +264,18 @@ func completeSchemaComponents(body hcl.Body, schema *hcl.BodySchema, attrs hcl.A
 							continue
 						}
 
-						keyName := strings.ToLower(seetie.ValueToString(k))
+						keyName := seetie.ValueToString(k)
+						switch name {
+						case "add_request_headers", "add_response_headers", "beta_scope", "headers", "set_request_headers", "set_response_headers":
+							// header field names, method names: handle object keys case-insensitively
+							keyName = strings.ToLower(keyName)
+						}
 						if _, ok := unique[keyName]; ok {
 							errors = errors.Append(&hcl.Diagnostic{
 								Subject:  &expr.SrcRange,
 								Severity: hcl.DiagError,
 								Summary:  fmt.Sprintf("key in an attribute must be unique: '%s'", keyName),
-								Detail:   "Key must be unique for " + string(keyName) + ".",
+								Detail:   "Key must be unique for " + keyName + ".",
 							})
 						}
 


Test patch:
diff --git a/config/configload/validate_test.go b/config/configload/validate_test.go
index b4a1d95d..ef0cd54a 100644
--- a/config/configload/validate_test.go
+++ b/config/configload/validate_test.go
@@ -194,3 +194,222 @@ func TestLabels(t *testing.T) {
 		})
 	}
 }
+
+func TestAttributeObjectKeys(t *testing.T) {
+	tests := []struct {
+		name  string
+		hcl   string
+		error string
+	}{
+		{
+			"add_request_headers",
+			`server {
+  api {
+    endpoint "/a" {
+      add_request_headers = {
+        a = "a"
+        A = "A"
+      }
+    }
+  }
+}`,
+			"couper.hcl:4,29-7,8: key in an attribute must be unique: 'a'; Key must be unique for a.",
+		},
+		{
+			"add_response_headers",
+			`server {
+  api {
+    endpoint "/a" {
+      add_response_headers = {
+        a = "a"
+        A = "A"
+      }
+    }
+  }
+}`,
+			"couper.hcl:4,30-7,8: key in an attribute must be unique: 'a'; Key must be unique for a.",
+		},
+		{
+			"beta_scope",
+			`server {
+  api {
+    endpoint "/a" {
+      beta_scope = {
+        get = "a"
+        GeT = "A"
+      }
+    }
+  }
+}`,
+			"couper.hcl:4,20-7,8: key in an attribute must be unique: 'get'; Key must be unique for get.",
+		},
+		{
+			"headers",
+			`server {
+  api {
+    endpoint "/a" {
+      request {
+        headers = {
+          a = "a"
+          A = "A"
+        }
+      }
+    }
+  }
+}`,
+			"couper.hcl:5,19-8,10: key in an attribute must be unique: 'a'; Key must be unique for a.",
+		},
+		{
+			"set_request_headers",
+			`server {
+  api {
+    endpoint "/a" {
+      set_request_headers = {
+        a = "a"
+        A = "A"
+      }
+    }
+  }
+}`,
+			"couper.hcl:4,29-7,8: key in an attribute must be unique: 'a'; Key must be unique for a.",
+		},
+		{
+			"set_response_headers",
+			`server {
+  api {
+    endpoint "/a" {
+      set_response_headers = {
+        a = "a"
+        A = "A"
+      }
+    }
+  }
+}`,
+			"couper.hcl:4,30-7,8: key in an attribute must be unique: 'a'; Key must be unique for a.",
+		},
+		{
+			"json_body",
+			`server {
+  api {
+    endpoint "/a" {
+      request {
+        json_body = {
+          a = "a"
+          A = "A"
+        }
+      }
+    }
+  }
+}`,
+			"",
+		},
+		{
+			"form_body",
+			`server {
+  api {
+    endpoint "/a" {
+      request {
+        form_body = {
+          a = "a"
+          A = "A"
+        }
+      }
+    }
+  }
+}`,
+			"",
+		},
+		{
+			"beta_roles_map",
+			`server {}
+definitions {
+  jwt "a" {
+    signature_algorithm = "HS256"
+    key = "asdf"
+    beta_roles_map = {
+      a = []
+      A = []
+	}
+  }
+}`,
+			"",
+		},
+		{
+			"beta_scope_map",
+			`server {}
+definitions {
+  jwt "a" {
+    signature_algorithm = "HS256"
+    key = "asdf"
+    beta_scope_map = {
+      a = []
+      A = []
+	}
+  }
+}`,
+			"",
+		},
+		{
+			"claims",
+			`server {}
+definitions {
+  jwt "a" {
+    signature_algorithm = "HS256"
+    key = "asdf"
+    claims = {
+      a = "a"
+      A = "A"
+	}
+  }
+}`,
+			"",
+		},
+		{
+			"custom_log_fields",
+			`server {}
+definitions {
+  jwt "a" {
+    signature_algorithm = "HS256"
+    key = "asdf"
+    custom_log_fields = {
+      a = "a"
+      A = "A"
+	}
+  }
+}`,
+			"",
+		},
+		{
+			"environment_variables",
+			`server {}
+defaults {
+  environment_variables = {
+    a = "a"
+    A = "A"
+  }
+}`,
+			"",
+		},
+	}
+
+	logger, _ := logrustest.NewNullLogger()
+	log := logger.WithContext(context.TODO())
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(subT *testing.T) {
+			conf, err := LoadBytes([]byte(tt.hcl), "couper.hcl")
+			if conf != nil {
+				_, err = runtime.NewServerConfiguration(conf, log, nil)
+			}
+
+			var errMsg string
+			if err != nil {
+				errMsg = err.Error()
+			}
+
+			if tt.error != errMsg {
+				subT.Errorf("%q: Unexpected configuration error:\n\tWant: %q\n\tGot:  %q", tt.name, tt.error, errMsg)
+			}
+		})
+	}
+}