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