internal/control/commitrefs_test.go

v1.40.0
gitbay/internal/control/commitrefs_test.go history · blame · raw

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}