Line data Source code
1 : // Copyright (C) 2017 The Android Open Source Project
2 : //
3 : // Licensed under the Apache License, Version 2.0 (the "License");
4 : // you may not use this file except in compliance with the License.
5 : // You may obtain a copy of the License at
6 : //
7 : // http://www.apache.org/licenses/LICENSE-2.0
8 : //
9 : // Unless required by applicable law or agreed to in writing, software
10 : // distributed under the License is distributed on an "AS IS" BASIS,
11 : // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 : // See the License for the specific language governing permissions and
13 : // limitations under the License.
14 :
15 : package com.google.gerrit.acceptance;
16 :
17 : import static com.google.common.truth.Fact.fact;
18 : import static com.google.common.truth.Truth.assertAbout;
19 : import static com.google.gerrit.extensions.api.changes.RecipientType.BCC;
20 : import static com.google.gerrit.extensions.api.changes.RecipientType.CC;
21 : import static com.google.gerrit.extensions.api.changes.RecipientType.TO;
22 : import static java.util.stream.Collectors.toList;
23 :
24 : import com.google.common.base.Joiner;
25 : import com.google.common.collect.ImmutableList;
26 : import com.google.common.truth.FailureMetadata;
27 : import com.google.common.truth.Subject;
28 : import com.google.common.truth.Truth;
29 : import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations;
30 : import com.google.gerrit.common.Nullable;
31 : import com.google.gerrit.entities.Address;
32 : import com.google.gerrit.entities.EmailHeader;
33 : import com.google.gerrit.entities.EmailHeader.AddressList;
34 : import com.google.gerrit.entities.EmailHeader.StringEmailHeader;
35 : import com.google.gerrit.entities.NotifyConfig.NotifyType;
36 : import com.google.gerrit.extensions.api.changes.RecipientType;
37 : import com.google.gerrit.extensions.api.changes.ReviewInput;
38 : import com.google.gerrit.extensions.api.changes.ReviewResult;
39 : import com.google.gerrit.extensions.api.projects.ConfigInput;
40 : import com.google.gerrit.extensions.client.GeneralPreferencesInfo;
41 : import com.google.gerrit.extensions.client.GeneralPreferencesInfo.EmailStrategy;
42 : import com.google.gerrit.extensions.client.InheritableBoolean;
43 : import com.google.gerrit.extensions.client.ReviewerState;
44 : import com.google.gerrit.testing.FakeEmailSender;
45 : import com.google.gerrit.testing.FakeEmailSender.Message;
46 : import com.google.inject.Inject;
47 : import java.util.ArrayList;
48 : import java.util.HashMap;
49 : import java.util.HashSet;
50 : import java.util.List;
51 : import java.util.Map;
52 : import java.util.Set;
53 : import java.util.function.Function;
54 : import org.eclipse.jgit.junit.TestRepository;
55 : import org.junit.After;
56 : import org.junit.Before;
57 :
58 1 : public abstract class AbstractNotificationTest extends AbstractDaemonTest {
59 : @Inject private RequestScopeOperations requestScopeOperations;
60 :
61 : @Before
62 : public void enableReviewerByEmail() throws Exception {
63 1 : requestScopeOperations.setApiUser(admin.id());
64 1 : ConfigInput conf = new ConfigInput();
65 1 : conf.enableReviewerByEmail = InheritableBoolean.TRUE;
66 1 : gApi.projects().name(project.get()).config(conf);
67 1 : }
68 :
69 : @Override
70 : protected ProjectResetter.Config resetProjects() {
71 : // Don't reset anything so that stagedUsers can be cached across all tests.
72 : // Without this caching these tests become much too slow.
73 1 : return new ProjectResetter.Config();
74 : }
75 :
76 : protected static FakeEmailSenderSubject assertThat(FakeEmailSender sender) {
77 1 : return assertAbout(fakeEmailSenders()).that(sender);
78 : }
79 :
80 : protected static Subject.Factory<FakeEmailSenderSubject, FakeEmailSender> fakeEmailSenders() {
81 1 : return FakeEmailSenderSubject::new;
82 : }
83 :
84 : protected void setEmailStrategy(TestAccount account, EmailStrategy strategy) throws Exception {
85 1 : setEmailStrategy(account, strategy, true);
86 1 : }
87 :
88 : protected void setEmailStrategy(TestAccount account, EmailStrategy strategy, boolean record)
89 : throws Exception {
90 1 : if (record) {
91 1 : accountsModifyingEmailStrategy.add(account);
92 : }
93 1 : requestScopeOperations.setApiUser(account.id());
94 1 : GeneralPreferencesInfo prefs = gApi.accounts().self().getPreferences();
95 1 : prefs.emailStrategy = strategy;
96 1 : gApi.accounts().self().setPreferences(prefs);
97 1 : }
98 :
99 : protected static class FakeEmailSenderSubject extends Subject {
100 : private final FakeEmailSender fakeEmailSender;
101 : private String emailTitle;
102 : private Message message;
103 : private StagedUsers users;
104 1 : private Map<RecipientType, List<String>> recipients = new HashMap<>();
105 1 : private Set<String> accountedFor = new HashSet<>();
106 :
107 : FakeEmailSenderSubject(FailureMetadata failureMetadata, FakeEmailSender target) {
108 1 : super(failureMetadata, target);
109 1 : fakeEmailSender = target;
110 1 : }
111 :
112 : public FakeEmailSenderSubject didNotSend() {
113 1 : Message message = fakeEmailSender.peekMessage();
114 1 : if (message != null) {
115 0 : failWithoutActual(fact("expected no message", message));
116 : }
117 1 : return this;
118 : }
119 :
120 : public FakeEmailSenderSubject sent(String messageType, StagedUsers users) {
121 1 : message = fakeEmailSender.nextMessage();
122 1 : if (message == null) {
123 0 : failWithoutActual(fact("expected message", "not sent"));
124 : }
125 1 : recipients = new HashMap<>();
126 1 : recipients.put(TO, parseAddresses(message, "To"));
127 1 : recipients.put(CC, parseAddresses(message, "Cc"));
128 1 : recipients.put(
129 : BCC,
130 1 : message.rcpt().stream()
131 1 : .map(Address::email)
132 1 : .filter(e -> !recipients.get(TO).contains(e) && !recipients.get(CC).contains(e))
133 1 : .collect(toList()));
134 1 : this.users = users;
135 1 : if (!message.headers().containsKey("X-Gerrit-MessageType")) {
136 0 : failWithoutActual(
137 0 : fact("expected to have message sent with", "X-Gerrit-MessageType header"));
138 : }
139 1 : EmailHeader header = message.headers().get("X-Gerrit-MessageType");
140 1 : if (!header.equals(new StringEmailHeader(messageType))) {
141 0 : failWithoutActual(
142 0 : fact("expected message of type", messageType),
143 0 : fact(
144 : "actual",
145 0 : header instanceof StringEmailHeader
146 0 : ? ((StringEmailHeader) header).getString()
147 0 : : header));
148 : }
149 1 : EmailHeader titleHeader = message.headers().get("Subject");
150 1 : if (titleHeader instanceof StringEmailHeader) {
151 1 : emailTitle = ((StringEmailHeader) titleHeader).getString();
152 : }
153 :
154 1 : return this;
155 : }
156 :
157 : private static String recipientMapToString(
158 : Map<RecipientType, List<String>> recipients, Function<String, String> emailToName) {
159 0 : StringBuilder buf = new StringBuilder();
160 0 : buf.append('[');
161 0 : for (RecipientType type : ImmutableList.of(TO, CC, BCC)) {
162 0 : buf.append('\n');
163 0 : buf.append(type);
164 0 : buf.append(':');
165 0 : String delim = " ";
166 0 : for (String r : recipients.get(type)) {
167 0 : buf.append(delim);
168 0 : buf.append(emailToName.apply(r));
169 0 : delim = ", ";
170 0 : }
171 0 : }
172 0 : buf.append("\n]");
173 0 : return buf.toString();
174 : }
175 :
176 : List<String> parseAddresses(Message msg, String headerName) {
177 1 : EmailHeader header = msg.headers().get(headerName);
178 1 : if (header == null) {
179 0 : return ImmutableList.of();
180 : }
181 1 : Truth.assertThat(header).isInstanceOf(AddressList.class);
182 1 : AddressList addrList = (AddressList) header;
183 1 : return addrList.getAddressList().stream().map(Address::email).collect(toList());
184 : }
185 :
186 : public FakeEmailSenderSubject to(String... emails) {
187 1 : return rcpt(users.supportReviewersByEmail ? TO : null, emails);
188 : }
189 :
190 : public FakeEmailSenderSubject cc(String... emails) {
191 1 : return rcpt(users.supportReviewersByEmail ? CC : null, emails);
192 : }
193 :
194 : public FakeEmailSenderSubject bcc(String... emails) {
195 0 : return rcpt(users.supportReviewersByEmail ? BCC : null, emails);
196 : }
197 :
198 : public FakeEmailSenderSubject title(String expectedEmailTitle) {
199 1 : if (!emailTitle.equals(expectedEmailTitle)) {
200 0 : failWithoutActual(
201 0 : fact("Expected email title", expectedEmailTitle),
202 0 : fact("but actual title is", emailTitle));
203 : }
204 1 : return this;
205 : }
206 :
207 : private FakeEmailSenderSubject rcpt(@Nullable RecipientType type, String[] emails) {
208 1 : for (String email : emails) {
209 1 : rcpt(type, email);
210 : }
211 1 : return this;
212 : }
213 :
214 : private void rcpt(@Nullable RecipientType type, String email) {
215 1 : rcpt(TO, email, TO.equals(type));
216 1 : rcpt(CC, email, CC.equals(type));
217 1 : rcpt(BCC, email, BCC.equals(type));
218 1 : }
219 :
220 : private void rcpt(@Nullable RecipientType type, String email, boolean expected) {
221 1 : if (recipients.get(type).contains(email) != expected) {
222 0 : failWithoutActual(
223 0 : fact(
224 0 : expected ? "expected to notify" : "expected not to notify",
225 0 : type + ": " + users.emailToName(email)),
226 0 : fact("but notified", recipientMapToString(recipients, users::emailToName)));
227 : }
228 1 : if (expected) {
229 1 : accountedFor.add(email);
230 : }
231 1 : }
232 :
233 : public FakeEmailSenderSubject noOneElse() {
234 1 : for (Map.Entry<NotifyType, TestAccount> watchEntry : users.watchers.entrySet()) {
235 1 : if (!accountedFor.contains(watchEntry.getValue().email())) {
236 1 : notTo(watchEntry.getKey());
237 : }
238 1 : }
239 :
240 1 : Map<RecipientType, List<String>> unaccountedFor = new HashMap<>();
241 1 : boolean ok = true;
242 1 : for (Map.Entry<RecipientType, List<String>> entry : recipients.entrySet()) {
243 1 : unaccountedFor.put(entry.getKey(), new ArrayList<>());
244 1 : for (String address : entry.getValue()) {
245 1 : if (!accountedFor.contains(address)) {
246 0 : unaccountedFor.get(entry.getKey()).add(address);
247 0 : ok = false;
248 : }
249 1 : }
250 1 : }
251 1 : if (!ok) {
252 0 : failWithoutActual(
253 0 : fact(
254 : "expected assertions for",
255 0 : recipientMapToString(unaccountedFor, e -> users.emailToName(e))));
256 : }
257 1 : return this;
258 : }
259 :
260 : public FakeEmailSenderSubject notTo(String... emails) {
261 0 : return rcpt(null, emails);
262 : }
263 :
264 : public FakeEmailSenderSubject to(TestAccount... accounts) {
265 1 : return rcpt(TO, accounts);
266 : }
267 :
268 : public FakeEmailSenderSubject cc(TestAccount... accounts) {
269 1 : return rcpt(CC, accounts);
270 : }
271 :
272 : public FakeEmailSenderSubject bcc(TestAccount... accounts) {
273 1 : return rcpt(BCC, accounts);
274 : }
275 :
276 : public FakeEmailSenderSubject notTo(TestAccount... accounts) {
277 1 : return rcpt(null, accounts);
278 : }
279 :
280 : private FakeEmailSenderSubject rcpt(@Nullable RecipientType type, TestAccount[] accounts) {
281 1 : for (TestAccount account : accounts) {
282 1 : rcpt(type, account);
283 : }
284 1 : return this;
285 : }
286 :
287 : private void rcpt(@Nullable RecipientType type, TestAccount account) {
288 1 : rcpt(type, account.email());
289 1 : }
290 :
291 : public FakeEmailSenderSubject to(NotifyType... watches) {
292 0 : return rcpt(TO, watches);
293 : }
294 :
295 : public FakeEmailSenderSubject cc(NotifyType... watches) {
296 0 : return rcpt(CC, watches);
297 : }
298 :
299 : public FakeEmailSenderSubject bcc(NotifyType... watches) {
300 1 : return rcpt(BCC, watches);
301 : }
302 :
303 : public FakeEmailSenderSubject notTo(NotifyType... watches) {
304 1 : return rcpt(null, watches);
305 : }
306 :
307 : private FakeEmailSenderSubject rcpt(@Nullable RecipientType type, NotifyType[] watches) {
308 1 : for (NotifyType watch : watches) {
309 1 : rcpt(type, watch);
310 : }
311 1 : return this;
312 : }
313 :
314 : private void rcpt(@Nullable RecipientType type, NotifyType watch) {
315 1 : if (!users.watchers.containsKey(watch)) {
316 0 : failWithoutActual(fact("expected to be configured to watch", watch));
317 : }
318 1 : rcpt(type, users.watchers.get(watch));
319 1 : }
320 : }
321 :
322 1 : private static final Map<String, StagedUsers> stagedUsers = new HashMap<>();
323 :
324 : // TestAccount doesn't implement hashCode/equals, so this set is according
325 : // to object identity. That's fine for our purposes.
326 1 : private Set<TestAccount> accountsModifyingEmailStrategy = new HashSet<>();
327 :
328 : @After
329 : public void resetEmailStrategies() throws Exception {
330 1 : for (TestAccount account : accountsModifyingEmailStrategy) {
331 1 : setEmailStrategy(account, EmailStrategy.ENABLED, false);
332 1 : }
333 1 : accountsModifyingEmailStrategy.clear();
334 1 : }
335 :
336 : protected class StagedUsers {
337 : public static final String REVIEWER_BY_EMAIL = "reviewerByEmail@example.com";
338 : public static final String CC_BY_EMAIL = "ccByEmail@example.com";
339 :
340 : public final TestAccount owner;
341 : public final TestAccount author;
342 : public final TestAccount uploader;
343 : public final TestAccount reviewer;
344 : public final TestAccount ccer;
345 : public final TestAccount starrer;
346 : public final TestAccount assignee;
347 : public final TestAccount watchingProjectOwner;
348 1 : private final Map<NotifyType, TestAccount> watchers = new HashMap<>();
349 1 : private final Map<String, TestAccount> accountsByEmail = new HashMap<>();
350 :
351 : public boolean supportReviewersByEmail;
352 :
353 : private String usersCacheKey() {
354 1 : return description.getClassName();
355 : }
356 :
357 : private TestAccount reindexAndCopy(TestAccount account) {
358 1 : reindexAccount(account.id());
359 1 : return account;
360 : }
361 :
362 1 : public StagedUsers() throws Exception {
363 1 : synchronized (stagedUsers) {
364 1 : if (stagedUsers.containsKey(usersCacheKey())) {
365 1 : StagedUsers existing = stagedUsers.get(usersCacheKey());
366 1 : owner = reindexAndCopy(existing.owner);
367 1 : author = reindexAndCopy(existing.author);
368 1 : uploader = reindexAndCopy(existing.uploader);
369 1 : reviewer = reindexAndCopy(existing.reviewer);
370 1 : ccer = reindexAndCopy(existing.ccer);
371 1 : starrer = reindexAndCopy(existing.starrer);
372 1 : assignee = reindexAndCopy(existing.assignee);
373 1 : watchingProjectOwner = reindexAndCopy(existing.watchingProjectOwner);
374 1 : watchers.putAll(existing.watchers);
375 1 : return;
376 : }
377 :
378 1 : owner = testAccount("owner");
379 1 : reviewer = testAccount("reviewer");
380 1 : author = testAccount("author");
381 1 : uploader = testAccount("uploader");
382 1 : ccer = testAccount("ccer");
383 1 : starrer = testAccount("starrer");
384 1 : assignee = testAccount("assignee");
385 :
386 1 : watchingProjectOwner = testAccount("watchingProjectOwner", "Administrators");
387 1 : requestScopeOperations.setApiUser(watchingProjectOwner.id());
388 1 : watch(allProjects.get(), pwi -> pwi.notifyNewChanges = true);
389 :
390 1 : for (NotifyType watch : NotifyType.values()) {
391 1 : if (watch == NotifyType.ALL) {
392 1 : continue;
393 : }
394 1 : TestAccount watcher = testAccount(watch.toString());
395 1 : requestScopeOperations.setApiUser(watcher.id());
396 1 : watch(
397 1 : allProjects.get(),
398 : pwi -> {
399 1 : pwi.notifyAllComments = watch.equals(NotifyType.ALL_COMMENTS);
400 1 : pwi.notifyAbandonedChanges = watch.equals(NotifyType.ABANDONED_CHANGES);
401 1 : pwi.notifyNewChanges = watch.equals(NotifyType.NEW_CHANGES);
402 1 : pwi.notifyNewPatchSets = watch.equals(NotifyType.NEW_PATCHSETS);
403 1 : pwi.notifySubmittedChanges = watch.equals(NotifyType.SUBMITTED_CHANGES);
404 1 : });
405 1 : watchers.put(watch, watcher);
406 : }
407 :
408 1 : stagedUsers.put(usersCacheKey(), this);
409 1 : }
410 1 : }
411 :
412 : private String email(String username) {
413 : // Email validator rejects usernames longer than 64 bytes.
414 1 : if (username.length() > 64) {
415 1 : username = username.substring(username.length() - 64);
416 1 : if (username.startsWith(".")) {
417 1 : username = username.substring(1);
418 : }
419 : }
420 1 : return username + "@example.com";
421 : }
422 :
423 : public TestAccount testAccount(String name) throws Exception {
424 1 : String username = name(name);
425 1 : TestAccount account = accountCreator.create(username, email(username), name, null);
426 1 : accountsByEmail.put(account.email(), account);
427 1 : return account;
428 : }
429 :
430 : public TestAccount testAccount(String name, String groupName) throws Exception {
431 1 : String username = name(name);
432 1 : TestAccount account = accountCreator.create(username, email(username), name, null, groupName);
433 1 : accountsByEmail.put(account.email(), account);
434 1 : return account;
435 : }
436 :
437 : String emailToName(String email) {
438 0 : if (accountsByEmail.containsKey(email)) {
439 0 : return accountsByEmail.get(email).fullName();
440 : }
441 0 : return email;
442 : }
443 :
444 : protected void addReviewers(PushOneCommit.Result r) throws Exception {
445 : ReviewInput in =
446 1 : ReviewInput.noScore()
447 1 : .reviewer(reviewer.email())
448 1 : .reviewer(REVIEWER_BY_EMAIL)
449 1 : .reviewer(ccer.email(), ReviewerState.CC, false)
450 1 : .reviewer(CC_BY_EMAIL, ReviewerState.CC, false);
451 1 : ReviewResult result = gApi.changes().id(r.getChangeId()).current().review(in);
452 1 : supportReviewersByEmail = true;
453 1 : if (result.reviewers.values().stream().anyMatch(v -> v.error != null)) {
454 0 : supportReviewersByEmail = false;
455 : in =
456 0 : ReviewInput.noScore()
457 0 : .reviewer(reviewer.email())
458 0 : .reviewer(ccer.email(), ReviewerState.CC, false);
459 0 : result = gApi.changes().id(r.getChangeId()).current().review(in);
460 : }
461 1 : Truth.assertThat(result.reviewers.values().stream().allMatch(v -> v.error == null)).isTrue();
462 1 : }
463 : }
464 :
465 : protected interface PushOptionGenerator {
466 : List<String> pushOptions(StagedUsers users);
467 : }
468 :
469 : protected class StagedPreChange extends StagedUsers {
470 : public final TestRepository<?> repo;
471 : protected final PushOneCommit.Result result;
472 : public final String changeId;
473 :
474 : StagedPreChange(String ref) throws Exception {
475 1 : this(ref, null);
476 1 : }
477 :
478 : StagedPreChange(String ref, @Nullable PushOptionGenerator pushOptionGenerator)
479 1 : throws Exception {
480 1 : super();
481 1 : List<String> pushOptions = null;
482 1 : if (pushOptionGenerator != null) {
483 1 : pushOptions = pushOptionGenerator.pushOptions(this);
484 : }
485 1 : if (pushOptions != null) {
486 1 : ref = ref + '%' + Joiner.on(',').join(pushOptions);
487 : }
488 1 : requestScopeOperations.setApiUser(owner.id());
489 1 : repo = cloneProject(project, owner);
490 1 : PushOneCommit push = pushFactory.create(owner.newIdent(), repo);
491 1 : result = push.to(ref);
492 1 : result.assertOkStatus();
493 1 : changeId = result.getChangeId();
494 1 : }
495 : }
496 :
497 : protected StagedPreChange stagePreChange(String ref) throws Exception {
498 1 : return new StagedPreChange(ref);
499 : }
500 :
501 : protected StagedPreChange stagePreChange(
502 : String ref, @Nullable PushOptionGenerator pushOptionGenerator) throws Exception {
503 1 : return new StagedPreChange(ref, pushOptionGenerator);
504 : }
505 :
506 : protected class StagedChange extends StagedPreChange {
507 1 : StagedChange(String ref) throws Exception {
508 1 : super(ref);
509 :
510 1 : requestScopeOperations.setApiUser(starrer.id());
511 1 : gApi.accounts().self().starChange(result.getChangeId());
512 :
513 1 : requestScopeOperations.setApiUser(owner.id());
514 1 : addReviewers(result);
515 1 : sender.clear();
516 1 : }
517 : }
518 :
519 : protected StagedChange stageReviewableChange() throws Exception {
520 1 : StagedChange sc = new StagedChange("refs/for/master");
521 1 : sender.clear();
522 1 : return sc;
523 : }
524 :
525 : protected StagedChange stageWipChange() throws Exception {
526 1 : StagedChange sc = new StagedChange("refs/for/master%wip");
527 1 : sender.clear();
528 1 : return sc;
529 : }
530 :
531 : protected StagedChange stageReviewableWipChange() throws Exception {
532 1 : StagedChange sc = stageReviewableChange();
533 1 : requestScopeOperations.setApiUser(sc.owner.id());
534 1 : gApi.changes().id(sc.changeId).setWorkInProgress();
535 1 : sender.clear();
536 1 : return sc;
537 : }
538 :
539 : protected StagedChange stageAbandonedReviewableChange() throws Exception {
540 1 : StagedChange sc = stageReviewableChange();
541 1 : requestScopeOperations.setApiUser(sc.owner.id());
542 1 : gApi.changes().id(sc.changeId).abandon();
543 1 : sender.clear();
544 1 : return sc;
545 : }
546 :
547 : protected StagedChange stageAbandonedReviewableWipChange() throws Exception {
548 1 : StagedChange sc = stageReviewableWipChange();
549 1 : requestScopeOperations.setApiUser(sc.owner.id());
550 1 : gApi.changes().id(sc.changeId).abandon();
551 1 : sender.clear();
552 1 : return sc;
553 : }
554 :
555 : protected StagedChange stageAbandonedWipChange() throws Exception {
556 1 : StagedChange sc = stageWipChange();
557 1 : requestScopeOperations.setApiUser(sc.owner.id());
558 1 : gApi.changes().id(sc.changeId).abandon();
559 1 : sender.clear();
560 1 : return sc;
561 : }
562 : }
|