internal/control/notifications_test.go

v1.32.0
gitbay/internal/control/notifications_test.go history · blame · raw

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}