internal/control/mr_test.go

0338e6ace3de199d5fc383852649919b68ef3e42
gitbay/internal/control/mr_test.go history · blame · raw

264 lines · 9820 bytes

  1package control
  2
  3import (
  4	"bytes"
  5	"encoding/json"
  6	"os"
  7	"path/filepath"
  8	"strconv"
  9	"strings"
 10	"testing"
 11
 12	"gitbay.org/gitbay/internal/protocol"
 13	"gitbay.org/gitbay/internal/store"
 14)
 15
 16// mrTestCtx runs control commands as owner against st, capturing output.
 17func mrTestCtx(st *store.Store, owner store.User) (*Ctx, *bytes.Buffer, *bytes.Buffer) {
 18	out, errOut := &bytes.Buffer{}, &bytes.Buffer{}
 19	c := &Ctx{User: owner, Scope: "full", Store: st, Stdout: out, Stderr: errOut}
 20	return c, out, errOut
 21}
 22
 23// twoMRTestRepo is a repository with two open merge requests, both
 24// authored by the returned owner, so authorOrWrite never gets in the way.
 25func twoMRTestRepo(t *testing.T) (*store.Store, store.Repo, store.User) {
 26	t.Helper()
 27	st, repo, uid := newQueueTestRepo(t)
 28	owner := store.User{ID: uid, Username: "alice"}
 29	if _, err := st.CreateMR(repo.ID, uid, repo.ID, "feature1", "main", "one", "", "abc111", "md", false); err != nil {
 30		t.Fatal(err)
 31	}
 32	if _, err := st.CreateMR(repo.ID, uid, repo.ID, "feature2", "main", "two", "", "abc222", "md", false); err != nil {
 33		t.Fatal(err)
 34	}
 35	return st, repo, owner
 36}
 37
 38func mrShowJSON(t *testing.T, st *store.Store, owner store.User, path string, n int64) mrOut {
 39	t.Helper()
 40	c, out, errOut := mrTestCtx(st, owner)
 41	if code := Dispatch(c, []string{"mr", "show", path, strconv.FormatInt(n, 10), "--json"}); code != protocol.ExitOK {
 42		t.Fatalf("mr show: exit %d, %s", code, errOut.String())
 43	}
 44	var env struct {
 45		Data mrOut `json:"data"`
 46	}
 47	if err := json.Unmarshal(out.Bytes(), &env); err != nil {
 48		t.Fatalf("mr show JSON: %v\n%s", err, out.String())
 49	}
 50	return env.Data
 51}
 52
 53// eventDataFor pulls the most recent data_json for a kind, so a test can
 54// check what mr close recorded without a store accessor built just for it.
 55func eventDataFor(t *testing.T, st *store.Store, kind string) string {
 56	t.Helper()
 57	var data string
 58	err := st.DB.QueryRow("SELECT data_json FROM events WHERE kind = ? ORDER BY id DESC LIMIT 1", kind).Scan(&data)
 59	if err != nil {
 60		t.Fatalf("event %s: %v", kind, err)
 61	}
 62	return data
 63}
 64
 65// Closing a merge request can name the one that carries its change
 66// forward; mr show and the mr.closed event both then carry it (#223).
 67func TestMRCloseWithBy(t *testing.T) {
 68	st, repo, owner := twoMRTestRepo(t)
 69	c, _, errOut := mrTestCtx(st, owner)
 70	if code := Dispatch(c, []string{"mr", "close", repo.Path(), "1", "--by", "2"}); code != protocol.ExitOK {
 71		t.Fatalf("mr close: exit %d, %s", code, errOut.String())
 72	}
 73	got := mrShowJSON(t, st, owner, repo.Path(), 1)
 74	if got.State != "closed" || got.SupersededBy != 2 {
 75		t.Fatalf("mr show !1 = %+v, want closed superseded_by 2", got)
 76	}
 77	if data := eventDataFor(t, st, "mr.closed"); !strings.Contains(data, `"by":2`) {
 78		t.Fatalf("mr.closed event = %s, want it to carry by:2", data)
 79	}
 80}
 81
 82// mr close --by refuses a merge request naming itself.
 83func TestMRCloseBySelfRefused(t *testing.T) {
 84	st, repo, owner := twoMRTestRepo(t)
 85	c, _, errOut := mrTestCtx(st, owner)
 86	code := Dispatch(c, []string{"mr", "close", repo.Path(), "1", "--by", "1"})
 87	if code != protocol.ExitUsage {
 88		t.Fatalf("exit = %d, want %d; stderr: %s", code, protocol.ExitUsage, errOut.String())
 89	}
 90	if !strings.Contains(errOut.String(), "cannot supersede itself") {
 91		t.Fatalf("stderr = %q, want it to say a merge request cannot supersede itself", errOut.String())
 92	}
 93}
 94
 95// mr close --by refuses a merge request number that does not exist in
 96// the repository.
 97func TestMRCloseByMissingRefused(t *testing.T) {
 98	st, repo, owner := twoMRTestRepo(t)
 99	c, _, errOut := mrTestCtx(st, owner)
100	code := Dispatch(c, []string{"mr", "close", repo.Path(), "1", "--by", "99"})
101	if code != protocol.ExitNotFound {
102		t.Fatalf("exit = %d, want %d; stderr: %s", code, protocol.ExitNotFound, errOut.String())
103	}
104	if !strings.Contains(errOut.String(), "no merge request !99") {
105		t.Fatalf("stderr = %q, want it to name !99 as missing", errOut.String())
106	}
107}
108
109// mr edit --superseded-by sets and clears the field on a closed merge
110// request.
111func TestMREditSupersededBySetAndClear(t *testing.T) {
112	st, repo, owner := twoMRTestRepo(t)
113	c, _, errOut := mrTestCtx(st, owner)
114	if code := Dispatch(c, []string{"mr", "close", repo.Path(), "1"}); code != protocol.ExitOK {
115		t.Fatalf("mr close: exit %d, %s", code, errOut.String())
116	}
117	c, _, errOut = mrTestCtx(st, owner)
118	if code := Dispatch(c, []string{"mr", "edit", repo.Path(), "1", "--superseded-by", "2"}); code != protocol.ExitOK {
119		t.Fatalf("mr edit --superseded-by 2: exit %d, %s", code, errOut.String())
120	}
121	if got := mrShowJSON(t, st, owner, repo.Path(), 1); got.SupersededBy != 2 {
122		t.Fatalf("SupersededBy = %d, want 2", got.SupersededBy)
123	}
124	c, _, errOut = mrTestCtx(st, owner)
125	if code := Dispatch(c, []string{"mr", "edit", repo.Path(), "1", "--superseded-by", "none"}); code != protocol.ExitOK {
126		t.Fatalf("mr edit --superseded-by none: exit %d, %s", code, errOut.String())
127	}
128	if got := mrShowJSON(t, st, owner, repo.Path(), 1); got.SupersededBy != 0 {
129		t.Fatalf("SupersededBy after clear = %d, want 0", got.SupersededBy)
130	}
131}
132
133// mr edit --superseded-by refuses a self-reference the same way mr close
134// --by does.
135func TestMREditSupersededBySelfRefused(t *testing.T) {
136	st, repo, owner := twoMRTestRepo(t)
137	c, _, errOut := mrTestCtx(st, owner)
138	if code := Dispatch(c, []string{"mr", "close", repo.Path(), "1"}); code != protocol.ExitOK {
139		t.Fatalf("mr close: exit %d, %s", code, errOut.String())
140	}
141	c, _, errOut = mrTestCtx(st, owner)
142	code := Dispatch(c, []string{"mr", "edit", repo.Path(), "1", "--superseded-by", "1"})
143	if code != protocol.ExitUsage {
144		t.Fatalf("exit = %d, want %d; stderr: %s", code, protocol.ExitUsage, errOut.String())
145	}
146	if !strings.Contains(errOut.String(), "cannot supersede itself") {
147		t.Fatalf("stderr = %q, want it to say a merge request cannot supersede itself", errOut.String())
148	}
149}
150
151// mr edit --superseded-by refuses a merge request number that does not
152// exist in the repository.
153func TestMREditSupersededByMissingRefused(t *testing.T) {
154	st, repo, owner := twoMRTestRepo(t)
155	c, _, errOut := mrTestCtx(st, owner)
156	if code := Dispatch(c, []string{"mr", "close", repo.Path(), "1"}); code != protocol.ExitOK {
157		t.Fatalf("mr close: exit %d, %s", code, errOut.String())
158	}
159	c, _, errOut = mrTestCtx(st, owner)
160	code := Dispatch(c, []string{"mr", "edit", repo.Path(), "1", "--superseded-by", "99"})
161	if code != protocol.ExitNotFound {
162		t.Fatalf("exit = %d, want %d; stderr: %s", code, protocol.ExitNotFound, errOut.String())
163	}
164	if !strings.Contains(errOut.String(), "no merge request !99") {
165		t.Fatalf("stderr = %q, want it to name !99 as missing", errOut.String())
166	}
167}
168
169// mr edit --superseded-by refuses an open merge request: only a closed
170// one can be superseded.
171func TestMREditSupersededByOnOpenMRRefused(t *testing.T) {
172	st, repo, owner := twoMRTestRepo(t)
173	c, _, errOut := mrTestCtx(st, owner)
174	code := Dispatch(c, []string{"mr", "edit", repo.Path(), "1", "--superseded-by", "2"})
175	if code != protocol.ExitUsage {
176		t.Fatalf("exit = %d, want %d; stderr: %s", code, protocol.ExitUsage, errOut.String())
177	}
178	if !strings.Contains(errOut.String(), "only a closed merge request can be superseded") {
179		t.Fatalf("stderr = %q, want the closed-only refusal", errOut.String())
180	}
181}
182
183// mr show pluralizes multi-row section headings with counts.
184func TestMRShowPluralizesMultiRowSections(t *testing.T) {
185	st, repo, uid := newQueueTestRepo(t)
186	owner := store.User{ID: uid, Username: "alice"}
187
188	// Create git commits for the MR
189	git := gitRunner(t)
190	root := t.TempDir()
191
192	// Create a temporary repository to set up commits
193	src := filepath.Join(root, "src")
194	os.MkdirAll(src, 0o755)
195	git(root, "init", "-q", "-b", "main", "src")
196
197	// Create base commit on main
198	os.WriteFile(filepath.Join(src, "file.txt"), []byte("content"), 0o644)
199	git(src, "add", ".")
200	git(src, "commit", "-q", "-m", "initial")
201
202	// Create feature branch with 2 commits
203	git(src, "checkout", "-q", "-b", "feature")
204	os.WriteFile(filepath.Join(src, "file.txt"), []byte("content1"), 0o644)
205	git(src, "add", ".")
206	git(src, "commit", "-q", "-m", "commit1")
207
208	os.WriteFile(filepath.Join(src, "file.txt"), []byte("content2"), 0o644)
209	git(src, "add", ".")
210	git(src, "commit", "-q", "-m", "commit2")
211	headSHA := strings.TrimSpace(git(src, "rev-parse", "HEAD"))
212
213	// Clone as a bare repository to the gitbay path
214	dir := RepoDir(root, repo.OwnerName, repo.Name)
215	os.MkdirAll(filepath.Dir(dir), 0o755)
216	git(root, "clone", "-q", "--bare", "src", dir)
217
218	// Create the MR head ref in the bare repository
219	git(dir, "update-ref", "refs/merge-requests/1/head", headSHA)
220
221	// Create an MR
222	_, err := st.CreateMR(repo.ID, uid, repo.ID, "feature", "main", "Feature", "", headSHA, "md", false)
223	if err != nil {
224		t.Fatalf("CreateMR: %v", err)
225	}
226
227	// Add 2 checks
228	if err := st.SetCommitStatus(repo.ID, headSHA, "check1", "success", "", "", 0); err != nil {
229		t.Fatal(err)
230	}
231	if err := st.SetCommitStatus(repo.ID, headSHA, "check2", "success", "", "", 0); err != nil {
232		t.Fatal(err)
233	}
234
235	// Add 2 reviews
236	bob, err := st.CreateUser("bob", false)
237	if err != nil {
238		t.Fatal(err)
239	}
240	if err := st.AddMRReview(1, bob, "approve", headSHA); err != nil {
241		t.Fatal(err)
242	}
243	charlie, err := st.CreateUser("charlie", false)
244	if err != nil {
245		t.Fatal(err)
246	}
247	if err := st.AddMRReview(1, charlie, "approve", headSHA); err != nil {
248		t.Fatal(err)
249	}
250
251	// Call mr show in plain text mode
252	c, out, errOut := mrTestCtx(st, owner)
253	c.Cfg.Server.Root = root
254	if code := Dispatch(c, []string{"mr", "show", repo.Path(), "1"}); code != protocol.ExitOK {
255		t.Fatalf("mr show: exit %d, %s", code, errOut.String())
256	}
257
258	outStr := out.String()
259	for _, want := range []string{"commits (2):", "checks (2):", "reviews (2):"} {
260		if !strings.Contains(outStr, want) {
261			t.Errorf("missing %q in:\n%s", want, outStr)
262		}
263	}
264}