summaryrefslogtreecommitdiffstats
path: root/models/webhook
diff options
context:
space:
mode:
authorDaniel Baumann <daniel@debian.org>2024-10-18 20:33:49 +0200
committerDaniel Baumann <daniel@debian.org>2024-12-12 23:57:56 +0100
commite68b9d00a6e05b3a941f63ffb696f91e554ac5ec (patch)
tree97775d6c13b0f416af55314eb6a89ef792474615 /models/webhook
parentInitial commit. (diff)
downloadforgejo-e68b9d00a6e05b3a941f63ffb696f91e554ac5ec.tar.xz
forgejo-e68b9d00a6e05b3a941f63ffb696f91e554ac5ec.zip
Adding upstream version 9.0.3.
Signed-off-by: Daniel Baumann <daniel@debian.org>
Diffstat (limited to '')
-rw-r--r--models/webhook/hooktask.go262
-rw-r--r--models/webhook/main_test.go19
-rw-r--r--models/webhook/webhook.go516
-rw-r--r--models/webhook/webhook_system.go83
-rw-r--r--models/webhook/webhook_test.go350
5 files changed, 1230 insertions, 0 deletions
diff --git a/models/webhook/hooktask.go b/models/webhook/hooktask.go
new file mode 100644
index 0000000..8734feb
--- /dev/null
+++ b/models/webhook/hooktask.go
@@ -0,0 +1,262 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package webhook
+
+import (
+ "context"
+ "errors"
+ "time"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/modules/json"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/timeutil"
+ webhook_module "code.gitea.io/gitea/modules/webhook"
+
+ gouuid "github.com/google/uuid"
+ "xorm.io/builder"
+)
+
+// ___ ___ __ ___________ __
+// / | \ ____ ____ | | _\__ ___/____ _____| | __
+// / ~ \/ _ \ / _ \| |/ / | | \__ \ / ___/ |/ /
+// \ Y ( <_> | <_> ) < | | / __ \_\___ \| <
+// \___|_ / \____/ \____/|__|_ \ |____| (____ /____ >__|_ \
+// \/ \/ \/ \/ \/
+
+// HookRequest represents hook task request information.
+type HookRequest struct {
+ URL string `json:"url"`
+ HTTPMethod string `json:"http_method"`
+ Headers map[string]string `json:"headers"`
+ Body string `json:"body"`
+}
+
+// HookResponse represents hook task response information.
+type HookResponse struct {
+ Status int `json:"status"`
+ Headers map[string]string `json:"headers"`
+ Body string `json:"body"`
+}
+
+// HookTask represents a hook task.
+type HookTask struct {
+ ID int64 `xorm:"pk autoincr"`
+ HookID int64 `xorm:"index"`
+ UUID string `xorm:"unique"`
+ PayloadContent string `xorm:"LONGTEXT"`
+ // PayloadVersion number to allow for smooth version upgrades:
+ // - PayloadVersion 1: PayloadContent contains the JSON as sent to the URL
+ // - PayloadVersion 2: PayloadContent contains the original event
+ PayloadVersion int `xorm:"DEFAULT 1"`
+
+ EventType webhook_module.HookEventType
+ IsDelivered bool
+ Delivered timeutil.TimeStampNano
+
+ // History info.
+ IsSucceed bool
+ RequestContent string `xorm:"LONGTEXT"`
+ RequestInfo *HookRequest `xorm:"-"`
+ ResponseContent string `xorm:"LONGTEXT"`
+ ResponseInfo *HookResponse `xorm:"-"`
+}
+
+func init() {
+ db.RegisterModel(new(HookTask))
+}
+
+// BeforeUpdate will be invoked by XORM before updating a record
+// representing this object
+func (t *HookTask) BeforeUpdate() {
+ if t.RequestInfo != nil {
+ t.RequestContent = t.simpleMarshalJSON(t.RequestInfo)
+ }
+ if t.ResponseInfo != nil {
+ t.ResponseContent = t.simpleMarshalJSON(t.ResponseInfo)
+ }
+}
+
+// AfterLoad updates the webhook object upon setting a column
+func (t *HookTask) AfterLoad() {
+ if len(t.RequestContent) == 0 {
+ return
+ }
+
+ t.RequestInfo = &HookRequest{}
+ if err := json.Unmarshal([]byte(t.RequestContent), t.RequestInfo); err != nil {
+ log.Error("Unmarshal RequestContent[%d]: %v", t.ID, err)
+ }
+
+ if len(t.ResponseContent) > 0 {
+ t.ResponseInfo = &HookResponse{}
+ if err := json.Unmarshal([]byte(t.ResponseContent), t.ResponseInfo); err != nil {
+ log.Error("Unmarshal ResponseContent[%d]: %v", t.ID, err)
+ }
+ }
+}
+
+func (t *HookTask) simpleMarshalJSON(v any) string {
+ p, err := json.Marshal(v)
+ if err != nil {
+ log.Error("Marshal [%d]: %v", t.ID, err)
+ }
+ return string(p)
+}
+
+// HookTasks returns a list of hook tasks by given conditions, order by ID desc.
+func HookTasks(ctx context.Context, hookID int64, page int) ([]*HookTask, error) {
+ tasks := make([]*HookTask, 0, setting.Webhook.PagingNum)
+ return tasks, db.GetEngine(ctx).
+ Limit(setting.Webhook.PagingNum, (page-1)*setting.Webhook.PagingNum).
+ Where("hook_id=?", hookID).
+ Desc("id").
+ Find(&tasks)
+}
+
+// CreateHookTask creates a new hook task,
+// it handles conversion from Payload to PayloadContent.
+func CreateHookTask(ctx context.Context, t *HookTask) (*HookTask, error) {
+ t.UUID = gouuid.New().String()
+ if t.Delivered == 0 {
+ t.Delivered = timeutil.TimeStampNanoNow()
+ }
+ if t.PayloadVersion == 0 {
+ return nil, errors.New("missing HookTask.PayloadVersion")
+ }
+ return t, db.Insert(ctx, t)
+}
+
+func GetHookTaskByID(ctx context.Context, id int64) (*HookTask, error) {
+ t := &HookTask{}
+
+ has, err := db.GetEngine(ctx).ID(id).Get(t)
+ if err != nil {
+ return nil, err
+ }
+ if !has {
+ return nil, ErrHookTaskNotExist{
+ TaskID: id,
+ }
+ }
+ return t, nil
+}
+
+// UpdateHookTask updates information of hook task.
+func UpdateHookTask(ctx context.Context, t *HookTask) error {
+ _, err := db.GetEngine(ctx).ID(t.ID).AllCols().Update(t)
+ return err
+}
+
+// ReplayHookTask copies a hook task to get re-delivered
+func ReplayHookTask(ctx context.Context, hookID int64, uuid string) (*HookTask, error) {
+ task, exist, err := db.Get[HookTask](ctx, builder.Eq{"hook_id": hookID, "uuid": uuid})
+ if err != nil {
+ return nil, err
+ } else if !exist {
+ return nil, ErrHookTaskNotExist{
+ HookID: hookID,
+ UUID: uuid,
+ }
+ }
+
+ return CreateHookTask(ctx, &HookTask{
+ HookID: task.HookID,
+ PayloadContent: task.PayloadContent,
+ EventType: task.EventType,
+ PayloadVersion: task.PayloadVersion,
+ })
+}
+
+// FindUndeliveredHookTaskIDs will find the next 100 undelivered hook tasks with ID greater than the provided lowerID
+func FindUndeliveredHookTaskIDs(ctx context.Context, lowerID int64) ([]int64, error) {
+ const batchSize = 100
+
+ tasks := make([]int64, 0, batchSize)
+ return tasks, db.GetEngine(ctx).
+ Select("id").
+ Table(new(HookTask)).
+ Where("is_delivered=?", false).
+ And("id > ?", lowerID).
+ Asc("id").
+ Limit(batchSize).
+ Find(&tasks)
+}
+
+func MarkTaskDelivered(ctx context.Context, task *HookTask) (bool, error) {
+ count, err := db.GetEngine(ctx).ID(task.ID).Where("is_delivered = ?", false).Cols("is_delivered").Update(&HookTask{
+ ID: task.ID,
+ IsDelivered: true,
+ })
+
+ return count != 0, err
+}
+
+// CleanupHookTaskTable deletes rows from hook_task as needed.
+func CleanupHookTaskTable(ctx context.Context, cleanupType HookTaskCleanupType, olderThan time.Duration, numberToKeep int) error {
+ log.Trace("Doing: CleanupHookTaskTable")
+
+ if cleanupType == OlderThan {
+ deleteOlderThan := time.Now().Add(-olderThan).UnixNano()
+ deletes, err := db.GetEngine(ctx).
+ Where("is_delivered = ? and delivered < ?", true, deleteOlderThan).
+ Delete(new(HookTask))
+ if err != nil {
+ return err
+ }
+ log.Trace("Deleted %d rows from hook_task", deletes)
+ } else if cleanupType == PerWebhook {
+ hookIDs := make([]int64, 0, 10)
+ err := db.GetEngine(ctx).
+ Table("webhook").
+ Where("id > 0").
+ Cols("id").
+ Find(&hookIDs)
+ if err != nil {
+ return err
+ }
+ for _, hookID := range hookIDs {
+ select {
+ case <-ctx.Done():
+ return db.ErrCancelledf("Before deleting hook_task records for hook id %d", hookID)
+ default:
+ }
+ if err = deleteDeliveredHookTasksByWebhook(ctx, hookID, numberToKeep); err != nil {
+ return err
+ }
+ }
+ }
+ log.Trace("Finished: CleanupHookTaskTable")
+ return nil
+}
+
+func deleteDeliveredHookTasksByWebhook(ctx context.Context, hookID int64, numberDeliveriesToKeep int) error {
+ log.Trace("Deleting hook_task rows for webhook %d, keeping the most recent %d deliveries", hookID, numberDeliveriesToKeep)
+ deliveryDates := make([]int64, 0, 10)
+ err := db.GetEngine(ctx).Table("hook_task").
+ Where("hook_task.hook_id = ? AND hook_task.is_delivered = ? AND hook_task.delivered is not null", hookID, true).
+ Cols("hook_task.delivered").
+ Join("INNER", "webhook", "hook_task.hook_id = webhook.id").
+ OrderBy("hook_task.delivered desc").
+ Limit(1, numberDeliveriesToKeep).
+ Find(&deliveryDates)
+ if err != nil {
+ return err
+ }
+
+ if len(deliveryDates) > 0 {
+ deletes, err := db.GetEngine(ctx).
+ Where("hook_id = ? and is_delivered = ? and delivered <= ?", hookID, true, deliveryDates[0]).
+ Delete(new(HookTask))
+ if err != nil {
+ return err
+ }
+ log.Trace("Deleted %d hook_task rows for webhook %d", deletes, hookID)
+ } else {
+ log.Trace("No hook_task rows to delete for webhook %d", hookID)
+ }
+
+ return nil
+}
diff --git a/models/webhook/main_test.go b/models/webhook/main_test.go
new file mode 100644
index 0000000..f19465d
--- /dev/null
+++ b/models/webhook/main_test.go
@@ -0,0 +1,19 @@
+// Copyright 2020 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package webhook
+
+import (
+ "testing"
+
+ "code.gitea.io/gitea/models/unittest"
+)
+
+func TestMain(m *testing.M) {
+ unittest.MainTest(m, &unittest.TestOptions{
+ FixtureFiles: []string{
+ "webhook.yml",
+ "hook_task.yml",
+ },
+ })
+}
diff --git a/models/webhook/webhook.go b/models/webhook/webhook.go
new file mode 100644
index 0000000..f3370f3
--- /dev/null
+++ b/models/webhook/webhook.go
@@ -0,0 +1,516 @@
+// Copyright 2014 The Gogs Authors. All rights reserved.
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package webhook
+
+import (
+ "context"
+ "fmt"
+ "strings"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/modules/json"
+ "code.gitea.io/gitea/modules/log"
+ "code.gitea.io/gitea/modules/optional"
+ "code.gitea.io/gitea/modules/secret"
+ "code.gitea.io/gitea/modules/setting"
+ "code.gitea.io/gitea/modules/timeutil"
+ "code.gitea.io/gitea/modules/util"
+ webhook_module "code.gitea.io/gitea/modules/webhook"
+
+ "xorm.io/builder"
+)
+
+// ErrWebhookNotExist represents a "WebhookNotExist" kind of error.
+type ErrWebhookNotExist struct {
+ ID int64
+}
+
+// IsErrWebhookNotExist checks if an error is a ErrWebhookNotExist.
+func IsErrWebhookNotExist(err error) bool {
+ _, ok := err.(ErrWebhookNotExist)
+ return ok
+}
+
+func (err ErrWebhookNotExist) Error() string {
+ return fmt.Sprintf("webhook does not exist [id: %d]", err.ID)
+}
+
+func (err ErrWebhookNotExist) Unwrap() error {
+ return util.ErrNotExist
+}
+
+// ErrHookTaskNotExist represents a "HookTaskNotExist" kind of error.
+type ErrHookTaskNotExist struct {
+ TaskID int64
+ HookID int64
+ UUID string
+}
+
+// IsErrHookTaskNotExist checks if an error is a ErrHookTaskNotExist.
+func IsErrHookTaskNotExist(err error) bool {
+ _, ok := err.(ErrHookTaskNotExist)
+ return ok
+}
+
+func (err ErrHookTaskNotExist) Error() string {
+ return fmt.Sprintf("hook task does not exist [task: %d, hook: %d, uuid: %s]", err.TaskID, err.HookID, err.UUID)
+}
+
+func (err ErrHookTaskNotExist) Unwrap() error {
+ return util.ErrNotExist
+}
+
+// HookContentType is the content type of a web hook
+type HookContentType int
+
+const (
+ // ContentTypeJSON is a JSON payload for web hooks
+ ContentTypeJSON HookContentType = iota + 1
+ // ContentTypeForm is an url-encoded form payload for web hook
+ ContentTypeForm
+)
+
+var hookContentTypes = map[string]HookContentType{
+ "json": ContentTypeJSON,
+ "form": ContentTypeForm,
+}
+
+// ToHookContentType returns HookContentType by given name.
+func ToHookContentType(name string) HookContentType {
+ return hookContentTypes[name]
+}
+
+// HookTaskCleanupType is the type of cleanup to perform on hook_task
+type HookTaskCleanupType int
+
+const (
+ // OlderThan hook_task rows will be cleaned up by the age of the row
+ OlderThan HookTaskCleanupType = iota
+ // PerWebhook hook_task rows will be cleaned up by leaving the most recent deliveries for each webhook
+ PerWebhook
+)
+
+var hookTaskCleanupTypes = map[string]HookTaskCleanupType{
+ "OlderThan": OlderThan,
+ "PerWebhook": PerWebhook,
+}
+
+// ToHookTaskCleanupType returns HookTaskCleanupType by given name.
+func ToHookTaskCleanupType(name string) HookTaskCleanupType {
+ return hookTaskCleanupTypes[name]
+}
+
+// Name returns the name of a given web hook's content type
+func (t HookContentType) Name() string {
+ switch t {
+ case ContentTypeJSON:
+ return "json"
+ case ContentTypeForm:
+ return "form"
+ }
+ return ""
+}
+
+// IsValidHookContentType returns true if given name is a valid hook content type.
+func IsValidHookContentType(name string) bool {
+ _, ok := hookContentTypes[name]
+ return ok
+}
+
+// Webhook represents a web hook object.
+type Webhook struct {
+ ID int64 `xorm:"pk autoincr"`
+ RepoID int64 `xorm:"INDEX"` // An ID of 0 indicates either a default or system webhook
+ OwnerID int64 `xorm:"INDEX"`
+ IsSystemWebhook bool
+ URL string `xorm:"url TEXT"`
+ HTTPMethod string `xorm:"http_method"`
+ ContentType HookContentType
+ Secret string `xorm:"TEXT"`
+ Events string `xorm:"TEXT"`
+ *webhook_module.HookEvent `xorm:"-"`
+ IsActive bool `xorm:"INDEX"`
+ Type webhook_module.HookType `xorm:"VARCHAR(16) 'type'"`
+ Meta string `xorm:"TEXT"` // store hook-specific attributes
+ LastStatus webhook_module.HookStatus // Last delivery status
+
+ // HeaderAuthorizationEncrypted should be accessed using HeaderAuthorization() and SetHeaderAuthorization()
+ HeaderAuthorizationEncrypted string `xorm:"TEXT"`
+
+ CreatedUnix timeutil.TimeStamp `xorm:"INDEX created"`
+ UpdatedUnix timeutil.TimeStamp `xorm:"INDEX updated"`
+}
+
+func init() {
+ db.RegisterModel(new(Webhook))
+}
+
+// AfterLoad updates the webhook object upon setting a column
+func (w *Webhook) AfterLoad() {
+ w.HookEvent = &webhook_module.HookEvent{}
+ if err := json.Unmarshal([]byte(w.Events), w.HookEvent); err != nil {
+ log.Error("Unmarshal[%d]: %v", w.ID, err)
+ }
+}
+
+// History returns history of webhook by given conditions.
+func (w *Webhook) History(ctx context.Context, page int) ([]*HookTask, error) {
+ return HookTasks(ctx, w.ID, page)
+}
+
+// UpdateEvent handles conversion from HookEvent to Events.
+func (w *Webhook) UpdateEvent() error {
+ data, err := json.Marshal(w.HookEvent)
+ w.Events = string(data)
+ return err
+}
+
+// HasCreateEvent returns true if hook enabled create event.
+func (w *Webhook) HasCreateEvent() bool {
+ return w.SendEverything ||
+ (w.ChooseEvents && w.HookEvents.Create)
+}
+
+// HasDeleteEvent returns true if hook enabled delete event.
+func (w *Webhook) HasDeleteEvent() bool {
+ return w.SendEverything ||
+ (w.ChooseEvents && w.HookEvents.Delete)
+}
+
+// HasForkEvent returns true if hook enabled fork event.
+func (w *Webhook) HasForkEvent() bool {
+ return w.SendEverything ||
+ (w.ChooseEvents && w.HookEvents.Fork)
+}
+
+// HasIssuesEvent returns true if hook enabled issues event.
+func (w *Webhook) HasIssuesEvent() bool {
+ return w.SendEverything ||
+ (w.ChooseEvents && w.HookEvents.Issues)
+}
+
+// HasIssuesAssignEvent returns true if hook enabled issues assign event.
+func (w *Webhook) HasIssuesAssignEvent() bool {
+ return w.SendEverything ||
+ (w.ChooseEvents && w.HookEvents.IssueAssign)
+}
+
+// HasIssuesLabelEvent returns true if hook enabled issues label event.
+func (w *Webhook) HasIssuesLabelEvent() bool {
+ return w.SendEverything ||
+ (w.ChooseEvents && w.HookEvents.IssueLabel)
+}
+
+// HasIssuesMilestoneEvent returns true if hook enabled issues milestone event.
+func (w *Webhook) HasIssuesMilestoneEvent() bool {
+ return w.SendEverything ||
+ (w.ChooseEvents && w.HookEvents.IssueMilestone)
+}
+
+// HasIssueCommentEvent returns true if hook enabled issue_comment event.
+func (w *Webhook) HasIssueCommentEvent() bool {
+ return w.SendEverything ||
+ (w.ChooseEvents && w.HookEvents.IssueComment)
+}
+
+// HasPushEvent returns true if hook enabled push event.
+func (w *Webhook) HasPushEvent() bool {
+ return w.PushOnly || w.SendEverything ||
+ (w.ChooseEvents && w.HookEvents.Push)
+}
+
+// HasPullRequestEvent returns true if hook enabled pull request event.
+func (w *Webhook) HasPullRequestEvent() bool {
+ return w.SendEverything ||
+ (w.ChooseEvents && w.HookEvents.PullRequest)
+}
+
+// HasPullRequestAssignEvent returns true if hook enabled pull request assign event.
+func (w *Webhook) HasPullRequestAssignEvent() bool {
+ return w.SendEverything ||
+ (w.ChooseEvents && w.HookEvents.PullRequestAssign)
+}
+
+// HasPullRequestLabelEvent returns true if hook enabled pull request label event.
+func (w *Webhook) HasPullRequestLabelEvent() bool {
+ return w.SendEverything ||
+ (w.ChooseEvents && w.HookEvents.PullRequestLabel)
+}
+
+// HasPullRequestMilestoneEvent returns true if hook enabled pull request milestone event.
+func (w *Webhook) HasPullRequestMilestoneEvent() bool {
+ return w.SendEverything ||
+ (w.ChooseEvents && w.HookEvents.PullRequestMilestone)
+}
+
+// HasPullRequestCommentEvent returns true if hook enabled pull_request_comment event.
+func (w *Webhook) HasPullRequestCommentEvent() bool {
+ return w.SendEverything ||
+ (w.ChooseEvents && w.HookEvents.PullRequestComment)
+}
+
+// HasPullRequestApprovedEvent returns true if hook enabled pull request review event.
+func (w *Webhook) HasPullRequestApprovedEvent() bool {
+ return w.SendEverything ||
+ (w.ChooseEvents && w.HookEvents.PullRequestReview)
+}
+
+// HasPullRequestRejectedEvent returns true if hook enabled pull request review event.
+func (w *Webhook) HasPullRequestRejectedEvent() bool {
+ return w.SendEverything ||
+ (w.ChooseEvents && w.HookEvents.PullRequestReview)
+}
+
+// HasPullRequestReviewCommentEvent returns true if hook enabled pull request review event.
+func (w *Webhook) HasPullRequestReviewCommentEvent() bool {
+ return w.SendEverything ||
+ (w.ChooseEvents && w.HookEvents.PullRequestReview)
+}
+
+// HasPullRequestSyncEvent returns true if hook enabled pull request sync event.
+func (w *Webhook) HasPullRequestSyncEvent() bool {
+ return w.SendEverything ||
+ (w.ChooseEvents && w.HookEvents.PullRequestSync)
+}
+
+// HasWikiEvent returns true if hook enabled wiki event.
+func (w *Webhook) HasWikiEvent() bool {
+ return w.SendEverything ||
+ (w.ChooseEvents && w.HookEvent.Wiki)
+}
+
+// HasReleaseEvent returns if hook enabled release event.
+func (w *Webhook) HasReleaseEvent() bool {
+ return w.SendEverything ||
+ (w.ChooseEvents && w.HookEvents.Release)
+}
+
+// HasRepositoryEvent returns if hook enabled repository event.
+func (w *Webhook) HasRepositoryEvent() bool {
+ return w.SendEverything ||
+ (w.ChooseEvents && w.HookEvents.Repository)
+}
+
+// HasPackageEvent returns if hook enabled package event.
+func (w *Webhook) HasPackageEvent() bool {
+ return w.SendEverything ||
+ (w.ChooseEvents && w.HookEvents.Package)
+}
+
+// HasPullRequestReviewRequestEvent returns true if hook enabled pull request review request event.
+func (w *Webhook) HasPullRequestReviewRequestEvent() bool {
+ return w.SendEverything ||
+ (w.ChooseEvents && w.HookEvents.PullRequestReviewRequest)
+}
+
+// EventCheckers returns event checkers
+func (w *Webhook) EventCheckers() []struct {
+ Has func() bool
+ Type webhook_module.HookEventType
+} {
+ return []struct {
+ Has func() bool
+ Type webhook_module.HookEventType
+ }{
+ {w.HasCreateEvent, webhook_module.HookEventCreate},
+ {w.HasDeleteEvent, webhook_module.HookEventDelete},
+ {w.HasForkEvent, webhook_module.HookEventFork},
+ {w.HasPushEvent, webhook_module.HookEventPush},
+ {w.HasIssuesEvent, webhook_module.HookEventIssues},
+ {w.HasIssuesAssignEvent, webhook_module.HookEventIssueAssign},
+ {w.HasIssuesLabelEvent, webhook_module.HookEventIssueLabel},
+ {w.HasIssuesMilestoneEvent, webhook_module.HookEventIssueMilestone},
+ {w.HasIssueCommentEvent, webhook_module.HookEventIssueComment},
+ {w.HasPullRequestEvent, webhook_module.HookEventPullRequest},
+ {w.HasPullRequestAssignEvent, webhook_module.HookEventPullRequestAssign},
+ {w.HasPullRequestLabelEvent, webhook_module.HookEventPullRequestLabel},
+ {w.HasPullRequestMilestoneEvent, webhook_module.HookEventPullRequestMilestone},
+ {w.HasPullRequestCommentEvent, webhook_module.HookEventPullRequestComment},
+ {w.HasPullRequestApprovedEvent, webhook_module.HookEventPullRequestReviewApproved},
+ {w.HasPullRequestRejectedEvent, webhook_module.HookEventPullRequestReviewRejected},
+ {w.HasPullRequestCommentEvent, webhook_module.HookEventPullRequestReviewComment},
+ {w.HasPullRequestSyncEvent, webhook_module.HookEventPullRequestSync},
+ {w.HasWikiEvent, webhook_module.HookEventWiki},
+ {w.HasRepositoryEvent, webhook_module.HookEventRepository},
+ {w.HasReleaseEvent, webhook_module.HookEventRelease},
+ {w.HasPackageEvent, webhook_module.HookEventPackage},
+ {w.HasPullRequestReviewRequestEvent, webhook_module.HookEventPullRequestReviewRequest},
+ }
+}
+
+// EventsArray returns an array of hook events
+func (w *Webhook) EventsArray() []string {
+ events := make([]string, 0, 7)
+
+ for _, c := range w.EventCheckers() {
+ if c.Has() {
+ events = append(events, string(c.Type))
+ }
+ }
+ return events
+}
+
+// HeaderAuthorization returns the decrypted Authorization header.
+// Not on the reference (*w), to be accessible on WebhooksNew.
+func (w Webhook) HeaderAuthorization() (string, error) {
+ if w.HeaderAuthorizationEncrypted == "" {
+ return "", nil
+ }
+ return secret.DecryptSecret(setting.SecretKey, w.HeaderAuthorizationEncrypted)
+}
+
+// HeaderAuthorizationTrimPrefix returns the decrypted Authorization with a specified prefix trimmed.
+func (w Webhook) HeaderAuthorizationTrimPrefix(prefix string) (string, error) {
+ s, err := w.HeaderAuthorization()
+ if err != nil {
+ return "", err
+ }
+ return strings.TrimPrefix(s, prefix), nil
+}
+
+// SetHeaderAuthorization encrypts and sets the Authorization header.
+func (w *Webhook) SetHeaderAuthorization(cleartext string) error {
+ if cleartext == "" {
+ w.HeaderAuthorizationEncrypted = ""
+ return nil
+ }
+ ciphertext, err := secret.EncryptSecret(setting.SecretKey, cleartext)
+ if err != nil {
+ return err
+ }
+ w.HeaderAuthorizationEncrypted = ciphertext
+ return nil
+}
+
+// CreateWebhook creates a new web hook.
+func CreateWebhook(ctx context.Context, w *Webhook) error {
+ w.Type = strings.TrimSpace(w.Type)
+ return db.Insert(ctx, w)
+}
+
+// CreateWebhooks creates multiple web hooks
+func CreateWebhooks(ctx context.Context, ws []*Webhook) error {
+ // xorm returns err "no element on slice when insert" for empty slices.
+ if len(ws) == 0 {
+ return nil
+ }
+ for i := 0; i < len(ws); i++ {
+ ws[i].Type = strings.TrimSpace(ws[i].Type)
+ }
+ return db.Insert(ctx, ws)
+}
+
+// GetWebhookByID returns webhook of repository by given ID.
+func GetWebhookByID(ctx context.Context, id int64) (*Webhook, error) {
+ bean := new(Webhook)
+ has, err := db.GetEngine(ctx).ID(id).Get(bean)
+ if err != nil {
+ return nil, err
+ } else if !has {
+ return nil, ErrWebhookNotExist{ID: id}
+ }
+ return bean, nil
+}
+
+// GetWebhookByRepoID returns webhook of repository by given ID.
+func GetWebhookByRepoID(ctx context.Context, repoID, id int64) (*Webhook, error) {
+ webhook := new(Webhook)
+ has, err := db.GetEngine(ctx).Where("id=? AND repo_id=?", id, repoID).Get(webhook)
+ if err != nil {
+ return nil, err
+ } else if !has {
+ return nil, ErrWebhookNotExist{ID: id}
+ }
+ return webhook, nil
+}
+
+// GetWebhookByOwnerID returns webhook of a user or organization by given ID.
+func GetWebhookByOwnerID(ctx context.Context, ownerID, id int64) (*Webhook, error) {
+ webhook := new(Webhook)
+ has, err := db.GetEngine(ctx).Where("id=? AND owner_id=?", id, ownerID).Get(webhook)
+ if err != nil {
+ return nil, err
+ } else if !has {
+ return nil, ErrWebhookNotExist{ID: id}
+ }
+ return webhook, nil
+}
+
+// ListWebhookOptions are options to filter webhooks on ListWebhooksByOpts
+type ListWebhookOptions struct {
+ db.ListOptions
+ RepoID int64
+ OwnerID int64
+ IsActive optional.Option[bool]
+}
+
+func (opts ListWebhookOptions) ToConds() builder.Cond {
+ cond := builder.NewCond()
+ if opts.RepoID != 0 {
+ cond = cond.And(builder.Eq{"webhook.repo_id": opts.RepoID})
+ }
+ if opts.OwnerID != 0 {
+ cond = cond.And(builder.Eq{"webhook.owner_id": opts.OwnerID})
+ }
+ if opts.IsActive.Has() {
+ cond = cond.And(builder.Eq{"webhook.is_active": opts.IsActive.Value()})
+ }
+ return cond
+}
+
+var _ db.FindOptionsOrder = ListWebhookOptions{}
+
+// ToOrders implements db.FindOptionsOrder, to sort the webhooks by id asc
+func (opts ListWebhookOptions) ToOrders() string {
+ return "webhook.id"
+}
+
+// UpdateWebhook updates information of webhook.
+func UpdateWebhook(ctx context.Context, w *Webhook) error {
+ _, err := db.GetEngine(ctx).ID(w.ID).AllCols().Update(w)
+ return err
+}
+
+// UpdateWebhookLastStatus updates last status of webhook.
+func UpdateWebhookLastStatus(ctx context.Context, w *Webhook) error {
+ _, err := db.GetEngine(ctx).ID(w.ID).Cols("last_status").Update(w)
+ return err
+}
+
+// DeleteWebhookByID uses argument bean as query condition,
+// ID must be specified and do not assign unnecessary fields.
+func DeleteWebhookByID(ctx context.Context, id int64) (err error) {
+ ctx, committer, err := db.TxContext(ctx)
+ if err != nil {
+ return err
+ }
+ defer committer.Close()
+
+ if count, err := db.DeleteByID[Webhook](ctx, id); err != nil {
+ return err
+ } else if count == 0 {
+ return ErrWebhookNotExist{ID: id}
+ } else if _, err = db.DeleteByBean(ctx, &HookTask{HookID: id}); err != nil {
+ return err
+ }
+
+ return committer.Commit()
+}
+
+// DeleteWebhookByRepoID deletes webhook of repository by given ID.
+func DeleteWebhookByRepoID(ctx context.Context, repoID, id int64) error {
+ if _, err := GetWebhookByRepoID(ctx, repoID, id); err != nil {
+ return err
+ }
+ return DeleteWebhookByID(ctx, id)
+}
+
+// DeleteWebhookByOwnerID deletes webhook of a user or organization by given ID.
+func DeleteWebhookByOwnerID(ctx context.Context, ownerID, id int64) error {
+ if _, err := GetWebhookByOwnerID(ctx, ownerID, id); err != nil {
+ return err
+ }
+ return DeleteWebhookByID(ctx, id)
+}
diff --git a/models/webhook/webhook_system.go b/models/webhook/webhook_system.go
new file mode 100644
index 0000000..62e8286
--- /dev/null
+++ b/models/webhook/webhook_system.go
@@ -0,0 +1,83 @@
+// Copyright 2021 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package webhook
+
+import (
+ "context"
+ "fmt"
+
+ "code.gitea.io/gitea/models/db"
+)
+
+// GetDefaultWebhooks returns all admin-default webhooks.
+func GetDefaultWebhooks(ctx context.Context) ([]*Webhook, error) {
+ return getAdminWebhooks(ctx, false)
+}
+
+// GetSystemOrDefaultWebhook returns admin system or default webhook by given ID.
+func GetSystemOrDefaultWebhook(ctx context.Context, id int64) (*Webhook, error) {
+ webhook := &Webhook{ID: id}
+ has, err := db.GetEngine(ctx).
+ Where("repo_id=? AND owner_id=?", 0, 0).
+ Get(webhook)
+ if err != nil {
+ return nil, err
+ } else if !has {
+ return nil, ErrWebhookNotExist{ID: id}
+ }
+ return webhook, nil
+}
+
+// GetSystemWebhooks returns all admin system webhooks.
+func GetSystemWebhooks(ctx context.Context, onlyActive bool) ([]*Webhook, error) {
+ return getAdminWebhooks(ctx, true, onlyActive)
+}
+
+func getAdminWebhooks(ctx context.Context, systemWebhooks bool, onlyActive ...bool) ([]*Webhook, error) {
+ webhooks := make([]*Webhook, 0, 5)
+ if len(onlyActive) > 0 && onlyActive[0] {
+ return webhooks, db.GetEngine(ctx).
+ Where("repo_id=? AND owner_id=? AND is_system_webhook=? AND is_active = ?", 0, 0, systemWebhooks, true).
+ OrderBy("id").
+ Find(&webhooks)
+ }
+ return webhooks, db.GetEngine(ctx).
+ Where("repo_id=? AND owner_id=? AND is_system_webhook=?", 0, 0, systemWebhooks).
+ OrderBy("id").
+ Find(&webhooks)
+}
+
+// DeleteDefaultSystemWebhook deletes an admin-configured default or system webhook (where Org and Repo ID both 0)
+func DeleteDefaultSystemWebhook(ctx context.Context, id int64) error {
+ return db.WithTx(ctx, func(ctx context.Context) error {
+ count, err := db.GetEngine(ctx).
+ Where("repo_id=? AND owner_id=?", 0, 0).
+ Delete(&Webhook{ID: id})
+ if err != nil {
+ return err
+ } else if count == 0 {
+ return ErrWebhookNotExist{ID: id}
+ }
+
+ _, err = db.DeleteByBean(ctx, &HookTask{HookID: id})
+ return err
+ })
+}
+
+// CopyDefaultWebhooksToRepo creates copies of the default webhooks in a new repo
+func CopyDefaultWebhooksToRepo(ctx context.Context, repoID int64) error {
+ ws, err := GetDefaultWebhooks(ctx)
+ if err != nil {
+ return fmt.Errorf("GetDefaultWebhooks: %v", err)
+ }
+
+ for _, w := range ws {
+ w.ID = 0
+ w.RepoID = repoID
+ if err := CreateWebhook(ctx, w); err != nil {
+ return fmt.Errorf("CreateWebhook: %v", err)
+ }
+ }
+ return nil
+}
diff --git a/models/webhook/webhook_test.go b/models/webhook/webhook_test.go
new file mode 100644
index 0000000..848440b
--- /dev/null
+++ b/models/webhook/webhook_test.go
@@ -0,0 +1,350 @@
+// Copyright 2017 The Gitea Authors. All rights reserved.
+// SPDX-License-Identifier: MIT
+
+package webhook
+
+import (
+ "context"
+ "testing"
+ "time"
+
+ "code.gitea.io/gitea/models/db"
+ "code.gitea.io/gitea/models/unittest"
+ "code.gitea.io/gitea/modules/json"
+ "code.gitea.io/gitea/modules/optional"
+ "code.gitea.io/gitea/modules/timeutil"
+ webhook_module "code.gitea.io/gitea/modules/webhook"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+func TestHookContentType_Name(t *testing.T) {
+ assert.Equal(t, "json", ContentTypeJSON.Name())
+ assert.Equal(t, "form", ContentTypeForm.Name())
+}
+
+func TestIsValidHookContentType(t *testing.T) {
+ assert.True(t, IsValidHookContentType("json"))
+ assert.True(t, IsValidHookContentType("form"))
+ assert.False(t, IsValidHookContentType("invalid"))
+}
+
+func TestWebhook_History(t *testing.T) {
+ require.NoError(t, unittest.PrepareTestDatabase())
+ webhook := unittest.AssertExistsAndLoadBean(t, &Webhook{ID: 1})
+ tasks, err := webhook.History(db.DefaultContext, 0)
+ require.NoError(t, err)
+ if assert.Len(t, tasks, 3) {
+ assert.Equal(t, int64(3), tasks[0].ID)
+ assert.Equal(t, int64(2), tasks[1].ID)
+ assert.Equal(t, int64(1), tasks[2].ID)
+ }
+
+ webhook = unittest.AssertExistsAndLoadBean(t, &Webhook{ID: 2})
+ tasks, err = webhook.History(db.DefaultContext, 0)
+ require.NoError(t, err)
+ assert.Empty(t, tasks)
+}
+
+func TestWebhook_UpdateEvent(t *testing.T) {
+ require.NoError(t, unittest.PrepareTestDatabase())
+ webhook := unittest.AssertExistsAndLoadBean(t, &Webhook{ID: 1})
+ hookEvent := &webhook_module.HookEvent{
+ PushOnly: true,
+ SendEverything: false,
+ ChooseEvents: false,
+ HookEvents: webhook_module.HookEvents{
+ Create: false,
+ Push: true,
+ PullRequest: false,
+ },
+ }
+ webhook.HookEvent = hookEvent
+ require.NoError(t, webhook.UpdateEvent())
+ assert.NotEmpty(t, webhook.Events)
+ actualHookEvent := &webhook_module.HookEvent{}
+ require.NoError(t, json.Unmarshal([]byte(webhook.Events), actualHookEvent))
+ assert.Equal(t, *hookEvent, *actualHookEvent)
+}
+
+func TestWebhook_EventsArray(t *testing.T) {
+ assert.Equal(t, []string{
+ "create", "delete", "fork", "push",
+ "issues", "issue_assign", "issue_label", "issue_milestone", "issue_comment",
+ "pull_request", "pull_request_assign", "pull_request_label", "pull_request_milestone",
+ "pull_request_comment", "pull_request_review_approved", "pull_request_review_rejected",
+ "pull_request_review_comment", "pull_request_sync", "wiki", "repository", "release",
+ "package", "pull_request_review_request",
+ },
+ (&Webhook{
+ HookEvent: &webhook_module.HookEvent{SendEverything: true},
+ }).EventsArray(),
+ )
+
+ assert.Equal(t, []string{"push"},
+ (&Webhook{
+ HookEvent: &webhook_module.HookEvent{PushOnly: true},
+ }).EventsArray(),
+ )
+}
+
+func TestCreateWebhook(t *testing.T) {
+ hook := &Webhook{
+ RepoID: 3,
+ URL: "www.example.com/unit_test",
+ ContentType: ContentTypeJSON,
+ Events: `{"push_only":false,"send_everything":false,"choose_events":false,"events":{"create":false,"push":true,"pull_request":true}}`,
+ }
+ unittest.AssertNotExistsBean(t, hook)
+ require.NoError(t, CreateWebhook(db.DefaultContext, hook))
+ unittest.AssertExistsAndLoadBean(t, hook)
+}
+
+func TestGetWebhookByRepoID(t *testing.T) {
+ require.NoError(t, unittest.PrepareTestDatabase())
+ hook, err := GetWebhookByRepoID(db.DefaultContext, 1, 1)
+ require.NoError(t, err)
+ assert.Equal(t, int64(1), hook.ID)
+
+ _, err = GetWebhookByRepoID(db.DefaultContext, unittest.NonexistentID, unittest.NonexistentID)
+ require.Error(t, err)
+ assert.True(t, IsErrWebhookNotExist(err))
+}
+
+func TestGetWebhookByOwnerID(t *testing.T) {
+ require.NoError(t, unittest.PrepareTestDatabase())
+ hook, err := GetWebhookByOwnerID(db.DefaultContext, 3, 3)
+ require.NoError(t, err)
+ assert.Equal(t, int64(3), hook.ID)
+
+ _, err = GetWebhookByOwnerID(db.DefaultContext, unittest.NonexistentID, unittest.NonexistentID)
+ require.Error(t, err)
+ assert.True(t, IsErrWebhookNotExist(err))
+}
+
+func TestGetActiveWebhooksByRepoID(t *testing.T) {
+ require.NoError(t, unittest.PrepareTestDatabase())
+
+ activateWebhook(t, 1)
+
+ hooks, err := db.Find[Webhook](db.DefaultContext, ListWebhookOptions{RepoID: 1, IsActive: optional.Some(true)})
+ require.NoError(t, err)
+ if assert.Len(t, hooks, 1) {
+ assert.Equal(t, int64(1), hooks[0].ID)
+ assert.True(t, hooks[0].IsActive)
+ }
+}
+
+func TestGetWebhooksByRepoID(t *testing.T) {
+ require.NoError(t, unittest.PrepareTestDatabase())
+ hooks, err := db.Find[Webhook](db.DefaultContext, ListWebhookOptions{RepoID: 1})
+ require.NoError(t, err)
+ if assert.Len(t, hooks, 2) {
+ assert.Equal(t, int64(1), hooks[0].ID)
+ assert.Equal(t, int64(2), hooks[1].ID)
+ }
+}
+
+func TestGetActiveWebhooksByOwnerID(t *testing.T) {
+ require.NoError(t, unittest.PrepareTestDatabase())
+
+ activateWebhook(t, 3)
+
+ hooks, err := db.Find[Webhook](db.DefaultContext, ListWebhookOptions{OwnerID: 3, IsActive: optional.Some(true)})
+ require.NoError(t, err)
+ if assert.Len(t, hooks, 1) {
+ assert.Equal(t, int64(3), hooks[0].ID)
+ assert.True(t, hooks[0].IsActive)
+ }
+}
+
+func activateWebhook(t *testing.T, hookID int64) {
+ t.Helper()
+ updated, err := db.GetEngine(db.DefaultContext).ID(hookID).Cols("is_active").Update(Webhook{IsActive: true})
+ assert.Equal(t, int64(1), updated)
+ require.NoError(t, err)
+}
+
+func TestGetWebhooksByOwnerID(t *testing.T) {
+ require.NoError(t, unittest.PrepareTestDatabase())
+
+ activateWebhook(t, 3)
+
+ hooks, err := db.Find[Webhook](db.DefaultContext, ListWebhookOptions{OwnerID: 3})
+ require.NoError(t, err)
+ if assert.Len(t, hooks, 1) {
+ assert.Equal(t, int64(3), hooks[0].ID)
+ assert.True(t, hooks[0].IsActive)
+ }
+}
+
+func TestUpdateWebhook(t *testing.T) {
+ require.NoError(t, unittest.PrepareTestDatabase())
+ hook := unittest.AssertExistsAndLoadBean(t, &Webhook{ID: 2})
+ hook.IsActive = true
+ hook.ContentType = ContentTypeForm
+ unittest.AssertNotExistsBean(t, hook)
+ require.NoError(t, UpdateWebhook(db.DefaultContext, hook))
+ unittest.AssertExistsAndLoadBean(t, hook)
+}
+
+func TestDeleteWebhookByRepoID(t *testing.T) {
+ require.NoError(t, unittest.PrepareTestDatabase())
+ unittest.AssertExistsAndLoadBean(t, &Webhook{ID: 2, RepoID: 1})
+ require.NoError(t, DeleteWebhookByRepoID(db.DefaultContext, 1, 2))
+ unittest.AssertNotExistsBean(t, &Webhook{ID: 2, RepoID: 1})
+
+ err := DeleteWebhookByRepoID(db.DefaultContext, unittest.NonexistentID, unittest.NonexistentID)
+ require.Error(t, err)
+ assert.True(t, IsErrWebhookNotExist(err))
+}
+
+func TestDeleteWebhookByOwnerID(t *testing.T) {
+ require.NoError(t, unittest.PrepareTestDatabase())
+ unittest.AssertExistsAndLoadBean(t, &Webhook{ID: 3, OwnerID: 3})
+ require.NoError(t, DeleteWebhookByOwnerID(db.DefaultContext, 3, 3))
+ unittest.AssertNotExistsBean(t, &Webhook{ID: 3, OwnerID: 3})
+
+ err := DeleteWebhookByOwnerID(db.DefaultContext, unittest.NonexistentID, unittest.NonexistentID)
+ require.Error(t, err)
+ assert.True(t, IsErrWebhookNotExist(err))
+}
+
+func TestHookTasks(t *testing.T) {
+ require.NoError(t, unittest.PrepareTestDatabase())
+ hookTasks, err := HookTasks(db.DefaultContext, 1, 1)
+ require.NoError(t, err)
+ if assert.Len(t, hookTasks, 3) {
+ assert.Equal(t, int64(3), hookTasks[0].ID)
+ assert.Equal(t, int64(2), hookTasks[1].ID)
+ assert.Equal(t, int64(1), hookTasks[2].ID)
+ }
+
+ hookTasks, err = HookTasks(db.DefaultContext, unittest.NonexistentID, 1)
+ require.NoError(t, err)
+ assert.Empty(t, hookTasks)
+}
+
+func TestCreateHookTask(t *testing.T) {
+ require.NoError(t, unittest.PrepareTestDatabase())
+ hookTask := &HookTask{
+ HookID: 3,
+ PayloadVersion: 2,
+ }
+ unittest.AssertNotExistsBean(t, hookTask)
+ _, err := CreateHookTask(db.DefaultContext, hookTask)
+ require.NoError(t, err)
+ unittest.AssertExistsAndLoadBean(t, hookTask)
+}
+
+func TestUpdateHookTask(t *testing.T) {
+ require.NoError(t, unittest.PrepareTestDatabase())
+
+ hook := unittest.AssertExistsAndLoadBean(t, &HookTask{ID: 1})
+ hook.PayloadContent = "new payload content"
+ hook.IsDelivered = true
+ unittest.AssertNotExistsBean(t, hook)
+ require.NoError(t, UpdateHookTask(db.DefaultContext, hook))
+ unittest.AssertExistsAndLoadBean(t, hook)
+}
+
+func TestCleanupHookTaskTable_PerWebhook_DeletesDelivered(t *testing.T) {
+ require.NoError(t, unittest.PrepareTestDatabase())
+ hookTask := &HookTask{
+ HookID: 3,
+ IsDelivered: true,
+ Delivered: timeutil.TimeStampNanoNow(),
+ PayloadVersion: 2,
+ }
+ unittest.AssertNotExistsBean(t, hookTask)
+ _, err := CreateHookTask(db.DefaultContext, hookTask)
+ require.NoError(t, err)
+ unittest.AssertExistsAndLoadBean(t, hookTask)
+
+ require.NoError(t, CleanupHookTaskTable(context.Background(), PerWebhook, 168*time.Hour, 0))
+ unittest.AssertNotExistsBean(t, hookTask)
+}
+
+func TestCleanupHookTaskTable_PerWebhook_LeavesUndelivered(t *testing.T) {
+ require.NoError(t, unittest.PrepareTestDatabase())
+ hookTask := &HookTask{
+ HookID: 4,
+ IsDelivered: false,
+ PayloadVersion: 2,
+ }
+ unittest.AssertNotExistsBean(t, hookTask)
+ _, err := CreateHookTask(db.DefaultContext, hookTask)
+ require.NoError(t, err)
+ unittest.AssertExistsAndLoadBean(t, hookTask)
+
+ require.NoError(t, CleanupHookTaskTable(context.Background(), PerWebhook, 168*time.Hour, 0))
+ unittest.AssertExistsAndLoadBean(t, hookTask)
+}
+
+func TestCleanupHookTaskTable_PerWebhook_LeavesMostRecentTask(t *testing.T) {
+ require.NoError(t, unittest.PrepareTestDatabase())
+ hookTask := &HookTask{
+ HookID: 4,
+ IsDelivered: true,
+ Delivered: timeutil.TimeStampNanoNow(),
+ PayloadVersion: 2,
+ }
+ unittest.AssertNotExistsBean(t, hookTask)
+ _, err := CreateHookTask(db.DefaultContext, hookTask)
+ require.NoError(t, err)
+ unittest.AssertExistsAndLoadBean(t, hookTask)
+
+ require.NoError(t, CleanupHookTaskTable(context.Background(), PerWebhook, 168*time.Hour, 1))
+ unittest.AssertExistsAndLoadBean(t, hookTask)
+}
+
+func TestCleanupHookTaskTable_OlderThan_DeletesDelivered(t *testing.T) {
+ require.NoError(t, unittest.PrepareTestDatabase())
+ hookTask := &HookTask{
+ HookID: 3,
+ IsDelivered: true,
+ Delivered: timeutil.TimeStampNano(time.Now().AddDate(0, 0, -8).UnixNano()),
+ PayloadVersion: 2,
+ }
+ unittest.AssertNotExistsBean(t, hookTask)
+ _, err := CreateHookTask(db.DefaultContext, hookTask)
+ require.NoError(t, err)
+ unittest.AssertExistsAndLoadBean(t, hookTask)
+
+ require.NoError(t, CleanupHookTaskTable(context.Background(), OlderThan, 168*time.Hour, 0))
+ unittest.AssertNotExistsBean(t, hookTask)
+}
+
+func TestCleanupHookTaskTable_OlderThan_LeavesUndelivered(t *testing.T) {
+ require.NoError(t, unittest.PrepareTestDatabase())
+ hookTask := &HookTask{
+ HookID: 4,
+ IsDelivered: false,
+ PayloadVersion: 2,
+ }
+ unittest.AssertNotExistsBean(t, hookTask)
+ _, err := CreateHookTask(db.DefaultContext, hookTask)
+ require.NoError(t, err)
+ unittest.AssertExistsAndLoadBean(t, hookTask)
+
+ require.NoError(t, CleanupHookTaskTable(context.Background(), OlderThan, 168*time.Hour, 0))
+ unittest.AssertExistsAndLoadBean(t, hookTask)
+}
+
+func TestCleanupHookTaskTable_OlderThan_LeavesTaskEarlierThanAgeToDelete(t *testing.T) {
+ require.NoError(t, unittest.PrepareTestDatabase())
+ hookTask := &HookTask{
+ HookID: 4,
+ IsDelivered: true,
+ Delivered: timeutil.TimeStampNano(time.Now().AddDate(0, 0, -6).UnixNano()),
+ PayloadVersion: 2,
+ }
+ unittest.AssertNotExistsBean(t, hookTask)
+ _, err := CreateHookTask(db.DefaultContext, hookTask)
+ require.NoError(t, err)
+ unittest.AssertExistsAndLoadBean(t, hookTask)
+
+ require.NoError(t, CleanupHookTaskTable(context.Background(), OlderThan, 168*time.Hour, 0))
+ unittest.AssertExistsAndLoadBean(t, hookTask)
+}