mirror of
https://github.com/lukaszraczylo/ghtracker.git
synced 2026-10-12 00:45:38 +00:00
A new pr_open alert carries the head commit SHA of each open, non-draft, same-repository pull request. An action with auto enabled sends its webhook once for each pull request and head commit. An authors list limits which pull requests trigger it. The alert never changes repository health.
145 lines
5.3 KiB
Go
145 lines
5.3 KiB
Go
package web
|
|
|
|
import (
|
|
"io"
|
|
"log/slog"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/prometheus/client_golang/prometheus"
|
|
"github.com/prometheus/client_golang/prometheus/testutil"
|
|
|
|
"github.com/lukaszraczylo/ghtracker/internal/collector"
|
|
"github.com/lukaszraczylo/ghtracker/internal/config"
|
|
"github.com/lukaszraczylo/ghtracker/internal/model"
|
|
)
|
|
|
|
func autoPR(n int, sha, author string, mut func(*model.PullRequest)) model.PullRequest {
|
|
p := model.PullRequest{Number: n, Title: "t", URL: prURL + strings.Repeat("x", n), Author: author, HeadSHA: sha, CreatedAt: now.Add(-time.Hour)}
|
|
if mut != nil {
|
|
mut(&p)
|
|
}
|
|
return p
|
|
}
|
|
|
|
// autoView builds the view through the real alert evaluation so fork and draft rules are exercised.
|
|
func autoView(prs ...model.PullRequest) collector.View {
|
|
r := model.Repo{FullName: "o/r", Up: true, RefreshedAt: now, PRs: prs}
|
|
alerts := collector.Evaluate(&r, now, config.Thresholds{PRWarnAfter: -1, PRCritAfter: -1, IssueStaleAfter: -1, WorkflowStuck: -1, DataStaleAfter: -1})
|
|
return collector.View{Now: now, Repos: []model.Repo{r}, Alerts: alerts}
|
|
}
|
|
|
|
type autoStep struct {
|
|
prs []model.PullRequest
|
|
code int
|
|
wantPosts int32
|
|
}
|
|
|
|
func TestAutoRunner(t *testing.T) {
|
|
fork := func(p *model.PullRequest) { p.Fork = true }
|
|
draft := func(p *model.PullRequest) { p.Draft = true }
|
|
tests := map[string]struct {
|
|
authors []string
|
|
steps []autoStep
|
|
}{
|
|
"fork skipped": {steps: []autoStep{{[]model.PullRequest{autoPR(1, "a", "x", fork)}, 200, 0}}},
|
|
"draft skipped": {steps: []autoStep{{[]model.PullRequest{autoPR(1, "a", "x", draft)}, 200, 0}}},
|
|
"author filter": {authors: []string{"Alice"}, steps: []autoStep{
|
|
{[]model.PullRequest{autoPR(1, "a", "bob", nil), autoPR(2, "b", "alice", nil)}, 200, 1},
|
|
}},
|
|
"once per sha": {steps: []autoStep{
|
|
{[]model.PullRequest{autoPR(1, "a", "x", nil)}, 200, 1},
|
|
{[]model.PullRequest{autoPR(1, "a", "x", nil)}, 200, 0},
|
|
}},
|
|
"new sha resends": {steps: []autoStep{
|
|
{[]model.PullRequest{autoPR(1, "a", "x", nil)}, 200, 1},
|
|
{[]model.PullRequest{autoPR(1, "b", "x", nil)}, 201, 1},
|
|
}},
|
|
"reappearing alert resends": {steps: []autoStep{
|
|
{[]model.PullRequest{autoPR(1, "a", "x", nil)}, 200, 1},
|
|
{nil, 200, 0},
|
|
{[]model.PullRequest{autoPR(1, "a", "x", nil)}, 200, 1},
|
|
}},
|
|
"failure retries": {steps: []autoStep{
|
|
{[]model.PullRequest{autoPR(1, "a", "x", nil)}, 500, 1},
|
|
{[]model.PullRequest{autoPR(1, "a", "x", nil)}, 400, 1},
|
|
{[]model.PullRequest{autoPR(1, "a", "x", nil)}, 200, 1},
|
|
{[]model.PullRequest{autoPR(1, "a", "x", nil)}, 200, 0},
|
|
}},
|
|
"409 retries": {steps: []autoStep{
|
|
{[]model.PullRequest{autoPR(1, "a", "x", nil)}, 409, 1},
|
|
{[]model.PullRequest{autoPR(1, "a", "x", nil)}, 200, 1},
|
|
{[]model.PullRequest{autoPR(1, "a", "x", nil)}, 200, 0},
|
|
}},
|
|
"cap per refresh": {steps: []autoStep{
|
|
{manyPRs(8), 200, maxAutoSends},
|
|
{manyPRs(8), 200, 3},
|
|
{manyPRs(8), 200, 0},
|
|
}},
|
|
}
|
|
for name, tc := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
hook := newFakeHook(t)
|
|
a := NewActions([]config.Action{{ID: "review", Kinds: []string{collector.KindPROpen}, Auto: true, Authors: tc.authors,
|
|
Token: testToken, Webhook: config.Webhook{URL: hook.srv.URL + "/hook", Timeout: 2 * time.Second}}},
|
|
slog.New(slog.NewTextHandler(io.Discard, nil)), prometheus.NewRegistry())
|
|
runner := NewAutoRunner(a)
|
|
for i, st := range tc.steps {
|
|
hook.postCode = st.code
|
|
before := hook.postCalls.Load()
|
|
runner.Run(t.Context(), autoView(st.prs...))
|
|
if got := hook.postCalls.Load() - before; got != st.wantPosts {
|
|
t.Fatalf("step %d: posts = %d, want %d", i, got, st.wantPosts)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func manyPRs(n int) []model.PullRequest {
|
|
out := make([]model.PullRequest, n)
|
|
for i := range out {
|
|
out[i] = autoPR(i+1, "s", "x", nil)
|
|
}
|
|
return out
|
|
}
|
|
|
|
func TestAutoRunnerPayloadAuthAndMetrics(t *testing.T) {
|
|
hook := newFakeHook(t)
|
|
a := NewActions([]config.Action{{ID: "review", Kinds: []string{collector.KindPROpen}, Auto: true,
|
|
Token: testToken, Webhook: config.Webhook{URL: hook.srv.URL + "/hook", Timeout: 2 * time.Second}}},
|
|
slog.New(slog.NewTextHandler(io.Discard, nil)), prometheus.NewRegistry())
|
|
NewAutoRunner(a).Run(t.Context(), autoView(autoPR(1, "abc", "x", nil)))
|
|
hook.mu.Lock()
|
|
defer hook.mu.Unlock()
|
|
if hook.lastAuth != "Bearer "+testToken {
|
|
t.Fatalf("auth = %q", hook.lastAuth)
|
|
}
|
|
for _, want := range []string{`"action":"review"`, `"kind":"pr_open"`, `"ref":"abc"`, `"url":"` + prURL + `x"`} {
|
|
if !strings.Contains(hook.lastBody, want) {
|
|
t.Fatalf("body %s lacks %s", hook.lastBody, want)
|
|
}
|
|
}
|
|
if got := testutil.ToFloat64(a.counter.WithLabelValues("review", resultAutoOK)); got != 1 {
|
|
t.Fatalf("auto_ok = %v, want 1", got)
|
|
}
|
|
}
|
|
|
|
func TestNewAutoRunnerNilWithoutAutoActions(t *testing.T) {
|
|
a := NewActions([]config.Action{{ID: "x", Webhook: config.Webhook{URL: "http://h.test"}}}, slog.New(slog.NewTextHandler(io.Discard, nil)), nil)
|
|
if NewAutoRunner(a) != nil {
|
|
t.Fatal("runner must be nil when no action is auto")
|
|
}
|
|
}
|
|
|
|
func TestStateCarriesPROpenRef(t *testing.T) {
|
|
st := buildState(autoView(autoPR(1, "abc", "x", nil)), true)
|
|
if len(st.Alerts) != 1 || st.Alerts[0].Kind != collector.KindPROpen || st.Alerts[0].Ref != "abc" || st.Alerts[0].Severity != "ok" {
|
|
t.Fatalf("alerts = %+v", st.Alerts)
|
|
}
|
|
if st.Counts.Crit != 0 || st.Counts.Warn != 0 || st.Repos[0].Health != "ok" {
|
|
t.Fatalf("counts/health changed: %+v %s", st.Counts, st.Repos[0].Health)
|
|
}
|
|
}
|