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.
247 lines
8.1 KiB
Go
247 lines
8.1 KiB
Go
package config
|
|
|
|
import (
|
|
"errors"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func noFile(string) ([]byte, error) { return nil, errors.New("no file") }
|
|
func keyFile(string) ([]byte, error) { return []byte("PEM"), nil }
|
|
func env(m map[string]string) func(string) string { return func(k string) string { return m[k] } }
|
|
|
|
func valid() *Config {
|
|
return &Config{GitHub: GitHub{AppID: 1, PrivateKeyFile: "k.pem"}, Repos: []string{"a/b"}}
|
|
}
|
|
|
|
func TestFinalizeDefaults(t *testing.T) {
|
|
c := valid()
|
|
if err := c.finalize(env(nil), keyFile); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if c.RefreshInterval != 6*time.Hour || c.Listen != ":8080" || c.Concurrency != 2 || c.MaxItems != 100 {
|
|
t.Fatalf("defaults wrong: %+v", c)
|
|
}
|
|
if c.ManualRefresh == nil || !*c.ManualRefresh {
|
|
t.Fatal("manual refresh must default to on")
|
|
}
|
|
if c.GitHub.APIURL != DefaultAPIURL || c.GitHub.PrivateKeyPEM != "PEM" {
|
|
t.Fatalf("github defaults wrong: %+v", c.GitHub)
|
|
}
|
|
if want := 12*time.Hour + 30*time.Minute; c.Thresholds.DataStaleAfter != want {
|
|
t.Fatalf("data stale = %s, want %s", c.Thresholds.DataStaleAfter, want)
|
|
}
|
|
}
|
|
|
|
func TestFinalizeEnvKeyWins(t *testing.T) {
|
|
c := valid()
|
|
c.GitHub.PrivateKeyFile = ""
|
|
if err := c.finalize(env(map[string]string{EnvPrivateKey: "ENVPEM"}), noFile); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if c.GitHub.PrivateKeyPEM != "ENVPEM" {
|
|
t.Fatalf("got %q", c.GitHub.PrivateKeyPEM)
|
|
}
|
|
}
|
|
|
|
func TestFinalizeErrors(t *testing.T) {
|
|
tests := map[string]func(*Config){
|
|
"no app id": func(c *Config) { c.GitHub.AppID = 0 },
|
|
"no key": func(c *Config) { c.GitHub.PrivateKeyFile = "" },
|
|
"unreadable key": func(c *Config) { c.GitHub.PrivateKeyFile = "x" },
|
|
"no repos": func(c *Config) { c.Repos = nil },
|
|
"bad repo": func(c *Config) { c.Repos = []string{"justname"} },
|
|
"bad repo slashes": func(c *Config) { c.Repos = []string{"a/b/c"} },
|
|
"refresh too small": func(c *Config) { c.RefreshInterval = time.Second },
|
|
"crit below warn": func(c *Config) {
|
|
c.Thresholds.PRWarnAfter, c.Thresholds.PRCritAfter = 10*time.Hour, time.Hour
|
|
},
|
|
}
|
|
for name, mutate := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
c := valid()
|
|
mutate(c)
|
|
if err := c.finalize(env(nil), noFile); err == nil {
|
|
t.Fatal("expected error")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestNormalizeReposDedupes(t *testing.T) {
|
|
got, err := normalizeRepos([]string{"A/b", " a/B ", "c/d"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(got) != 2 || got[0] != "A/b" || got[1] != "c/d" {
|
|
t.Fatalf("got %v", got)
|
|
}
|
|
}
|
|
|
|
func TestNegativeThresholdDisables(t *testing.T) {
|
|
c := valid()
|
|
c.Thresholds.PRCritAfter = -1
|
|
if err := c.finalize(env(nil), keyFile); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if c.Thresholds.PRCritAfter != -1 {
|
|
t.Fatalf("negative must be kept, got %s", c.Thresholds.PRCritAfter)
|
|
}
|
|
}
|
|
|
|
func TestLoadRejectsUnknownKeysAndReadsFile(t *testing.T) {
|
|
dir := t.TempDir()
|
|
write := func(name, body string) string {
|
|
p := filepath.Join(dir, name)
|
|
if err := os.WriteFile(p, []byte(body), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return p
|
|
}
|
|
key := write("k.pem", "PEM")
|
|
good := write("ok.yaml", "refresh_interval: 2h\ngithub:\n app_id: 5\n private_key_file: "+key+"\nrepos: [a/b]\n")
|
|
c, err := Load(good)
|
|
if err != nil || c.RefreshInterval != 2*time.Hour || c.GitHub.PrivateKeyPEM != "PEM" {
|
|
t.Fatalf("c=%+v err=%v", c, err)
|
|
}
|
|
typo := write("typo.yaml", "refresh_intervall: 2h\ngithub:\n app_id: 5\n private_key_file: "+key+"\nrepos: [a/b]\n")
|
|
if _, err := Load(typo); err == nil {
|
|
t.Fatal("unknown key must be rejected")
|
|
}
|
|
if _, err := Load(filepath.Join(dir, "missing.yaml")); err == nil {
|
|
t.Fatal("missing file must error")
|
|
}
|
|
}
|
|
|
|
func TestManualRefreshCanBeDisabled(t *testing.T) {
|
|
off := false
|
|
c := valid()
|
|
c.ManualRefresh = &off
|
|
if err := c.finalize(env(nil), keyFile); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if *c.ManualRefresh {
|
|
t.Fatal("explicit false must stay off")
|
|
}
|
|
}
|
|
|
|
func TestWebBase(t *testing.T) {
|
|
tests := map[string]string{
|
|
"": "https://github.com",
|
|
"https://api.github.com": "https://github.com",
|
|
"https://api.github.com/": "https://github.com",
|
|
"https://ghe.example.com/api/v3": "https://ghe.example.com",
|
|
"https://ghe.example.com/api/v3/": "https://ghe.example.com",
|
|
"http://ghe.internal:8080/api/v3": "http://ghe.internal:8080",
|
|
"https://api.acme.ghe.com": "https://acme.ghe.com",
|
|
"https://github.example.com/sub/api/v3": "https://github.example.com/sub",
|
|
"not a url": "https://github.com",
|
|
}
|
|
for in, want := range tests {
|
|
if got := WebBase(in); got != want {
|
|
t.Errorf("WebBase(%q) = %q, want %q", in, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestActionsDefaultOff(t *testing.T) {
|
|
c := valid()
|
|
if err := c.finalize(env(nil), keyFile); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(c.Actions) != 0 {
|
|
t.Fatalf("actions must default to none: %+v", c.Actions)
|
|
}
|
|
}
|
|
|
|
func TestActionsFinalize(t *testing.T) {
|
|
good := func() Action {
|
|
return Action{ID: "rerun", Webhook: Webhook{URL: "https://hooks.example.test/rerun", TokenEnv: "T"}}
|
|
}
|
|
tests := map[string]struct {
|
|
mutate func(*Action)
|
|
extra []Action
|
|
wantErr bool
|
|
}{
|
|
"ok": {mutate: func(*Action) {}},
|
|
"no token env": {mutate: func(a *Action) { a.Webhook.TokenEnv = "" }},
|
|
"bad id upper": {mutate: func(a *Action) { a.ID = "Re_run" }, wantErr: true},
|
|
"empty id": {mutate: func(a *Action) { a.ID = "" }, wantErr: true},
|
|
"duplicate id": {mutate: func(*Action) {}, extra: []Action{good()}, wantErr: true},
|
|
"bad webhook": {mutate: func(a *Action) { a.Webhook.URL = "ftp://x" }, wantErr: true},
|
|
"no webhook": {mutate: func(a *Action) { a.Webhook.URL = "" }, wantErr: true},
|
|
"bad status url": {mutate: func(a *Action) { a.StatusURL = "nope" }, wantErr: true},
|
|
"status url ok": {mutate: func(a *Action) { a.StatusURL = "http://status.example.test/s" }},
|
|
"negative timeout": {mutate: func(a *Action) { a.Webhook.Timeout = -time.Second }, wantErr: true},
|
|
"token env unset": {mutate: func(a *Action) { a.Webhook.TokenEnv = "MISSING" }, wantErr: true},
|
|
"auto with kinds": {mutate: func(a *Action) { a.Auto, a.Kinds = true, []string{"pr_open"} }},
|
|
"auto no kinds": {mutate: func(a *Action) { a.Auto = true }, wantErr: true},
|
|
}
|
|
for name, tc := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
a := good()
|
|
tc.mutate(&a)
|
|
c := valid()
|
|
c.Actions = append([]Action{a}, tc.extra...)
|
|
err := c.finalize(env(map[string]string{"T": "s3cret"}), keyFile)
|
|
if (err != nil) != tc.wantErr {
|
|
t.Fatalf("err = %v, wantErr %v", err, tc.wantErr)
|
|
}
|
|
if err != nil && strings.Contains(err.Error(), "s3cret") {
|
|
t.Fatal("error leaks token")
|
|
}
|
|
if err == nil {
|
|
got := c.Actions[0]
|
|
if got.Label != "rerun" || got.Webhook.Timeout != DefaultActionTimeout {
|
|
t.Fatalf("defaults wrong: %+v", got)
|
|
}
|
|
if a.Webhook.TokenEnv == "T" && got.Token != "s3cret" {
|
|
t.Fatalf("token not read: %+v", got)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestActionAppliesTo(t *testing.T) {
|
|
tests := []struct {
|
|
kind string
|
|
kinds []string
|
|
want bool
|
|
}{
|
|
{kinds: nil, kind: "anything", want: true},
|
|
{kinds: []string{"workflow_failed"}, kind: "workflow_failed", want: true},
|
|
{kinds: []string{"workflow_failed"}, kind: "pr_waiting", want: false},
|
|
{kinds: []string{"a", "b"}, kind: "b", want: true},
|
|
}
|
|
for _, tc := range tests {
|
|
if got := (Action{Kinds: tc.kinds}).AppliesTo(tc.kind); got != tc.want {
|
|
t.Errorf("%v.AppliesTo(%q) = %v", tc.kinds, tc.kind, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestActionAuthorAllowed(t *testing.T) {
|
|
tests := map[string]struct {
|
|
author string
|
|
authors []string
|
|
want bool
|
|
}{
|
|
"no filter": {"anyone", nil, true},
|
|
"listed": {"alice", []string{"alice"}, true},
|
|
"case-insensitive": {"aLICE", []string{"Alice"}, true},
|
|
"unlisted": {"bob", []string{"alice"}, false},
|
|
"empty author": {"", []string{"alice"}, false},
|
|
}
|
|
for name, tc := range tests {
|
|
t.Run(name, func(t *testing.T) {
|
|
if got := (Action{Authors: tc.authors}).AuthorAllowed(tc.author); got != tc.want {
|
|
t.Fatalf("AuthorAllowed(%q) = %v, want %v", tc.author, got, tc.want)
|
|
}
|
|
})
|
|
}
|
|
}
|