Skip to content

Commit

Permalink
Add support for webhook API (#228)
Browse files Browse the repository at this point in the history
* Add WebhookAPI and impl

* Add WebhookAPI to API list and generate mock

* Add fixtures for webhook

* Webhook update API returns No Content

* Add test for webhook.go
  • Loading branch information
nukosuke committed Apr 22, 2022
1 parent b64dba3 commit aa6418f
Show file tree
Hide file tree
Showing 7 changed files with 310 additions and 9 deletions.
21 changes: 21 additions & 0 deletions fixture/GET/webhook.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"webhook": {
"authentication": {
"add_position": "header",
"type": "basic_auth"
},
"created_at": "2020-10-20T08:16:28Z",
"created_by": "1234567",
"endpoint": "https://example.com/status/200",
"http_method": "POST",
"id": "01EJFTSCC78X5V07NPY2MHR00M",
"name": "Example Webhook",
"request_format": "json",
"status": "active",
"subscriptions": [
"conditional_ticket_events"
],
"updated_at": "2020-10-20T08:16:28Z",
"updated_by": "1234567"
}
}
21 changes: 21 additions & 0 deletions fixture/POST/webhooks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"webhook": {
"authentication": {
"add_position": "header",
"type": "basic_auth"
},
"created_at": "2020-10-20T08:16:28Z",
"created_by": "1234567",
"endpoint": "https://example.com/status/200",
"http_method": "POST",
"id": "01EJFTSCC78X5V07NPY2MHR00M",
"name": "Example Webhook",
"request_format": "json",
"status": "active",
"subscriptions": [
"conditional_ticket_events"
],
"updated_at": "2020-10-20T08:16:28Z",
"updated_by": "1234567"
}
}
17 changes: 9 additions & 8 deletions zendesk/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,21 +15,22 @@ type API interface {
GroupMembershipAPI
LocaleAPI
MacroAPI
TicketAPI
TicketFieldAPI
TicketFormAPI
TriggerAPI
TargetAPI
UserAPI
UserFieldAPI
OrganizationAPI
OrganizationMembershipAPI
SearchAPI
SLAPolicyAPI
TargetAPI
TagAPI
TicketAuditAPI
TicketAPI
TicketCommentAPI
TicketFieldAPI
TicketFormAPI
TriggerAPI
UserAPI
UserFieldAPI
ViewAPI
OrganizationMembershipAPI
WebhookAPI
}

var _ API = (*Client)(nil)
58 changes: 58 additions & 0 deletions zendesk/mock/client.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

117 changes: 117 additions & 0 deletions zendesk/webhook.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package zendesk

import (
"context"
"encoding/json"
"fmt"
"time"
)

// Webhook is struct for webhook payload.
// https://developer.zendesk.com/api-reference/event-connectors/webhooks/webhooks/
type Webhook struct {
Authentication *WebhookAuthentication `json:"authentication,omitempty"`
CreatedAt time.Time `json:"created_at,omitempty"`
CreatedBy string `json:"created_by,omitempty"`
Description string `json:"description,omitempty"`
Endpoint string `json:"endpoint"`
ExternalSource interface{} `json:"external_source,omitempty"`
HTTPMethod string `json:"http_method"`
ID string `json:"id,omitempty"`
Name string `json:"name"`
RequestFormat string `json:"request_format"`
SigningSecret *WebhookSigningSecret `json:"signing_secret,omitempty"`
Status string `json:"status"`
Subscriptions []string `json:"subscriptions,omitempty"`
UpdatedAt time.Time `json:"updated_at,omitempty"`
UpdatedBy string `json:"updated_by,omitempty"`
}

type WebhookAuthentication struct {
Type string `json:"type"`
Data interface{} `json:"data"`
AddPosition string `json:"add_position"`
}

type WebhookSigningSecret struct {
Algorithm string `json:"algorithm"`
Secret string `json:"secret"`
}

type WebhookAPI interface {
CreateWebhook(ctx context.Context, hook *Webhook) (*Webhook, error)
GetWebhook(ctx context.Context, webhookID string) (*Webhook, error)
UpdateWebhook(ctx context.Context, webhookID string, hook *Webhook) error
DeleteWebhook(ctx context.Context, webhookID string) error
}

// CreateWebhook creates new webhook.
//
// https://developer.zendesk.com/api-reference/event-connectors/webhooks/webhooks/#create-or-clone-webhook
func (z *Client) CreateWebhook(ctx context.Context, hook *Webhook) (*Webhook, error) {
var data, result struct {
Webhook *Webhook `json:"webhook"`
}
data.Webhook = hook

body, err := z.post(ctx, "/webhooks", data)
if err != nil {
return nil, err
}

err = json.Unmarshal(body, &result)
if err != nil {
return nil, err
}
return result.Webhook, nil
}

