internal/control/mr_test.go
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}