feat: add GitHub and GitLab webhook support
All checks were successful
Check / check (pull_request) Successful in 1m50s
All checks were successful
Check / check (pull_request) Successful in 1m50s
Add auto-detection of webhook source (Gitea, GitHub, GitLab) by examining HTTP headers (X-Gitea-Event, X-GitHub-Event, X-Gitlab-Event). Parse push webhook payloads from all three platforms into a normalized PushEvent type for unified processing. Each platform's payload format is handled by dedicated parser functions with correct field mapping and commit URL extraction. The webhook handler now detects the source automatically — existing Gitea webhooks continue to work unchanged, while GitHub and GitLab webhooks are parsed with their respective payload formats. Includes comprehensive tests for source detection, event type extraction, payload parsing for all three platforms, commit URL fallback logic, and integration tests via HandleWebhook.
This commit is contained in:
@@ -1,12 +1,12 @@
|
|||||||
# µPaaS by [@sneak](https://sneak.berlin)
|
# µPaaS by [@sneak](https://sneak.berlin)
|
||||||
|
|
||||||
A simple self-hosted PaaS that auto-deploys Docker containers from Git repositories via Gitea webhooks.
|
A simple self-hosted PaaS that auto-deploys Docker containers from Git repositories via webhooks from Gitea, GitHub, or GitLab.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Single admin user with argon2id password hashing
|
- Single admin user with argon2id password hashing
|
||||||
- Per-app SSH keypairs for read-only deploy keys
|
- Per-app SSH keypairs for read-only deploy keys
|
||||||
- Per-app UUID-based webhook URLs for Gitea integration
|
- Per-app UUID-based webhook URLs with auto-detection of Gitea, GitHub, and GitLab
|
||||||
- Branch filtering - only deploy on configured branch changes
|
- Branch filtering - only deploy on configured branch changes
|
||||||
- Environment variables, labels, and volume mounts per app
|
- Environment variables, labels, and volume mounts per app
|
||||||
- Docker builds via socket access
|
- Docker builds via socket access
|
||||||
@@ -19,7 +19,7 @@ A simple self-hosted PaaS that auto-deploys Docker containers from Git repositor
|
|||||||
- Complex CI pipelines
|
- Complex CI pipelines
|
||||||
- Multiple container orchestration
|
- Multiple container orchestration
|
||||||
- SPA/API-first design
|
- SPA/API-first design
|
||||||
- Support for non-Gitea webhooks
|
- Support for non-push webhook events (e.g. issues, merge requests)
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
@@ -44,7 +44,7 @@ upaas/
|
|||||||
│ │ ├── auth/ # Authentication service
|
│ │ ├── auth/ # Authentication service
|
||||||
│ │ ├── deploy/ # Deployment orchestration
|
│ │ ├── deploy/ # Deployment orchestration
|
||||||
│ │ ├── notify/ # Notifications (ntfy, Slack)
|
│ │ ├── notify/ # Notifications (ntfy, Slack)
|
||||||
│ │ └── webhook/ # Gitea webhook processing
|
│ │ └── webhook/ # Webhook processing (Gitea, GitHub, GitLab)
|
||||||
│ └── ssh/ # SSH key generation
|
│ └── ssh/ # SSH key generation
|
||||||
├── static/ # Embedded CSS/JS assets
|
├── static/ # Embedded CSS/JS assets
|
||||||
└── templates/ # Embedded HTML templates
|
└── templates/ # Embedded HTML templates
|
||||||
|
|||||||
@@ -7,12 +7,14 @@ import (
|
|||||||
"github.com/go-chi/chi/v5"
|
"github.com/go-chi/chi/v5"
|
||||||
|
|
||||||
"sneak.berlin/go/upaas/internal/models"
|
"sneak.berlin/go/upaas/internal/models"
|
||||||
|
"sneak.berlin/go/upaas/internal/service/webhook"
|
||||||
)
|
)
|
||||||
|
|
||||||
// maxWebhookBodySize is the maximum allowed size of a webhook request body (1MB).
|
// maxWebhookBodySize is the maximum allowed size of a webhook request body (1MB).
|
||||||
const maxWebhookBodySize = 1 << 20
|
const maxWebhookBodySize = 1 << 20
|
||||||
|
|
||||||
// HandleWebhook handles incoming Gitea webhooks.
|
// HandleWebhook handles incoming webhooks from Gitea, GitHub, or GitLab.
|
||||||
|
// The webhook source is auto-detected from HTTP headers.
|
||||||
func (h *Handlers) HandleWebhook() http.HandlerFunc {
|
func (h *Handlers) HandleWebhook() http.HandlerFunc {
|
||||||
return func(writer http.ResponseWriter, request *http.Request) {
|
return func(writer http.ResponseWriter, request *http.Request) {
|
||||||
secret := chi.URLParam(request, "secret")
|
secret := chi.URLParam(request, "secret")
|
||||||
@@ -50,16 +52,17 @@ func (h *Handlers) HandleWebhook() http.HandlerFunc {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get event type from header
|
// Auto-detect webhook source from headers
|
||||||
eventType := request.Header.Get("X-Gitea-Event")
|
source := webhook.DetectWebhookSource(request.Header)
|
||||||
if eventType == "" {
|
|
||||||
eventType = "push"
|
// Extract event type based on detected source
|
||||||
}
|
eventType := webhook.DetectEventType(request.Header, source)
|
||||||
|
|
||||||
// Process webhook
|
// Process webhook
|
||||||
webhookErr := h.webhook.HandleWebhook(
|
webhookErr := h.webhook.HandleWebhook(
|
||||||
request.Context(),
|
request.Context(),
|
||||||
application,
|
application,
|
||||||
|
source,
|
||||||
eventType,
|
eventType,
|
||||||
body,
|
body,
|
||||||
)
|
)
|
||||||
|
|||||||
248
internal/service/webhook/payloads.go
Normal file
248
internal/service/webhook/payloads.go
Normal file
@@ -0,0 +1,248 @@
|
|||||||
|
package webhook
|
||||||
|
|
||||||
|
import "encoding/json"
|
||||||
|
|
||||||
|
// GiteaPushPayload represents a Gitea push webhook payload.
|
||||||
|
//
|
||||||
|
//nolint:tagliatelle // Field names match Gitea API (snake_case)
|
||||||
|
type GiteaPushPayload struct {
|
||||||
|
Ref string `json:"ref"`
|
||||||
|
Before string `json:"before"`
|
||||||
|
After string `json:"after"`
|
||||||
|
CompareURL UnparsedURL `json:"compare_url"`
|
||||||
|
Repository struct {
|
||||||
|
FullName string `json:"full_name"`
|
||||||
|
CloneURL UnparsedURL `json:"clone_url"`
|
||||||
|
SSHURL string `json:"ssh_url"`
|
||||||
|
HTMLURL UnparsedURL `json:"html_url"`
|
||||||
|
} `json:"repository"`
|
||||||
|
Pusher struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
} `json:"pusher"`
|
||||||
|
Commits []struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
URL UnparsedURL `json:"url"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Author struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
} `json:"author"`
|
||||||
|
} `json:"commits"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GitHubPushPayload represents a GitHub push webhook payload.
|
||||||
|
//
|
||||||
|
//nolint:tagliatelle // Field names match GitHub API (snake_case)
|
||||||
|
type GitHubPushPayload struct {
|
||||||
|
Ref string `json:"ref"`
|
||||||
|
Before string `json:"before"`
|
||||||
|
After string `json:"after"`
|
||||||
|
CompareURL string `json:"compare"`
|
||||||
|
Repository struct {
|
||||||
|
FullName string `json:"full_name"`
|
||||||
|
CloneURL UnparsedURL `json:"clone_url"`
|
||||||
|
SSHURL string `json:"ssh_url"`
|
||||||
|
HTMLURL UnparsedURL `json:"html_url"`
|
||||||
|
} `json:"repository"`
|
||||||
|
Pusher struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
} `json:"pusher"`
|
||||||
|
HeadCommit *struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
URL UnparsedURL `json:"url"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
} `json:"head_commit"`
|
||||||
|
Commits []struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
URL UnparsedURL `json:"url"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Author struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
} `json:"author"`
|
||||||
|
} `json:"commits"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GitLabPushPayload represents a GitLab push webhook payload.
|
||||||
|
//
|
||||||
|
//nolint:tagliatelle // Field names match GitLab API (snake_case)
|
||||||
|
type GitLabPushPayload struct {
|
||||||
|
Ref string `json:"ref"`
|
||||||
|
Before string `json:"before"`
|
||||||
|
After string `json:"after"`
|
||||||
|
UserName string `json:"user_name"`
|
||||||
|
UserEmail string `json:"user_email"`
|
||||||
|
Project struct {
|
||||||
|
PathWithNamespace string `json:"path_with_namespace"`
|
||||||
|
GitHTTPURL UnparsedURL `json:"git_http_url"`
|
||||||
|
GitSSHURL string `json:"git_ssh_url"`
|
||||||
|
WebURL UnparsedURL `json:"web_url"`
|
||||||
|
} `json:"project"`
|
||||||
|
Commits []struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
URL UnparsedURL `json:"url"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Author struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
} `json:"author"`
|
||||||
|
} `json:"commits"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParsePushPayload parses a raw webhook payload into a normalized PushEvent
|
||||||
|
// based on the detected webhook source. Returns an error if JSON unmarshaling
|
||||||
|
// fails. For SourceUnknown, falls back to Gitea format for backward
|
||||||
|
// compatibility.
|
||||||
|
func ParsePushPayload(source Source, payload []byte) (*PushEvent, error) {
|
||||||
|
switch source {
|
||||||
|
case SourceGitHub:
|
||||||
|
return parseGitHubPush(payload)
|
||||||
|
case SourceGitLab:
|
||||||
|
return parseGitLabPush(payload)
|
||||||
|
case SourceGitea, SourceUnknown:
|
||||||
|
// Gitea and unknown both use Gitea format for backward compatibility.
|
||||||
|
return parseGiteaPush(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unreachable for known source values, but satisfies exhaustive checker.
|
||||||
|
return parseGiteaPush(payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseGiteaPush(payload []byte) (*PushEvent, error) {
|
||||||
|
var p GiteaPushPayload
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseGitHubPush(payload []byte) (*PushEvent, error) {
|
||||||
|
var p GitHubPushPayload
|
||||||
|
|
||||||
|
unmarshalErr := json.Unmarshal(payload, &p)
|
||||||
|
if unmarshalErr != nil {
|
||||||
|
return nil, unmarshalErr
|
||||||
|
}
|
||||||
|
|
||||||
|
commitURL := extractGitHubCommitURL(p)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseGitLabPush(payload []byte) (*PushEvent, error) {
|
||||||
|
var p GitLabPushPayload
|
||||||
|
|
||||||
|
unmarshalErr := json.Unmarshal(payload, &p)
|
||||||
|
if unmarshalErr != nil {
|
||||||
|
return nil, unmarshalErr
|
||||||
|
}
|
||||||
|
|
||||||
|
commitURL := extractGitLabCommitURL(p)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractBranch extracts the branch name from a git ref.
|
||||||
|
func extractBranch(ref string) string {
|
||||||
|
// refs/heads/main -> main
|
||||||
|
const prefix = "refs/heads/"
|
||||||
|
|
||||||
|
if len(ref) >= len(prefix) && ref[:len(prefix)] == prefix {
|
||||||
|
return ref[len(prefix):]
|
||||||
|
}
|
||||||
|
|
||||||
|
return ref
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractGiteaCommitURL extracts the commit URL from a Gitea push payload.
|
||||||
|
// Prefers the URL from the head commit, falls back to constructing from repo URL.
|
||||||
|
func extractGiteaCommitURL(payload GiteaPushPayload) UnparsedURL {
|
||||||
|
for _, commit := range payload.Commits {
|
||||||
|
if commit.ID == payload.After && commit.URL != "" {
|
||||||
|
return commit.URL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload.Repository.HTMLURL != "" && payload.After != "" {
|
||||||
|
return UnparsedURL(payload.Repository.HTMLURL.String() + "/commit/" + payload.After)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractGitHubCommitURL extracts the commit URL from a GitHub push payload.
|
||||||
|
// Prefers head_commit.url, then searches commits, then constructs from repo URL.
|
||||||
|
func extractGitHubCommitURL(payload GitHubPushPayload) UnparsedURL {
|
||||||
|
if payload.HeadCommit != nil && payload.HeadCommit.URL != "" {
|
||||||
|
return payload.HeadCommit.URL
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, commit := range payload.Commits {
|
||||||
|
if commit.ID == payload.After && commit.URL != "" {
|
||||||
|
return commit.URL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload.Repository.HTMLURL != "" && payload.After != "" {
|
||||||
|
return UnparsedURL(payload.Repository.HTMLURL.String() + "/commit/" + payload.After)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractGitLabCommitURL extracts the commit URL from a GitLab push payload.
|
||||||
|
// Prefers commit URL from the commits list, falls back to constructing from
|
||||||
|
// project web URL.
|
||||||
|
func extractGitLabCommitURL(payload GitLabPushPayload) UnparsedURL {
|
||||||
|
for _, commit := range payload.Commits {
|
||||||
|
if commit.ID == payload.After && commit.URL != "" {
|
||||||
|
return commit.URL
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if payload.Project.WebURL != "" && payload.After != "" {
|
||||||
|
return UnparsedURL(payload.Project.WebURL.String() + "/-/commit/" + payload.After)
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
package webhook
|
package webhook
|
||||||
|
|
||||||
|
import "net/http"
|
||||||
|
|
||||||
// UnparsedURL is a URL stored as a plain string without parsing.
|
// UnparsedURL is a URL stored as a plain string without parsing.
|
||||||
// Use this instead of string when the value is known to be a URL
|
// Use this instead of string when the value is known to be a URL
|
||||||
// but should not be parsed into a net/url.URL (e.g. webhook URLs,
|
// but should not be parsed into a net/url.URL (e.g. webhook URLs,
|
||||||
@@ -8,3 +10,84 @@ type UnparsedURL string
|
|||||||
|
|
||||||
// String implements the fmt.Stringer interface.
|
// String implements the fmt.Stringer interface.
|
||||||
func (u UnparsedURL) String() string { return string(u) }
|
func (u UnparsedURL) String() string { return string(u) }
|
||||||
|
|
||||||
|
// Source identifies which git hosting platform sent the webhook.
|
||||||
|
type Source string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// SourceGitea indicates the webhook was sent by a Gitea instance.
|
||||||
|
SourceGitea Source = "gitea"
|
||||||
|
|
||||||
|
// SourceGitHub indicates the webhook was sent by GitHub.
|
||||||
|
SourceGitHub Source = "github"
|
||||||
|
|
||||||
|
// SourceGitLab indicates the webhook was sent by a GitLab instance.
|
||||||
|
SourceGitLab Source = "gitlab"
|
||||||
|
|
||||||
|
// SourceUnknown indicates the webhook source could not be determined.
|
||||||
|
SourceUnknown Source = "unknown"
|
||||||
|
)
|
||||||
|
|
||||||
|
// String implements the fmt.Stringer interface.
|
||||||
|
func (s Source) String() string { return string(s) }
|
||||||
|
|
||||||
|
// DetectWebhookSource determines the webhook source from HTTP headers.
|
||||||
|
// It checks for platform-specific event headers in this order:
|
||||||
|
// Gitea (X-Gitea-Event), GitHub (X-GitHub-Event), GitLab (X-Gitlab-Event).
|
||||||
|
// Returns SourceUnknown if no recognized header is found.
|
||||||
|
func DetectWebhookSource(headers http.Header) Source {
|
||||||
|
if headers.Get("X-Gitea-Event") != "" {
|
||||||
|
return SourceGitea
|
||||||
|
}
|
||||||
|
|
||||||
|
if headers.Get("X-Github-Event") != "" {
|
||||||
|
return SourceGitHub
|
||||||
|
}
|
||||||
|
|
||||||
|
if headers.Get("X-Gitlab-Event") != "" {
|
||||||
|
return SourceGitLab
|
||||||
|
}
|
||||||
|
|
||||||
|
return SourceUnknown
|
||||||
|
}
|
||||||
|
|
||||||
|
// DetectEventType extracts the event type string from HTTP headers
|
||||||
|
// based on the detected webhook source. Returns "push" as a fallback
|
||||||
|
// when no event header is found.
|
||||||
|
func DetectEventType(headers http.Header, source Source) string {
|
||||||
|
switch source {
|
||||||
|
case SourceGitea:
|
||||||
|
if v := headers.Get("X-Gitea-Event"); v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
case SourceGitHub:
|
||||||
|
if v := headers.Get("X-Github-Event"); v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
case SourceGitLab:
|
||||||
|
if v := headers.Get("X-Gitlab-Event"); v != "" {
|
||||||
|
return v
|
||||||
|
}
|
||||||
|
case SourceUnknown:
|
||||||
|
// Fall through to default
|
||||||
|
}
|
||||||
|
|
||||||
|
return "push"
|
||||||
|
}
|
||||||
|
|
||||||
|
// PushEvent is a normalized representation of a push webhook payload
|
||||||
|
// from any supported source (Gitea, GitHub, GitLab). The webhook
|
||||||
|
// service converts source-specific payloads into this format before
|
||||||
|
// processing.
|
||||||
|
type PushEvent struct {
|
||||||
|
Source Source
|
||||||
|
Ref string
|
||||||
|
Before string
|
||||||
|
After string
|
||||||
|
Branch string
|
||||||
|
RepoName string
|
||||||
|
CloneURL UnparsedURL
|
||||||
|
HTMLURL UnparsedURL
|
||||||
|
CommitURL UnparsedURL
|
||||||
|
Pusher string
|
||||||
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ package webhook
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
|
|
||||||
@@ -44,68 +43,46 @@ func New(_ fx.Lifecycle, params ServiceParams) (*Service, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GiteaPushPayload represents a Gitea push webhook payload.
|
// HandleWebhook processes a webhook request from any supported source
|
||||||
//
|
// (Gitea, GitHub, or GitLab). The source parameter determines which
|
||||||
//nolint:tagliatelle // Field names match Gitea API (snake_case)
|
// payload format to use for parsing.
|
||||||
type GiteaPushPayload struct {
|
|
||||||
Ref string `json:"ref"`
|
|
||||||
Before string `json:"before"`
|
|
||||||
After string `json:"after"`
|
|
||||||
CompareURL UnparsedURL `json:"compare_url"`
|
|
||||||
Repository struct {
|
|
||||||
FullName string `json:"full_name"`
|
|
||||||
CloneURL UnparsedURL `json:"clone_url"`
|
|
||||||
SSHURL string `json:"ssh_url"`
|
|
||||||
HTMLURL UnparsedURL `json:"html_url"`
|
|
||||||
} `json:"repository"`
|
|
||||||
Pusher struct {
|
|
||||||
Username string `json:"username"`
|
|
||||||
Email string `json:"email"`
|
|
||||||
} `json:"pusher"`
|
|
||||||
Commits []struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
URL UnparsedURL `json:"url"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
Author struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Email string `json:"email"`
|
|
||||||
} `json:"author"`
|
|
||||||
} `json:"commits"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// HandleWebhook processes a webhook request.
|
|
||||||
func (svc *Service) HandleWebhook(
|
func (svc *Service) HandleWebhook(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
app *models.App,
|
app *models.App,
|
||||||
|
source Source,
|
||||||
eventType string,
|
eventType string,
|
||||||
payload []byte,
|
payload []byte,
|
||||||
) error {
|
) error {
|
||||||
svc.log.Info("processing webhook", "app", app.Name, "event", eventType)
|
svc.log.Info("processing webhook",
|
||||||
|
"app", app.Name,
|
||||||
|
"source", source.String(),
|
||||||
|
"event", eventType,
|
||||||
|
)
|
||||||
|
|
||||||
// Parse payload
|
// Parse payload into normalized push event
|
||||||
var pushPayload GiteaPushPayload
|
pushEvent, parseErr := ParsePushPayload(source, payload)
|
||||||
|
if parseErr != nil {
|
||||||
unmarshalErr := json.Unmarshal(payload, &pushPayload)
|
svc.log.Warn("failed to parse webhook payload",
|
||||||
if unmarshalErr != nil {
|
"error", parseErr,
|
||||||
svc.log.Warn("failed to parse webhook payload", "error", unmarshalErr)
|
"source", source.String(),
|
||||||
// Continue anyway to log the event
|
)
|
||||||
|
// Continue with empty push event to still log the webhook
|
||||||
|
pushEvent = &PushEvent{Source: source}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract branch from ref
|
|
||||||
branch := extractBranch(pushPayload.Ref)
|
|
||||||
commitSHA := pushPayload.After
|
|
||||||
commitURL := extractCommitURL(pushPayload)
|
|
||||||
|
|
||||||
// Check if branch matches
|
// Check if branch matches
|
||||||
matched := branch == app.Branch
|
matched := pushEvent.Branch == app.Branch
|
||||||
|
|
||||||
// Create webhook event record
|
// Create webhook event record
|
||||||
event := models.NewWebhookEvent(svc.db)
|
event := models.NewWebhookEvent(svc.db)
|
||||||
event.AppID = app.ID
|
event.AppID = app.ID
|
||||||
event.EventType = eventType
|
event.EventType = eventType
|
||||||
event.Branch = branch
|
event.Branch = pushEvent.Branch
|
||||||
event.CommitSHA = sql.NullString{String: commitSHA, Valid: commitSHA != ""}
|
event.CommitSHA = sql.NullString{String: pushEvent.After, Valid: pushEvent.After != ""}
|
||||||
event.CommitURL = sql.NullString{String: commitURL.String(), Valid: commitURL != ""}
|
event.CommitURL = sql.NullString{
|
||||||
|
String: pushEvent.CommitURL.String(),
|
||||||
|
Valid: pushEvent.CommitURL != "",
|
||||||
|
}
|
||||||
event.Payload = sql.NullString{String: string(payload), Valid: true}
|
event.Payload = sql.NullString{String: string(payload), Valid: true}
|
||||||
event.Matched = matched
|
event.Matched = matched
|
||||||
event.Processed = false
|
event.Processed = false
|
||||||
@@ -117,9 +94,10 @@ func (svc *Service) HandleWebhook(
|
|||||||
|
|
||||||
svc.log.Info("webhook event recorded",
|
svc.log.Info("webhook event recorded",
|
||||||
"app", app.Name,
|
"app", app.Name,
|
||||||
"branch", branch,
|
"source", source.String(),
|
||||||
|
"branch", pushEvent.Branch,
|
||||||
"matched", matched,
|
"matched", matched,
|
||||||
"commit", commitSHA,
|
"commit", pushEvent.After,
|
||||||
)
|
)
|
||||||
|
|
||||||
// If branch matches, trigger deployment
|
// If branch matches, trigger deployment
|
||||||
@@ -154,33 +132,3 @@ func (svc *Service) triggerDeployment(
|
|||||||
_ = event.Save(deployCtx)
|
_ = event.Save(deployCtx)
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
// extractBranch extracts the branch name from a git ref.
|
|
||||||
func extractBranch(ref string) string {
|
|
||||||
// refs/heads/main -> main
|
|
||||||
const prefix = "refs/heads/"
|
|
||||||
|
|
||||||
if len(ref) >= len(prefix) && ref[:len(prefix)] == prefix {
|
|
||||||
return ref[len(prefix):]
|
|
||||||
}
|
|
||||||
|
|
||||||
return ref
|
|
||||||
}
|
|
||||||
|
|
||||||
// extractCommitURL extracts the commit URL from the webhook payload.
|
|
||||||
// Prefers the URL from the head commit, falls back to constructing from repo URL.
|
|
||||||
func extractCommitURL(payload GiteaPushPayload) UnparsedURL {
|
|
||||||
// Try to find the URL from the head commit (matching After SHA)
|
|
||||||
for _, commit := range payload.Commits {
|
|
||||||
if commit.ID == payload.After && commit.URL != "" {
|
|
||||||
return commit.URL
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fall back to constructing URL from repo HTML URL
|
|
||||||
if payload.Repository.HTMLURL != "" && payload.After != "" {
|
|
||||||
return UnparsedURL(payload.Repository.HTMLURL.String() + "/commit/" + payload.After)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package webhook_test
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -102,44 +103,57 @@ func createTestApp(
|
|||||||
return app
|
return app
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestDetectWebhookSource tests auto-detection of webhook source from HTTP headers.
|
||||||
|
//
|
||||||
//nolint:funlen // table-driven test with comprehensive test cases
|
//nolint:funlen // table-driven test with comprehensive test cases
|
||||||
func TestExtractBranch(testingT *testing.T) {
|
func TestDetectWebhookSource(testingT *testing.T) {
|
||||||
testingT.Parallel()
|
testingT.Parallel()
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
ref string
|
headers map[string]string
|
||||||
expected string
|
expected webhook.Source
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "extracts main branch",
|
name: "detects Gitea from X-Gitea-Event header",
|
||||||
ref: "refs/heads/main",
|
headers: map[string]string{"X-Gitea-Event": "push"},
|
||||||
expected: "main",
|
expected: webhook.SourceGitea,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "extracts feature branch",
|
name: "detects GitHub from X-GitHub-Event header",
|
||||||
ref: "refs/heads/feature/new-feature",
|
headers: map[string]string{"X-GitHub-Event": "push"},
|
||||||
expected: "feature/new-feature",
|
expected: webhook.SourceGitHub,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "extracts develop branch",
|
name: "detects GitLab from X-Gitlab-Event header",
|
||||||
ref: "refs/heads/develop",
|
headers: map[string]string{"X-Gitlab-Event": "Push Hook"},
|
||||||
expected: "develop",
|
expected: webhook.SourceGitLab,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "returns raw ref if no prefix",
|
name: "returns unknown when no recognized header",
|
||||||
ref: "main",
|
headers: map[string]string{"Content-Type": "application/json"},
|
||||||
expected: "main",
|
expected: webhook.SourceUnknown,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "handles empty ref",
|
name: "returns unknown for empty headers",
|
||||||
ref: "",
|
headers: map[string]string{},
|
||||||
expected: "",
|
expected: webhook.SourceUnknown,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "handles partial prefix",
|
name: "Gitea takes precedence over GitHub",
|
||||||
ref: "refs/heads/",
|
headers: map[string]string{
|
||||||
expected: "",
|
"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,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,123 +161,375 @@ func TestExtractBranch(testingT *testing.T) {
|
|||||||
testingT.Run(testCase.name, func(t *testing.T) {
|
testingT.Run(testCase.name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
// We test via HandleWebhook since extractBranch is not exported.
|
headers := http.Header{}
|
||||||
// The test verifies behavior indirectly through the webhook event's branch.
|
for key, value := range testCase.headers {
|
||||||
svc, dbInst, cleanup := setupTestService(t)
|
headers.Set(key, value)
|
||||||
defer cleanup()
|
}
|
||||||
|
|
||||||
app := createTestApp(t, dbInst, testCase.expected)
|
result := webhook.DetectWebhookSource(headers)
|
||||||
|
assert.Equal(t, testCase.expected, result)
|
||||||
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) {
|
// 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()
|
t.Parallel()
|
||||||
|
|
||||||
svc, dbInst, cleanup := setupTestService(t)
|
assert.Equal(t, "gitea", webhook.SourceGitea.String())
|
||||||
defer cleanup()
|
assert.Equal(t, "github", webhook.SourceGitHub.String())
|
||||||
|
assert.Equal(t, "gitlab", webhook.SourceGitLab.String())
|
||||||
|
assert.Equal(t, "unknown", webhook.SourceUnknown.String())
|
||||||
|
}
|
||||||
|
|
||||||
app := createTestApp(t, dbInst, "main")
|
// 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(`{
|
payload := []byte(`{
|
||||||
"ref": "refs/heads/main",
|
"ref": "refs/heads/main",
|
||||||
"before": "0000000000000000000000000000000000000000",
|
"before": "0000000000000000000000000000000000000000",
|
||||||
"after": "abc123def456",
|
"after": "abc123def456789",
|
||||||
|
"compare_url": "https://gitea.example.com/myorg/myrepo/compare/000...abc",
|
||||||
"repository": {
|
"repository": {
|
||||||
"full_name": "user/repo",
|
"full_name": "myorg/myrepo",
|
||||||
"clone_url": "https://gitea.example.com/user/repo.git",
|
"clone_url": "https://gitea.example.com/myorg/myrepo.git",
|
||||||
"ssh_url": "git@gitea.example.com:user/repo.git"
|
"ssh_url": "git@gitea.example.com:myorg/myrepo.git",
|
||||||
|
"html_url": "https://gitea.example.com/myorg/myrepo"
|
||||||
},
|
},
|
||||||
"pusher": {"username": "testuser", "email": "test@example.com"},
|
"pusher": {"username": "developer", "email": "dev@example.com"},
|
||||||
"commits": [{"id": "abc123def456", "message": "Test commit",
|
"commits": [
|
||||||
"author": {"name": "Test User", "email": "test@example.com"}}]
|
{
|
||||||
|
"id": "abc123def456789",
|
||||||
|
"url": "https://gitea.example.com/myorg/myrepo/commit/abc123def456789",
|
||||||
|
"message": "Fix bug",
|
||||||
|
"author": {"name": "Developer", "email": "dev@example.com"}
|
||||||
|
}
|
||||||
|
]
|
||||||
}`)
|
}`)
|
||||||
|
|
||||||
err := svc.HandleWebhook(context.Background(), app, "push", payload)
|
event, err := webhook.ParsePushPayload(webhook.SourceGitea, payload)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Allow async deployment goroutine to complete before test cleanup
|
assert.Equal(t, webhook.SourceGitea, event.Source)
|
||||||
time.Sleep(100 * time.Millisecond)
|
assert.Equal(t, "refs/heads/main", event.Ref)
|
||||||
|
|
||||||
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.Equal(t, "main", event.Branch)
|
||||||
assert.True(t, event.Matched)
|
assert.Equal(t, "abc123def456789", event.After)
|
||||||
assert.Equal(t, "abc123def456", event.CommitSHA.String)
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHandleWebhookNonMatchingBranch(t *testing.T) {
|
// TestParsePushPayloadGitHub tests parsing of GitHub push payloads.
|
||||||
|
func TestParsePushPayloadGitHub(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
svc, dbInst, cleanup := setupTestService(t)
|
payload := []byte(`{
|
||||||
defer cleanup()
|
"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"}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`)
|
||||||
|
|
||||||
app := createTestApp(t, dbInst, "main")
|
event, err := webhook.ParsePushPayload(webhook.SourceGitHub, payload)
|
||||||
|
|
||||||
payload := []byte(`{"ref": "refs/heads/develop", "after": "def789ghi012"}`)
|
|
||||||
|
|
||||||
err := svc.HandleWebhook(context.Background(), app, "push", payload)
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
events, err := app.GetWebhookEvents(context.Background(), 10)
|
assert.Equal(t, webhook.SourceGitHub, event.Source)
|
||||||
require.NoError(t, err)
|
assert.Equal(t, "refs/heads/main", event.Ref)
|
||||||
require.Len(t, events, 1)
|
assert.Equal(t, "main", event.Branch)
|
||||||
|
assert.Equal(t, "abc123def456789", event.After)
|
||||||
assert.Equal(t, "develop", events[0].Branch)
|
assert.Equal(t, "myorg/myrepo", event.RepoName)
|
||||||
assert.False(t, events[0].Matched)
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHandleWebhookInvalidJSON(t *testing.T) {
|
// TestParsePushPayloadGitLab tests parsing of GitLab push payloads.
|
||||||
|
func TestParsePushPayloadGitLab(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
svc, dbInst, cleanup := setupTestService(t)
|
payload := []byte(`{
|
||||||
defer cleanup()
|
"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"}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`)
|
||||||
|
|
||||||
app := createTestApp(t, dbInst, "main")
|
event, err := webhook.ParsePushPayload(webhook.SourceGitLab, payload)
|
||||||
|
|
||||||
err := svc.HandleWebhook(context.Background(), app, "push", []byte(`{invalid json}`))
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
events, err := app.GetWebhookEvents(context.Background(), 10)
|
assert.Equal(t, webhook.SourceGitLab, event.Source)
|
||||||
require.NoError(t, err)
|
assert.Equal(t, "refs/heads/develop", event.Ref)
|
||||||
require.Len(t, events, 1)
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestHandleWebhookEmptyPayload(t *testing.T) {
|
// TestParsePushPayloadUnknownFallsBackToGitea tests that unknown source uses Gitea parser.
|
||||||
|
func TestParsePushPayloadUnknownFallsBackToGitea(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
svc, dbInst, cleanup := setupTestService(t)
|
payload := []byte(`{
|
||||||
defer cleanup()
|
"ref": "refs/heads/main",
|
||||||
|
"after": "abc123",
|
||||||
|
"repository": {"full_name": "user/repo"},
|
||||||
|
"pusher": {"username": "user"}
|
||||||
|
}`)
|
||||||
|
|
||||||
app := createTestApp(t, dbInst, "main")
|
event, err := webhook.ParsePushPayload(webhook.SourceUnknown, payload)
|
||||||
|
|
||||||
err := svc.HandleWebhook(context.Background(), app, "push", []byte(`{}`))
|
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
events, err := app.GetWebhookEvents(context.Background(), 10)
|
assert.Equal(t, webhook.SourceGitea, event.Source)
|
||||||
require.NoError(t, err)
|
assert.Equal(t, "main", event.Branch)
|
||||||
require.Len(t, events, 1)
|
assert.Equal(t, "abc123", event.After)
|
||||||
assert.False(t, events[0].Matched)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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) {
|
func TestGiteaPushPayloadParsing(testingT *testing.T) {
|
||||||
testingT.Parallel()
|
testingT.Parallel()
|
||||||
|
|
||||||
@@ -322,6 +588,354 @@ func TestGiteaPushPayloadParsing(testingT *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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.
|
// TestSetupTestService verifies the test helper creates a working test service.
|
||||||
func TestSetupTestService(testingT *testing.T) {
|
func TestSetupTestService(testingT *testing.T) {
|
||||||
testingT.Parallel()
|
testingT.Parallel()
|
||||||
@@ -341,3 +955,25 @@ func TestSetupTestService(testingT *testing.T) {
|
|||||||
require.NoError(t, err)
|
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)
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user