From 6627b3ad34c8448fa013b80cbf4d60181ab3ffd7 Mon Sep 17 00:00:00 2001 From: Luke Young Date: Thu, 24 Nov 2016 22:45:32 -0800 Subject: [PATCH 1/3] Working CreateComment method with new JSON handling --- h1/activity.go | 64 +++++++++++++------ h1/h1.go | 25 +++++++- h1/h1_test.go | 9 +++ h1/report_service.go | 16 +++++ h1/report_service_test.go | 58 +++++++++++++++++ h1/tests/responses/report_create-comment.json | 32 ++++++++++ 6 files changed, 182 insertions(+), 22 deletions(-) create mode 100644 h1/tests/responses/report_create-comment.json diff --git a/h1/activity.go b/h1/activity.go index 637fd8d..42efa44 100644 --- a/h1/activity.go +++ b/h1/activity.go @@ -29,42 +29,64 @@ import ( // HackerOne API docs: https://api.hackerone.com/docs/v1#activity type Activity struct { report *Report - ID *string `json:"id"` - Type *string `json:"type"` - Message *string `json:"message"` - Internal *bool `json:"internal"` - CreatedAt *Timestamp `json:"created_at"` - UpdatedAt *Timestamp `json:"updated_at"` - RawActor json.RawMessage `json:"actor"` // Used by the Actor() method + ID *string `json:"-"` + Type *string `json:"-"` + Message *string `json:"message,omitempty"` + Internal *bool `json:"internal,omitempty"` + CreatedAt *Timestamp `json:"created_at,omitempty"` + UpdatedAt *Timestamp `json:"updated_at,omitempty"` + RawActor json.RawMessage `json:"actor,omitempty"` // Used by the Actor() method Attachments []Attachment `json:"attachments,omitempty"` rawData []byte // Used by the Activity() method } // Helper types for JSONUnmarshal type activity Activity // Used to avoid recursion of JSONUnmarshal -type activityUnmarshalHelper struct { - activity +type activityJSONHelper struct { + ID *string `json:"id,omitempty"` + Type *string `json:"type,omitempty"` Attributes *activity `json:"attributes"` - Relationships struct { - Attachments struct { - Data []Attachment `json:"data"` + Relationships *struct { + Attachments *struct { + Data []Attachment `json:"data,omitempty"` } `json:"attachments,omitempty"` - RawActor struct { - Data json.RawMessage `json:"data"` - } `json:"actor"` - } `json:"relationships"` + RawActor *struct { + Data json.RawMessage `json:"data,omitempty"` + } `json:"actor,omitempty"` + } `json:"relationships,omitempty"` +} + +// MarshalJSON allows JSONAPI attributes and relationships to unmarshal cleanly. +func (a *Activity) MarshalJSON() ([]byte, error) { + act := activity(*a) + helper := activityJSONHelper{ + ID: a.ID, + Type: a.Type, + Attributes: &act, + } + // TODO: Build relationships if needed + //helper.Relationships.Attachments.Data = act.Attachments + act.Attachments = nil + return json.Marshal(&helper) } // UnmarshalJSON allows JSONAPI attributes and relationships to unmarshal cleanly. func (a *Activity) UnmarshalJSON(b []byte) error { - var helper activityUnmarshalHelper - helper.Attributes = &helper.activity + var helper activityJSONHelper if err := json.Unmarshal(b, &helper); err != nil { return err } - *a = Activity(helper.activity) - a.Attachments = helper.Relationships.Attachments.Data - a.RawActor = helper.Relationships.RawActor.Data + *a = Activity(*helper.Attributes) + a.ID = helper.ID + a.Type = helper.Type + if helper.Relationships != nil { + if helper.Relationships.Attachments != nil { + a.Attachments = helper.Relationships.Attachments.Data + } + if helper.Relationships.RawActor != nil { + a.RawActor = helper.Relationships.RawActor.Data + } + } a.rawData = b return nil } diff --git a/h1/h1.go b/h1/h1.go index 7ab25a4..3d5376c 100644 --- a/h1/h1.go +++ b/h1/h1.go @@ -24,8 +24,10 @@ package h1 import ( "github.com/google/go-querystring/query" + "bytes" "encoding/json" "fmt" + "io" "io/ioutil" "net/http" "net/url" @@ -104,6 +106,11 @@ func NewClient(httpClient *http.Client) *Client { return c } +// dataRequest is used to cast requests to H1 +type dataRequest struct { + Data interface{} `json:"data,omitempty"` +} + // NewRequest creates an API request. A relative URL can be provided in urlStr func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Request, error) { rel, err := url.Parse(urlStr) @@ -111,11 +118,27 @@ func (c *Client) NewRequest(method, urlStr string, body interface{}) (*http.Requ return nil, err } - req, err := http.NewRequest(method, c.BaseURL.ResolveReference(rel).String(), nil) + var buf io.ReadWriter + if body != nil { + buf = new(bytes.Buffer) + dat := dataRequest{ + Data: body, + } + err := json.NewEncoder(buf).Encode(&dat) + if err != nil { + return nil, err + } + } + + req, err := http.NewRequest(method, c.BaseURL.ResolveReference(rel).String(), buf) if err != nil { return nil, err } + if body != nil { + req.Header.Add("Content-Type", "application/json") + } + req.Header.Add("User-Agent", c.UserAgent) return req, nil diff --git a/h1/h1_test.go b/h1/h1_test.go index c643d52..2115f91 100644 --- a/h1/h1_test.go +++ b/h1/h1_test.go @@ -27,6 +27,7 @@ import ( "bytes" "io" "io/ioutil" + "math" "net/http" "net/http/httptest" "net/url" @@ -144,6 +145,14 @@ func Test_NewRequest(t *testing.T) { _, err := client.NewRequest("GET", "http://[fe80::1%en0]/", nil) assert.NotNil(t, err) + // Check that an invalid body fails + _, err = client.NewRequest("GET", "/", struct { + InvalidField float64 `json:"invalid_field"` + }{ + math.NaN(), + }) + assert.NotNil(t, err) + // Check that an invalid base URL fails badclient := NewClient(nil) badclient.BaseURL = &url.URL{ diff --git a/h1/report_service.go b/h1/report_service.go index 776e9be..20b6f10 100644 --- a/h1/report_service.go +++ b/h1/report_service.go @@ -44,6 +44,22 @@ func (s *ReportService) Get(ID string) (*Report, *Response, error) { return rResp, resp, err } +// CreateComment creates a Comment on a report by ID +func (s *ReportService) CreateComment(ID string, comment *Activity) (*Activity, *Response, error) { + req, err := s.client.NewRequest("POST", fmt.Sprintf("reports/%s/activities", ID), comment) + if err != nil { + return nil, nil, err + } + + rResp := new(Activity) + resp, err := s.client.Do(req, rResp) + if err != nil { + return nil, resp, err + } + + return rResp, resp, err +} + // ReportListFilter specifies optional parameters to the ReportService.List method. // // HackerOne API docs: https://api.hackerone.com/docs/v1#reports/query diff --git a/h1/report_service_test.go b/h1/report_service_test.go index d33542a..4a9f048 100644 --- a/h1/report_service_test.go +++ b/h1/report_service_test.go @@ -23,9 +23,11 @@ package h1 import ( "github.com/stretchr/testify/assert" + "io/ioutil" "net/http" "net/http/httptest" "net/url" + "strings" "testing" ) @@ -104,6 +106,62 @@ func Test_ReportService_Get(t *testing.T) { assert.Equal(t, &expectedReport, actual) } +var expectedCommentRequest = `{"data":{"type":"activity-comment","attributes":{"message":"A fix has been deployed. Can you retest, please?","internal":false}}}` +var expectedComment = Activity{ + ID: String("1337"), + Type: String(ActivityCommentType), + Message: String("A fix has been deployed. Can you retest, please?"), + Internal: Bool(false), + CreatedAt: NewTimestamp("2016-02-02T04:05:06.000Z"), + UpdatedAt: NewTimestamp("2016-02-02T04:05:06.000Z"), +} + +func Test_ReportService_CreateComment(t *testing.T) { + // Verify that an invalid url fails + c := NewClient(nil) + c.BaseURL = &url.URL{} + _, _, err := c.Report.CreateComment("%A", nil) + assert.NotNil(t, err) + + // Verify that an error response fails + errorServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Error(w, "Oh No", 500) + })) + defer errorServer.Close() + u, err := url.Parse(errorServer.URL) + assert.Nil(t, err) + c.BaseURL = u + _, _, err = c.Report.CreateComment("123456", nil) + assert.NotNil(t, err) + + // Verify that it gets a response correctly and it has the correct request body + commentServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if strings.TrimSpace(string(body)) != strings.TrimSpace(expectedCommentRequest) { + http.Error(w, "Non-matching request!", http.StatusBadRequest) + return + } + http.ServeFile(w, r, "tests/responses/report_create-comment.json") + })) + defer commentServer.Close() + u, err = url.Parse(commentServer.URL) + assert.Nil(t, err) + c.BaseURL = u + actual, _, err := c.Report.CreateComment("123456", &Activity{ + Type: String(ActivityCommentType), + Internal: Bool(false), + Message: String("A fix has been deployed. Can you retest, please?"), + }) + assert.Nil(t, err) + actual.RawActor = nil + actual.rawData = nil + assert.Equal(t, &expectedComment, actual) +} + func Test_ReportService_List(t *testing.T) { // Verify that an invalid url fails diff --git a/h1/tests/responses/report_create-comment.json b/h1/tests/responses/report_create-comment.json new file mode 100644 index 0000000..9d1e338 --- /dev/null +++ b/h1/tests/responses/report_create-comment.json @@ -0,0 +1,32 @@ +{ + "data": { + "id": "1337", + "type": "activity-comment", + "attributes": { + "message": "A fix has been deployed. Can you retest, please?", + "created_at": "2016-02-02T04:05:06.000Z", + "updated_at": "2016-02-02T04:05:06.000Z", + "internal": false + }, + "relationships": { + "actor": { + "data": { + "id": "1337", + "type": "user", + "attributes": { + "username": "api-example", + "name": "API Example", + "disabled": false, + "created_at": "2016-02-02T04:05:06.000Z", + "profile_picture": { + "62x62": "/assets/avatars/default.png", + "82x82": "/assets/avatars/default.png", + "110x110": "/assets/avatars/default.png", + "260x260": "/assets/avatars/default.png" + } + } + } + } + } + } +} From 740115813e09ce77d684bd2af7f4970f3722f2c7 Mon Sep 17 00:00:00 2001 From: Luke Young Date: Thu, 24 Nov 2016 23:09:05 -0800 Subject: [PATCH 2/3] Working CreateComment and UpdateAssignee --- h1/report_service.go | 50 ++++++++++++++++++- h1/report_service_test.go | 100 +++++++++++++++++++++++++++++++++++--- 2 files changed, 142 insertions(+), 8 deletions(-) diff --git a/h1/report_service.go b/h1/report_service.go index 20b6f10..94a4be6 100644 --- a/h1/report_service.go +++ b/h1/report_service.go @@ -45,7 +45,13 @@ func (s *ReportService) Get(ID string) (*Report, *Response, error) { } // CreateComment creates a Comment on a report by ID -func (s *ReportService) CreateComment(ID string, comment *Activity) (*Activity, *Response, error) { +func (s *ReportService) CreateComment(ID string, message string, internal bool) (*Activity, *Response, error) { + comment := &Activity{ + Type: String(ActivityCommentType), + Internal: &internal, + Message: &message, + } + req, err := s.client.NewRequest("POST", fmt.Sprintf("reports/%s/activities", ID), comment) if err != nil { return nil, nil, err @@ -60,6 +66,48 @@ func (s *ReportService) CreateComment(ID string, comment *Activity) (*Activity, return rResp, resp, err } +// reportUpdateAssigneeRequest is used for making report assignee updates +type reportUpdateAssigneeRequestAttributes struct { + Message string `json:"message"` +} +type reportUpdateAssigneeRequest struct { + ID *string `json:"id,omitempty"` + Type string `json:"type"` + Attributes reportUpdateAssigneeRequestAttributes `json:"attributes"` +} + +// UpdateAssignee creates a Comment on a report by ID +func (s *ReportService) UpdateAssignee(ID string, message string, assignee interface{}) (*Report, *Response, error) { + request := &reportUpdateAssigneeRequest{ + Attributes: reportUpdateAssigneeRequestAttributes{ + Message: message, + }, + } + switch assignee.(type) { + case *User: + request.ID = assignee.(*User).ID + request.Type = "user" + case *Group: + request.ID = assignee.(*Group).ID + request.Type = "group" + default: + request.Type = "nobody" + } + + req, err := s.client.NewRequest("PUT", fmt.Sprintf("reports/%s/assignee", ID), request) + if err != nil { + return nil, nil, err + } + + rResp := new(Report) + resp, err := s.client.Do(req, rResp) + if err != nil { + return nil, resp, err + } + + return rResp, resp, err +} + // ReportListFilter specifies optional parameters to the ReportService.List method. // // HackerOne API docs: https://api.hackerone.com/docs/v1#reports/query diff --git a/h1/report_service_test.go b/h1/report_service_test.go index 4a9f048..6858cdc 100644 --- a/h1/report_service_test.go +++ b/h1/report_service_test.go @@ -120,7 +120,7 @@ func Test_ReportService_CreateComment(t *testing.T) { // Verify that an invalid url fails c := NewClient(nil) c.BaseURL = &url.URL{} - _, _, err := c.Report.CreateComment("%A", nil) + _, _, err := c.Report.CreateComment("%A", "A fix has been deployed. Can you retest, please?", false) assert.NotNil(t, err) // Verify that an error response fails @@ -131,7 +131,7 @@ func Test_ReportService_CreateComment(t *testing.T) { u, err := url.Parse(errorServer.URL) assert.Nil(t, err) c.BaseURL = u - _, _, err = c.Report.CreateComment("123456", nil) + _, _, err = c.Report.CreateComment("123456", "A fix has been deployed. Can you retest, please?", false) assert.NotNil(t, err) // Verify that it gets a response correctly and it has the correct request body @@ -151,17 +151,103 @@ func Test_ReportService_CreateComment(t *testing.T) { u, err = url.Parse(commentServer.URL) assert.Nil(t, err) c.BaseURL = u - actual, _, err := c.Report.CreateComment("123456", &Activity{ - Type: String(ActivityCommentType), - Internal: Bool(false), - Message: String("A fix has been deployed. Can you retest, please?"), - }) + actual, _, err := c.Report.CreateComment("123456", "A fix has been deployed. Can you retest, please?", false) assert.Nil(t, err) actual.RawActor = nil actual.rawData = nil assert.Equal(t, &expectedComment, actual) } +var expectedUpdateAssigneeRequest1 = `{"data":{"type":"nobody","attributes":{"message":"@member Please check this out!"}}}` +var expectedUpdateAssigneeRequest2 = `{"data":{"id":"1337","type":"user","attributes":{"message":"@member Please check this out!"}}}` +var expectedUpdateAssigneeRequest3 = `{"data":{"id":"1337","type":"group","attributes":{"message":"@member Please check this out!"}}}` + +func Test_ReportService_UpdateAssignee(t *testing.T) { + // Verify that an invalid url fails + c := NewClient(nil) + c.BaseURL = &url.URL{} + _, _, err := c.Report.UpdateAssignee("%A", "", nil) + assert.NotNil(t, err) + + // Verify that an error response fails + errorServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Error(w, "Oh No", 500) + })) + defer errorServer.Close() + u, err := url.Parse(errorServer.URL) + assert.Nil(t, err) + c.BaseURL = u + _, _, err = c.Report.UpdateAssignee("123456", "", nil) + assert.NotNil(t, err) + + // Verify that it gets a response correctly and it has the correct request body + reportServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if strings.TrimSpace(string(body)) != strings.TrimSpace(expectedUpdateAssigneeRequest1) { + http.Error(w, "Non-matching request!", http.StatusBadRequest) + return + } + http.ServeFile(w, r, "tests/responses/report.json") + })) + defer reportServer.Close() + u, err = url.Parse(reportServer.URL) + assert.Nil(t, err) + c.BaseURL = u + actual, _, err := c.Report.UpdateAssignee("123456", "@member Please check this out!", nil) + assert.Nil(t, err) + assert.Equal(t, &expectedReport, actual) + + // Verify that it gets a response correctly and it has the correct request body + reportServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if strings.TrimSpace(string(body)) != strings.TrimSpace(expectedUpdateAssigneeRequest2) { + http.Error(w, "Non-matching request!", http.StatusBadRequest) + return + } + http.ServeFile(w, r, "tests/responses/report.json") + })) + defer reportServer.Close() + u, err = url.Parse(reportServer.URL) + assert.Nil(t, err) + c.BaseURL = u + actual, _, err = c.Report.UpdateAssignee("123456", "@member Please check this out!", &User{ + ID: String("1337"), + }) + assert.Nil(t, err) + assert.Equal(t, &expectedReport, actual) + + // Verify that it gets a response correctly and it has the correct request body + reportServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if strings.TrimSpace(string(body)) != strings.TrimSpace(expectedUpdateAssigneeRequest3) { + http.Error(w, "Non-matching request!", http.StatusBadRequest) + return + } + http.ServeFile(w, r, "tests/responses/report.json") + })) + defer reportServer.Close() + u, err = url.Parse(reportServer.URL) + assert.Nil(t, err) + c.BaseURL = u + actual, _, err = c.Report.UpdateAssignee("123456", "@member Please check this out!", &Group{ + ID: String("1337"), + }) + assert.Nil(t, err) + assert.Equal(t, &expectedReport, actual) +} + func Test_ReportService_List(t *testing.T) { // Verify that an invalid url fails From e359010ef22663c00ab0311623200b782842b74f Mon Sep 17 00:00:00 2001 From: Luke Young Date: Thu, 24 Nov 2016 23:20:03 -0800 Subject: [PATCH 3/3] Add UpdateState method --- h1/report_service.go | 34 +++++++++++++++++++++++++++++++ h1/report_service_test.go | 42 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/h1/report_service.go b/h1/report_service.go index 94a4be6..143ffcd 100644 --- a/h1/report_service.go +++ b/h1/report_service.go @@ -108,6 +108,40 @@ func (s *ReportService) UpdateAssignee(ID string, message string, assignee inter return rResp, resp, err } +// reportUpdateAssigneeRequest is used for making report assignee updates +type reportStateChangeRequestAttributes struct { + Message string `json:"message,omitempty"` + State string `json:"state"` +} +type reportStateChangeRequest struct { + Type string `json:"type"` + Attributes reportStateChangeRequestAttributes `json:"attributes"` +} + +// UpdateState changes a report's state by ID +func (s *ReportService) UpdateState(ID string, state string, message string) (*Report, *Response, error) { + request := &reportStateChangeRequest{ + Type: "state-change", + Attributes: reportStateChangeRequestAttributes{ + Message: message, + State: state, + }, + } + + req, err := s.client.NewRequest("POST", fmt.Sprintf("reports/%s/state_changes", ID), request) + if err != nil { + return nil, nil, err + } + + rResp := new(Report) + resp, err := s.client.Do(req, rResp) + if err != nil { + return nil, resp, err + } + + return rResp, resp, err +} + // ReportListFilter specifies optional parameters to the ReportService.List method. // // HackerOne API docs: https://api.hackerone.com/docs/v1#reports/query diff --git a/h1/report_service_test.go b/h1/report_service_test.go index 6858cdc..bae5e11 100644 --- a/h1/report_service_test.go +++ b/h1/report_service_test.go @@ -248,6 +248,48 @@ func Test_ReportService_UpdateAssignee(t *testing.T) { assert.Equal(t, &expectedReport, actual) } +var expectedStateChangeRequest = `{"data":{"type":"state-change","attributes":{"message":"This vulnerability has been resolved. Thanks!","state":"resolved"}}}` + +func Test_ReportService_UpdateState(t *testing.T) { + // Verify that an invalid url fails + c := NewClient(nil) + c.BaseURL = &url.URL{} + _, _, err := c.Report.UpdateState("%A", "", "") + assert.NotNil(t, err) + + // Verify that an error response fails + errorServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + http.Error(w, "Oh No", 500) + })) + defer errorServer.Close() + u, err := url.Parse(errorServer.URL) + assert.Nil(t, err) + c.BaseURL = u + _, _, err = c.Report.UpdateState("123456", "", "") + assert.NotNil(t, err) + + // Verify that it gets a response correctly and it has the correct request body + reportServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + body, err := ioutil.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + if strings.TrimSpace(string(body)) != strings.TrimSpace(expectedStateChangeRequest) { + http.Error(w, "Non-matching request!", http.StatusBadRequest) + return + } + http.ServeFile(w, r, "tests/responses/report.json") + })) + defer reportServer.Close() + u, err = url.Parse(reportServer.URL) + assert.Nil(t, err) + c.BaseURL = u + actual, _, err := c.Report.UpdateState("123456", ReportStateResolved, "This vulnerability has been resolved. Thanks!") + assert.Nil(t, err) + assert.Equal(t, &expectedReport, actual) +} + func Test_ReportService_List(t *testing.T) { // Verify that an invalid url fails