Repository: google/go-github
Pull request #746 (2017-10-05 19:11:52)

Adds state to project options per github's docs
State was missing, useful for updating projects per:
https://developer.github.com/v3/projects/#update-a-project

Adds the ability to `open`/`close` projects based on `ProjectOptions` with `ProjectUpdate`


Patch:
diff --git a/github/messages.go b/github/messages.go
index c0f315a7..3a4d4644 100644
--- a/github/messages.go
+++ b/github/messages.go
@@ -20,6 +20,7 @@ import (
 	"hash"
 	"io/ioutil"
 	"net/http"
+	"net/url"
 	"strings"
 )
 
@@ -122,6 +123,8 @@ func messageMAC(signature string) ([]byte, func() hash.Hash, error) {
 
 // ValidatePayload validates an incoming GitHub Webhook event request
 // and returns the (JSON) payload.
+// The Content-Type header of the payload can be "application/json" or "application/x-www-form-urlencoded".
+// If the Content-Type is neither then an error is returned.
 // secretKey is the GitHub Webhook secret message.
 //
 // Example usage:
@@ -133,13 +136,43 @@ func messageMAC(signature string) ([]byte, func() hash.Hash, error) {
 //     }
 //
 func ValidatePayload(r *http.Request, secretKey []byte) (payload []byte, err error) {
-	payload, err = ioutil.ReadAll(r.Body)
-	if err != nil {
-		return nil, err
+	var body []byte // Raw body that GitHub uses to calculate the signature.
+
+	switch ct := r.Header.Get("Content-Type"); ct {
+	case "application/json":
+		var err error
+		if body, err = ioutil.ReadAll(r.Body); err != nil {
+			return nil, err
+		}
+
+		// If the content type is application/json,
+		// the JSON payload is just the original body.
+		payload = body
+
+	case "application/x-www-form-urlencoded":
+		// payloadFormParam is the name of the form parameter that the JSON payload
+		// will be in if a webhook has its content type set to application/x-www-form-urlencoded.
+		const payloadFormParam = "payload"
+
+		var err error
+		if body, err = ioutil.ReadAll(r.Body); err != nil {
+			return nil, err
+		}
+
+		// If the content type is application/x-www-form-urlencoded,
+		// the JSON payload will be under the "payload" form param.
+		form, err := url.ParseQuery(string(body))
+		if err != nil {
+			return nil, err
+		}
+		payload = []byte(form.Get(payloadFormParam))
+
+	default:
+		return nil, fmt.Errorf("Webhook request has unsupported Content-Type %q", ct)
 	}
 
 	sig := r.Header.Get(signatureHeader)
-	if err := validateSignature(sig, payload, secretKey); err != nil {
+	if err := validateSignature(sig, body, secretKey); err != nil {
 		return nil, err
 	}
 	return payload, nil
diff --git a/github/projects.go b/github/projects.go
index 89685c9d..a9b143df 100644
--- a/github/projects.go
+++ b/github/projects.go
@@ -65,6 +65,12 @@ type ProjectOptions struct {
 	Name string `json:"name,omitempty"`
 	// The body of the project. (Optional.)
 	Body string `json:"body,omitempty"`
+
+	// The following field(s) are only applicable for update.
+	// They should be left with zero values for creation.
+
+	// State of the project. Either "open" or "closed". (Optional.)
+	State string `json:"state,omitempty"`
 }
 
 // UpdateProject updates a repository project.


Test patch:
diff --git a/github/messages_test.go b/github/messages_test.go
index 5f348f2a..eebfa10d 100644
--- a/github/messages_test.go
+++ b/github/messages_test.go
@@ -9,7 +9,9 @@ import (
 	"bytes"
 	"encoding/json"
 	"net/http"
+	"net/url"
 	"reflect"
+	"strings"
 	"testing"
 )
 
@@ -68,6 +70,7 @@ func TestValidatePayload(t *testing.T) {
 		if test.signature != "" {
 			req.Header.Set(signatureHeader, test.signature)
 		}
+		req.Header.Set("Content-Type", "application/json")
 
 		got, err := ValidatePayload(req, secretKey)
 		if err != nil {
@@ -82,6 +85,77 @@ func TestValidatePayload(t *testing.T) {
 	}
 }
 
+func TestValidatePayload_FormGet(t *testing.T) {
+	payload := `{"yo":true}`
+	signature := "sha1=3374ef144403e8035423b23b02e2c9d7a4c50368"
+	secretKey := []byte("0123456789abcdef")
+
+	form := url.Values{}
+	form.Add("payload", payload)
+	req, err := http.NewRequest("POST", "http://localhost/event", strings.NewReader(form.Encode()))
+	if err != nil {
+		t.Fatalf("NewRequest: %v", err)
+	}
+	req.PostForm = form
+	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+	req.Header.Set(signatureHeader, signature)
+
+	got, err := ValidatePayload(req, secretKey)
+	if err != nil {
+		t.Errorf("ValidatePayload(%#v): err = %v, want nil", payload, err)
+	}
+	if string(got) != payload {
+		t.Errorf("ValidatePayload = %q, want %q", got, payload)
+	}
+
+	// check that if payload is invalid we get error
+	req.Header.Set(signatureHeader, "invalid signature")
+	if _, err = ValidatePayload(req, nil); err == nil {
+		t.Error("ValidatePayload = nil, want err")
+	}
+}
+
+func TestValidatePayload_FormPost(t *testing.T) {
+	payload := `{"yo":true}`
+	signature := "sha1=3374ef144403e8035423b23b02e2c9d7a4c50368"
+	secretKey := []byte("0123456789abcdef")
+
+	form := url.Values{}
+	form.Set("payload", payload)
+	buf := bytes.NewBufferString(form.Encode())
+	req, err := http.NewRequest("POST", "http://localhost/event", buf)
+	if err != nil {
+		t.Fatalf("NewRequest: %v", err)
+	}
+	req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+	req.Header.Set(signatureHeader, signature)
+
+	got, err := ValidatePayload(req, secretKey)
+	if err != nil {
+		t.Errorf("ValidatePayload(%#v): err = %v, want nil", payload, err)
+	}
+	if string(got) != payload {
+		t.Errorf("ValidatePayload = %q, want %q", got, payload)
+	}
+
+	// check that if payload is invalid we get error
+	req.Header.Set(signatureHeader, "invalid signature")
+	if _, err = ValidatePayload(req, nil); err == nil {
+		t.Error("ValidatePayload = nil, want err")
+	}
+}
+
+func TestValidatePayload_InvalidContentType(t *testing.T) {
+	req, err := http.NewRequest("POST", "http://localhost/event", nil)
+	if err != nil {
+		t.Fatalf("NewRequest: %v", err)
+	}
+	req.Header.Set("Content-Type", "invalid content type")
+	if _, err = ValidatePayload(req, nil); err == nil {
+		t.Error("ValidatePayload = nil, want err")
+	}
+}
+
 func TestParseWebHook(t *testing.T) {
 	tests := []struct {
 		payload     interface{}
diff --git a/github/projects_test.go b/github/projects_test.go
index 1045ca9b..bf287e02 100644
--- a/github/projects_test.go
+++ b/github/projects_test.go
@@ -18,7 +18,7 @@ func TestProjectsService_UpdateProject(t *testing.T) {
 	setup()
 	defer teardown()
 
-	input := &ProjectOptions{Name: "Project Name", Body: "Project body."}
+	input := &ProjectOptions{Name: "Project Name", Body: "Project body.", State: "open"}
 
 	mux.HandleFunc("/projects/1", func(w http.ResponseWriter, r *http.Request) {
 		testMethod(t, r, "PATCH")