mirror of
				https://gitea.com/gitea/tea.git
				synced 2025-10-30 16:55:25 +01:00 
			
		
		
		
	 3495ec5ed4
			
		
	
	3495ec5ed4
	
	
	
		
			
			## Summary This PR adds support for organization-level and global webhooks in the tea CLI tool. ## Changes Made ### Organization Webhooks - Added `--org` flag to webhook commands to operate on organization-level webhooks - Implemented full CRUD operations for org webhooks (create, list, update, delete) - Extended TeaContext to support organization scope ### Global Webhooks - Added `--global` flag with placeholder implementation - Ready for when Gitea SDK adds global webhook API methods ### Technical Details - Updated context handling to support org/global scopes - Modified all webhook subcommands (create, list, update, delete) - Maintained backward compatibility for repository webhooks - Updated tests and documentation ## Usage Examples ```bash # Repository webhooks (existing) tea webhooks list tea webhooks create https://example.com/hook --events push # Organization webhooks (new) tea webhooks list --org myorg tea webhooks create https://example.com/hook --org myorg --events push,pull_request # Global webhooks (future) tea webhooks list --global ``` ## Testing - All existing tests pass - Updated test expectations for new descriptions - Manual testing of org webhook operations completed Closes: webhook management feature request Reviewed-on: https://gitea.com/gitea/tea/pulls/798 Reviewed-by: Lunny Xiao <xiaolunwen@gmail.com> Co-authored-by: Ross Golder <ross@golder.org> Co-committed-by: Ross Golder <ross@golder.org>
		
			
				
	
	
		
			394 lines
		
	
	
		
			9.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			394 lines
		
	
	
		
			9.7 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright 2024 The Gitea Authors. All rights reserved.
 | |
| // SPDX-License-Identifier: MIT
 | |
| 
 | |
| package webhooks
 | |
| 
 | |
| import (
 | |
| 	"strings"
 | |
| 	"testing"
 | |
| 
 | |
| 	"code.gitea.io/sdk/gitea"
 | |
| 	"github.com/stretchr/testify/assert"
 | |
| 	"github.com/urfave/cli/v3"
 | |
| )
 | |
| 
 | |
