22
33import static io .quarkiverse .githubapp .testing .GitHubAppTesting .given ;
44import static org .mockito .ArgumentMatchers .any ;
5+ import static org .mockito .ArgumentMatchers .contains ;
56import static org .mockito .ArgumentMatchers .eq ;
67import static org .mockito .Mockito .mock ;
78import static org .mockito .Mockito .times ;
1011import static org .mockito .Mockito .when ;
1112
1213import java .io .IOException ;
14+ import java .util .List ;
1315
1416import org .junit .jupiter .api .Test ;
1517import org .junit .jupiter .api .extension .ExtendWith ;
1921import org .kohsuke .github .GHCommit ;
2022import org .kohsuke .github .GHCommitPointer ;
2123import org .kohsuke .github .GHEvent ;
24+ import org .kohsuke .github .GHIssueComment ;
2225import org .kohsuke .github .GHPullRequest ;
2326import org .kohsuke .github .GHPullRequestCommitDetail ;
2427import org .kohsuke .github .GHRepository ;
@@ -80,6 +83,7 @@ void pullRequestHasTwoCheckRuns() throws IOException {
8083
8184 mockPR = mocks .pullRequest (samplePullRequestId );
8285 when (mockPR .getRepository ()).thenReturn (mockRepo );
86+ when (mockPR .getComments ()).thenReturn (List .of ());
8387
8488 setupMockHeadCommit ();
8589
@@ -101,6 +105,8 @@ void pullRequestHasTwoCheckRuns() throws IOException {
101105 verify (mockCheckRunBuilder , times (2 )).withConclusion (eq (GHCheckRun .Conclusion .SUCCESS ));
102106 verify (mockCheckRunBuilder , times (2 )).create ();
103107
108+ verify (mockPR , times (1 )).getComments ();
109+
104110 verifyNoMoreInteractions (mocks .ghObjects ());
105111 });
106112 }
@@ -119,6 +125,7 @@ void pullRequestHasOneCheckRunSucessAndOneCheckRunFailIfMergeCommit() throws IOE
119125
120126 mockPR = mocks .pullRequest (samplePullRequestId );
121127 when (mockPR .getRepository ()).thenReturn (mockRepo );
128+ when (mockPR .getComments ()).thenReturn (List .of ());
122129
123130 setupMockHeadCommit ();
124131
@@ -143,10 +150,41 @@ void pullRequestHasOneCheckRunSucessAndOneCheckRunFailIfMergeCommit() throws IOE
143150 verify (mockCheckRunBuilder , times (1 )).add (any (Output .class ));
144151 verify (mockCheckRunBuilder , times (2 )).create ();
145152
153+ verify (mockPR , times (1 )).getComments ();
154+ verify (mockPR , times (1 )).comment (contains ("squash merged" ));
155+
146156 verifyNoMoreInteractions (mocks .ghObjects ());
147157 });
148158 }
149159
160+ /**
161+ * If PR contains merge commits and no existing bot comment,
162+ * a comment advising squash merge is posted.
163+ */
164+ @ Test
165+ void pullRequestWithMergeCommitPostsSquashMergeComment () throws IOException {
166+
167+ setupMock ();
168+
169+ given ().github (mocks -> {
170+ mocks .configFile ("quarkus-github-bot.yml" ).fromString ("features: [ CHECK_CONTRIBUTION_RULES ]\n " );
171+
172+ mockPR = mocks .pullRequest (samplePullRequestId );
173+ when (mockPR .getRepository ()).thenReturn (mockRepo );
174+ when (mockPR .getComments ()).thenReturn (List .of ());
175+
176+ setupMockHeadCommit ();
177+
178+ GHPullRequestCommitDetail mockMergeCommitDetail = setupMockMergeCommit ();
179+ PagedIterable <GHPullRequestCommitDetail > iterableMock = MockHelper .mockPagedIterable (mockMergeCommitDetail );
180+ when (mockPR .listCommits ()).thenReturn (iterableMock );
181+ })
182+ .when ().payloadFromString (getSamplePullRequestPayload ())
183+ .event (GHEvent .PULL_REQUEST )
184+ .then ().github (mocks -> verify (mockPR , times (1 ))
185+ .comment (contains ("squash merged" )));
186+ }
187+
150188 /**
151189 * If PR contain 1 fixup commit and not any merge commit,
152190 * then 2 checks run are created in PR: 1 in FAILURE and 1 in SUCESS
@@ -161,6 +199,7 @@ void pullRequestHasOneCheckRunSucessAndOneCheckRunFailIfFixupCommit() throws IOE
161199
162200 mockPR = mocks .pullRequest (samplePullRequestId );
163201 when (mockPR .getRepository ()).thenReturn (mockRepo );
202+ when (mockPR .getComments ()).thenReturn (List .of ());
164203
165204 setupMockHeadCommit ();
166205
@@ -185,10 +224,75 @@ void pullRequestHasOneCheckRunSucessAndOneCheckRunFailIfFixupCommit() throws IOE
185224 verify (mockCheckRunBuilder , times (1 )).add (any (Output .class ));
186225 verify (mockCheckRunBuilder , times (2 )).create ();
187226
227+ verify (mockPR , times (1 )).getComments ();
228+
188229 verifyNoMoreInteractions (mocks .ghObjects ());
189230 });
190231 }
191232
233+ /**
234+ * If PR no longer contains merge commits but a bot comment exists,
235+ * the comment is deleted.
236+ */
237+ @ Test
238+ void pullRequestWithoutMergeCommitDeletesExistingComment () throws IOException {
239+
240+ setupMock ();
241+
242+ GHIssueComment mockComment = mock (GHIssueComment .class );
243+ when (mockComment .getBody ()).thenReturn (
244+ "old comment" + io .quarkus .bot .util .Strings .MERGE_COMMIT_COMMENT_MARKER );
245+
246+ given ().github (mocks -> {
247+ mocks .configFile ("quarkus-github-bot.yml" ).fromString ("features: [ CHECK_CONTRIBUTION_RULES ]\n " );
248+
249+ mockPR = mocks .pullRequest (samplePullRequestId );
250+ when (mockPR .getRepository ()).thenReturn (mockRepo );
251+ when (mockPR .getComments ()).thenReturn (List .of (mockComment ));
252+
253+ setupMockHeadCommit ();
254+
255+ PagedIterable <GHPullRequestCommitDetail > iterableMock = MockHelper .mockPagedIterable ();
256+ when (mockPR .listCommits ()).thenReturn (iterableMock );
257+ })
258+ .when ().payloadFromString (getSamplePullRequestPayload ())
259+ .event (GHEvent .PULL_REQUEST )
260+ .then ().github (mocks -> verify (mockComment , times (1 ))
261+ .delete ());
262+ }
263+
264+ /**
265+ * If PR contains merge commits and a bot comment already exists,
266+ * no new comment is posted (idempotent).
267+ */
268+ @ Test
269+ void pullRequestWithMergeCommitDoesNotDuplicateComment () throws IOException {
270+
271+ setupMock ();
272+
273+ GHIssueComment mockComment = mock (GHIssueComment .class );
274+ when (mockComment .getBody ()).thenReturn (
275+ "existing comment" + io .quarkus .bot .util .Strings .MERGE_COMMIT_COMMENT_MARKER );
276+
277+ given ().github (mocks -> {
278+ mocks .configFile ("quarkus-github-bot.yml" ).fromString ("features: [ CHECK_CONTRIBUTION_RULES ]\n " );
279+
280+ mockPR = mocks .pullRequest (samplePullRequestId );
281+ when (mockPR .getRepository ()).thenReturn (mockRepo );
282+ when (mockPR .getComments ()).thenReturn (List .of (mockComment ));
283+
284+ setupMockHeadCommit ();
285+
286+ GHPullRequestCommitDetail mockMergeCommitDetail = setupMockMergeCommit ();
287+ PagedIterable <GHPullRequestCommitDetail > iterableMock = MockHelper .mockPagedIterable (mockMergeCommitDetail );
288+ when (mockPR .listCommits ()).thenReturn (iterableMock );
289+ })
290+ .when ().payloadFromString (getSamplePullRequestPayload ())
291+ .event (GHEvent .PULL_REQUEST )
292+ .then ().github (mocks -> verify (mockPR , times (0 ))
293+ .comment (any (String .class )));
294+ }
295+
192296 private static long samplePullRequestId = 1091703530 ;
193297
194298 private static String sampleHeadCommitSha = "7277231f08d6641edbdc07ee327dca1cc11e754d" ;
0 commit comments