upaas/internal/service/webhook/webhook_test.go
sneak 3f9d83c436 Initial commit with server startup infrastructure
Core infrastructure:
- Uber fx dependency injection
- Chi router with middleware stack
- SQLite database with embedded migrations
- Embedded templates and static assets
- Structured logging with slog

Features implemented:
- Authentication (login, logout, session management, argon2id hashing)
- App management (create, edit, delete, list)
- Deployment pipeline (clone, build, deploy, health check)
- Webhook processing for Gitea
- Notifications (ntfy, Slack)
- Environment variables, labels, volumes per app
- SSH key generation for deploy keys

Server startup:
- Server.Run() starts HTTP server on configured port
- Server.Shutdown() for graceful shutdown
- SetupRoutes() wires all handlers with chi router
2025-12-29 15:46:03 +07:00

335 lines
8.4 KiB
Go

package webhook_test
import (
"context"
"encoding/json"
"os"
"path/filepath"
"testing"
"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, 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
}
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)
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)
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)
})
}