diff --git a/README.md b/README.md
index 08971e5..1bb51d4 100644
--- a/README.md
+++ b/README.md
@@ -109,6 +109,13 @@ mailtrap domains send-setup-instructions --id 123 --email "admin@yourdomain.com"
mailtrap templates list
mailtrap templates create --name "Welcome" --subject "Hello {{name}}" --body-html '
Hi!
'
+# Webhooks
+mailtrap webhooks list
+mailtrap webhooks get --id 1
+mailtrap webhooks create --url "https://example.com/hooks" --type email_sending --sending-stream transactional --event-types delivery,bounce
+mailtrap webhooks update --id 1 --active=false --event-types delivery,bounce,unsubscribe
+mailtrap webhooks delete --id 1
+
# Contacts
mailtrap contacts create --email "user@example.com" --first-name "John"
mailtrap contact-lists list
@@ -141,6 +148,7 @@ mailtrap domains list --output text
| **Domains** | `domains list`, `domains get`, `domains create`, `domains delete`, `domains send-setup-instructions` |
| **Templates** | `templates list`, `templates get`, `templates create`, `templates update`, `templates delete` |
| **Suppressions** | `suppressions list`, `suppressions delete` |
+| **Webhooks** | `webhooks list`, `webhooks get`, `webhooks create`, `webhooks update`, `webhooks delete` |
| **Stats** | `stats get`, `stats by-domain`, `stats by-category`, `stats by-esp`, `stats by-date` |
| **Email Logs** | `email-logs list`, `email-logs get` |
| **Contacts** | `contacts get`, `contacts create`, `contacts update`, `contacts delete`, `contacts import`, `contacts export`, `contacts import-status`, `contacts export-status`, `contacts create-event` |
diff --git a/cmd/root.go b/cmd/root.go
index cd1d13a..3be8dcf 100644
--- a/cmd/root.go
+++ b/cmd/root.go
@@ -28,6 +28,7 @@ import (
"github.com/mailtrap/mailtrap-cli/internal/commands/suppressions"
"github.com/mailtrap/mailtrap-cli/internal/commands/templates"
"github.com/mailtrap/mailtrap-cli/internal/commands/tokens"
+ "github.com/mailtrap/mailtrap-cli/internal/commands/webhooks"
)
func NewRootCmd(f *cmdutil.Factory) *cobra.Command {
@@ -57,6 +58,7 @@ func NewRootCmd(f *cmdutil.Factory) *cobra.Command {
cmd.AddCommand(stats.NewCmdStats(f))
cmd.AddCommand(templates.NewCmdTemplates(f))
cmd.AddCommand(email_logs.NewCmdEmailLogs(f))
+ cmd.AddCommand(webhooks.NewCmdWebhooks(f))
// Sandbox
cmd.AddCommand(projects.NewCmdProjects(f))
diff --git a/internal/commands/webhooks/create.go b/internal/commands/webhooks/create.go
new file mode 100644
index 0000000..a71f0e5
--- /dev/null
+++ b/internal/commands/webhooks/create.go
@@ -0,0 +1,98 @@
+package webhooks
+
+import (
+ "context"
+
+ "github.com/mailtrap/mailtrap-cli/internal/client"
+ "github.com/mailtrap/mailtrap-cli/internal/cmdutil"
+ "github.com/mailtrap/mailtrap-cli/internal/config"
+ "github.com/mailtrap/mailtrap-cli/internal/output"
+ "github.com/spf13/cobra"
+)
+
+type WebhookWithSecret struct {
+ Webhook
+ SigningSecret string `json:"signing_secret,omitempty"`
+}
+
+type webhookCreateResponse struct {
+ Data WebhookWithSecret `json:"data"`
+}
+
+var webhookCreateColumns = append(append([]output.Column{}, webhookColumns...), output.Column{Header: "SIGNING SECRET", Field: "signing_secret"})
+
+func NewCmdCreate(f *cmdutil.Factory) *cobra.Command {
+ var (
+ webhookURL string
+ webhookType string
+ active bool
+ payloadFormat string
+ sendingStream string
+ eventTypes []string
+ domainID int
+ )
+
+ cmd := &cobra.Command{
+ Use: "create",
+ Short: "Create a webhook",
+ RunE: func(cmd *cobra.Command, args []string) error {
+ if err := cmdutil.RequireFlag("url", webhookURL); err != nil {
+ return err
+ }
+ if err := cmdutil.RequireFlag("type", webhookType); err != nil {
+ return err
+ }
+
+ c, err := f.NewClient()
+ if err != nil {
+ return err
+ }
+
+ if _, err := config.RequireAccountID(); err != nil {
+ return err
+ }
+
+ path := cmdutil.AccountPath("webhooks")
+
+ webhookFields := map[string]interface{}{
+ "url": webhookURL,
+ "webhook_type": webhookType,
+ }
+ if cmd.Flags().Changed("active") {
+ webhookFields["active"] = active
+ }
+ if cmd.Flags().Changed("payload-format") {
+ webhookFields["payload_format"] = payloadFormat
+ }
+ if cmd.Flags().Changed("sending-stream") {
+ webhookFields["sending_stream"] = sendingStream
+ }
+ if cmd.Flags().Changed("event-types") {
+ webhookFields["event_types"] = eventTypes
+ }
+ if cmd.Flags().Changed("domain-id") {
+ webhookFields["domain_id"] = domainID
+ }
+
+ body := map[string]interface{}{"webhook": webhookFields}
+
+ var resp webhookCreateResponse
+ if err := c.Post(context.Background(), client.BaseGeneral, path, body, &resp); err != nil {
+ return err
+ }
+
+ format := cmdutil.GetOutputFormat()
+ return output.Print(f.IOStreams.Out, format, resp.Data, webhookCreateColumns)
+ },
+ }
+
+ cmd.Flags().StringVar(&webhookURL, "url", "", "Webhook URL (required)")
+ cmd.Flags().StringVar(&webhookType, "type", "", "Webhook type: email_sending, audit_log (required)")
+ cmd.Flags().BoolVar(&active, "active", true, "Whether the webhook is active")
+ cmd.Flags().StringVar(&payloadFormat, "payload-format", "", "Payload format: json, jsonlines")
+ cmd.Flags().StringVar(&sendingStream, "sending-stream", "", "Sending stream: transactional, bulk")
+ cmd.Flags().StringSliceVar(&eventTypes, "event-types", nil, "Event types (comma-separated): delivery, soft_bounce, bounce, suspension, unsubscribe, open, spam_complaint, click, reject")
+ cmd.Flags().IntVar(&domainID, "domain-id", 0, "Domain ID to scope the webhook to")
+
+ return cmd
+}
diff --git a/internal/commands/webhooks/delete.go b/internal/commands/webhooks/delete.go
new file mode 100644
index 0000000..57889cf
--- /dev/null
+++ b/internal/commands/webhooks/delete.go
@@ -0,0 +1,47 @@
+package webhooks
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/mailtrap/mailtrap-cli/internal/client"
+ "github.com/mailtrap/mailtrap-cli/internal/cmdutil"
+ "github.com/mailtrap/mailtrap-cli/internal/config"
+ "github.com/spf13/cobra"
+)
+
+func NewCmdDelete(f *cmdutil.Factory) *cobra.Command {
+ var webhookID string
+
+ cmd := &cobra.Command{
+ Use: "delete",
+ Short: "Delete a webhook",
+ RunE: func(cmd *cobra.Command, args []string) error {
+ if err := cmdutil.RequireFlag("id", webhookID); err != nil {
+ return err
+ }
+
+ c, err := f.NewClient()
+ if err != nil {
+ return err
+ }
+
+ if _, err := config.RequireAccountID(); err != nil {
+ return err
+ }
+
+ path := cmdutil.AccountPath("webhooks", webhookID)
+
+ if err := c.Delete(context.Background(), client.BaseGeneral, path, nil); err != nil {
+ return err
+ }
+
+ fmt.Fprintln(f.IOStreams.Out, "Webhook deleted successfully.")
+ return nil
+ },
+ }
+
+ cmd.Flags().StringVar(&webhookID, "id", "", "Webhook ID")
+
+ return cmd
+}
diff --git a/internal/commands/webhooks/get.go b/internal/commands/webhooks/get.go
new file mode 100644
index 0000000..5d2d96c
--- /dev/null
+++ b/internal/commands/webhooks/get.go
@@ -0,0 +1,48 @@
+package webhooks
+
+import (
+ "context"
+
+ "github.com/mailtrap/mailtrap-cli/internal/client"
+ "github.com/mailtrap/mailtrap-cli/internal/cmdutil"
+ "github.com/mailtrap/mailtrap-cli/internal/config"
+ "github.com/mailtrap/mailtrap-cli/internal/output"
+ "github.com/spf13/cobra"
+)
+
+func NewCmdGet(f *cmdutil.Factory) *cobra.Command {
+ var webhookID string
+
+ cmd := &cobra.Command{
+ Use: "get",
+ Short: "Get a webhook",
+ RunE: func(cmd *cobra.Command, args []string) error {
+ if err := cmdutil.RequireFlag("id", webhookID); err != nil {
+ return err
+ }
+
+ c, err := f.NewClient()
+ if err != nil {
+ return err
+ }
+
+ if _, err := config.RequireAccountID(); err != nil {
+ return err
+ }
+
+ path := cmdutil.AccountPath("webhooks", webhookID)
+
+ var resp webhookResponse
+ if err := c.Get(context.Background(), client.BaseGeneral, path, nil, &resp); err != nil {
+ return err
+ }
+
+ format := cmdutil.GetOutputFormat()
+ return output.Print(f.IOStreams.Out, format, resp.Data, webhookColumns)
+ },
+ }
+
+ cmd.Flags().StringVar(&webhookID, "id", "", "Webhook ID")
+
+ return cmd
+}
\ No newline at end of file
diff --git a/internal/commands/webhooks/list.go b/internal/commands/webhooks/list.go
new file mode 100644
index 0000000..45153a8
--- /dev/null
+++ b/internal/commands/webhooks/list.go
@@ -0,0 +1,70 @@
+package webhooks
+
+import (
+ "context"
+
+ "github.com/mailtrap/mailtrap-cli/internal/client"
+ "github.com/mailtrap/mailtrap-cli/internal/cmdutil"
+ "github.com/mailtrap/mailtrap-cli/internal/config"
+ "github.com/mailtrap/mailtrap-cli/internal/output"
+ "github.com/spf13/cobra"
+)
+
+type Webhook struct {
+ ID int `json:"id"`
+ URL string `json:"url"`
+ Active bool `json:"active"`
+ WebhookType string `json:"webhook_type"`
+ PayloadFormat string `json:"payload_format"`
+ SendingStream *string `json:"sending_stream,omitempty"`
+ DomainID *int `json:"domain_id,omitempty"`
+ EventTypes []string `json:"event_types,omitempty"`
+}
+
+type webhookListResponse struct {
+ Data []Webhook `json:"data"`
+}
+
+type webhookResponse struct {
+ Data Webhook `json:"data"`
+}
+
+var webhookColumns = []output.Column{
+ {Header: "ID", Field: "id"},
+ {Header: "URL", Field: "url"},
+ {Header: "TYPE", Field: "webhook_type"},
+ {Header: "ACTIVE", Field: "active"},
+ {Header: "FORMAT", Field: "payload_format"},
+ {Header: "STREAM", Field: "sending_stream"},
+ {Header: "DOMAIN ID", Field: "domain_id"},
+ {Header: "EVENTS", Field: "event_types"},
+}
+
+func NewCmdList(f *cmdutil.Factory) *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "list",
+ Short: "List all webhooks",
+ RunE: func(cmd *cobra.Command, args []string) error {
+ c, err := f.NewClient()
+ if err != nil {
+ return err
+ }
+
+ if _, err := config.RequireAccountID(); err != nil {
+ return err
+ }
+
+ path := cmdutil.AccountPath("webhooks")
+
+ var resp webhookListResponse
+ if err := c.Get(context.Background(), client.BaseGeneral, path, nil, &resp); err != nil {
+ return err
+ }
+
+ format := cmdutil.GetOutputFormat()
+ return output.Print(f.IOStreams.Out, format, resp.Data, webhookColumns)
+ },
+ }
+
+ return cmd
+}
diff --git a/internal/commands/webhooks/update.go b/internal/commands/webhooks/update.go
new file mode 100644
index 0000000..4ba0e4b
--- /dev/null
+++ b/internal/commands/webhooks/update.go
@@ -0,0 +1,74 @@
+package webhooks
+
+import (
+ "context"
+
+ "github.com/mailtrap/mailtrap-cli/internal/client"
+ "github.com/mailtrap/mailtrap-cli/internal/cmdutil"
+ "github.com/mailtrap/mailtrap-cli/internal/config"
+ "github.com/mailtrap/mailtrap-cli/internal/output"
+ "github.com/spf13/cobra"
+)
+
+func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command {
+ var (
+ webhookID string
+ webhookURL string
+ active bool
+ payloadFormat string
+ eventTypes []string
+ )
+
+ cmd := &cobra.Command{
+ Use: "update",
+ Short: "Update a webhook",
+ RunE: func(cmd *cobra.Command, args []string) error {
+ if err := cmdutil.RequireFlag("id", webhookID); err != nil {
+ return err
+ }
+
+ c, err := f.NewClient()
+ if err != nil {
+ return err
+ }
+
+ if _, err := config.RequireAccountID(); err != nil {
+ return err
+ }
+
+ path := cmdutil.AccountPath("webhooks", webhookID)
+
+ webhookFields := map[string]interface{}{}
+ if cmd.Flags().Changed("url") {
+ webhookFields["url"] = webhookURL
+ }
+ if cmd.Flags().Changed("active") {
+ webhookFields["active"] = active
+ }
+ if cmd.Flags().Changed("payload-format") {
+ webhookFields["payload_format"] = payloadFormat
+ }
+ if cmd.Flags().Changed("event-types") {
+ webhookFields["event_types"] = eventTypes
+ }
+
+ body := map[string]interface{}{"webhook": webhookFields}
+
+ var resp webhookResponse
+ if err := c.Patch(context.Background(), client.BaseGeneral, path, body, &resp); err != nil {
+ return err
+ }
+
+ format := cmdutil.GetOutputFormat()
+ return output.Print(f.IOStreams.Out, format, resp.Data, webhookColumns)
+ },
+ }
+
+ cmd.Flags().StringVar(&webhookID, "id", "", "Webhook ID (required)")
+ cmd.Flags().StringVar(&webhookURL, "url", "", "Webhook URL")
+ cmd.Flags().BoolVar(&active, "active", true, "Whether the webhook is active")
+ cmd.Flags().StringVar(&payloadFormat, "payload-format", "", "Payload format: json, jsonlines")
+ cmd.Flags().StringSliceVar(&eventTypes, "event-types", nil, "Event types (comma-separated): delivery, soft_bounce, bounce, suspension, unsubscribe, open, spam_complaint, click, reject")
+
+ return cmd
+}
\ No newline at end of file
diff --git a/internal/commands/webhooks/webhooks.go b/internal/commands/webhooks/webhooks.go
new file mode 100644
index 0000000..6a874cf
--- /dev/null
+++ b/internal/commands/webhooks/webhooks.go
@@ -0,0 +1,21 @@
+package webhooks
+
+import (
+ "github.com/mailtrap/mailtrap-cli/internal/cmdutil"
+ "github.com/spf13/cobra"
+)
+
+func NewCmdWebhooks(f *cmdutil.Factory) *cobra.Command {
+ cmd := &cobra.Command{
+ Use: "webhooks",
+ Short: "Manage webhooks",
+ }
+
+ cmd.AddCommand(NewCmdList(f))
+ cmd.AddCommand(NewCmdGet(f))
+ cmd.AddCommand(NewCmdCreate(f))
+ cmd.AddCommand(NewCmdUpdate(f))
+ cmd.AddCommand(NewCmdDelete(f))
+
+ return cmd
+}
diff --git a/internal/commands/webhooks/webhooks_test.go b/internal/commands/webhooks/webhooks_test.go
new file mode 100644
index 0000000..ef1daa4
--- /dev/null
+++ b/internal/commands/webhooks/webhooks_test.go
@@ -0,0 +1,366 @@
+package webhooks_test
+
+import (
+ "bytes"
+ "encoding/json"
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "strings"
+ "testing"
+
+ "github.com/mailtrap/mailtrap-cli/internal/client"
+ "github.com/mailtrap/mailtrap-cli/internal/cmdutil"
+ "github.com/mailtrap/mailtrap-cli/internal/commands/webhooks"
+ "github.com/mailtrap/mailtrap-cli/internal/config"
+ "github.com/spf13/viper"
+)
+
+func setupTest(handler http.HandlerFunc) (*cmdutil.Factory, *bytes.Buffer, func()) {
+ server := httptest.NewServer(handler)
+
+ c := client.New("test-token")
+ c.SetBaseURL(client.BaseGeneral, server.URL)
+
+ buf := &bytes.Buffer{}
+ f := &cmdutil.Factory{
+ Config: func() *config.Config {
+ return &config.Config{APIToken: "test-token", AccountID: "123"}
+ },
+ IOStreams: &cmdutil.IOStreams{
+ Out: buf,
+ ErrOut: &bytes.Buffer{},
+ },
+ ClientOverride: c,
+ }
+
+ viper.Set("api-token", "test-token")
+ viper.Set("account-id", "123")
+ viper.Set("output", "table")
+
+ return f, buf, func() {
+ server.Close()
+ viper.Reset()
+ }
+}
+
+func TestWebhooksList(t *testing.T) {
+ f, buf, cleanup := setupTest(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodGet {
+ t.Errorf("expected GET, got %s", r.Method)
+ }
+ if !strings.HasSuffix(r.URL.Path, "/api/accounts/123/webhooks") {
+ t.Errorf("unexpected path: %s", r.URL.Path)
+ }
+ if r.Header.Get("Api-Token") != "test-token" {
+ t.Errorf("expected Api-Token header 'test-token', got %q", r.Header.Get("Api-Token"))
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(map[string]interface{}{
+ "data": []map[string]interface{}{
+ {
+ "id": 1,
+ "url": "https://example.com/mailtrap/webhooks",
+ "active": true,
+ "webhook_type": "email_sending",
+ "payload_format": "json",
+ "sending_stream": "transactional",
+ "domain_id": 435,
+ "event_types": []string{"delivery", "bounce"},
+ },
+ {
+ "id": 2,
+ "url": "https://example.com/audit",
+ "active": true,
+ "webhook_type": "audit_log",
+ "payload_format": "json",
+ },
+ },
+ })
+ })
+ defer cleanup()
+
+ cmd := webhooks.NewCmdWebhooks(f)
+ cmd.SetArgs([]string{"list"})
+ cmd.SetOut(buf)
+
+ if err := cmd.Execute(); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ output := buf.String()
+ if !strings.Contains(output, "email_sending") {
+ t.Errorf("expected output to contain 'email_sending', got:\n%s", output)
+ }
+ if !strings.Contains(output, "audit_log") {
+ t.Errorf("expected output to contain 'audit_log', got:\n%s", output)
+ }
+ if !strings.Contains(output, "transactional") {
+ t.Errorf("expected output to contain 'transactional', got:\n%s", output)
+ }
+}
+
+func TestWebhooksListJSON(t *testing.T) {
+ f, buf, cleanup := setupTest(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(map[string]interface{}{
+ "data": []map[string]interface{}{
+ {
+ "id": 1,
+ "url": "https://example.com/mailtrap/webhooks",
+ "active": true,
+ "webhook_type": "email_sending",
+ "payload_format": "json",
+ },
+ },
+ })
+ })
+ defer cleanup()
+
+ viper.Set("output", "json")
+
+ cmd := webhooks.NewCmdWebhooks(f)
+ cmd.SetArgs([]string{"list"})
+ cmd.SetOut(buf)
+
+ if err := cmd.Execute(); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ var result []map[string]interface{}
+ if err := json.Unmarshal(buf.Bytes(), &result); err != nil {
+ t.Fatalf("output is not valid JSON: %v\noutput:\n%s", err, buf.String())
+ }
+
+ if len(result) != 1 {
+ t.Fatalf("expected 1 webhook, got %d", len(result))
+ }
+ if result[0]["webhook_type"] != "email_sending" {
+ t.Errorf("expected webhook_type 'email_sending', got %v", result[0]["webhook_type"])
+ }
+}
+
+func TestWebhooksGet(t *testing.T) {
+ f, buf, cleanup := setupTest(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodGet {
+ t.Errorf("expected GET, got %s", r.Method)
+ }
+ if !strings.HasSuffix(r.URL.Path, "/api/accounts/123/webhooks/1") {
+ t.Errorf("unexpected path: %s", r.URL.Path)
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(map[string]interface{}{
+ "data": map[string]interface{}{
+ "id": 1,
+ "url": "https://example.com/mailtrap/webhooks",
+ "active": true,
+ "webhook_type": "email_sending",
+ "payload_format": "json",
+ "sending_stream": "transactional",
+ "domain_id": 435,
+ "event_types": []string{"delivery", "bounce"},
+ },
+ })
+ })
+ defer cleanup()
+
+ cmd := webhooks.NewCmdWebhooks(f)
+ cmd.SetArgs([]string{"get", "--id", "1"})
+ cmd.SetOut(buf)
+
+ if err := cmd.Execute(); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ output := buf.String()
+ if !strings.Contains(output, "email_sending") {
+ t.Errorf("expected output to contain 'email_sending', got:\n%s", output)
+ }
+ if !strings.Contains(output, "https://example.com/mailtrap/webhooks") {
+ t.Errorf("expected output to contain webhook URL, got:\n%s", output)
+ }
+}
+
+func TestWebhooksGetMissingID(t *testing.T) {
+ f, _, cleanup := setupTest(func(w http.ResponseWriter, r *http.Request) {})
+ defer cleanup()
+
+ buf := &bytes.Buffer{}
+ f.IOStreams.Out = buf
+
+ cmd := webhooks.NewCmdWebhooks(f)
+ cmd.SetArgs([]string{"get"})
+ cmd.SetOut(buf)
+
+ err := cmd.Execute()
+ if err == nil {
+ t.Fatal("expected error when --id is missing")
+ }
+ if !strings.Contains(err.Error(), "--id is required") {
+ t.Errorf("expected '--id is required' error, got: %v", err)
+ }
+}
+
+func TestWebhooksCreate(t *testing.T) {
+ f, buf, cleanup := setupTest(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPost {
+ t.Errorf("expected POST, got %s", r.Method)
+ }
+ if !strings.HasSuffix(r.URL.Path, "/api/accounts/123/webhooks") {
+ t.Errorf("unexpected path: %s", r.URL.Path)
+ }
+
+ body, _ := io.ReadAll(r.Body)
+ var reqBody map[string]map[string]interface{}
+ json.Unmarshal(body, &reqBody)
+
+ webhook := reqBody["webhook"]
+ if webhook["url"] != "https://example.com/hooks" {
+ t.Errorf("unexpected url: %v", webhook["url"])
+ }
+ if webhook["webhook_type"] != "email_sending" {
+ t.Errorf("unexpected webhook_type: %v", webhook["webhook_type"])
+ }
+ if webhook["sending_stream"] != "transactional" {
+ t.Errorf("unexpected sending_stream: %v", webhook["sending_stream"])
+ }
+ if events, ok := webhook["event_types"].([]interface{}); !ok || len(events) != 2 {
+ t.Errorf("expected 2 event_types, got %v", webhook["event_types"])
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(map[string]interface{}{
+ "data": map[string]interface{}{
+ "id": 10,
+ "url": "https://example.com/hooks",
+ "active": true,
+ "webhook_type": "email_sending",
+ "payload_format": "json",
+ "sending_stream": "transactional",
+ "event_types": []string{"delivery", "bounce"},
+ "signing_secret": "abc123secret",
+ },
+ })
+ })
+ defer cleanup()
+
+ cmd := webhooks.NewCmdWebhooks(f)
+ cmd.SetArgs([]string{
+ "create",
+ "--url", "https://example.com/hooks",
+ "--type", "email_sending",
+ "--sending-stream", "transactional",
+ "--event-types", "delivery,bounce",
+ })
+ cmd.SetOut(buf)
+
+ if err := cmd.Execute(); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ output := buf.String()
+ if !strings.Contains(output, "abc123secret") {
+ t.Errorf("expected output to contain signing secret, got:\n%s", output)
+ }
+ if !strings.Contains(output, "email_sending") {
+ t.Errorf("expected output to contain 'email_sending', got:\n%s", output)
+ }
+}
+
+func TestWebhooksCreateMissingURL(t *testing.T) {
+ f, _, cleanup := setupTest(func(w http.ResponseWriter, r *http.Request) {})
+ defer cleanup()
+
+ buf := &bytes.Buffer{}
+ f.IOStreams.Out = buf
+
+ cmd := webhooks.NewCmdWebhooks(f)
+ cmd.SetArgs([]string{"create", "--type", "email_sending"})
+ cmd.SetOut(buf)
+
+ err := cmd.Execute()
+ if err == nil {
+ t.Fatal("expected error when --url is missing")
+ }
+ if !strings.Contains(err.Error(), "--url is required") {
+ t.Errorf("expected '--url is required' error, got: %v", err)
+ }
+}
+
+func TestWebhooksUpdate(t *testing.T) {
+ f, buf, cleanup := setupTest(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodPatch {
+ t.Errorf("expected PATCH, got %s", r.Method)
+ }
+ if !strings.HasSuffix(r.URL.Path, "/api/accounts/123/webhooks/1") {
+ t.Errorf("unexpected path: %s", r.URL.Path)
+ }
+
+ body, _ := io.ReadAll(r.Body)
+ var reqBody map[string]map[string]interface{}
+ json.Unmarshal(body, &reqBody)
+
+ webhook := reqBody["webhook"]
+ if webhook["active"] != false {
+ t.Errorf("expected active=false, got %v", webhook["active"])
+ }
+ if _, ok := webhook["url"]; ok {
+ t.Errorf("did not expect url to be set, got %v", webhook["url"])
+ }
+
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(map[string]interface{}{
+ "data": map[string]interface{}{
+ "id": 1,
+ "url": "https://example.com/mailtrap/webhooks",
+ "active": false,
+ "webhook_type": "email_sending",
+ "payload_format": "json",
+ },
+ })
+ })
+ defer cleanup()
+
+ cmd := webhooks.NewCmdWebhooks(f)
+ cmd.SetArgs([]string{"update", "--id", "1", "--active=false"})
+ cmd.SetOut(buf)
+
+ if err := cmd.Execute(); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ output := buf.String()
+ if !strings.Contains(output, "email_sending") {
+ t.Errorf("expected output to contain 'email_sending', got:\n%s", output)
+ }
+ if !strings.Contains(output, "false") {
+ t.Errorf("expected output to contain 'false' for active, got:\n%s", output)
+ }
+}
+
+func TestWebhooksDelete(t *testing.T) {
+ f, buf, cleanup := setupTest(func(w http.ResponseWriter, r *http.Request) {
+ if r.Method != http.MethodDelete {
+ t.Errorf("expected DELETE, got %s", r.Method)
+ }
+ if !strings.HasSuffix(r.URL.Path, "/api/accounts/123/webhooks/1") {
+ t.Errorf("unexpected path: %s", r.URL.Path)
+ }
+ w.WriteHeader(http.StatusOK)
+ })
+ defer cleanup()
+
+ cmd := webhooks.NewCmdWebhooks(f)
+ cmd.SetArgs([]string{"delete", "--id", "1"})
+ cmd.SetOut(buf)
+
+ if err := cmd.Execute(); err != nil {
+ t.Fatalf("unexpected error: %v", err)
+ }
+
+ if !strings.Contains(buf.String(), "Webhook deleted successfully") {
+ t.Errorf("expected success message, got:\n%s", buf.String())
+ }
+}