| func TestValidateWebhookType(t *testing.T) {
 | |
| 	validTypes := []string{"gitea", "gogs", "slack", "discord", "dingtalk", "telegram", "msteams", "feishu", "wechatwork", "packagist"}
 | |
| 
 | |
| 	for _, validType := range validTypes {
 | |
| 		t.Run("Valid_"+validType, func(t *testing.T) {
 | |
| 			hookType := gitea.HookType(validType)
 | |
| 			assert.NotEmpty(t, string(hookType))
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestParseWebhookEvents(t *testing.T) {
 | |
| 	tests := []struct {
 | |
| 		name     string
 | |
| 		input    string
 | |
| 		expected []string
 | |
| 	}{
 | |
| 		{
 | |
| 			name:     "Single event",
 | |
| 			input:    "push",
 | |
| 			expected: []string{"push"},
 | |
| 		},
 | |
| 		{
 | |
| 			name:     "Multiple events",
 | |
| 			input:    "push,pull_request,issues",
 | |
| 			expected: []string{"push", "pull_request", "issues"},
 | |
| 		},
 | |
| 		{
 | |
| 			name:     "Events with spaces",
 | |
| 			input:    "push, pull_request , issues",
 | |
| 			expected: []string{"push", "pull_request", "issues"},
 | |
| 		},
 | |
| 		{
 | |
| 			name:     "Empty event",
 | |
| 			input:    "",
 | |
| 			expected: []string{""},
 | |
| 		},
 | |
| 		{
 | |
| 			name:     "Single comma",
 | |
| 			input:    ",",
 | |
| 			expected: []string{"", ""},
 | |
| 		},
 | |
| 		{
 | |
| 			name:     "Complex events",
 | |
| 			input:    "pull_request,pull_request_review_approved,pull_request_sync",
 | |
| 			expected: []string{"pull_request", "pull_request_review_approved", "pull_request_sync"},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for _, tt := range tests {
 | |
| 		t.Run(tt.name, func(t *testing.T) {
 | |
| 			eventsList := strings.Split(tt.input, ",")
 | |
| 			events := make([]string, len(eventsList))
 | |
| 			for i, event := range eventsList {
 | |
| 				events[i] = strings.TrimSpace(event)
 | |
| 			}
 | |
| 
 | |
| 			assert.Equal(t, tt.expected, events)
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestWebhookConfigConstruction(t *testing.T) {
 | |
| 	tests := []struct {
 | |
| 		name           string
 | |
| 		url            string
 | |
| 		secret         string
 | |
| 		branchFilter   string
 | |
| 		authHeader     string
 | |
| 		expectedKeys   []string
 | |
| 		expectedValues map[string]string
 | |
| 	}{
 | |
| 		{
 | |
| 			name:         "Basic config",
 | |
| 			url:          "https://example.com/webhook",
 | |
| 			expectedKeys: []string{"url", "http_method", "content_type"},
 | |
| 			expectedValues: map[string]string{
 | |
| 				"url":          "https://example.com/webhook",
 | |
| 				"http_method":  "post",
 | |
| 				"content_type": "json",
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name:         "Config with secret",
 | |
| 			url:          "https://example.com/webhook",
 | |
| 			secret:       "my-secret",
 | |
| 			expectedKeys: []string{"url", "http_method", "content_type", "secret"},
 | |
| 			expectedValues: map[string]string{
 | |
| 				"url":          "https://example.com/webhook",
 | |
| 				"http_method":  "post",
 | |
| 				"content_type": "json",
 | |
| 				"secret":       "my-secret",
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name:         "Config with branch filter",
 | |
| 			url:          "https://example.com/webhook",
 | |
| 			branchFilter: "main,develop",
 | |
| 			expectedKeys: []string{"url", "http_method", "content_type", "branch_filter"},
 | |
| 			expectedValues: map[string]string{
 | |
| 				"url":           "https://example.com/webhook",
 | |
| 				"http_method":   "post",
 | |
| 				"content_type":  "json",
 | |
| 				"branch_filter": "main,develop",
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name:         "Config with auth header",
 | |
| 			url:          "https://example.com/webhook",
 | |
| 			authHeader:   "Bearer token123",
 | |
| 			expectedKeys: []string{"url", "http_method", "content_type", "authorization_header"},
 | |
| 			expectedValues: map[string]string{
 | |
| 				"url":                  "https://example.com/webhook",
 | |
| 				"http_method":          "post",
 | |
| 				"content_type":         "json",
 | |
| 				"authorization_header": "Bearer token123",
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name:         "Complete config",
 | |
| 			url:          "https://example.com/webhook",
 | |
| 			secret:       "secret123",
 | |
| 			branchFilter: "main",
 | |
| 			authHeader:   "X-Token: abc",
 | |
| 			expectedKeys: []string{"url", "http_method", "content_type", "secret", "branch_filter", "authorization_header"},
 | |
| 			expectedValues: map[string]string{
 | |
| 				"url":                  "https://example.com/webhook",
 | |
| 				"http_method":          "post",
 | |
| 				"content_type":         "json",
 | |
| 				"secret":               "secret123",
 | |
| 				"branch_filter":        "main",
 | |
| 				"authorization_header": "X-Token: abc",
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for _, tt := range tests {
 | |
| 		t.Run(tt.name, func(t *testing.T) {
 | |
| 			config := map[string]string{
 | |
| 				"url":          tt.url,
 | |
| 				"http_method":  "post",
 | |
| 				"content_type": "json",
 | |
| 			}
 | |
| 
 | |
| 			if tt.secret != "" {
 | |
| 				config["secret"] = tt.secret
 | |
| 			}
 | |
| 			if tt.branchFilter != "" {
 | |
| 				config["branch_filter"] = tt.branchFilter
 | |
| 			}
 | |
| 			if tt.authHeader != "" {
 | |
| 				config["authorization_header"] = tt.authHeader
 | |
| 			}
 | |
| 
 | |
| 			// Check all expected keys exist
 | |
| 			for _, key := range tt.expectedKeys {
 | |
| 				assert.Contains(t, config, key, "Expected key %s not found", key)
 | |
| 			}
 | |
| 
 | |
| 			// Check expected values
 | |
| 			for key, expectedValue := range tt.expectedValues {
 | |
| 				assert.Equal(t, expectedValue, config[key], "Value mismatch for key %s", key)
 | |
| 			}
 | |
| 
 | |
| 			// Check no unexpected keys
 | |
| 			assert.Len(t, config, len(tt.expectedKeys), "Config has unexpected keys")
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestWebhookCreateOptions(t *testing.T) {
 | |
| 	tests := []struct {
 | |
| 		name        string
 | |
| 		webhookType string
 | |
| 		events      []string
 | |
| 		active      bool
 | |
| 		config      map[string]string
 | |
| 	}{
 | |
| 		{
 | |
| 			name:        "Gitea webhook",
 | |
| 			webhookType: "gitea",
 | |
| 			events:      []string{"push", "pull_request"},
 | |
| 			active:      true,
 | |
| 			config: map[string]string{
 | |
| 				"url":          "https://example.com/webhook",
 | |
| 				"http_method":  "post",
 | |
| 				"content_type": "json",
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name:        "Slack webhook",
 | |
| 			webhookType: "slack",
 | |
| 			events:      []string{"push"},
 | |
| 			active:      true,
 | |
| 			config: map[string]string{
 | |
| 				"url":          "https://hooks.slack.com/services/xxx",
 | |
| 				"http_method":  "post",
 | |
| 				"content_type": "json",
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name:        "Discord webhook",
 | |
| 			webhookType: "discord",
 | |
| 			events:      []string{"pull_request", "pull_request_review_approved"},
 | |
| 			active:      false,
 | |
| 			config: map[string]string{
 | |
| 				"url":          "https://discord.com/api/webhooks/xxx",
 | |
| 				"http_method":  "post",
 | |
| 				"content_type": "json",
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for _, tt := range tests {
 | |
| 		t.Run(tt.name, func(t *testing.T) {
 | |
| 			option := gitea.CreateHookOption{
 | |
| 				Type:   gitea.HookType(tt.webhookType),
 | |
| 				Config: tt.config,
 | |
| 				Events: tt.events,
 | |
| 				Active: tt.active,
 | |
| 			}
 | |
| 
 | |
| 			assert.Equal(t, gitea.HookType(tt.webhookType), option.Type)
 | |
| 			assert.Equal(t, tt.events, option.Events)
 | |
| 			assert.Equal(t, tt.active, option.Active)
 | |
| 			assert.Equal(t, tt.config, option.Config)
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestWebhookURLValidation(t *testing.T) {
 | |
| 	tests := []struct {
 | |
| 		name      string
 | |
| 		url       string
 | |
| 		expectErr bool
 | |
| 	}{
 | |
| 		{
 | |
| 			name:      "Valid HTTPS URL",
 | |
| 			url:       "https://example.com/webhook",
 | |
| 			expectErr: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name:      "Valid HTTP URL",
 | |
| 			url:       "http://localhost:8080/webhook",
 | |
| 			expectErr: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name:      "Slack webhook URL",
 | |
| 			url:       "https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX",
 | |
| 			expectErr: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name:      "Discord webhook URL",
 | |
| 			url:       "https://discord.com/api/webhooks/123456789/abcdefgh",
 | |
| 			expectErr: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name:      "Empty URL",
 | |
| 			url:       "",
 | |
| 			expectErr: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name:      "Invalid URL scheme",
 | |
| 			url:       "ftp://example.com/webhook",
 | |
| 			expectErr: false, // URL validation is handled by Gitea API
 | |
| 		},
 | |
| 		{
 | |
| 			name:      "URL with path",
 | |
| 			url:       "https://example.com/api/v1/webhook",
 | |
| 			expectErr: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name:      "URL with query params",
 | |
| 			url:       "https://example.com/webhook?token=abc123",
 | |
| 			expectErr: false,
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for _, tt := range tests {
 | |
| 		t.Run(tt.name, func(t *testing.T) {
 | |
| 			// Basic URL validation - empty check
 | |
| 			if tt.url == "" && tt.expectErr {
 | |
| 				assert.Empty(t, tt.url, "Empty URL should be caught")
 | |
| 			} else if tt.url != "" {
 | |
| 				assert.NotEmpty(t, tt.url, "Non-empty URL should pass basic validation")
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestWebhookEventValidation(t *testing.T) {
 | |
| 	validEvents := []string{
 | |
| 		"push",
 | |
| 		"pull_request",
 | |
| 		"pull_request_sync",
 | |
| 		"pull_request_comment",
 | |
| 		"pull_request_review_approved",
 | |
| 		"pull_request_review_rejected",
 | |
| 		"pull_request_assigned",
 | |
| 		"pull_request_label",
 | |
| 		"pull_request_milestone",
 | |
| 		"issues",
 | |
| 		"issue_comment",
 | |
| 		"issue_assign",
 | |
| 		"issue_label",
 | |
| 		"issue_milestone",
 | |
| 		"create",
 | |
| 		"delete",
 | |
| 		"fork",
 | |
| 		"release",
 | |
| 		"wiki",
 | |
| 		"repository",
 | |
| 	}
 | |
| 
 | |
| 	for _, event := range validEvents {
 | |
| 		t.Run("Event_"+event, func(t *testing.T) {
 | |
| 			assert.NotEmpty(t, event, "Event name should not be empty")
 | |
| 			assert.NotContains(t, event, " ", "Event name should not contain spaces")
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestCreateCommandFlags(t *testing.T) {
 | |
| 	cmd := &CmdWebhooksCreate
 | |
| 
 | |
| 	// Test flag existence
 | |
| 	expectedFlags := []string{
 | |
| 		"type",
 | |
| 		"secret",
 | |
| 		"events",
 | |
| 		"active",
 | |
| 		"branch-filter",
 | |
| 		"authorization-header",
 | |
| 	}
 | |
| 
 | |
| 	for _, flagName := range expectedFlags {
 | |
| 		found := false
 | |
| 		for _, flag := range cmd.Flags {
 | |
| 			if flag.Names()[0] == flagName {
 | |
| 				found = true
 | |
| 				break
 | |
| 			}
 | |
| 		}
 | |
| 		assert.True(t, found, "Expected flag %s not found", flagName)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestCreateCommandMetadata(t *testing.T) {
 | |
| 	cmd := &CmdWebhooksCreate
 | |
| 
 | |
| 	assert.Equal(t, "create", cmd.Name)
 | |
| 	assert.Contains(t, cmd.Aliases, "c")
 | |
| 	assert.Equal(t, "Create a webhook", cmd.Usage)
 | |
| 	assert.Equal(t, "Create a webhook in repository, organization, or globally", cmd.Description)
 | |
| 	assert.Equal(t, "<webhook-url>", cmd.ArgsUsage)
 | |
| 	assert.NotNil(t, cmd.Action)
 | |
| }
 | |
| 
 | |
| func TestDefaultFlagValues(t *testing.T) {
 | |
| 	cmd := &CmdWebhooksCreate
 | |
| 
 | |
| 	// Find specific flags and test their defaults
 | |
| 	for _, flag := range cmd.Flags {
 | |
| 		switch f := flag.(type) {
 | |
| 		case *cli.StringFlag:
 | |
| 			switch f.Name {
 | |
| 			case "type":
 | |
| 				assert.Equal(t, "gitea", f.Value)
 | |
| 			case "events":
 | |
| 				assert.Equal(t, "push", f.Value)
 | |
| 			}
 | |
| 		case *cli.BoolFlag:
 | |
| 			switch f.Name {
 | |
| 			case "active":
 | |
| 				assert.True(t, f.Value)
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| }
 |