internal/control/commitrefs_test.go
105 lines · 4309 bytes
1package control
2
3import (
4 "fmt"
5 "slices"
6 "strings"
7 "testing"
8
9 "gitbay.org/gitbay/internal/store"
10)
11
12// The same keyword set has to work wherever the intent is written: a
13// commit message, or a merge request title or body.
14func TestClosingRefs(t *testing.T) {
15 for _, tc := range []struct {
16 name string
17 text string
18 want []closeRef
19 }{
20 {"closes", "Closes #50", []closeRef{{"", 50}}},
21 {"lowercase and fix", "fixes #7", []closeRef{{"", 7}}},
22 {"resolved", "resolved: #12", []closeRef{{"", 12}}},
23 {"several", "Closes #1\n\nAlso fixes #2 and resolves #3", []closeRef{{"", 1}, {"", 2}, {"", 3}}},
24 {"repeats collapse", "closes #4, closes #4", []closeRef{{"", 4}}},
25 {"bare references do not close", "see #9 for context", nil},
26 {"cross-repo carries the path", "closes krz/other#3", []closeRef{{"krz/other", 3}}},
27 {"same number in two repos", "closes #3, closes krz/other#3", []closeRef{{"", 3}, {"krz/other", 3}}},
28 {"keyword must be its own word", "unclosed #5", nil},
29 } {
30 t.Run(tc.name, func(t *testing.T) {
31 got := closingRefs(tc.text)
32 slices.SortFunc(got, func(a, b closeRef) int {
33 if a.Path != b.Path {
34 return strings.Compare(a.Path, b.Path)
35 }
36 return int(a.N - b.N)
37 })
38 if !slices.Equal(got, tc.want) {
39 t.Errorf("closingRefs(%q) = %v, want %v", tc.text, got, tc.want)
40 }
41 })
42 }
43}
44
45// A merged merge request's description closes an issue in another
46// repository only when the merger holds write there. This drives the
47// same target resolution the commit path uses, without needing git.
48func TestMRDescriptionClosesAcrossRepos(t *testing.T) {
49 f := newOrgFixture(t)
50 libIssue, _ := f.st.CreateIssue(f.priv.ID, f.alice, "in priv", "", "md")
51 appIssue, _ := f.st.CreateIssue(f.app.ID, f.alice, "in app", "", "md")
52 _ = libIssue
53 _ = appIssue
54 mr := func(n int64, title string) store.MR {
55 return store.MR{Number: n, Title: title, Body: ""}
56 }
57 // carol cannot write acme/priv: the issue stays open and no comment
58 // lands.
59 ProcessMRDescription(f.st, f.app, mr(1, "Closes acme/priv#1"), f.carol, "full")
60 if iss, _ := f.st.IssueByNumber(f.priv.ID, 1); iss.State != "open" {
61 t.Fatal("outsider closed a private repo's issue")
62 }
63 // A deploy key on alice/app is bound to alice/app: alice's own access
64 // to acme/priv is not the key's to use.
65 deploy := fmt.Sprintf("deploy:%d:rw", f.app.ID)
66 ProcessMRDescription(f.st, f.app, mr(2, "Closes acme/priv#1"), f.alice, deploy)
67 if iss, _ := f.st.IssueByNumber(f.priv.ID, 1); iss.State != "open" {
68 t.Fatal("a deploy key closed an issue outside its binding")
69 }
70 // The same key naming its own repository by full path is the bare
71 // form spelled out, not a cross-repo close (#213).
72 if _, err := f.st.CreateIssue(f.app.ID, f.alice, "in app too", "", "md"); err != nil {
73 t.Fatal(err)
74 }
75 ProcessMRDescription(f.st, f.app, mr(6, "Closes alice/app#2"), f.alice, deploy)
76 if iss, _ := f.st.IssueByNumber(f.app.ID, 2); iss.State != "closed" {
77 t.Fatal("a deploy key did not close its own repository's issue by full path")
78 }
79 // An archived target is read-only, cross-repo closes included.
80 if _, err := f.st.UpdateRepoSettings(f.priv.ID, func(rs *store.RepoSettings) { rs.Archived = true }); err != nil {
81 t.Fatal(err)
82 }
83 ProcessMRDescription(f.st, f.app, mr(3, "Closes acme/priv#1"), f.alice, "full")
84 if iss, _ := f.st.IssueByNumber(f.priv.ID, 1); iss.State != "open" {
85 t.Fatal("an archived repository's issue was closed")
86 }
87 if _, err := f.st.UpdateRepoSettings(f.priv.ID, func(rs *store.RepoSettings) { rs.Archived = false }); err != nil {
88 t.Fatal(err)
89 }
90 // alice can: it closes with a comment naming the source repository.
91 ProcessMRDescription(f.st, f.app, mr(4, "Closes acme/priv#1"), f.alice, "full")
92 iss, _ := f.st.IssueByNumber(f.priv.ID, 1)
93 if iss.State != "closed" {
94 t.Fatal("writer did not close across repos")
95 }
96 comments, _ := f.st.ListIssueComments(iss.ID)
97 if len(comments) != 1 || !strings.Contains(comments[0].Body, "(/alice/app/mrs/4)") {
98 t.Fatalf("close comment = %+v", comments)
99 }
100 // An unknown path is text; a bare #N still acts in the source repo.
101 ProcessMRDescription(f.st, f.app, mr(5, "Closes nobody/nothing#1 and closes #1"), f.alice, "full")
102 if iss, _ := f.st.IssueByNumber(f.app.ID, 1); iss.State != "closed" {
103 t.Fatal("bare #N stopped working")
104 }
105}