From 312c070c57e6a1909b5207e453d2c2177ef0684f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Giedrius=20Statkevi=C4=8Dius?= Date: Wed, 16 Mar 2022 12:04:07 +0200 Subject: [PATCH 1/5] *: add Slack Connection API structs/methods --- slack_connection.go | 128 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 slack_connection.go diff --git a/slack_connection.go b/slack_connection.go new file mode 100644 index 00000000..8debcc09 --- /dev/null +++ b/slack_connection.go @@ -0,0 +1,128 @@ +package pagerduty + +import ( + "context" + "fmt" + "net/http" + + "github.com/google/go-querystring/query" +) + +// SlackConnectionConfig is the configuration of a Slack connection as per the documentation +// https://developer.pagerduty.com/api-reference/c2NoOjExMjA5MzMy-slack-connection. +type SlackConnectionConfig struct { + Events []string `json:"events"` + Urgency *string `json:"urgency"` + Priorities []string `json:"priorities"` +} + +// SlackConnection is an entity that represents a Slack connections as per the +// documentation https://developer.pagerduty.com/api-reference/c2NoOjExMjA5MzMy-slack-connection. +type SlackConnection struct { + SourceID string `json:"source_id"` + SourceName string `json:"source_name"` + SourceType string `json:"source_type"` + + ChannelID string `json:"channel_id"` + ChannelName string `json:"channel_name"` + NotificationType string `json:"notification_type"` + + Config SlackConnectionConfig `json:"config"` +} + +// SlackConnectionObject is an API object returned by getter functions. +type SlackConnectionObject struct { + SlackConnection + APIObject +} + +// ListSlackConnectionsResponse is an API object returned by the list function. +type ListSlackConnectionsResponse struct { + Connections []SlackConnection `json:"slack_connections"` + APIListObject +} + +// ListSlackConnectionsOptions is the data structure used when calling the ListSlackConnections API endpoint. +type ListSlackConnectionsOptions struct { + // Limit is the pagination parameter that limits the number of results per + // page. PagerDuty defaults this value to 50 if omitted, and sets an upper + // bound of 100. + Limit uint `url:"limit,omitempty"` + + // Offset is the pagination parameter that specifies the offset at which to + // start pagination results. When trying to request the next page of + // results, the new Offset value should be currentOffset + Limit. + Offset uint `url:"offset,omitempty"` +} + +// CreateSlackConnectionWithContext creates a Slack connection. +func (c *Client) CreateSlackConnectionWithContext(ctx context.Context, slackTeamID string, s SlackConnection) (SlackConnectionObject, error) { + d := map[string]SlackConnection{ + "slack_connection": s, + } + + resp, err := c.post(ctx, "/workspaces/"+slackTeamID+"/connections", d, nil) + return getSlackConnectionFromResponse(c, resp, err) +} + +// GetSlackConnection gets a Slack connection. +func (c *Client) GetSlackConnectionWithContext(ctx context.Context, slackTeamID, connectionID string) (SlackConnectionObject, error) { + resp, err := c.get(ctx, "/workspaces/"+slackTeamID+"/connections/"+connectionID) + return getSlackConnectionFromResponse(c, resp, err) +} + +// DeleteSlackConnectionWithContext deletes a Slack connection. +func (c *Client) DeleteSlackConnectionWithContext(ctx context.Context, slackTeamID, connectionID string) error { + _, err := c.delete(ctx, "/workspaces/"+slackTeamID+"/connections/"+connectionID) + return err +} + +// UpdateSlackConnectionWithContext updates an existing Slack connection. +func (c *Client) UpdateSlackConnectionWithContext(ctx context.Context, slackTeamID, connectionID string, s SlackConnection) (SlackConnectionObject, error) { + d := map[string]SlackConnection{ + "slack_connection": s, + } + + resp, err := c.put(ctx, "/workspaces/"+slackTeamID+"/connections/"+connectionID, d, nil) + return getSlackConnectionFromResponse(c, resp, err) +} + +// ListSlackConnectionsWithContext lists Slack connections. +func (c *Client) ListSlackConnectionsWithContext(ctx context.Context, slackTeamID string, o ListSlackConnectionsOptions) (*ListSlackConnectionsResponse, error) { + v, err := query.Values(o) + if err != nil { + return nil, err + } + + resp, err := c.get(ctx, "/workspaces/"+slackTeamID+"/connections?"+v.Encode()) + if err != nil { + return nil, err + } + + var result ListSlackConnectionsResponse + if err = c.decodeJSON(resp, &result); err != nil { + return nil, err + } + + return &result, nil +} + +func getSlackConnectionFromResponse(c *Client, resp *http.Response, err error) (SlackConnectionObject, error) { + if err != nil { + return SlackConnectionObject{}, err + } + + var target map[string]SlackConnectionObject + if dErr := c.decodeJSON(resp, &target); dErr != nil { + return SlackConnectionObject{}, fmt.Errorf("Could not decode JSON response: %v", dErr) + } + + const rootNode = "slack_connection" + + t, nodeOK := target[rootNode] + if !nodeOK { + return SlackConnectionObject{}, fmt.Errorf("JSON response does not have %s field", rootNode) + } + + return t, nil +} From ca2c0c77dba2f5a383a411d89ada66b8d9f0b276 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Giedrius=20Statkevi=C4=8Dius?= Date: Wed, 16 Mar 2022 12:44:43 +0200 Subject: [PATCH 2/5] *: add tests for Slack connection API --- slack_connection_test.go | 338 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 338 insertions(+) create mode 100644 slack_connection_test.go diff --git a/slack_connection_test.go b/slack_connection_test.go new file mode 100644 index 00000000..6c8a7bf6 --- /dev/null +++ b/slack_connection_test.go @@ -0,0 +1,338 @@ +package pagerduty + +import ( + "context" + "net/http" + "testing" +) + +// Delete Slack Connection test. +func TestSlackConnection_Delete(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/workspaces/foo/connections/connectionid", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "DELETE") + }) + + client := defaultTestClient(server.URL, "foo") + err := client.DeleteSlackConnectionWithContext(context.Background(), "foo", "connectionid") + if err != nil { + t.Fatal(err) + } +} + +// List Slack Connections test. +func TestSlackConnection_List(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/workspaces/foo/connections", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + _, _ = w.Write([]byte(` + { + "slack_connections":[ + { + "id":"A12BCDE", + "source_id":"A1234B5", + "source_name":"test_service", + "source_type":"service_reference", + "channel_id":"A123B456C7D", + "channel_name":"random", + "notification_type":"responder", + "config":{ + "events":[ + "incident.acknowledged", + "incident.annotated", + "incident.delegated", + "incident.escalated", + "incident.reassigned", + "incident.resolved", + "incident.triggered", + "incident.unacknowledged", + "incident.priority_updated", + "incident.responder.added", + "incident.responder.replied", + "incident.status_update_published", + "incident.reopened", + "incident.action_invocation.created", + "incident.action_invocation.updated", + "incident.action_invocation.terminated" + ], + "priorities":[ + "ABCDEF1", + "AB1CDE2" + ], + "urgency":null + } + } + ], + "limit":1, + "offset":0, + "more":true, + "total":99 + }`)) + }) + + client := defaultTestClient(server.URL, "foo") + + res, err := client.ListSlackConnectionsWithContext(context.Background(), "foo", ListSlackConnectionsOptions{}) + + want := &ListSlackConnectionsResponse{ + APIListObject: APIListObject{ + Limit: 1, + More: true, + Total: 99, + }, + Connections: []SlackConnection{ + { + SourceID: "A1234B5", + SourceName: "test_service", + SourceType: "service_reference", + ChannelID: "A123B456C7D", + ChannelName: "random", + NotificationType: "responder", + Config: SlackConnectionConfig{ + Events: []string{ + "incident.acknowledged", + "incident.annotated", + "incident.delegated", + "incident.escalated", + "incident.reassigned", + "incident.resolved", + "incident.triggered", + "incident.unacknowledged", + "incident.priority_updated", + "incident.responder.added", + "incident.responder.replied", + "incident.status_update_published", + "incident.reopened", + "incident.action_invocation.created", + "incident.action_invocation.updated", + "incident.action_invocation.terminated", + }, + Priorities: []string{"ABCDEF1", "AB1CDE2"}, + Urgency: nil, + }, + }, + }, + } + + if err != nil { + t.Fatal(err) + } + testEqual(t, want, res) +} + +// Update Slack Connection test. +func TestSlackConnection_Update(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/workspaces/foo/connections/connectionid", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "PUT") + _, _ = w.Write([]byte(` + { + "slack_connection": { + "id": "A12BCDE", + "source_id": "A1234B5", + "source_name": "test_service", + "source_type": "service_reference", + "channel_id": "FOOBAR", + "channel_name": "random", + "notification_type": "responder", + "config": { + "events": [ + "incident.acknowledged", + "incident.annotated", + "incident.delegated", + "incident.escalated", + "incident.reassigned", + "incident.resolved", + "incident.triggered", + "incident.unacknowledged", + "incident.priority_updated", + "incident.responder.added", + "incident.responder.replied", + "incident.status_update_published", + "incident.reopened", + "incident.action_invocation.created", + "incident.action_invocation.updated", + "incident.action_invocation.terminated" + ], + "priorities": [ + "ABCDEF1", + "AB1CDE2" + ], + "urgency": null + } + } + }`)) + }) + + client := defaultTestClient(server.URL, "foo") + + res, err := client.UpdateSlackConnectionWithContext(context.Background(), "foo", "connectionid", SlackConnection{ + SourceID: "A1234B5", + SourceName: "test_service", + SourceType: "service_reference", + ChannelID: "FOOBAR", + ChannelName: "random", + NotificationType: "responder", + Config: SlackConnectionConfig{ + Events: []string{ + "incident.acknowledged", + "incident.annotated", + "incident.delegated", + "incident.escalated", + "incident.reassigned", + "incident.resolved", + "incident.triggered", + "incident.unacknowledged", + "incident.priority_updated", + "incident.responder.added", + "incident.responder.replied", + "incident.status_update_published", + "incident.reopened", + "incident.action_invocation.created", + "incident.action_invocation.updated", + "incident.action_invocation.terminated", + }, + Priorities: []string{"ABCDEF1", "AB1CDE2"}, + Urgency: nil, + }, + }) + + want := SlackConnectionObject{ + APIObject: APIObject{ + ID: "A12BCDE", + }, + SlackConnection: SlackConnection{ + SourceID: "A1234B5", + SourceName: "test_service", + SourceType: "service_reference", + ChannelID: "FOOBAR", + ChannelName: "random", + NotificationType: "responder", + Config: SlackConnectionConfig{ + Events: []string{ + "incident.acknowledged", + "incident.annotated", + "incident.delegated", + "incident.escalated", + "incident.reassigned", + "incident.resolved", + "incident.triggered", + "incident.unacknowledged", + "incident.priority_updated", + "incident.responder.added", + "incident.responder.replied", + "incident.status_update_published", + "incident.reopened", + "incident.action_invocation.created", + "incident.action_invocation.updated", + "incident.action_invocation.terminated", + }, + Priorities: []string{"ABCDEF1", "AB1CDE2"}, + Urgency: nil, + }, + }, + } + + if err != nil { + t.Fatal(err) + } + testEqual(t, want, res) +} + +// Get Slack Connection test. +func TestSlackConnection_Get(t *testing.T) { + setup() + defer teardown() + + mux.HandleFunc("/workspaces/foo/connections/connectionid", func(w http.ResponseWriter, r *http.Request) { + testMethod(t, r, "GET") + _, _ = w.Write([]byte(` + { + "slack_connection": { + "id": "A12BCDE", + "source_id": "A1234B5", + "source_name": "test_service", + "source_type": "service_reference", + "channel_id": "AABBCC", + "channel_name": "random", + "notification_type": "responder", + "config": { + "events": [ + "incident.acknowledged", + "incident.annotated", + "incident.delegated", + "incident.escalated", + "incident.reassigned", + "incident.resolved", + "incident.triggered", + "incident.unacknowledged", + "incident.priority_updated", + "incident.responder.added", + "incident.responder.replied", + "incident.status_update_published", + "incident.reopened", + "incident.action_invocation.created", + "incident.action_invocation.updated", + "incident.action_invocation.terminated" + ], + "priorities": [ + "ABCDEF1", + "AB1CDE2" + ], + "urgency": null + } + } + }`)) + }) + + client := defaultTestClient(server.URL, "foo") + + res, err := client.GetSlackConnectionWithContext(context.Background(), "foo", "connectionid") + + want := SlackConnectionObject{ + APIObject: APIObject{ + ID: "A12BCDE", + }, + SlackConnection: SlackConnection{ + SourceID: "A1234B5", + SourceName: "test_service", + SourceType: "service_reference", + ChannelID: "AABBCC", + ChannelName: "random", + NotificationType: "responder", + Config: SlackConnectionConfig{ + Events: []string{ + "incident.acknowledged", + "incident.annotated", + "incident.delegated", + "incident.escalated", + "incident.reassigned", + "incident.resolved", + "incident.triggered", + "incident.unacknowledged", + "incident.priority_updated", + "incident.responder.added", + "incident.responder.replied", + "incident.status_update_published", + "incident.reopened", + "incident.action_invocation.created", + "incident.action_invocation.updated", + "incident.action_invocation.terminated", + }, + Priorities: []string{"ABCDEF1", "AB1CDE2"}, + Urgency: nil, + }, + }, + } + + if err != nil { + t.Fatal(err) + } + testEqual(t, want, res) +} From d66c620da8b9f527bb2e1cee649b9ee607e8e59f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Giedrius=20Statkevi=C4=8Dius?= Date: Thu, 17 Mar 2022 12:38:07 +0200 Subject: [PATCH 3/5] client: fix request duping --- client.go | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/client.go b/client.go index 3ab93820..2224b73d 100644 --- a/client.go +++ b/client.go @@ -499,17 +499,25 @@ func (c *Client) prepRequest(req *http.Request, authRequired bool, headers map[s } func dupeRequest(r *http.Request) (*http.Request, error) { - data, err := ioutil.ReadAll(r.Body) - if err != nil { - return nil, fmt.Errorf("failed to copy request body: %w", err) - } + var data []byte + // Body can be nil in GET requests, for example. + if r.Body != nil { + bodyData, err := ioutil.ReadAll(r.Body) + if err != nil { + return nil, fmt.Errorf("failed to copy request body: %w", err) + } - _ = r.Body.Close() + _ = r.Body.Close() + + data = bodyData + } dreq := r.Clone(r.Context()) - r.Body = ioutil.NopCloser(bytes.NewReader(data)) - dreq.Body = ioutil.NopCloser(bytes.NewReader(data)) + if data != nil { + r.Body = ioutil.NopCloser(bytes.NewReader(data)) + dreq.Body = ioutil.NopCloser(bytes.NewReader(data)) + } return dreq, nil } From 13cbcf1b2ae45508de5110c6237175e5b2cabb45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Giedrius=20Statkevi=C4=8Dius?= Date: Thu, 17 Mar 2022 12:42:21 +0200 Subject: [PATCH 4/5] slack_connection: fix path --- slack_connection.go | 10 +++++----- slack_connection_test.go | 8 ++++---- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/slack_connection.go b/slack_connection.go index 8debcc09..be237bcc 100644 --- a/slack_connection.go +++ b/slack_connection.go @@ -61,19 +61,19 @@ func (c *Client) CreateSlackConnectionWithContext(ctx context.Context, slackTeam "slack_connection": s, } - resp, err := c.post(ctx, "/workspaces/"+slackTeamID+"/connections", d, nil) + resp, err := c.post(ctx, "/integration-slack/workspaces/"+slackTeamID+"/connections", d, nil) return getSlackConnectionFromResponse(c, resp, err) } // GetSlackConnection gets a Slack connection. func (c *Client) GetSlackConnectionWithContext(ctx context.Context, slackTeamID, connectionID string) (SlackConnectionObject, error) { - resp, err := c.get(ctx, "/workspaces/"+slackTeamID+"/connections/"+connectionID) + resp, err := c.get(ctx, "/integration-slack/workspaces/"+slackTeamID+"/connections/"+connectionID) return getSlackConnectionFromResponse(c, resp, err) } // DeleteSlackConnectionWithContext deletes a Slack connection. func (c *Client) DeleteSlackConnectionWithContext(ctx context.Context, slackTeamID, connectionID string) error { - _, err := c.delete(ctx, "/workspaces/"+slackTeamID+"/connections/"+connectionID) + _, err := c.delete(ctx, "/integration-slack/workspaces/"+slackTeamID+"/connections/"+connectionID) return err } @@ -83,7 +83,7 @@ func (c *Client) UpdateSlackConnectionWithContext(ctx context.Context, slackTeam "slack_connection": s, } - resp, err := c.put(ctx, "/workspaces/"+slackTeamID+"/connections/"+connectionID, d, nil) + resp, err := c.put(ctx, "/integration-slack/workspaces/"+slackTeamID+"/connections/"+connectionID, d, nil) return getSlackConnectionFromResponse(c, resp, err) } @@ -94,7 +94,7 @@ func (c *Client) ListSlackConnectionsWithContext(ctx context.Context, slackTeamI return nil, err } - resp, err := c.get(ctx, "/workspaces/"+slackTeamID+"/connections?"+v.Encode()) + resp, err := c.get(ctx, "/integration-slack/workspaces/"+slackTeamID+"/connections?"+v.Encode()) if err != nil { return nil, err } diff --git a/slack_connection_test.go b/slack_connection_test.go index 6c8a7bf6..89fe161d 100644 --- a/slack_connection_test.go +++ b/slack_connection_test.go @@ -11,7 +11,7 @@ func TestSlackConnection_Delete(t *testing.T) { setup() defer teardown() - mux.HandleFunc("/workspaces/foo/connections/connectionid", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/integration-slack/workspaces/foo/connections/connectionid", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "DELETE") }) @@ -27,7 +27,7 @@ func TestSlackConnection_List(t *testing.T) { setup() defer teardown() - mux.HandleFunc("/workspaces/foo/connections", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/integration-slack/workspaces/foo/connections", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") _, _ = w.Write([]byte(` { @@ -129,7 +129,7 @@ func TestSlackConnection_Update(t *testing.T) { setup() defer teardown() - mux.HandleFunc("/workspaces/foo/connections/connectionid", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/integration-slack/workspaces/foo/connections/connectionid", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "PUT") _, _ = w.Write([]byte(` { @@ -250,7 +250,7 @@ func TestSlackConnection_Get(t *testing.T) { setup() defer teardown() - mux.HandleFunc("/workspaces/foo/connections/connectionid", func(w http.ResponseWriter, r *http.Request) { + mux.HandleFunc("/integration-slack/workspaces/foo/connections/connectionid", func(w http.ResponseWriter, r *http.Request) { testMethod(t, r, "GET") _, _ = w.Write([]byte(` { From 8ae4c32bc4119d647b3dc2a1f0e7fbc054aad63d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Giedrius=20Statkevi=C4=8Dius?= Date: Thu, 17 Mar 2022 12:55:43 +0200 Subject: [PATCH 5/5] slack_connection: fix types in response --- slack_connection.go | 2 +- slack_connection_test.go | 59 ++++++++++++++++++++++------------------ 2 files changed, 33 insertions(+), 28 deletions(-) diff --git a/slack_connection.go b/slack_connection.go index be237bcc..34990d04 100644 --- a/slack_connection.go +++ b/slack_connection.go @@ -38,7 +38,7 @@ type SlackConnectionObject struct { // ListSlackConnectionsResponse is an API object returned by the list function. type ListSlackConnectionsResponse struct { - Connections []SlackConnection `json:"slack_connections"` + Connections []SlackConnectionObject `json:"slack_connections"` APIListObject } diff --git a/slack_connection_test.go b/slack_connection_test.go index 89fe161d..977ecb31 100644 --- a/slack_connection_test.go +++ b/slack_connection_test.go @@ -84,35 +84,40 @@ func TestSlackConnection_List(t *testing.T) { More: true, Total: 99, }, - Connections: []SlackConnection{ + Connections: []SlackConnectionObject{ { - SourceID: "A1234B5", - SourceName: "test_service", - SourceType: "service_reference", - ChannelID: "A123B456C7D", - ChannelName: "random", - NotificationType: "responder", - Config: SlackConnectionConfig{ - Events: []string{ - "incident.acknowledged", - "incident.annotated", - "incident.delegated", - "incident.escalated", - "incident.reassigned", - "incident.resolved", - "incident.triggered", - "incident.unacknowledged", - "incident.priority_updated", - "incident.responder.added", - "incident.responder.replied", - "incident.status_update_published", - "incident.reopened", - "incident.action_invocation.created", - "incident.action_invocation.updated", - "incident.action_invocation.terminated", + APIObject: APIObject{ + ID: "A12BCDE", + }, + SlackConnection: SlackConnection{ + SourceID: "A1234B5", + SourceName: "test_service", + SourceType: "service_reference", + ChannelID: "A123B456C7D", + ChannelName: "random", + NotificationType: "responder", + Config: SlackConnectionConfig{ + Events: []string{ + "incident.acknowledged", + "incident.annotated", + "incident.delegated", + "incident.escalated", + "incident.reassigned", + "incident.resolved", + "incident.triggered", + "incident.unacknowledged", + "incident.priority_updated", + "incident.responder.added", + "incident.responder.replied", + "incident.status_update_published", + "incident.reopened", + "incident.action_invocation.created", + "incident.action_invocation.updated", + "incident.action_invocation.terminated", + }, + Priorities: []string{"ABCDEF1", "AB1CDE2"}, + Urgency: nil, }, - Priorities: []string{"ABCDEF1", "AB1CDE2"}, - Urgency: nil, }, }, },