Repository: google/go-github (go)

Issue:
HookRequest.Headers and HookResponse.Headers maps need to be canonicalized
As discussed in #3554, attempting to access known header keys directly from the maps provided by:
* [HookRequest.Headers](https://github.com/google/go-github/blob/c099baf334af9b3362514dd9d3164c4be0097056/github/repos_hooks_deliveries.go#L45)
* [HookResponse.Headers](https://github.com/google/go-github/blob/c099baf334af9b3362514dd9d3164c4be0097056/github/repos_hooks_deliveries.go#L56)

using the [provided constant, canonicalized header keys](https://github.com/google/go-github/blob/c099baf334af9b3362514dd9d3164c4be0097056/github/messages.go#L36-L43) does not work when the external header keys do not exactly match the case of our provided constants.

There are two solutions to this problem:

Solution 1:
* canonicalize all header keys every time a `HookDelivery` struct is returned ([here](https://github.com/google/go-github/blob/c099baf334af9b3362514dd9d3164c4be0097056/github/repos_hooks_deliveries.go#L69), [here](https://github.com/google/go-github/blob/c099baf334af9b3362514dd9d3164c4be0097056/github/repos_hooks_deliveries.go#L95), and [here](https://github.com/google/go-github/blob/c099baf334af9b3362514dd9d3164c4be0097056/github/repos_hooks_deliveries.go#L116), plus six more locations).

Solution 2:
* add two new helper methods that would provide a [Header.Get](https://pkg.go.dev/net/http#Header.Get)-like experience for `HookRequest` and `HookResponse` such that the header key lookup is totally case insensitive (both for the search strings and for the contents of the map), like this:

```go
// GetHeader gets the value associated with the given key (ignoring key case).
func (h *HookRequest) GetHeader(key string) string {
  ...
}

// GetHeader gets the value associated with the given key (ignoring key case).
func (h *HookResponse) GetHeader(key string) string {
  ...
}
```

I'm open to either solution. @sparshev - do you have a preference?

This would be a great PR for any new contributor to this repo or a new Go developer.
All contributions are greatly appreciated!

Feel free to volunteer for any issue and the issue can be assigned to you so that others don't attempt to duplicate the work.
Also feel free to perform a code review on any PR that has the label "NeedsReview" - we appreciate those contributions too!
We have a `REVIEWERS` file that you could add your username to if you are willing to be contacted when reviews are needed.

Please check out our [CONTRIBUTING.md](https://github.com/google/go-github/blob/master/CONTRIBUTING.md) guide to get started. (In particular, please remember to `go generate ./...` and don't use force-push to your PRs.)

Thank you!

Pull request:
Add case-instensitive GetHeader for HookRequest & HookResponse
Simple implementation for case-insensitive GetHeader. I thought about caching the keys, but it seems too complicated for now with additional not predictable memory consumption. Tests are included.

Fixes #3555

Patch:
diff --git a/github/repos_hooks_deliveries.go b/github/repos_hooks_deliveries.go
index c8029f62..bcd4b336 100644
--- a/github/repos_hooks_deliveries.go
+++ b/github/repos_hooks_deliveries.go
@@ -9,6 +9,7 @@ import (
 	"context"
 	"encoding/json"
 	"fmt"
+	"strings"
 )
 
 // HookDelivery represents the data that is received from GitHub's Webhook Delivery API
@@ -39,6 +40,16 @@ func (d HookDelivery) String() string {
 	return Stringify(d)
 }
 
+// getHeader common function for GetHeader funcs of HookRequest & HookResponse.
+func getHeader(headers map[string]string, key string) string {
+	for k, v := range headers {
+		if strings.EqualFold(k, key) {
+			return v
+		}
+	}
+	return ""
+}
+
 // HookRequest is a part of HookDelivery that contains
 // the HTTP headers and the JSON payload of the webhook request.
 type HookRequest struct {
@@ -46,6 +57,11 @@ type HookRequest struct {
 	RawPayload *json.RawMessage  `json:"payload,omitempty"`
 }
 
+// GetHeader gets the value associated with the given key (ignoring key case).
+func (r *HookRequest) GetHeader(key string) string {
+	return getHeader(r.Headers, key)
+}
+
 func (r HookRequest) String() string {
 	return Stringify(r)
 }
@@ -57,6 +73,11 @@ type HookResponse struct {
 	RawPayload *json.RawMessage  `json:"payload,omitempty"`
 }
 
+// GetHeader gets the value associated with the given key (ignoring key case).
+func (r *HookResponse) GetHeader(key string) string {
+	return getHeader(r.Headers, key)
+}
+
 func (r HookResponse) String() string {
 	return Stringify(r)
 }


Test patch:
diff --git a/github/repos_hooks_deliveries_test.go b/github/repos_hooks_deliveries_test.go
index 6acb7a81..91ff3ad7 100644
--- a/github/repos_hooks_deliveries_test.go
+++ b/github/repos_hooks_deliveries_test.go
@@ -294,6 +294,41 @@ func TestHookRequest_Marshal(t *testing.T) {
 	testJSONMarshal(t, r, want)
 }
 
+func TestHookRequest_GetHeader(t *testing.T) {
+	t.Parallel()
+
+	header := make(map[string]string)
+	header["key1"] = "value1"
+	header["Key+2"] = "value2"
+	header["kEy-3"] = "value3"
+	header["KEY_4"] = "value4"
+
+	r := &HookRequest{
+		Headers: header,
+	}
+
+	// Checking positive cases
+	testPrefixes := []string{"key", "Key", "kEy", "KEY"}
+	for hdrKey, hdrValue := range header {
+		for _, prefix := range testPrefixes {
+			key := prefix + hdrKey[3:]
+			if val := r.GetHeader(key); val != hdrValue {
+				t.Errorf("GetHeader(%q) is not working: %q != %q", key, val, hdrValue)
+			}
+		}
+	}
+
+	// Checking negative case
+	key := "asd"
+	if val := r.GetHeader(key); val != "" {
+		t.Errorf("GetHeader(%q) should return empty value: %q != %q", key, val, "")
+	}
+	key = "kay1"
+	if val := r.GetHeader(key); val != "" {
+		t.Errorf("GetHeader(%q) should return empty value: %q != %q", key, val, "")
+	}
+}
+
 func TestHookResponse_Marshal(t *testing.T) {
 	t.Parallel()
 	testJSONMarshal(t, &HookResponse{}, "{}")
@@ -320,6 +355,41 @@ func TestHookResponse_Marshal(t *testing.T) {
 	testJSONMarshal(t, r, want)
 }
 
+func TestHookResponse_GetHeader(t *testing.T) {
+	t.Parallel()
+
+	header := make(map[string]string)
+	header["key1"] = "value1"
+	header["Key+2"] = "value2"
+	header["kEy-3"] = "value3"
+	header["KEY_4"] = "value4"
+
+	r := &HookResponse{
+		Headers: header,
+	}
+
+	// Checking positive cases
+	testPrefixes := []string{"key", "Key", "kEy", "KEY"}
+	for hdrKey, hdrValue := range header {
+		for _, prefix := range testPrefixes {
+			key := prefix + hdrKey[3:]
+			if val := r.GetHeader(key); val != hdrValue {
+				t.Errorf("GetHeader(%q) is not working: %q != %q", key, val, hdrValue)
+			}
+		}
+	}
+
+	// Checking negative case
+	key := "asd"
+	if val := r.GetHeader(key); val != "" {
+		t.Errorf("GetHeader(%q) should return empty value: %q != %q", key, val, "")
+	}
+	key = "kay1"
+	if val := r.GetHeader(key); val != "" {
+		t.Errorf("GetHeader(%q) should return empty value: %q != %q", key, val, "")
+	}
+}
+
 func TestHookDelivery_Marshal(t *testing.T) {
 	t.Parallel()
 	testJSONMarshal(t, &HookDelivery{}, "{}")