mirror of
https://github.com/go-gitea/gitea.git
synced 2026-10-26 06:16:56 +09:00
feat(api): add REST API endpoints for repository webhook delivery history (#38775)
Webhook delivery history (request/response, status, timestamp) was only
reachable through the repo settings UI, which shows the last ~10
deliveries with no filtering. There was no way to fetch this data
programmatically for debugging integrations or auditing webhook
activity.
This adds two read endpoints at the repository level:
- `GET /repos/{owner}/{repo}/hooks/{id}/deliveries` — paginated list of
a hook's deliveries, most recent first
- `GET /repos/{owner}/{repo}/hooks/{id}/deliveries/{uuid}` — a single
delivery, including request/response headers, body, and status
Both reuse the existing `hook_task` storage and retention policy (the
`CleanupHookTaskTable` cron job) that already back the "Recent
Deliveries" web UI, and are gated by the same `reqAdmin()` +
`reqWebhooksEnabled()` guard as the existing hook endpoints.
Org, user, and admin/system-level webhooks are intentionally out of
scope for this PR to keep it reviewable; they can follow as separate
PRs using the same pattern.
## Usage
```bash
curl -H "Authorization: token $TOKEN" \
https://gitea.example.com/api/v1/repos/{owner}/{repo}/hooks/{id}/deliveries
curl -H "Authorization: token $TOKEN" \
https://gitea.example.com/api/v1/repos/{owner}/{repo}/hooks/{id}/deliveries/{uuid}
```
## Testing
- Unit tests for the new model-layer query functions
(`ListHookTasks`, `GetHookTaskByUUID`)
- Integration tests covering: list + get, an undelivered task's response
is `null`, a hook/delivery UUID from another repository 404s instead
of leaking across repos, and pagination (`page`/`limit`, `Link` and
`X-Total-Count` headers, correct ordering/no overlap across pages)
- `make lint-go`, `make generate-swagger`
Fixes: https://github.com/go-gitea/gitea/issues/38463.
Co-authored-by: bircni <bircni@icloud.com>
Co-authored-by: silverwind <me@silverwind.io>
This commit is contained in:
11 files changed
+824
-6
No files matched your search
@@ -12,10 +12,13 @@ import (
|
||||
repo_model "gitea.dev/models/repo"
|
||||
"gitea.dev/models/unittest"
|
||||
user_model "gitea.dev/models/user"
|
||||
webhook_model "gitea.dev/models/webhook"
|
||||
api "gitea.dev/modules/structs"
|
||||
webhook_module "gitea.dev/modules/webhook"
|
||||
"gitea.dev/tests"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestAPICreateHook(t *testing.T) {
|
||||
@@ -96,3 +99,68 @@ func TestAPICreateHook(t *testing.T) {
|
||||
cleared := DecodeJSON(t, clearResp, &api.Hook{})
|
||||
assert.Empty(t, cleared.Name)
|
||||
}
|
||||
|
||||
func TestAPIListAndGetRepoHookDeliveries(t *testing.T) {
|
||||
defer tests.PrepareTestEnv(t)()
|
||||
|
||||
repo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 37})
|
||||
owner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: repo.OwnerID})
|
||||
|
||||
session := loginUser(t, "user1")
|
||||
token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository)
|
||||
hook := DecodeJSON(t, MakeRequest(t, NewRequestWithJSON(t, "POST", fmt.Sprintf("/api/v1/repos/%s/%s/hooks", owner.Name, repo.Name), api.CreateHookOption{
|
||||
Type: "gitea",
|
||||
Config: api.CreateHookOptionConfig{
|
||||
"content_type": "json",
|
||||
"url": "http://example.com/",
|
||||
},
|
||||
}).AddTokenAuth(token), http.StatusCreated), &api.Hook{})
|
||||
|
||||
tasks := make([]*webhook_model.HookTask, 0, 4)
|
||||
for i := range 4 {
|
||||
task, err := webhook_model.CreateHookTask(t.Context(), &webhook_model.HookTask{
|
||||
HookID: hook.ID,
|
||||
EventType: webhook_module.HookEventPush,
|
||||
PayloadVersion: 2,
|
||||
IsDelivered: i < 3,
|
||||
IsSucceed: i < 3,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
tasks = append(tasks, task)
|
||||
}
|
||||
|
||||
deliveriesURL := fmt.Sprintf("/api/v1/repos/%s/%s/hooks/%d/deliveries", owner.Name, repo.Name, hook.ID)
|
||||
deliveries := DecodeJSON(t, MakeRequest(t, NewRequest(t, "GET", deliveriesURL).AddTokenAuth(token), http.StatusOK), &api.HookDeliveryList{})
|
||||
require.Len(t, *deliveries, 4)
|
||||
assert.Equal(t, tasks[3].UUID, (*deliveries)[0].UUID)
|
||||
assert.False(t, (*deliveries)[0].IsDelivered)
|
||||
assert.Nil(t, (*deliveries)[0].Response)
|
||||
assert.Nil(t, (*deliveries)[0].Delivered)
|
||||
|
||||
for page, expectedUUIDs := range [][]string{{tasks[3].UUID, tasks[2].UUID}, {tasks[1].UUID, tasks[0].UUID}} {
|
||||
t.Run(fmt.Sprintf("page_%d", page+1), func(t *testing.T) {
|
||||
resp := MakeRequest(t, NewRequest(t, "GET", fmt.Sprintf("%s?limit=2&page=%d", deliveriesURL, page+1)).AddTokenAuth(token), http.StatusOK)
|
||||
deliveries := DecodeJSON(t, resp, &api.HookDeliveryList{})
|
||||
require.Len(t, *deliveries, len(expectedUUIDs))
|
||||
assert.Equal(t, "4", resp.Header().Get("X-Total-Count"))
|
||||
for i, uuid := range expectedUUIDs {
|
||||
assert.Equal(t, uuid, (*deliveries)[i].UUID)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
delivery := DecodeJSON(t, MakeRequest(t, NewRequest(t, "GET", deliveriesURL+"/"+tasks[0].UUID).AddTokenAuth(token), http.StatusOK), &api.HookDelivery{})
|
||||
assert.Equal(t, tasks[0].UUID, delivery.UUID)
|
||||
assert.True(t, delivery.IsDelivered)
|
||||
assert.True(t, delivery.IsSucceeded)
|
||||
assert.NotNil(t, delivery.Delivered)
|
||||
|
||||
MakeRequest(t, NewRequest(t, "GET", deliveriesURL+"/does-not-exist").AddTokenAuth(token), http.StatusNotFound)
|
||||
|
||||
otherRepo := unittest.AssertExistsAndLoadBean(t, &repo_model.Repository{ID: 1})
|
||||
otherOwner := unittest.AssertExistsAndLoadBean(t, &user_model.User{ID: otherRepo.OwnerID})
|
||||
for _, suffix := range []string{"", "/" + tasks[0].UUID} {
|
||||
MakeRequest(t, NewRequest(t, "GET", fmt.Sprintf("/api/v1/repos/%s/%s/hooks/%d/deliveries%s", otherOwner.Name, otherRepo.Name, hook.ID, suffix)).
|
||||
AddTokenAuth(token), http.StatusNotFound)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user