internal/control/notifications_test.go
269 lines · 8744 bytes
1package control
2
3import (
4 "bytes"
5 "fmt"
6 "strings"
7 "testing"
8
9 "gitbay.org/gitbay/internal/config"
10 "gitbay.org/gitbay/internal/protocol"
11 "gitbay.org/gitbay/internal/store"
12)
13
14// notifTestCtx opens an in-memory store, migrates it, and creates one user
15// to act as. Modeled on the store setup in snippet_test.go; this package
16// has no shared testCtx helper.
17func notifTestCtx(t *testing.T, username string) *Ctx {
18 t.Helper()
19 st, err := store.Open(":memory:")
20 if err != nil {
21 t.Fatal(err)
22 }
23 t.Cleanup(func() { st.Close() })
24 if err := st.MigrateUp(); err != nil {
25 t.Fatal(err)
26 }
27 uid, err := st.CreateUser(username, false)
28 if err != nil {
29 t.Fatal(err)
30 }
31 var out bytes.Buffer
32 return &Ctx{
33 User: store.User{ID: uid, Username: username},
34 Scope: "full",
35 Store: st,
36 // Push enabled is the instance state the push tests assume; the
37 // disabled case sets it back to false explicitly.
38 Cfg: config.Config{Push: config.Push{Enabled: true}},
39 Stdin: strings.NewReader(""),
40 Stdout: &out,
41 Stderr: &out,
42 }
43}
44
45// testRepoWithWatcher returns a Ctx acting as alice, a repository she
46// owns with issue #1 open on it, and bob's user id with a watch row on
47// it — the shared setup for notify's recipient-widening tests.
48func testRepoWithWatcher(t *testing.T) (*Ctx, store.Repo, int64) {
49 t.Helper()
50 c := notifTestCtx(t, "alice")
51 repoID, err := c.Store.CreateRepo("user", c.User.ID, "app", "public")
52 if err != nil {
53 t.Fatal(err)
54 }
55 repo, err := c.Store.RepoByID(repoID)
56 if err != nil {
57 t.Fatal(err)
58 }
59 if _, err := c.Store.CreateIssue(repo.ID, c.User.ID, "title", "", "markdown"); 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 if err := c.Store.SetRepoWatch(repo.ID, bob, "watching"); err != nil {
67 t.Fatal(err)
68 }
69 return c, repo, bob
70}
71
72func TestNotifyQueuesPush(t *testing.T) {
73 c, repo, bob := testRepoWithWatcher(t) // alice acts, bob watches
74 c.Store.AddPushDevice(bob, "tok-b", "iphone")
75
76 notify(c, []int64{bob}, notice{repo: repo, kind: "issue",
77 subject: "[alice/app] #1: title",
78 action: "opened issue #1",
79 path: "alice/app/issues/1"})
80
81 due, err := c.Store.DuePush(20)
82 if err != nil {
83 t.Fatalf("DuePush: %v", err)
84 }
85 if len(due) != 1 {
86 t.Fatalf("want one queued push, got %d", len(due))
87 }
88 // The push body is the inbox row's summary, so the two surfaces
89 // cannot disagree about what happened.
90 if due[0].Title != "alice/app" {
91 t.Fatalf("title = %q", due[0].Title)
92 }
93 if due[0].Body != "alice opened issue #1" {
94 t.Fatalf("body = %q", due[0].Body)
95 }
96 if due[0].Path != "alice/app/issues/1" {
97 t.Fatalf("path = %q", due[0].Path)
98 }
99}
100
101func TestNotifyQueuesNoPushForTheActor(t *testing.T) {
102 c, repo, _ := testRepoWithWatcher(t)
103 c.Store.AddPushDevice(c.User.ID, "tok-self", "iphone")
104
105 notify(c, []int64{c.User.ID}, notice{repo: repo, kind: "issue",
106 subject: "s", action: "opened issue #1", path: "alice/app/issues/1"})
107
108 // NotifyRecipients already drops the actor; push inherits that and
109 // must not find its own way around it.
110 if due, _ := c.Store.DuePush(20); len(due) != 0 {
111 t.Fatalf("queued a push to the actor")
112 }
113}
114
115// TestNotifyQueuesNoPushWhenDisabled: on an instance with [push]
116// enabled = false nothing drains the queue, and the retention sweep only
117// collects rows that were sent or dead-lettered, so a row written here is
118// never collected. The mail half already gates on the instance having
119// SMTP; push gates the same way.
120func TestNotifyQueuesNoPushWhenDisabled(t *testing.T) {
121 c, repo, bob := testRepoWithWatcher(t)
122 c.Cfg.Push.Enabled = false
123 c.Store.AddPushDevice(bob, "tok-b", "iphone")
124
125 notify(c, []int64{bob}, notice{repo: repo, kind: "issue",
126 subject: "s", action: "opened issue #1", path: "alice/app/issues/1"})
127
128 if due, _ := c.Store.DuePush(20); len(due) != 0 {
129 t.Fatalf("queued %d pushes on a push-disabled instance", len(due))
130 }
131 // The inbox row is still filed: push is the optional half, not the
132 // notice.
133 if n := c.Store.UnreadNotices(bob); n != 1 {
134 t.Fatalf("unread notices = %d, want 1", n)
135 }
136}
137
138// TestNotificationsDeviceAddRefusedWhenPushDisabled: registering a device
139// on an instance that cannot deliver would report success and then never
140// push, with notifications settings show still saying push is on.
141func TestNotificationsDeviceAddRefusedWhenPushDisabled(t *testing.T) {
142 c := notifTestCtx(t, "alice")
143 c.Cfg.Push.Enabled = false
144 c.Stdin = strings.NewReader("DEVTOKEN\n")
145 var out bytes.Buffer
146 c.Stdout, c.Stderr = &out, &out
147
148 if code := runNotificationsDeviceAdd(c, nil); code != protocol.ExitFailure {
149 t.Fatalf("exit %d, want %d", code, protocol.ExitFailure)
150 }
151 if devices, _ := c.Store.PushDevices(c.User.ID); len(devices) != 0 {
152 t.Fatalf("device registered anyway: %+v", devices)
153 }
154 if !strings.Contains(out.String(), "[push] enabled = false") {
155 t.Fatalf("message does not name the instance setting: %q", out.String())
156 }
157}
158
159func TestNotificationsDeviceAddReadsStdin(t *testing.T) {
160 c := notifTestCtx(t, "alice")
161 c.Stdin = strings.NewReader("DEVTOKEN\n")
162 if code := runNotificationsDeviceAdd(c, []string{"--label", "iphone"}); code != 0 {
163 t.Fatalf("exit %d", code)
164 }
165 devices, _ := c.Store.PushDevices(c.User.ID)
166 if len(devices) != 1 || devices[0].Token != "DEVTOKEN" {
167 t.Fatalf("got %+v", devices)
168 }
169 if devices[0].Label != "iphone" {
170 t.Fatalf("label = %q", devices[0].Label)
171 }
172}
173
174func TestNotificationsDeviceListTruncatesTheToken(t *testing.T) {
175 c := notifTestCtx(t, "alice")
176 long := strings.Repeat("a", 64)
177 c.Store.AddPushDevice(c.User.ID, long, "iphone")
178 var out bytes.Buffer
179 c.Stdout = &out
180 if code := runNotificationsDeviceList(c, nil); code != 0 {
181 t.Fatalf("exit %d", code)
182 }
183 if strings.Contains(out.String(), long) {
184 t.Fatal("the full token was printed")
185 }
186}
187
188// TestNotificationsDeviceListTruncatesTheTokenJSON is the JSON-path twin
189// of the above: the plain and JSON output share the same rows slice, but
190// nothing enforces that beyond reading the code, so both paths get their
191// own test of the guarantee.
192func TestNotificationsDeviceListTruncatesTheTokenJSON(t *testing.T) {
193 c := notifTestCtx(t, "alice")
194 long := strings.Repeat("a", 64)
195 c.Store.AddPushDevice(c.User.ID, long, "iphone")
196 var out bytes.Buffer
197 c.Stdout, c.JSON = &out, true
198 if code := runNotificationsDeviceList(c, nil); code != 0 {
199 t.Fatalf("exit %d", code)
200 }
201 if strings.Contains(out.String(), long) {
202 t.Fatal("the full token was printed")
203 }
204}
205
206// TestNotificationsDeviceListMasksAShortToken: a token at or under the
207// truncation cut length is not returned unchanged. ShortToken's short
208// path used to return the token verbatim, a full echo of anything eight
209// characters or fewer; runNotificationsDeviceAdd enforces no minimum
210// length, so a short token is a value the command will store.
211func TestNotificationsDeviceListMasksAShortToken(t *testing.T) {
212 c := notifTestCtx(t, "alice")
213 short := "abc123"
214 c.Store.AddPushDevice(c.User.ID, short, "iphone")
215 var out bytes.Buffer
216 c.Stdout = &out
217 if code := runNotificationsDeviceList(c, nil); code != 0 {
218 t.Fatalf("exit %d", code)
219 }
220 if strings.Contains(out.String(), short) {
221 t.Fatal("the short token was printed verbatim")
222 }
223}
224
225// device add returns the row id. Without it a client that wants to
226// deregister has to list devices and match its own token against the
227// truncated display value, which is identity by rendered string.
228func TestNotificationsDeviceAddReturnsTheID(t *testing.T) {
229 c := notifTestCtx(t, "alice")
230 c.Stdin = strings.NewReader("DEVTOKEN\n")
231 var out bytes.Buffer
232 c.Stdout, c.JSON = &out, true
233 if code := runNotificationsDeviceAdd(c, nil); code != 0 {
234 t.Fatalf("exit %d", code)
235 }
236 devices, _ := c.Store.PushDevices(c.User.ID)
237 if len(devices) != 1 {
238 t.Fatalf("want one device, got %d", len(devices))
239 }
240 want := fmt.Sprintf(`"id":%d`, devices[0].ID)
241 if !strings.Contains(out.String(), want) {
242 t.Fatalf("output %s does not carry %s", out.String(), want)
243 }
244}
245
246func TestNotificationsDeviceAddPlainSaysRegisteredDevice(t *testing.T) {
247 c := notifTestCtx(t, "alice")
248 c.Stdin = strings.NewReader("DEVTOKEN\n")
249 var out bytes.Buffer
250 c.Stdout, c.JSON = &out, false
251 if code := runNotificationsDeviceAdd(c, nil); code != 0 {
252 t.Fatalf("exit %d", code)
253 }
254 if got := out.String(); !strings.HasPrefix(got, "registered device ") {
255 t.Errorf("device add printed %q", got)
256 }
257}
258
259func TestNotificationsSettingsShowsPush(t *testing.T) {
260 c := notifTestCtx(t, "alice")
261 var out bytes.Buffer
262 c.Stdout, c.JSON = &out, true
263 if code := runNotificationsSettingsShow(c, nil); code != 0 {
264 t.Fatalf("exit %d", code)
265 }
266 if !strings.Contains(out.String(), `"push":true`) {
267 t.Fatalf("no push key: %s", out.String())
268 }
269}