internal/control/notifications_test.go

e9566eed86ebcd185c4b85f63d667b5e671fe787
gitbay/internal/control/notifications_test.go history · blame · raw

256 lines · 8333 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 TestNotificationsSettingsShowsPush(t *testing.T) {
247	c := notifTestCtx(t, "alice")
248	var out bytes.Buffer
249	c.Stdout, c.JSON = &out, true
250	if code := runNotificationsSettingsShow(c, nil); code != 0 {
251		t.Fatalf("exit %d", code)
252	}
253	if !strings.Contains(out.String(), `"push":true`) {
254		t.Fatalf("no push key: %s", out.String())
255	}
256}