package webhook_test import ( "context" "encoding/json" "os" "path/filepath" "testing" "time" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/fx" "git.eeqj.de/sneak/upaas/internal/config" "git.eeqj.de/sneak/upaas/internal/database" "git.eeqj.de/sneak/upaas/internal/docker" "git.eeqj.de/sneak/upaas/internal/globals" "git.eeqj.de/sneak/upaas/internal/logger" "git.eeqj.de/sneak/upaas/internal/models" "git.eeqj.de/sneak/upaas/internal/service/deploy" "git.eeqj.de/sneak/upaas/internal/service/notify" "git.eeqj.de/sneak/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.SSHPrivateKey = "private-key" app.SSHPublicKey = "public-key" app.Status = models.AppStatusPending err := app.Save(context.Background()) require.NoError(t, err) return app } //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, "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, "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, "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, "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, "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) } 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) }) } // 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) }) }