internal/control/checksgate_test.go

8dcfa45a8ac03a5ff9c36828274d05acadcf846c
gitbay/internal/control/checksgate_test.go history · blame · raw

168 lines · 5859 bytes

  1package control
  2
  3import (
  4	"os"
  5	"path/filepath"
  6	"slices"
  7	"strings"
  8	"testing"
  9
 10	"gitbay.org/gitbay/internal/store"
 11)
 12
 13// gatesForHead builds a repository with require_checks on, a bare dir
 14// holding the given .gitbay/ci.yml (empty string for none), and one MR
 15// whose head carries no statuses at all. seed records a status on the
 16// base commit, standing for a repository that reports from outside.
 17func gatesForHead(t *testing.T, ciYML string) GatesOut {
 18	return gatesForHeadSeeded(t, ciYML, false)
 19}
 20
 21func gatesForHeadSeeded(t *testing.T, ciYML string, seed bool) GatesOut {
 22	return gatesFor(t, ciYML, nil, func(st *store.Store, repoID, uid int64, targetSHA, _ string) {
 23		if !seed {
 24			return
 25		}
 26		if err := st.SetCommitStatus(repoID, targetSHA, "lint", "success", "", "", uid); err != nil {
 27			t.Fatal(err)
 28		}
 29	})
 30}
 31
 32// gatesFor builds a repository with require_checks on and set applied to
 33// its settings, a bare dir holding the given .gitbay/ci.yml (empty
 34// string for none), and one MR; seed records statuses before the gates
 35// are computed.
 36func gatesFor(t *testing.T, ciYML string, set func(*store.RepoSettings),
 37	seed func(st *store.Store, repoID, uid int64, targetSHA, headSHA string)) GatesOut {
 38	t.Helper()
 39	st, repo, uid := newQueueTestRepo(t)
 40	if _, err := st.UpdateRepoSettings(repo.ID, func(s *store.RepoSettings) {
 41		s.RequireChecks = true
 42		if set != nil {
 43			set(s)
 44		}
 45	}); err != nil {
 46		t.Fatal(err)
 47	}
 48	repo, err := st.RepoByID(repo.ID)
 49	if err != nil {
 50		t.Fatal(err)
 51	}
 52
 53	git := gitRunner(t)
 54	root := t.TempDir()
 55	src := filepath.Join(root, "src")
 56	os.MkdirAll(src, 0o755)
 57	git(root, "init", "-q", "-b", "main", "src")
 58	os.WriteFile(filepath.Join(src, "README"), []byte("x\n"), 0o644)
 59	git(src, "add", ".")
 60	git(src, "commit", "-q", "-m", "base")
 61	targetSHA := strings.TrimSpace(git(src, "rev-parse", "HEAD"))
 62	git(src, "checkout", "-q", "-b", "feature")
 63	if ciYML != "" {
 64		os.MkdirAll(filepath.Join(src, ".gitbay"), 0o755)
 65		os.WriteFile(filepath.Join(src, ".gitbay", "ci.yml"), []byte(ciYML), 0o644)
 66	}
 67	os.WriteFile(filepath.Join(src, "README"), []byte("y\n"), 0o644)
 68	git(src, "add", ".")
 69	git(src, "commit", "-q", "-m", "change")
 70	headSHA := strings.TrimSpace(git(src, "rev-parse", "HEAD"))
 71
 72	dir := RepoDir(root, repo.OwnerName, repo.Name)
 73	os.MkdirAll(filepath.Dir(dir), 0o755)
 74	git(root, "clone", "-q", "--bare", src, dir)
 75
 76	if _, err := st.CreateMR(repo.ID, uid, repo.ID, "feature", "main", "t", "", headSHA, "md", false); err != nil {
 77		t.Fatal(err)
 78	}
 79	mr, err := st.MRByNumber(repo.ID, 1)
 80	if err != nil {
 81		t.Fatal(err)
 82	}
 83	if seed != nil {
 84		seed(st, repo.ID, uid, targetSHA, headSHA)
 85	}
 86	g, err := MergeGates(st, repo, mr, dir, targetSHA, headSHA)
 87	if err != nil {
 88		t.Fatal(err)
 89	}
 90	return g
 91}
 92
 93func checksUnmet(g GatesOut) bool {
 94	for _, u := range g.Unmet {
 95		if strings.Contains(u, "green checks") {
 96			return true
 97		}
 98	}
 99	return false
100}
101
102// A repository with require_checks on and no CI configuration at its head
103// can never report a status, so the gate has nothing to wait for and the
104// merge must go through rather than be refused forever.
105func TestRequireChecksPassesWithNoCIConfig(t *testing.T) {
106	if g := gatesForHead(t, ""); checksUnmet(g) {
107		t.Fatalf("refused a head with no CI configuration: %v", g.Unmet)
108	}
109}
110
111// Jobs that only run on a schedule or on tags never report on a merge
112// request head either.
113func TestRequireChecksPassesWithNoPushJobs(t *testing.T) {
114	cfg := "jobs:\n  nightly:\n    schedule: \"0 3 * * *\"\n    steps:\n      - echo hi\n" +
115		"  release:\n    tags: \"v*\"\n    steps:\n      - echo hi\n"
116	if g := gatesForHead(t, cfg); checksUnmet(g) {
117		t.Fatalf("refused a head whose jobs never run on a push: %v", g.Unmet)
118	}
119}
120
121// A push job at the head should have reported something. Silence there
122// means CI did not run, which is what the gate is for.
123func TestRequireChecksRefusesSilentPushJob(t *testing.T) {
124	cfg := "jobs:\n  unit:\n    steps:\n      - echo hi\n"
125	if g := gatesForHead(t, cfg); !checksUnmet(g) {
126		t.Fatalf("allowed a head whose push job reported nothing: %v", g.Unmet)
127	}
128}
129
130// A repository whose checks come from outside — `status set`, no
131// .gitbay/ci.yml — looks like one with no CI at all. Having reported
132// before is what says a report was coming, so a silent head there is
133// still refused.
134func TestRequireChecksRefusesSilentHeadInReportingRepo(t *testing.T) {
135	if g := gatesForHeadSeeded(t, "", true); !checksUnmet(g) {
136		t.Fatalf("allowed a silent head in a repository that reports statuses: %v", g.Unmet)
137	}
138}
139
140// A required context that has not reported holds the merge as pending,
141// even when every status that did report is green (#258).
142func TestRequiredContextMissingIsPending(t *testing.T) {
143	g := gatesFor(t, "", func(s *store.RepoSettings) { s.RequiredContexts = []string{"ext/deploy", "lint"} },
144		func(st *store.Store, repoID, uid int64, _, headSHA string) {
145			if err := st.SetCommitStatus(repoID, headSHA, "lint", "success", "", "", uid); err != nil {
146				t.Fatal(err)
147			}
148		})
149	if g.Checks != "pending" || !slices.Equal(g.ChecksMissing, []string{"ext/deploy"}) {
150		t.Fatalf("checks %q, missing %v", g.Checks, g.ChecksMissing)
151	}
152	if !checksUnmet(g) || !strings.Contains(strings.Join(g.Unmet, "\n"), "ext/deploy=missing") {
153		t.Fatalf("unmet: %v", g.Unmet)
154	}
155}
156
157// Every required context reported green: nothing is held.
158func TestRequiredContextsReportedPass(t *testing.T) {
159	g := gatesFor(t, "", func(s *store.RepoSettings) { s.RequiredContexts = []string{"lint"} },
160		func(st *store.Store, repoID, uid int64, _, headSHA string) {
161			if err := st.SetCommitStatus(repoID, headSHA, "lint", "success", "", "", uid); err != nil {
162				t.Fatal(err)
163			}
164		})
165	if checksUnmet(g) || len(g.ChecksMissing) != 0 || g.Checks != "success" {
166		t.Fatalf("checks %q, missing %v, unmet %v", g.Checks, g.ChecksMissing, g.Unmet)
167	}
168}