From bc3cd1a10dbc20adcfdf6763689d108a64d7f2a9 Mon Sep 17 00:00:00 2001 From: Ben Fradet Date: Fri, 15 Jun 2018 23:10:10 +0200 Subject: [PATCH] Add support for the remove label endpoint --- docs/src/main/tut/issue.md | 22 +++++++++++ .../test/scala/github4s/unit/ApiSpec.scala | 39 +++++++++++++++++++ .../github4s/utils/MockGithubApiServer.scala | 26 +++++++++++++ .../src/main/scala/github4s/GithubAPIs.scala | 8 ++++ .../src/main/scala/github4s/HttpClient.scala | 10 +++++ .../src/main/scala/github4s/api/Issues.scala | 23 +++++++++++ .../github4s/free/algebra/IssuesOps.scala | 17 ++++++++ .../free/interpreters/Interpreters.scala | 2 + .../github4s/integration/GHIssuesSpec.scala | 11 ++++++ .../scala/github4s/unit/GHIssuesSpec.scala | 13 +++++++ .../test/scala/github4s/unit/IssuesSpec.scala | 21 ++++++++++ .../test/scala/github4s/utils/BaseSpec.scala | 14 +++++++ 12 files changed, 206 insertions(+) diff --git a/docs/src/main/tut/issue.md b/docs/src/main/tut/issue.md index 6cc63fd5c..6f5729c9c 100644 --- a/docs/src/main/tut/issue.md +++ b/docs/src/main/tut/issue.md @@ -300,6 +300,28 @@ The `result` on the right is the corresponding assigned [List[Label]][issue-scal See [the API doc](https://developer.github.com/v3/issues/labels/#add-labels-to-an-issue) for full reference. +### Remove label + +You can remove a label from an issue with the following parameters: + + - the repository coordinates (`owner` and `name` of the repository). + - `number`: The issue number. + - `label`: The label that requires removing. + + To remove an existing label from an issue: + +```tut:silent +val removedLabelList = Github(accessToken).issues.removeLabel("47deg", "github4s", 123, "bug") +removedLabelList.exec[cats.Id, HttpResponse[String]]() match { + case Left(e) => println(s"Something went wrong: ${e.getMessage}") + case Right(r) => println(r.result) +} +``` + +The `result` on the right is the corresponding removed [List[Label]][issue-scala] + +See [the API doc](https://developer.github.com/v3/issues/labels/#remove-a-label-from-an-issue) for full reference. + As you can see, a few features of the issue endpoint are missing. As a result, if you'd like to see a feature supported, feel free to create an issue and/or a pull request! diff --git a/github4s/jvm/src/test/scala/github4s/unit/ApiSpec.scala b/github4s/jvm/src/test/scala/github4s/unit/ApiSpec.scala index 067d8f83f..56628c7a9 100644 --- a/github4s/jvm/src/test/scala/github4s/unit/ApiSpec.scala +++ b/github4s/jvm/src/test/scala/github4s/unit/ApiSpec.scala @@ -1093,6 +1093,45 @@ class ApiSpec response should be('left) } + "Issues >> RemoveLabel" should "return the removed issue labels when a valid issue number is provided" in { + val response = + issues.removeLabel( + accessToken, + headerUserAgent, + validRepoOwner, + validRepoName, + validIssueNumber, + validIssueLabel.head) + response should be('right) + + response.toOption map { r ⇒ + r.result.nonEmpty shouldBe true + r.statusCode shouldBe okStatusCode + } + } + it should "return an error if an invalid issue number is provided" in { + val response = + issues.removeLabel( + accessToken, + headerUserAgent, + validRepoOwner, + validRepoName, + invalidIssueNumber, + validIssueLabel.head) + response should be('left) + } + it should "return an error if no tokens are provided" in { + val response = + issues.removeLabel( + None, + headerUserAgent, + validRepoOwner, + validRepoName, + validIssueNumber, + validIssueLabel.head) + response should be('left) + } + "Issues >> AddLabels" should "return the assigned issue labels when a valid issue number is provided" in { val response = issues.addLabels( diff --git a/github4s/jvm/src/test/scala/github4s/utils/MockGithubApiServer.scala b/github4s/jvm/src/test/scala/github4s/utils/MockGithubApiServer.scala index c59fa62e7..188c879a6 100644 --- a/github4s/jvm/src/test/scala/github4s/utils/MockGithubApiServer.scala +++ b/github4s/jvm/src/test/scala/github4s/utils/MockGithubApiServer.scala @@ -885,6 +885,32 @@ trait MockGithubApiServer extends MockServerService with FakeResponses with Test .withHeader(not("Authorization"))) .respond(response.withStatusCode(notFoundStatusCode).withBody(notFoundResponse)) + //Issues >> Remove label from an Issue + mockServer + .when(request + .withMethod("DELETE") + .withPath( + s"/repos/$validRepoOwner/$validRepoName/issues/$validIssueNumber/labels/${validIssueLabel.head}") + .withHeader("Authorization", tokenHeader)) + .respond(response.withStatusCode(okStatusCode).withBody(listLabelsValidResponse)) + + mockServer + .when( + request + .withMethod("DELETE") + .withPath( + s"/repos/$validRepoOwner/$validRepoName/issues/$invalidIssueNumber/labels/${validIssueLabel.head}") + .withHeader("Authorization", tokenHeader)) + .respond(response.withStatusCode(notFoundStatusCode).withBody(notFoundResponse)) + + mockServer + .when(request + .withMethod("DELETE") + .withPath( + s"/repos/$validRepoOwner/$validRepoName/issues/$validIssueNumber/labels/${validIssueLabel.head}") + .withHeader(not("Authorization"))) + .respond(response.withStatusCode(notFoundStatusCode).withBody(notFoundResponse)) + //Issues >> List labels for an Issue mockServer .when( diff --git a/github4s/shared/src/main/scala/github4s/GithubAPIs.scala b/github4s/shared/src/main/scala/github4s/GithubAPIs.scala index 0fcd51fd9..2a99cb1f1 100644 --- a/github4s/shared/src/main/scala/github4s/GithubAPIs.scala +++ b/github4s/shared/src/main/scala/github4s/GithubAPIs.scala @@ -246,6 +246,14 @@ class GHIssues(accessToken: Option[String] = None)(implicit O: IssueOps[GitHub4s ): GHIO[GHResponse[List[Label]]] = O.addLabels(owner, repo, number, labels, accessToken) + def removeLabel( + owner: String, + repo: String, + number: Int, + label: String + ): GHIO[GHResponse[List[Label]]] = + O.removeLabel(owner, repo, number, label, accessToken) + } class GHActivities(accessToken: Option[String] = None)(implicit O: ActivityOps[GitHub4s]) { diff --git a/github4s/shared/src/main/scala/github4s/HttpClient.scala b/github4s/shared/src/main/scala/github4s/HttpClient.scala index 3657d06a3..4378ef29f 100644 --- a/github4s/shared/src/main/scala/github4s/HttpClient.scala +++ b/github4s/shared/src/main/scala/github4s/HttpClient.scala @@ -187,6 +187,16 @@ class HttpClient[C, M[_]]( httpRbImpl.runEmpty( httpRequestBuilder(buildURL(method)).deleteMethod.withHeaders(headers).withAuth(accessToken)) + def deleteWithResponse[A]( + accessToken: Option[String] = None, + url: String, + headers: Map[String, String] = Map.empty + )(implicit D: Decoder[A]): M[GHResponse[A]] = + httpRbImpl.run[A]( + httpRequestBuilder(buildURL(url)).deleteMethod + .withAuth(accessToken) + .withHeaders(headers)) + private def buildURL(method: String) = urls.baseUrl + method val defaultPage: Int = 1 diff --git a/github4s/shared/src/main/scala/github4s/api/Issues.scala b/github4s/shared/src/main/scala/github4s/api/Issues.scala index de5e4526b..828898c2c 100644 --- a/github4s/shared/src/main/scala/github4s/api/Issues.scala +++ b/github4s/shared/src/main/scala/github4s/api/Issues.scala @@ -299,4 +299,27 @@ class Issues[C, M[_]]( headers, labels.asJson.noSpaces) + /** + * Remove the specified label from an Issue + * + * @param accessToken to identify the authenticated user + * @param headers optional user headers to include in the request + * @param owner of the repo + * @param repo name of the repo + * @param number Issue number + * @param label the name of the label to remove from the issue + * @return a GHResponse with the list of labels removed from the Issue. + */ + def removeLabel( + accessToken: Option[String] = None, + headers: Map[String, String] = Map(), + owner: String, + repo: String, + number: Int, + label: String): M[GHResponse[List[Label]]] = + httpClient.deleteWithResponse[List[Label]]( + accessToken, + s"repos/$owner/$repo/issues/$number/labels/$label", + headers) + } diff --git a/github4s/shared/src/main/scala/github4s/free/algebra/IssuesOps.scala b/github4s/shared/src/main/scala/github4s/free/algebra/IssuesOps.scala index 0ec76799c..2de5220a8 100644 --- a/github4s/shared/src/main/scala/github4s/free/algebra/IssuesOps.scala +++ b/github4s/shared/src/main/scala/github4s/free/algebra/IssuesOps.scala @@ -114,6 +114,14 @@ final case class AddLabels( accessToken: Option[String] = None ) extends IssueOp[GHResponse[List[Label]]] +final case class RemoveLabel( + owner: String, + repo: String, + number: Int, + label: String, + accessToken: Option[String] = None +) extends IssueOp[GHResponse[List[Label]]] + /** * Exposes Issue operations as a Free monadic algebra that may be combined with other Algebras via * Coproduct @@ -221,6 +229,15 @@ class IssueOps[F[_]](implicit I: InjectK[IssueOp, F]) { ): Free[F, GHResponse[List[Label]]] = Free.inject[IssueOp, F](AddLabels(owner, repo, number, labels, accessToken)) + def removeLabel( + owner: String, + repo: String, + number: Int, + label: String, + accessToken: Option[String] = None + ): Free[F, GHResponse[List[Label]]] = + Free.inject[IssueOp, F](RemoveLabel(owner, repo, number, label, accessToken)) + } /** diff --git a/github4s/shared/src/main/scala/github4s/free/interpreters/Interpreters.scala b/github4s/shared/src/main/scala/github4s/free/interpreters/Interpreters.scala index c2ffe69db..df5d615ce 100644 --- a/github4s/shared/src/main/scala/github4s/free/interpreters/Interpreters.scala +++ b/github4s/shared/src/main/scala/github4s/free/interpreters/Interpreters.scala @@ -229,6 +229,8 @@ class Interpreters[M[_], C]( issues.listLabels(accessToken, headers, owner, repo, number) case AddLabels(owner, repo, number, labels, accessToken) ⇒ issues.addLabels(accessToken, headers, owner, repo, number, labels) + case RemoveLabel(owner, repo, number, label, accessToken) ⇒ + issues.removeLabel(accessToken, headers, owner, repo, number, label) } } } diff --git a/github4s/shared/src/test/scala/github4s/integration/GHIssuesSpec.scala b/github4s/shared/src/test/scala/github4s/integration/GHIssuesSpec.scala index 759dbc494..69baf3aea 100644 --- a/github4s/shared/src/test/scala/github4s/integration/GHIssuesSpec.scala +++ b/github4s/shared/src/test/scala/github4s/integration/GHIssuesSpec.scala @@ -93,6 +93,17 @@ trait GHIssuesSpec[T] extends BaseIntegrationSpec[T] { }) } + "Issues >> RemoveLabel" should "return a list of removed labels" in { + val response = Github(accessToken).issues + .removeLabel(validRepoOwner, validRepoName, validIssueNumber, validIssueLabel.head) + .execFuture[T](headerUserAgent) + + testFutureIsRight[List[Label]](response, { r => + r.result.nonEmpty shouldBe true + r.statusCode shouldBe okStatusCode + }) + } + "Issues >> AddLabels" should "return a list of labels" in { val response = Github(accessToken).issues .addLabels(validRepoOwner, validRepoName, validIssueNumber, validIssueLabel) diff --git a/github4s/shared/src/test/scala/github4s/unit/GHIssuesSpec.scala b/github4s/shared/src/test/scala/github4s/unit/GHIssuesSpec.scala index 0d6f97fac..480b0ab51 100644 --- a/github4s/shared/src/test/scala/github4s/unit/GHIssuesSpec.scala +++ b/github4s/shared/src/test/scala/github4s/unit/GHIssuesSpec.scala @@ -202,4 +202,17 @@ class GHIssuesSpec extends BaseSpec { ghIssues.addLabels(validRepoOwner, validRepoName, validIssueNumber, validIssueLabel) } + "Issues.RemoveLabel" should "call to IssuesOps with the right parameters" in { + val response: Free[GitHub4s, GHResponse[List[Label]]] = + Free.pure(Right(GHResult(List(label), okStatusCode, Map.empty))) + + val commentOps = mock[IssueOpsTest] + (commentOps.removeLabel _) + .expects(validRepoOwner, validRepoName, validIssueNumber, validIssueLabel.head, sampleToken) + .returns(response) + + val ghIssues = new GHIssues(sampleToken)(commentOps) + ghIssues.removeLabel(validRepoOwner, validRepoName, validIssueNumber, validIssueLabel.head) + } + } diff --git a/github4s/shared/src/test/scala/github4s/unit/IssuesSpec.scala b/github4s/shared/src/test/scala/github4s/unit/IssuesSpec.scala index d144a048d..2aed9ace7 100644 --- a/github4s/shared/src/test/scala/github4s/unit/IssuesSpec.scala +++ b/github4s/shared/src/test/scala/github4s/unit/IssuesSpec.scala @@ -291,4 +291,25 @@ class IssuesSpec extends BaseSpec { validIssueNumber, validIssueLabel) } + + "Issues.RemoveLabel" should "call httpClient.delete with the right parameters" in { + val response: GHResponse[List[Label]] = + Right(GHResult(List(label), okStatusCode, Map.empty)) + + val httpClientMock = httpClientMockDeleteWithResponse[List[Label]]( + url = s"repos/$validRepoOwner/$validRepoName/issues/$validIssueNumber/labels/${validIssueLabel.head}", + response = response + ) + + val issues = new Issues[String, Id] { + override val httpClient: HttpClient[String, Id] = httpClientMock + } + issues.removeLabel( + sampleToken, + headerUserAgent, + validRepoOwner, + validRepoName, + validIssueNumber, + validIssueLabel.head) + } } diff --git a/github4s/shared/src/test/scala/github4s/utils/BaseSpec.scala b/github4s/shared/src/test/scala/github4s/utils/BaseSpec.scala index afc160120..45459bfac 100644 --- a/github4s/shared/src/test/scala/github4s/utils/BaseSpec.scala +++ b/github4s/shared/src/test/scala/github4s/utils/BaseSpec.scala @@ -134,6 +134,20 @@ trait BaseSpec extends FlatSpec with Matchers with TestData with IdInstances wit httpClientMock } + def httpClientMockDeleteWithResponse[T]( + url: String, + response: GHResponse[T]): HttpClient[String, Id] = { + val httpClientMock = mock[HttpClientTest] + (httpClientMock + .deleteWithResponse[T]( + _: Option[String], + _: String, + _: Map[String, String])(_: Decoder[T])) + .expects(sampleToken, url, headerUserAgent, *) + .returns(response) + httpClientMock + } + class GitDataOpsTest extends GitDataOps[GitHub4s] class PullRequestOpsTest extends PullRequestOps[GitHub4s] class RepositoryOpsTest extends RepositoryOps[GitHub4s]