Files
Roberto Jiménez Sánchez 047499a363 Provisioning: introduce concept of provisioning extras (#104981)
* Spike: Extras

* Attempt to wire it up

* Hack

* Fix issue with jobs

* Wire more things up

* Fix more wiring stuff

* Remove webhook secret key from main registration

* Move secret encryption also outside register

* Add TODOs in code

* Add more explanations

* Move connectors to different package

* Move pull request job into webhooks

* Separate registration

* Remove duplicate files

* Fix missing function

* Extract webhook repository logic out of the core github repository

* Use status patcher in webhook connector

* Fix change in go mod

* Change hooks signature

* Remove TODOs

* Remove Webhook methos from go-git

* Remove leftover

* Fix mistake in OpenAPI spec

* Fix some tests

* Fix some issues

* Fix linting
2025-05-13 09:50:43 +02:00

1677 lines
49 KiB
Go

package webhooks
import (
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"errors"
"fmt"
"net/http"
"os"
"path"
"slices"
"strings"
"testing"
provisioning "github.com/grafana/grafana/pkg/apis/provisioning/v0alpha1"
pgh "github.com/grafana/grafana/pkg/registry/apis/provisioning/repository/github"
"github.com/grafana/grafana/pkg/registry/apis/provisioning/secrets"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func TestParseWebhooks(t *testing.T) {
tests := []struct {
messageType string
name string
expected provisioning.WebhookResponse
}{
{"ping", "check", provisioning.WebhookResponse{
Code: http.StatusOK,
}},
{"pull_request", "opened", provisioning.WebhookResponse{
Code: http.StatusAccepted, // 202
Job: &provisioning.JobSpec{
Repository: "unit-test-repo",
Action: provisioning.JobActionPullRequest,
PullRequest: &provisioning.PullRequestJobOptions{
Ref: "dashboard/1733653266690",
Hash: "ab5446a53df9e5f8bdeed52250f51fad08e822bc",
PR: 12,
URL: "https://github.com/grafana/git-ui-sync-demo/pull/12",
},
},
}},
{"push", "different_branch", provisioning.WebhookResponse{
Code: http.StatusOK, // we don't care about a branch that isn't the one we configured
}},
{"push", "nothing_relevant", provisioning.WebhookResponse{
Code: http.StatusAccepted,
Job: &provisioning.JobSpec{ // we want to always push a sync job
Repository: "unit-test-repo",
Action: provisioning.JobActionPull,
Pull: &provisioning.SyncJobOptions{
Incremental: true,
},
},
}},
{"push", "nested", provisioning.WebhookResponse{
Code: http.StatusAccepted,
Job: &provisioning.JobSpec{
Repository: "unit-test-repo",
Action: provisioning.JobActionPull,
Pull: &provisioning.SyncJobOptions{
Incremental: true,
},
},
}},
{"issue_comment", "created", provisioning.WebhookResponse{
Code: http.StatusNotImplemented,
}},
}
gh := &githubWebhookRepository{
config: &provisioning.Repository{
ObjectMeta: metav1.ObjectMeta{
Name: "unit-test-repo",
},
Spec: provisioning.RepositorySpec{
Sync: provisioning.SyncOptions{
Enabled: true, // required to accept sync job
},
GitHub: &provisioning.GitHubRepositoryConfig{
URL: "https://github.com/grafana/git-ui-sync-demo",
Branch: "main",
GenerateDashboardPreviews: true,
},
},
},
owner: "grafana",
repo: "git-ui-sync-demo",
}
for _, tt := range tests {
name := fmt.Sprintf("webhook-%s-%s.json", tt.messageType, tt.name)
t.Run(name, func(t *testing.T) {
// nolint:gosec
payload, err := os.ReadFile(path.Join("testdata", name))
require.NoError(t, err)
rsp, err := gh.parseWebhook(tt.messageType, payload)
require.NoError(t, err)
require.Equal(t, tt.expected.Code, rsp.Code)
require.Equal(t, tt.expected.Job, rsp.Job)
})
}
}
func TestGitHubRepository_Webhook(t *testing.T) {
tests := []struct {
name string
config *provisioning.Repository
webhookSecret string
setupRequest func() *http.Request
mockSetup func(t *testing.T, mockSecrets *secrets.MockService)
expected *provisioning.WebhookResponse
expectedError error
}{
{
name: "missing webhook configuration",
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
GitHub: &provisioning.GitHubRepositoryConfig{
Branch: "main",
},
},
Status: provisioning.RepositoryStatus{
// No webhook configuration
},
},
setupRequest: func() *http.Request {
req, _ := http.NewRequest("POST", "/webhook", nil)
return req
},
expectedError: fmt.Errorf("unexpected webhook request"),
},
{
name: "secret decryption error",
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
GitHub: &provisioning.GitHubRepositoryConfig{
Branch: "main",
},
},
Status: provisioning.RepositoryStatus{
Webhook: &provisioning.WebhookStatus{
EncryptedSecret: []byte("encrypted-secret"),
},
},
},
setupRequest: func() *http.Request {
req, _ := http.NewRequest("POST", "/webhook", nil)
return req
},
mockSetup: func(t *testing.T, mockSecrets *secrets.MockService) {
mockSecrets.EXPECT().Decrypt(mock.Anything, []byte("encrypted-secret")).
Return(nil, errors.New("decryption failed"))
},
expectedError: fmt.Errorf("failed to decrypt secret: decryption failed"),
},
{
name: "invalid signature",
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
GitHub: &provisioning.GitHubRepositoryConfig{
Branch: "main",
},
},
Status: provisioning.RepositoryStatus{
Webhook: &provisioning.WebhookStatus{
EncryptedSecret: []byte("encrypted-secret"),
},
},
},
webhookSecret: "webhook-secret",
setupRequest: func() *http.Request {
req, _ := http.NewRequest("POST", "/webhook", strings.NewReader("invalid payload"))
req.Header.Set("X-Hub-Signature-256", "invalid")
req.Header.Set("Content-Type", "application/json")
return req
},
mockSetup: func(t *testing.T, mockSecrets *secrets.MockService) {
mockSecrets.EXPECT().Decrypt(mock.Anything, []byte("encrypted-secret")).
Return([]byte("webhook-secret"), nil)
},
expectedError: apierrors.NewUnauthorized("invalid signature"),
},
{
name: "ping event",
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
GitHub: &provisioning.GitHubRepositoryConfig{
Branch: "main",
},
},
Status: provisioning.RepositoryStatus{
Webhook: &provisioning.WebhookStatus{
EncryptedSecret: []byte("encrypted-secret"),
},
},
},
webhookSecret: "webhook-secret",
setupRequest: func() *http.Request {
payload := `{}`
req, _ := http.NewRequest("POST", "/webhook", strings.NewReader(payload))
req.Header.Set("X-GitHub-Event", "ping")
req.Header.Set("Content-Type", "application/json")
// Create a valid signature
mac := hmac.New(sha256.New, []byte("webhook-secret"))
mac.Write([]byte(payload))
signature := hex.EncodeToString(mac.Sum(nil))
req.Header.Set("X-Hub-Signature-256", "sha256="+signature)
return req
},
mockSetup: func(t *testing.T, mockSecrets *secrets.MockService) {
mockSecrets.EXPECT().Decrypt(mock.Anything, []byte("encrypted-secret")).
Return([]byte("webhook-secret"), nil)
},
expected: &provisioning.WebhookResponse{
Code: http.StatusOK,
Message: "ping received",
},
},
{
name: "push event for different branch",
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
GitHub: &provisioning.GitHubRepositoryConfig{
Branch: "main",
},
Sync: provisioning.SyncOptions{
Enabled: true,
},
},
Status: provisioning.RepositoryStatus{
Webhook: &provisioning.WebhookStatus{
EncryptedSecret: []byte("encrypted-secret"),
},
},
},
webhookSecret: "webhook-secret",
setupRequest: func() *http.Request {
payload := `{
"ref": "refs/heads/feature",
"repository": {
"full_name": "grafana/grafana"
}
}`
req, _ := http.NewRequest("POST", "/webhook", strings.NewReader(payload))
req.Header.Set("X-GitHub-Event", "push")
req.Header.Set("Content-Type", "application/json")
// Create a valid signature
mac := hmac.New(sha256.New, []byte("webhook-secret"))
mac.Write([]byte(payload))
signature := hex.EncodeToString(mac.Sum(nil))
req.Header.Set("X-Hub-Signature-256", "sha256="+signature)
return req
},
mockSetup: func(t *testing.T, mockSecrets *secrets.MockService) {
mockSecrets.EXPECT().Decrypt(mock.Anything, []byte("encrypted-secret")).
Return([]byte("webhook-secret"), nil)
},
expected: &provisioning.WebhookResponse{
Code: http.StatusOK,
},
},
{
name: "push event for main branch",
config: &provisioning.Repository{
ObjectMeta: metav1.ObjectMeta{
Name: "test-repo",
},
Spec: provisioning.RepositorySpec{
GitHub: &provisioning.GitHubRepositoryConfig{
Branch: "main",
},
Sync: provisioning.SyncOptions{
Enabled: true,
},
},
Status: provisioning.RepositoryStatus{
Webhook: &provisioning.WebhookStatus{
EncryptedSecret: []byte("encrypted-secret"),
},
},
},
webhookSecret: "webhook-secret",
setupRequest: func() *http.Request {
payload := `{
"ref": "refs/heads/main",
"repository": {
"full_name": "grafana/grafana"
}
}`
req, _ := http.NewRequest("POST", "/webhook", strings.NewReader(payload))
req.Header.Set("X-GitHub-Event", "push")
req.Header.Set("Content-Type", "application/json")
// Create a valid signature
mac := hmac.New(sha256.New, []byte("webhook-secret"))
mac.Write([]byte(payload))
signature := hex.EncodeToString(mac.Sum(nil))
req.Header.Set("X-Hub-Signature-256", "sha256="+signature)
return req
},
mockSetup: func(t *testing.T, mockSecrets *secrets.MockService) {
mockSecrets.EXPECT().Decrypt(mock.Anything, []byte("encrypted-secret")).
Return([]byte("webhook-secret"), nil)
},
expected: &provisioning.WebhookResponse{
Code: http.StatusAccepted,
Job: &provisioning.JobSpec{
Repository: "test-repo",
Action: provisioning.JobActionPull,
Pull: &provisioning.SyncJobOptions{
Incremental: true,
},
},
},
},
{
name: "push event with missing repository",
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
GitHub: &provisioning.GitHubRepositoryConfig{
Branch: "main",
},
},
Status: provisioning.RepositoryStatus{
Webhook: &provisioning.WebhookStatus{
EncryptedSecret: []byte("encrypted-secret"),
},
},
},
webhookSecret: "webhook-secret",
setupRequest: func() *http.Request {
payload := `{
"ref": "refs/heads/main"
}`
req, _ := http.NewRequest("POST", "/webhook", strings.NewReader(payload))
req.Header.Set("X-GitHub-Event", "push")
req.Header.Set("Content-Type", "application/json")
// Create a valid signature
mac := hmac.New(sha256.New, []byte("webhook-secret"))
mac.Write([]byte(payload))
signature := hex.EncodeToString(mac.Sum(nil))
req.Header.Set("X-Hub-Signature-256", "sha256="+signature)
return req
},
mockSetup: func(t *testing.T, mockSecrets *secrets.MockService) {
mockSecrets.EXPECT().Decrypt(mock.Anything, []byte("encrypted-secret")).
Return([]byte("webhook-secret"), nil)
},
expectedError: fmt.Errorf("missing repository in push event"),
},
{
name: "push event with repository mismatch",
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
GitHub: &provisioning.GitHubRepositoryConfig{
Branch: "main",
},
},
Status: provisioning.RepositoryStatus{
Webhook: &provisioning.WebhookStatus{
EncryptedSecret: []byte("encrypted-secret"),
},
},
},
webhookSecret: "webhook-secret",
setupRequest: func() *http.Request {
payload := `{
"ref": "refs/heads/main",
"repository": {
"full_name": "different-owner/different-repo"
}
}`
req, _ := http.NewRequest("POST", "/webhook", strings.NewReader(payload))
req.Header.Set("X-GitHub-Event", "push")
req.Header.Set("Content-Type", "application/json")
// Create a valid signature
mac := hmac.New(sha256.New, []byte("webhook-secret"))
mac.Write([]byte(payload))
signature := hex.EncodeToString(mac.Sum(nil))
req.Header.Set("X-Hub-Signature-256", "sha256="+signature)
return req
},
mockSetup: func(t *testing.T, mockSecrets *secrets.MockService) {
mockSecrets.EXPECT().Decrypt(mock.Anything, []byte("encrypted-secret")).
Return([]byte("webhook-secret"), nil)
},
expectedError: fmt.Errorf("repository mismatch"),
},
{
name: "push event when sync is disabled",
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
GitHub: &provisioning.GitHubRepositoryConfig{
Branch: "main",
},
Sync: provisioning.SyncOptions{
Enabled: false,
},
},
Status: provisioning.RepositoryStatus{
Webhook: &provisioning.WebhookStatus{
EncryptedSecret: []byte("encrypted-secret"),
},
},
},
webhookSecret: "webhook-secret",
setupRequest: func() *http.Request {
payload := `{
"ref": "refs/heads/main",
"repository": {
"full_name": "grafana/grafana"
}
}`
req, _ := http.NewRequest("POST", "/webhook", strings.NewReader(payload))
req.Header.Set("X-GitHub-Event", "push")
req.Header.Set("Content-Type", "application/json")
// Create a valid signature
mac := hmac.New(sha256.New, []byte("webhook-secret"))
mac.Write([]byte(payload))
signature := hex.EncodeToString(mac.Sum(nil))
req.Header.Set("X-Hub-Signature-256", "sha256="+signature)
return req
},
mockSetup: func(t *testing.T, mockSecrets *secrets.MockService) {
mockSecrets.EXPECT().Decrypt(mock.Anything, []byte("encrypted-secret")).
Return([]byte("webhook-secret"), nil)
},
expected: &provisioning.WebhookResponse{
Code: http.StatusOK,
},
},
{
name: "pull request event - opened",
config: &provisioning.Repository{
ObjectMeta: metav1.ObjectMeta{
Name: "test-repo",
},
Spec: provisioning.RepositorySpec{
GitHub: &provisioning.GitHubRepositoryConfig{
Branch: "main",
},
},
Status: provisioning.RepositoryStatus{
Webhook: &provisioning.WebhookStatus{
EncryptedSecret: []byte("encrypted-secret"),
},
},
},
webhookSecret: "webhook-secret",
setupRequest: func() *http.Request {
payload := `{
"action": "opened",
"pull_request": {
"html_url": "https://github.com/grafana/grafana/pull/123",
"number": 123,
"head": {
"ref": "feature-branch",
"sha": "abcdef1234567890"
},
"base": {
"ref": "main"
}
},
"repository": {
"full_name": "grafana/grafana"
}
}`
req, _ := http.NewRequest("POST", "/webhook", strings.NewReader(payload))
req.Header.Set("X-GitHub-Event", "pull_request")
req.Header.Set("Content-Type", "application/json")
// Create a valid signature
mac := hmac.New(sha256.New, []byte("webhook-secret"))
mac.Write([]byte(payload))
signature := hex.EncodeToString(mac.Sum(nil))
req.Header.Set("X-Hub-Signature-256", "sha256="+signature)
return req
},
mockSetup: func(t *testing.T, mockSecrets *secrets.MockService) {
mockSecrets.EXPECT().Decrypt(mock.Anything, []byte("encrypted-secret")).
Return([]byte("webhook-secret"), nil)
},
expected: &provisioning.WebhookResponse{
Code: http.StatusAccepted,
Message: "pull request: opened",
Job: &provisioning.JobSpec{
Repository: "test-repo",
Action: provisioning.JobActionPullRequest,
PullRequest: &provisioning.PullRequestJobOptions{
URL: "https://github.com/grafana/grafana/pull/123",
PR: 123,
Ref: "feature-branch",
Hash: "abcdef1234567890",
},
},
},
},
{
name: "pull request event - synchronize",
config: &provisioning.Repository{
ObjectMeta: metav1.ObjectMeta{
Name: "test-repo",
},
Spec: provisioning.RepositorySpec{
GitHub: &provisioning.GitHubRepositoryConfig{
Branch: "main",
},
},
Status: provisioning.RepositoryStatus{
Webhook: &provisioning.WebhookStatus{
EncryptedSecret: []byte("encrypted-secret"),
},
},
},
webhookSecret: "webhook-secret",
setupRequest: func() *http.Request {
payload := `{
"action": "synchronize",
"pull_request": {
"html_url": "https://github.com/grafana/grafana/pull/123",
"number": 123,
"head": {
"ref": "feature-branch",
"sha": "abcdef1234567890"
},
"base": {
"ref": "main"
}
},
"repository": {
"full_name": "grafana/grafana"
}
}`
req, _ := http.NewRequest("POST", "/webhook", strings.NewReader(payload))
req.Header.Set("X-GitHub-Event", "pull_request")
req.Header.Set("Content-Type", "application/json")
// Create a valid signature
mac := hmac.New(sha256.New, []byte("webhook-secret"))
mac.Write([]byte(payload))
signature := hex.EncodeToString(mac.Sum(nil))
req.Header.Set("X-Hub-Signature-256", "sha256="+signature)
return req
},
mockSetup: func(t *testing.T, mockSecrets *secrets.MockService) {
mockSecrets.EXPECT().Decrypt(mock.Anything, []byte("encrypted-secret")).
Return([]byte("webhook-secret"), nil)
},
expected: &provisioning.WebhookResponse{
Code: http.StatusAccepted,
Message: "pull request: synchronize",
Job: &provisioning.JobSpec{
Repository: "test-repo",
Action: provisioning.JobActionPullRequest,
PullRequest: &provisioning.PullRequestJobOptions{
URL: "https://github.com/grafana/grafana/pull/123",
PR: 123,
Ref: "feature-branch",
Hash: "abcdef1234567890",
},
},
},
},
{
name: "pull request event - wrong base branch",
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
GitHub: &provisioning.GitHubRepositoryConfig{
Branch: "main",
},
},
Status: provisioning.RepositoryStatus{
Webhook: &provisioning.WebhookStatus{
EncryptedSecret: []byte("encrypted-secret"),
},
},
},
webhookSecret: "webhook-secret",
setupRequest: func() *http.Request {
payload := `{
"action": "opened",
"pull_request": {
"html_url": "https://github.com/grafana/grafana/pull/123",
"number": 123,
"head": {
"ref": "feature-branch",
"sha": "abcdef1234567890"
},
"base": {
"ref": "develop"
}
},
"repository": {
"full_name": "grafana/grafana"
}
}`
req, _ := http.NewRequest("POST", "/webhook", strings.NewReader(payload))
req.Header.Set("X-GitHub-Event", "pull_request")
req.Header.Set("Content-Type", "application/json")
// Create a valid signature
mac := hmac.New(sha256.New, []byte("webhook-secret"))
mac.Write([]byte(payload))
signature := hex.EncodeToString(mac.Sum(nil))
req.Header.Set("X-Hub-Signature-256", "sha256="+signature)
return req
},
mockSetup: func(t *testing.T, mockSecrets *secrets.MockService) {
mockSecrets.EXPECT().Decrypt(mock.Anything, []byte("encrypted-secret")).
Return([]byte("webhook-secret"), nil)
},
expected: &provisioning.WebhookResponse{
Code: http.StatusOK,
Message: "ignoring pull request event as develop is not the configured branch",
},
},
{
name: "pull request event - ignored action",
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
GitHub: &provisioning.GitHubRepositoryConfig{
Branch: "main",
},
},
Status: provisioning.RepositoryStatus{
Webhook: &provisioning.WebhookStatus{
EncryptedSecret: []byte("encrypted-secret"),
},
},
},
webhookSecret: "webhook-secret",
setupRequest: func() *http.Request {
payload := `{
"action": "closed",
"pull_request": {
"html_url": "https://github.com/grafana/grafana/pull/123",
"number": 123,
"head": {
"ref": "feature-branch",
"sha": "abcdef1234567890"
},
"base": {
"ref": "main"
}
},
"repository": {
"full_name": "grafana/grafana"
}
}`
req, _ := http.NewRequest("POST", "/webhook", strings.NewReader(payload))
req.Header.Set("X-GitHub-Event", "pull_request")
req.Header.Set("Content-Type", "application/json")
// Create a valid signature
mac := hmac.New(sha256.New, []byte("webhook-secret"))
mac.Write([]byte(payload))
signature := hex.EncodeToString(mac.Sum(nil))
req.Header.Set("X-Hub-Signature-256", "sha256="+signature)
return req
},
mockSetup: func(t *testing.T, mockSecrets *secrets.MockService) {
mockSecrets.EXPECT().Decrypt(mock.Anything, []byte("encrypted-secret")).
Return([]byte("webhook-secret"), nil)
},
expected: &provisioning.WebhookResponse{
Code: http.StatusOK,
Message: "ignore pull request event: closed",
},
},
{
name: "pull request event missing repository",
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
GitHub: &provisioning.GitHubRepositoryConfig{
Branch: "main",
},
},
Status: provisioning.RepositoryStatus{
Webhook: &provisioning.WebhookStatus{
EncryptedSecret: []byte("encrypted-secret"),
},
},
},
webhookSecret: "webhook-secret",
setupRequest: func() *http.Request {
payload := `{
"action": "opened",
"pull_request": {
"html_url": "https://github.com/grafana/grafana/pull/123",
"number": 123,
"head": {
"ref": "feature-branch",
"sha": "abcdef1234567890"
},
"base": {
"ref": "main"
}
}
}`
req, _ := http.NewRequest("POST", "/webhook", strings.NewReader(payload))
req.Header.Set("X-GitHub-Event", "pull_request")
req.Header.Set("Content-Type", "application/json")
// Create a valid signature
mac := hmac.New(sha256.New, []byte("webhook-secret"))
mac.Write([]byte(payload))
signature := hex.EncodeToString(mac.Sum(nil))
req.Header.Set("X-Hub-Signature-256", "sha256="+signature)
return req
},
mockSetup: func(t *testing.T, mockSecrets *secrets.MockService) {
mockSecrets.EXPECT().Decrypt(mock.Anything, []byte("encrypted-secret")).
Return([]byte("webhook-secret"), nil)
},
expectedError: fmt.Errorf("missing repository in pull request event"),
},
{
name: "pull request event with missing GitHub config",
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
// GitHub config is intentionally missing
},
Status: provisioning.RepositoryStatus{
Webhook: &provisioning.WebhookStatus{
EncryptedSecret: []byte("encrypted-secret"),
},
},
},
webhookSecret: "webhook-secret",
setupRequest: func() *http.Request {
payload := `{
"action": "opened",
"pull_request": {
"html_url": "https://github.com/grafana/grafana/pull/123",
"number": 123,
"head": {
"ref": "feature-branch",
"sha": "abcdef1234567890"
},
"base": {
"ref": "main"
}
},
"repository": {
"full_name": "grafana/grafana"
}
}`
req, _ := http.NewRequest("POST", "/webhook", strings.NewReader(payload))
req.Header.Set("X-GitHub-Event", "pull_request")
req.Header.Set("Content-Type", "application/json")
// Create a valid signature
mac := hmac.New(sha256.New, []byte("webhook-secret"))
mac.Write([]byte(payload))
signature := hex.EncodeToString(mac.Sum(nil))
req.Header.Set("X-Hub-Signature-256", "sha256="+signature)
return req
},
mockSetup: func(t *testing.T, mockSecrets *secrets.MockService) {
mockSecrets.EXPECT().Decrypt(mock.Anything, []byte("encrypted-secret")).
Return([]byte("webhook-secret"), nil)
},
expectedError: fmt.Errorf("missing GitHub config"),
},
{
name: "pull request event with repository mismatch",
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
GitHub: &provisioning.GitHubRepositoryConfig{
Branch: "main",
},
},
Status: provisioning.RepositoryStatus{
Webhook: &provisioning.WebhookStatus{
EncryptedSecret: []byte("encrypted-secret"),
},
},
},
webhookSecret: "webhook-secret",
setupRequest: func() *http.Request {
payload := `{
"action": "opened",
"pull_request": {
"html_url": "https://github.com/different-owner/different-repo/pull/123",
"number": 123,
"head": {
"ref": "feature-branch",
"sha": "abcdef1234567890"
},
"base": {
"ref": "main"
}
},
"repository": {
"full_name": "different-owner/different-repo"
}
}`
req, _ := http.NewRequest("POST", "/webhook", strings.NewReader(payload))
req.Header.Set("X-GitHub-Event", "pull_request")
req.Header.Set("Content-Type", "application/json")
// Create a valid signature
mac := hmac.New(sha256.New, []byte("webhook-secret"))
mac.Write([]byte(payload))
signature := hex.EncodeToString(mac.Sum(nil))
req.Header.Set("X-Hub-Signature-256", "sha256="+signature)
return req
},
mockSetup: func(t *testing.T, mockSecrets *secrets.MockService) {
mockSecrets.EXPECT().Decrypt(mock.Anything, []byte("encrypted-secret")).
Return([]byte("webhook-secret"), nil)
},
expectedError: fmt.Errorf("repository mismatch"),
},
{
name: "pull request event missing pull request info",
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
GitHub: &provisioning.GitHubRepositoryConfig{
Branch: "main",
},
},
Status: provisioning.RepositoryStatus{
Webhook: &provisioning.WebhookStatus{
EncryptedSecret: []byte("encrypted-secret"),
},
},
},
webhookSecret: "webhook-secret",
setupRequest: func() *http.Request {
payload := `{
"action": "opened",
"repository": {
"full_name": "grafana/grafana"
}
}`
req, _ := http.NewRequest("POST", "/webhook", strings.NewReader(payload))
req.Header.Set("X-GitHub-Event", "pull_request")
req.Header.Set("Content-Type", "application/json")
// Create a valid signature
mac := hmac.New(sha256.New, []byte("webhook-secret"))
mac.Write([]byte(payload))
signature := hex.EncodeToString(mac.Sum(nil))
req.Header.Set("X-Hub-Signature-256", "sha256="+signature)
return req
},
mockSetup: func(t *testing.T, mockSecrets *secrets.MockService) {
mockSecrets.EXPECT().Decrypt(mock.Anything, []byte("encrypted-secret")).
Return([]byte("webhook-secret"), nil)
},
expectedError: fmt.Errorf("expected PR in event"),
},
{
name: "unsupported event type",
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
GitHub: &provisioning.GitHubRepositoryConfig{
Branch: "main",
},
},
Status: provisioning.RepositoryStatus{
Webhook: &provisioning.WebhookStatus{
EncryptedSecret: []byte("encrypted-secret"),
},
},
},
webhookSecret: "webhook-secret",
setupRequest: func() *http.Request {
payload := `{}`
req, _ := http.NewRequest("POST", "/webhook", strings.NewReader(payload))
req.Header.Set("X-GitHub-Event", "team")
req.Header.Set("Content-Type", "application/json")
// Create a valid signature
mac := hmac.New(sha256.New, []byte("webhook-secret"))
mac.Write([]byte(payload))
signature := hex.EncodeToString(mac.Sum(nil))
req.Header.Set("X-Hub-Signature-256", "sha256="+signature)
return req
},
mockSetup: func(t *testing.T, mockSecrets *secrets.MockService) {
mockSecrets.EXPECT().Decrypt(mock.Anything, []byte("encrypted-secret")).
Return([]byte("webhook-secret"), nil)
},
expected: &provisioning.WebhookResponse{
Code: http.StatusNotImplemented,
Message: "unsupported messageType: team",
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Create a mock secrets service
mockSecrets := secrets.NewMockService(t)
// Set up the mock expectations
if tt.mockSetup != nil {
tt.mockSetup(t, mockSecrets)
}
// Create a GitHub repository with the test config
repo := &githubWebhookRepository{
config: tt.config,
owner: "grafana",
repo: "grafana",
secrets: mockSecrets,
}
// Call the Webhook method
response, err := repo.Webhook(context.Background(), tt.setupRequest())
// Check the error
if tt.expectedError != nil {
require.Error(t, err)
var statusErr *apierrors.StatusError
if errors.As(tt.expectedError, &statusErr) {
var actualStatusErr *apierrors.StatusError
require.True(t, errors.As(err, &actualStatusErr), "Expected StatusError but got different error type")
require.Equal(t, statusErr.Status().Message, actualStatusErr.Status().Message)
require.Equal(t, statusErr.Status().Code, actualStatusErr.Status().Code)
} else {
require.Equal(t, tt.expectedError.Error(), err.Error())
}
} else {
require.NoError(t, err)
require.Equal(t, tt.expected.Code, response.Code)
require.Equal(t, tt.expected.Message, response.Message)
if tt.expected.Job != nil {
require.NotNil(t, response.Job)
require.Equal(t, tt.expected.Job.Action, response.Job.Action)
if tt.expected.Job.Pull != nil {
require.Equal(t, tt.expected.Job.Pull.Incremental, response.Job.Pull.Incremental)
}
if tt.expected.Job.PullRequest != nil {
require.Equal(t, tt.expected.Job.PullRequest.URL, response.Job.PullRequest.URL)
require.Equal(t, tt.expected.Job.PullRequest.PR, response.Job.PullRequest.PR)
require.Equal(t, tt.expected.Job.PullRequest.Ref, response.Job.PullRequest.Ref)
require.Equal(t, tt.expected.Job.PullRequest.Hash, response.Job.PullRequest.Hash)
}
} else {
require.Nil(t, response.Job)
}
}
// Verify all mock expectations were met
mockSecrets.AssertExpectations(t)
})
}
}
func TestGitHubRepository_CommentPullRequest(t *testing.T) {
tests := []struct {
name string
setupMock func(m *pgh.MockClient)
prNumber int
comment string
expectedError error
}{
{
name: "successfully comment on pull request",
setupMock: func(m *pgh.MockClient) {
m.On("CreatePullRequestComment", mock.Anything, "grafana", "grafana", 123, "Test comment").
Return(nil)
},
prNumber: 123,
comment: "Test comment",
expectedError: nil,
},
{
name: "error commenting on pull request",
setupMock: func(m *pgh.MockClient) {
m.On("CreatePullRequestComment", mock.Anything, "grafana", "grafana", 456, "Error comment").
Return(fmt.Errorf("failed to create comment"))
},
prNumber: 456,
comment: "Error comment",
expectedError: fmt.Errorf("failed to create comment"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Setup mock GitHub client
mockGH := pgh.NewMockClient(t)
tt.setupMock(mockGH)
// Create repository with mock
repo := &githubWebhookRepository{
gh: mockGH,
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
GitHub: &provisioning.GitHubRepositoryConfig{
Branch: "main",
},
},
},
owner: "grafana",
repo: "grafana",
}
// Call the CommentPullRequest method
err := repo.CommentPullRequest(context.Background(), tt.prNumber, tt.comment)
// Check results
if tt.expectedError != nil {
require.Error(t, err)
require.Equal(t, tt.expectedError.Error(), err.Error())
} else {
require.NoError(t, err)
}
// Verify all mock expectations were met
mockGH.AssertExpectations(t)
})
}
}
func TestGitHubRepository_OnCreate(t *testing.T) {
tests := []struct {
name string
setupMock func(m *pgh.MockClient)
config *provisioning.Repository
webhookURL string
expectedHook *provisioning.WebhookStatus
expectedError error
}{
{
name: "successfully create webhook",
setupMock: func(m *pgh.MockClient) {
m.On("CreateWebhook", mock.Anything, "grafana", "grafana", mock.MatchedBy(func(cfg pgh.WebhookConfig) bool {
return cfg.URL == "https://example.com/webhook" &&
cfg.ContentType == "json" &&
cfg.Active == true
})).Return(pgh.WebhookConfig{
ID: 123,
URL: "https://example.com/webhook",
Secret: "test-secret",
}, nil)
},
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
GitHub: &provisioning.GitHubRepositoryConfig{
Branch: "main",
},
},
},
webhookURL: "https://example.com/webhook",
expectedHook: &provisioning.WebhookStatus{
ID: 123,
URL: "https://example.com/webhook",
Secret: "test-secret",
},
expectedError: nil,
},
{
name: "no webhook URL",
setupMock: func(m *pgh.MockClient) {
// No webhook creation expected
},
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
GitHub: &provisioning.GitHubRepositoryConfig{
Branch: "main",
},
},
},
webhookURL: "",
expectedHook: nil,
expectedError: nil,
},
{
name: "error creating webhook",
setupMock: func(m *pgh.MockClient) {
m.On("CreateWebhook", mock.Anything, "grafana", "grafana", mock.Anything).
Return(pgh.WebhookConfig{}, fmt.Errorf("failed to create webhook"))
},
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
GitHub: &provisioning.GitHubRepositoryConfig{
Branch: "main",
},
},
},
webhookURL: "https://example.com/webhook",
expectedHook: nil,
expectedError: fmt.Errorf("failed to create webhook"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Setup mock GitHub client
mockGH := pgh.NewMockClient(t)
tt.setupMock(mockGH)
// Create repository with mock
repo := &githubWebhookRepository{
gh: mockGH,
config: tt.config,
owner: "grafana",
repo: "grafana",
webhookURL: tt.webhookURL,
}
// Call the OnCreate method
hookOps, err := repo.OnCreate(context.Background())
// Check results
if tt.expectedError != nil {
require.Error(t, err)
require.Equal(t, tt.expectedError.Error(), err.Error())
require.Nil(t, hookOps)
} else {
require.NoError(t, err)
if tt.expectedHook != nil {
require.NotNil(t, hookOps)
require.Len(t, hookOps, 1)
require.Equal(t, "replace", hookOps[0]["op"])
require.Equal(t, "/status/webhook", hookOps[0]["path"])
require.Equal(t, tt.expectedHook.ID, hookOps[0]["value"].(*provisioning.WebhookStatus).ID)
require.Equal(t, tt.expectedHook.URL, hookOps[0]["value"].(*provisioning.WebhookStatus).URL)
require.NotEmpty(t, hookOps[0]["value"].(*provisioning.WebhookStatus).Secret) // Secret is randomly generated, so just check it's not empty
} else {
require.Nil(t, hookOps)
}
}
// Verify all mock expectations were met
mockGH.AssertExpectations(t)
})
}
}
func TestGitHubRepository_OnUpdate(t *testing.T) {
tests := []struct {
name string
setupMock func(m *pgh.MockClient)
config *provisioning.Repository
webhookURL string
expectedHook *provisioning.WebhookStatus
expectedError error
}{
{
name: "successfully update webhook when webhook exists",
setupMock: func(m *pgh.MockClient) {
// Mock getting the existing webhook
m.On("GetWebhook", mock.Anything, "grafana", "grafana", int64(123)).
Return(pgh.WebhookConfig{
ID: 123,
URL: "https://example.com/webhook",
Events: []string{"push"},
}, nil)
// Mock editing the webhook
m.On("EditWebhook", mock.Anything, "grafana", "grafana", mock.MatchedBy(func(hook pgh.WebhookConfig) bool {
return hook.ID == 123 && hook.URL == "https://example.com/webhook-updated" &&
slices.Equal(hook.Events, subscribedEvents)
})).Return(nil)
},
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
GitHub: &provisioning.GitHubRepositoryConfig{
Branch: "main",
},
},
Status: provisioning.RepositoryStatus{
Webhook: &provisioning.WebhookStatus{
ID: 123,
URL: "https://example.com/webhook",
},
},
},
webhookURL: "https://example.com/webhook-updated",
expectedHook: &provisioning.WebhookStatus{
ID: 123,
URL: "https://example.com/webhook-updated",
SubscribedEvents: subscribedEvents,
},
expectedError: nil,
},
{
name: "create webhook when it doesn't exist",
setupMock: func(m *pgh.MockClient) {
// Mock webhook not found
m.On("GetWebhook", mock.Anything, "grafana", "grafana", int64(123)).
Return(pgh.WebhookConfig{}, pgh.ErrResourceNotFound)
// Mock creating a new webhook
m.On("CreateWebhook", mock.Anything, "grafana", "grafana", mock.MatchedBy(func(hook pgh.WebhookConfig) bool {
return hook.URL == "https://example.com/webhook" &&
hook.ContentType == "json" &&
slices.Equal(hook.Events, subscribedEvents) &&
hook.Active == true
})).Return(pgh.WebhookConfig{
ID: 456,
URL: "https://example.com/webhook",
Events: subscribedEvents,
}, nil)
},
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
GitHub: &provisioning.GitHubRepositoryConfig{
Branch: "main",
},
},
Status: provisioning.RepositoryStatus{
Webhook: &provisioning.WebhookStatus{
ID: 123,
URL: "https://example.com/old-webhook",
},
},
},
webhookURL: "https://example.com/webhook",
expectedHook: &provisioning.WebhookStatus{
ID: 456,
URL: "https://example.com/webhook",
SubscribedEvents: subscribedEvents,
},
expectedError: nil,
},
{
name: "no webhook URL provided",
setupMock: func(m *pgh.MockClient) {
// No mocks needed
},
config: &provisioning.Repository{},
webhookURL: "",
expectedHook: nil,
expectedError: nil,
},
{
name: "error getting webhook",
setupMock: func(m *pgh.MockClient) {
m.On("GetWebhook", mock.Anything, "grafana", "grafana", int64(123)).
Return(pgh.WebhookConfig{}, fmt.Errorf("failed to get webhook"))
},
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
GitHub: &provisioning.GitHubRepositoryConfig{
Branch: "main",
},
},
Status: provisioning.RepositoryStatus{
Webhook: &provisioning.WebhookStatus{
ID: 123,
URL: "https://example.com/webhook",
},
},
},
webhookURL: "https://example.com/webhook",
expectedHook: nil,
expectedError: fmt.Errorf("get webhook: failed to get webhook"),
},
{
name: "error editing webhook",
setupMock: func(m *pgh.MockClient) {
// Mock getting the existing webhook
m.On("GetWebhook", mock.Anything, "grafana", "grafana", int64(123)).
Return(pgh.WebhookConfig{
ID: 123,
URL: "https://example.com/webhook",
Events: []string{"push"},
}, nil)
// Mock editing the webhook with error
m.On("EditWebhook", mock.Anything, "grafana", "grafana", mock.Anything).
Return(fmt.Errorf("failed to edit webhook"))
},
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
GitHub: &provisioning.GitHubRepositoryConfig{
Branch: "main",
},
},
Status: provisioning.RepositoryStatus{
Webhook: &provisioning.WebhookStatus{
ID: 123,
URL: "https://example.com/webhook",
},
},
},
webhookURL: "https://example.com/webhook-updated",
expectedHook: nil,
expectedError: fmt.Errorf("edit webhook: failed to edit webhook"),
},
{
name: "create webhook when webhook status is nil",
setupMock: func(m *pgh.MockClient) {
// Mock creating a new webhook
m.On("CreateWebhook", mock.Anything, "grafana", "grafana", mock.Anything).
Return(pgh.WebhookConfig{
ID: 456,
URL: "https://example.com/webhook",
Events: subscribedEvents,
Active: true,
ContentType: "json",
}, nil)
},
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
GitHub: &provisioning.GitHubRepositoryConfig{
Branch: "main",
},
},
Status: provisioning.RepositoryStatus{
Webhook: nil, // Webhook status is nil
},
},
webhookURL: "https://example.com/webhook",
expectedHook: &provisioning.WebhookStatus{
ID: 456,
URL: "https://example.com/webhook",
SubscribedEvents: subscribedEvents,
},
expectedError: nil,
},
{
name: "create webhook when webhook ID is zero",
setupMock: func(m *pgh.MockClient) {
// Mock creating a new webhook
m.On("CreateWebhook", mock.Anything, "grafana", "grafana", mock.Anything).
Return(pgh.WebhookConfig{
ID: 789,
URL: "https://example.com/webhook",
Events: subscribedEvents,
Active: true,
ContentType: "json",
}, nil)
},
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
GitHub: &provisioning.GitHubRepositoryConfig{
Branch: "main",
},
},
Status: provisioning.RepositoryStatus{
Webhook: &provisioning.WebhookStatus{
ID: 0, // Webhook ID is zero
URL: "https://example.com/webhook",
},
},
},
webhookURL: "https://example.com/webhook",
expectedHook: &provisioning.WebhookStatus{
ID: 789,
URL: "https://example.com/webhook",
SubscribedEvents: subscribedEvents,
},
expectedError: nil,
},
{
name: "error when creating webhook fails",
setupMock: func(m *pgh.MockClient) {
// Mock webhook creation failure
m.On("CreateWebhook", mock.Anything, "grafana", "grafana", mock.Anything).
Return(pgh.WebhookConfig{}, fmt.Errorf("failed to create webhook"))
},
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
GitHub: &provisioning.GitHubRepositoryConfig{
Branch: "main",
},
},
Status: provisioning.RepositoryStatus{
Webhook: nil, // Webhook status is nil
},
},
webhookURL: "https://example.com/webhook",
expectedHook: nil,
expectedError: fmt.Errorf("failed to create webhook"),
},
{
name: "creates webhook when ErrResourceNotFound",
setupMock: func(m *pgh.MockClient) {
// Mock webhook not found
m.On("GetWebhook", mock.Anything, "grafana", "grafana", int64(123)).
Return(pgh.WebhookConfig{}, pgh.ErrResourceNotFound)
// Mock creating a new webhook
m.On("CreateWebhook", mock.Anything, "grafana", "grafana", mock.MatchedBy(func(hook pgh.WebhookConfig) bool {
return hook.URL == "https://example.com/webhook" &&
hook.ContentType == "json" &&
slices.Equal(hook.Events, subscribedEvents) &&
hook.Active == true
})).Return(pgh.WebhookConfig{
ID: 456,
URL: "https://example.com/webhook",
Events: subscribedEvents,
}, nil)
},
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
GitHub: &provisioning.GitHubRepositoryConfig{
Branch: "main",
},
},
Status: provisioning.RepositoryStatus{
Webhook: &provisioning.WebhookStatus{
ID: 123,
URL: "https://example.com/old-webhook",
},
},
},
webhookURL: "https://example.com/webhook",
expectedHook: &provisioning.WebhookStatus{
ID: 456,
URL: "https://example.com/webhook",
SubscribedEvents: subscribedEvents,
},
expectedError: nil,
},
{
name: "error on create when not found",
setupMock: func(m *pgh.MockClient) {
// Mock webhook not found
m.On("GetWebhook", mock.Anything, "grafana", "grafana", int64(123)).
Return(pgh.WebhookConfig{}, pgh.ErrResourceNotFound)
// Mock error when creating a new webhook
m.On("CreateWebhook", mock.Anything, "grafana", "grafana", mock.MatchedBy(func(hook pgh.WebhookConfig) bool {
return hook.URL == "https://example.com/webhook" &&
hook.ContentType == "json" &&
slices.Equal(hook.Events, subscribedEvents) &&
hook.Active == true
})).Return(pgh.WebhookConfig{}, fmt.Errorf("failed to create webhook"))
},
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
GitHub: &provisioning.GitHubRepositoryConfig{
Branch: "main",
},
},
Status: provisioning.RepositoryStatus{
Webhook: &provisioning.WebhookStatus{
ID: 123,
URL: "https://example.com/old-webhook",
},
},
},
webhookURL: "https://example.com/webhook",
expectedHook: nil,
expectedError: fmt.Errorf("failed to create webhook"),
},
{
name: "no update needed when URL and events match",
setupMock: func(m *pgh.MockClient) {
// Mock getting the existing webhook with matching URL and events
m.On("GetWebhook", mock.Anything, "grafana", "grafana", int64(123)).
Return(pgh.WebhookConfig{
ID: 123,
URL: "https://example.com/webhook",
Events: subscribedEvents,
}, nil)
// No EditWebhook call expected since no changes needed
},
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
GitHub: &provisioning.GitHubRepositoryConfig{
Branch: "main",
},
},
Status: provisioning.RepositoryStatus{
Webhook: &provisioning.WebhookStatus{
ID: 123,
URL: "https://example.com/webhook",
Secret: "secret",
},
},
},
webhookURL: "https://example.com/webhook",
expectedHook: &provisioning.WebhookStatus{
ID: 123,
URL: "https://example.com/webhook",
SubscribedEvents: subscribedEvents,
Secret: "secret",
},
expectedError: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Setup mock GitHub client
mockGH := pgh.NewMockClient(t)
tt.setupMock(mockGH)
// Create repository with mock
repo := &githubWebhookRepository{
gh: mockGH,
config: tt.config,
owner: "grafana",
repo: "grafana",
webhookURL: tt.webhookURL,
}
// Call the OnUpdate method
hookOps, err := repo.OnUpdate(context.Background())
// Check results
if tt.expectedError != nil {
require.Error(t, err)
require.Equal(t, tt.expectedError.Error(), err.Error())
require.Nil(t, hookOps)
} else {
require.NoError(t, err)
if tt.expectedHook != nil {
require.NotNil(t, hookOps)
require.Len(t, hookOps, 1)
require.Equal(t, "replace", hookOps[0]["op"])
require.Equal(t, "/status/webhook", hookOps[0]["path"])
require.Equal(t, tt.expectedHook.ID, hookOps[0]["value"].(*provisioning.WebhookStatus).ID)
require.Equal(t, tt.expectedHook.URL, hookOps[0]["value"].(*provisioning.WebhookStatus).URL)
if tt.expectedHook.Secret != "" {
require.Equal(t, tt.expectedHook.Secret, hookOps[0]["value"].(*provisioning.WebhookStatus).Secret)
} else {
require.NotEmpty(t, hookOps[0]["value"].(*provisioning.WebhookStatus).Secret) // Secret is randomly generated, so just check it's not empty
}
require.ElementsMatch(t, tt.expectedHook.SubscribedEvents, hookOps[0]["value"].(*provisioning.WebhookStatus).SubscribedEvents)
} else {
require.Nil(t, hookOps)
}
}
// Verify all mock expectations were met
mockGH.AssertExpectations(t)
})
}
}
func TestGitHubRepository_OnDelete(t *testing.T) {
tests := []struct {
name string
setupMock func(m *pgh.MockClient)
config *provisioning.Repository
webhookURL string
expectedError error
}{
{
name: "successfully delete webhook",
setupMock: func(m *pgh.MockClient) {
// Mock deleting the webhook
m.On("DeleteWebhook", mock.Anything, "grafana", "grafana", int64(123)).
Return(nil)
},
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
GitHub: &provisioning.GitHubRepositoryConfig{
Branch: "main",
},
},
Status: provisioning.RepositoryStatus{
Webhook: &provisioning.WebhookStatus{
ID: 123,
URL: "https://example.com/webhook",
},
},
},
webhookURL: "https://example.com/webhook",
expectedError: nil,
},
{
name: "no webhook URL provided",
setupMock: func(m *pgh.MockClient) {
// No mocks needed
},
config: &provisioning.Repository{},
webhookURL: "",
expectedError: nil,
},
{
name: "webhook not found in status",
setupMock: func(m *pgh.MockClient) {
// No mocks needed
},
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
GitHub: &provisioning.GitHubRepositoryConfig{
Branch: "main",
},
},
Status: provisioning.RepositoryStatus{
Webhook: nil, // Webhook status is nil
},
},
webhookURL: "https://example.com/webhook",
expectedError: fmt.Errorf("webhook not found"),
},
{
name: "error deleting webhook",
setupMock: func(m *pgh.MockClient) {
// Mock webhook deletion failure
m.On("DeleteWebhook", mock.Anything, "grafana", "grafana", int64(123)).
Return(fmt.Errorf("failed to delete webhook"))
},
config: &provisioning.Repository{
Spec: provisioning.RepositorySpec{
GitHub: &provisioning.GitHubRepositoryConfig{
Branch: "main",
},
},
Status: provisioning.RepositoryStatus{
Webhook: &provisioning.WebhookStatus{
ID: 123,
URL: "https://example.com/webhook",
},
},
},
webhookURL: "https://example.com/webhook",
expectedError: fmt.Errorf("delete webhook: failed to delete webhook"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Setup mock GitHub client
mockGH := pgh.NewMockClient(t)
tt.setupMock(mockGH)
// Create repository with mock
repo := &githubWebhookRepository{
gh: mockGH,
config: tt.config,
owner: "grafana",
repo: "grafana",
webhookURL: tt.webhookURL,
}
// Call the OnDelete method
err := repo.OnDelete(context.Background())
// Check results
if tt.expectedError != nil {
require.Error(t, err)
require.Equal(t, tt.expectedError.Error(), err.Error())
} else {
require.NoError(t, err)
}
// Verify all mock expectations were met
mockGH.AssertExpectations(t)
})
}
}