e2e/inbox_test.go
145 lines · 5131 bytes
2 symbols in this file
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 t.Parallel()
45 inst := startInstance(t)
46
47 aliceKey := inst.newKey(t, "alice")
48 bobKey := inst.newKey(t, "bob")
49 eveKey := inst.newKey(t, "eve")
50 inst.admin(t, "admin", "user", "create", "alice", "--key", aliceKey+".pub")
51 inst.admin(t, "admin", "user", "create", "bob", "--key", bobKey+".pub")
52 inst.admin(t, "admin", "user", "create", "eve", "--key", eveKey+".pub")
53
54 if _, errOut, code := inst.ssh(t, aliceKey, "", "repo", "create", "alice/app"); code != 0 {
55 t.Fatalf("repo create: %s", errOut)
56 }
57
58 // Bob opens an issue. The owner hears about it; the actor does not.
59 if _, errOut, code := inst.ssh(t, bobKey, "", "issue", "create", "alice/app",
60 "--title", "'it leaks'", "--body", "'memory climbs'"); code != 0 {
61 t.Fatalf("issue create: %s", errOut)
62 }
63 got := notices(t, inst, aliceKey)
64 if len(got) != 1 || got[0].Actor != "bob" || got[0].Summary != "opened issue #1" ||
65 got[0].Repo != "alice/app" || got[0].Kind != "issue" || got[0].Path != "alice/app/issues/1" {
66 t.Fatalf("alice inbox = %+v", got)
67 }
68 if n := notices(t, inst, bobKey); len(n) != 0 {
69 t.Fatalf("actor notified about own action: %+v", n)
70 }
71
72 // Eve neither owns the repository nor took part in the issue, so
73 // nothing has reached her.
74 if n := notices(t, inst, eveKey); len(n) != 0 {
75 t.Fatalf("uninvolved user notified: %+v", n)
76 }
77
78 // Watching widens the recipients to her.
79 if _, errOut, code := inst.ssh(t, eveKey, "", "repo", "watch", "alice/app"); code != 0 {
80 t.Fatalf("repo watch: %s", errOut)
81 }
82 if _, _, code := inst.ssh(t, aliceKey, "", "issue", "comment", "alice/app", "1", "--message", "'on it'"); code != 0 {
83 t.Fatal("alice comment failed")
84 }
85 eve := notices(t, inst, eveKey)
86 if len(eve) != 1 || eve[0].Summary != "commented on #1" || eve[0].Actor != "alice" {
87 t.Fatalf("watcher inbox = %+v", eve)
88 }
89 if n := len(notices(t, inst, bobKey)); n != 1 {
90 t.Fatalf("issue author inbox = %d rows, want 1", n)
91 }
92
93 // Muting beats being a participant: bob wrote the issue and still
94 // hears nothing more, while the watcher does.
95 if _, errOut, code := inst.ssh(t, bobKey, "", "repo", "mute", "alice/app"); code != 0 {
96 t.Fatalf("repo mute: %s", errOut)
97 }
98 if _, _, code := inst.ssh(t, aliceKey, "", "issue", "comment", "alice/app", "1", "--message", "'more'"); code != 0 {
99 t.Fatal("alice comment failed")
100 }
101 if n := len(notices(t, inst, bobKey)); n != 1 {
102 t.Fatalf("muted participant inbox = %d rows, want 1", n)
103 }
104 if n := len(notices(t, inst, eveKey)); n != 2 {
105 t.Fatalf("watcher inbox = %d rows, want 2", n)
106 }
107
108 // Reading one drops it from the default list but not from --all.
109 eve = notices(t, inst, eveKey)
110 if _, errOut, code := inst.ssh(t, eveKey, "", "notifications", "read", strconv.FormatInt(eve[0].ID, 10)); code != 0 {
111 t.Fatalf("notifications read: %s", errOut)
112 }
113 if n := len(notices(t, inst, eveKey)); n != 1 {
114 t.Fatalf("unread list = %d rows, want 1", n)
115 }
116 all := notices(t, inst, eveKey, "--all")
117 if len(all) != 2 || all[0].ReadAt == "" {
118 t.Fatalf("--all = %+v", all)
119 }
120
121 // The dashboard carries the badge count, and --all sweeps it to zero.
122 out, _, code := inst.ssh(t, eveKey, "", "dashboard", "--json")
123 if code != 0 || !strings.Contains(out, `"unread":1`) {
124 t.Fatalf("dashboard unread: %s", out)
125 }
126 if _, errOut, code := inst.ssh(t, eveKey, "", "notifications", "read", "--all"); code != 0 {
127 t.Fatalf("read --all: %s", errOut)
128 }
129 if n := len(notices(t, inst, eveKey)); n != 0 {
130 t.Fatalf("unread after sweep = %d", n)
131 }
132
133 // One account cannot mark another's notifications read.
134 alice := notices(t, inst, aliceKey)
135 if len(alice) == 0 {
136 t.Fatal("alice has no notices to test with")
137 }
138 if out, _, code := inst.ssh(t, eveKey, "", "notifications", "read", strconv.FormatInt(alice[0].ID, 10), "--json"); code != 0 ||
139 !strings.Contains(out, `"read":0`) {
140 t.Fatalf("cross-account read: %s", out)
141 }
142 if n := len(notices(t, inst, aliceKey)); n != len(alice) {
143 t.Fatal("another account cleared alice's inbox")
144 }
145}