// GetWebhook gets a specified webhook.
//
// https://developer.zendesk.com/api-reference/event-connectors/webhooks/webhooks/#show-webhook
func (z *Client) GetWebhook(ctx context.Context, webhookID string) (*Webhook, error) {
var result struct {
Webhook *Webhook `json:"webhook"`
}

body, err := z.get(ctx, fmt.Sprintf("/webhooks/%s", webhookID))
if err != nil {
return nil, err
}

err = json.Unmarshal(body, &result)
if err != nil {
return nil, err
}

return result.Webhook, nil
}

// UpdateWebhook updates a webhook with the specified webhook.
//
// https://developer.zendesk.com/api-reference/event-connectors/webhooks/webhooks/#update-webhook
func (z *Client) UpdateWebhook(ctx context.Context, webhookID string, hook *Webhook) error {
var data struct {
Webhook *Webhook `json:"webhook"`
}
data.Webhook = hook

_, err := z.put(ctx, fmt.Sprintf("/webhooks/%s", webhookID), data)
if err != nil {
return err
}

return nil
}

// DeleteWebhook deletes the specified webhook.
//
// https://developer.zendesk.com/api-reference/event-connectors/webhooks/webhooks/#delete-webhook
func (z *Client) DeleteWebhook(ctx context.Context, webhookID string) error {
err := z.delete(ctx, fmt.Sprintf("/webhooks/%s", webhookID))
if err != nil {
return err
}

return nil
}
82 changes: 82 additions & 0 deletions zendesk/webhook_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package zendesk

import (
"context"
"net/http"
"net/http/httptest"
"testing"
)

func TestCreateWebhook(t *testing.T) {
mockAPI := newMockAPI(http.MethodPost, "webhooks.json")
client := newTestClient(mockAPI)
defer mockAPI.Close()

hook, err := client.CreateWebhook(context.Background(), &Webhook{
Authentication: &WebhookAuthentication{
AddPosition: "header",
Data: map[string]string{
"password": "hello_123",
"username": "john_smith",
},
Type: "basic_auth",
},
Endpoint: "https://example.com/status/200",
HTTPMethod: http.MethodGet,
Name: "Example Webhook",
RequestFormat: "json",
Status: "active",
Subscriptions: []string{"conditional_ticket_events"},
})
if err != nil {
t.Fatalf("Failed to create webhook: %v", err)
}

if len(hook.Subscriptions) != 1 || hook.Authentication.AddPosition != "header" {
t.Fatalf("Invalid response of webhook: %v", hook)
}
}

func TestGetWebhook(t *testing.T) {
mockAPI := newMockAPI(http.MethodGet, "webhook.json")
client := newTestClient(mockAPI)
defer mockAPI.Close()

hook, err := client.GetWebhook(ctx, "01EJFTSCC78X5V07NPY2MHR00M")
if err != nil {
t.Fatalf("Failed to get webhook: %s", err)
}

expectedID := "01EJFTSCC78X5V07NPY2MHR00M"
if hook.ID != expectedID {
t.Fatalf("Returned webhook does not have the expected ID %s. Webhook ID is %s", expectedID, hook.ID)
}
}

func TestUpdateWebhook(t *testing.T) {
mockAPI := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
w.Write(nil)
}))
client := newTestClient(mockAPI)
defer mockAPI.Close()

err := client.UpdateWebhook(ctx, "01EJFTSCC78X5V07NPY2MHR00M", &Webhook{})
if err != nil {
t.Fatalf("Failed to send request to create webhook: %s", err)
}
}

func TestDeleteWebhook(t *testing.T) {
mockAPI := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
w.Write(nil)
}))
client := newTestClient(mockAPI)
defer mockAPI.Close()

err := client.DeleteWebhook(ctx, "01EJFTSCC78X5V07NPY2MHR00M")
if err != nil {
t.Fatalf("Failed to delete webhook: %s", err)
}
}
3 changes: 2 additions & 1 deletion zendesk/zendesk.go
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,8 @@ func (z *Client) put(ctx context.Context, path string, data interface{}) ([]byte
return nil, err
}

if resp.StatusCode != http.StatusOK {
// NOTE: some webhook mutation APIs return status No Content.
if !(resp.StatusCode == http.StatusOK || resp.StatusCode == http.StatusNoContent) {
return nil, Error{
body: body,
resp: resp,
Expand Down

0 comments on commit aa6418f

Please sign in to comment.