internal/control/commitrefs_test.go

d775997ead04022093817ab513287b5886065247
gitbay/internal/control/commitrefs_test.go history · blame · raw

77 lines · 2894 bytes

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