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>
		
			
				
	
	
		
			472 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			472 lines
		
	
	
		
			11 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 TestUpdateCommandMetadata(t *testing.T) {
 | |
| 	cmd := &CmdWebhooksUpdate
 | |
| 
 | |
| 	assert.Equal(t, "update", cmd.Name)
 | |
| 	assert.Contains(t, cmd.Aliases, "edit")
 | |
| 	assert.Contains(t, cmd.Aliases, "u")
 | |
| 	assert.Equal(t, "Update a webhook", cmd.Usage)
 | |
| 	assert.Equal(t, "Update webhook configuration in repository, organization, or globally", cmd.Description)
 | |
| 	assert.Equal(t, "<webhook-id>", cmd.ArgsUsage)
 | |
| 	assert.NotNil(t, cmd.Action)
 | |
| }
 | |
| 
 | |
| func TestUpdateCommandFlags(t *testing.T) {
 | |
| 	cmd := &CmdWebhooksUpdate
 | |
| 
 | |
| 	expectedFlags := []string{
 | |
| 		"url",
 | |
| 		"secret",
 | |
| 		"events",
 | |
| 		"active",
 | |
| 		"inactive",
 | |
| 		"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 TestUpdateActiveInactiveFlags(t *testing.T) {
 | |
| 	tests := []struct {
 | |
| 		name           string
 | |
| 		activeSet      bool
 | |
| 		activeValue    bool
 | |
| 		inactiveSet    bool
 | |
| 		inactiveValue  bool
 | |
| 		originalActive bool
 | |
| 		expectedActive bool
 | |
| 	}{
 | |
| 		{
 | |
| 			name:           "Set active to true",
 | |
| 			activeSet:      true,
 | |
| 			activeValue:    true,
 | |
| 			inactiveSet:    false,
 | |
| 			originalActive: false,
 | |
| 			expectedActive: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name:           "Set active to false",
 | |
| 			activeSet:      true,
 | |
| 			activeValue:    false,
 | |
| 			inactiveSet:    false,
 | |
| 			originalActive: true,
 | |
| 			expectedActive: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name:           "Set inactive to true",
 | |
| 			activeSet:      false,
 | |
| 			inactiveSet:    true,
 | |
| 			inactiveValue:  true,
 | |
| 			originalActive: true,
 | |
| 			expectedActive: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name:           "Set inactive to false",
 | |
| 			activeSet:      false,
 | |
| 			inactiveSet:    true,
 | |
| 			inactiveValue:  false,
 | |
| 			originalActive: false,
 | |
| 			expectedActive: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name:           "No flags set",
 | |
| 			activeSet:      false,
 | |
| 			inactiveSet:    false,
 | |
| 			originalActive: true,
 | |
| 			expectedActive: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name:           "Active flag takes precedence",
 | |
| 			activeSet:      true,
 | |
| 			activeValue:    true,
 | |
| 			inactiveSet:    true,
 | |
| 			inactiveValue:  true,
 | |
| 			originalActive: false,
 | |
| 			expectedActive: true,
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for _, tt := range tests {
 | |
| 		t.Run(tt.name, func(t *testing.T) {
 | |
| 			// Simulate the logic from runWebhooksUpdate
 | |
| 			active := tt.originalActive
 | |
| 
 | |
| 			if tt.activeSet {
 | |
| 				active = tt.activeValue
 | |
| 			} else if tt.inactiveSet {
 | |
| 				active = !tt.inactiveValue
 | |
| 			}
 | |
| 
 | |
| 			assert.Equal(t, tt.expectedActive, active)
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestUpdateConfigPreservation(t *testing.T) {
 | |
| 	// Test that existing configuration is preserved when not updated
 | |
| 	originalConfig := map[string]string{
 | |
| 		"url":                  "https://old.example.com/webhook",
 | |
| 		"secret":               "old-secret",
 | |
| 		"branch_filter":        "main",
 | |
| 		"authorization_header": "Bearer old-token",
 | |
| 		"http_method":          "post",
 | |
| 		"content_type":         "json",
 | |
| 	}
 | |
| 
 | |
| 	tests := []struct {
 | |
| 		name           string
 | |
| 		updates        map[string]string
 | |
| 		expectedConfig map[string]string
 | |
| 	}{
 | |
| 		{
 | |
| 			name: "Update only URL",
 | |
| 			updates: map[string]string{
 | |
| 				"url": "https://new.example.com/webhook",
 | |
| 			},
 | |
| 			expectedConfig: map[string]string{
 | |
| 				"url":                  "https://new.example.com/webhook",
 | |
| 				"secret":               "old-secret",
 | |
| 				"branch_filter":        "main",
 | |
| 				"authorization_header": "Bearer old-token",
 | |
| 				"http_method":          "post",
 | |
| 				"content_type":         "json",
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "Update secret and auth header",
 | |
| 			updates: map[string]string{
 | |
| 				"secret":               "new-secret",
 | |
| 				"authorization_header": "X-Token: new-token",
 | |
| 			},
 | |
| 			expectedConfig: map[string]string{
 | |
| 				"url":                  "https://old.example.com/webhook",
 | |
| 				"secret":               "new-secret",
 | |
| 				"branch_filter":        "main",
 | |
| 				"authorization_header": "X-Token: new-token",
 | |
| 				"http_method":          "post",
 | |
| 				"content_type":         "json",
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "Clear branch filter",
 | |
| 			updates: map[string]string{
 | |
| 				"branch_filter": "",
 | |
| 			},
 | |
| 			expectedConfig: map[string]string{
 | |
| 				"url":                  "https://old.example.com/webhook",
 | |
| 				"secret":               "old-secret",
 | |
| 				"branch_filter":        "",
 | |
| 				"authorization_header": "Bearer old-token",
 | |
| 				"http_method":          "post",
 | |
| 				"content_type":         "json",
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name:    "No updates",
 | |
| 			updates: map[string]string{},
 | |
| 			expectedConfig: map[string]string{
 | |
| 				"url":                  "https://old.example.com/webhook",
 | |
| 				"secret":               "old-secret",
 | |
| 				"branch_filter":        "main",
 | |
| 				"authorization_header": "Bearer old-token",
 | |
| 				"http_method":          "post",
 | |
| 				"content_type":         "json",
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for _, tt := range tests {
 | |
| 		t.Run(tt.name, func(t *testing.T) {
 | |
| 			// Copy original config
 | |
| 			config := make(map[string]string)
 | |
| 			for k, v := range originalConfig {
 | |
| 				config[k] = v
 | |
| 			}
 | |
| 
 | |
| 			// Apply updates
 | |
| 			for k, v := range tt.updates {
 | |
| 				config[k] = v
 | |
| 			}
 | |
| 
 | |
| 			// Verify expected config
 | |
| 			assert.Equal(t, tt.expectedConfig, config)
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestUpdateEventsHandling(t *testing.T) {
 | |
| 	tests := []struct {
 | |
| 		name           string
 | |
| 		originalEvents []string
 | |
| 		newEvents      string
 | |
| 		setEvents      bool
 | |
| 		expectedEvents []string
 | |
| 	}{
 | |
| 		{
 | |
| 			name:           "Update events",
 | |
| 			originalEvents: []string{"push"},
 | |
| 			newEvents:      "push,pull_request,issues",
 | |
| 			setEvents:      true,
 | |
| 			expectedEvents: []string{"push", "pull_request", "issues"},
 | |
| 		},
 | |
| 		{
 | |
| 			name:           "Clear events",
 | |
| 			originalEvents: []string{"push", "pull_request"},
 | |
| 			newEvents:      "",
 | |
| 			setEvents:      true,
 | |
| 			expectedEvents: []string{""},
 | |
| 		},
 | |
| 		{
 | |
| 			name:           "No event update",
 | |
| 			originalEvents: []string{"push", "pull_request"},
 | |
| 			newEvents:      "",
 | |
| 			setEvents:      false,
 | |
| 			expectedEvents: []string{"push", "pull_request"},
 | |
| 		},
 | |
| 		{
 | |
| 			name:           "Single event",
 | |
| 			originalEvents: []string{"push", "issues"},
 | |
| 			newEvents:      "pull_request",
 | |
| 			setEvents:      true,
 | |
| 			expectedEvents: []string{"pull_request"},
 | |
| 		},
 | |
| 		{
 | |
| 			name:           "Events with spaces",
 | |
| 			originalEvents: []string{"push"},
 | |
| 			newEvents:      "push, pull_request , issues",
 | |
| 			setEvents:      true,
 | |
| 			expectedEvents: []string{"push", "pull_request", "issues"},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for _, tt := range tests {
 | |
| 		t.Run(tt.name, func(t *testing.T) {
 | |
| 			events := tt.originalEvents
 | |
| 
 | |
| 			if tt.setEvents {
 | |
| 				eventsList := []string{}
 | |
| 				if tt.newEvents != "" {
 | |
| 					parts := strings.Split(tt.newEvents, ",")
 | |
| 					for _, part := range parts {
 | |
| 						eventsList = append(eventsList, strings.TrimSpace(part))
 | |
| 					}
 | |
| 				} else {
 | |
| 					eventsList = []string{""}
 | |
| 				}
 | |
| 				events = eventsList
 | |
| 			}
 | |
| 
 | |
| 			assert.Equal(t, tt.expectedEvents, events)
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestUpdateEditHookOption(t *testing.T) {
 | |
| 	tests := []struct {
 | |
| 		name     string
 | |
| 		config   map[string]string
 | |
| 		events   []string
 | |
| 		active   bool
 | |
| 		expected gitea.EditHookOption
 | |
| 	}{
 | |
| 		{
 | |
| 			name: "Complete update",
 | |
| 			config: map[string]string{
 | |
| 				"url":    "https://example.com/webhook",
 | |
| 				"secret": "new-secret",
 | |
| 			},
 | |
| 			events: []string{"push", "pull_request"},
 | |
| 			active: true,
 | |
| 			expected: gitea.EditHookOption{
 | |
| 				Config: map[string]string{
 | |
| 					"url":    "https://example.com/webhook",
 | |
| 					"secret": "new-secret",
 | |
| 				},
 | |
| 				Events: []string{"push", "pull_request"},
 | |
| 				Active: &[]bool{true}[0],
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name: "Config only update",
 | |
| 			config: map[string]string{
 | |
| 				"url": "https://new.example.com/webhook",
 | |
| 			},
 | |
| 			events: []string{"push"},
 | |
| 			active: false,
 | |
| 			expected: gitea.EditHookOption{
 | |
| 				Config: map[string]string{
 | |
| 					"url": "https://new.example.com/webhook",
 | |
| 				},
 | |
| 				Events: []string{"push"},
 | |
| 				Active: &[]bool{false}[0],
 | |
| 			},
 | |
| 		},
 | |
| 		{
 | |
| 			name:   "Minimal update",
 | |
| 			config: map[string]string{},
 | |
| 			events: []string{},
 | |
| 			active: true,
 | |
| 			expected: gitea.EditHookOption{
 | |
| 				Config: map[string]string{},
 | |
| 				Events: []string{},
 | |
| 				Active: &[]bool{true}[0],
 | |
| 			},
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for _, tt := range tests {
 | |
| 		t.Run(tt.name, func(t *testing.T) {
 | |
| 			option := gitea.EditHookOption{
 | |
| 				Config: tt.config,
 | |
| 				Events: tt.events,
 | |
| 				Active: &tt.active,
 | |
| 			}
 | |
| 
 | |
| 			assert.Equal(t, tt.expected.Config, option.Config)
 | |
| 			assert.Equal(t, tt.expected.Events, option.Events)
 | |
| 			assert.Equal(t, *tt.expected.Active, *option.Active)
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestUpdateWebhookIDValidation(t *testing.T) {
 | |
| 	tests := []struct {
 | |
| 		name        string
 | |
| 		webhookID   string
 | |
| 		expectedID  int64
 | |
| 		expectError bool
 | |
| 	}{
 | |
| 		{
 | |
| 			name:        "Valid webhook ID",
 | |
| 			webhookID:   "123",
 | |
| 			expectedID:  123,
 | |
| 			expectError: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name:        "Single digit ID",
 | |
| 			webhookID:   "1",
 | |
| 			expectedID:  1,
 | |
| 			expectError: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name:        "Large webhook ID",
 | |
| 			webhookID:   "999999",
 | |
| 			expectedID:  999999,
 | |
| 			expectError: false,
 | |
| 		},
 | |
| 		{
 | |
| 			name:        "Zero webhook ID",
 | |
| 			webhookID:   "0",
 | |
| 			expectedID:  0,
 | |
| 			expectError: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name:        "Negative webhook ID",
 | |
| 			webhookID:   "-1",
 | |
| 			expectedID:  0,
 | |
| 			expectError: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name:        "Non-numeric webhook ID",
 | |
| 			webhookID:   "abc",
 | |
| 			expectedID:  0,
 | |
| 			expectError: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name:        "Empty webhook ID",
 | |
| 			webhookID:   "",
 | |
| 			expectedID:  0,
 | |
| 			expectError: true,
 | |
| 		},
 | |
| 		{
 | |
| 			name:        "Float webhook ID",
 | |
| 			webhookID:   "12.34",
 | |
| 			expectedID:  0,
 | |
| 			expectError: true,
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	for _, tt := range tests {
 | |
| 		t.Run(tt.name, func(t *testing.T) {
 | |
| 			// This simulates the utils.ArgToIndex function behavior
 | |
| 			if tt.webhookID == "" {
 | |
| 				assert.True(t, tt.expectError)
 | |
| 				return
 | |
| 			}
 | |
| 
 | |
| 			// Basic validation - check if it's numeric
 | |
| 			isNumeric := true
 | |
| 			for _, char := range tt.webhookID {
 | |
| 				if char < '0' || char > '9' {
 | |
| 					if !(char == '-' && tt.webhookID[0] == '-') {
 | |
| 						isNumeric = false
 | |
| 						break
 | |
| 					}
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			if !isNumeric || tt.webhookID == "0" || (len(tt.webhookID) > 0 && tt.webhookID[0] == '-') {
 | |
| 				assert.True(t, tt.expectError, "Should expect error for invalid ID: %s", tt.webhookID)
 | |
| 			} else {
 | |
| 				assert.False(t, tt.expectError, "Should not expect error for valid ID: %s", tt.webhookID)
 | |
| 			}
 | |
| 		})
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func TestUpdateFlagTypes(t *testing.T) {
 | |
| 	cmd := &CmdWebhooksUpdate
 | |
| 
 | |
| 	flagTypes := map[string]string{
 | |
| 		"url":                  "string",
 | |
| 		"secret":               "string",
 | |
| 		"events":               "string",
 | |
| 		"active":               "bool",
 | |
| 		"inactive":             "bool",
 | |
| 		"branch-filter":        "string",
 | |
| 		"authorization-header": "string",
 | |
| 	}
 | |
| 
 | |
| 	for flagName, expectedType := range flagTypes {
 | |
| 		found := false
 | |
| 		for _, flag := range cmd.Flags {
 | |
| 			if flag.Names()[0] == flagName {
 | |
| 				found = true
 | |
| 				switch expectedType {
 | |
| 				case "string":
 | |
| 					_, ok := flag.(*cli.StringFlag)
 | |
| 					assert.True(t, ok, "Flag %s should be a StringFlag", flagName)
 | |
| 				case "bool":
 | |
| 					_, ok := flag.(*cli.BoolFlag)
 | |
| 					assert.True(t, ok, "Flag %s should be a BoolFlag", flagName)
 | |
| 				}
 | |
| 				break
 | |
| 			}
 | |
| 		}
 | |
| 		assert.True(t, found, "Flag %s not found", flagName)
 | |
| 	}
 | |
| }
 |