All checks were successful
Check / check (pull_request) Successful in 3m22s
Bump golangci-lint from v2.10.1 to v2.12.2 in the Dockerfile lint stage (tag+digest pin) and script/bootstrap release-archive pins (linux amd64/arm64 sha256s). Replace .golangci.yml with the canonical v2-layout config so linter settings (lll 88, funlen 80/50, cyclop 15, dupl 100) actually apply. Fix all findings surfaced by the new linter and config: - noctx: use httptest.NewRequestWithContext in all tests - gosec G710/G703: route app redirects through a path-escaping redirectToApp helper; annotate internal log path usage - goconst: introduce shared constants for template/JSON keys and repeated test literals - lll: wrap lines to the 88-column limit - dupl: extract shared helpers (generic findAllByAppID in models, deleteAppResource in handlers, parsePush in webhook payloads, table-driven/helper-based test dedup) - nolintlint: drop nolint directives made obsolete by the new limits Record the change in TODO.md; make check is green.
1075 lines
28 KiB
Go
1075 lines
28 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"
|
|
)
|
|
|
|
const (
|
|
giteaEventHeader = "X-Gitea-Event"
|
|
githubEventHeader = "X-GitHub-Event"
|
|
gitlabEventHeader = "X-Gitlab-Event"
|
|
gitlabPushHook = "Push Hook"
|
|
pushEventType = "push"
|
|
branchMain = "main"
|
|
refMain = "refs/heads/main"
|
|
testCommitSHA = "abc123def456789"
|
|
testPusher = "developer"
|
|
)
|
|
|
|
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.
|
|
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{giteaEventHeader: pushEventType},
|
|
expected: webhook.SourceGitea,
|
|
},
|
|
{
|
|
name: "detects GitHub from X-GitHub-Event header",
|
|
headers: map[string]string{githubEventHeader: pushEventType},
|
|
expected: webhook.SourceGitHub,
|
|
},
|
|
{
|
|
name: "detects GitLab from X-Gitlab-Event header",
|
|
headers: map[string]string{gitlabEventHeader: gitlabPushHook},
|
|
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{
|
|
giteaEventHeader: pushEventType,
|
|
githubEventHeader: pushEventType,
|
|
},
|
|
expected: webhook.SourceGitea,
|
|
},
|
|
{
|
|
name: "GitHub takes precedence over GitLab",
|
|
headers: map[string]string{
|
|
githubEventHeader: pushEventType,
|
|
gitlabEventHeader: gitlabPushHook,
|
|
},
|
|
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{giteaEventHeader: pushEventType},
|
|
source: webhook.SourceGitea,
|
|
expected: pushEventType,
|
|
},
|
|
{
|
|
name: "extracts GitHub event type",
|
|
headers: map[string]string{githubEventHeader: pushEventType},
|
|
source: webhook.SourceGitHub,
|
|
expected: pushEventType,
|
|
},
|
|
{
|
|
name: "extracts GitLab event type",
|
|
headers: map[string]string{gitlabEventHeader: gitlabPushHook},
|
|
source: webhook.SourceGitLab,
|
|
expected: gitlabPushHook,
|
|
},
|
|
{
|
|
name: "returns push for unknown source",
|
|
headers: map[string]string{},
|
|
source: webhook.SourceUnknown,
|
|
expected: pushEventType,
|
|
},
|
|
{
|
|
name: "returns push when header missing for source",
|
|
headers: map[string]string{},
|
|
source: webhook.SourceGitea,
|
|
expected: pushEventType,
|
|
},
|
|
}
|
|
|
|
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())
|
|
}
|
|
|
|
// pushEventExpectation describes the expected normalized fields of a parsed
|
|
// push payload.
|
|
type pushEventExpectation struct {
|
|
source webhook.Source
|
|
ref string
|
|
branch string
|
|
after string
|
|
repoName string
|
|
cloneURL webhook.UnparsedURL
|
|
htmlURL webhook.UnparsedURL
|
|
commitURL webhook.UnparsedURL
|
|
pusher string
|
|
}
|
|
|
|
// assertPushEvent parses payload for want.source and asserts every
|
|
// normalized PushEvent field matches want.
|
|
func assertPushEvent(t *testing.T, payload []byte, want pushEventExpectation) {
|
|
t.Helper()
|
|
|
|
event, err := webhook.ParsePushPayload(want.source, payload)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, want.source, event.Source)
|
|
assert.Equal(t, want.ref, event.Ref)
|
|
assert.Equal(t, want.branch, event.Branch)
|
|
assert.Equal(t, want.after, event.After)
|
|
|
|
assertPushEventOrigin(t, event, want)
|
|
}
|
|
|
|
// assertPushEventOrigin asserts the repository and pusher fields of event.
|
|
func assertPushEventOrigin(
|
|
t *testing.T,
|
|
event *webhook.PushEvent,
|
|
want pushEventExpectation,
|
|
) {
|
|
t.Helper()
|
|
|
|
assert.Equal(t, want.repoName, event.RepoName)
|
|
assert.Equal(t, want.cloneURL, event.CloneURL)
|
|
assert.Equal(t, want.htmlURL, event.HTMLURL)
|
|
assert.Equal(t, want.commitURL, event.CommitURL)
|
|
assert.Equal(t, want.pusher, event.Pusher)
|
|
}
|
|
|
|
// giteaPushJSON returns a realistic Gitea push webhook payload.
|
|
func giteaPushJSON() []byte {
|
|
return []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"}
|
|
}
|
|
]
|
|
}`)
|
|
}
|
|
|
|
// githubPushJSON returns a realistic GitHub push webhook payload.
|
|
func githubPushJSON() []byte {
|
|
return []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"}
|
|
}
|
|
]
|
|
}`)
|
|
}
|
|
|
|
// gitlabPushJSON returns a realistic GitLab push webhook payload.
|
|
func gitlabPushJSON() []byte {
|
|
return []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"}
|
|
}
|
|
]
|
|
}`)
|
|
}
|
|
|
|
// pushPayloadJSON returns the push payload fixture for source.
|
|
func pushPayloadJSON(t *testing.T, source webhook.Source) []byte {
|
|
t.Helper()
|
|
|
|
switch source {
|
|
case webhook.SourceGitHub:
|
|
return githubPushJSON()
|
|
case webhook.SourceGitLab:
|
|
return gitlabPushJSON()
|
|
case webhook.SourceGitea, webhook.SourceUnknown:
|
|
return giteaPushJSON()
|
|
}
|
|
|
|
t.Fatalf("no push payload fixture for source %v", source)
|
|
|
|
return nil
|
|
}
|
|
|
|
// TestParsePushPayload tests parsing of Gitea, GitHub, and GitLab push
|
|
// payloads into normalized PushEvents.
|
|
func TestParsePushPayload(testingT *testing.T) {
|
|
testingT.Parallel()
|
|
|
|
tests := []pushEventExpectation{
|
|
{
|
|
source: webhook.SourceGitea,
|
|
ref: refMain,
|
|
branch: branchMain,
|
|
after: testCommitSHA,
|
|
repoName: "myorg/myrepo",
|
|
cloneURL: "https://gitea.example.com/myorg/myrepo.git",
|
|
htmlURL: "https://gitea.example.com/myorg/myrepo",
|
|
commitURL: "https://gitea.example.com/myorg/myrepo/commit/abc123def456789",
|
|
pusher: testPusher,
|
|
},
|
|
{
|
|
source: webhook.SourceGitHub,
|
|
ref: refMain,
|
|
branch: branchMain,
|
|
after: testCommitSHA,
|
|
repoName: "myorg/myrepo",
|
|
cloneURL: "https://github.com/myorg/myrepo.git",
|
|
htmlURL: "https://github.com/myorg/myrepo",
|
|
commitURL: "https://github.com/myorg/myrepo/commit/abc123def456789",
|
|
pusher: testPusher,
|
|
},
|
|
{
|
|
source: webhook.SourceGitLab,
|
|
ref: "refs/heads/develop",
|
|
branch: "develop",
|
|
after: testCommitSHA,
|
|
repoName: "mygroup/myproject",
|
|
cloneURL: "https://gitlab.com/mygroup/myproject.git",
|
|
htmlURL: "https://gitlab.com/mygroup/myproject",
|
|
commitURL: "https://gitlab.com/mygroup/myproject/-/commit/abc123def456789",
|
|
pusher: testPusher,
|
|
},
|
|
}
|
|
|
|
for _, testCase := range tests {
|
|
testingT.Run(testCase.source.String(), func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
assertPushEvent(t, pushPayloadJSON(t, testCase.source), testCase)
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestParsePushPayloadUnknownFallsBackToGitea tests that unknown source
|
|
// uses the 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, branchMain, 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 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 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).
|
|
func TestExtractBranch(testingT *testing.T) {
|
|
testingT.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
ref string
|
|
expected string
|
|
}{
|
|
{
|
|
name: "extracts main branch",
|
|
ref: refMain,
|
|
expected: branchMain,
|
|
},
|
|
{
|
|
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: branchMain,
|
|
expected: branchMain,
|
|
},
|
|
{
|
|
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, pushEventType, 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, branchMain)
|
|
|
|
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, pushEventType, 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, pushEventType, event.EventType)
|
|
assert.Equal(t, branchMain, 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, branchMain)
|
|
|
|
payload := []byte(`{"ref": "refs/heads/develop", "after": "def789ghi012"}`)
|
|
|
|
err := svc.HandleWebhook(
|
|
context.Background(), app, webhook.SourceGitea, pushEventType, 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, branchMain)
|
|
|
|
err := svc.HandleWebhook(
|
|
context.Background(), app, webhook.SourceGitea, pushEventType,
|
|
[]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, branchMain)
|
|
|
|
err := svc.HandleWebhook(
|
|
context.Background(), app, webhook.SourceGitea, pushEventType, []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)
|
|
}
|
|
|
|
// assertHandleWebhookDeploys runs HandleWebhook for payload against a fresh
|
|
// app on branchMain and asserts the recorded event matched with the given
|
|
// commit SHA and commit URL.
|
|
func assertHandleWebhookDeploys(
|
|
t *testing.T,
|
|
source webhook.Source,
|
|
payload []byte,
|
|
wantSHA string,
|
|
wantCommitURL string,
|
|
) {
|
|
t.Helper()
|
|
|
|
svc, dbInst, cleanup := setupTestService(t)
|
|
defer cleanup()
|
|
|
|
app := createTestApp(t, dbInst, branchMain)
|
|
|
|
err := svc.HandleWebhook(context.Background(), app, source, pushEventType, 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, branchMain, event.Branch)
|
|
assert.True(t, event.Matched)
|
|
assert.Equal(t, wantSHA, event.CommitSHA.String)
|
|
assert.Equal(t, wantCommitURL, event.CommitURL.String)
|
|
}
|
|
|
|
// TestHandleWebhookGitHubSource tests HandleWebhook with a GitHub push payload.
|
|
func TestHandleWebhookGitHubSource(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
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"
|
|
}
|
|
}`)
|
|
|
|
assertHandleWebhookDeploys(
|
|
t, webhook.SourceGitHub, payload,
|
|
"github123", "https://github.com/org/repo/commit/github123",
|
|
)
|
|
}
|
|
|
|
// TestHandleWebhookGitLabSource tests HandleWebhook with a GitLab push payload.
|
|
func TestHandleWebhookGitLabSource(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
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"
|
|
}
|
|
]
|
|
}`)
|
|
|
|
assertHandleWebhookDeploys(
|
|
t, webhook.SourceGitLab, payload,
|
|
"gitlab456", "https://gitlab.com/group/project/-/commit/gitlab456",
|
|
)
|
|
}
|
|
|
|
// 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: refMain,
|
|
Before: "000",
|
|
After: "abc",
|
|
Branch: branchMain,
|
|
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, branchMain, event.Branch)
|
|
assert.Equal(t, webhook.SourceGitHub, event.Source)
|
|
assert.Equal(t, "abc", event.After)
|
|
}
|