Update golangci-lint to v2.12.2 with canonical config
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.
This commit is contained in:
2026-08-07 17:16:57 +00:00
parent 291f85f3ed
commit a4b8ea4402
41 changed files with 1172 additions and 797 deletions

View File

@@ -16,6 +16,12 @@ import (
"sneak.berlin/go/upaas/internal/service/app"
)
// testRepoURL is the default repository URL used across tests.
const testRepoURL = "git@example.com:user/repo.git"
// giteaRepoURL is the gitea repository URL used across tests.
const giteaRepoURL = "git@gitea.example.com:user/repo.git"
func setupTestService(t *testing.T) (*app.Service, func()) {
t.Helper()
@@ -58,7 +64,8 @@ func setupTestService(t *testing.T) (*app.Service, func()) {
}
// deleteItemTestHelper is a generic helper for testing delete operations.
// It creates an app, adds an item, verifies it exists, deletes it, and verifies it's gone.
// It creates an app, adds an item, verifies it exists, deletes it, and
// verifies it's gone.
func deleteItemTestHelper(
t *testing.T,
appName string,
@@ -73,7 +80,7 @@ func deleteItemTestHelper(
createdApp, err := svc.CreateApp(context.Background(), app.CreateAppInput{
Name: appName,
RepoURL: "git@example.com:user/repo.git",
RepoURL: testRepoURL,
})
require.NoError(t, err)
@@ -92,6 +99,35 @@ func deleteItemTestHelper(
assert.Equal(t, 0, count)
}
// runDeleteItemTest adapts typed list/delete callbacks so delete tests for
// different item types can share deleteItemTestHelper.
func runDeleteItemTest[T any](
t *testing.T,
appName string,
addItem func(ctx context.Context, svc *app.Service, appID string) error,
listItems func(ctx context.Context, application *models.App) ([]T, error),
deleteFirst func(ctx context.Context, svc *app.Service, item T) error,
) {
t.Helper()
deleteItemTestHelper(t, appName,
addItem,
func(ctx context.Context, application *models.App) (int, error) {
items, err := listItems(ctx, application)
return len(items), err
},
func(ctx context.Context, svc *app.Service, application *models.App) error {
items, err := listItems(ctx, application)
if err != nil {
return err
}
return deleteFirst(ctx, svc, items[0])
},
)
}
func TestCreateAppWithGeneratedKeys(t *testing.T) {
t.Parallel()
@@ -100,7 +136,7 @@ func TestCreateAppWithGeneratedKeys(t *testing.T) {
input := app.CreateAppInput{
Name: "test-app",
RepoURL: "git@gitea.example.com:user/repo.git",
RepoURL: giteaRepoURL,
Branch: "main",
DockerfilePath: "Dockerfile",
}
@@ -110,7 +146,7 @@ func TestCreateAppWithGeneratedKeys(t *testing.T) {
require.NotNil(t, createdApp)
assert.Equal(t, "test-app", createdApp.Name)
assert.Equal(t, "git@gitea.example.com:user/repo.git", createdApp.RepoURL)
assert.Equal(t, giteaRepoURL, createdApp.RepoURL)
assert.Equal(t, "main", createdApp.Branch)
assert.Equal(t, "Dockerfile", createdApp.DockerfilePath)
assert.NotEmpty(t, createdApp.ID)
@@ -130,7 +166,7 @@ func TestCreateAppDefaults(t *testing.T) {
input := app.CreateAppInput{
Name: "test-app-defaults",
RepoURL: "git@gitea.example.com:user/repo.git",
RepoURL: giteaRepoURL,
}
createdApp, err := svc.CreateApp(context.Background(), input)
@@ -148,7 +184,7 @@ func TestCreateAppOptionalFields(t *testing.T) {
input := app.CreateAppInput{
Name: "test-app-full",
RepoURL: "git@gitea.example.com:user/repo.git",
RepoURL: giteaRepoURL,
Branch: "develop",
DockerNetwork: "my-network",
NtfyTopic: "https://ntfy.sh/my-topic",
@@ -176,7 +212,7 @@ func TestUpdateApp(testingT *testing.T) {
createdApp, err := svc.CreateApp(context.Background(), app.CreateAppInput{
Name: "original-name",
RepoURL: "git@example.com:user/repo.git",
RepoURL: testRepoURL,
})
require.NoError(t, err)
@@ -208,7 +244,7 @@ func TestUpdateApp(testingT *testing.T) {
createdApp, err := svc.CreateApp(context.Background(), app.CreateAppInput{
Name: "test-clear",
RepoURL: "git@example.com:user/repo.git",
RepoURL: testRepoURL,
NtfyTopic: "https://ntfy.sh/topic",
SlackWebhook: "https://slack.com/hook",
})
@@ -216,7 +252,7 @@ func TestUpdateApp(testingT *testing.T) {
err = svc.UpdateApp(context.Background(), createdApp, app.UpdateAppInput{
Name: "test-clear",
RepoURL: "git@example.com:user/repo.git",
RepoURL: testRepoURL,
Branch: "main",
})
require.NoError(t, err)
@@ -240,7 +276,7 @@ func TestDeleteApp(testingT *testing.T) {
createdApp, err := svc.CreateApp(context.Background(), app.CreateAppInput{
Name: "to-delete",
RepoURL: "git@example.com:user/repo.git",
RepoURL: testRepoURL,
})
require.NoError(t, err)
@@ -264,7 +300,7 @@ func TestGetApp(testingT *testing.T) {
created, err := svc.CreateApp(context.Background(), app.CreateAppInput{
Name: "findable-app",
RepoURL: "git@example.com:user/repo.git",
RepoURL: testRepoURL,
})
require.NoError(t, err)
@@ -299,7 +335,7 @@ func TestGetAppByWebhookSecret(testingT *testing.T) {
created, err := svc.CreateApp(context.Background(), app.CreateAppInput{
Name: "webhook-app",
RepoURL: "git@example.com:user/repo.git",
RepoURL: testRepoURL,
})
require.NoError(t, err)
@@ -378,7 +414,7 @@ func TestEnvVarsAddAndRetrieve(t *testing.T) {
createdApp, err := svc.CreateApp(context.Background(), app.CreateAppInput{
Name: "env-test",
RepoURL: "git@example.com:user/repo.git",
RepoURL: testRepoURL,
})
require.NoError(t, err)
@@ -411,29 +447,33 @@ func TestEnvVarsAddAndRetrieve(t *testing.T) {
assert.Equal(t, "secret123", keys["API_KEY"])
}
// addDeletableEnvVar seeds the env var removed in the delete test.
func addDeletableEnvVar(
ctx context.Context, svc *app.Service, appID string,
) error {
return svc.AddEnvVar(ctx, appID, "TO_DELETE", "value")
}
func TestEnvVarsDelete(t *testing.T) {
t.Parallel()
deleteItemTestHelper(t, "env-delete-test",
func(ctx context.Context, svc *app.Service, appID string) error {
return svc.AddEnvVar(ctx, appID, "TO_DELETE", "value")
runDeleteItemTest(t, "env-delete-test", addDeletableEnvVar,
func(ctx context.Context, application *models.App) ([]*models.EnvVar, error) {
return application.GetEnvVars(ctx)
},
func(ctx context.Context, application *models.App) (int, error) {
envVars, err := application.GetEnvVars(ctx)
return len(envVars), err
},
func(ctx context.Context, svc *app.Service, application *models.App) error {
envVars, err := application.GetEnvVars(ctx)
if err != nil {
return err
}
return svc.DeleteEnvVar(ctx, envVars[0].ID)
func(ctx context.Context, svc *app.Service, item *models.EnvVar) error {
return svc.DeleteEnvVar(ctx, item.ID)
},
)
}
// addDeletableLabel seeds the label removed in the delete test.
func addDeletableLabel(
ctx context.Context, svc *app.Service, appID string,
) error {
return svc.AddLabel(ctx, appID, "to.delete", "value")
}
func TestLabels(testingT *testing.T) {
testingT.Parallel()
@@ -445,7 +485,7 @@ func TestLabels(testingT *testing.T) {
createdApp, err := svc.CreateApp(context.Background(), app.CreateAppInput{
Name: "label-test",
RepoURL: "git@example.com:user/repo.git",
RepoURL: testRepoURL,
})
require.NoError(t, err)
@@ -468,22 +508,12 @@ func TestLabels(testingT *testing.T) {
testingT.Run("deletes label", func(t *testing.T) {
t.Parallel()
deleteItemTestHelper(t, "label-delete-test",
func(ctx context.Context, svc *app.Service, appID string) error {
return svc.AddLabel(ctx, appID, "to.delete", "value")
runDeleteItemTest(t, "label-delete-test", addDeletableLabel,
func(ctx context.Context, application *models.App) ([]*models.Label, error) {
return application.GetLabels(ctx)
},
func(ctx context.Context, application *models.App) (int, error) {
labels, err := application.GetLabels(ctx)
return len(labels), err
},
func(ctx context.Context, svc *app.Service, application *models.App) error {
labels, err := application.GetLabels(ctx)
if err != nil {
return err
}
return svc.DeleteLabel(ctx, labels[0].ID)
func(ctx context.Context, svc *app.Service, item *models.Label) error {
return svc.DeleteLabel(ctx, item.ID)
},
)
})
@@ -497,7 +527,7 @@ func TestVolumesAddAndRetrieve(t *testing.T) {
createdApp, err := svc.CreateApp(context.Background(), app.CreateAppInput{
Name: "volume-test",
RepoURL: "git@example.com:user/repo.git",
RepoURL: testRepoURL,
})
require.NoError(t, err)
@@ -547,7 +577,7 @@ func TestVolumesDelete(t *testing.T) {
createdApp, err := svc.CreateApp(context.Background(), app.CreateAppInput{
Name: "volume-delete-test",
RepoURL: "git@example.com:user/repo.git",
RepoURL: testRepoURL,
})
require.NoError(t, err)
@@ -583,7 +613,7 @@ func TestUpdateAppStatus(testingT *testing.T) {
createdApp, err := svc.CreateApp(context.Background(), app.CreateAppInput{
Name: "status-test",
RepoURL: "git@example.com:user/repo.git",
RepoURL: testRepoURL,
})
require.NoError(t, err)
assert.Equal(t, models.AppStatusPending, createdApp.Status)

View File

@@ -121,7 +121,7 @@ func getSessionCookie(t *testing.T, svc *auth.Service) *http.Cookie {
require.NoError(t, err)
recorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, "/", nil)
request := httptest.NewRequestWithContext(t.Context(), http.MethodGet, "/", nil)
err = svc.CreateSession(recorder, request, user)
require.NoError(t, err)
@@ -144,7 +144,11 @@ func TestSessionCookieSecureFlag(testingT *testing.T) {
svc := setupAuthService(t, false)
cookie := getSessionCookie(t, svc)
require.NotNil(t, cookie, "session cookie should exist")
assert.True(t, cookie.Secure, "session cookie should have Secure flag in production mode")
assert.True(
t,
cookie.Secure,
"session cookie should have Secure flag in production mode",
)
})
}
@@ -324,7 +328,12 @@ func TestCreateUserRaceCondition(testingT *testing.T) {
}
assert.Equal(t, 1, successes, "exactly one goroutine should succeed")
assert.Equal(t, goroutines-1, failures, "all other goroutines should fail with ErrUserExists")
assert.Equal(
t,
goroutines-1,
failures,
"all other goroutines should fail with ErrUserExists",
)
})
}
@@ -380,7 +389,9 @@ func TestDestroySessionMaxAge(testingT *testing.T) {
defer cleanup()
recorder := httptest.NewRecorder()
request := httptest.NewRequest(http.MethodGet, "/", nil)
request := httptest.NewRequestWithContext(
t.Context(), http.MethodGet, "/", nil,
)
err := svc.DestroySession(recorder, request)
require.NoError(t, err)

View File

@@ -66,7 +66,8 @@ const logFilePermissions = 0o640
// logTimestampFormat is the format for log file timestamps.
const logTimestampFormat = "20060102T150405Z"
// logFileShortSHALength is the number of characters to use for commit SHA in log filenames.
// logFileShortSHALength is the number of characters to use for commit SHA
// in log filenames.
const logFileShortSHALength = 12
// dockerLogMessage represents a Docker build log message.
@@ -87,7 +88,10 @@ type deploymentLogWriter struct {
flushCtx context.Context //nolint:containedctx // needed for async flush goroutine
}
func newDeploymentLogWriter(ctx context.Context, deployment *models.Deployment) *deploymentLogWriter {
func newDeploymentLogWriter(
ctx context.Context,
deployment *models.Deployment,
) *deploymentLogWriter {
w := &deploymentLogWriter{
deployment: deployment,
done: make(chan struct{}),
@@ -257,7 +261,10 @@ func (svc *Service) GetBuildDir(appName string) string {
// GetLogFilePath returns the path to the log file for a deployment.
// Returns empty string if the path cannot be determined.
func (svc *Service) GetLogFilePath(app *models.App, deployment *models.Deployment) string {
func (svc *Service) GetLogFilePath(
app *models.App,
deployment *models.Deployment,
) string {
hostname, err := os.Hostname()
if err != nil {
hostname = "unknown"
@@ -275,7 +282,8 @@ func (svc *Service) GetLogFilePath(app *models.App, deployment *models.Deploymen
// Use started_at timestamp
timestamp := deployment.StartedAt.UTC().Format(logTimestampFormat)
// Build filename: appname_sha_timestamp.log.txt (or appname_timestamp.log.txt if no SHA)
// Build filename: appname_sha_timestamp.log.txt
// (or appname_timestamp.log.txt if no SHA)
var filename string
if sha != "" {
filename = fmt.Sprintf("%s_%s_%s.log.txt", app.Name, sha, timestamp)
@@ -308,7 +316,8 @@ func (svc *Service) CancelDeploy(appID string) bool {
// Deploy deploys an app. If cancelExisting is true (e.g. webhook-triggered),
// any in-progress deploy for the same app will be cancelled before starting.
// If cancelExisting is false and a deploy is in progress, ErrDeploymentInProgress is returned.
// If cancelExisting is false and a deploy is in progress,
// ErrDeploymentInProgress is returned.
func (svc *Service) Deploy(
ctx context.Context,
app *models.App,
@@ -342,7 +351,8 @@ func (svc *Service) Deploy(
// Fetch webhook event and create deployment record
webhookEvent := svc.fetchWebhookEvent(deployCtx, webhookEventID)
// Use a background context for DB operations that must complete regardless of cancellation
// Use a background context for DB operations that must complete
// regardless of cancellation
bgCtx := context.WithoutCancel(deployCtx)
deployment, err := svc.createDeploymentRecord(bgCtx, app, webhookEventID, webhookEvent)
@@ -401,7 +411,10 @@ func (svc *Service) createRollbackDeployment(
return nil, fmt.Errorf("failed to create rollback deployment: %w", saveErr)
}
_ = deployment.AppendLog(ctx, "Rolling back to previous image: "+app.PreviousImageID.String)
_ = deployment.AppendLog(
ctx,
"Rolling back to previous image: "+app.PreviousImageID.String,
)
return deployment, nil
}
@@ -417,7 +430,11 @@ func (svc *Service) executeRollback(
svc.removeOldContainer(ctx, app, deployment)
rollbackOpts, err := svc.buildContainerOptions(ctx, app, docker.ImageID(previousImageID))
rollbackOpts, err := svc.buildContainerOptions(
ctx,
app,
docker.ImageID(previousImageID),
)
if err != nil {
svc.failDeployment(bgCtx, app, deployment, err)
@@ -426,7 +443,12 @@ func (svc *Service) executeRollback(
containerID, err := svc.docker.CreateContainer(ctx, rollbackOpts)
if err != nil {
svc.failDeployment(bgCtx, app, deployment, fmt.Errorf("failed to create rollback container: %w", err))
svc.failDeployment(
bgCtx,
app,
deployment,
fmt.Errorf("failed to create rollback container: %w", err),
)
return fmt.Errorf("failed to create rollback container: %w", err)
}
@@ -436,7 +458,12 @@ func (svc *Service) executeRollback(
startErr := svc.docker.StartContainer(ctx, containerID)
if startErr != nil {
svc.failDeployment(bgCtx, app, deployment, fmt.Errorf("failed to start rollback container: %w", startErr))
svc.failDeployment(
bgCtx,
app,
deployment,
fmt.Errorf("failed to start rollback container: %w", startErr),
)
return fmt.Errorf("failed to start rollback container: %w", startErr)
}
@@ -695,7 +722,11 @@ func (svc *Service) cleanupCancelledDeploy(
if removeErr != nil {
svc.log.Error("failed to remove image from cancelled deploy",
"error", removeErr, "app", app.Name, "image", imageID)
_ = deployment.AppendLog(ctx, "WARNING: failed to clean up image "+imageID.String()+": "+removeErr.Error())
_ = deployment.AppendLog(
ctx,
"WARNING: failed to clean up image "+
imageID.String()+": "+removeErr.Error(),
)
} else {
svc.log.Info("cleaned up image from cancelled deploy",
"app", app.Name, "image", imageID)
@@ -870,14 +901,24 @@ func (svc *Service) cloneRepository(
err := os.MkdirAll(appBuildsDir, buildsDirPermissions)
if err != nil {
svc.failDeployment(ctx, app, deployment, fmt.Errorf("failed to create builds dir: %w", err))
svc.failDeployment(
ctx,
app,
deployment,
fmt.Errorf("failed to create builds dir: %w", err),
)
return "", nil, fmt.Errorf("failed to create builds dir: %w", err)
}
buildDir, err := os.MkdirTemp(appBuildsDir, fmt.Sprintf("%d-*", deployment.ID))
if err != nil {
svc.failDeployment(ctx, app, deployment, fmt.Errorf("failed to create temp dir: %w", err))
svc.failDeployment(
ctx,
app,
deployment,
fmt.Errorf("failed to create temp dir: %w", err),
)
return "", nil, fmt.Errorf("failed to create temp dir: %w", err)
}
@@ -908,7 +949,12 @@ func (svc *Service) cloneRepository(
)
if cloneErr != nil {
cleanup()
svc.failDeployment(ctx, app, deployment, fmt.Errorf("failed to clone repo: %w", cloneErr))
svc.failDeployment(
ctx,
app,
deployment,
fmt.Errorf("failed to clone repo: %w", cloneErr),
)
return "", nil, fmt.Errorf("failed to clone repo: %w", cloneErr)
}

View File

@@ -32,7 +32,10 @@ func TestCleanupCancelledDeploy_RemovesBuildDir(t *testing.T) {
require.NoError(t, os.MkdirAll(deployDir, 0o750))
// Create a file inside to verify full removal
require.NoError(t, os.WriteFile(filepath.Join(deployDir, "work"), []byte("test"), 0o600))
require.NoError(
t,
os.WriteFile(filepath.Join(deployDir, "work"), []byte("test"), 0o600),
)
// Also create a dir for a different deployment (should NOT be removed)
otherDir := filepath.Join(buildDir, "99-xyz789")

View File

@@ -31,7 +31,9 @@ func TestBuildContainerOptionsUsesImageID(t *testing.T) {
const expectedImageID = docker.ImageID("sha256:abc123def456")
opts, err := svc.BuildContainerOptionsExported(context.Background(), app, expectedImageID)
opts, err := svc.BuildContainerOptionsExported(
context.Background(), app, expectedImageID,
)
if err != nil {
t.Fatalf("buildContainerOptions returned error: %v", err)
}
@@ -77,14 +79,20 @@ func TestBuildContainerOptionsNoResourceLimits(t *testing.T) {
}
}
func TestBuildContainerOptionsCPULimit(t *testing.T) {
t.Parallel()
// buildOptsForApp saves an app configured by setup and returns the container
// options built for it.
func buildOptsForApp(
t *testing.T,
name string,
setup func(app *models.App),
) docker.CreateContainerOptions {
t.Helper()
db := database.NewTestDatabase(t)
app := models.NewApp(db)
app.Name = "cpulimit"
app.CPULimit = sql.NullFloat64{Float64: 0.5, Valid: true}
app.Name = name
setup(app)
err := app.Save(context.Background())
if err != nil {
@@ -101,6 +109,16 @@ func TestBuildContainerOptionsCPULimit(t *testing.T) {
t.Fatalf("buildContainerOptions returned error: %v", err)
}
return opts
}
func TestBuildContainerOptionsCPULimit(t *testing.T) {
t.Parallel()
opts := buildOptsForApp(t, "cpulimit", func(app *models.App) {
app.CPULimit = sql.NullFloat64{Float64: 0.5, Valid: true}
})
if opts.CPULimit != 0.5 {
t.Errorf("expected CPULimit=0.5, got %v", opts.CPULimit)
}
@@ -109,26 +127,9 @@ func TestBuildContainerOptionsCPULimit(t *testing.T) {
func TestBuildContainerOptionsMemoryLimit(t *testing.T) {
t.Parallel()
db := database.NewTestDatabase(t)
app := models.NewApp(db)
app.Name = "memlimit"
app.MemoryLimit = sql.NullInt64{Int64: 536870912, Valid: true} // 512m
err := app.Save(context.Background())
if err != nil {
t.Fatalf("failed to save app: %v", err)
}
log := slog.New(slog.NewTextHandler(os.Stderr, nil))
svc := deploy.NewTestService(log)
opts, err := svc.BuildContainerOptionsExported(
context.Background(), app, docker.ImageID("test:latest"),
)
if err != nil {
t.Fatalf("buildContainerOptions returned error: %v", err)
}
opts := buildOptsForApp(t, "memlimit", func(app *models.App) {
app.MemoryLimit = sql.NullInt64{Int64: 536870912, Valid: true} // 512m
})
if opts.MemoryLimit != 536870912 {
t.Errorf("expected MemoryLimit=536870912, got %v", opts.MemoryLimit)

View File

@@ -26,7 +26,11 @@ func (svc *Service) CancelActiveDeploy(appID string) {
}
// RegisterActiveDeploy registers an active deploy for testing.
func (svc *Service) RegisterActiveDeploy(appID string, cancel context.CancelFunc, done chan struct{}) {
func (svc *Service) RegisterActiveDeploy(
appID string,
cancel context.CancelFunc,
done chan struct{},
) {
svc.activeDeploys.Store(appID, &activeDeploy{cancel: cancel, done: done})
}
@@ -41,7 +45,11 @@ func (svc *Service) UnlockApp(appID string) {
}
// NewTestServiceWithConfig creates a Service with config and docker client for testing.
func NewTestServiceWithConfig(log *slog.Logger, cfg *config.Config, dockerClient *docker.Client) *Service {
func NewTestServiceWithConfig(
log *slog.Logger,
cfg *config.Config,
dockerClient *docker.Client,
) *Service {
return &Service{
log: log,
config: cfg,

View File

@@ -159,7 +159,8 @@ func (svc *Service) NotifyDeployFailed(
) {
duration := time.Since(deployment.StartedAt)
title := "Deploy failed: " + app.Name
message := "Deployment failed after " + formatDuration(duration) + ": " + deployErr.Error()
message := "Deployment failed after " + formatDuration(duration) +
": " + deployErr.Error()
svc.sendNotifications(ctx, app, title, message, message, "error")
}
@@ -266,7 +267,8 @@ func (svc *Service) sendNtfy(
request.Header.Set("Title", title)
request.Header.Set("Priority", svc.ntfyPriority(priority))
resp, err := svc.client.Do(request) // #nosec G704 -- URL from validated config, not user input
// #nosec G704 -- URL from validated config, not user input
resp, err := svc.client.Do(request)
if err != nil {
return fmt.Errorf("failed to send ntfy request: %w", err)
}
@@ -363,7 +365,8 @@ func (svc *Service) sendSlack(
request.Header.Set("Content-Type", "application/json")
resp, err := svc.client.Do(request) // #nosec G704 -- URL from validated config, not user input
// #nosec G704 -- URL from validated config, not user input
resp, err := svc.client.Do(request)
if err != nil {
return fmt.Errorf("failed to send slack request: %w", err)
}

View File

@@ -98,88 +98,77 @@ type GitLabPushPayload struct {
func ParsePushPayload(source Source, payload []byte) (*PushEvent, error) {
switch source {
case SourceGitHub:
return parseGitHubPush(payload)
return parsePush(payload, githubPushEvent)
case SourceGitLab:
return parseGitLabPush(payload)
return parsePush(payload, gitlabPushEvent)
case SourceGitea, SourceUnknown:
// Gitea and unknown both use Gitea format for backward compatibility.
return parseGiteaPush(payload)
return parsePush(payload, giteaPushEvent)
}
// Unreachable for known source values, but satisfies exhaustive checker.
return parseGiteaPush(payload)
return parsePush(payload, giteaPushEvent)
}
func parseGiteaPush(payload []byte) (*PushEvent, error) {
var p GiteaPushPayload
// parsePush unmarshals payload into P and converts it into a normalized
// PushEvent via build.
func parsePush[P any](payload []byte, build func(P) *PushEvent) (*PushEvent, error) {
var p P
unmarshalErr := json.Unmarshal(payload, &p)
if unmarshalErr != nil {
return nil, unmarshalErr
}
commitURL := extractGiteaCommitURL(p)
return &PushEvent{
Source: SourceGitea,
Ref: p.Ref,
Before: p.Before,
After: p.After,
Branch: extractBranch(p.Ref),
RepoName: p.Repository.FullName,
CloneURL: p.Repository.CloneURL,
HTMLURL: p.Repository.HTMLURL,
CommitURL: commitURL,
Pusher: p.Pusher.Username,
}, nil
return build(p), nil
}
func parseGitHubPush(payload []byte) (*PushEvent, error) {
var p GitHubPushPayload
unmarshalErr := json.Unmarshal(payload, &p)
if unmarshalErr != nil {
return nil, unmarshalErr
}
commitURL := extractGitHubCommitURL(p)
// basePushEvent builds a PushEvent populated with the fields shared by all
// webhook sources.
func basePushEvent(source Source, ref, before, after string) *PushEvent {
return &PushEvent{
Source: SourceGitHub,
Ref: p.Ref,
Before: p.Before,
After: p.After,
Branch: extractBranch(p.Ref),
RepoName: p.Repository.FullName,
CloneURL: p.Repository.CloneURL,
HTMLURL: p.Repository.HTMLURL,
CommitURL: commitURL,
Pusher: p.Pusher.Name,
}, nil
Source: source,
Ref: ref,
Before: before,
After: after,
Branch: extractBranch(ref),
}
}
func parseGitLabPush(payload []byte) (*PushEvent, error) {
var p GitLabPushPayload
// giteaPushEvent converts a Gitea push payload to a normalized PushEvent.
func giteaPushEvent(p GiteaPushPayload) *PushEvent {
event := basePushEvent(SourceGitea, p.Ref, p.Before, p.After)
event.RepoName = p.Repository.FullName
event.CloneURL = p.Repository.CloneURL
event.HTMLURL = p.Repository.HTMLURL
event.CommitURL = extractGiteaCommitURL(p)
event.Pusher = p.Pusher.Username
unmarshalErr := json.Unmarshal(payload, &p)
if unmarshalErr != nil {
return nil, unmarshalErr
}
return event
}
commitURL := extractGitLabCommitURL(p)
// gitlabPushEvent converts a GitLab push payload to a normalized PushEvent.
func gitlabPushEvent(p GitLabPushPayload) *PushEvent {
event := basePushEvent(SourceGitLab, p.Ref, p.Before, p.After)
event.RepoName = p.Project.PathWithNamespace
event.CloneURL = p.Project.GitHTTPURL
event.HTMLURL = p.Project.WebURL
event.CommitURL = extractGitLabCommitURL(p)
event.Pusher = p.UserName
return &PushEvent{
Source: SourceGitLab,
Ref: p.Ref,
Before: p.Before,
After: p.After,
Branch: extractBranch(p.Ref),
RepoName: p.Project.PathWithNamespace,
CloneURL: p.Project.GitHTTPURL,
HTMLURL: p.Project.WebURL,
CommitURL: commitURL,
Pusher: p.UserName,
}, nil
return event
}
// githubPushEvent converts a GitHub push payload to a normalized PushEvent.
func githubPushEvent(p GitHubPushPayload) *PushEvent {
event := basePushEvent(SourceGitHub, p.Ref, p.Before, p.After)
event.RepoName = p.Repository.FullName
event.CloneURL = p.Repository.CloneURL
event.HTMLURL = p.Repository.HTMLURL
event.CommitURL = extractGitHubCommitURL(p)
event.Pusher = p.Pusher.Name
return event
}
// extractBranch extracts the branch name from a git ref.

View File

@@ -24,6 +24,18 @@ import (
"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
@@ -45,9 +57,14 @@ func setupTestDeps(t *testing.T) *testDeps {
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"}
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})
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}
@@ -58,14 +75,19 @@ func setupTestService(t *testing.T) (*webhook.Service, *database.Database, func(
deps := setupTestDeps(t)
dockerClient, err := docker.New(fx.Lifecycle(nil), docker.Params{Logger: deps.logger, Config: deps.config})
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})
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,
Logger: deps.logger, Config: deps.config, Database: deps.db,
Docker: dockerClient, Notify: notifySvc,
})
require.NoError(t, err)
@@ -104,8 +126,6 @@ func createTestApp(
}
// 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()
@@ -116,17 +136,17 @@ func TestDetectWebhookSource(testingT *testing.T) {
}{
{
name: "detects Gitea from X-Gitea-Event header",
headers: map[string]string{"X-Gitea-Event": "push"},
headers: map[string]string{giteaEventHeader: pushEventType},
expected: webhook.SourceGitea,
},
{
name: "detects GitHub from X-GitHub-Event header",
headers: map[string]string{"X-GitHub-Event": "push"},
headers: map[string]string{githubEventHeader: pushEventType},
expected: webhook.SourceGitHub,
},
{
name: "detects GitLab from X-Gitlab-Event header",
headers: map[string]string{"X-Gitlab-Event": "Push Hook"},
headers: map[string]string{gitlabEventHeader: gitlabPushHook},
expected: webhook.SourceGitLab,
},
{
@@ -142,16 +162,16 @@ func TestDetectWebhookSource(testingT *testing.T) {
{
name: "Gitea takes precedence over GitHub",
headers: map[string]string{
"X-Gitea-Event": "push",
"X-GitHub-Event": "push",
giteaEventHeader: pushEventType,
githubEventHeader: pushEventType,
},
expected: webhook.SourceGitea,
},
{
name: "GitHub takes precedence over GitLab",
headers: map[string]string{
"X-GitHub-Event": "push",
"X-Gitlab-Event": "Push Hook",
githubEventHeader: pushEventType,
gitlabEventHeader: gitlabPushHook,
},
expected: webhook.SourceGitHub,
},
@@ -184,33 +204,33 @@ func TestDetectEventType(testingT *testing.T) {
}{
{
name: "extracts Gitea event type",
headers: map[string]string{"X-Gitea-Event": "push"},
headers: map[string]string{giteaEventHeader: pushEventType},
source: webhook.SourceGitea,
expected: "push",
expected: pushEventType,
},
{
name: "extracts GitHub event type",
headers: map[string]string{"X-GitHub-Event": "push"},
headers: map[string]string{githubEventHeader: pushEventType},
source: webhook.SourceGitHub,
expected: "push",
expected: pushEventType,
},
{
name: "extracts GitLab event type",
headers: map[string]string{"X-Gitlab-Event": "Push Hook"},
headers: map[string]string{gitlabEventHeader: gitlabPushHook},
source: webhook.SourceGitLab,
expected: "Push Hook",
expected: gitlabPushHook,
},
{
name: "returns push for unknown source",
headers: map[string]string{},
source: webhook.SourceUnknown,
expected: "push",
expected: pushEventType,
},
{
name: "returns push when header missing for source",
headers: map[string]string{},
source: webhook.SourceGitea,
expected: "push",
expected: pushEventType,
},
}
@@ -250,11 +270,54 @@ func TestUnparsedURLString(t *testing.T) {
assert.Empty(t, empty.String())
}
// TestParsePushPayloadGitea tests parsing of Gitea push payloads.
func TestParsePushPayloadGitea(t *testing.T) {
t.Parallel()
// 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
}
payload := []byte(`{
// 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",
@@ -275,29 +338,11 @@ func TestParsePushPayloadGitea(t *testing.T) {
}
]
}`)
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(`{
// githubPushJSON returns a realistic GitHub push webhook payload.
func githubPushJSON() []byte {
return []byte(`{
"ref": "refs/heads/main",
"before": "0000000000000000000000000000000000000000",
"after": "abc123def456789",
@@ -323,29 +368,11 @@ func TestParsePushPayloadGitHub(t *testing.T) {
}
]
}`)
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(`{
// gitlabPushJSON returns a realistic GitLab push webhook payload.
func gitlabPushJSON() []byte {
return []byte(`{
"ref": "refs/heads/develop",
"before": "0000000000000000000000000000000000000000",
"after": "abc123def456789",
@@ -366,25 +393,78 @@ func TestParsePushPayloadGitLab(t *testing.T) {
}
]
}`)
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.
// 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()
@@ -399,7 +479,7 @@ func TestParsePushPayloadUnknownFallsBackToGitea(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, webhook.SourceGitea, event.Source)
assert.Equal(t, "main", event.Branch)
assert.Equal(t, branchMain, event.Branch)
assert.Equal(t, "abc123", event.After)
}
@@ -462,7 +542,10 @@ func TestGitHubCommitURLFallback(t *testing.T) {
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)
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) {
@@ -477,7 +560,10 @@ func TestGitHubCommitURLFallback(t *testing.T) {
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)
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) {
@@ -491,7 +577,10 @@ func TestGitHubCommitURLFallback(t *testing.T) {
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)
assert.Equal(t,
webhook.UnparsedURL("https://github.com/u/r/commit/abc123"),
event.CommitURL,
)
})
}
@@ -511,7 +600,10 @@ func TestGitLabCommitURLFallback(t *testing.T) {
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)
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) {
@@ -525,7 +617,10 @@ func TestGitLabCommitURLFallback(t *testing.T) {
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)
assert.Equal(t,
webhook.UnparsedURL("https://gitlab.com/g/p/-/commit/abc123"),
event.CommitURL,
)
})
}
@@ -588,7 +683,8 @@ func TestGiteaPushPayloadParsing(testingT *testing.T) {
})
}
// TestGitHubPushPayloadParsing tests direct deserialization of the GitHub payload struct.
// TestGitHubPushPayloadParsing tests deserialization of the GitHub payload
// struct.
func TestGitHubPushPayloadParsing(t *testing.T) {
t.Parallel()
@@ -633,7 +729,8 @@ func TestGitHubPushPayloadParsing(t *testing.T) {
assert.Len(t, p.Commits, 1)
}
// TestGitLabPushPayloadParsing tests direct deserialization of the GitLab payload struct.
// TestGitLabPushPayloadParsing tests deserialization of the GitLab payload
// struct.
func TestGitLabPushPayloadParsing(t *testing.T) {
t.Parallel()
@@ -671,9 +768,8 @@ func TestGitLabPushPayloadParsing(t *testing.T) {
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
// TestExtractBranch tests branch extraction via HandleWebhook integration
// (extractBranch is unexported).
func TestExtractBranch(testingT *testing.T) {
testingT.Parallel()
@@ -684,8 +780,8 @@ func TestExtractBranch(testingT *testing.T) {
}{
{
name: "extracts main branch",
ref: "refs/heads/main",
expected: "main",
ref: refMain,
expected: branchMain,
},
{
name: "extracts feature branch",
@@ -699,8 +795,8 @@ func TestExtractBranch(testingT *testing.T) {
},
{
name: "returns raw ref if no prefix",
ref: "main",
expected: "main",
ref: branchMain,
expected: branchMain,
},
{
name: "handles empty ref",
@@ -728,7 +824,7 @@ func TestExtractBranch(testingT *testing.T) {
payload := []byte(`{"ref": "` + testCase.ref + `"}`)
err := svc.HandleWebhook(
context.Background(), app, webhook.SourceGitea, "push", payload,
context.Background(), app, webhook.SourceGitea, pushEventType, payload,
)
require.NoError(t, err)
@@ -750,7 +846,7 @@ func TestHandleWebhookMatchingBranch(t *testing.T) {
svc, dbInst, cleanup := setupTestService(t)
defer cleanup()
app := createTestApp(t, dbInst, "main")
app := createTestApp(t, dbInst, branchMain)
payload := []byte(`{
"ref": "refs/heads/main",
@@ -767,7 +863,7 @@ func TestHandleWebhookMatchingBranch(t *testing.T) {
}`)
err := svc.HandleWebhook(
context.Background(), app, webhook.SourceGitea, "push", payload,
context.Background(), app, webhook.SourceGitea, pushEventType, payload,
)
require.NoError(t, err)
@@ -779,8 +875,8 @@ func TestHandleWebhookMatchingBranch(t *testing.T) {
require.Len(t, events, 1)
event := events[0]
assert.Equal(t, "push", event.EventType)
assert.Equal(t, "main", event.Branch)
assert.Equal(t, pushEventType, event.EventType)
assert.Equal(t, branchMain, event.Branch)
assert.True(t, event.Matched)
assert.Equal(t, "abc123def456", event.CommitSHA.String)
}
@@ -791,12 +887,12 @@ func TestHandleWebhookNonMatchingBranch(t *testing.T) {
svc, dbInst, cleanup := setupTestService(t)
defer cleanup()
app := createTestApp(t, dbInst, "main")
app := createTestApp(t, dbInst, branchMain)
payload := []byte(`{"ref": "refs/heads/develop", "after": "def789ghi012"}`)
err := svc.HandleWebhook(
context.Background(), app, webhook.SourceGitea, "push", payload,
context.Background(), app, webhook.SourceGitea, pushEventType, payload,
)
require.NoError(t, err)
@@ -814,10 +910,11 @@ func TestHandleWebhookInvalidJSON(t *testing.T) {
svc, dbInst, cleanup := setupTestService(t)
defer cleanup()
app := createTestApp(t, dbInst, "main")
app := createTestApp(t, dbInst, branchMain)
err := svc.HandleWebhook(
context.Background(), app, webhook.SourceGitea, "push", []byte(`{invalid json}`),
context.Background(), app, webhook.SourceGitea, pushEventType,
[]byte(`{invalid json}`),
)
require.NoError(t, err)
@@ -832,10 +929,10 @@ func TestHandleWebhookEmptyPayload(t *testing.T) {
svc, dbInst, cleanup := setupTestService(t)
defer cleanup()
app := createTestApp(t, dbInst, "main")
app := createTestApp(t, dbInst, branchMain)
err := svc.HandleWebhook(
context.Background(), app, webhook.SourceGitea, "push", []byte(`{}`),
context.Background(), app, webhook.SourceGitea, pushEventType, []byte(`{}`),
)
require.NoError(t, err)
@@ -845,14 +942,43 @@ func TestHandleWebhookEmptyPayload(t *testing.T) {
assert.False(t, events[0].Matched)
}
// TestHandleWebhookGitHubSource tests HandleWebhook with a GitHub push payload.
func TestHandleWebhookGitHubSource(t *testing.T) {
t.Parallel()
// 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, "main")
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",
@@ -870,34 +996,16 @@ func TestHandleWebhookGitHubSource(t *testing.T) {
}
}`)
err := svc.HandleWebhook(
context.Background(), app, webhook.SourceGitHub, "push", payload,
assertHandleWebhookDeploys(
t, webhook.SourceGitHub, payload,
"github123", "https://github.com/org/repo/commit/github123",
)
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",
@@ -917,23 +1025,10 @@ func TestHandleWebhookGitLabSource(t *testing.T) {
]
}`)
err := svc.HandleWebhook(
context.Background(), app, webhook.SourceGitLab, "push", payload,
assertHandleWebhookDeploys(
t, webhook.SourceGitLab, payload,
"gitlab456", "https://gitlab.com/group/project/-/commit/gitlab456",
)
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.
@@ -962,10 +1057,10 @@ func TestPushEventConstruction(t *testing.T) {
event := webhook.PushEvent{
Source: webhook.SourceGitHub,
Ref: "refs/heads/main",
Ref: refMain,
Before: "000",
After: "abc",
Branch: "main",
Branch: branchMain,
RepoName: "org/repo",
CloneURL: webhook.UnparsedURL("https://github.com/org/repo.git"),
HTMLURL: webhook.UnparsedURL("https://github.com/org/repo"),
@@ -973,7 +1068,7 @@ func TestPushEventConstruction(t *testing.T) {
Pusher: "user",
}
assert.Equal(t, "main", event.Branch)
assert.Equal(t, branchMain, event.Branch)
assert.Equal(t, webhook.SourceGitHub, event.Source)
assert.Equal(t, "abc", event.After)
}