e2e/inbox_test.go
144 lines · 5123 bytes
1package e2e
2
3import (
4 "encoding/json"
5 "strconv"
6 "strings"
7 "testing"
8)
9
10// notices reads a user's inbox. The instance under test has no SMTP, so
11// this also asserts the inbox is filed independently of mail.
12func notices(t *testing.T, inst *instance, key string, args ...string) []struct {
13 ID int64 `json:"id"`
14 Repo string `json:"repo"`
15 Kind string `json:"kind"`
16 Actor string `json:"actor"`
17 Summary string `json:"summary"`
18 Path string `json:"path"`
19 ReadAt string `json:"read_at"`
20} {
21 t.Helper()
22 out, errOut, code := inst.ssh(t, key, "", append([]string{"notifications", "list"}, append(args, "--json")...)...)
23 if code != 0 {
24 t.Fatalf("notifications list: %s", errOut)
25 }
26 var env struct {
27 Data []struct {
28 ID int64 `json:"id"`
29 Repo string `json:"repo"`
30 Kind string `json:"kind"`
31 Actor string `json:"actor"`
32 Summary string `json:"summary"`
33 Path string `json:"path"`
34 ReadAt string `json:"read_at"`
35 } `json:"data"`
36 }
37 if err := json.Unmarshal([]byte(out), &env); err != nil {
38 t.Fatalf("not JSON: %v\n%s", err, out)
39 }
40 return env.Data
41}
42
43func TestNotificationInbox(t *testing.T) {
44 inst := startInstance(t)
45
46 aliceKey := inst.newKey(t, "alice")
47 bobKey := inst.newKey(t, "bob")
48 eveKey := inst.newKey(t, "eve")
49 inst.admin(t, "admin", "user", "create", "alice", "--key", aliceKey+".pub")
50 inst.admin(t, "admin", "user", "create", "bob", "--key", bobKey+".pub")
51 inst.admin(t, "admin", "user", "create", "eve", "--key", eveKey+".pub")
52
53 if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/app"); code != 0 {
54 t.Fatalf("repo create: %s", errOut)
55 }
56
57 // Bob opens an issue. The owner hears about it; the actor does not.
58 if _, errOut, code := inst.ssh(t, bobKey, "", "issue", "create", "alice/app",
59 "--title", "'it leaks'", "--body", "'memory climbs'"); code != 0 {
60 t.Fatalf("issue create: %s", errOut)
61 }
62 got := notices(t, inst, aliceKey)
63 if len(got) != 1 || got[0].Actor != "bob" || got[0].Summary != "opened issue #1" ||
64 got[0].Repo != "alice/app" || got[0].Kind != "issue" || got[0].Path != "alice/app/issues/1" {
65 t.Fatalf("alice inbox = %+v", got)
66 }
67 if n := notices(t, inst, bobKey); len(n) != 0 {
68 t.Fatalf("actor notified about own action: %+v", n)
69 }
70
71 // Eve neither owns the repository nor took part in the issue, so
72 // nothing has reached her.
73 if n := notices(t, inst, eveKey); len(n) != 0 {
74 t.Fatalf("uninvolved user notified: %+v", n)
75 }
76
77 // Watching widens the recipients to her.
78 if _, errOut, code := inst.ssh(t, eveKey, "", "repo", "watch", "alice/app"); code != 0 {
79 t.Fatalf("repo watch: %s", errOut)
80 }
81 if _, _, code := inst.ssh(t, aliceKey, "", "issue", "comment", "alice/app", "1", "--message", "'on it'"); code != 0 {
82 t.Fatal("alice comment failed")
83 }
84 eve := notices(t, inst, eveKey)
85 if len(eve) != 1 || eve[0].Summary != "commented on #1" || eve[0].Actor != "alice" {
86 t.Fatalf("watcher inbox = %+v", eve)
87 }
88 if n := len(notices(t, inst, bobKey)); n != 1 {
89 t.Fatalf("issue author inbox = %d rows, want 1", n)
90 }
91
92 // Muting beats being a participant: bob wrote the issue and still
93 // hears nothing more, while the watcher does.
94 if _, errOut, code := inst.ssh(t, bobKey, "", "repo", "unwatch", "alice/app"); code != 0 {
95 t.Fatalf("repo unwatch: %s", errOut)
96 }
97 if _, _, code := inst.ssh(t, aliceKey, "", "issue", "comment", "alice/app", "1", "--message", "'more'"); code != 0 {
98 t.Fatal("alice comment failed")
99 }
100 if n := len(notices(t, inst, bobKey)); n != 1 {
101 t.Fatalf("muted participant inbox = %d rows, want 1", n)
102 }
103 if n := len(notices(t, inst, eveKey)); n != 2 {
104 t.Fatalf("watcher inbox = %d rows, want 2", n)
105 }
106
107 // Reading one drops it from the default list but not from --all.
108 eve = notices(t, inst, eveKey)
109 if _, errOut, code := inst.ssh(t, eveKey, "", "notifications", "read", strconv.FormatInt(eve[0].ID, 10)); code != 0 {
110 t.Fatalf("notifications read: %s", errOut)
111 }
112 if n := len(notices(t, inst, eveKey)); n != 1 {
113 t.Fatalf("unread list = %d rows, want 1", n)
114 }
115 all := notices(t, inst, eveKey, "--all")
116 if len(all) != 2 || all[0].ReadAt == "" {
117 t.Fatalf("--all = %+v", all)
118 }
119
120 // The dashboard carries the badge count, and --all sweeps it to zero.
121 out, _, code := inst.ssh(t, eveKey, "", "dashboard", "--json")
122 if code != 0 || !strings.Contains(out, `"unread":1`) {
123 t.Fatalf("dashboard unread: %s", out)
124 }
125 if _, errOut, code := inst.ssh(t, eveKey, "", "notifications", "read", "--all"); code != 0 {
126 t.Fatalf("read --all: %s", errOut)
127 }
128 if n := len(notices(t, inst, eveKey)); n != 0 {
129 t.Fatalf("unread after sweep = %d", n)
130 }
131
132 // One account cannot mark another's notifications read.
133 alice := notices(t, inst, aliceKey)
134 if len(alice) == 0 {
135 t.Fatal("alice has no notices to test with")
136 }
137 if out, _, code := inst.ssh(t, eveKey, "", "notifications", "read", strconv.FormatInt(alice[0].ID, 10), "--json"); code != 0 ||
138 !strings.Contains(out, `"read":0`) {
139 t.Fatalf("cross-account read: %s", out)
140 }
141 if n := len(notices(t, inst, aliceKey)); n != len(alice) {
142 t.Fatal("another account cleared alice's inbox")
143 }
144}