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.pr;
 24 
 25 import org.junit.jupiter.api.*;
 26 import org.openjdk.skara.forge.Review;
 27 import org.openjdk.skara.issuetracker.Comment;
 28 import org.openjdk.skara.test.*;
 29 import org.openjdk.skara.vcs.Repository;
 30 
 31 import java.io.IOException;
 32 import java.util.*;
 33 
 34 import static org.junit.jupiter.api.Assertions.*;
 35 import static org.openjdk.skara.bots.pr.PullRequestAsserts.assertLastCommentContains;
 36 
 37 class SolvesTests {
 38     @Test
 39     void simple(TestInfo testInfo) throws IOException {
 40         try (var credentials = new HostCredentials(testInfo);
 41              var tempFolder = new TemporaryDirectory()) {
 42             var author = credentials.getHostedRepository();
 43             var integrator = credentials.getHostedRepository();
 44 
 45             var censusBuilder = credentials.getCensusBuilder()
 46                                            .addReviewer(integrator.forge().currentUser().id())
 47                                            .addCommitter(author.forge().currentUser().id());
 48             var prBot = new PullRequestBot(integrator, censusBuilder.build(), "master");
 49 
 50             // Populate the projects repository
 51             var localRepoFolder = tempFolder.path().resolve("localrepo");
 52             var localRepo = CheckableRepository.init(localRepoFolder, author.repositoryType());
 53             var masterHash = localRepo.resolve("master").orElseThrow();
 54             assertFalse(CheckableRepository.hasBeenEdited(localRepo));
 55             localRepo.push(masterHash, author.url(), "master", true);
 56 
 57             // Make a change with a corresponding PR
 58             var editHash = CheckableRepository.appendAndCommit(localRepo);
 59             localRepo.push(editHash, author.url(), "edit", true);
 60             var pr = credentials.createPullRequest(author, "master", "edit", "123: This is a pull request");
 61 
 62             // No arguments
 63             pr.addComment("/solves");
 64             TestBotRunner.runPeriodicItems(prBot);
 65 
 66             // The bot should reply with a help message
 67             assertLastCommentContains(pr,"To add an additional");
 68 
 69             // Invalid syntax
 70             pr.addComment("/solves something I guess");
 71             TestBotRunner.runPeriodicItems(prBot);
 72 
 73             // The bot should reply with a failure message
 74             assertLastCommentContains(pr,"Invalid");
 75 
 76             // Add an issue
 77             pr.addComment("/solves 1234: An issue");
 78             TestBotRunner.runPeriodicItems(prBot);
 79 
 80             // The bot should reply with a success message
 81             assertLastCommentContains(pr,"Adding additional");
 82 
 83             // Try to remove a not-previously-added issue
 84             pr.addComment("/solves 1235");
 85             TestBotRunner.runPeriodicItems(prBot);
 86 
 87             // The bot should reply with a failure message
 88             assertLastCommentContains(pr,"Could not find");
 89 
 90             // Now remove the added one
 91             pr.addComment("/solves 1234");
 92             TestBotRunner.runPeriodicItems(prBot);
 93 
 94             // The bot should reply with a success message
 95             assertLastCommentContains(pr,"Removing additional");
 96 
 97             // Add two more issues
 98             pr.addComment("/solves 12345: Another issue");
 99             pr.addComment("/solves 123456: Yet another issue");
100             TestBotRunner.runPeriodicItems(prBot);
101 
102             // The bot should reply with a success message
103             assertLastCommentContains(pr,"Adding additional");
104 
105             // Update the description of the first one
106             pr.addComment("/solves 12345: This is indeed another issue");
107             TestBotRunner.runPeriodicItems(prBot);
108 
109             // The bot should reply with a success message
110             assertLastCommentContains(pr,"Updating description");
111 
112             // Approve it as another user
113             var approvalPr = integrator.pullRequest(pr.id());
114             approvalPr.addReview(Review.Verdict.APPROVED, "Approved");
115             TestBotRunner.runPeriodicItems(prBot);
116             TestBotRunner.runPeriodicItems(prBot);
117 
118             // The commit message preview should contain the additional issues
119             var preview = pr.comments().stream()
120                             .filter(comment -> comment.body().contains("The commit message will be"))
121                             .map(Comment::body)
122                             .findFirst()
123                             .orElseThrow();
124             assertTrue(preview.contains("123: This is a pull request"));
125             assertTrue(preview.contains("12345: This is indeed another issue"));
126             assertTrue(preview.contains("123456: Yet another issue"));
127 
128             // Integrate
129             pr.addComment("/integrate");
130             TestBotRunner.runPeriodicItems(prBot);
131 
132             // The bot should reply with an ok message
133             assertLastCommentContains(pr,"Pushed as commit");
134 
135             // The change should now be present on the master branch
136             var pushedFolder = tempFolder.path().resolve("pushed");
137             var pushedRepo = Repository.materialize(pushedFolder, author.url(), "master");
138             assertTrue(CheckableRepository.hasBeenEdited(pushedRepo));
139 
140             var headHash = pushedRepo.resolve("HEAD").orElseThrow();
141             var headCommit = pushedRepo.commits(headHash.hex() + "^.." + headHash.hex()).asList().get(0);
142 
143             // The additional issues should be present in the commit message
144             assertEquals(List.of("123: This is a pull request",
145                                  "12345: This is indeed another issue",
146                                  "123456: Yet another issue",
147                                  "",
148                                  "Reviewed-by: integrationreviewer1"), headCommit.message());
149         }
150     }
151 
152     @Test
153     void invalidCommandAuthor(TestInfo testInfo) throws IOException {
154         try (var credentials = new HostCredentials(testInfo);
155              var tempFolder = new TemporaryDirectory()) {
156             var author = credentials.getHostedRepository();
157             var integrator = credentials.getHostedRepository();
158             var external = credentials.getHostedRepository();
159 
160             var censusBuilder = credentials.getCensusBuilder()
161                                            .addAuthor(author.forge().currentUser().id());
162             var mergeBot = new PullRequestBot(integrator, censusBuilder.build(), "master");
163 
164             // Populate the projects repository
165             var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
166             var masterHash = localRepo.resolve("master").orElseThrow();
167             assertFalse(CheckableRepository.hasBeenEdited(localRepo));
168             localRepo.push(masterHash, author.url(), "master", true);
169 
170             // Make a change with a corresponding PR
171             var editHash = CheckableRepository.appendAndCommit(localRepo);
172             localRepo.push(editHash, author.url(), "edit", true);
173             var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");
174 
175             // Issue a solves command not as the PR author
176             var externalPr = external.pullRequest(pr.id());
177             externalPr.addComment("/solves 1234: an issue");
178             TestBotRunner.runPeriodicItems(mergeBot);
179 
180             // The bot should reply with an error message
181             var error = pr.comments().stream()
182                           .filter(comment -> comment.body().contains("Only the author"))
183                           .count();
184             assertEquals(1, error);
185         }
186     }
187 
188     @Test
189     void issueInTitle(TestInfo testInfo) throws IOException {
190         try (var credentials = new HostCredentials(testInfo);
191              var tempFolder = new TemporaryDirectory()) {
192             var author = credentials.getHostedRepository();
193             var integrator = credentials.getHostedRepository();
194 
195             var censusBuilder = credentials.getCensusBuilder()
196                                            .addAuthor(author.forge().currentUser().id());
197             var prBot = new PullRequestBot(integrator, censusBuilder.build(), "master");
198 
199             // Populate the projects repository
200             var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
201             var masterHash = localRepo.resolve("master").orElseThrow();
202             assertFalse(CheckableRepository.hasBeenEdited(localRepo));
203             localRepo.push(masterHash, author.url(), "master", true);
204 
205             // Make a change with a corresponding PR
206             var editHash = CheckableRepository.appendAndCommit(localRepo);
207             localRepo.push(editHash, author.url(), "edit", true);
208             var pr = credentials.createPullRequest(author, "master", "edit", "This is a pull request");
209 
210             // Add an issue
211             pr.addComment("/solves 1234: An issue");
212             TestBotRunner.runPeriodicItems(prBot);
213 
214             // The bot should reply with a success message
215             assertLastCommentContains(pr,"current title");
216 
217             var updatedPr = author.pullRequest(pr.id());
218             assertEquals("1234: An issue", updatedPr.title());
219 
220             // Update the issue description
221             pr.addComment("/solves 1234: Yes this is an issue");
222             TestBotRunner.runPeriodicItems(prBot);
223 
224             // The bot should reply with a success message
225             assertLastCommentContains(pr,"will now be updated");
226 
227             updatedPr = author.pullRequest(pr.id());
228             assertEquals("1234: Yes this is an issue", updatedPr.title());
229         }
230     }
231 
232     @Test
233     void issueInBody(TestInfo testInfo) throws IOException {
234         try (var credentials = new HostCredentials(testInfo);
235              var tempFolder = new TemporaryDirectory()) {
236             var author = credentials.getHostedRepository();
237             var integrator = credentials.getHostedRepository();
238             var issues = credentials.getIssueProject();
239 
240             var censusBuilder = credentials.getCensusBuilder()
241                                            .addAuthor(author.forge().currentUser().id());
242             var prBot = new PullRequestBot(integrator, censusBuilder.build(), "master",
243                                            Map.of(), Map.of(), Map.of(), Set.of(), Map.of(), issues);
244 
245             // Populate the projects repository
246             var localRepo = CheckableRepository.init(tempFolder.path(), author.repositoryType());
247             var masterHash = localRepo.resolve("master").orElseThrow();
248             assertFalse(CheckableRepository.hasBeenEdited(localRepo));
249             localRepo.push(masterHash, author.url(), "master", true);
250 
251             // Make a change with a corresponding PR
252             var editHash = CheckableRepository.appendAndCommit(localRepo);
253             localRepo.push(editHash, author.url(), "edit", true);
254             var issue1 = issues.createIssue("First", List.of("Hello"));
255             var pr = credentials.createPullRequest(author, "master", "edit",
256                                                    issue1.id() + ": This is a pull request");
257 
258             // First check
259             TestBotRunner.runPeriodicItems(prBot);
260             assertTrue(pr.body().contains(issue1.id()));
261             assertTrue(pr.body().contains("First"));
262             assertTrue(pr.body().contains("## Issue\n"));
263 
264             // Add an extra issue
265             var issue2 = issues.createIssue("Second", List.of("There"));
266             pr.addComment("/solves " + issue2.id() + ": Description");
267 
268             // Check that the body was updated
269             TestBotRunner.runPeriodicItems(prBot);
270             TestBotRunner.runPeriodicItems(prBot);
271             assertTrue(pr.body().contains(issue1.id()));
272             assertTrue(pr.body().contains("First"));
273             assertTrue(pr.body().contains(issue2.id()));
274             assertTrue(pr.body().contains("Second"));
275             assertFalse(pr.body().contains("## Issue\n"));
276             assertTrue(pr.body().contains("## Issues\n"));
277         }
278     }
279 }