1 /*
  2  * Copyright (c) 2019, Oracle and/or its affiliates. All rights reserved.
  3  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  4  *
  5  * This code is free software; you can redistribute it and/or modify it
  6  * under the terms of the GNU General Public License version 2 only, as
  7  * published by the Free Software Foundation.
  8  *
  9  * This code is distributed in the hope that it will be useful, but WITHOUT
 10  * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 11  * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 12  * version 2 for more details (a copy is included in the LICENSE file that
 13  * accompanied this code).
 14  *
 15  * You should have received a copy of the GNU General Public License version
 16  * 2 along with this work; if not, write to the Free Software Foundation,
 17  * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 18  *
 19  * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 20  * or visit www.oracle.com if you need additional information or have any
 21  * questions.
 22  */
 23 package org.openjdk.skara.bots.notify;
 24 
 25 import org.openjdk.skara.email.*;
 26 import org.openjdk.skara.forge.HostedRepository;
 27 import org.openjdk.skara.json.*;
 28 import org.openjdk.skara.mailinglist.MailingListServerFactory;
 29 import org.openjdk.skara.storage.StorageBuilder;
 30 import org.openjdk.skara.test.*;
 31 import org.openjdk.skara.vcs.Tag;
 32 
 33 import org.junit.jupiter.api.*;
 34 
 35 import java.io.IOException;
 36 import java.nio.charset.StandardCharsets;
 37 import java.nio.file.*;
 38 import java.time.Duration;
 39 import java.util.*;
 40 import java.util.regex.Pattern;
 41 import java.util.stream.Collectors;
 42 
 43 import static org.junit.jupiter.api.Assertions.*;
 44 
 45 class UpdaterTests {
 46     private List<Path> findJsonFiles(Path folder, String partialName) throws IOException {
 47         return Files.walk(folder)
 48                     .filter(path -> path.toString().endsWith(".json"))
 49                     .filter(path -> path.toString().contains(partialName))
 50                     .collect(Collectors.toList());
 51     }
 52 
 53     private StorageBuilder<Tag> createTagStorage(HostedRepository repository) throws IOException {
 54         return new StorageBuilder<Tag>("tags.txt")
 55                 .remoteRepository(repository, "refs/heads/history", "Duke", "duke@openjdk.java.net", "Updated tags");
 56     }
 57 
 58     private StorageBuilder<ResolvedBranch> createBranchStorage(HostedRepository repository) throws IOException {
 59         return new StorageBuilder<ResolvedBranch>("branches.txt")
 60                 .remoteRepository(repository, "refs/heads/history", "Duke", "duke@openjdk.java.net", "Updated branches");
 61     }
 62 
 63     @Test
 64     void testJsonUpdaterBranch(TestInfo testInfo) throws IOException {
 65         try (var credentials = new HostCredentials(testInfo);
 66              var tempFolder = new TemporaryDirectory()) {
 67             var repo = credentials.getHostedRepository();
 68             var localRepoFolder = tempFolder.path().resolve("repo");
 69             var localRepo = CheckableRepository.init(localRepoFolder, repo.repositoryType());
 70             credentials.commitLock(localRepo);
 71             localRepo.pushAll(repo.url());
 72 
 73             var tagStorage = createTagStorage(repo);
 74             var branchStorage = createBranchStorage(repo);
 75             var jsonFolder = tempFolder.path().resolve("json");
 76             Files.createDirectory(jsonFolder);
 77             var storageFolder = tempFolder.path().resolve("storage");
 78 
 79             var updater = new JsonUpdater(jsonFolder, "12", "team");
 80             var notifyBot = new JNotifyBot(repo, storageFolder, Pattern.compile("master"), tagStorage, branchStorage, List.of(updater));
 81 
 82             TestBotRunner.runPeriodicItems(notifyBot);
 83             assertEquals(List.of(), findJsonFiles(jsonFolder, ""));
 84 
 85             var editHash = CheckableRepository.appendAndCommit(localRepo, "One more line", "12345678: Fixes");
 86             localRepo.push(editHash, repo.url(), "master");
 87             TestBotRunner.runPeriodicItems(notifyBot);
 88             var jsonFiles = findJsonFiles(jsonFolder, "");
 89             assertEquals(1, jsonFiles.size());
 90             var jsonData = Files.readString(jsonFiles.get(0), StandardCharsets.UTF_8);
 91             var json = JSON.parse(jsonData);
 92             assertEquals(1, json.asArray().size());
 93             assertEquals(repo.webUrl(editHash).toString(), json.asArray().get(0).get("url").asString());
 94             assertEquals(List.of("12345678"), json.asArray().get(0).get("issue").asArray().stream()
 95                                                   .map(JSONValue::asString)
 96                                                   .collect(Collectors.toList()));
 97         }
 98     }
 99 
100     @Test
101     void testJsonUpdaterTag(TestInfo testInfo) throws IOException {
102         try (var credentials = new HostCredentials(testInfo);
103              var tempFolder = new TemporaryDirectory()) {
104             var repo = credentials.getHostedRepository();
105             var localRepoFolder = tempFolder.path().resolve("repo");
106             var localRepo = CheckableRepository.init(localRepoFolder, repo.repositoryType());
107             credentials.commitLock(localRepo);
108             var masterHash = localRepo.resolve("master").orElseThrow();
109             localRepo.tag(masterHash, "jdk-12+1", "Added tag 1", "Duke", "duke@openjdk.java.net");
110             localRepo.pushAll(repo.url());
111 
112             var tagStorage = createTagStorage(repo);
113             var branchStorage = createBranchStorage(repo);
114             var jsonFolder = tempFolder.path().resolve("json");
115             Files.createDirectory(jsonFolder);
116             var storageFolder =tempFolder.path().resolve("storage");
117 
118             var updater = new JsonUpdater(jsonFolder, "12", "team");
119             var notifyBot = new JNotifyBot(repo, storageFolder, Pattern.compile("master"), tagStorage, branchStorage, List.of(updater));
120 
121             TestBotRunner.runPeriodicItems(notifyBot);
122             assertEquals(List.of(), findJsonFiles(jsonFolder, ""));
123 
124             var editHash = CheckableRepository.appendAndCommit(localRepo, "Another line", "23456789: More fixes");
125             localRepo.fetch(repo.url(), "history:history");
126             localRepo.tag(editHash, "jdk-12+2", "Added tag 2", "Duke", "duke@openjdk.java.net");
127             var editHash2 = CheckableRepository.appendAndCommit(localRepo, "Another line", "34567890: Even more fixes");
128             localRepo.tag(editHash2, "jdk-12+4", "Added tag 3", "Duke", "duke@openjdk.java.net");
129             localRepo.pushAll(repo.url());
130 
131             TestBotRunner.runPeriodicItems(notifyBot);
132             var jsonFiles = findJsonFiles(jsonFolder, "");
133             assertEquals(3, jsonFiles.size());
134 
135             for (var file : jsonFiles) {
136                 var jsonData = Files.readString(file, StandardCharsets.UTF_8);
137                 var json = JSON.parse(jsonData);
138 
139                 if (json.asArray().get(0).contains("date")) {
140                     assertEquals(2, json.asArray().size());
141                     assertEquals(List.of("23456789"), json.asArray().get(0).get("issue").asArray().stream()
142                                                           .map(JSONValue::asString)
143                                                           .collect(Collectors.toList()));
144                     assertEquals(repo.webUrl(editHash).toString(), json.asArray().get(0).get("url").asString());
145                     assertEquals("team", json.asArray().get(0).get("build").asString());
146                     assertEquals(List.of("34567890"), json.asArray().get(1).get("issue").asArray().stream()
147                                                           .map(JSONValue::asString)
148                                                           .collect(Collectors.toList()));
149                     assertEquals(repo.webUrl(editHash2).toString(), json.asArray().get(1).get("url").asString());
150                     assertEquals("team", json.asArray().get(1).get("build").asString());
151                 } else {
152                     assertEquals(1, json.asArray().size());
153                     if (json.asArray().get(0).get("build").asString().equals("b02")) {
154                         assertEquals(List.of("23456789"), json.asArray().get(0).get("issue").asArray().stream()
155                                                               .map(JSONValue::asString)
156                                                               .collect(Collectors.toList()));
157                     } else {
158                         assertEquals("b04", json.asArray().get(0).get("build").asString());
159                         assertEquals(List.of("34567890"), json.asArray().get(0).get("issue").asArray().stream()
160                                                               .map(JSONValue::asString)
161                                                               .collect(Collectors.toList()));
162                     }
163                 }
164             }
165         }
166     }
167 
168     @Test
169     void testMailingList(TestInfo testInfo) throws IOException {
170         try (var listServer = new TestMailmanServer();
171              var credentials = new HostCredentials(testInfo);
172              var tempFolder = new TemporaryDirectory()) {
173             var repo = credentials.getHostedRepository();
174             var repoFolder = tempFolder.path().resolve("repo");
175             var localRepo = CheckableRepository.init(repoFolder, repo.repositoryType());
176             var masterHash = localRepo.resolve("master").orElseThrow();
177             credentials.commitLock(localRepo);
178             localRepo.pushAll(repo.url());
179 
180             var listAddress = EmailAddress.parse(listServer.createList("test"));
181             var mailmanServer = MailingListServerFactory.createMailmanServer(listServer.getArchive(), listServer.getSMTP(), Duration.ZERO);
182             var mailmanList = mailmanServer.getList(listAddress.address());
183             var tagStorage = createTagStorage(repo);
184             var branchStorage = createBranchStorage(repo);
185             var storageFolder = tempFolder.path().resolve("storage");
186 
187             var sender = EmailAddress.from("duke", "duke@duke.duke");
188             var updater = new MailingListUpdater(mailmanList, listAddress, sender, null, false, MailingListUpdater.Mode.ALL,
189                                                  Map.of("extra1", "value1", "extra2", "value2"), Pattern.compile("none"));
190             var notifyBot = new JNotifyBot(repo, storageFolder, Pattern.compile("master"), tagStorage, branchStorage, List.of(updater));
191 
192             // No mail should be sent on the first run as there is no history
193             TestBotRunner.runPeriodicItems(notifyBot);
194             assertThrows(RuntimeException.class, () -> listServer.processIncoming(Duration.ofMillis(1)));
195 
196             var editHash = CheckableRepository.appendAndCommit(localRepo, "Another line", "23456789: More fixes");
197             localRepo.push(editHash, repo.url(), "master");
198             TestBotRunner.runPeriodicItems(notifyBot);
199             listServer.processIncoming();
200 
201             var conversations = mailmanList.conversations(Duration.ofDays(1));
202             var email = conversations.get(0).first();
203             assertEquals(listAddress, email.sender());
204             assertEquals(sender, email.author());
205             assertEquals(email.recipients(), List.of(listAddress));
206             assertTrue(email.subject().contains(": 23456789: More fixes"));
207             assertFalse(email.subject().contains("master"));
208             assertTrue(email.body().contains("Changeset: " + editHash.abbreviate()));
209             assertTrue(email.body().contains("23456789: More fixes"));
210             assertFalse(email.body().contains("Committer"));
211             assertFalse(email.body().contains(masterHash.abbreviate()));
212             assertTrue(email.hasHeader("extra1"));
213             assertEquals("value1", email.headerValue("extra1"));
214             assertTrue(email.hasHeader("extra2"));
215             assertEquals("value2", email.headerValue("extra2"));
216         }
217     }
218 
219     @Test
220     void testMailingListMultiple(TestInfo testInfo) throws IOException {
221         try (var listServer = new TestMailmanServer();
222              var credentials = new HostCredentials(testInfo);
223              var tempFolder = new TemporaryDirectory()) {
224             var repo = credentials.getHostedRepository();
225             var repoFolder = tempFolder.path().resolve("repo");
226             var localRepo = CheckableRepository.init(repoFolder, repo.repositoryType());
227             var masterHash = localRepo.resolve("master").orElseThrow();
228             credentials.commitLock(localRepo);
229             localRepo.pushAll(repo.url());
230 
231             var listAddress = EmailAddress.parse(listServer.createList("test"));
232             var mailmanServer = MailingListServerFactory.createMailmanServer(listServer.getArchive(), listServer.getSMTP(), Duration.ZERO);
233             var mailmanList = mailmanServer.getList(listAddress.address());
234             var tagStorage = createTagStorage(repo);
235             var branchStorage = createBranchStorage(repo);
236             var storageFolder = tempFolder.path().resolve("storage");
237 
238             var sender = EmailAddress.from("duke", "duke@duke.duke");
239             var updater = new MailingListUpdater(mailmanList, listAddress, sender, null, false,
240                                                  MailingListUpdater.Mode.ALL, Map.of(), Pattern.compile(".*"));
241             var notifyBot = new JNotifyBot(repo, storageFolder, Pattern.compile("master"), tagStorage, branchStorage, List.of(updater));
242 
243             // No mail should be sent on the first run as there is no history
244             TestBotRunner.runPeriodicItems(notifyBot);
245             assertThrows(RuntimeException.class, () -> listServer.processIncoming(Duration.ofMillis(1)));
246 
247             var editHash1 = CheckableRepository.appendAndCommit(localRepo, "Another line", "23456789: More fixes",
248                                                                 "first_author", "first@author.example.com");
249             localRepo.push(editHash1, repo.url(), "master");
250             var editHash2 = CheckableRepository.appendAndCommit(localRepo, "Yet another line", "3456789A: Even more fixes",
251                                                                 "another_author", "another@author.example.com");
252             localRepo.push(editHash2, repo.url(), "master");
253 
254             TestBotRunner.runPeriodicItems(notifyBot);
255             listServer.processIncoming();
256 
257             var conversations = mailmanList.conversations(Duration.ofDays(1));
258             var email = conversations.get(0).first();
259             assertEquals(listAddress, email.sender());
260             assertEquals(EmailAddress.from("another_author", "another@author.example.com"), email.author());
261             assertEquals(email.recipients(), List.of(listAddress));
262             assertTrue(email.subject().contains(": 2 new changesets"));
263             assertFalse(email.subject().contains("master"));
264             assertTrue(email.body().contains("Changeset: " + editHash1.abbreviate()));
265             assertTrue(email.body().contains("23456789: More fixes"));
266             assertTrue(email.body().contains("Changeset: " + editHash2.abbreviate()));
267             assertTrue(email.body().contains("3456789A: Even more fixes"));
268             assertFalse(email.body().contains(masterHash.abbreviate()));
269         }
270     }
271 
272     @Test
273     void testMailingListSponsored(TestInfo testInfo) throws IOException {
274         try (var listServer = new TestMailmanServer();
275              var credentials = new HostCredentials(testInfo);
276              var tempFolder = new TemporaryDirectory()) {
277             var repo = credentials.getHostedRepository();
278             var repoFolder = tempFolder.path().resolve("repo");
279             var localRepo = CheckableRepository.init(repoFolder, repo.repositoryType());
280             var masterHash = localRepo.resolve("master").orElseThrow();
281             credentials.commitLock(localRepo);
282             localRepo.pushAll(repo.url());
283 
284             var listAddress = EmailAddress.parse(listServer.createList("test"));
285             var mailmanServer = MailingListServerFactory.createMailmanServer(listServer.getArchive(), listServer.getSMTP(), Duration.ZERO);
286             var mailmanList = mailmanServer.getList(listAddress.address());
287             var tagStorage = createTagStorage(repo);
288             var branchStorage = createBranchStorage(repo);
289             var storageFolder = tempFolder.path().resolve("storage");
290 
291             var sender = EmailAddress.from("duke", "duke@duke.duke");
292             var updater = new MailingListUpdater(mailmanList, listAddress, sender, null, false,
293                                                  MailingListUpdater.Mode.ALL, Map.of(), Pattern.compile(".*"));
294             var notifyBot = new JNotifyBot(repo, storageFolder, Pattern.compile("master"), tagStorage, branchStorage, List.of(updater));
295 
296             // No mail should be sent on the first run as there is no history
297             TestBotRunner.runPeriodicItems(notifyBot);
298             assertThrows(RuntimeException.class, () -> listServer.processIncoming(Duration.ofMillis(1)));
299 
300             var editHash = CheckableRepository.appendAndCommit(localRepo, "Another line", "23456789: More fixes",
301                                                                "author", "author@test.test",
302                                                                "committer", "committer@test.test");
303             localRepo.push(editHash, repo.url(), "master");
304             TestBotRunner.runPeriodicItems(notifyBot);
305             listServer.processIncoming();
306 
307             var conversations = mailmanList.conversations(Duration.ofDays(1));
308             var email = conversations.get(0).first();
309             assertEquals(listAddress, email.sender());
310             assertEquals(EmailAddress.from("committer", "committer@test.test"), email.author());
311             assertEquals(email.recipients(), List.of(listAddress));
312             assertTrue(email.body().contains("Changeset: " + editHash.abbreviate()));
313             assertTrue(email.body().contains("23456789: More fixes"));
314             assertTrue(email.body().contains("Author:    author <author@test.test>"));
315             assertTrue(email.body().contains("Committer: committer <committer@test.test>"));
316             assertFalse(email.body().contains(masterHash.abbreviate()));
317         }
318     }
319 
320     @Test
321     void testMailingListMultipleBranches(TestInfo testInfo) throws IOException {
322         try (var listServer = new TestMailmanServer();
323              var credentials = new HostCredentials(testInfo);
324              var tempFolder = new TemporaryDirectory()) {
325             var repo = credentials.getHostedRepository();
326             var repoFolder = tempFolder.path().resolve("repo");
327             var localRepo = CheckableRepository.init(repoFolder, repo.repositoryType());
328             var masterHash = localRepo.resolve("master").orElseThrow();
329             credentials.commitLock(localRepo);
330             var branch = localRepo.branch(masterHash, "another");
331             localRepo.pushAll(repo.url());
332 
333             var listAddress = EmailAddress.parse(listServer.createList("test"));
334             var mailmanServer = MailingListServerFactory.createMailmanServer(listServer.getArchive(), listServer.getSMTP(), Duration.ZERO);
335             var mailmanList = mailmanServer.getList(listAddress.address());
336             var tagStorage = createTagStorage(repo);
337             var branchStorage = createBranchStorage(repo);
338             var storageFolder = tempFolder.path().resolve("storage");
339 
340             var sender = EmailAddress.from("duke", "duke@duke.duke");
341             var author = EmailAddress.from("author", "author@duke.duke");
342             var updater = new MailingListUpdater(mailmanList, listAddress, sender, author, true,
343                                                  MailingListUpdater.Mode.ALL, Map.of(), Pattern.compile(".*"));
344             var notifyBot = new JNotifyBot(repo, storageFolder, Pattern.compile("master|another"), tagStorage, branchStorage, List.of(updater));
345 
346             // No mail should be sent on the first run as there is no history
347             TestBotRunner.runPeriodicItems(notifyBot);
348             assertThrows(RuntimeException.class, () -> listServer.processIncoming(Duration.ofMillis(1)));
349 
350             var editHash1 = CheckableRepository.appendAndCommit(localRepo, "Another line", "23456789: More fixes");
351             localRepo.push(editHash1, repo.url(), "master");
352             var editHash2 = CheckableRepository.appendAndCommit(localRepo, "Yet another line", "3456789A: Even more fixes");
353             localRepo.push(editHash2, repo.url(), "master");
354 
355             TestBotRunner.runPeriodicItems(notifyBot);
356             listServer.processIncoming();
357 
358             var conversations = mailmanList.conversations(Duration.ofDays(1));
359             var email = conversations.get(0).first();
360             assertEquals(listAddress, email.sender());
361             assertEquals(author, email.author());
362             assertEquals(email.recipients(), List.of(listAddress));
363             assertFalse(email.subject().contains("another"));
364             assertTrue(email.subject().contains(": master: 2 new changesets"));
365             assertTrue(email.body().contains("Changeset: " + editHash1.abbreviate()));
366             assertTrue(email.body().contains("23456789: More fixes"));
367             assertTrue(email.body().contains("Changeset: " + editHash2.abbreviate()));
368             assertTrue(email.body().contains("3456789A: Even more fixes"));
369             assertFalse(email.body().contains(masterHash.abbreviate()));
370             assertFalse(email.body().contains("456789AB: Yet more fixes"));
371 
372             localRepo.checkout(branch, true);
373             var editHash3 = CheckableRepository.appendAndCommit(localRepo, "Another branch", "456789AB: Yet more fixes");
374             localRepo.push(editHash3, repo.url(), "another");
375 
376             TestBotRunner.runPeriodicItems(notifyBot);
377             listServer.processIncoming();
378 
379             conversations = mailmanList.conversations(Duration.ofDays(1));
380             conversations.sort(Comparator.comparing(conversation -> conversation.first().subject()));
381             email = conversations.get(0).first();
382             assertEquals(author, email.author());
383             assertEquals(listAddress, email.sender());
384             assertEquals(email.recipients(), List.of(listAddress));
385             assertTrue(email.subject().contains(": another: 456789AB: Yet more fixes"));
386             assertFalse(email.subject().contains("master"));
387             assertTrue(email.body().contains("Changeset: " + editHash3.abbreviate()));
388             assertTrue(email.body().contains("456789AB: Yet more fixes"));
389             assertFalse(email.body().contains("Changeset: " + editHash2.abbreviate()));
390             assertFalse(email.body().contains("3456789A: Even more fixes"));
391         }
392     }
393 
394     @Test
395     void testMailingListPROnly(TestInfo testInfo) throws IOException {
396         try (var listServer = new TestMailmanServer();
397              var credentials = new HostCredentials(testInfo);
398              var tempFolder = new TemporaryDirectory()) {
399             var repo = credentials.getHostedRepository();
400             var repoFolder = tempFolder.path().resolve("repo");
401             var localRepo = CheckableRepository.init(repoFolder, repo.repositoryType());
402             var masterHash = localRepo.resolve("master").orElseThrow();
403             credentials.commitLock(localRepo);
404             localRepo.pushAll(repo.url());
405 
406             var listAddress = EmailAddress.parse(listServer.createList("test"));
407             var mailmanServer = MailingListServerFactory.createMailmanServer(listServer.getArchive(), listServer.getSMTP(), Duration.ZERO);
408             var mailmanList = mailmanServer.getList(listAddress.address());
409             var tagStorage = createTagStorage(repo);
410             var branchStorage = createBranchStorage(repo);
411             var storageFolder = tempFolder.path().resolve("storage");
412 
413             var sender = EmailAddress.from("duke", "duke@duke.duke");
414             var author = EmailAddress.from("author", "author@duke.duke");
415             var updater = new MailingListUpdater(mailmanList, listAddress, sender, author, false,
416                                                  MailingListUpdater.Mode.PR_ONLY, Map.of("extra1", "value1"),
417                                                  Pattern.compile(".*"));
418             var notifyBot = new JNotifyBot(repo, storageFolder, Pattern.compile("master"), tagStorage, branchStorage, List.of(updater));
419 
420             // No mail should be sent on the first run as there is no history
421             TestBotRunner.runPeriodicItems(notifyBot);
422             assertThrows(RuntimeException.class, () -> listServer.processIncoming(Duration.ofMillis(1)));
423 
424             var editHash = CheckableRepository.appendAndCommit(localRepo, "Another line", "23456789: More fixes");
425             localRepo.push(editHash, repo.url(), "edit");
426             var pr = credentials.createPullRequest(repo, "master", "edit", "RFR: My PR");
427 
428             // Create a potentially conflicting one
429             var otherHash = CheckableRepository.appendAndCommit(localRepo, "Another line", "23456789: More fixes");
430             localRepo.push(otherHash, repo.url(), "other");
431             var otherPr = credentials.createPullRequest(repo, "master", "other", "RFR: My other PR");
432 
433             // PR hasn't been integrated yet, so there should be no mail
434             TestBotRunner.runPeriodicItems(notifyBot);
435             assertThrows(RuntimeException.class, () -> listServer.processIncoming(Duration.ofMillis(1)));
436 
437             // Simulate an RFR email
438             var rfr = Email.create(sender, "RFR: My PR", "PR: " + pr.webUrl().toString())
439                     .recipient(listAddress)
440                     .build();
441             mailmanList.post(rfr);
442             listServer.processIncoming();
443 
444             // And an integration
445             pr.addComment("Pushed as commit " + editHash.hex() + ".");
446             localRepo.push(editHash, repo.url(), "master");
447             TestBotRunner.runPeriodicItems(notifyBot);
448             listServer.processIncoming();
449 
450             var conversations = mailmanList.conversations(Duration.ofDays(1));
451             assertEquals(1, conversations.size());
452             var first = conversations.get(0).first();
453             var email = conversations.get(0).replies(first).get(0);
454             assertEquals(listAddress, email.sender());
455             assertEquals(author, email.author());
456             assertEquals(email.recipients(), List.of(listAddress));
457             assertEquals("Re: [Integrated] RFR: My PR", email.subject());
458             assertFalse(email.subject().contains("master"));
459             assertTrue(email.body().contains("Changeset: " + editHash.abbreviate()));
460             assertTrue(email.body().contains("23456789: More fixes"));
461             assertFalse(email.body().contains("Committer"));
462             assertFalse(email.body().contains(masterHash.abbreviate()));
463             assertTrue(email.hasHeader("extra1"));
464             assertEquals("value1", email.headerValue("extra1"));
465 
466             // Now push the other one without a matching PR - PR_ONLY will not generate a mail
467             localRepo.push(otherHash, repo.url(), "master");
468             TestBotRunner.runPeriodicItems(notifyBot);
469             assertThrows(RuntimeException.class, () -> listServer.processIncoming(Duration.ofSeconds(1)));
470         }
471     }
472 
473     @Test
474     void testMailingListPR(TestInfo testInfo) throws IOException {
475         try (var listServer = new TestMailmanServer();
476              var credentials = new HostCredentials(testInfo);
477              var tempFolder = new TemporaryDirectory()) {
478             var repo = credentials.getHostedRepository();
479             var repoFolder = tempFolder.path().resolve("repo");
480             var localRepo = CheckableRepository.init(repoFolder, repo.repositoryType());
481             var masterHash = localRepo.resolve("master").orElseThrow();
482             credentials.commitLock(localRepo);
483             localRepo.pushAll(repo.url());
484 
485             var listAddress = EmailAddress.parse(listServer.createList("test"));
486             var mailmanServer = MailingListServerFactory.createMailmanServer(listServer.getArchive(), listServer.getSMTP(), Duration.ZERO);
487             var mailmanList = mailmanServer.getList(listAddress.address());
488             var tagStorage = createTagStorage(repo);
489             var branchStorage = createBranchStorage(repo);
490             var storageFolder = tempFolder.path().resolve("storage");
491 
492             var sender = EmailAddress.from("duke", "duke@duke.duke");
493             var updater = new MailingListUpdater(mailmanList, listAddress, sender, null, false,
494                                                  MailingListUpdater.Mode.PR, Map.of(), Pattern.compile(".*"));
495             var notifyBot = new JNotifyBot(repo, storageFolder, Pattern.compile("master"), tagStorage, branchStorage, List.of(updater));
496 
497             // No mail should be sent on the first run as there is no history
498             TestBotRunner.runPeriodicItems(notifyBot);
499             assertThrows(RuntimeException.class, () -> listServer.processIncoming(Duration.ofMillis(1)));
500 
501             var editHash = CheckableRepository.appendAndCommit(localRepo, "Another line", "23456789: More fixes");
502             localRepo.push(editHash, repo.url(), "edit");
503             var pr = credentials.createPullRequest(repo, "master", "edit", "RFR: My PR");
504 
505             // Create a potentially conflicting one
506             var otherHash = CheckableRepository.appendAndCommit(localRepo, "Another line", "23456789: More fixes");
507             localRepo.push(otherHash, repo.url(), "other");
508             var otherPr = credentials.createPullRequest(repo, "master", "other", "RFR: My other PR");
509 
510             // PR hasn't been integrated yet, so there should be no mail
511             TestBotRunner.runPeriodicItems(notifyBot);
512             assertThrows(RuntimeException.class, () -> listServer.processIncoming(Duration.ofMillis(1)));
513 
514             // Simulate an RFR email
515             var rfr = Email.create("RFR: My PR", "PR:\n" + pr.webUrl().toString())
516                            .author(EmailAddress.from("duke", "duke@duke.duke"))
517                            .recipient(listAddress)
518                            .build();
519             mailmanList.post(rfr);
520             listServer.processIncoming();
521 
522             // And an integration
523             pr.addComment("Pushed as commit " + editHash.hex() + ".");
524             localRepo.push(editHash, repo.url(), "master");
525 
526             // Push the other one without a matching PR
527             localRepo.push(otherHash, repo.url(), "master");
528 
529             TestBotRunner.runPeriodicItems(notifyBot);
530             listServer.processIncoming();
531             listServer.processIncoming();
532 
533             var conversations = mailmanList.conversations(Duration.ofDays(1));
534             conversations.sort(Comparator.comparing(conversation -> conversation.first().subject()));
535             assertEquals(2, conversations.size());
536 
537             var prConversation = conversations.get(0);
538             var pushConversation = conversations.get(1);
539 
540             var prEmail = prConversation.replies(prConversation.first()).get(0);
541             assertEquals(listAddress, prEmail.sender());
542             assertEquals(EmailAddress.from("testauthor", "ta@none.none"), prEmail.author());
543             assertEquals(prEmail.recipients(), List.of(listAddress));
544             assertEquals("Re: [Integrated] RFR: My PR", prEmail.subject());
545             assertFalse(prEmail.subject().contains("master"));
546             assertTrue(prEmail.body().contains("Changeset: " + editHash.abbreviate()));
547             assertTrue(prEmail.body().contains("23456789: More fixes"));
548             assertFalse(prEmail.body().contains("Committer"));
549             assertFalse(prEmail.body().contains(masterHash.abbreviate()));
550 
551             var pushEmail = pushConversation.first();
552             assertEquals(listAddress, pushEmail.sender());
553             assertEquals(EmailAddress.from("testauthor", "ta@none.none"), pushEmail.author());
554             assertEquals(pushEmail.recipients(), List.of(listAddress));
555             assertTrue(pushEmail.subject().contains("23456789: More fixes"));
556         }
557     }
558 
559     @Test
560     void testMailinglistTag(TestInfo testInfo) throws IOException {
561         try (var credentials = new HostCredentials(testInfo);
562              var tempFolder = new TemporaryDirectory();
563              var listServer = new TestMailmanServer()) {
564             var repo = credentials.getHostedRepository();
565             var localRepoFolder = tempFolder.path().resolve("repo");
566             var localRepo = CheckableRepository.init(localRepoFolder, repo.repositoryType());
567             credentials.commitLock(localRepo);
568             var masterHash = localRepo.resolve("master").orElseThrow();
569             localRepo.tag(masterHash, "jdk-12+1", "Added tag 1", "Duke Tagger", "tagger@openjdk.java.net");
570             localRepo.pushAll(repo.url());
571 
572             var listAddress = EmailAddress.parse(listServer.createList("test"));
573             var mailmanServer = MailingListServerFactory.createMailmanServer(listServer.getArchive(), listServer.getSMTP(), Duration.ZERO);
574             var mailmanList = mailmanServer.getList(listAddress.address());
575             var tagStorage = createTagStorage(repo);
576             var branchStorage = createBranchStorage(repo);
577             var storageFolder = tempFolder.path().resolve("storage");
578 
579             var sender = EmailAddress.from("duke", "duke@duke.duke");
580             var updater = new MailingListUpdater(mailmanList, listAddress, sender, null, false, MailingListUpdater.Mode.ALL,
581                                                  Map.of("extra1", "value1", "extra2", "value2"),
582                                                  Pattern.compile(".*"));
583             var prOnlyUpdater = new MailingListUpdater(mailmanList, listAddress, sender, null, false,
584                                                        MailingListUpdater.Mode.PR_ONLY, Map.of(), Pattern.compile(".*"));
585             var notifyBot = new JNotifyBot(repo, storageFolder, Pattern.compile("master"), tagStorage, branchStorage,
586                                            List.of(updater, prOnlyUpdater));
587 
588             // No mail should be sent on the first run as there is no history
589             TestBotRunner.runPeriodicItems(notifyBot);
590             assertThrows(RuntimeException.class, () -> listServer.processIncoming(Duration.ofMillis(1)));
591 
592             var editHash = CheckableRepository.appendAndCommit(localRepo, "Another line", "23456789: More fixes");
593             localRepo.fetch(repo.url(), "history:history");
594             localRepo.tag(editHash, "jdk-12+2", "Added tag 2", "Duke Tagger", "tagger@openjdk.java.net");
595             CheckableRepository.appendAndCommit(localRepo, "Another line 1", "34567890: Even more fixes");
596             CheckableRepository.appendAndCommit(localRepo, "Another line 2", "45678901: Yet even more fixes");
597             var editHash2 = CheckableRepository.appendAndCommit(localRepo, "Another line 3", "56789012: Still even more fixes");
598             localRepo.tag(editHash2, "jdk-12+4", "Added tag 3", "Duke Tagger", "tagger@openjdk.java.net");
599             CheckableRepository.appendAndCommit(localRepo, "Another line 4", "67890123: Brand new fixes");
600             var editHash3 = CheckableRepository.appendAndCommit(localRepo, "Another line 5", "78901234: More brand new fixes");
601             localRepo.tag(editHash3, "jdk-13+0", "Added tag 4", "Duke Tagger", "tagger@openjdk.java.net");
602             localRepo.pushAll(repo.url());
603 
604             TestBotRunner.runPeriodicItems(notifyBot);
605             listServer.processIncoming();
606             listServer.processIncoming();
607             listServer.processIncoming();
608             listServer.processIncoming();
609 
610             var conversations = mailmanList.conversations(Duration.ofDays(1));
611             assertEquals(4, conversations.size());
612 
613             for (var conversation : conversations) {
614                 var email = conversation.first();
615                 if (email.subject().equals("git: test: Added tag jdk-12+2 for changeset " + editHash.abbreviate())) {
616                     assertTrue(email.body().contains("23456789: More fixes"));
617                     assertFalse(email.body().contains("34567890: Even more fixes"));
618                     assertFalse(email.body().contains("45678901: Yet even more fixes"));
619                     assertFalse(email.body().contains("56789012: Still even more fixes"));
620                     assertFalse(email.body().contains("67890123: Brand new fixes"));
621                     assertFalse(email.body().contains("78901234: More brand new fixes"));
622                     assertEquals(EmailAddress.from("Duke Tagger", "tagger@openjdk.java.net"), email.author());
623                 } else if (email.subject().equals("git: test: Added tag jdk-12+4 for changeset " + editHash2.abbreviate())) {
624                     assertFalse(email.body().contains("23456789: More fixes"));
625                     assertTrue(email.body().contains("34567890: Even more fixes"));
626                     assertTrue(email.body().contains("45678901: Yet even more fixes"));
627                     assertTrue(email.body().contains("56789012: Still even more fixes"));
628                     assertFalse(email.body().contains("67890123: Brand new fixes"));
629                     assertFalse(email.body().contains("78901234: More brand new fixes"));
630                     assertEquals(EmailAddress.from("Duke Tagger", "tagger@openjdk.java.net"), email.author());
631                 } else if (email.subject().equals("git: test: Added tag jdk-13+0 for changeset " + editHash3.abbreviate())) {
632                     assertFalse(email.body().contains("23456789: More fixes"));
633                     assertFalse(email.body().contains("34567890: Even more fixes"));
634                     assertFalse(email.body().contains("45678901: Yet even more fixes"));
635                     assertFalse(email.body().contains("56789012: Still even more fixes"));
636                     assertFalse(email.body().contains("67890123: Brand new fixes"));
637                     assertTrue(email.body().contains("78901234: More brand new fixes"));
638                     assertEquals(EmailAddress.from("Duke Tagger", "tagger@openjdk.java.net"), email.author());
639                 } else if (email.subject().equals("git: test: 6 new changesets")) {
640                     assertTrue(email.body().contains("23456789: More fixes"));
641                     assertTrue(email.body().contains("34567890: Even more fixes"));
642                     assertTrue(email.body().contains("45678901: Yet even more fixes"));
643                     assertTrue(email.body().contains("56789012: Still even more fixes"));
644                     assertTrue(email.body().contains("67890123: Brand new fixes"));
645                     assertTrue(email.body().contains("78901234: More brand new fixes"));
646                     assertEquals(EmailAddress.from("testauthor", "ta@none.none"), email.author());
647                 } else {
648                     fail("Mismatched subject: " + email.subject());
649                 }
650                 assertTrue(email.hasHeader("extra1"));
651                 assertEquals("value1", email.headerValue("extra1"));
652                 assertTrue(email.hasHeader("extra2"));
653                 assertEquals("value2", email.headerValue("extra2"));
654             }
655         }
656     }
657 
658     @Test
659     void testMailingListBranch(TestInfo testInfo) throws IOException {
660         try (var listServer = new TestMailmanServer();
661              var credentials = new HostCredentials(testInfo);
662              var tempFolder = new TemporaryDirectory()) {
663             var repo = credentials.getHostedRepository();
664             var repoFolder = tempFolder.path().resolve("repo");
665             var localRepo = CheckableRepository.init(repoFolder, repo.repositoryType());
666             var masterHash = localRepo.resolve("master").orElseThrow();
667             credentials.commitLock(localRepo);
668             localRepo.pushAll(repo.url());
669 
670             var listAddress = EmailAddress.parse(listServer.createList("test"));
671             var mailmanServer = MailingListServerFactory.createMailmanServer(listServer.getArchive(), listServer.getSMTP(), Duration.ZERO);
672             var mailmanList = mailmanServer.getList(listAddress.address());
673             var tagStorage = createTagStorage(repo);
674             var branchStorage = createBranchStorage(repo);
675             var storageFolder = tempFolder.path().resolve("storage");
676 
677             var sender = EmailAddress.from("duke", "duke@duke.duke");
678             var updater = new MailingListUpdater(mailmanList, listAddress, sender, null, false, MailingListUpdater.Mode.ALL,
679                                                  Map.of("extra1", "value1", "extra2", "value2"),
680                                                  Pattern.compile(".*"));
681             var notifyBot = new JNotifyBot(repo, storageFolder, Pattern.compile("master|newbranch."), tagStorage, branchStorage, List.of(updater));
682 
683             // No mail should be sent on the first run as there is no history
684             TestBotRunner.runPeriodicItems(notifyBot);
685             assertThrows(RuntimeException.class, () -> listServer.processIncoming(Duration.ofMillis(1)));
686 
687             CheckableRepository.appendAndCommit(localRepo, "Another line", "12345678: Some fixes");
688             var editHash = CheckableRepository.appendAndCommit(localRepo, "Another line", "23456789: More fixes");
689             localRepo.push(editHash, repo.url(), "newbranch1");
690             TestBotRunner.runPeriodicItems(notifyBot);
691             listServer.processIncoming();
692 
693             var conversations = mailmanList.conversations(Duration.ofDays(1));
694             var email = conversations.get(0).first();
695             assertEquals(listAddress, email.sender());
696             assertEquals(EmailAddress.from("testauthor", "ta@none.none"), email.author());
697             assertEquals(email.recipients(), List.of(listAddress));
698             assertEquals("git: test: created branch newbranch1 based on the branch master containing 2 unique commits", email.subject());
699             assertTrue(email.body().contains("12345678: Some fixes"));
700             assertTrue(email.hasHeader("extra1"));
701             assertEquals("value1", email.headerValue("extra1"));
702             assertTrue(email.hasHeader("extra2"));
703             assertEquals("value2", email.headerValue("extra2"));
704 
705             TestBotRunner.runPeriodicItems(notifyBot);
706             assertThrows(RuntimeException.class, () -> listServer.processIncoming(Duration.ofMillis(1)));
707 
708             localRepo.push(editHash, repo.url(), "newbranch2");
709             TestBotRunner.runPeriodicItems(notifyBot);
710             listServer.processIncoming();
711 
712             var newConversation = mailmanList.conversations(Duration.ofDays(1)).stream()
713                                              .filter(c -> !c.equals(conversations.get(0)))
714                                              .findFirst().orElseThrow();
715             email = newConversation.first();
716             assertEquals(listAddress, email.sender());
717             assertEquals(sender, email.author());
718             assertEquals(email.recipients(), List.of(listAddress));
719             assertEquals("git: test: created branch newbranch2 based on the branch newbranch1 containing 0 unique commits", email.subject());
720             assertEquals("The new branch newbranch2 is currently identical to the newbranch1 branch.", email.body());
721         }
722     }
723 
724     @Test
725     void testIssue(TestInfo testInfo) throws IOException {
726         try (var credentials = new HostCredentials(testInfo);
727              var tempFolder = new TemporaryDirectory()) {
728             var repo = credentials.getHostedRepository();
729             var repoFolder = tempFolder.path().resolve("repo");
730             var localRepo = CheckableRepository.init(repoFolder, repo.repositoryType());
731             credentials.commitLock(localRepo);
732             localRepo.pushAll(repo.url());
733 
734             var tagStorage = createTagStorage(repo);
735             var branchStorage = createBranchStorage(repo);
736             var storageFolder = tempFolder.path().resolve("storage");
737 
738             var issueProject = credentials.getIssueProject();
739             var updater = new IssueUpdater(issueProject);
740             var notifyBot = new JNotifyBot(repo, storageFolder, Pattern.compile("master"), tagStorage, branchStorage, List.of(updater));
741 
742             // Initialize history
743             TestBotRunner.runPeriodicItems(notifyBot);
744 
745             // Create an issue and commit a fix
746             var issue = issueProject.createIssue("This is an issue", List.of("Indeed"));
747             var editHash = CheckableRepository.appendAndCommit(localRepo, "Another line", issue.id() + ": Fix that issue");
748             localRepo.push(editHash, repo.url(), "master");
749             TestBotRunner.runPeriodicItems(notifyBot);
750 
751             // The changeset should be reflected in a comment
752             var comments = issue.comments();
753             assertEquals(1, comments.size());
754             var comment = comments.get(0);
755             assertTrue(comment.body().contains(editHash.abbreviate()));
756 
757             // There should be no open issues
758             assertEquals(0, issueProject.issues().size());
759         }
760     }
761 }