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