All checks were successful
Check / check (pull_request) Successful in 1m50s
Add auto-detection of webhook source (Gitea, GitHub, GitLab) by examining HTTP headers (X-Gitea-Event, X-GitHub-Event, X-Gitlab-Event). Parse push webhook payloads from all three platforms into a normalized PushEvent type for unified processing. Each platform's payload format is handled by dedicated parser functions with correct field mapping and commit URL extraction. The webhook handler now detects the source automatically — existing Gitea webhooks continue to work unchanged, while GitHub and GitLab webhooks are parsed with their respective payload formats. Includes comprehensive tests for source detection, event type extraction, payload parsing for all three platforms, commit URL fallback logic, and integration tests via HandleWebhook.
980 lines
27 KiB
Go
980 lines
27 KiB
Go
package webhook_test
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"go.uber.org/fx"
|
|
|
|
"sneak.berlin/go/upaas/internal/config"
|
|
"sneak.berlin/go/upaas/internal/database"
|
|
"sneak.berlin/go/upaas/internal/docker"
|
|
"sneak.berlin/go/upaas/internal/globals"
|
|
"sneak.berlin/go/upaas/internal/logger"
|
|
"sneak.berlin/go/upaas/internal/models"
|
|
"sneak.berlin/go/upaas/internal/service/deploy"
|
|
"sneak.berlin/go/upaas/internal/service/notify"
|
|
"sneak.berlin/go/upaas/internal/service/webhook"
|
|
)
|
|
|
|
type testDeps struct {
|
|
logger *logger.Logger
|
|
config *config.Config
|
|
db *database.Database
|
|
tmpDir string
|
|
}
|
|
|
|
func setupTestDeps(t *testing.T) *testDeps {
|
|
t.Helper()
|
|
|
|
tmpDir := t.TempDir()
|
|
|
|
globals.SetAppname("upaas-test")
|
|
globals.SetVersion("test")
|
|
|
|
globalsInst, err := globals.New(fx.Lifecycle(nil))
|
|
require.NoError(t, err)
|
|
|
|
loggerInst, err := logger.New(fx.Lifecycle(nil), logger.Params{Globals: globalsInst})
|
|
require.NoError(t, err)
|
|
|
|
cfg := &config.Config{Port: 8080, DataDir: tmpDir, SessionSecret: "test-secret-key-at-least-32-chars"}
|
|
|
|
dbInst, err := database.New(fx.Lifecycle(nil), database.Params{Logger: loggerInst, Config: cfg})
|
|
require.NoError(t, err)
|
|
|
|
return &testDeps{logger: loggerInst, config: cfg, db: dbInst, tmpDir: tmpDir}
|
|
}
|
|
|
|
func setupTestService(t *testing.T) (*webhook.Service, *database.Database, func()) {
|
|
t.Helper()
|
|
|
|
deps := setupTestDeps(t)
|
|
|
|
dockerClient, err := docker.New(fx.Lifecycle(nil), docker.Params{Logger: deps.logger, Config: deps.config})
|
|
require.NoError(t, err)
|
|
|
|
notifySvc, err := notify.New(fx.Lifecycle(nil), notify.ServiceParams{Logger: deps.logger})
|
|
require.NoError(t, err)
|
|
|
|
deploySvc, err := deploy.New(fx.Lifecycle(nil), deploy.ServiceParams{
|
|
Logger: deps.logger, Config: deps.config, Database: deps.db, Docker: dockerClient, Notify: notifySvc,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
svc, err := webhook.New(fx.Lifecycle(nil), webhook.ServiceParams{
|
|
Logger: deps.logger, Database: deps.db, Deploy: deploySvc,
|
|
})
|
|
require.NoError(t, err)
|
|
|
|
// t.TempDir() automatically cleans up after test
|
|
return svc, deps.db, func() {}
|
|
}
|
|
|
|
func createTestApp(
|
|
t *testing.T,
|
|
dbInst *database.Database,
|
|
branch string,
|
|
) *models.App {
|
|
t.Helper()
|
|
|
|
app := models.NewApp(dbInst)
|
|
app.ID = "test-app-id"
|
|
app.Name = "test-app"
|
|
app.RepoURL = "git@gitea.example.com:user/repo.git"
|
|
app.Branch = branch
|
|
app.DockerfilePath = "Dockerfile"
|
|
app.WebhookSecret = "webhook-secret-123"
|
|
app.WebhookSecretHash = database.HashWebhookSecret(app.WebhookSecret)
|
|
app.SSHPrivateKey = "private-key"
|
|
app.SSHPublicKey = "public-key"
|
|
app.Status = models.AppStatusPending
|
|
|
|
err := app.Save(context.Background())
|
|
require.NoError(t, err)
|
|
|
|
return app
|
|
}
|
|
|
|
// TestDetectWebhookSource tests auto-detection of webhook source from HTTP headers.
|
|
//
|
|
//nolint:funlen // table-driven test with comprehensive test cases
|
|
func TestDetectWebhookSource(testingT *testing.T) {
|
|
testingT.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
headers map[string]string
|
|
expected webhook.Source
|
|
}{
|
|
{
|
|
name: "detects Gitea from X-Gitea-Event header",
|
|
headers: map[string]string{"X-Gitea-Event": "push"},
|
|
expected: webhook.SourceGitea,
|
|
},
|
|
{
|
|
name: "detects GitHub from X-GitHub-Event header",
|
|
headers: map[string]string{"X-GitHub-Event": "push"},
|
|
expected: webhook.SourceGitHub,
|
|
},
|
|
{
|
|
name: "detects GitLab from X-Gitlab-Event header",
|
|
headers: map[string]string{"X-Gitlab-Event": "Push Hook"},
|
|
expected: webhook.SourceGitLab,
|
|
},
|
|
{
|
|
name: "returns unknown when no recognized header",
|
|
headers: map[string]string{"Content-Type": "application/json"},
|
|
expected: webhook.SourceUnknown,
|
|
},
|
|
{
|
|
name: "returns unknown for empty headers",
|
|
headers: map[string]string{},
|
|
expected: webhook.SourceUnknown,
|
|
},
|
|
{
|
|
name: "Gitea takes precedence over GitHub",
|
|
headers: map[string]string{
|
|
"X-Gitea-Event": "push",
|
|
"X-GitHub-Event": "push",
|
|
},
|
|
expected: webhook.SourceGitea,
|
|
},
|
|
{
|
|
name: "GitHub takes precedence over GitLab",
|
|
headers: map[string]string{
|
|
"X-GitHub-Event": "push",
|
|
"X-Gitlab-Event": "Push Hook",
|
|
},
|
|
expected: webhook.SourceGitHub,
|
|
},
|
|
}
|
|
|
|
for _, testCase := range tests {
|
|
testingT.Run(testCase.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
headers := http.Header{}
|
|
for key, value := range testCase.headers {
|
|
headers.Set(key, value)
|
|
}
|
|
|
|
result := webhook.DetectWebhookSource(headers)
|
|
assert.Equal(t, testCase.expected, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestDetectEventType tests event type extraction from HTTP headers.
|
|
func TestDetectEventType(testingT *testing.T) {
|
|
testingT.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
headers map[string]string
|
|
source webhook.Source
|
|
expected string
|
|
}{
|
|
{
|
|
name: "extracts Gitea event type",
|
|
headers: map[string]string{"X-Gitea-Event": "push"},
|
|
source: webhook.SourceGitea,
|
|
expected: "push",
|
|
},
|
|
{
|
|
name: "extracts GitHub event type",
|
|
headers: map[string]string{"X-GitHub-Event": "push"},
|
|
source: webhook.SourceGitHub,
|
|
expected: "push",
|
|
},
|
|
{
|
|
name: "extracts GitLab event type",
|
|
headers: map[string]string{"X-Gitlab-Event": "Push Hook"},
|
|
source: webhook.SourceGitLab,
|
|
expected: "Push Hook",
|
|
},
|
|
{
|
|
name: "returns push for unknown source",
|
|
headers: map[string]string{},
|
|
source: webhook.SourceUnknown,
|
|
expected: "push",
|
|
},
|
|
{
|
|
name: "returns push when header missing for source",
|
|
headers: map[string]string{},
|
|
source: webhook.SourceGitea,
|
|
expected: "push",
|
|
},
|
|
}
|
|
|
|
for _, testCase := range tests {
|
|
testingT.Run(testCase.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
headers := http.Header{}
|
|
for key, value := range testCase.headers {
|
|
headers.Set(key, value)
|
|
}
|
|
|
|
result := webhook.DetectEventType(headers, testCase.source)
|
|
assert.Equal(t, testCase.expected, result)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestWebhookSourceString tests the String method on WebhookSource.
|
|
func TestWebhookSourceString(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
assert.Equal(t, "gitea", webhook.SourceGitea.String())
|
|
assert.Equal(t, "github", webhook.SourceGitHub.String())
|
|
assert.Equal(t, "gitlab", webhook.SourceGitLab.String())
|
|
assert.Equal(t, "unknown", webhook.SourceUnknown.String())
|
|
}
|
|
|
|
// TestUnparsedURLString tests the String method on UnparsedURL.
|
|
func TestUnparsedURLString(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
u := webhook.UnparsedURL("https://example.com/test")
|
|
assert.Equal(t, "https://example.com/test", u.String())
|
|
|
|
empty := webhook.UnparsedURL("")
|
|
assert.Empty(t, empty.String())
|
|
}
|
|
|
|
// TestParsePushPayloadGitea tests parsing of Gitea push payloads.
|
|
func TestParsePushPayloadGitea(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
payload := []byte(`{
|
|
"ref": "refs/heads/main",
|
|
"before": "0000000000000000000000000000000000000000",
|
|
"after": "abc123def456789",
|
|
"compare_url": "https://gitea.example.com/myorg/myrepo/compare/000...abc",
|
|
"repository": {
|
|
"full_name": "myorg/myrepo",
|
|
"clone_url": "https://gitea.example.com/myorg/myrepo.git",
|
|
"ssh_url": "git@gitea.example.com:myorg/myrepo.git",
|
|
"html_url": "https://gitea.example.com/myorg/myrepo"
|
|
},
|
|
"pusher": {"username": "developer", "email": "dev@example.com"},
|
|
"commits": [
|
|
{
|
|
"id": "abc123def456789",
|
|
"url": "https://gitea.example.com/myorg/myrepo/commit/abc123def456789",
|
|
"message": "Fix bug",
|
|
"author": {"name": "Developer", "email": "dev@example.com"}
|
|
}
|
|
]
|
|
}`)
|
|
|
|
event, err := webhook.ParsePushPayload(webhook.SourceGitea, payload)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, webhook.SourceGitea, event.Source)
|
|
assert.Equal(t, "refs/heads/main", event.Ref)
|
|
assert.Equal(t, "main", event.Branch)
|
|
assert.Equal(t, "abc123def456789", event.After)
|
|
assert.Equal(t, "myorg/myrepo", event.RepoName)
|
|
assert.Equal(t, webhook.UnparsedURL("https://gitea.example.com/myorg/myrepo.git"), event.CloneURL)
|
|
assert.Equal(t, webhook.UnparsedURL("https://gitea.example.com/myorg/myrepo"), event.HTMLURL)
|
|
assert.Equal(t,
|
|
webhook.UnparsedURL("https://gitea.example.com/myorg/myrepo/commit/abc123def456789"),
|
|
event.CommitURL,
|
|
)
|
|
assert.Equal(t, "developer", event.Pusher)
|
|
}
|
|
|
|
// TestParsePushPayloadGitHub tests parsing of GitHub push payloads.
|
|
func TestParsePushPayloadGitHub(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
payload := []byte(`{
|
|
"ref": "refs/heads/main",
|
|
"before": "0000000000000000000000000000000000000000",
|
|
"after": "abc123def456789",
|
|
"compare": "https://github.com/myorg/myrepo/compare/000...abc",
|
|
"repository": {
|
|
"full_name": "myorg/myrepo",
|
|
"clone_url": "https://github.com/myorg/myrepo.git",
|
|
"ssh_url": "git@github.com:myorg/myrepo.git",
|
|
"html_url": "https://github.com/myorg/myrepo"
|
|
},
|
|
"pusher": {"name": "developer", "email": "dev@example.com"},
|
|
"head_commit": {
|
|
"id": "abc123def456789",
|
|
"url": "https://github.com/myorg/myrepo/commit/abc123def456789",
|
|
"message": "Fix bug"
|
|
},
|
|
"commits": [
|
|
{
|
|
"id": "abc123def456789",
|
|
"url": "https://github.com/myorg/myrepo/commit/abc123def456789",
|
|
"message": "Fix bug",
|
|
"author": {"name": "Developer", "email": "dev@example.com"}
|
|
}
|
|
]
|
|
}`)
|
|
|
|
event, err := webhook.ParsePushPayload(webhook.SourceGitHub, payload)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, webhook.SourceGitHub, event.Source)
|
|
assert.Equal(t, "refs/heads/main", event.Ref)
|
|
assert.Equal(t, "main", event.Branch)
|
|
assert.Equal(t, "abc123def456789", event.After)
|
|
assert.Equal(t, "myorg/myrepo", event.RepoName)
|
|
assert.Equal(t, webhook.UnparsedURL("https://github.com/myorg/myrepo.git"), event.CloneURL)
|
|
assert.Equal(t, webhook.UnparsedURL("https://github.com/myorg/myrepo"), event.HTMLURL)
|
|
assert.Equal(t,
|
|
webhook.UnparsedURL("https://github.com/myorg/myrepo/commit/abc123def456789"),
|
|
event.CommitURL,
|
|
)
|
|
assert.Equal(t, "developer", event.Pusher)
|
|
}
|
|
|
|
// TestParsePushPayloadGitLab tests parsing of GitLab push payloads.
|
|
func TestParsePushPayloadGitLab(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
payload := []byte(`{
|
|
"ref": "refs/heads/develop",
|
|
"before": "0000000000000000000000000000000000000000",
|
|
"after": "abc123def456789",
|
|
"user_name": "developer",
|
|
"user_email": "dev@example.com",
|
|
"project": {
|
|
"path_with_namespace": "mygroup/myproject",
|
|
"git_http_url": "https://gitlab.com/mygroup/myproject.git",
|
|
"git_ssh_url": "git@gitlab.com:mygroup/myproject.git",
|
|
"web_url": "https://gitlab.com/mygroup/myproject"
|
|
},
|
|
"commits": [
|
|
{
|
|
"id": "abc123def456789",
|
|
"url": "https://gitlab.com/mygroup/myproject/-/commit/abc123def456789",
|
|
"message": "Fix bug",
|
|
"author": {"name": "Developer", "email": "dev@example.com"}
|
|
}
|
|
]
|
|
}`)
|
|
|
|
event, err := webhook.ParsePushPayload(webhook.SourceGitLab, payload)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, webhook.SourceGitLab, event.Source)
|
|
assert.Equal(t, "refs/heads/develop", event.Ref)
|
|
assert.Equal(t, "develop", event.Branch)
|
|
assert.Equal(t, "abc123def456789", event.After)
|
|
assert.Equal(t, "mygroup/myproject", event.RepoName)
|
|
assert.Equal(t, webhook.UnparsedURL("https://gitlab.com/mygroup/myproject.git"), event.CloneURL)
|
|
assert.Equal(t, webhook.UnparsedURL("https://gitlab.com/mygroup/myproject"), event.HTMLURL)
|
|
assert.Equal(t,
|
|
webhook.UnparsedURL("https://gitlab.com/mygroup/myproject/-/commit/abc123def456789"),
|
|
event.CommitURL,
|
|
)
|
|
assert.Equal(t, "developer", event.Pusher)
|
|
}
|
|
|
|
// TestParsePushPayloadUnknownFallsBackToGitea tests that unknown source uses Gitea parser.
|
|
func TestParsePushPayloadUnknownFallsBackToGitea(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
payload := []byte(`{
|
|
"ref": "refs/heads/main",
|
|
"after": "abc123",
|
|
"repository": {"full_name": "user/repo"},
|
|
"pusher": {"username": "user"}
|
|
}`)
|
|
|
|
event, err := webhook.ParsePushPayload(webhook.SourceUnknown, payload)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, webhook.SourceGitea, event.Source)
|
|
assert.Equal(t, "main", event.Branch)
|
|
assert.Equal(t, "abc123", event.After)
|
|
}
|
|
|
|
// TestParsePushPayloadInvalidJSON tests that invalid JSON returns an error.
|
|
func TestParsePushPayloadInvalidJSON(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
sources := []webhook.Source{
|
|
webhook.SourceGitea,
|
|
webhook.SourceGitHub,
|
|
webhook.SourceGitLab,
|
|
}
|
|
|
|
for _, source := range sources {
|
|
t.Run(source.String(), func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
_, err := webhook.ParsePushPayload(source, []byte(`{invalid json}`))
|
|
require.Error(t, err)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestParsePushPayloadEmptyPayload tests parsing of empty JSON objects.
|
|
func TestParsePushPayloadEmptyPayload(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
sources := []webhook.Source{
|
|
webhook.SourceGitea,
|
|
webhook.SourceGitHub,
|
|
webhook.SourceGitLab,
|
|
}
|
|
|
|
for _, source := range sources {
|
|
t.Run(source.String(), func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
event, err := webhook.ParsePushPayload(source, []byte(`{}`))
|
|
require.NoError(t, err)
|
|
|
|
assert.Empty(t, event.Branch)
|
|
assert.Empty(t, event.After)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestGitHubCommitURLFallback tests commit URL extraction fallback paths for GitHub.
|
|
func TestGitHubCommitURLFallback(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("uses head_commit URL when available", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
payload := []byte(`{
|
|
"ref": "refs/heads/main",
|
|
"after": "abc123",
|
|
"head_commit": {"id": "abc123", "url": "https://github.com/u/r/commit/abc123"},
|
|
"repository": {"html_url": "https://github.com/u/r"}
|
|
}`)
|
|
|
|
event, err := webhook.ParsePushPayload(webhook.SourceGitHub, payload)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, webhook.UnparsedURL("https://github.com/u/r/commit/abc123"), event.CommitURL)
|
|
})
|
|
|
|
t.Run("falls back to commits list", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
payload := []byte(`{
|
|
"ref": "refs/heads/main",
|
|
"after": "abc123",
|
|
"commits": [{"id": "abc123", "url": "https://github.com/u/r/commit/abc123"}],
|
|
"repository": {"html_url": "https://github.com/u/r"}
|
|
}`)
|
|
|
|
event, err := webhook.ParsePushPayload(webhook.SourceGitHub, payload)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, webhook.UnparsedURL("https://github.com/u/r/commit/abc123"), event.CommitURL)
|
|
})
|
|
|
|
t.Run("constructs URL from repo HTML URL", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
payload := []byte(`{
|
|
"ref": "refs/heads/main",
|
|
"after": "abc123",
|
|
"repository": {"html_url": "https://github.com/u/r"}
|
|
}`)
|
|
|
|
event, err := webhook.ParsePushPayload(webhook.SourceGitHub, payload)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, webhook.UnparsedURL("https://github.com/u/r/commit/abc123"), event.CommitURL)
|
|
})
|
|
}
|
|
|
|
// TestGitLabCommitURLFallback tests commit URL extraction fallback paths for GitLab.
|
|
func TestGitLabCommitURLFallback(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("uses commit URL from list", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
payload := []byte(`{
|
|
"ref": "refs/heads/main",
|
|
"after": "abc123",
|
|
"project": {"web_url": "https://gitlab.com/g/p"},
|
|
"commits": [{"id": "abc123", "url": "https://gitlab.com/g/p/-/commit/abc123"}]
|
|
}`)
|
|
|
|
event, err := webhook.ParsePushPayload(webhook.SourceGitLab, payload)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, webhook.UnparsedURL("https://gitlab.com/g/p/-/commit/abc123"), event.CommitURL)
|
|
})
|
|
|
|
t.Run("constructs URL from project web URL", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
payload := []byte(`{
|
|
"ref": "refs/heads/main",
|
|
"after": "abc123",
|
|
"project": {"web_url": "https://gitlab.com/g/p"}
|
|
}`)
|
|
|
|
event, err := webhook.ParsePushPayload(webhook.SourceGitLab, payload)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, webhook.UnparsedURL("https://gitlab.com/g/p/-/commit/abc123"), event.CommitURL)
|
|
})
|
|
}
|
|
|
|
// TestGiteaPushPayloadParsing tests direct deserialization of the Gitea payload struct.
|
|
func TestGiteaPushPayloadParsing(testingT *testing.T) {
|
|
testingT.Parallel()
|
|
|
|
testingT.Run("parses full payload", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
payload := []byte(`{
|
|
"ref": "refs/heads/main",
|
|
"before": "0000000000000000000000000000000000000000",
|
|
"after": "abc123def456789",
|
|
"repository": {
|
|
"full_name": "myorg/myrepo",
|
|
"clone_url": "https://gitea.example.com/myorg/myrepo.git",
|
|
"ssh_url": "git@gitea.example.com:myorg/myrepo.git"
|
|
},
|
|
"pusher": {
|
|
"username": "developer",
|
|
"email": "dev@example.com"
|
|
},
|
|
"commits": [
|
|
{
|
|
"id": "abc123def456789",
|
|
"message": "Fix bug in feature",
|
|
"author": {
|
|
"name": "Developer",
|
|
"email": "dev@example.com"
|
|
}
|
|
},
|
|
{
|
|
"id": "def456789abc123",
|
|
"message": "Add tests",
|
|
"author": {
|
|
"name": "Developer",
|
|
"email": "dev@example.com"
|
|
}
|
|
}
|
|
]
|
|
}`)
|
|
|
|
var pushPayload webhook.GiteaPushPayload
|
|
|
|
err := json.Unmarshal(payload, &pushPayload)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, "refs/heads/main", pushPayload.Ref)
|
|
assert.Equal(t, "abc123def456789", pushPayload.After)
|
|
assert.Equal(t, "myorg/myrepo", pushPayload.Repository.FullName)
|
|
assert.Equal(
|
|
t,
|
|
"git@gitea.example.com:myorg/myrepo.git",
|
|
pushPayload.Repository.SSHURL,
|
|
)
|
|
assert.Equal(t, "developer", pushPayload.Pusher.Username)
|
|
assert.Len(t, pushPayload.Commits, 2)
|
|
assert.Equal(t, "Fix bug in feature", pushPayload.Commits[0].Message)
|
|
})
|
|
}
|
|
|
|
// TestGitHubPushPayloadParsing tests direct deserialization of the GitHub payload struct.
|
|
func TestGitHubPushPayloadParsing(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
payload := []byte(`{
|
|
"ref": "refs/heads/main",
|
|
"before": "0000000000",
|
|
"after": "abc123",
|
|
"compare": "https://github.com/o/r/compare/000...abc",
|
|
"repository": {
|
|
"full_name": "o/r",
|
|
"clone_url": "https://github.com/o/r.git",
|
|
"ssh_url": "git@github.com:o/r.git",
|
|
"html_url": "https://github.com/o/r"
|
|
},
|
|
"pusher": {"name": "octocat", "email": "octocat@github.com"},
|
|
"head_commit": {
|
|
"id": "abc123",
|
|
"url": "https://github.com/o/r/commit/abc123",
|
|
"message": "Update README"
|
|
},
|
|
"commits": [
|
|
{
|
|
"id": "abc123",
|
|
"url": "https://github.com/o/r/commit/abc123",
|
|
"message": "Update README",
|
|
"author": {"name": "Octocat", "email": "octocat@github.com"}
|
|
}
|
|
]
|
|
}`)
|
|
|
|
var p webhook.GitHubPushPayload
|
|
|
|
err := json.Unmarshal(payload, &p)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, "refs/heads/main", p.Ref)
|
|
assert.Equal(t, "abc123", p.After)
|
|
assert.Equal(t, "o/r", p.Repository.FullName)
|
|
assert.Equal(t, "octocat", p.Pusher.Name)
|
|
assert.NotNil(t, p.HeadCommit)
|
|
assert.Equal(t, "abc123", p.HeadCommit.ID)
|
|
assert.Len(t, p.Commits, 1)
|
|
}
|
|
|
|
// TestGitLabPushPayloadParsing tests direct deserialization of the GitLab payload struct.
|
|
func TestGitLabPushPayloadParsing(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
payload := []byte(`{
|
|
"ref": "refs/heads/main",
|
|
"before": "0000000000",
|
|
"after": "abc123",
|
|
"user_name": "gitlab-user",
|
|
"user_email": "user@gitlab.com",
|
|
"project": {
|
|
"path_with_namespace": "group/project",
|
|
"git_http_url": "https://gitlab.com/group/project.git",
|
|
"git_ssh_url": "git@gitlab.com:group/project.git",
|
|
"web_url": "https://gitlab.com/group/project"
|
|
},
|
|
"commits": [
|
|
{
|
|
"id": "abc123",
|
|
"url": "https://gitlab.com/group/project/-/commit/abc123",
|
|
"message": "Fix pipeline",
|
|
"author": {"name": "GitLab User", "email": "user@gitlab.com"}
|
|
}
|
|
]
|
|
}`)
|
|
|
|
var p webhook.GitLabPushPayload
|
|
|
|
err := json.Unmarshal(payload, &p)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, "refs/heads/main", p.Ref)
|
|
assert.Equal(t, "abc123", p.After)
|
|
assert.Equal(t, "group/project", p.Project.PathWithNamespace)
|
|
assert.Equal(t, "gitlab-user", p.UserName)
|
|
assert.Len(t, p.Commits, 1)
|
|
}
|
|
|
|
// TestExtractBranch tests branch extraction via HandleWebhook integration (extractBranch is unexported).
|
|
//
|
|
//nolint:funlen // table-driven test with comprehensive test cases
|
|
func TestExtractBranch(testingT *testing.T) {
|
|
testingT.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
ref string
|
|
expected string
|
|
}{
|
|
{
|
|
name: "extracts main branch",
|
|
ref: "refs/heads/main",
|
|
expected: "main",
|
|
},
|
|
{
|
|
name: "extracts feature branch",
|
|
ref: "refs/heads/feature/new-feature",
|
|
expected: "feature/new-feature",
|
|
},
|
|
{
|
|
name: "extracts develop branch",
|
|
ref: "refs/heads/develop",
|
|
expected: "develop",
|
|
},
|
|
{
|
|
name: "returns raw ref if no prefix",
|
|
ref: "main",
|
|
expected: "main",
|
|
},
|
|
{
|
|
name: "handles empty ref",
|
|
ref: "",
|
|
expected: "",
|
|
},
|
|
{
|
|
name: "handles partial prefix",
|
|
ref: "refs/heads/",
|
|
expected: "",
|
|
},
|
|
}
|
|
|
|
for _, testCase := range tests {
|
|
testingT.Run(testCase.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// We test via HandleWebhook since extractBranch is not exported.
|
|
// The test verifies behavior indirectly through the webhook event's branch.
|
|
svc, dbInst, cleanup := setupTestService(t)
|
|
defer cleanup()
|
|
|
|
app := createTestApp(t, dbInst, testCase.expected)
|
|
|
|
payload := []byte(`{"ref": "` + testCase.ref + `"}`)
|
|
|
|
err := svc.HandleWebhook(
|
|
context.Background(), app, webhook.SourceGitea, "push", payload,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
// Allow async deployment goroutine to complete before test cleanup
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
events, err := app.GetWebhookEvents(context.Background(), 10)
|
|
require.NoError(t, err)
|
|
require.Len(t, events, 1)
|
|
|
|
assert.Equal(t, testCase.expected, events[0].Branch)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHandleWebhookMatchingBranch(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
svc, dbInst, cleanup := setupTestService(t)
|
|
defer cleanup()
|
|
|
|
app := createTestApp(t, dbInst, "main")
|
|
|
|
payload := []byte(`{
|
|
"ref": "refs/heads/main",
|
|
"before": "0000000000000000000000000000000000000000",
|
|
"after": "abc123def456",
|
|
"repository": {
|
|
"full_name": "user/repo",
|
|
"clone_url": "https://gitea.example.com/user/repo.git",
|
|
"ssh_url": "git@gitea.example.com:user/repo.git"
|
|
},
|
|
"pusher": {"username": "testuser", "email": "test@example.com"},
|
|
"commits": [{"id": "abc123def456", "message": "Test commit",
|
|
"author": {"name": "Test User", "email": "test@example.com"}}]
|
|
}`)
|
|
|
|
err := svc.HandleWebhook(
|
|
context.Background(), app, webhook.SourceGitea, "push", payload,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
// Allow async deployment goroutine to complete before test cleanup
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
events, err := app.GetWebhookEvents(context.Background(), 10)
|
|
require.NoError(t, err)
|
|
require.Len(t, events, 1)
|
|
|
|
event := events[0]
|
|
assert.Equal(t, "push", event.EventType)
|
|
assert.Equal(t, "main", event.Branch)
|
|
assert.True(t, event.Matched)
|
|
assert.Equal(t, "abc123def456", event.CommitSHA.String)
|
|
}
|
|
|
|
func TestHandleWebhookNonMatchingBranch(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
svc, dbInst, cleanup := setupTestService(t)
|
|
defer cleanup()
|
|
|
|
app := createTestApp(t, dbInst, "main")
|
|
|
|
payload := []byte(`{"ref": "refs/heads/develop", "after": "def789ghi012"}`)
|
|
|
|
err := svc.HandleWebhook(
|
|
context.Background(), app, webhook.SourceGitea, "push", payload,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
events, err := app.GetWebhookEvents(context.Background(), 10)
|
|
require.NoError(t, err)
|
|
require.Len(t, events, 1)
|
|
|
|
assert.Equal(t, "develop", events[0].Branch)
|
|
assert.False(t, events[0].Matched)
|
|
}
|
|
|
|
func TestHandleWebhookInvalidJSON(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
svc, dbInst, cleanup := setupTestService(t)
|
|
defer cleanup()
|
|
|
|
app := createTestApp(t, dbInst, "main")
|
|
|
|
err := svc.HandleWebhook(
|
|
context.Background(), app, webhook.SourceGitea, "push", []byte(`{invalid json}`),
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
events, err := app.GetWebhookEvents(context.Background(), 10)
|
|
require.NoError(t, err)
|
|
require.Len(t, events, 1)
|
|
}
|
|
|
|
func TestHandleWebhookEmptyPayload(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
svc, dbInst, cleanup := setupTestService(t)
|
|
defer cleanup()
|
|
|
|
app := createTestApp(t, dbInst, "main")
|
|
|
|
err := svc.HandleWebhook(
|
|
context.Background(), app, webhook.SourceGitea, "push", []byte(`{}`),
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
events, err := app.GetWebhookEvents(context.Background(), 10)
|
|
require.NoError(t, err)
|
|
require.Len(t, events, 1)
|
|
assert.False(t, events[0].Matched)
|
|
}
|
|
|
|
// TestHandleWebhookGitHubSource tests HandleWebhook with a GitHub push payload.
|
|
func TestHandleWebhookGitHubSource(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
svc, dbInst, cleanup := setupTestService(t)
|
|
defer cleanup()
|
|
|
|
app := createTestApp(t, dbInst, "main")
|
|
|
|
payload := []byte(`{
|
|
"ref": "refs/heads/main",
|
|
"after": "github123",
|
|
"repository": {
|
|
"full_name": "org/repo",
|
|
"clone_url": "https://github.com/org/repo.git",
|
|
"html_url": "https://github.com/org/repo"
|
|
},
|
|
"pusher": {"name": "octocat", "email": "octocat@github.com"},
|
|
"head_commit": {
|
|
"id": "github123",
|
|
"url": "https://github.com/org/repo/commit/github123",
|
|
"message": "Update feature"
|
|
}
|
|
}`)
|
|
|
|
err := svc.HandleWebhook(
|
|
context.Background(), app, webhook.SourceGitHub, "push", payload,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
// Allow async deployment goroutine to complete before test cleanup
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
events, err := app.GetWebhookEvents(context.Background(), 10)
|
|
require.NoError(t, err)
|
|
require.Len(t, events, 1)
|
|
|
|
event := events[0]
|
|
assert.Equal(t, "main", event.Branch)
|
|
assert.True(t, event.Matched)
|
|
assert.Equal(t, "github123", event.CommitSHA.String)
|
|
assert.Equal(t, "https://github.com/org/repo/commit/github123", event.CommitURL.String)
|
|
}
|
|
|
|
// TestHandleWebhookGitLabSource tests HandleWebhook with a GitLab push payload.
|
|
func TestHandleWebhookGitLabSource(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
svc, dbInst, cleanup := setupTestService(t)
|
|
defer cleanup()
|
|
|
|
app := createTestApp(t, dbInst, "main")
|
|
|
|
payload := []byte(`{
|
|
"ref": "refs/heads/main",
|
|
"after": "gitlab456",
|
|
"user_name": "gitlab-dev",
|
|
"user_email": "dev@gitlab.com",
|
|
"project": {
|
|
"path_with_namespace": "group/project",
|
|
"git_http_url": "https://gitlab.com/group/project.git",
|
|
"web_url": "https://gitlab.com/group/project"
|
|
},
|
|
"commits": [
|
|
{
|
|
"id": "gitlab456",
|
|
"url": "https://gitlab.com/group/project/-/commit/gitlab456",
|
|
"message": "Deploy fix"
|
|
}
|
|
]
|
|
}`)
|
|
|
|
err := svc.HandleWebhook(
|
|
context.Background(), app, webhook.SourceGitLab, "push", payload,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
// Allow async deployment goroutine to complete before test cleanup
|
|
time.Sleep(100 * time.Millisecond)
|
|
|
|
events, err := app.GetWebhookEvents(context.Background(), 10)
|
|
require.NoError(t, err)
|
|
require.Len(t, events, 1)
|
|
|
|
event := events[0]
|
|
assert.Equal(t, "main", event.Branch)
|
|
assert.True(t, event.Matched)
|
|
assert.Equal(t, "gitlab456", event.CommitSHA.String)
|
|
assert.Equal(t, "https://gitlab.com/group/project/-/commit/gitlab456", event.CommitURL.String)
|
|
}
|
|
|
|
// TestSetupTestService verifies the test helper creates a working test service.
|
|
func TestSetupTestService(testingT *testing.T) {
|
|
testingT.Parallel()
|
|
|
|
testingT.Run("creates working test service", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
svc, dbInst, cleanup := setupTestService(t)
|
|
defer cleanup()
|
|
|
|
require.NotNil(t, svc)
|
|
require.NotNil(t, dbInst)
|
|
|
|
// Verify database is working
|
|
tmpDir := filepath.Dir(dbInst.Path())
|
|
_, err := os.Stat(tmpDir)
|
|
require.NoError(t, err)
|
|
})
|
|
}
|
|
|
|
// TestPushEventConstruction tests that PushEvent can be constructed directly.
|
|
func TestPushEventConstruction(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
event := webhook.PushEvent{
|
|
Source: webhook.SourceGitHub,
|
|
Ref: "refs/heads/main",
|
|
Before: "000",
|
|
After: "abc",
|
|
Branch: "main",
|
|
RepoName: "org/repo",
|
|
CloneURL: webhook.UnparsedURL("https://github.com/org/repo.git"),
|
|
HTMLURL: webhook.UnparsedURL("https://github.com/org/repo"),
|
|
CommitURL: webhook.UnparsedURL("https://github.com/org/repo/commit/abc"),
|
|
Pusher: "user",
|
|
}
|
|
|
|
assert.Equal(t, "main", event.Branch)
|
|
assert.Equal(t, webhook.SourceGitHub, event.Source)
|
|
assert.Equal(t, "abc", event.After)
|
|
}
|