internal/store/inbox_test.go
239 lines · 7516 bytes
1package store
2
3import "testing"
4
5func inboxFixture(t *testing.T) (s *Store, repoID, owner, other int64) {
6 t.Helper()
7 s = open(t)
8 if err := s.MigrateUp(); err != nil {
9 t.Fatal(err)
10 }
11 owner, err := s.CreateUser("cmc", true)
12 if err != nil {
13 t.Fatal(err)
14 }
15 other, err = s.CreateUser("kim", false)
16 if err != nil {
17 t.Fatal(err)
18 }
19 repoID, err = s.CreateRepo("user", owner, "lib", "public")
20 if err != nil {
21 t.Fatal(err)
22 }
23 return s, repoID, owner, other
24}
25
26func TestInboxUnreadAndRead(t *testing.T) {
27 s, repoID, owner, _ := inboxFixture(t)
28 for _, n := range []string{"opened issue #1", "commented on #1", "closed #1"} {
29 if err := s.AddNotice(owner, repoID, "issue", "kim", n, "cmc/lib/issues/1"); err != nil {
30 t.Fatal(err)
31 }
32 }
33 if got := s.UnreadNotices(owner); got != 3 {
34 t.Fatalf("unread = %d, want 3", got)
35 }
36
37 // Newest first, and the repo path is resolved from the polymorphic owner.
38 got, err := s.Inbox(owner, true, 0, 0)
39 if err != nil {
40 t.Fatal(err)
41 }
42 if len(got) != 3 || got[0].Summary != "closed #1" || got[0].RepoPath != "cmc/lib" {
43 t.Fatalf("inbox = %+v", got)
44 }
45
46 // Marking one read drops it from the unread list but not from --all.
47 if n, err := s.MarkNoticesRead(owner, []int64{got[0].ID}); err != nil || n != 1 {
48 t.Fatalf("MarkNoticesRead = %d, %v", n, err)
49 }
50 if got := s.UnreadNotices(owner); got != 2 {
51 t.Fatalf("unread after read = %d, want 2", got)
52 }
53 all, _ := s.Inbox(owner, false, 0, 0)
54 if len(all) != 3 || all[0].ReadAt == "" {
55 t.Fatalf("all = %+v", all)
56 }
57
58 // The rest go in one sweep, and a second sweep changes nothing.
59 if n, _ := s.MarkNoticesRead(owner, nil); n != 2 {
60 t.Fatalf("sweep marked %d, want 2", n)
61 }
62 if n, _ := s.MarkNoticesRead(owner, nil); n != 0 {
63 t.Fatalf("second sweep marked %d, want 0", n)
64 }
65}
66
67// An id belonging to someone else matches nothing, so `notifications read
68// <id>` cannot reach into another account's inbox.
69func TestInboxIsPerUser(t *testing.T) {
70 s, repoID, owner, other := inboxFixture(t)
71 if err := s.AddNotice(owner, repoID, "issue", "kim", "opened issue #1", "cmc/lib/issues/1"); err != nil {
72 t.Fatal(err)
73 }
74 mine, _ := s.Inbox(owner, true, 0, 0)
75 if n, err := s.MarkNoticesRead(other, []int64{mine[0].ID}); err != nil || n != 0 {
76 t.Fatalf("cross-user read marked %d rows (%v)", n, err)
77 }
78 if s.UnreadNotices(owner) != 1 {
79 t.Fatal("another user's read cleared the owner's notice")
80 }
81 if got, _ := s.Inbox(other, true, 0, 0); len(got) != 0 {
82 t.Fatalf("other user sees %+v", got)
83 }
84}
85
86func TestInboxPaging(t *testing.T) {
87 s, repoID, owner, _ := inboxFixture(t)
88 for i := 0; i < 5; i++ {
89 if err := s.AddNotice(owner, repoID, "issue", "kim", "note", "cmc/lib/issues/1"); err != nil {
90 t.Fatal(err)
91 }
92 }
93 first, err := s.Inbox(owner, true, 2, 0)
94 if err != nil {
95 t.Fatal(err)
96 }
97 if len(first) != 2 {
98 t.Fatalf("first page %d rows, want 2", len(first))
99 }
100 next, err := s.Inbox(owner, true, 2, first[1].ID)
101 if err != nil {
102 t.Fatal(err)
103 }
104 if len(next) != 2 || next[0].ID >= first[1].ID {
105 t.Fatalf("second page %+v does not follow %+v", next, first)
106 }
107}
108
109// Watching widens the recipients, muting removes a user who would
110// otherwise be told, and the actor is never notified of their own action.
111func TestNotifyRecipients(t *testing.T) {
112 s, repoID, owner, other := inboxFixture(t)
113 third, err := s.CreateUser("lee", false)
114 if err != nil {
115 t.Fatal(err)
116 }
117
118 got, err := s.NotifyRecipients(repoID, other, []int64{owner, other}, true)
119 if err != nil {
120 t.Fatal(err)
121 }
122 if len(got) != 1 || got[0] != owner {
123 t.Fatalf("default recipients = %v, want [%d]", got, owner)
124 }
125
126 if err := s.SetRepoWatch(repoID, third, "watching"); err != nil {
127 t.Fatal(err)
128 }
129 if got, _ := s.NotifyRecipients(repoID, other, []int64{owner}, true); len(got) != 2 {
130 t.Fatalf("watcher not added: %v", got)
131 }
132
133 // A watcher who is also a target is listed once.
134 if got, _ := s.NotifyRecipients(repoID, other, []int64{owner, third}, true); len(got) != 2 {
135 t.Fatalf("watcher duplicated: %v", got)
136 }
137 // A direct notice stays with its targets; watchers are not added.
138 if got, _ := s.NotifyRecipients(repoID, other, []int64{owner}, false); len(got) != 1 || got[0] != owner {
139 t.Fatalf("direct notice widened: %v", got)
140 }
141
142 // Muting beats owning the repository.
143 if err := s.SetRepoWatch(repoID, owner, "muted"); err != nil {
144 t.Fatal(err)
145 }
146 got, _ = s.NotifyRecipients(repoID, other, []int64{owner}, true)
147 if len(got) != 1 || got[0] != third {
148 t.Fatalf("muted owner still notified: %v", got)
149 }
150 if s.RepoWatchState(repoID, owner) != "muted" {
151 t.Fatal("watch state not recorded")
152 }
153 // Clearing returns the owner to the default and they are told again.
154 if err := s.ClearRepoWatch(repoID, owner); err != nil {
155 t.Fatal(err)
156 }
157 if s.RepoWatchState(repoID, owner) != "" {
158 t.Fatal("clear left a state")
159 }
160 if got, _ := s.NotifyRecipients(repoID, other, []int64{owner}, true); len(got) != 2 {
161 t.Fatalf("cleared owner not notified: %v", got)
162 }
163 if err := s.SetRepoWatch(repoID, owner, "muted"); err != nil {
164 t.Fatal(err)
165 }
166
167 // Watching after muting replaces the row rather than adding one.
168 if err := s.SetRepoWatch(repoID, owner, "watching"); err != nil {
169 t.Fatal(err)
170 }
171 if s.RepoWatchState(repoID, owner) != "watching" {
172 t.Fatal("watch did not replace mute")
173 }
174}
175
176// With the watch preference on, an account that can write to a
177// repository is a recipient without a repo_watchers row; read access,
178// the preference off, a mute, and a direct notice each leave it out.
179func TestNotifyRecipientsDefaultWatch(t *testing.T) {
180 s, repoID, owner, other := inboxFixture(t)
181 writer, err := s.CreateUser("lee", false)
182 if err != nil {
183 t.Fatal(err)
184 }
185 reader, err := s.CreateUser("pat", false)
186 if err != nil {
187 t.Fatal(err)
188 }
189 if err := s.GrantAccess(repoID, writer, "write"); err != nil {
190 t.Fatal(err)
191 }
192 if err := s.GrantAccess(repoID, reader, "read"); err != nil {
193 t.Fatal(err)
194 }
195 for _, id := range []int64{writer, reader} {
196 if on, err := s.WatchEnabled(id); err != nil || on {
197 t.Fatalf("default preference = %v, %v", on, err)
198 }
199 if err := s.SetWatchEnabled(id, true); err != nil {
200 t.Fatal(err)
201 }
202 }
203 if on, _ := s.WatchEnabled(writer); !on {
204 t.Fatal("preference not recorded")
205 }
206
207 got, err := s.NotifyRecipients(repoID, other, []int64{owner}, true)
208 if err != nil {
209 t.Fatal(err)
210 }
211 if len(got) != 2 || got[0] != owner || got[1] != writer {
212 t.Fatalf("recipients = %v, want [%d %d]", got, owner, writer)
213 }
214 // The preference does not turn a direct notice into a broadcast.
215 if got, _ := s.NotifyRecipients(repoID, other, []int64{owner}, false); len(got) != 1 {
216 t.Fatalf("direct notice widened: %v", got)
217 }
218 // The writer acting is not told about their own action.
219 if got, _ := s.NotifyRecipients(repoID, writer, []int64{owner}, true); len(got) != 1 || got[0] != owner {
220 t.Fatalf("actor notified: %v", got)
221 }
222 // A mute on the repository beats the preference.
223 if err := s.SetRepoWatch(repoID, writer, "muted"); err != nil {
224 t.Fatal(err)
225 }
226 if got, _ := s.NotifyRecipients(repoID, other, []int64{owner}, true); len(got) != 1 {
227 t.Fatalf("muted writer notified: %v", got)
228 }
229 if err := s.ClearRepoWatch(repoID, writer); err != nil {
230 t.Fatal(err)
231 }
232 // Turning it off returns the writer to the default.
233 if err := s.SetWatchEnabled(writer, false); err != nil {
234 t.Fatal(err)
235 }
236 if got, _ := s.NotifyRecipients(repoID, other, []int64{owner}, true); len(got) != 1 {
237 t.Fatalf("preference off still widens: %v", got)
238 }
239}