mirror of
https://gitea.com/gitea/tea.git
synced 2025-10-20 14:34:05 +02:00

## 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>
471 lines
11 KiB
Go
471 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)
|
|
}
|
|
}
|