internal/control/issue_test.go

v1.39.0
gitbay/internal/control/issue_test.go history · blame · raw

188 lines · 6090 bytes

  1package control
  2
  3import (
  4	"bytes"
  5	"errors"
  6	"slices"
  7	"testing"
  8
  9	"gitbay.org/gitbay/internal/protocol"
 10	"gitbay.org/gitbay/internal/store"
 11)
 12
 13// TestIssueCreateWithLabelRequiresWrite checks that attaching a label at
 14// create time needs the same write access issue label requires, not just
 15// the read access that lets anyone file the issue in the first place.
 16func TestIssueCreateWithLabelRequiresWrite(t *testing.T) {
 17	c := notifTestCtx(t, "alice")
 18	repoID, err := c.Store.CreateRepo("user", c.User.ID, "app", "public")
 19	if err != nil {
 20		t.Fatal(err)
 21	}
 22	repo, err := c.Store.RepoByID(repoID)
 23	if err != nil {
 24		t.Fatal(err)
 25	}
 26	if err := c.Store.SetLabel(repo, "bug", "ff0000"); err != nil {
 27		t.Fatal(err)
 28	}
 29	bobID, err := c.Store.CreateUser("bob", false)
 30	if err != nil {
 31		t.Fatal(err)
 32	}
 33	bob := *c
 34	bob.User = store.User{ID: bobID, Username: "bob"}
 35	var out bytes.Buffer
 36	bob.Stdout, bob.Stderr = &out, &out
 37
 38	if code := runIssueCreate(&bob, []string{repo.Path(), "--title", "t", "--label", "bug"}); code != protocol.ExitDenied {
 39		t.Fatalf("exit %d, want %d", code, protocol.ExitDenied)
 40	}
 41	if _, err := c.Store.IssueByNumber(repo.ID, 1); !errors.Is(err, store.ErrNotFound) {
 42		t.Fatalf("issue was created despite the denial: %v", err)
 43	}
 44}
 45
 46func TestIssueCreateSetsLabelsMilestoneAndAssignee(t *testing.T) {
 47	c := notifTestCtx(t, "alice")
 48	repoID, err := c.Store.CreateRepo("user", c.User.ID, "app", "public")
 49	if err != nil {
 50		t.Fatal(err)
 51	}
 52	repo, err := c.Store.RepoByID(repoID)
 53	if err != nil {
 54		t.Fatal(err)
 55	}
 56	if err := c.Store.SetLabel(repo, "bug", "ff0000"); err != nil {
 57		t.Fatal(err)
 58	}
 59	if _, err := c.Store.CreateMilestone(repo, "m1", "", ""); err != nil {
 60		t.Fatal(err)
 61	}
 62	bob, err := c.Store.CreateUser("bob", false)
 63	if err != nil {
 64		t.Fatal(err)
 65	}
 66
 67	if code := runIssueCreate(c, []string{repo.Path(), "--title", "t",
 68		"--label", "bug", "--milestone", "m1", "--assignee", "bob"}); code != 0 {
 69		t.Fatalf("exit %d", code)
 70	}
 71	rows, _ := c.Store.Inbox(bob, false, 20, 0)
 72	if len(rows) != 1 || rows[0].Summary != "assigned you to #1" {
 73		t.Errorf("bob's inbox = %+v, want one assigned-you notice", rows)
 74	}
 75	var kinds []string
 76	ev, err := c.Store.DB.Query("SELECT kind FROM events WHERE repo_id = ? ORDER BY id", repo.ID)
 77	if err != nil {
 78		t.Fatal(err)
 79	}
 80	for ev.Next() {
 81		var k string
 82		ev.Scan(&k)
 83		kinds = append(kinds, k)
 84	}
 85	ev.Close()
 86	if want := []string{"issue.created", "issue.labeled", "issue.milestoned", "issue.assigned"}; !slices.Equal(kinds, want) {
 87		t.Errorf("events = %v, want %v", kinds, want)
 88	}
 89	issue, err := c.Store.IssueByNumber(repo.ID, 1)
 90	if err != nil {
 91		t.Fatal(err)
 92	}
 93	if len(issue.Labels) != 1 || issue.Labels[0] != "bug" {
 94		t.Errorf("labels = %v", issue.Labels)
 95	}
 96	if issue.Milestone != "m1" {
 97		t.Errorf("milestone = %q", issue.Milestone)
 98	}
 99	if len(issue.Assignees) != 1 || issue.Assignees[0] != "bob" {
100		t.Errorf("assignees = %v", issue.Assignees)
101	}
102}
103
104// A milestone or assignee that does not resolve is refused before the
105// issue exists, so a typo creates nothing.
106func TestIssueCreateRefusesUnknownMilestoneOrAssigneeFirst(t *testing.T) {
107	c := notifTestCtx(t, "alice")
108	repoID, err := c.Store.CreateRepo("user", c.User.ID, "app", "public")
109	if err != nil {
110		t.Fatal(err)
111	}
112	repo, err := c.Store.RepoByID(repoID)
113	if err != nil {
114		t.Fatal(err)
115	}
116	for _, extra := range [][]string{{"--milestone", "nope"}, {"--assignee", "nobody"}} {
117		args := append([]string{repo.Path(), "--title", "t", "--body", "b", "--label", "bug"}, extra...)
118		if code := runIssueCreate(c, args); code != protocol.ExitNotFound {
119			t.Errorf("%v: exit %d, want %d", extra, code, protocol.ExitNotFound)
120		}
121		if _, err := c.Store.IssueByNumber(repo.ID, 1); !errors.Is(err, store.ErrNotFound) {
122			t.Fatalf("%v: an issue was created: %v", extra, err)
123		}
124	}
125}
126
127func TestIssueAssignNotifiesTheAssignee(t *testing.T) {
128	c, repo, bob := testRepoWithWatcher(t)
129
130	if code := runIssueAssign(c, []string{repo.Path(), "1", "--add", "bob"}); code != 0 {
131		t.Fatalf("exit %d", code)
132	}
133	rows, _ := c.Store.Inbox(bob, false, 20, 0)
134	if len(rows) != 1 || rows[0].Summary != "assigned you to #1" {
135		t.Fatalf("got %+v", rows)
136	}
137}
138
139// The spec files a notice for each account newly added. SetIssueAssignee
140// inserts ON CONFLICT DO NOTHING and reports nothing either way, so a
141// client reconciling an assignee list by re-sending the whole set would
142// file, mail and push a row on every save.
143func TestIssueAssignDoesNotRenotifyAnExistingAssignee(t *testing.T) {
144	c, repo, bob := testRepoWithWatcher(t)
145	c.Store.AddPushDevice(bob, "tok-b", "iphone")
146
147	if code := runIssueAssign(c, []string{repo.Path(), "1", "--add", "bob"}); code != 0 {
148		t.Fatalf("exit %d", code)
149	}
150	if code := runIssueAssign(c, []string{repo.Path(), "1", "--add", "bob"}); code != 0 {
151		t.Fatalf("exit %d", code)
152	}
153
154	rows, _ := c.Store.Inbox(bob, false, 20, 0)
155	if len(rows) != 1 {
156		t.Fatalf("filed %d rows for one assignment: %+v", len(rows), rows)
157	}
158	if due, _ := c.Store.DuePush(20); len(due) != 1 {
159		t.Fatalf("queued %d pushes for one assignment", len(due))
160	}
161	// The second call still succeeds and still reports the assignee.
162	updated, err := c.Store.IssueByNumber(repo.ID, 1)
163	if err != nil {
164		t.Fatal(err)
165	}
166	if len(updated.Assignees) != 1 || updated.Assignees[0] != "bob" {
167		t.Fatalf("assignees: %+v", updated.Assignees)
168	}
169}
170
171func TestIssueAssignIsSilentForTheActorAndForRemovals(t *testing.T) {
172	c, repo, bob := testRepoWithWatcher(t)
173
174	// Assigning yourself announces nothing: notify drops the actor.
175	runIssueAssign(c, []string{repo.Path(), "1", "--add", "alice"})
176	if rows, _ := c.Store.Inbox(c.User.ID, false, 20, 0); len(rows) != 0 {
177		t.Fatalf("self-assignment notified: %+v", rows)
178	}
179
180	// Unassigning files nothing.
181	runIssueAssign(c, []string{repo.Path(), "1", "--add", "bob"})
182	before, _ := c.Store.Inbox(bob, false, 20, 0)
183	runIssueAssign(c, []string{repo.Path(), "1", "--remove", "bob"})
184	after, _ := c.Store.Inbox(bob, false, 20, 0)
185	if len(after) != len(before) {
186		t.Fatalf("removal filed a row: %d then %d", len(before), len(after))
187	}
188}