Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add GHIssue#queryComments, with a since() filter #1524

Merged
merged 2 commits into from Sep 27, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
14 changes: 13 additions & 1 deletion src/main/java/org/kohsuke/github/GHIssue.java
Expand Up @@ -479,18 +479,30 @@ public List<GHIssueComment> getComments() throws IOException {
}

/**
* Obtains all the comments associated with this issue.
* Obtains all the comments associated with this issue, witout any filter.
*
* @return the paged iterable
* @throws IOException
* the io exception
* @see <a href="https://docs.github.com/en/rest/issues/comments#list-issue-comments">List issue comments</a>
* @see #queryComments() queryComments to apply filters.
*/
public PagedIterable<GHIssueComment> listComments() throws IOException {
return root().createRequest()
.withUrlPath(getIssuesApiRoute() + "/comments")
.toIterable(GHIssueComment[].class, item -> item.wrapUp(this));
}

/**
* Search comments on this issue by specifying filters through a builder pattern.
*
* @return the query builder
* @see <a href="https://docs.github.com/en/rest/issues/comments#list-issue-comments">List issue comments</a>
*/
public GHIssueCommentQueryBuilder queryComments() {
return new GHIssueCommentQueryBuilder(this);
}

@Preview(SQUIRREL_GIRL)
public GHReaction createReaction(ReactionContent content) throws IOException {
return root().createRequest()
Expand Down
61 changes: 61 additions & 0 deletions src/main/java/org/kohsuke/github/GHIssueCommentQueryBuilder.java
@@ -0,0 +1,61 @@
package org.kohsuke.github;

import java.util.Date;

/**
* Builds a query for listing comments on an issue.
* <p>
* Call various methods that set the filter criteria, then the {@link #list()} method to actually retrieve the comments.
*
* <pre>
* GHIssue issue = ...;
* for (GHIssueComment comment : issue.queryComments().since(x).list()) {
* ...
* }
* </pre>
*
* @author Yoann Rodiere
* @see GHIssue#queryComments() GHIssue#queryComments()
* @see <a href="https://docs.github.com/en/rest/issues/comments#list-issue-comments">List issue comments</a>
*/
public class GHIssueCommentQueryBuilder {
private final Requester req;
private final GHIssue issue;

GHIssueCommentQueryBuilder(GHIssue issue) {
this.issue = issue;
this.req = issue.root().createRequest().withUrlPath(issue.getIssuesApiRoute() + "/comments");
}

/**
* Only comments created/updated after this date will be returned.
*
* @param date
* the date
* @return the query builder
*/
public GHIssueCommentQueryBuilder since(Date date) {
req.with("since", GitHubClient.printDate(date));
return this;
}

/**
* Only comments created/updated after this timestamp will be returned.
*
* @param timestamp
* the timestamp
* @return the query builder
*/
public GHIssueCommentQueryBuilder since(long timestamp) {
return since(new Date(timestamp));
}

/**
* Lists up the comments with the criteria added so far.
*
* @return the paged iterable
*/
public PagedIterable<GHIssueComment> list() {
return req.toIterable(GHIssueComment[].class, item -> item.wrapUp(issue));
}
}
266 changes: 266 additions & 0 deletions src/test/java/org/kohsuke/github/GHIssueTest.java
@@ -0,0 +1,266 @@
package org.kohsuke.github;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import java.io.IOException;
import java.time.temporal.ChronoUnit;
import java.util.Collection;
import java.util.Date;
import java.util.List;

import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.containsInAnyOrder;
import static org.hamcrest.Matchers.containsString;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.hasProperty;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;

/**
* @author Kohsuke Kawaguchi
* @author Yoann Rodiere
*/
public class GHIssueTest extends AbstractGitHubWireMockTest {

@Before
@After
public void cleanUp() throws Exception {
// Cleanup is only needed when proxying
if (!mockGitHub.isUseProxy()) {
return;
}

for (GHIssue issue : getRepository(this.getNonRecordingGitHub()).getIssues(GHIssueState.OPEN)) {
issue.close();
}
}

@Test
public void createIssue() throws Exception {
String name = "createIssue";
GHRepository repo = getRepository();
GHIssue issue = repo.createIssue(name).body("## test").create();
assertThat(issue.getTitle(), equalTo(name));
}

@Test
public void issueComment() throws Exception {
String name = "createIssueComment";
GHIssue issue = getRepository().createIssue(name).body("## test").create();

List<GHIssueComment> comments;
comments = issue.listComments().toList();
assertThat(comments, hasSize(0));
comments = issue.queryComments().list().toList();
assertThat(comments, hasSize(0));

GHIssueComment firstComment = issue.comment("First comment");
Date firstCommentCreatedAt = firstComment.getCreatedAt();
Date firstCommentCreatedAtPlus1Second = Date
.from(firstComment.getCreatedAt().toInstant().plus(1, ChronoUnit.SECONDS));

comments = issue.listComments().toList();
assertThat(comments, hasSize(1));
assertThat(comments, contains(hasProperty("body", equalTo("First comment"))));

comments = issue.queryComments().list().toList();
assertThat(comments, hasSize(1));
assertThat(comments, contains(hasProperty("body", equalTo("First comment"))));

// Test "since"
comments = issue.queryComments().since(firstCommentCreatedAt).list().toList();
assertThat(comments, hasSize(1));
assertThat(comments, contains(hasProperty("body", equalTo("First comment"))));
comments = issue.queryComments().since(firstCommentCreatedAtPlus1Second).list().toList();
assertThat(comments, hasSize(0));

// "since" is only precise up to the second,
// so if we want to differentiate comments, we need to be completely sure they're created
// at least 1 second from each other.
// Waiting 2 seconds to avoid edge cases.
Thread.sleep(2000);

GHIssueComment secondComment = issue.comment("Second comment");
Date secondCommentCreatedAt = secondComment.getCreatedAt();
Date secondCommentCreatedAtPlus1Second = Date
.from(secondComment.getCreatedAt().toInstant().plus(1, ChronoUnit.SECONDS));
assertThat(
"There's an error in the setup of this test; please fix it."
+ " The second comment should be created at least one second after the first one.",
firstCommentCreatedAtPlus1Second.getTime() <= secondCommentCreatedAt.getTime());

comments = issue.listComments().toList();
assertThat(comments, hasSize(2));
assertThat(comments,
contains(hasProperty("body", equalTo("First comment")),
hasProperty("body", equalTo("Second comment"))));
comments = issue.queryComments().list().toList();
assertThat(comments, hasSize(2));
assertThat(comments,
contains(hasProperty("body", equalTo("First comment")),
hasProperty("body", equalTo("Second comment"))));

// Test "since"
comments = issue.queryComments().since(firstCommentCreatedAt).list().toList();
assertThat(comments, hasSize(2));
assertThat(comments,
contains(hasProperty("body", equalTo("First comment")),
hasProperty("body", equalTo("Second comment"))));
comments = issue.queryComments().since(firstCommentCreatedAtPlus1Second).list().toList();
assertThat(comments, hasSize(1));
assertThat(comments, contains(hasProperty("body", equalTo("Second comment"))));
comments = issue.queryComments().since(secondCommentCreatedAt).list().toList();
assertThat(comments, hasSize(1));
assertThat(comments, contains(hasProperty("body", equalTo("Second comment"))));
comments = issue.queryComments().since(secondCommentCreatedAtPlus1Second).list().toList();
assertThat(comments, hasSize(0));

// Test "since" with timestamp instead of Date
comments = issue.queryComments().since(secondCommentCreatedAt.getTime()).list().toList();
assertThat(comments, hasSize(1));
assertThat(comments, contains(hasProperty("body", equalTo("Second comment"))));
}

@Test
public void closeIssue() throws Exception {
String name = "closeIssue";
GHIssue issue = getRepository().createIssue(name).body("## test").create();
assertThat(issue.getTitle(), equalTo(name));
assertThat(getRepository().getIssue(issue.getNumber()).getState(), equalTo(GHIssueState.OPEN));
issue.close();
assertThat(getRepository().getIssue(issue.getNumber()).getState(), equalTo(GHIssueState.CLOSED));
}

@Test
// Requires push access to the test repo to pass
public void setLabels() throws Exception {
GHIssue issue = getRepository().createIssue("setLabels").body("## test").create();
String label = "setLabels_label_name";
issue.setLabels(label);

Collection<GHLabel> labels = getRepository().getIssue(issue.getNumber()).getLabels();
assertThat(labels.size(), equalTo(1));
GHLabel savedLabel = labels.iterator().next();
assertThat(savedLabel.getName(), equalTo(label));
assertThat(savedLabel.getId(), notNullValue());
assertThat(savedLabel.getNodeId(), notNullValue());
assertThat(savedLabel.isDefault(), is(false));
}

@Test
// Requires push access to the test repo to pass
public void addLabels() throws Exception {
GHIssue issue = getRepository().createIssue("addLabels").body("## test").create();
String addedLabel1 = "addLabels_label_name_1";
String addedLabel2 = "addLabels_label_name_2";
String addedLabel3 = "addLabels_label_name_3";

List<GHLabel> resultingLabels = issue.addLabels(addedLabel1);
assertThat(resultingLabels.size(), equalTo(1));
GHLabel ghLabel = resultingLabels.get(0);
assertThat(ghLabel.getName(), equalTo(addedLabel1));

int requestCount = mockGitHub.getRequestCount();
resultingLabels = issue.addLabels(addedLabel2, addedLabel3);
// multiple labels can be added with one api call
assertThat(mockGitHub.getRequestCount(), equalTo(requestCount + 1));

assertThat(resultingLabels.size(), equalTo(3));
assertThat(resultingLabels,
containsInAnyOrder(hasProperty("name", equalTo(addedLabel1)),
hasProperty("name", equalTo(addedLabel2)),
hasProperty("name", equalTo(addedLabel3))));

// Adding a label which is already present does not throw an error
resultingLabels = issue.addLabels(ghLabel);
assertThat(resultingLabels.size(), equalTo(3));
}

@Test
// Requires push access to the test repo to pass
public void addLabelsConcurrencyIssue() throws Exception {
String addedLabel1 = "addLabelsConcurrencyIssue_label_name_1";
String addedLabel2 = "addLabelsConcurrencyIssue_label_name_2";

GHIssue issue1 = getRepository().createIssue("addLabelsConcurrencyIssue").body("## test").create();
issue1.getLabels();

GHIssue issue2 = getRepository().getIssue(issue1.getNumber());
issue2.addLabels(addedLabel2);

Collection<GHLabel> labels = issue1.addLabels(addedLabel1);

assertThat(labels.size(), equalTo(2));
assertThat(labels,
containsInAnyOrder(hasProperty("name", equalTo(addedLabel1)),
hasProperty("name", equalTo(addedLabel2))));
}

@Test
// Requires push access to the test repo to pass
public void removeLabels() throws Exception {
GHIssue issue = getRepository().createIssue("removeLabels").body("## test").create();
String label1 = "removeLabels_label_name_1";
String label2 = "removeLabels_label_name_2";
String label3 = "removeLabels_label_name_3";
issue.setLabels(label1, label2, label3);

Collection<GHLabel> labels = getRepository().getIssue(issue.getNumber()).getLabels();
assertThat(labels.size(), equalTo(3));
GHLabel ghLabel3 = labels.stream().filter(label -> label3.equals(label.getName())).findFirst().get();

int requestCount = mockGitHub.getRequestCount();
List<GHLabel> resultingLabels = issue.removeLabels(label2, label3);
// each label deleted is a separate api call
assertThat(mockGitHub.getRequestCount(), equalTo(requestCount + 2));

assertThat(resultingLabels.size(), equalTo(1));
assertThat(resultingLabels.get(0).getName(), equalTo(label1));

// Removing some labels that are not present does not throw
// This is consistent with earlier behavior and with addLabels()
issue.removeLabels(ghLabel3);

// Calling removeLabel() on label that is not present will throw
try {
issue.removeLabel(label3);
fail("Expected GHFileNotFoundException");
} catch (GHFileNotFoundException e) {
assertThat(e.getMessage(), containsString("Label does not exist"));
}
}

@Test
// Requires push access to the test repo to pass
public void setAssignee() throws Exception {
GHIssue issue = getRepository().createIssue("setAssignee").body("## test").create();
GHMyself user = gitHub.getMyself();
issue.assignTo(user);

assertThat(getRepository().getIssue(issue.getNumber()).getAssignee(), equalTo(user));
}

@Test
public void getUserTest() throws IOException {
GHIssue issue = getRepository().createIssue("getUserTest").create();
GHIssue issueSingle = getRepository().getIssue(issue.getNumber());
assertThat(issueSingle.getUser().root(), notNullValue());

PagedIterable<GHIssue> ghIssues = getRepository().listIssues(GHIssueState.OPEN);
for (GHIssue otherIssue : ghIssues) {
assertThat(otherIssue.getUser().root(), notNullValue());
}
}

protected GHRepository getRepository() throws IOException {
return getRepository(gitHub);
}

private GHRepository getRepository(GitHub gitHub) throws IOException {
return gitHub.getOrganization(GITHUB_API_TEST_ORG).getRepository("GHIssueTest");
}

}