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) }