diff --git a/pom.xml b/pom.xml index a1e32865..4710b5b3 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 github-client - 0.4.1-SNAPSHOT + 0.3.7-SNAPSHOT com.spotify @@ -221,6 +221,12 @@ ${junit.version} test + + org.junit.jupiter + junit-jupiter-params + ${junit.version} + test + org.mockito diff --git a/src/main/java/com/spotify/github/jackson/CommentReactionContentDeserializer.java b/src/main/java/com/spotify/github/jackson/CommentReactionContentDeserializer.java new file mode 100644 index 00000000..abbaeb73 --- /dev/null +++ b/src/main/java/com/spotify/github/jackson/CommentReactionContentDeserializer.java @@ -0,0 +1,43 @@ +/*- + * -\-\- + * github-api + * -- + * Copyright (C) 2016 - 2020 Spotify AB + * -- + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * -/-/- + */ +package com.spotify.github.jackson; + +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.spotify.github.v3.comment.CommentReactionContent; + +import java.io.IOException; +/** + * Custom deserializer for {@link CommentReactionContent}. + */ +public class CommentReactionContentDeserializer extends JsonDeserializer { + @Override + public CommentReactionContent deserialize(final JsonParser p, final DeserializationContext ctxt) + throws IOException { + String value = p.getText(); + for (CommentReactionContent content : CommentReactionContent.values()) { + if (content.toString().equals(value)) { + return content; + } + } + return null; + } +} diff --git a/src/main/java/com/spotify/github/jackson/CommentReactionContentSerializer.java b/src/main/java/com/spotify/github/jackson/CommentReactionContentSerializer.java new file mode 100644 index 00000000..c476dc49 --- /dev/null +++ b/src/main/java/com/spotify/github/jackson/CommentReactionContentSerializer.java @@ -0,0 +1,35 @@ +/*- + * -\-\- + * github-api + * -- + * Copyright (C) 2016 - 2020 Spotify AB + * -- + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * -/-/- + */ +package com.spotify.github.jackson; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.spotify.github.v3.comment.CommentReactionContent; +import java.io.IOException; +/** + * Custom serializer for {@link CommentReactionContent}. + */ +public class CommentReactionContentSerializer extends JsonSerializer { + @Override + public void serialize(final CommentReactionContent value, final JsonGenerator gen, final SerializerProvider serializers) throws IOException { + gen.writeString(value.toString()); + } +} diff --git a/src/main/java/com/spotify/github/v3/clients/GitHubClient.java b/src/main/java/com/spotify/github/v3/clients/GitHubClient.java index 803bfe51..c6884b27 100644 --- a/src/main/java/com/spotify/github/v3/clients/GitHubClient.java +++ b/src/main/java/com/spotify/github/v3/clients/GitHubClient.java @@ -32,6 +32,7 @@ import com.spotify.github.v3.checks.AccessToken; import com.spotify.github.v3.checks.Installation; import com.spotify.github.v3.comment.Comment; +import com.spotify.github.v3.comment.CommentReaction; import com.spotify.github.v3.exceptions.ReadOnlyRepositoryException; import com.spotify.github.v3.exceptions.RequestNotOkException; import com.spotify.github.v3.git.Reference; @@ -85,6 +86,8 @@ public class GitHubClient { }; static final TypeReference> LIST_COMMENT_TYPE_REFERENCE = new TypeReference<>() {}; + static final TypeReference> LIST_COMMENT_REACTION_TYPE_REFERENCE = + new TypeReference<>() {}; static final TypeReference> LIST_REPOSITORY = new TypeReference<>() {}; static final TypeReference> LIST_COMMIT_TYPE_REFERENCE = diff --git a/src/main/java/com/spotify/github/v3/clients/IssueClient.java b/src/main/java/com/spotify/github/v3/clients/IssueClient.java index 2ba32318..9d8df594 100644 --- a/src/main/java/com/spotify/github/v3/clients/IssueClient.java +++ b/src/main/java/com/spotify/github/v3/clients/IssueClient.java @@ -7,9 +7,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -20,16 +20,18 @@ package com.spotify.github.v3.clients; -import static com.spotify.github.v3.clients.GitHubClient.IGNORE_RESPONSE_CONSUMER; -import static com.spotify.github.v3.clients.GitHubClient.LIST_COMMENT_TYPE_REFERENCE; +import static com.spotify.github.v3.clients.GitHubClient.*; import com.google.common.collect.ImmutableMap; import com.spotify.github.async.AsyncPage; import com.spotify.github.v3.comment.Comment; +import com.spotify.github.v3.comment.CommentReaction; +import com.spotify.github.v3.comment.CommentReactionContent; import com.spotify.github.v3.issues.Issue; import java.lang.invoke.MethodHandles; import java.util.Iterator; import java.util.concurrent.CompletableFuture; +import okhttp3.Response; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -39,6 +41,8 @@ public class IssueClient { static final String COMMENTS_URI_NUMBER_TEMPLATE = "/repos/%s/%s/issues/%s/comments"; static final String COMMENTS_URI_TEMPLATE = "/repos/%s/%s/issues/comments"; static final String COMMENTS_URI_ID_TEMPLATE = "/repos/%s/%s/issues/comments/%s"; + static final String COMMENTS_REACTION_TEMPLATE = "/repos/%s/%s/issues/comments/%s/reactions"; + static final String COMMENTS_REACTION_ID_TEMPLATE = "/repos/%s/%s/issues/%s/reactions/%s"; static final String ISSUES_URI_ID_TEMPLATE = "/repos/%s/%s/issues/%s"; private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); @@ -128,13 +132,59 @@ private Iterator> listComments(final String path) { return new GithubPageIterator<>(new GithubPage<>(github, path, LIST_COMMENT_TYPE_REFERENCE)); } - /*** + /** * Get issue by id * - * @param id + * @param id issue id * @return the Issue for the given id if exists. */ public CompletableFuture getIssue(final int id) { return github.request(String.format(ISSUES_URI_ID_TEMPLATE, owner, repo, id), Issue.class); } + + /** + * Create a reaction on a comment. See Create + * reaction for an issue comment + * + * @param commentId comment id + * @param reaction reaction content + * @return the Comment that was just created + */ + public CompletableFuture createCommentReaction( + final long commentId, final CommentReactionContent reaction) { + final String path = String.format(COMMENTS_REACTION_TEMPLATE, owner, repo, commentId); + final String requestBody = + github.json().toJsonUnchecked(ImmutableMap.of("content", reaction.toString())); + return github.post(path, requestBody, CommentReaction.class); + } + + /** + * Delete a reaction on a comment. See List + * reactions for an issue comment + * + * @param issueNumber issue number + * @param reactionId reaction id + */ + public CompletableFuture deleteCommentReaction( + final long issueNumber, final long reactionId) { + final String path = + String.format(COMMENTS_REACTION_ID_TEMPLATE, owner, repo, issueNumber, reactionId); + return github.delete(path); + } + + /** + * List reactions on a comment. See List + * reactions for an issue comment + * + * @param commentId comment id + * @return reactions + */ + public GithubPageIterator listCommentReaction(final long commentId) { + final String path = String.format(COMMENTS_REACTION_TEMPLATE, owner, repo, commentId); + return new GithubPageIterator<>( + new GithubPage<>(github, path, LIST_COMMENT_REACTION_TYPE_REFERENCE)); + } } diff --git a/src/main/java/com/spotify/github/v3/comment/CommentReaction.java b/src/main/java/com/spotify/github/v3/comment/CommentReaction.java new file mode 100644 index 00000000..705d0dd2 --- /dev/null +++ b/src/main/java/com/spotify/github/v3/comment/CommentReaction.java @@ -0,0 +1,50 @@ +/*- + * -\-\- + * github-api + * -- + * Copyright (C) 2016 - 2020 Spotify AB + * -- + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * -/-/- + */ +package com.spotify.github.v3.comment; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.spotify.github.GithubStyle; +import com.spotify.github.UpdateTracking; +import com.spotify.github.v3.User; +import org.immutables.value.Value; + +/** + * Comment reaction object. + * + *

See About + * GitHub Issue Comment reactions + */ +@Value.Immutable +@GithubStyle +@JsonSerialize(as = ImmutableCommentReaction.class) +@JsonDeserialize(as = ImmutableCommentReaction.class) +public interface CommentReaction extends UpdateTracking { + + /** Reaction ID. */ + long id(); + + /** Reaction user. */ + User user(); + + /** Reaction content. */ + CommentReactionContent content(); +} diff --git a/src/main/java/com/spotify/github/v3/comment/CommentReactionContent.java b/src/main/java/com/spotify/github/v3/comment/CommentReactionContent.java new file mode 100644 index 00000000..cc4f8727 --- /dev/null +++ b/src/main/java/com/spotify/github/v3/comment/CommentReactionContent.java @@ -0,0 +1,56 @@ +/*- + * -\-\- + * github-api + * -- + * Copyright (C) 2016 - 2020 Spotify AB + * -- + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * -/-/- + */ +package com.spotify.github.v3.comment; + +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.databind.annotation.JsonSerialize; +import com.spotify.github.jackson.CommentReactionContentDeserializer; +import com.spotify.github.jackson.CommentReactionContentSerializer; + +/** + * Comment reaction content. + * + *

See About + * GitHub Issue Comment reactions + */ +@JsonDeserialize(using = CommentReactionContentDeserializer.class) +@JsonSerialize(using = CommentReactionContentSerializer.class) +public enum CommentReactionContent { + THUMBS_UP("+1"), // 👍 + THUMBS_DOWN("-1"), // 👎 + LAUGH("laugh"), // 😄 + HOORAY("hooray"), // 🎉 + CONFUSED("confused"), // 😕 + HEART("heart"), // ❤️ + ROCKET("rocket"), // 🚀 + EYES("eyes"); // 👀 + + private final String reaction; + + CommentReactionContent(final String reaction) { + this.reaction = reaction; + } + + @Override + public String toString() { + return reaction; + } +} diff --git a/src/test/java/com/spotify/github/v3/clients/IssueClientTest.java b/src/test/java/com/spotify/github/v3/clients/IssueClientTest.java index 473fe26f..b7e898d6 100644 --- a/src/test/java/com/spotify/github/v3/clients/IssueClientTest.java +++ b/src/test/java/com/spotify/github/v3/clients/IssueClientTest.java @@ -7,9 +7,9 @@ * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at - * + * * http://www.apache.org/licenses/LICENSE-2.0 - * + * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. @@ -22,8 +22,7 @@ import static com.google.common.io.Resources.getResource; import static com.spotify.github.FixtureHelper.loadFixture; -import static com.spotify.github.v3.clients.IssueClient.COMMENTS_URI_NUMBER_TEMPLATE; -import static com.spotify.github.v3.clients.IssueClient.ISSUES_URI_ID_TEMPLATE; +import static com.spotify.github.v3.clients.IssueClient.*; import static com.spotify.github.v3.clients.MockHelper.createMockResponse; import static java.lang.String.format; import static java.nio.charset.Charset.defaultCharset; @@ -37,15 +36,19 @@ import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; +import static org.mockito.Mockito.*; +import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import com.google.common.io.Resources; import com.spotify.github.async.Async; import com.spotify.github.async.AsyncPage; import com.spotify.github.jackson.Json; +import com.spotify.github.v3.ImmutableUser; import com.spotify.github.v3.comment.Comment; +import com.spotify.github.v3.comment.CommentReaction; +import com.spotify.github.v3.comment.CommentReactionContent; +import com.spotify.github.v3.comment.ImmutableCommentReaction; import com.spotify.github.v3.exceptions.RequestNotOkException; import com.spotify.github.v3.issues.Issue; import java.io.IOException; @@ -56,6 +59,8 @@ import okhttp3.Response; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; public class IssueClientTest { @@ -63,7 +68,6 @@ public class IssueClientTest { private IssueClient issueClient; private Json json; - @BeforeEach public void setUp() { json = Json.create(); @@ -94,7 +98,8 @@ public void testCommentPaginationSpliterator() throws IOException { .thenReturn(completedFuture(lastPageResponse)); final Iterable> pageIterator = () -> issueClient.listComments(123); - final List listComments = Async.streamFromPaginatingIterable(pageIterator).collect(toList()); + final List listComments = + Async.streamFromPaginatingIterable(pageIterator).collect(toList()); assertThat(listComments.size(), is(30)); assertThat(listComments.get(0).id(), is(1345268)); @@ -161,10 +166,92 @@ public void testGetIssue() throws IOException { assertThat(issue.labels().get(0).name(), is("bug")); } + @ParameterizedTest + @EnumSource(CommentReactionContent.class) + public void testCreateIssueCommentReaction(CommentReactionContent reaction) { + long commentId = 22369886; + final CompletableFuture reactionResponse = + completedFuture( + ImmutableCommentReaction.builder() + .id(42L) + .content(reaction) + .user(ImmutableUser.builder().login("octocat").build()) + .build()); + final String path = format(COMMENTS_REACTION_TEMPLATE, "someowner", "somerepo", commentId); + final String requestBody = + github.json().toJsonUnchecked(ImmutableMap.of("content", reaction.toString())); + when(github.post(eq(path), eq(requestBody), eq(CommentReaction.class))) + .thenReturn(reactionResponse); + + final var commentReaction = issueClient.createCommentReaction(commentId, reaction).join(); + + assertThat(commentReaction.id(), is(42L)); + assertNotNull(commentReaction.user()); + assertThat(commentReaction.user().login(), is("octocat")); + assertThat(commentReaction.content().toString(), is(reaction.toString())); + verify(github, times(1)).post(eq(path), eq(requestBody), eq(CommentReaction.class)); + } + + @Test + public void testDeleteIssueCommentReaction() { + long issueNumber = 42; + long reactionId = 385825; + final String path = + format(COMMENTS_REACTION_ID_TEMPLATE, "someowner", "somerepo", issueNumber, reactionId); + Response mockResponse = mock(Response.class); + when(mockResponse.code()).thenReturn(204); + when(github.delete(eq(path))).thenReturn(completedFuture(mockResponse)); + + final var response = issueClient.deleteCommentReaction(issueNumber, reactionId).join(); + + assertThat(response.code(), is(204)); + assertThat(response, is(mockResponse)); + verify(github, times(1)).delete(eq(path)); + } + + @Test + public void testListIssueCommentReaction() throws IOException { + long commentId = 22369886; + final CompletableFuture> listResponse = + completedFuture( + List.of( + (ImmutableCommentReaction.builder() + .id(42L) + .content(CommentReactionContent.HEART) + .user(ImmutableUser.builder().login("octocat").build()) + .build()))); + final String path = format(COMMENTS_REACTION_TEMPLATE, "someowner", "somerepo", commentId); + + final String firstPageLink = + format( + "; rel=\"last\"", + commentId); + final String firstPageBody = github.json().toJsonUnchecked(listResponse.join().toArray()); + final Response firstPageResponse = createMockResponse(firstPageLink, firstPageBody); + + when(github.request(eq(path))).thenReturn(completedFuture(firstPageResponse)); + final List listCommentReactions = Lists.newArrayList(); + issueClient + .listCommentReaction(commentId) + .forEachRemaining( + page -> { + page.iterator().forEachRemaining(listCommentReactions::add); + }); + + assertThat(listCommentReactions.size(), is(1)); + assertNotNull(listCommentReactions.get(0)); + assertThat(listCommentReactions.get(0).user().login(), is("octocat")); + assertThat( + listCommentReactions.get(0).content().toString(), + is(CommentReactionContent.HEART.toString())); + verify(github, atLeastOnce()).request(eq(path)); + } + @Test public void testGetIssueNoIssue() { final String path = format(ISSUES_URI_ID_TEMPLATE, "someowner", "somerepo", 2); - when(github.request(eq(path), eq(Issue.class))).thenReturn(failedFuture(new RequestNotOkException("", "", 404, "", new HashMap<>()))); + when(github.request(eq(path), eq(Issue.class))) + .thenReturn(failedFuture(new RequestNotOkException("", "", 404, "", new HashMap<>()))); assertThrows(CompletionException.class, () -> issueClient.getIssue(2).join()); } diff --git a/src/test/java/com/spotify/github/v3/comment/CommentReactionContentTest.java b/src/test/java/com/spotify/github/v3/comment/CommentReactionContentTest.java new file mode 100644 index 00000000..cade32ba --- /dev/null +++ b/src/test/java/com/spotify/github/v3/comment/CommentReactionContentTest.java @@ -0,0 +1,40 @@ +/*- + * -\-\- + * github-api + * -- + * Copyright (C) 2016 - 2020 Spotify AB + * -- + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * -/-/- + */ +package com.spotify.github.v3.comment; + +import static org.junit.jupiter.api.Assertions.*; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +class CommentReactionContentTest { + @ParameterizedTest + @EnumSource(CommentReactionContent.class) + public void testDeserialize(CommentReactionContent reaction) throws Exception { + ObjectMapper mapper = new ObjectMapper(); + + String json = "\"" + reaction.toString() + "\""; + + CommentReactionContent content = mapper.readValue(json, CommentReactionContent.class); + + assertEquals(reaction, content); + } +}